Browse Source
- Windows: through GNTP library - OSX: through Growl library for local server and GTNP for remote server - Unix/Linux: through GNTP library For Ubuntu there's support for NotifyOSD. Made Growl class texts translatable.pull/7/head
22 changed files with 986 additions and 95 deletions
@ -0,0 +1,443 @@ |
|||||
|
import re |
||||
|
import hashlib |
||||
|
import time |
||||
|
import platform |
||||
|
|
||||
|
__version__ = '0.4' |
||||
|
|
||||
|
class BaseError(Exception): |
||||
|
pass |
||||
|
|
||||
|
class ParseError(BaseError): |
||||
|
def gntp_error(self): |
||||
|
error = GNTPError(errorcode=500,errordesc='Error parsing the message') |
||||
|
return error.encode() |
||||
|
|
||||
|
class AuthError(BaseError): |
||||
|
def gntp_error(self): |
||||
|
error = GNTPError(errorcode=400,errordesc='Error with authorization') |
||||
|
return error.encode() |
||||
|
|
||||
|
class UnsupportedError(BaseError): |
||||
|
def gntp_error(self): |
||||
|
error = GNTPError(errorcode=500,errordesc='Currently unsupported by gntp.py') |
||||
|
return error.encode() |
||||
|
|
||||
|
class _GNTPBase(object): |
||||
|
info = { |
||||
|
'version':'1.0', |
||||
|
'messagetype':None, |
||||
|
'encryptionAlgorithmID':None |
||||
|
} |
||||
|
_requiredHeaders = [] |
||||
|
headers = {} |
||||
|
resources = {} |
||||
|
def add_origin_info(self): |
||||
|
self.add_header('Origin-Machine-Name',platform.node()) |
||||
|
self.add_header('Origin-Software-Name','gntp.py') |
||||
|
self.add_header('Origin-Software-Version',__version__) |
||||
|
self.add_header('Origin-Platform-Name',platform.system()) |
||||
|
self.add_header('Origin-Platform-Version',platform.platform()) |
||||
|
def __str__(self): |
||||
|
return self.encode() |
||||
|
def _parse_info(self,data): |
||||
|
''' |
||||
|
Parse the first line of a GNTP message to get security and other info values |
||||
|
@param data: GNTP Message |
||||
|
@return: GNTP Message information in a dictionary |
||||
|
''' |
||||
|
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>] |
||||
|
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)'+ |
||||
|
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?'+ |
||||
|
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n', data,re.IGNORECASE) |
||||
|
|
||||
|
if not match: |
||||
|
raise ParseError('ERROR_PARSING_INFO_LINE') |
||||
|
|
||||
|
info = match.groupdict() |
||||
|
if info['encryptionAlgorithmID'] == 'NONE': |
||||
|
info['encryptionAlgorithmID'] = None |
||||
|
|
||||
|
return info |
||||
|
def set_password(self,password,encryptAlgo='MD5'): |
||||
|
''' |
||||
|
Set a password for a GNTP Message |
||||
|
@param password: Null to clear password |
||||
|
@param encryptAlgo: Supports MD5,SHA1,SHA256,SHA512 |
||||
|
@todo: Support other hash functions |
||||
|
''' |
||||
|
hash = { |
||||
|
'MD5': hashlib.md5, |
||||
|
'SHA1': hashlib.sha1, |
||||
|
'SHA256': hashlib.sha256, |
||||
|
'SHA512': hashlib.sha512, |
||||
|
} |
||||
|
|
||||
|
self.password = password |
||||
|
self.encryptAlgo = encryptAlgo.upper() |
||||
|
if not password: |
||||
|
self.info['encryptionAlgorithmID'] = None |
||||
|
self.info['keyHashAlgorithm'] = None; |
||||
|
return |
||||
|
if not self.encryptAlgo in hash.keys(): |
||||
|
raise UnsupportedError('INVALID HASH "%s"'%self.encryptAlgo) |
||||
|
|
||||
|
hashfunction = hash.get(self.encryptAlgo) |
||||
|
|
||||
|
password = password.encode('utf8') |
||||
|
seed = time.ctime() |
||||
|
salt = hashfunction(seed).hexdigest() |
||||
|
saltHash = hashfunction(seed).digest() |
||||
|
keyBasis = password+saltHash |
||||
|
key = hashfunction(keyBasis).digest() |
||||
|
keyHash = hashfunction(key).hexdigest() |
||||
|
|
||||
|
self.info['keyHashAlgorithmID'] = self.encryptAlgo |
||||
|
self.info['keyHash'] = keyHash.upper() |
||||
|
self.info['salt'] = salt.upper() |
||||
|
def _decode_hex(self,value): |
||||
|
''' |
||||
|
Helper function to decode hex string to `proper` hex string |
||||
|
@param value: Value to decode |
||||
|
@return: Hex string |
||||
|
''' |
||||
|
result = '' |
||||
|
for i in range(0,len(value),2): |
||||
|
tmp = int(value[i:i+2],16) |
||||
|
result += chr(tmp) |
||||
|
return result |
||||
|
def _decode_binary(self,rawIdentifier,identifier): |
||||
|
rawIdentifier += '\r\n\r\n' |
||||
|
dataLength = int(identifier['Length']) |
||||
|
pointerStart = self.raw.find(rawIdentifier)+len(rawIdentifier) |
||||
|
pointerEnd = pointerStart + dataLength |
||||
|
data = self.raw[pointerStart:pointerEnd] |
||||
|
if not len(data) == dataLength: |
||||
|
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s'%(dataLength,len(data))) |
||||
|
return data |
||||
|
def _validate_password(self,password): |
||||
|
''' |
||||
|
Validate GNTP Message against stored password |
||||
|
''' |
||||
|
self.password = password |
||||
|
if password == None: raise Exception() |
||||
|
keyHash = self.info.get('keyHash',None) |
||||
|
if keyHash is None and self.password is None: |
||||
|
return True |
||||
|
if keyHash is None: |
||||
|
raise AuthError('Invalid keyHash') |
||||
|
if self.password is None: |
||||
|
raise AuthError('Missing password') |
||||
|
|
||||
|
password = self.password.encode('utf8') |
||||
|
saltHash = self._decode_hex(self.info['salt']) |
||||
|
|
||||
|
keyBasis = password+saltHash |
||||
|
key = hashlib.md5(keyBasis).digest() |
||||
|
keyHash = hashlib.md5(key).hexdigest() |
||||
|
|
||||
|
if not keyHash.upper() == self.info['keyHash'].upper(): |
||||
|
raise AuthError('Invalid Hash') |
||||
|
return True |
||||
|
def validate(self): |
||||
|
''' |
||||
|
Verify required headers |
||||
|
''' |
||||
|
for header in self._requiredHeaders: |
||||
|
if not self.headers.get(header,False): |
||||
|
raise ParseError('Missing Notification Header: '+header) |
||||
|
|
||||
|
def _format_info(self): |
||||
|
''' |
||||
|
Generate info line for GNTP Message |
||||
|
@return: Info line string |
||||
|
''' |
||||
|
info = u'GNTP/%s %s'%( |
||||
|
self.info.get('version'), |
||||
|
self.info.get('messagetype'), |
||||
|
) |
||||
|
if self.info.get('encryptionAlgorithmID',None): |
||||
|
info += ' %s:%s'%( |
||||
|
self.info.get('encryptionAlgorithmID'), |
||||
|
self.info.get('ivValue'), |
||||
|
) |
||||
|
else: |
||||
|
info+=' NONE' |
||||
|
|
||||
|
if self.info.get('keyHashAlgorithmID',None): |
||||
|
info += ' %s:%s.%s'%( |
||||
|
self.info.get('keyHashAlgorithmID'), |
||||
|
self.info.get('keyHash'), |
||||
|
self.info.get('salt') |
||||
|
) |
||||
|
|
||||
|
return info |
||||
|
def _parse_dict(self,data): |
||||
|
''' |
||||
|
Helper function to parse blocks of GNTP headers into a dictionary |
||||
|
@param data: |
||||
|
@return: Dictionary of headers |
||||
|
''' |
||||
|
dict = {} |
||||
|
for line in data.split('\r\n'): |
||||
|
match = re.match('([\w-]+):(.+)', line) |
||||
|
if not match: continue |
||||
|
|
||||
|
key = match.group(1).strip() |
||||
|
val = match.group(2).strip() |
||||
|
dict[key] = val |
||||
|
return dict |
||||
|
def add_header(self,key,value): |
||||
|
if isinstance(value, unicode): |
||||
|
self.headers[key] = value |
||||
|
else: |
||||
|
self.headers[key] = unicode('%s'%value,'utf8','replace') |
||||
|
def decode(self,data,password=None): |
||||
|
''' |
||||
|
Decode GNTP Message |
||||
|
@param data: |
||||
|
''' |
||||
|
self.password = password |
||||
|
self.raw = data |
||||
|
parts = self.raw.split('\r\n\r\n') |
||||
|
self.info = self._parse_info(data) |
||||
|
self.headers = self._parse_dict(parts[0]) |
||||
|
def encode(self): |
||||
|
''' |
||||
|
Encode a GNTP Message |
||||
|
@return: GNTP Message ready to be sent |
||||
|
''' |
||||
|
self.validate() |
||||
|
EOL = u'\r\n' |
||||
|
|
||||
|
message = self._format_info() + EOL |
||||
|
#Headers |
||||
|
for k,v in self.headers.iteritems(): |
||||
|
message += u'%s: %s%s'%(k,v,EOL) |
||||
|
|
||||
|
message += EOL |
||||
|
return message.encode('utf8') |
||||
|
class GNTPRegister(_GNTPBase): |
||||
|
''' |
||||
|
GNTP Registration Message |
||||
|
''' |
||||
|
notifications = [] |
||||
|
_requiredHeaders = [ |
||||
|
'Application-Name', |
||||
|
'Notifications-Count' |
||||
|
] |
||||
|
_requiredNotificationHeaders = ['Notification-Name'] |
||||
|
def __init__(self,data=None,password=None): |
||||
|
''' |
||||
|
@param data: (Optional) See decode() |
||||
|
@param password: (Optional) Password to use while encoding/decoding messages |
||||
|
''' |
||||
|
self.info['messagetype'] = 'REGISTER' |
||||
|
|
||||
|
if data: |
||||
|
self.decode(data,password) |
||||
|
else: |
||||
|
self.set_password(password) |
||||
|
self.add_header('Application-Name', 'pygntp') |
||||
|
self.add_header('Notifications-Count', 0) |
||||
|
self.add_origin_info() |
||||
|
def validate(self): |
||||
|
''' |
||||
|
Validate required headers and validate notification headers |
||||
|
''' |
||||
|
for header in self._requiredHeaders: |
||||
|
if not self.headers.get(header,False): |
||||
|
raise ParseError('Missing Registration Header: '+header) |
||||
|
for notice in self.notifications: |
||||
|
for header in self._requiredNotificationHeaders: |
||||
|
if not notice.get(header,False): |
||||
|
raise ParseError('Missing Notification Header: '+header) |
||||
|
def decode(self,data,password): |
||||
|
''' |
||||
|
Decode existing GNTP Registration message |
||||
|
@param data: Message to decode. |
||||
|
''' |
||||
|
self.raw = data |
||||
|
parts = self.raw.split('\r\n\r\n') |
||||
|
self.info = self._parse_info(data) |
||||
|
self._validate_password(password) |
||||
|
self.headers = self._parse_dict(parts[0]) |
||||
|
|
||||
|
for i,part in enumerate(parts): |
||||
|
if i==0: continue #Skip Header |
||||
|
if part.strip()=='': continue |
||||
|
notice = self._parse_dict(part) |
||||
|
if notice.get('Notification-Name',False): |
||||
|
self.notifications.append(notice) |
||||
|
elif notice.get('Identifier',False): |
||||
|
notice['Data'] = self._decode_binary(part,notice) |
||||
|
#open('register.png','wblol').write(notice['Data']) |
||||
|
self.resources[ notice.get('Identifier') ] = notice |
||||
|
|
||||
|
def add_notification(self,name,enabled=True): |
||||
|
''' |
||||
|
Add new Notification to Registration message |
||||
|
@param name: Notification Name |
||||
|
@param enabled: Default Notification to Enabled |
||||
|
''' |
||||
|
notice = {} |
||||
|
notice['Notification-Name'] = u'%s'%name |
||||
|
notice['Notification-Enabled'] = u'%s'%enabled |
||||
|
|
||||
|
self.notifications.append(notice) |
||||
|
self.add_header('Notifications-Count', len(self.notifications)) |
||||
|
def encode(self): |
||||
|
''' |
||||
|
Encode a GNTP Registration Message |
||||
|
@return: GNTP Registration Message ready to be sent |
||||
|
''' |
||||
|
self.validate() |
||||
|
EOL = u'\r\n' |
||||
|
|
||||
|
message = self._format_info() + EOL |
||||
|
#Headers |
||||
|
for k,v in self.headers.iteritems(): |
||||
|
message += u'%s: %s%s'%(k,v,EOL) |
||||
|
|
||||
|
#Notifications |
||||
|
if len(self.notifications)>0: |
||||
|
for notice in self.notifications: |
||||
|
message += EOL |
||||
|
for k,v in notice.iteritems(): |
||||
|
message += u'%s: %s%s'%(k,v,EOL) |
||||
|
|
||||
|
message += EOL |
||||
|
return message |
||||
|
|
||||
|
class GNTPNotice(_GNTPBase): |
||||
|
''' |
||||
|
GNTP Notification Message |
||||
|
''' |
||||
|
_requiredHeaders = [ |
||||
|
'Application-Name', |
||||
|
'Notification-Name', |
||||
|
'Notification-Title' |
||||
|
] |
||||
|
def __init__(self,data=None,app=None,name=None,title=None,password=None): |
||||
|
''' |
||||
|
|
||||
|
@param data: (Optional) See decode() |
||||
|
@param app: (Optional) Set Application-Name |
||||
|
@param name: (Optional) Set Notification-Name |
||||
|
@param title: (Optional) Set Notification Title |
||||
|
@param password: (Optional) Password to use while encoding/decoding messages |
||||
|
''' |
||||
|
self.info['messagetype'] = 'NOTIFY' |
||||
|
|
||||
|
if data: |
||||
|
self.decode(data,password) |
||||
|
else: |
||||
|
self.set_password(password) |
||||
|
if app: |
||||
|
self.add_header('Application-Name', app) |
||||
|
if name: |
||||
|
self.add_header('Notification-Name', name) |
||||
|
if title: |
||||
|
self.add_header('Notification-Title', title) |
||||
|
self.add_origin_info() |
||||
|
def decode(self,data,password): |
||||
|
''' |
||||
|
Decode existing GNTP Notification message |
||||
|
@param data: Message to decode. |
||||
|
''' |
||||
|
self.raw = data |
||||
|
parts = self.raw.split('\r\n\r\n') |
||||
|
self.info = self._parse_info(data) |
||||
|
self._validate_password(password) |
||||
|
self.headers = self._parse_dict(parts[0]) |
||||
|
|
||||
|
for i,part in enumerate(parts): |
||||
|
if i==0: continue #Skip Header |
||||
|
if part.strip()=='': continue |
||||
|
notice = self._parse_dict(part) |
||||
|
if notice.get('Identifier',False): |
||||
|
notice['Data'] = self._decode_binary(part,notice) |
||||
|
#open('notice.png','wblol').write(notice['Data']) |
||||
|
self.resources[ notice.get('Identifier') ] = notice |
||||
|
def encode(self): |
||||
|
''' |
||||
|
Encode a GNTP Notification Message |
||||
|
@return: GNTP Notification Message ready to be sent |
||||
|
''' |
||||
|
self.validate() |
||||
|
EOL = u'\r\n' |
||||
|
|
||||
|
message = self._format_info() + EOL |
||||
|
#Headers |
||||
|
for k,v in self.headers.iteritems(): |
||||
|
message += u'%s: %s%s'%(k,v,EOL) |
||||
|
|
||||
|
message += EOL |
||||
|
return message.encode('utf8') |
||||
|
|
||||
|
class GNTPSubscribe(_GNTPBase): |
||||
|
def __init__(self,data=None,password=None): |
||||
|
self.info['messagetype'] = 'SUBSCRIBE' |
||||
|
self._requiredHeaders = [ |
||||
|
'Subscriber-ID', |
||||
|
'Subscriber-Name', |
||||
|
] |
||||
|
if data: |
||||
|
self.decode(data,password) |
||||
|
else: |
||||
|
self.set_password(password) |
||||
|
self.add_origin_info() |
||||
|
|
||||
|
class GNTPOK(_GNTPBase): |
||||
|
_requiredHeaders = ['Response-Action'] |
||||
|
def __init__(self,data=None,action=None): |
||||
|
''' |
||||
|
@param data: (Optional) See _GNTPResponse.decode() |
||||
|
@param action: (Optional) Set type of action the OK Response is for |
||||
|
''' |
||||
|
self.info['messagetype'] = '-OK' |
||||
|
if data: |
||||
|
self.decode(data) |
||||
|
if action: |
||||
|
self.add_header('Response-Action', action) |
||||
|
self.add_origin_info() |
||||
|
|
||||
|
class GNTPError(_GNTPBase): |
||||
|
_requiredHeaders = ['Error-Code','Error-Description'] |
||||
|
def __init__(self,data=None,errorcode=None,errordesc=None): |
||||
|
''' |
||||
|
@param data: (Optional) See _GNTPResponse.decode() |
||||
|
@param errorcode: (Optional) Error code |
||||
|
@param errordesc: (Optional) Error Description |
||||
|
''' |
||||
|
self.info['messagetype'] = '-ERROR' |
||||
|
if data: |
||||
|
self.decode(data) |
||||
|
if errorcode: |
||||
|
self.add_header('Error-Code', errorcode) |
||||
|
self.add_header('Error-Description', errordesc) |
||||
|
self.add_origin_info() |
||||
|
def error(self): |
||||
|
return self.headers['Error-Code'],self.headers['Error-Description'] |
||||
|
|
||||
|
def parse_gntp(data,password=None): |
||||
|
''' |
||||
|
Attempt to parse a message as a GNTP message |
||||
|
@param data: Message to be parsed |
||||
|
@param password: Optional password to be used to verify the message |
||||
|
''' |
||||
|
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',data,re.IGNORECASE) |
||||
|
if not match: |
||||
|
raise ParseError('INVALID_GNTP_INFO') |
||||
|
info = match.groupdict() |
||||
|
if info['messagetype'] == 'REGISTER': |
||||
|
return GNTPRegister(data,password=password) |
||||
|
elif info['messagetype'] == 'NOTIFY': |
||||
|
return GNTPNotice(data,password=password) |
||||
|
elif info['messagetype'] == 'SUBSCRIBE': |
||||
|
return GNTPSubscribe(data,password=password) |
||||
|
elif info['messagetype'] == '-OK': |
||||
|
return GNTPOK(data) |
||||
|
elif info['messagetype'] == '-ERROR': |
||||
|
return GNTPError(data) |
||||
|
raise ParseError('INVALID_GNTP_MESSAGE') |
@ -0,0 +1,130 @@ |
|||||
|
""" |
||||
|
A Python module that uses GNTP to post messages |
||||
|
Mostly mirrors the Growl.py file that comes with Mac Growl |
||||
|
http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py |
||||
|
""" |
||||
|
import gntp |
||||
|
import socket |
||||
|
import logging |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
class GrowlNotifier(object): |
||||
|
applicationName = 'Python GNTP' |
||||
|
notifications = [] |
||||
|
defaultNotifications = [] |
||||
|
applicationIcon = None |
||||
|
passwordHash = 'MD5' |
||||
|
|
||||
|
#GNTP Specific |
||||
|
password = None |
||||
|
hostname = 'localhost' |
||||
|
port = 23053 |
||||
|
|
||||
|
def __init__(self, applicationName=None, notifications=None, defaultNotifications=None, applicationIcon=None, hostname=None, password=None, port=None): |
||||
|
if applicationName: |
||||
|
self.applicationName = applicationName |
||||
|
assert self.applicationName, 'An application name is required.' |
||||
|
|
||||
|
if notifications: |
||||
|
self.notifications = list(notifications) |
||||
|
assert self.notifications, 'A sequence of one or more notification names is required.' |
||||
|
|
||||
|
if defaultNotifications is not None: |
||||
|
self.defaultNotifications = list(defaultNotifications) |
||||
|
elif not self.defaultNotifications: |
||||
|
self.defaultNotifications = list(self.notifications) |
||||
|
|
||||
|
if applicationIcon is not None: |
||||
|
self.applicationIcon = self._checkIcon(applicationIcon) |
||||
|
elif self.applicationIcon is not None: |
||||
|
self.applicationIcon = self._checkIcon(self.applicationIcon) |
||||
|
|
||||
|
#GNTP Specific |
||||
|
if password: |
||||
|
self.password = password |
||||
|
|
||||
|
if hostname: |
||||
|
self.hostname = hostname |
||||
|
assert self.hostname, 'Requires valid hostname' |
||||
|
|
||||
|
if port: |
||||
|
self.port = int(port) |
||||
|
assert isinstance(self.port,int), 'Requires valid port' |
||||
|
|
||||
|
def _checkIcon(self, data): |
||||
|
''' |
||||
|
Check the icon to see if it's valid |
||||
|
@param data: |
||||
|
@todo Consider checking for a valid URL |
||||
|
''' |
||||
|
return data |
||||
|
|
||||
|
def register(self): |
||||
|
''' |
||||
|
Send GNTP Registration |
||||
|
''' |
||||
|
logger.info('Sending registration to %s:%s',self.hostname,self.port) |
||||
|
register = gntp.GNTPRegister() |
||||
|
register.add_header('Application-Name',self.applicationName) |
||||
|
for notification in self.notifications: |
||||
|
enabled = notification in self.defaultNotifications |
||||
|
register.add_notification(notification,enabled) |
||||
|
if self.applicationIcon: |
||||
|
register.add_header('Application-Icon',self.applicationIcon) |
||||
|
if self.password: |
||||
|
register.set_password(self.password,self.passwordHash) |
||||
|
response = self.send('register',register.encode()) |
||||
|
if isinstance(response,gntp.GNTPOK): return True |
||||
|
logger.debug('Invalid response %s',response.error()) |
||||
|
return response.error() |
||||
|
|
||||
|
def notify(self, noteType, title, description, icon=None, sticky=False, priority=None): |
||||
|
''' |
||||
|
Send a GNTP notifications |
||||
|
''' |
||||
|
logger.info('Sending notification [%s] to %s:%s',noteType,self.hostname,self.port) |
||||
|
assert noteType in self.notifications |
||||
|
notice = gntp.GNTPNotice() |
||||
|
notice.add_header('Application-Name',self.applicationName) |
||||
|
notice.add_header('Notification-Name',noteType) |
||||
|
notice.add_header('Notification-Title',title) |
||||
|
if self.password: |
||||
|
notice.set_password(self.password,self.passwordHash) |
||||
|
if sticky: |
||||
|
notice.add_header('Notification-Sticky',sticky) |
||||
|
if priority: |
||||
|
notice.add_header('Notification-Priority',priority) |
||||
|
if icon: |
||||
|
notice.add_header('Notification-Icon',self._checkIcon(icon)) |
||||
|
if description: |
||||
|
notice.add_header('Notification-Text',description) |
||||
|
response = self.send('notify',notice.encode()) |
||||
|
if isinstance(response,gntp.GNTPOK): return True |
||||
|
logger.debug('Invalid response %s',response.error()) |
||||
|
return response.error() |
||||
|
def subscribe(self,id,name,port): |
||||
|
sub = gntp.GNTPSubscribe() |
||||
|
sub.add_header('Subscriber-ID',id) |
||||
|
sub.add_header('Subscriber-Name',name) |
||||
|
sub.add_header('Subscriber-Port',port) |
||||
|
if self.password: |
||||
|
sub.set_password(self.password,self.passwordHash) |
||||
|
response = self.send('subscribe',sub.encode()) |
||||
|
if isinstance(response,gntp.GNTPOK): return True |
||||
|
logger.debug('Invalid response %s',response.error()) |
||||
|
return response.error() |
||||
|
def send(self,type,data): |
||||
|
''' |
||||
|
Send the GNTP Packet |
||||
|
''' |
||||
|
logger.debug('To : %s:%s <%s>\n%s',self.hostname,self.port,type,data) |
||||
|
|
||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
||||
|
s.connect((self.hostname,self.port)) |
||||
|
s.send(data.encode('utf-8', 'replace')) |
||||
|
response = gntp.parse_gntp(s.recv(1024)) |
||||
|
s.close() |
||||
|
|
||||
|
logger.debug('From : %s:%s <%s>\n%s',self.hostname,self.port,response.__class__,response) |
||||
|
return response |
@ -0,0 +1,223 @@ |
|||||
|
#!/usr/bin/python -OO |
||||
|
# Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org> |
||||
|
# |
||||
|
# This program is free software; you can redistribute it and/or |
||||
|
# modify it under the terms of the GNU General Public License |
||||
|
# as published by the Free Software Foundation; either version 2 |
||||
|
# of the License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU General Public License |
||||
|
# along with this program; if not, write to the Free Software |
||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
|
# |
||||
|
""" |
||||
|
sabnzbd.growler - Send notifications to Growl |
||||
|
""" |
||||
|
#------------------------------------------------------------------------------ |
||||
|
|
||||
|
import os.path |
||||
|
import logging |
||||
|
import socket |
||||
|
|
||||
|
import sabnzbd |
||||
|
import sabnzbd.cfg |
||||
|
from sabnzbd.encoding import unicoder, latin1 |
||||
|
from gntp import GNTPRegister |
||||
|
from gntp.notifier import GrowlNotifier |
||||
|
try: |
||||
|
import Growl |
||||
|
_HAVE_OSX_GROWL = True |
||||
|
except ImportError: |
||||
|
_HAVE_OSX_GROWL = False |
||||
|
try: |
||||
|
import pynotify |
||||
|
_HAVE_NTFOSD = True |
||||
|
except ImportError: |
||||
|
_HAVE_NTFOSD = False |
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
# Define translatable message table |
||||
|
TT = lambda x:x |
||||
|
_NOTIFICATION = { |
||||
|
'startup' : TT('Startup/Shutdown'), #: Message class for Growl server |
||||
|
'download' : TT('Added NZB'), #: Message class for Growl server |
||||
|
'pp' : TT('Post-processing started'), #: Message class for Growl server |
||||
|
'complete' : TT('Job finished'), #: Message class for Growl server |
||||
|
'other' : TT('Other Messages') #: Message class for Growl server |
||||
|
} |
||||
|
_KEYS = ('startup', 'download', 'pp', 'complete', 'other') |
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
# Setup platform dependent Growl support |
||||
|
# |
||||
|
_GROWL_ICON = None # Platform-dependant icon path |
||||
|
_GROWL = None # Instance of the Notifier after registration |
||||
|
_GROWL_REG = False # Succesful registration |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
def get_icon(): |
||||
|
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico') |
||||
|
if not os.path.isfile(icon): |
||||
|
icon = None |
||||
|
return icon |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
def change_value(): |
||||
|
""" Signal that we should register with a new Growl server |
||||
|
""" |
||||
|
global _GROWL_REG |
||||
|
_GROWL_REG = False |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
def have_growl(): |
||||
|
""" Return if any Growl support is present |
||||
|
""" |
||||
|
return True |
||||
|
|
||||
|
def have_ntfosd(): |
||||
|
""" Return if any PyNotify support is present |
||||
|
""" |
||||
|
return bool(_HAVE_NTFOSD) |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
def send_notification(title , msg, gtype): |
||||
|
""" Send Notification message |
||||
|
""" |
||||
|
if have_growl(): |
||||
|
send_growl(title, msg, gtype) |
||||
|
if have_ntfosd(): |
||||
|
send_notify_osd(title, msg) |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
def register_growl(): |
||||
|
""" Register this app with Growl |
||||
|
""" |
||||
|
host, port = sabnzbd.misc.split_host(sabnzbd.cfg.growl_server()) |
||||
|
|
||||
|
if host: |
||||
|
sys_name = '@' + sabnzbd.misc.hostname().lower() |
||||
|
else: |
||||
|
sys_name = '' |
||||
|
|
||||
|
# Clean up persistent data in GNTP to make re-registration work |
||||
|
GNTPRegister.notifications = [] |
||||
|
GNTPRegister.headers = {} |
||||
|
|
||||
|
growler = GrowlNotifier( |
||||
|
applicationName = 'SABnzbd%s' % sys_name, |
||||
|
applicationIcon = get_icon(), |
||||
|
notifications = [Tx(_NOTIFICATION[key]) for key in _KEYS], |
||||
|
hostname = host or None, |
||||
|
port = port or 23053, |
||||
|
password = sabnzbd.cfg.growl_password() or None |
||||
|
) |
||||
|
|
||||
|
try: |
||||
|
ret = growler.register() |
||||
|
if ret is None or isinstance(ret, bool): |
||||
|
logging.info('Registered with Growl') |
||||
|
ret = growler |
||||
|
else: |
||||
|
logging.debug('Cannot register with Growl %s', ret) |
||||
|
del growler |
||||
|
ret = None |
||||
|
except socket.error, err: |
||||
|
logging.debug('Cannot register with Growl %s', err) |
||||
|
del growler |
||||
|
ret = None |
||||
|
return ret |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
def send_growl(title , msg, gtype): |
||||
|
""" Send Growl message |
||||
|
""" |
||||
|
global _GROWL, _GROWL_REG |
||||
|
if not sabnzbd.cfg.growl_enable(): |
||||
|
return |
||||
|
|
||||
|
if _HAVE_OSX_GROWL and not sabnzbd.cfg.growl_server(): |
||||
|
send_local_growl(title, msg, gtype) |
||||
|
return |
||||
|
|
||||
|
for n in (0, 1): |
||||
|
if not _GROWL_REG: _GROWL = None |
||||
|
_GROWL = _GROWL or register_growl() |
||||
|
if _GROWL: |
||||
|
assert isinstance(_GROWL, GrowlNotifier) |
||||
|
_GROWL_REG = True |
||||
|
logging.debug('Send to Growl: %s %s %s', gtype, latin1(title), latin1(msg)) |
||||
|
try: |
||||
|
ret = _GROWL.notify( |
||||
|
noteType = Tx(_NOTIFICATION.get(gtype, 'other')), |
||||
|
title = title, |
||||
|
description = unicoder(msg), |
||||
|
#icon = options.icon, |
||||
|
#sticky = options.sticky, |
||||
|
#priority = options.priority |
||||
|
) |
||||
|
if ret is None or isinstance(ret, bool): |
||||
|
return |
||||
|
elif ret[0] == '401': |
||||
|
_GROWL = False |
||||
|
else: |
||||
|
logging.debug('Growl error %s', ret) |
||||
|
return |
||||
|
except socket.error, err: |
||||
|
logging.debug('Growl error %s', err) |
||||
|
return |
||||
|
else: |
||||
|
return |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
# Local OSX Growl support |
||||
|
# |
||||
|
if _HAVE_OSX_GROWL: |
||||
|
|
||||
|
if os.path.isfile('sabnzbdplus.icns'): |
||||
|
_OSX_ICON = Growl.Image.imageFromPath('sabnzbdplus.icns') |
||||
|
elif os.path.isfile('osx/resources/sabnzbdplus.icns'): |
||||
|
_OSX_ICON = Growl.Image.imageFromPath('osx/resources/sabnzbdplus.icns') |
||||
|
else: |
||||
|
_OSX_ICON = Growl.Image.imageWithIconForApplication('Terminal') |
||||
|
|
||||
|
def send_local_growl(title , msg, gtype): |
||||
|
""" Send to local Growl server, OSX-only """ |
||||
|
notes = [Tx(_NOTIFICATION[key]) for key in _KEYS] |
||||
|
growler = Growl.GrowlNotifier( |
||||
|
applicationName = 'SABnzbd', |
||||
|
applicationIcon = _OSX_ICON, |
||||
|
notifications = notes, |
||||
|
defaultNotifications = notes |
||||
|
) |
||||
|
growler.register() |
||||
|
growler.notify(Tx(_NOTIFICATION.get(gtype, 'other')), title, msg) |
||||
|
|
||||
|
|
||||
|
#------------------------------------------------------------------------------ |
||||
|
# Ubuntu NotifyOSD Support |
||||
|
# |
||||
|
if _HAVE_NTFOSD: |
||||
|
_NTFOSD = False |
||||
|
def send_notify_osd(title, message): |
||||
|
""" Send a message to NotifyOSD |
||||
|
""" |
||||
|
global _NTFOSD |
||||
|
if sabnzbd.cfg.ntfosd_enable(): |
||||
|
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico') |
||||
|
_NTFOSD = _NTFOSD or pynotify.init('icon-summary-body') |
||||
|
if _NTFOSD: |
||||
|
logging.info('Send to NotifyOSD: %s / %s', latin1(title), latin1(message)) |
||||
|
note = pynotify.Notification(title, message, icon) |
||||
|
note.show() |
@ -1,65 +0,0 @@ |
|||||
#!/usr/bin/python -OO |
|
||||
# Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org> |
|
||||
# |
|
||||
# This program is free software; you can redistribute it and/or |
|
||||
# modify it under the terms of the GNU General Public License |
|
||||
# as published by the Free Software Foundation; either version 2 |
|
||||
# of the License, or (at your option) any later version. |
|
||||
# |
|
||||
# This program is distributed in the hope that it will be useful, |
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
# GNU General Public License for more details. |
|
||||
# |
|
||||
# You should have received a copy of the GNU General Public License |
|
||||
# along with this program; if not, write to the Free Software |
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
||||
# |
|
||||
#""" |
|
||||
#TO FIX : Translations are not working with this implementation |
|
||||
# Growl Registration may only be done once per run ? |
|
||||
# Registration is made too early, the language module has not read the text file yet |
|
||||
#NOTIFICATION = {'startup':'grwl-notif-startup','download':'grwl-notif-dl','pp':'grwl-notif-pp','other':'grwl-notif-other'} |
|
||||
NOTIFICATION = {'startup':'1. On Startup/Shutdown','download':'2. On adding NZB','pp':'3. On post-processing','complete':'4. On download terminated','other':'5. Other Messages'} |
|
||||
|
|
||||
# For a future release, make texts translatable. |
|
||||
if 0: |
|
||||
#------------------------------------------------------------------------------ |
|
||||
# Define translatable message table |
|
||||
TT = lambda x:x |
|
||||
_NOTIFICATION = { |
|
||||
'startup' : TT('Startup/Shutdown'), #: Message class for Growl server |
|
||||
'download' : TT('Added NZB'), #: Message class for Growl server |
|
||||
'pp' : TT('Post-processing started'), #: Message class for Growl server |
|
||||
'complete' : TT('Job finished'), #: Message class for Growl server |
|
||||
'other' : TT('Other Messages') #: Message class for Growl server |
|
||||
} |
|
||||
|
|
||||
try: |
|
||||
import Growl |
|
||||
import os.path |
|
||||
import logging |
|
||||
|
|
||||
if os.path.isfile('sabnzbdplus.icns'): |
|
||||
nIcon = Growl.Image.imageFromPath('sabnzbdplus.icns') |
|
||||
elif os.path.isfile('osx/resources/sabnzbdplus.icns'): |
|
||||
nIcon = Growl.Image.imageFromPath('osx/resources/sabnzbdplus.icns') |
|
||||
else: |
|
||||
nIcon = Growl.Image.imageWithIconForApplication('Terminal') |
|
||||
|
|
||||
def sendGrowlMsg(nTitle , nMsg, nType=NOTIFICATION['other']): |
|
||||
gnotifier = SABGrowlNotifier(applicationIcon=nIcon) |
|
||||
gnotifier.register() |
|
||||
#TO FIX |
|
||||
#gnotifier.notify(T(nType), nTitle, nMsg) |
|
||||
gnotifier.notify(nType, nTitle, nMsg) |
|
||||
|
|
||||
class SABGrowlNotifier(Growl.GrowlNotifier): |
|
||||
applicationName = "SABnzbd" |
|
||||
#TO FIX |
|
||||
#notifications = [T(notification) for notification in NOTIFICATION.values()] |
|
||||
notifications = NOTIFICATION.values() |
|
||||
|
|
||||
except ImportError: |
|
||||
def sendGrowlMsg(nTitle , nMsg, nType): |
|
||||
pass |
|
Loading…
Reference in new issue