mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-10 23:42:37 -07:00
Manual merge v2 notifications encryption
This commit is contained in:
commit
59f12c71a9
11 changed files with 11616 additions and 84 deletions
|
@ -481,7 +481,7 @@ def dbcheck():
|
|||
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, '
|
||||
'session_key INTEGER, rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||
'user_id INTEGER, user TEXT, notifier_id INTEGER, agent_id INTEGER, agent_name TEXT, notify_action TEXT, '
|
||||
'subject_text TEXT, body_text TEXT, script_args TEXT)'
|
||||
'subject_text TEXT, body_text TEXT, script_args TEXT, success INTEGER DEFAULT 0)'
|
||||
)
|
||||
|
||||
# library_sections table :: This table keeps record of the servers library sections
|
||||
|
@ -958,6 +958,18 @@ def dbcheck():
|
|||
'ALTER TABLE notify_log ADD COLUMN notifier_id INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade notify_log table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT send_success FROM notify_log')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table notify_log.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE notify_log ADD COLUMN success INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE notify_log SET success = 1'
|
||||
)
|
||||
|
||||
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
|
||||
try:
|
||||
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()
|
||||
|
|
|
@ -1321,7 +1321,8 @@ class DataFactory(object):
|
|||
'notify_log.agent_name',
|
||||
'notify_log.notify_action',
|
||||
'notify_log.subject_text',
|
||||
'notify_log.body_text'
|
||||
'notify_log.body_text',
|
||||
'notify_log.success'
|
||||
]
|
||||
try:
|
||||
query = data_tables.ssp_query(table_name='notify_log',
|
||||
|
@ -1360,7 +1361,8 @@ class DataFactory(object):
|
|||
'agent_name': item['agent_name'],
|
||||
'notify_action': item['notify_action'],
|
||||
'subject_text': item['subject_text'],
|
||||
'body_text': body_text
|
||||
'body_text': body_text,
|
||||
'success': item['success']
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
|
|
|
@ -190,32 +190,43 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
|||
if not notifier_config:
|
||||
return
|
||||
|
||||
# Get the subject and body strings
|
||||
subject_string = notifier_config['notify_text'][notify_action]['subject']
|
||||
body_string = notifier_config['notify_text'][notify_action]['body']
|
||||
if notify_action == 'test':
|
||||
subject = kwargs.pop('subject', 'PlexPy')
|
||||
body = kwargs.pop('body', 'Test Notification')
|
||||
script_args = kwargs.pop('script_args', [])
|
||||
else:
|
||||
# Get the subject and body strings
|
||||
subject_string = notifier_config['notify_text'][notify_action]['subject']
|
||||
body_string = notifier_config['notify_text'][notify_action]['body']
|
||||
|
||||
# Format the subject and body strings
|
||||
subject, body, script_args = build_notify_text(subject=subject_string,
|
||||
body=body_string,
|
||||
notify_action=notify_action,
|
||||
parameters=parameters,
|
||||
agent_id=notifier_config['agent_id'])
|
||||
|
||||
# Send the notification
|
||||
notifiers.send_notification(notifier_id=notifier_config['id'],
|
||||
subject=subject,
|
||||
body=body,
|
||||
script_args=script_args,
|
||||
notify_action=notify_action,
|
||||
parameters=parameters)
|
||||
# Format the subject and body strings
|
||||
subject, body, script_args = build_notify_text(subject=subject_string,
|
||||
body=body_string,
|
||||
notify_action=notify_action,
|
||||
parameters=parameters,
|
||||
agent_id=notifier_config['agent_id'])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data or timeline_data,
|
||||
notify_action=notify_action,
|
||||
notifier=notifier_config,
|
||||
subject=subject,
|
||||
body=body,
|
||||
script_args=script_args)
|
||||
notification_id = set_notify_state(session=stream_data or timeline_data,
|
||||
notify_action=notify_action,
|
||||
notifier=notifier_config,
|
||||
subject=subject,
|
||||
body=body,
|
||||
script_args=script_args)
|
||||
|
||||
# Send the notification
|
||||
success = notifiers.send_notification(notifier_id=notifier_config['id'],
|
||||
subject=subject,
|
||||
body=body,
|
||||
script_args=script_args,
|
||||
notify_action=notify_action,
|
||||
notification_id=notification_id,
|
||||
parameters=parameters,
|
||||
**kwargs)
|
||||
|
||||
if success:
|
||||
set_notify_success(notification_id)
|
||||
return True
|
||||
|
||||
|
||||
def get_notify_state(session):
|
||||
|
@ -263,10 +274,19 @@ def set_notify_state(notify_action, notifier, subject, body, script_args, sessio
|
|||
'script_args': script_args}
|
||||
|
||||
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
|
||||
return monitor_db.last_insert_id()
|
||||
else:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to set notify state.")
|
||||
|
||||
|
||||
def set_notify_success(notification_id):
|
||||
keys = {'id': notification_id}
|
||||
values = {'success': 1}
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
|
||||
|
||||
|
||||
def build_media_notify_params(notify_action=None, session=None, timeline=None, **kwargs):
|
||||
# Get time formats
|
||||
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
||||
|
|
|
@ -35,6 +35,22 @@ import urllib2
|
|||
from urlparse import urlparse
|
||||
import uuid
|
||||
|
||||
try:
|
||||
from Cryptodome.Protocol.KDF import PBKDF2
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.Hash import HMAC, SHA1
|
||||
CRYPTODOME = True
|
||||
except ImportError:
|
||||
try:
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Random import get_random_bytes
|
||||
from Crypto.Hash import HMAC, SHA1
|
||||
CRYPTODOME = True
|
||||
except ImportError:
|
||||
CRYPTODOME = False
|
||||
|
||||
import gntp.notifier
|
||||
import facebook
|
||||
import twitter
|
||||
|
@ -49,6 +65,7 @@ import request
|
|||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||
from plexpy.helpers import checked
|
||||
|
||||
|
||||
AGENT_IDS = {'growl': 0,
|
||||
'prowl': 1,
|
||||
'xbmc': 2,
|
||||
|
@ -524,7 +541,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
|||
return False
|
||||
|
||||
|
||||
def send_notification(notifier_id=None, subject='', body='', notify_action='', **kwargs):
|
||||
def send_notification(notifier_id=None, subject='', body='', notify_action='', notification_id=None, **kwargs):
|
||||
notifier_config = get_notifier_config(notifier_id=notifier_id)
|
||||
if notifier_config:
|
||||
agent = get_agent_class(agent_id=notifier_config['agent_id'],
|
||||
|
@ -532,6 +549,7 @@ def send_notification(notifier_id=None, subject='', body='', notify_action='', *
|
|||
return agent.notify(subject=subject,
|
||||
body=body,
|
||||
action=notify_action.split('on_')[-1],
|
||||
notification_id=notification_id,
|
||||
**kwargs)
|
||||
else:
|
||||
logger.debug(u"PlexPy Notifiers :: Notification requested but no notifier_id received.")
|
||||
|
@ -671,12 +689,13 @@ class ANDROIDAPP(Notifier):
|
|||
"""
|
||||
PlexPy Android app notifications
|
||||
"""
|
||||
_DEFAULT_CONFIG = {'device_id': ''
|
||||
_DEFAULT_CONFIG = {'device_id': '',
|
||||
'priority': 3
|
||||
}
|
||||
|
||||
ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
|
||||
|
||||
def notify(self, subject='', body='', action='', **kwargs):
|
||||
def notify(self, subject='', body='', action='', notification_id=None, **kwargs):
|
||||
if not subject or not body:
|
||||
return
|
||||
|
||||
|
@ -685,25 +704,69 @@ class ANDROIDAPP(Notifier):
|
|||
logger.warn(u"PlexPy Notifiers :: Unable to send Android app notification: device not registered.")
|
||||
return
|
||||
|
||||
data = {'app_id': self.ONESIGNAL_APP_ID,
|
||||
'include_player_ids': [self.config['device_id']],
|
||||
'headings': {'en': subject.encode("utf-8")},
|
||||
'contents': {'en': body.encode("utf-8")}
|
||||
}
|
||||
plaintext_data = {'notification_id': notification_id,
|
||||
'subject': subject.encode("utf-8"),
|
||||
'body': body.encode("utf-8"),
|
||||
'priority': self.config['priority']}
|
||||
|
||||
http_handler = HTTPSConnection("onesignal.com")
|
||||
http_handler.request("POST",
|
||||
"/api/v1/notifications",
|
||||
headers={'Content-type': "application/json"},
|
||||
body=json.dumps(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
#logger.debug("Plaintext data: {}".format(plaintext_data))
|
||||
|
||||
if CRYPTODOME:
|
||||
# Key generation
|
||||
salt = get_random_bytes(16)
|
||||
passphrase = plexpy.CONFIG.API_KEY
|
||||
key_length = 32 # AES256
|
||||
iterations = 1000
|
||||
key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations,
|
||||
prf=lambda p, s: HMAC.new(p, s, SHA1).digest())
|
||||
|
||||
#logger.debug("Encryption key (base64): {}".format(base64.b64encode(key)))
|
||||
|
||||
# Encrypt using AES GCM
|
||||
nonce = get_random_bytes(16)
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce)
|
||||
encrypted_data, gcm_tag = cipher.encrypt_and_digest(json.dumps(plaintext_data))
|
||||
encrypted_data += gcm_tag
|
||||
|
||||
#logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data)))
|
||||
#logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag)))
|
||||
#logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
|
||||
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
|
||||
|
||||
payload = {'app_id': self.ONESIGNAL_APP_ID,
|
||||
'include_player_ids': [self.config['device_id']],
|
||||
'contents': {'en': 'PlexPy Notification'},
|
||||
'data': {'encrypted': True,
|
||||
'cipher_text': base64.b64encode(encrypted_data),
|
||||
'nonce': base64.b64encode(nonce),
|
||||
'salt': base64.b64encode(salt)}
|
||||
}
|
||||
else:
|
||||
logger.warn(u"PlexPy Notifiers :: PyCryptodome library is missing. "
|
||||
"Android app notifications will be sent unecrypted. "
|
||||
"Install the library to encrypt the notifications.")
|
||||
|
||||
payload = {'app_id': self.ONESIGNAL_APP_ID,
|
||||
'include_player_ids': [self.config['device_id']],
|
||||
'contents': {'en': 'PlexPy Notification'},
|
||||
'data': {'encrypted': False,
|
||||
'plain_text': plaintext_data}
|
||||
}
|
||||
|
||||
#logger.debug("OneSignal payload: {}".format(payload))
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
r = requests.post("https://onesignal.com/api/v1/notifications", headers=headers, json=payload)
|
||||
request_status = r.status_code
|
||||
|
||||
#logger.debug("OneSignal response: {}".format(r.content))
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"PlexPy Notifiers :: Android app notification sent.")
|
||||
return True
|
||||
elif request_status >= 400 and request_status < 500:
|
||||
logger.warn(u"PlexPy Notifiers :: Android app notification failed: [%s] %s" % (request_status, response.reason))
|
||||
logger.warn(u"PlexPy Notifiers :: Android app notification failed: [%s] %s" % (request_status, r.reason))
|
||||
return False
|
||||
else:
|
||||
logger.warn(u"PlexPy Notifiers :: Android app notification failed.")
|
||||
|
@ -729,27 +792,62 @@ class ANDROIDAPP(Notifier):
|
|||
return devices
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = []
|
||||
|
||||
if not CRYPTODOME:
|
||||
config_option.append({
|
||||
'label': 'Warning',
|
||||
'description': '<strong>The PyCryptodome library is missing. ' \
|
||||
'The content of your notifications will be sent unencrypted!</strong><br>' \
|
||||
'Please install the library to encrypt the notification contents. ' \
|
||||
'Instructions can be found in the ' \
|
||||
'<a href="' + helpers.anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)#notifications-pycryptodome' % plexpy.CONFIG.GIT_USER) + '" target="_blank">FAQ</a>.',
|
||||
'input_type': 'help'
|
||||
})
|
||||
else:
|
||||
config_option.append({
|
||||
'label': 'Note',
|
||||
'description': 'The PyCryptodome library was found. ' \
|
||||
'The content of your notifications will be sent encrypted!',
|
||||
'input_type': 'help'
|
||||
})
|
||||
|
||||
config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \
|
||||
'<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \
|
||||
'OneSignal</a> API. Some user data is collected and cannot be encrypted. ' \
|
||||
'Please read the <a href="' + helpers.anon_url('https://onesignal.com/privacy_policy') + '" target="_blank">' \
|
||||
'OneSignal Privacy Policy</a> for more details.'
|
||||
|
||||
devices = self.get_devices()
|
||||
|
||||
if not devices:
|
||||
devices_config = {'label': 'Device',
|
||||
'description': 'No devices registered. ' \
|
||||
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
|
||||
'style="cursor: pointer;">Get the Android App</a> and register a device.',
|
||||
'input_type': 'help'
|
||||
}
|
||||
config_option.append({
|
||||
'label': 'Device',
|
||||
'description': 'No devices registered. ' \
|
||||
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
|
||||
'style="cursor: pointer;">Get the Android App</a> and register a device.',
|
||||
'input_type': 'help'
|
||||
})
|
||||
else:
|
||||
devices_config = {'label': 'Device',
|
||||
'value': self.config['device_id'],
|
||||
'name': 'androidapp_device_id',
|
||||
'description': 'Set your Android app device or ' \
|
||||
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
|
||||
'style="cursor: pointer;">register a new device</a> with PlexPy.',
|
||||
'input_type': 'select',
|
||||
'select_options': devices
|
||||
}
|
||||
config_option.append({
|
||||
'label': 'Device',
|
||||
'value': self.config['device_id'],
|
||||
'name': 'androidapp_device_id',
|
||||
'description': 'Set your Android app device or ' \
|
||||
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
|
||||
'style="cursor: pointer;">register a new device</a> with PlexPy.',
|
||||
'input_type': 'select',
|
||||
'select_options': devices
|
||||
})
|
||||
|
||||
config_option = [devices_config]
|
||||
config_option.append({
|
||||
'label': 'Priority',
|
||||
'value': self.config['priority'],
|
||||
'name': 'androidapp_priority',
|
||||
'description': 'Set the notification priority.',
|
||||
'input_type': 'select',
|
||||
'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'}
|
||||
})
|
||||
|
||||
return config_option
|
||||
|
||||
|
@ -1006,23 +1104,23 @@ class DISCORD(Notifier):
|
|||
'description': 'Your Discord incoming webhook URL.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Discord Username',
|
||||
'value': self.config['username'],
|
||||
'name': 'discord_username',
|
||||
'description': 'The Discord username which will be used. Leave blank for webhook integration default.',
|
||||
'input_type': 'text'
|
||||
{'label': 'Discord Username',
|
||||
'value': self.config['username'],
|
||||
'name': 'discord_username',
|
||||
'description': 'The Discord username which will be used. Leave blank for webhook integration default.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Discord Avatar',
|
||||
'value': self.config['avatar_url'],
|
||||
'description': 'The image url for the avatar which will be used. Leave blank for webhook integration default.',
|
||||
'name': 'discord_avatar_url',
|
||||
'input_type': 'text'
|
||||
{'label': 'Discord Avatar',
|
||||
'value': self.config['avatar_url'],
|
||||
'description': 'The image url for the avatar which will be used. Leave blank for webhook integration default.',
|
||||
'name': 'discord_avatar_url',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Discord Color',
|
||||
'value': self.config['color'],
|
||||
'description': 'The hex color value (starting with \'#\') for the border along the left side of the message attachment.',
|
||||
'name': 'discord_color',
|
||||
'input_type': 'text'
|
||||
{'label': 'Discord Color',
|
||||
'value': self.config['color'],
|
||||
'description': 'The hex color value (starting with \'#\') for the border along the left side of the message attachment.',
|
||||
'name': 'discord_color',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'TTS',
|
||||
'value': self.config['tts'],
|
||||
|
@ -1843,7 +1941,7 @@ class NMA(Notifier):
|
|||
{'label': 'Priority',
|
||||
'value': self.config['priority'],
|
||||
'name': 'nma_priority',
|
||||
'description': 'Set the priority.',
|
||||
'description': 'Set the notification priority.',
|
||||
'input_type': 'select',
|
||||
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
|
||||
}
|
||||
|
@ -2104,7 +2202,7 @@ class PROWL(Notifier):
|
|||
{'label': 'Priority',
|
||||
'value': self.config['priority'],
|
||||
'name': 'prowl_priority',
|
||||
'description': 'Set the priority.',
|
||||
'description': 'Set the notification priority.',
|
||||
'input_type': 'select',
|
||||
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
|
||||
}
|
||||
|
@ -2354,7 +2452,7 @@ class PUSHOVER(Notifier):
|
|||
{'label': 'Priority',
|
||||
'value': self.config['priority'],
|
||||
'name': 'pushover_priority',
|
||||
'description': 'Set the priority.',
|
||||
'description': 'Set the notification priority.',
|
||||
'input_type': 'select',
|
||||
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
|
||||
},
|
||||
|
|
|
@ -3083,10 +3083,10 @@ class WebInterface(object):
|
|||
|
||||
if notifier:
|
||||
logger.debug(u"Sending %s%s notification." % (test, notifier['agent_name']))
|
||||
if notifiers.send_notification(notifier_id=notifier_id,
|
||||
if notification_handler.notify(notifier_id=notifier_id,
|
||||
notify_action=notify_action,
|
||||
subject=subject,
|
||||
body=body,
|
||||
notify_action=notify_action,
|
||||
**kwargs):
|
||||
return "Notification sent."
|
||||
else:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue