From 7befbef6ecff9c8b2f0abecd9532a008eb795795 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Thu, 31 Dec 2015 22:59:59 -0800 Subject: [PATCH] Add Facebook notification agent --- data/interfaces/default/css/plexpy.css | 4 +- .../default/notification_config.html | 41 +- data/interfaces/default/settings.html | 3 +- lib/pythonfacebook/__init__.py | 457 ++++++++++++++++++ lib/pythonfacebook/version.py | 17 + plexpy/config.py | 16 + plexpy/notifiers.py | 185 ++++++- plexpy/webserve.py | 19 +- 8 files changed, 713 insertions(+), 29 deletions(-) create mode 100644 lib/pythonfacebook/__init__.py create mode 100644 lib/pythonfacebook/version.py diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index 6aaec7f4..78f6b347 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -235,8 +235,8 @@ fieldset[disabled] .btn-bright:active, .btn-bright.disabled.active, .btn-bright[disabled].active, fieldset[disabled] .btn-bright.active { - background-color: #5cb85c; - border-color: #4cae4c; + background-color: #c9302c; + border-color: #ac2925; } .btn-bright .badge { color: #fff; diff --git a/data/interfaces/default/notification_config.html b/data/interfaces/default/notification_config.html index 9c41a9bd..10480b34 100644 --- a/data/interfaces/default/notification_config.html +++ b/data/interfaces/default/notification_config.html @@ -14,7 +14,12 @@ from plexpy import helpers
% for item in data: - % if item['input_type'] == 'text' or item['input_type'] == 'number' or item['input_type'] == 'password': + % if item['input_type'] == 'help': +
+ +

${item['description'] | n}

+
+ % elif item['input_type'] == 'text' or item['input_type'] == 'number' or item['input_type'] == 'password':
@@ -29,6 +34,7 @@ from plexpy import helpers
% elif item['input_type'] == 'button':
+
@@ -146,8 +152,7 @@ from plexpy import helpers $('#osxnotifyregister').click(function () { var osx_notify_app = $("#osx_notify_app").val(); - $.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { $('#ajaxMsg').html(" " + data); }); - $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); + $.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { showMsg(" " + data, false, true, 3000); }); }) $('#save-notification-item').click(function () { @@ -157,15 +162,37 @@ from plexpy import helpers return false; }); + function disableTwitterVerify() { + if ($('#twitter_key').val() != '') { $('#twitterStep2').prop('disabled', false); } + else { $('#twitterStep2').prop('disabled', true); } + } + disableTwitterVerify(); + $('#twitter_key').on('change', function () { + disableTwitterVerify() + }); + $('#twitterStep1').click(function () { $.get("/twitterStep1", function (data) {window.open(data); }) - .done(function () { $('#ajaxMsg').html(" Confirm Authorization. Check pop-up blocker if no response."); }); - $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); + .done(function () { showMsg(" Confirm Authorization. Check pop-up blocker if no response.", false, true, 3000); }); }); $('#twitterStep2').click(function () { var twitter_key = $("#twitter_key").val(); - $.get("/twitterStep2", { 'key': twitter_key }, function (data) { $('#ajaxMsg').html(" " + data); }); - $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); + $.get("/twitterStep2", { 'key': twitter_key }, function (data) { showMsg(" " + data, false, true, 3000); }); + }); + + function disableFacebookRequest() { + if ($('#facebook_app_id').val() != '' && $('#facebook_app_secret').val() != '') { $('#facebookStep1').prop('disabled', false); } + else { $('#facebookStep1').prop('disabled', true); } + } + disableFacebookRequest(); + $('#facebook_app_id, #facebook_app_secret').on('change', function () { + disableFacebookRequest() + }); + + $('#facebookStep1').click(function () { + doAjaxCall('set_notification_config', $(this), 'tabs', true); + $.get("/facebookStep1", function (data) {window.open(data); }) + .done(function () { showMsg(" Confirm Authorization. Check pop-up blocker if no response.", false, true, 3000); }); }); $('#test_notifier').click(function () { diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index c4dbe142..adcdd590 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -1563,8 +1563,7 @@ $(document).ready(function() { $('#osxnotifyregister').click(function () { var osx_notify_app = $("#osx_notify_reg").val(); - $.get("/osxnotifyregister", {'app': osx_notify_app}, function (data) { $('#ajaxMsg').html("
"+data+"
"); }); - $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut() + $.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { showMsg("
" + data + "
", false, true, 3000); }); }) $.ajax({ diff --git a/lib/pythonfacebook/__init__.py b/lib/pythonfacebook/__init__.py new file mode 100644 index 00000000..cfadcbbb --- /dev/null +++ b/lib/pythonfacebook/__init__.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python +# +# Copyright 2010 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Python client library for the Facebook Platform. + +This client library is designed to support the Graph API and the +official Facebook JavaScript SDK, which is the canonical way to +implement Facebook authentication. Read more about the Graph API at +http://developers.facebook.com/docs/api. You can download the Facebook +JavaScript SDK at http://github.com/facebook/connect-js/. + +""" + +import hashlib +import hmac +import binascii +import base64 +import requests +import json +import re + +try: + from urllib.parse import parse_qs, urlencode +except ImportError: + from urlparse import parse_qs + from urllib import urlencode + +from . import version + + +__version__ = version.__version__ + + +VALID_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5"] + + +class GraphAPI(object): + """A client for the Facebook Graph API. + + See http://developers.facebook.com/docs/api for complete + documentation for the API. + + The Graph API is made up of the objects in Facebook (e.g., people, + pages, events, photos) and the connections between them (e.g., + friends, photo tags, and event RSVPs). This client provides access + to those primitive types in a generic way. For example, given an + OAuth access token, this will fetch the profile of the active user + and the list of the user's friends: + + graph = facebook.GraphAPI(access_token) + user = graph.get_object("me") + friends = graph.get_connections(user["id"], "friends") + + You can see a list of all of the objects and connections supported + by the API at http://developers.facebook.com/docs/reference/api/. + + You can obtain an access token via OAuth or by using the Facebook + JavaScript SDK. See + http://developers.facebook.com/docs/authentication/ for details. + + If you are using the JavaScript SDK, you can use the + get_user_from_cookie() method below to get the OAuth access token + for the active user from the cookie saved by the SDK. + + """ + + def __init__(self, access_token=None, timeout=None, version=None, + proxies=None): + # The default version is only used if the version kwarg does not exist. + default_version = "2.0" + + self.access_token = access_token + self.timeout = timeout + self.proxies = proxies + + if version: + version_regex = re.compile("^\d\.\d$") + match = version_regex.search(str(version)) + if match is not None: + if str(version) not in VALID_API_VERSIONS: + raise GraphAPIError("Valid API versions are " + + str(VALID_API_VERSIONS).strip('[]')) + else: + self.version = "v" + str(version) + else: + raise GraphAPIError("Version number should be in the" + " following format: #.# (e.g. 2.0).") + else: + self.version = "v" + default_version + + def get_object(self, id, **args): + """Fetches the given object from the graph.""" + return self.request(self.version + "/" + id, args) + + def get_objects(self, ids, **args): + """Fetches all of the given object from the graph. + + We return a map from ID to object. If any of the IDs are + invalid, we raise an exception. + """ + args["ids"] = ",".join(ids) + return self.request(self.version + "/", args) + + def get_connections(self, id, connection_name, **args): + """Fetches the connections for given object.""" + return self.request( + "%s/%s/%s" % (self.version, id, connection_name), args) + + def put_object(self, parent_object, connection_name, **data): + """Writes the given object to the graph, connected to the given parent. + + For example, + + graph.put_object("me", "feed", message="Hello, world") + + writes "Hello, world" to the active user's wall. Likewise, this + will comment on the first post of the active user's feed: + + feed = graph.get_connections("me", "feed") + post = feed["data"][0] + graph.put_object(post["id"], "comments", message="First!") + + See http://developers.facebook.com/docs/api#publishing for all + of the supported writeable objects. + + Certain write operations require extended permissions. For + example, publishing to a user's feed requires the + "publish_actions" permission. See + http://developers.facebook.com/docs/publishing/ for details + about publishing permissions. + + """ + assert self.access_token, "Write operations require an access token" + return self.request( + self.version + "/" + parent_object + "/" + connection_name, + post_args=data, + method="POST") + + def put_wall_post(self, message, attachment={}, profile_id="me"): + """Writes a wall post to the given profile's wall. + + We default to writing to the authenticated user's wall if no + profile_id is specified. + + attachment adds a structured attachment to the status message + being posted to the Wall. It should be a dictionary of the form: + + {"name": "Link name" + "link": "http://www.example.com/", + "caption": "{*actor*} posted a new review", + "description": "This is a longer description of the attachment", + "picture": "http://www.example.com/thumbnail.jpg"} + + """ + return self.put_object(profile_id, "feed", message=message, + **attachment) + + def put_comment(self, object_id, message): + """Writes the given comment on the given post.""" + return self.put_object(object_id, "comments", message=message) + + def put_like(self, object_id): + """Likes the given post.""" + return self.put_object(object_id, "likes") + + def delete_object(self, id): + """Deletes the object with the given ID from the graph.""" + self.request(self.version + "/" + id, method="DELETE") + + def delete_request(self, user_id, request_id): + """Deletes the Request with the given ID for the given user.""" + self.request("%s_%s" % (request_id, user_id), method="DELETE") + + def put_photo(self, image, album_path="me/photos", **kwargs): + """ + Upload an image using multipart/form-data. + + image - A file object representing the image to be uploaded. + album_path - A path representing where the image should be uploaded. + + """ + return self.request( + self.version + "/" + album_path, + post_args=kwargs, + files={"source": image}, + method="POST") + + def get_version(self): + """Fetches the current version number of the Graph API being used.""" + args = {"access_token": self.access_token} + try: + response = requests.request("GET", + "https://graph.facebook.com/" + + self.version + "/me", + params=args, + timeout=self.timeout, + proxies=self.proxies) + except requests.HTTPError as e: + response = json.loads(e.read()) + raise GraphAPIError(response) + + try: + headers = response.headers + version = headers["facebook-api-version"].replace("v", "") + return float(version) + except Exception: + raise GraphAPIError("API version number not available") + + def request( + self, path, args=None, post_args=None, files=None, method=None): + """Fetches the given path in the Graph API. + + We translate args to a valid query string. If post_args is + given, we send a POST request to the given path with the given + arguments. + + """ + args = args or {} + + if post_args is not None: + method = "POST" + + if self.access_token: + if post_args is not None: + post_args["access_token"] = self.access_token + else: + args["access_token"] = self.access_token + + try: + response = requests.request(method or "GET", + "https://graph.facebook.com/" + + path, + timeout=self.timeout, + params=args, + data=post_args, + proxies=self.proxies, + files=files) + except requests.HTTPError as e: + response = json.loads(e.read()) + raise GraphAPIError(response) + + headers = response.headers + if 'json' in headers['content-type']: + result = response.json() + elif 'image/' in headers['content-type']: + mimetype = headers['content-type'] + result = {"data": response.content, + "mime-type": mimetype, + "url": response.url} + elif "access_token" in parse_qs(response.text): + query_str = parse_qs(response.text) + if "access_token" in query_str: + result = {"access_token": query_str["access_token"][0]} + if "expires" in query_str: + result["expires"] = query_str["expires"][0] + else: + raise GraphAPIError(response.json()) + else: + raise GraphAPIError('Maintype was not text, image, or querystring') + + if result and isinstance(result, dict) and result.get("error"): + raise GraphAPIError(result) + return result + + def fql(self, query): + """FQL query. + + Example query: "SELECT affiliations FROM user WHERE uid = me()" + + """ + return self.request(self.version + "/" + "fql", {"q": query}) + + def get_app_access_token(self, app_id, app_secret): + """Get the application's access token as a string.""" + args = {'grant_type': 'client_credentials', + 'client_id': app_id, + 'client_secret': app_secret} + + return self.request("oauth/access_token", args=args)["access_token"] + + def get_access_token_from_code( + self, code, redirect_uri, app_id, app_secret): + """Get an access token from the "code" returned from an OAuth dialog. + + Returns a dict containing the user-specific access token and its + expiration date (if applicable). + + """ + args = { + "code": code, + "redirect_uri": redirect_uri, + "client_id": app_id, + "client_secret": app_secret} + + return self.request("oauth/access_token", args) + + def extend_access_token(self, app_id, app_secret): + """ + Extends the expiration time of a valid OAuth access token. See + + + """ + args = { + "client_id": app_id, + "client_secret": app_secret, + "grant_type": "fb_exchange_token", + "fb_exchange_token": self.access_token, + } + + return self.request("oauth/access_token", args=args) + + def debug_access_token(self, token, app_id, app_secret): + """ + Gets information about a user access token issued by an app. See + + + We can generate the app access token by concatenating the app + id and secret: + + """ + args = { + "input_token": token, + "access_token": "%s|%s" % (app_id, app_secret) + } + return self.request("/debug_token", args=args) + + +class GraphAPIError(Exception): + def __init__(self, result): + self.result = result + self.code = None + try: + self.type = result["error_code"] + except: + self.type = "" + + # OAuth 2.0 Draft 10 + try: + self.message = result["error_description"] + except: + # OAuth 2.0 Draft 00 + try: + self.message = result["error"]["message"] + self.code = result["error"].get("code") + if not self.type: + self.type = result["error"].get("type", "") + except: + # REST server style + try: + self.message = result["error_msg"] + except: + self.message = result + + Exception.__init__(self, self.message) + + +def get_user_from_cookie(cookies, app_id, app_secret): + """Parses the cookie set by the official Facebook JavaScript SDK. + + cookies should be a dictionary-like object mapping cookie names to + cookie values. + + If the user is logged in via Facebook, we return a dictionary with + the keys "uid" and "access_token". The former is the user's + Facebook ID, and the latter can be used to make authenticated + requests to the Graph API. If the user is not logged in, we + return None. + + Download the official Facebook JavaScript SDK at + http://github.com/facebook/connect-js/. Read more about Facebook + authentication at + http://developers.facebook.com/docs/authentication/. + + """ + cookie = cookies.get("fbsr_" + app_id, "") + if not cookie: + return None + parsed_request = parse_signed_request(cookie, app_secret) + if not parsed_request: + return None + try: + result = GraphAPI().get_access_token_from_code( + parsed_request["code"], "", app_id, app_secret) + except GraphAPIError: + return None + result["uid"] = parsed_request["user_id"] + return result + + +def parse_signed_request(signed_request, app_secret): + """ Return dictionary with signed request data. + + We return a dictionary containing the information in the + signed_request. This includes a user_id if the user has authorised + your application, as well as any information requested. + + If the signed_request is malformed or corrupted, False is returned. + + """ + try: + encoded_sig, payload = map(str, signed_request.split('.', 1)) + + sig = base64.urlsafe_b64decode(encoded_sig + "=" * + ((4 - len(encoded_sig) % 4) % 4)) + data = base64.urlsafe_b64decode(payload + "=" * + ((4 - len(payload) % 4) % 4)) + except IndexError: + # Signed request was malformed. + return False + except TypeError: + # Signed request had a corrupted payload. + return False + except binascii.Error: + # Signed request had a corrupted payload. + return False + + data = json.loads(data.decode('ascii')) + if data.get('algorithm', '').upper() != 'HMAC-SHA256': + return False + + # HMAC can only handle ascii (byte) strings + # http://bugs.python.org/issue5285 + app_secret = app_secret.encode('ascii') + payload = payload.encode('ascii') + + expected_sig = hmac.new(app_secret, + msg=payload, + digestmod=hashlib.sha256).digest() + if sig != expected_sig: + return False + + return data + + +def auth_url(app_id, canvas_url, perms=None, **kwargs): + url = "https://www.facebook.com/dialog/oauth?" + kvps = {'client_id': app_id, 'redirect_uri': canvas_url} + if perms: + kvps['scope'] = ",".join(perms) + kvps.update(kwargs) + return url + urlencode(kvps) diff --git a/lib/pythonfacebook/version.py b/lib/pythonfacebook/version.py new file mode 100644 index 00000000..1ff2e07a --- /dev/null +++ b/lib/pythonfacebook/version.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# +# Copyright 2014 Martey Dodoo +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +__version__ = "1.0.0-alpha" diff --git a/plexpy/config.py b/plexpy/config.py index d899f583..f5afd041 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -79,6 +79,22 @@ _CONFIG_DEFINITIONS = { 'EMAIL_ON_EXTUP': (int, 'Email', 0), 'EMAIL_ON_INTUP': (int, 'Email', 0), 'ENABLE_HTTPS': (int, 'General', 0), + 'FACEBOOK_ENABLED': (int, 'Facebook', 0), + 'FACEBOOK_APP_ID': (str, 'Facebook', ''), + 'FACEBOOK_APP_SECRET': (str, 'Facebook', ''), + 'FACEBOOK_TOKEN': (str, 'Facebook', ''), + 'FACEBOOK_GROUP': (str, 'Facebook', ''), + 'FACEBOOK_ON_PLAY': (int, 'Facebook', 0), + 'FACEBOOK_ON_STOP': (int, 'Facebook', 0), + 'FACEBOOK_ON_PAUSE': (int, 'Facebook', 0), + 'FACEBOOK_ON_RESUME': (int, 'Facebook', 0), + 'FACEBOOK_ON_BUFFER': (int, 'Facebook', 0), + 'FACEBOOK_ON_WATCHED': (int, 'Facebook', 0), + 'FACEBOOK_ON_CREATED': (int, 'Facebook', 0), + 'FACEBOOK_ON_EXTDOWN': (int, 'Facebook', 0), + 'FACEBOOK_ON_INTDOWN': (int, 'Facebook', 0), + 'FACEBOOK_ON_EXTUP': (int, 'Facebook', 0), + 'FACEBOOK_ON_INTUP': (int, 'Facebook', 0), 'FIRST_RUN_COMPLETE': (int, 'General', 0), 'FREEZE_DB': (int, 'General', 0), 'GIT_BRANCH': (str, 'General', 'master'), diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index 221e11f2..1cbf2a35 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -34,6 +34,7 @@ from pynma import pynma import gntp.notifier import oauth2 as oauth import pythontwitter as twitter +import pythonfacebook as facebook import plexpy from plexpy import logger, helpers, request @@ -54,9 +55,10 @@ AGENT_IDS = {"Growl": 0, "IFTTT": 12, "Telegram": 13, "Slack": 14, - "Scripts": 15} - + "Scripts": 15, + "Facebook": 16} + def available_notification_agents(): agents = [{'name': 'Growl', 'id': AGENT_IDS['Growl'], @@ -312,8 +314,24 @@ def available_notification_agents(): 'on_extup': plexpy.CONFIG.SCRIPTS_ON_EXTUP, 'on_intdown': plexpy.CONFIG.SCRIPTS_ON_INTDOWN, 'on_intup': plexpy.CONFIG.SCRIPTS_ON_INTUP - } - + }, + {'name': 'Facebook', + 'id': AGENT_IDS['Facebook'], + 'config_prefix': 'facebook', + 'has_config': True, + 'state': checked(plexpy.CONFIG.FACEBOOK_ENABLED), + 'on_play': plexpy.CONFIG.FACEBOOK_ON_PLAY, + 'on_stop': plexpy.CONFIG.FACEBOOK_ON_STOP, + 'on_pause': plexpy.CONFIG.FACEBOOK_ON_PAUSE, + 'on_resume': plexpy.CONFIG.FACEBOOK_ON_RESUME, + 'on_buffer': plexpy.CONFIG.FACEBOOK_ON_BUFFER, + 'on_watched': plexpy.CONFIG.FACEBOOK_ON_WATCHED, + 'on_created': plexpy.CONFIG.FACEBOOK_ON_CREATED, + 'on_extdown': plexpy.CONFIG.FACEBOOK_ON_EXTDOWN, + 'on_intdown': plexpy.CONFIG.FACEBOOK_ON_INTDOWN, + 'on_extup': plexpy.CONFIG.FACEBOOK_ON_EXTUP, + 'on_intup': plexpy.CONFIG.FACEBOOK_ON_INTUP + } ] # OSX Notifications should only be visible if it can be used @@ -341,7 +359,7 @@ def available_notification_agents(): def get_notification_agent_config(config_id): - if config_id: + if str(config_id).isdigit(): config_id = int(config_id) if config_id == 0: @@ -392,6 +410,9 @@ def get_notification_agent_config(config_id): elif config_id == 15: script = Scripts() return script.return_config_options() + elif config_id == 16: + facebook = FacebookNotifier() + return facebook.return_config_options() else: return [] else: @@ -450,11 +471,15 @@ def send_notification(config_id, subject, body, **kwargs): elif config_id == 15: scripts = Scripts() scripts.notify(message=body, subject=subject, **kwargs) + elif config_id == 16: + facebook = FacebookNotifier() + facebook.notify(subject=subject, message=body) else: logger.debug(u"PlexPy Notifier :: Unknown agent id received.") else: logger.debug(u"PlexPy Notifier :: Notification requested but no agent id received.") + class GROWL(object): """ Growl notifications, for OS X. @@ -1181,14 +1206,14 @@ class TwitterNotifier(object): oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) # logger.debug('oauth_consumer: ' + str(oauth_consumer)) oauth_client = oauth.Client(oauth_consumer, token) - logger.info('oauth_client: ' + str(oauth_client)) + # logger.info('oauth_client: ' + str(oauth_client)) resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key) - logger.info('resp, content: ' + str(resp) + ',' + str(content)) + # logger.info('resp, content: ' + str(resp) + ',' + str(content)) access_token = dict(parse_qsl(content)) - logger.info('access_token: ' + str(access_token)) + # logger.info('access_token: ' + str(access_token)) - logger.info('resp[status] = ' + str(resp['status'])) + # 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.ERROR) return False @@ -1197,6 +1222,7 @@ class TwitterNotifier(object): logger.info('Access Token secret: %s' % access_token['oauth_token_secret']) plexpy.CONFIG.TWITTER_USERNAME = access_token['oauth_token'] plexpy.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret'] + plexpy.CONFIG.write() return True def _send_tweet(self, message=None): @@ -1205,35 +1231,42 @@ class TwitterNotifier(object): access_token_key = plexpy.CONFIG.TWITTER_USERNAME access_token_secret = plexpy.CONFIG.TWITTER_PASSWORD - logger.info(u"Sending tweet: " + message) + # logger.info(u"Sending tweet: " + message) api = twitter.Api(username, password, access_token_key, access_token_secret) try: api.PostUpdate(message) + logger.info(u"Twitter notifications sent.") except Exception as e: - logger.info(u"Error Sending Tweet: %s" % e) + logger.info(u"Error sending Tweet: %s" % e) return False return True def return_config_options(self): - config_option = [{'label': 'Request Authorisation', - 'value': 'Request Authorisation', + config_option = [{'label': 'Instructions', + 'description': 'Step 1: Click the Request Authorization button below.
\ + Step 2: Input the Authorization Key you received from Step 1 below.
\ + Step 3: Click the Verify Key button below.', + 'input_type': 'help' + }, + {'label': 'Request Authorization', + 'value': 'Request Authorization', 'name': 'twitterStep1', - 'description': 'Step 1: Click Request button above. (Ensure you allow the browser pop-up).', + 'description': 'Request Twitter authorization. (Ensure you allow the browser pop-up).', 'input_type': 'button' }, - {'label': 'Authorisation Key', + {'label': 'Authorization Key', 'value': '', 'name': 'twitter_key', - 'description': 'Step 2: Input the authorisation key you received from Step 1.', + 'description': 'Your Twitter authorization key.', 'input_type': 'text' }, {'label': 'Verify Key', 'value': 'Verify Key', 'name': 'twitterStep2', - 'description': 'Step 3: Verify the key.', + 'description': 'Verify your Twitter authorization key.', 'input_type': 'button' }, {'input_type': 'nosave' @@ -1635,6 +1668,7 @@ class TELEGRAM(object): return config_option + class SLACK(object): """ Slack Notifications @@ -1958,3 +1992,120 @@ class Scripts(object): ] return config_option + + +class FacebookNotifier(object): + + def __init__(self): + self.app_id = plexpy.CONFIG.FACEBOOK_APP_ID + self.app_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET + self.group_id = plexpy.CONFIG.FACEBOOK_GROUP + + if plexpy.CONFIG.ENABLE_HTTPS: + protocol = 'https' + else: + protocol = 'http' + + if plexpy.CONFIG.HTTP_HOST == '0.0.0.0': + host = 'localhost' + else: + host = plexpy.CONFIG.HTTP_HOST + + self.redirect_url = '%s://%s:%i/facebookStep2' % (protocol, host, plexpy.CONFIG.HTTP_PORT) + + + def notify(self, subject, message): + if not subject or not message: + return + else: + self._post_facebook(subject + ': ' + message) + + def test_notify(self): + return self._post_facebook("This is a test notification from PlexPy at " + helpers.now()) + + def _get_authorization(self): + return facebook.auth_url(app_id=self.app_id, + canvas_url=self.redirect_url, + perms=['user_managed_groups','publish_actions']) + + def _get_credentials(self, code): + logger.info('Requesting access token from Facebook') + + try: + # Request user access token + api = facebook.GraphAPI(version='2.5') + response = api.get_access_token_from_code(code=code, + redirect_uri=self.redirect_url, + app_id=self.app_id, + app_secret=self.app_secret) + access_token = response['access_token'] + + # Request extended user access token + api = facebook.GraphAPI(access_token=access_token, version='2.5') + response = api.extend_access_token(app_id=self.app_id, + app_secret=self.app_secret) + access_token = response['access_token'] + + plexpy.CONFIG.FACEBOOK_TOKEN = access_token + plexpy.CONFIG.write() + except Exception as e: + logger.info(u"Error requesting Facebook access token: %s" % e) + return False + + return True + + def _post_facebook(self, message=None): + access_token = plexpy.CONFIG.FACEBOOK_TOKEN + group_id = plexpy.CONFIG.FACEBOOK_GROUP + + if group_id: + api = facebook.GraphAPI(access_token=access_token, version='2.5') + + try: + api.put_wall_post(profile_id=group_id, message=message) + logger.info(u"Facebook notifications sent.") + except Exception as e: + logger.info(u"Error sending Facebook post: %s" % e) + return False + + return True + else: + logger.info('Error sending Facebook post: No Facebook Group ID provided.') + return False + + def return_config_options(self): + config_option = [{'label': 'Instructions', + 'description': 'Facebook notifications are experimental!

\ + Step 1: Visit Facebook Developers to create a new app using advanced setup.
\ + Step 2: Go to Settings > Advanced and fill in Valid OAuth redirect URIs with your PlexPy URL (i.e. http://localhost:8181).
\ + Step 3: Fill in the App ID and App Secret below.
\ + Step 4: Click the Request Authorization button below.', + 'input_type': 'help' + }, + {'label': 'Facebook App ID', + 'value': self.app_id, + 'name': 'facebook_app_id', + 'description': 'Your Facebook app ID.', + 'input_type': 'text' + }, + {'label': 'Facebook App Secret', + 'value': self.app_secret, + 'name': 'facebook_app_secret', + 'description': 'Your Facebook app secret.', + 'input_type': 'text' + }, + {'label': 'Request Authorization', + 'value': 'Request Authorization', + 'name': 'facebookStep1', + 'description': 'Request Facebook authorization. (Ensure you allow the browser pop-up).', + 'input_type': 'button' + }, + {'label': 'Facebook Group ID', + 'value': self.group_id, + 'name': 'facebook_group', + 'description': 'Your Facebook Group ID.', + 'input_type': 'text' + } + ] + + return config_option diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 412f2767..79ba325a 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -695,12 +695,29 @@ class WebInterface(object): cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" tweet = notifiers.TwitterNotifier() result = tweet._get_credentials(key) - logger.info(u"result: " + str(result)) + # logger.info(u"result: " + str(result)) if result: return "Key verification successful" else: return "Unable to verify key" + @cherrypy.expose + def facebookStep1(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + facebook = notifiers.FacebookNotifier() + return facebook._get_authorization() + + @cherrypy.expose + def facebookStep2(self, code): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + facebook = notifiers.FacebookNotifier() + result = facebook._get_credentials(code) + # logger.info(u"result: " + str(result)) + if result: + return "Key verification successful, you may close this page now." + else: + return "Unable to verify key" + @cherrypy.expose def osxnotifyregister(self, app): cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"