diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 02f40718..3859d4a7 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -1372,6 +1372,23 @@ +
+
+ +
+
+
+ +
+
+ Comma separated list. Leave blank to send to all devices +
+
+ +
+
+
+ @@ -2168,6 +2185,27 @@ } }); + if ($("#join").is(":checked")) + { + $("#joinoptions").show(); + } + else + { + $("#joinoptions").hide(); + } + + + $("#join").click(function(){ + if ($("#join").is(":checked")) + { + $("#joinoptions").slideDown(); + } + else + { + $("#joinoptions").slideUp(); + } + }); + if ($("#twitter").is(":checked")) { $("#twitteroptions").show(); diff --git a/headphones/config.py b/headphones/config.py index 98b23785..83c73415 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -145,6 +145,10 @@ _CONFIG_DEFINITIONS = { 'IGNORED_FILES': (list, 'Advanced', []), # path 'INCLUDE_EXTRAS': (int, 'General', 0), 'INTERFACE': (str, 'General', 'default'), + 'JOIN_APIKEY': (str, 'Join', ''), + 'JOIN_DEVICEID': (str, 'Join', ''), + 'JOIN_ENABLED': (int, 'Join', 0), + 'JOIN_ONSNATCH': (int, 'Join', 0), 'JOURNAL_MODE': (str, 'Advanced', 'wal'), 'KAT': (int, 'Kat', 0), 'KAT_PROXY_URL': (str, 'Kat', ''), diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 54f4f4b5..26cdb234 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from urllib import urlencode +from urllib import urlencode, quote_plus import urllib import subprocess import json @@ -148,7 +148,9 @@ class PROWL(object): http_handler.request("POST", "/publicapi/add", - headers={'Content-type': "application/x-www-form-urlencoded"}, + headers={ + 'Content-type': + "application/x-www-form-urlencoded"}, body=urlencode(data)) response = http_handler.getresponse() request_status = response.status @@ -203,28 +205,34 @@ class XBMC(object): url = host + '/xbmcCmds/xbmcHttp/?' + url_command if self.password: - return request.request_content(url, auth=(self.username, self.password)) + return request.request_content(url, + auth=(self.username, self.password)) else: return request.request_content(url) def _sendjson(self, host, method, params={}): - data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] + data = [ + {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] headers = {'Content-Type': 'application/json'} url = host + '/jsonrpc' if self.password: - response = request.request_json(url, method="post", data=json.dumps(data), - headers=headers, auth=(self.username, self.password)) + response = request.request_json( + url, method="post", + data=json.dumps(data), + headers=headers, auth=( + self.username, self.password)) else: - response = request.request_json(url, method="post", data=json.dumps(data), + response = request.request_json(url, method="post", + data=json.dumps(data), headers=headers) if response: return response[0]['result'] def update(self): - # From what I read you can't update the music library on a per directory or per path basis - # so need to update the whole thing + # From what I read you can't update the music library on a per + # directory or per path basis so need to update the whole thing hosts = [x.strip() for x in self.hosts.split(',')] @@ -247,18 +255,23 @@ class XBMC(object): logger.info('Sending notification command to XMBC @ ' + host) try: version = self._sendjson(host, 'Application.GetProperties', - {'properties': ['version']})['version']['major'] + {'properties': ['version']})[ + 'version']['major'] if version < 12: # Eden - notification = header + "," + message + "," + time + "," + albumartpath + notification = header + "," + message + "," + time + \ + "," + albumartpath notifycommand = {'command': 'ExecBuiltIn', - 'parameter': 'Notification(' + notification + ')'} + 'parameter': 'Notification(' + + notification + ')'} request = self._sendhttp(host, notifycommand) else: # Frodo - params = {'title': header, 'message': message, 'displaytime': int(time), + params = {'title': header, 'message': message, + 'displaytime': int(time), 'image': albumartpath} - request = self._sendjson(host, 'GUI.ShowNotification', params) + request = self._sendjson(host, 'GUI.ShowNotification', + params) if not request: raise Exception @@ -323,22 +336,28 @@ class Plex(object): url = host + '/xbmcCmds/xbmcHttp/?' + command if self.password: - response = request.request_response(url, auth=(self.username, self.password)) + response = request.request_response(url, auth=( + self.username, self.password)) else: response = request.request_response(url) return response def _sendjson(self, host, method, params={}): - data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] + data = [ + {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] headers = {'Content-Type': 'application/json'} url = host + '/jsonrpc' if self.password: - response = request.request_json(url, method="post", data=json.dumps(data), - headers=headers, auth=(self.username, self.password)) + response = request.request_json( + url, method="post", + data=json.dumps(data), + headers=headers, auth=( + self.username, self.password)) else: - response = request.request_json(url, method="post", data=json.dumps(data), + response = request.request_json(url, method="post", + data=json.dumps(data), headers=headers) if response: @@ -346,13 +365,14 @@ class Plex(object): def update(self): - # From what I read you can't update the music library on a per directory or per path basis - # so need to update the whole thing + # From what I read you can't update the music library on a per + # directory or per path basis so need to update the whole thing hosts = [x.strip() for x in self.server_hosts.split(',')] for host in hosts: - logger.info('Sending library update command to Plex Media Server@ ' + host) + logger.info( + 'Sending library update command to Plex Media Server@ ' + host) url = "%s/library/sections" % host if self.token: params = {'X-Plex-Token': self.token} @@ -369,7 +389,8 @@ class Plex(object): for s in sections: if s.getAttribute('type') == "artist": - url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key')) + url = "%s/library/sections/%s/refresh" % ( + host, s.getAttribute('key')) request.request_response(url, params=params) def notify(self, artist, album, albumartpath): @@ -381,27 +402,35 @@ class Plex(object): time = "3000" # in ms for host in hosts: - logger.info('Sending notification command to Plex client @ ' + host) + logger.info( + 'Sending notification command to Plex client @ ' + host) try: version = self._sendjson(host, 'Application.GetProperties', - {'properties': ['version']})['version']['major'] + {'properties': ['version']})[ + 'version']['major'] if version < 12: # Eden - notification = header + "," + message + "," + time + "," + albumartpath + notification = header + "," + message + "," + time + \ + "," + albumartpath notifycommand = {'command': 'ExecBuiltIn', - 'parameter': 'Notification(' + notification + ')'} + 'parameter': 'Notification(' + + notification + ')'} request = self._sendhttp(host, notifycommand) else: # Frodo - params = {'title': header, 'message': message, 'displaytime': int(time), + params = {'title': header, 'message': message, + 'displaytime': int(time), 'image': albumartpath} - request = self._sendjson(host, 'GUI.ShowNotification', params) + request = self._sendjson(host, 'GUI.ShowNotification', + params) if not request: raise Exception except Exception: - logger.error('Error sending notification request to Plex client @ ' + host) + logger.error( + 'Error sending notification request to Plex client @ ' + + host) class NMA(object): @@ -419,7 +448,8 @@ class NMA(object): message = "Headphones has snatched: " + snatched else: event = artist + ' - ' + album + ' complete!' - message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']' + message = "Headphones has downloaded and postprocessed: " + \ + artist + ' [' + album + ']' logger.debug(u"NMA event: " + event) logger.debug(u"NMA message: " + message) @@ -433,7 +463,8 @@ class NMA(object): if len(keys) > 1: batch = True - response = p.push(title, event, message, priority=nma_priority, batch_mode=batch) + response = p.push(title, event, message, priority=nma_priority, + batch_mode=batch) if not response[api][u'code'] == u'200': logger.error(u'Could not send notification to NotifyMyAndroid') @@ -461,9 +492,11 @@ class PUSHBULLET(object): data['device_iden'] = self.deviceid headers = {'Content-type': "application/json", - 'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY} + 'Authorization': 'Bearer ' + + headphones.CONFIG.PUSHBULLET_APIKEY} - response = request.request_json(url, method="post", headers=headers, data=json.dumps(data)) + response = request.request_json(url, method="post", headers=headers, + data=json.dumps(data)) if response: logger.info(u"PushBullet notifications sent.") @@ -492,7 +525,9 @@ class PUSHALOT(object): http_handler.request("POST", "/api/sendmessage", - headers={'Content-type': "application/x-www-form-urlencoded"}, + headers={ + 'Content-type': + "application/x-www-form-urlencoded"}, body=urlencode(data)) response = http_handler.getresponse() request_status = response.status @@ -512,6 +547,49 @@ class PUSHALOT(object): return False +class JOIN(object): + def __init__(self): + + self.enabled = headphones.CONFIG.JOIN_ENABLED + self.apikey = headphones.CONFIG.JOIN_APIKEY + self.deviceid = headphones.CONFIG.JOIN_DEVICEID + self.url = 'https://joinjoaomgcd.appspot.com/_ah/' \ + 'api/messaging/v1/sendPush?apikey={apikey}' \ + '&title={title}&text={text}' \ + '&icon={icon}' + + def notify(self, message, event): + if not headphones.CONFIG.JOIN_ENABLED or \ + not headphones.CONFIG.JOIN_APIKEY: + return + + icon = "https://cdn.rawgit.com/Headphones/" \ + "headphones/develop/data/images/headphoneslogo.png" + + if not self.deviceid: + self.deviceid = "group.all" + l = [x.strip() for x in self.deviceid.split(',')] + if len(l) > 1: + self.url += '&deviceIds={deviceid}' + else: + self.url += '&deviceId={deviceid}' + + response = urllib2.urlopen(self.url.format(apikey=self.apikey, + title=quote_plus(event), + text=quote_plus( + message.encode( + "utf-8")), + icon=icon, + deviceid=self.deviceid)) + + if response: + logger.info(u"Join notifications sent.") + return True + else: + logger.error(u"Join notification failed.") + return False + + class Synoindex(object): def __init__(self, util_loc='/usr/syno/bin/synoindex'): self.util_loc = util_loc @@ -524,7 +602,8 @@ class Synoindex(object): if not self.util_exists(): logger.warn( - "Error sending notification: synoindex utility not found at %s" % self.util_loc) + "Error sending notification: synoindex utility " + "not found at %s" % self.util_loc) return if os.path.isfile(path): @@ -533,16 +612,19 @@ class Synoindex(object): cmd_arg = '-A' else: logger.warn( - "Error sending notification: Path passed to synoindex was not a file or folder.") + "Error sending notification: Path passed to synoindex " + "was not a file or folder.") return cmd = [self.util_loc, cmd_arg, path] logger.info("Calling synoindex command: %s" % str(cmd)) try: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=headphones.PROG_DIR) out, error = p.communicate() - # synoindex never returns any codes other than '0', highly irritating + # synoindex never returns any codes other than '0', + # highly irritating except OSError, e: logger.warn("Error sending notification: %s" % str(e)) @@ -580,7 +662,8 @@ class PUSHOVER(object): headers = {'Content-type': "application/x-www-form-urlencoded"} - response = request.request_response(url, method="POST", headers=headers, data=data) + response = request.request_response(url, method="POST", + headers=headers, data=data) if response: logger.info(u"Pushover notifications sent.") @@ -614,20 +697,25 @@ class TwitterNotifier(object): def notify_snatch(self, title): if headphones.CONFIG.TWITTER_ONSNATCH: self._notifyTwitter( - common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now()) + common.notifyStrings[ + common.NOTIFY_SNATCH] + ': ' + title + ' at ' + + helpers.now()) def notify_download(self, title): if headphones.CONFIG.TWITTER_ENABLED: self._notifyTwitter(common.notifyStrings[ - common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now()) + common.NOTIFY_DOWNLOAD] + ': ' + + title + ' at ' + helpers.now()) def test_notify(self): return self._notifyTwitter( - "This is a test notification from Headphones at " + helpers.now(), force=True) + "This is a test notification from Headphones at " + helpers.now(), + force=True) def _get_authorization(self): - oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) + oauth_consumer = oauth.Consumer(key=self.consumer_key, + secret=self.consumer_secret) oauth_client = oauth.Client(oauth_consumer) logger.info('Requesting temp token from Twitter') @@ -635,32 +723,42 @@ class TwitterNotifier(object): resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET') if resp['status'] != '200': - logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status']) + logger.info( + 'Invalid respond from Twitter requesting temp token: %s' % + resp['status']) else: request_token = dict(parse_qsl(content)) headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token'] - headphones.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret'] + headphones.CONFIG.TWITTER_PASSWORD = request_token[ + 'oauth_token_secret'] - return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token'] + return self.AUTHORIZATION_URL + "?oauth_token=" + request_token[ + 'oauth_token'] def _get_credentials(self, key): request_token = {} request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME - request_token['oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD + request_token[ + 'oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD request_token['oauth_callback_confirmed'] = 'true' - token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) + token = oauth.Token(request_token['oauth_token'], + request_token['oauth_token_secret']) token.set_verifier(key) - logger.info('Generating and signing request for an access token using key ' + key) + logger.info( + 'Generating and signing request for an access token using key ' + + key) - oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) + oauth_consumer = oauth.Consumer(key=self.consumer_key, + secret=self.consumer_secret) logger.info('oauth_consumer: ' + str(oauth_consumer)) oauth_client = oauth.Client(oauth_consumer, token) logger.info('oauth_client: ' + str(oauth_client)) - resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', + resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, + method='POST', body='oauth_verifier=%s' % key) logger.info('resp, content: ' + str(resp) + ',' + str(content)) @@ -669,14 +767,18 @@ class TwitterNotifier(object): logger.info('resp[status] = ' + str(resp['status'])) if resp['status'] != '200': - logger.info('The request for a token with did not succeed: ' + str(resp['status']), + logger.info('The request for a token with did not succeed: ' + str( + resp['status']), logger.ERROR) return False else: - logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token']) - logger.info('Access Token secret: %s' % access_token['oauth_token_secret']) + logger.info('Your Twitter Access Token key: %s' % access_token[ + 'oauth_token']) + logger.info( + 'Access Token secret: %s' % access_token['oauth_token_secret']) headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token'] - headphones.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret'] + headphones.CONFIG.TWITTER_PASSWORD = access_token[ + 'oauth_token_secret'] return True def _send_tweet(self, message=None): @@ -688,7 +790,8 @@ class TwitterNotifier(object): logger.info(u"Sending tweet: " + message) - api = twitter.Api(username, password, access_token_key, access_token_secret) + api = twitter.Api(username, password, access_token_key, + access_token_secret) try: api.PostUpdate(message) @@ -741,7 +844,8 @@ class OSX_NOTIFY(object): ) NSUserNotification = self.objc.lookUpClass('NSUserNotification') - NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') + NSUserNotificationCenter = self.objc.lookUpClass( + 'NSUserNotificationCenter') NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool') if not NSUserNotification or not NSUserNotificationCenter: @@ -756,14 +860,17 @@ class OSX_NOTIFY(object): if text: notification.setInformativeText_(text) if sound: - notification.setSoundName_("NSUserNotificationDefaultSoundName") + notification.setSoundName_( + "NSUserNotificationDefaultSoundName") if image: - source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image) + source_img = self.AppKit.NSImage.alloc().\ + initByReferencingFile_(image) notification.setContentImage_(source_img) # notification.set_identityImage_(source_img) notification.setHasActionButton_(False) - notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() + notification_center = NSUserNotificationCenter.\ + defaultUserNotificationCenter() notification_center.deliverNotification_(notification) del pool @@ -784,7 +891,8 @@ class BOXCAR(object): def notify(self, title, message, rgid=None): try: if rgid: - message += '

MusicBrainz' % rgid + message += '

MusicBrainz' % rgid data = urllib.urlencode({ 'user_credentials': headphones.CONFIG.BOXCAR_TOKEN, @@ -820,8 +928,9 @@ class SubSonicNotifier(object): self.host = self.host + "/" # Invoke request - request.request_response(self.host + "musicFolderSettings.view?scanNow", - auth=(self.username, self.password)) + request.request_response( + self.host + "musicFolderSettings.view?scanNow", + auth=(self.username, self.password)) class Email(object): @@ -829,13 +938,15 @@ class Email(object): message = MIMEText(message, 'plain', "utf-8") message['Subject'] = subject - message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM)) + message['From'] = email.utils.formataddr( + ('Headphones', headphones.CONFIG.EMAIL_FROM)) message['To'] = headphones.CONFIG.EMAIL_TO try: if headphones.CONFIG.EMAIL_SSL: - mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER, - headphones.CONFIG.EMAIL_SMTP_PORT) + mailserver = smtplib.SMTP_SSL( + headphones.CONFIG.EMAIL_SMTP_SERVER, + headphones.CONFIG.EMAIL_SMTP_PORT) else: mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT) @@ -849,7 +960,8 @@ class Email(object): mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER, headphones.CONFIG.EMAIL_SMTP_PASSWORD) - mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO, + mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, + headphones.CONFIG.EMAIL_TO, message.as_string()) mailserver.quit() return True @@ -860,7 +972,6 @@ class Email(object): class TELEGRAM(object): - def notify(self, message, status): if not headphones.CONFIG.TELEGRAM_ENABLED: return @@ -878,14 +989,18 @@ class TELEGRAM(object): # Send message to user using Telegram's Bot API try: - response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload) + response = requests.post(TELEGRAM_API % (token, "sendMessage"), + data=payload) except Exception, e: logger.info(u'Telegram notify failed: ' + str(e)) # Error logging sent_successfuly = True if not response.status_code == 200: - logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text)) + logger.info( + u'Could not send notification to TelegramBot ' + u'(token=%s). Response: [%s]', + (token, response.text)) sent_successfuly = False logger.info(u"Telegram notifications sent.") @@ -893,7 +1008,6 @@ class TELEGRAM(object): class SLACK(object): - def notify(self, message, status): if not headphones.CONFIG.SLACK_ENABLED: return @@ -904,7 +1018,8 @@ class SLACK(object): channel = headphones.CONFIG.SLACK_CHANNEL emoji = headphones.CONFIG.SLACK_EMOJI - payload = {'channel': channel, 'text': status + ': ' + message, 'icon_emoji': emoji} + payload = {'channel': channel, 'text': status + ': ' + message, + 'icon_emoji': emoji} try: response = requests.post(SLACK_URL, json=payload) @@ -913,7 +1028,9 @@ class SLACK(object): sent_successfuly = True if not response.status_code == 200: - logger.info(u'Could not send notification to Slack. Response: [%s]', (response.text)) + logger.info( + u'Could not send notification to Slack. Response: [%s]', + (response.text)) sent_successfuly = False logger.info(u"Slack notifications sent.") diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 38e4bba5..ed3c136b 100755 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -574,6 +574,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, pushbullet = notifiers.PUSHBULLET() pushbullet.notify(pushmessage, statusmessage) + if headphones.CONFIG.JOIN_ENABLED: + logger.info(u"Join request") + join = notifiers.JOIN() + join.notify(pushmessage, statusmessage) + if headphones.CONFIG.TELEGRAM_ENABLED: logger.info(u"Telegram request") telegram = notifiers.TELEGRAM() diff --git a/headphones/searcher.py b/headphones/searcher.py index cb2ad014..e3c3f591 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -1077,6 +1077,10 @@ def send_to_downloader(data, bestqual, album): logger.info(u"Sending PushBullet notification") pushbullet = notifiers.PUSHBULLET() pushbullet.notify(name, "Download started") + if headphones.CONFIG.JOIN_ENABLED and headphones.CONFIG.JOIN_ONSNATCH: + logger.info(u"Sending Join notification") + join = notifiers.JOIN() + join.notify(name, "Download started") if headphones.CONFIG.SLACK_ENABLED and headphones.CONFIG.SLACK_ONSNATCH: logger.info(u"Sending Slack notification") slack = notifiers.SLACK() diff --git a/headphones/webserve.py b/headphones/webserve.py index bef5c315..6d95712a 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1412,7 +1412,11 @@ class WebInterface(object): "slack_url": headphones.CONFIG.SLACK_URL, "slack_channel": headphones.CONFIG.SLACK_CHANNEL, "slack_emoji": headphones.CONFIG.SLACK_EMOJI, - "slack_onsnatch": checked(headphones.CONFIG.SLACK_ONSNATCH) + "slack_onsnatch": checked(headphones.CONFIG.SLACK_ONSNATCH), + "join_enabled": checked(headphones.CONFIG.JOIN_ENABLED), + "join_onsnatch": checked(headphones.CONFIG.JOIN_ONSNATCH), + "join_apikey": headphones.CONFIG.JOIN_APIKEY, + "join_deviceid": headphones.CONFIG.JOIN_DEVICEID } for k, v in config.iteritems(): @@ -1480,7 +1484,8 @@ class WebInterface(object): "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled", "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", - "customauth", "idtag", "deluge_paused" + "customauth", "idtag", "deluge_paused", + "join_enabled", "join_onsnatch" ] for checked_config in checked_configs: if checked_config not in kwargs: @@ -1749,6 +1754,12 @@ class WebInterface(object): telegram = notifiers.TELEGRAM() telegram.notify("it works!", "lazers pew pew") + @cherrypy.expose + def testJoin(self): + logger.info("Testing Join notifications") + join = notifiers.JOIN() + join.notify("it works!", "Test message") + class Artwork(object): @cherrypy.expose