Update Facebook for provider link only

* Custom poster/metadata removed in Graph API v2.9
This commit is contained in:
JonnyWong16 2017-07-10 22:34:50 -07:00
parent 42a7ae36c2
commit c27c1379d0
3 changed files with 175 additions and 139 deletions

View file

@ -1,12 +1,13 @@
#!/usr/bin/env python
#
# Copyright 2010 Facebook
# Copyright 2015 Mobolic
#
# 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
# https://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
@ -19,8 +20,7 @@
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/.
https://developers.facebook.com/docs/graph-api.
"""
@ -33,9 +33,9 @@ import json
import re
try:
from urllib.parse import parse_qs, urlencode
from urllib.parse import parse_qs, urlencode, urlparse
except ImportError:
from urlparse import parse_qs
from urlparse import parse_qs, urlparse
from urllib import urlencode
from . import version
@ -43,15 +43,16 @@ from . import version
__version__ = version.__version__
VALID_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5"]
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?"
VALID_API_VERSIONS = ["2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9"]
VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"]
class GraphAPI(object):
"""A client for the Facebook Graph API.
See http://developers.facebook.com/docs/api for complete
documentation for the API.
https://developers.facebook.com/docs/graph-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.,
@ -65,11 +66,11 @@ class GraphAPI(object):
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/.
by the API at https://developers.facebook.com/docs/graph-api/reference/.
You can obtain an access token via OAuth or by using the Facebook
JavaScript SDK. See
http://developers.facebook.com/docs/authentication/ for details.
https://developers.facebook.com/docs/facebook-login 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
@ -78,13 +79,14 @@ class GraphAPI(object):
"""
def __init__(self, access_token=None, timeout=None, version=None,
proxies=None):
proxies=None, session=None):
# The default version is only used if the version kwarg does not exist.
default_version = "2.0"
default_version = VALID_API_VERSIONS[0]
self.access_token = access_token
self.timeout = timeout
self.proxies = proxies
self.session = session or requests.Session()
if version:
version_regex = re.compile("^\d\.\d$")
@ -101,9 +103,16 @@ class GraphAPI(object):
else:
self.version = "v" + default_version
def get_permissions(self, user_id):
"""Fetches the permissions object from the graph."""
response = self.request(
"{0}/{1}/permissions".format(self.version, user_id), {}
)["data"]
return {x["permission"] for x in response if x["status"] == "granted"}
def get_object(self, id, **args):
"""Fetches the given object from the graph."""
return self.request(self.version + "/" + id, args)
return self.request("{0}/{1}".format(self.version, id), args)
def get_objects(self, ids, **args):
"""Fetches all of the given object from the graph.
@ -114,10 +123,39 @@ class GraphAPI(object):
args["ids"] = ",".join(ids)
return self.request(self.version + "/", args)
def search(self, type, **args):
"""Fetches all objects of a given type from the graph.
Returns all objects of a given type from the graph as a dict.
"""
if type not in VALID_SEARCH_TYPES:
raise GraphAPIError('Valid types are: %s'
% ', '.join(VALID_SEARCH_TYPES))
args["type"] = type
return self.request(self.version + "/search/", 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)
"{0}/{1}/{2}".format(self.version, id, connection_name), args)
def get_all_connections(self, id, connection_name, **args):
"""Get all pages from a get_connections call
This will iterate over all pages returned by a get_connections call
and yield the individual items.
"""
while True:
page = self.get_connections(id, connection_name, **args)
for post in page['data']:
yield post
next = page.get('paging', {}).get('next')
if not next:
return
args = parse_qs(urlparse(next).query)
del args['access_token']
def put_object(self, parent_object, connection_name, **data):
"""Writes the given object to the graph, connected to the given parent.
@ -133,41 +171,17 @@ class GraphAPI(object):
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.
Certain operations require extended permissions. See
https://developers.facebook.com/docs/facebook-login/permissions
for details about permissions.
"""
assert self.access_token, "Write operations require an access token"
return self.request(
self.version + "/" + parent_object + "/" + connection_name,
"{0}/{1}/{2}".format(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)
@ -178,11 +192,11 @@ class GraphAPI(object):
def delete_object(self, id):
"""Deletes the object with the given ID from the graph."""
self.request(self.version + "/" + id, method="DELETE")
self.request("{0}/{1}".format(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")
self.request("{0}_{1}".format(request_id, user_id), method="DELETE")
def put_photo(self, image, album_path="me/photos", **kwargs):
"""
@ -193,7 +207,7 @@ class GraphAPI(object):
"""
return self.request(
self.version + "/" + album_path,
"{0}/{1}".format(self.version, album_path),
post_args=kwargs,
files={"source": image},
method="POST")
@ -202,12 +216,12 @@ class GraphAPI(object):
"""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)
response = self.session.request(
"GET",
FACEBOOK_GRAPH_URL + self.version + "/me",
params=args,
timeout=self.timeout,
proxies=self.proxies)
except requests.HTTPError as e:
response = json.loads(e.read())
raise GraphAPIError(response)
@ -228,26 +242,30 @@ class GraphAPI(object):
arguments.
"""
args = args or {}
if args is None:
args = dict()
if post_args is not None:
method = "POST"
# Add `access_token` to post_args or args if it has not already been
# included.
if self.access_token:
if post_args is not None:
# If post_args exists, we assume that args either does not exists
# or it does not need `access_token`.
if post_args and "access_token" not in post_args:
post_args["access_token"] = self.access_token
else:
elif "access_token" not in args:
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)
response = self.session.request(
method or "GET",
FACEBOOK_GRAPH_URL + 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)
@ -275,21 +293,23 @@ class GraphAPI(object):
raise GraphAPIError(result)
return result
def fql(self, query):
"""FQL query.
Example query: "SELECT affiliations FROM user WHERE uid = me()"
def get_app_access_token(self, app_id, app_secret, offline=False):
"""
return self.request(self.version + "/" + "fql", {"q": query})
Get the application's access token as a string.
If offline=True, use the concatenated app ID and secret
instead of making an API call.
<https://developers.facebook.com/docs/facebook-login/
access-tokens#apptokens>
"""
if offline:
return "{0}|{1}".format(app_id, app_secret)
else:
args = {'grant_type': 'client_credentials',
'client_id': app_id,
'client_secret': app_secret}
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"]
return self.request("{0}/oauth/access_token".format(self.version),
args=args)["access_token"]
def get_access_token_from_code(
self, code, redirect_uri, app_id, app_secret):
@ -305,13 +325,14 @@ class GraphAPI(object):
"client_id": app_id,
"client_secret": app_secret}
return self.request("oauth/access_token", args)
return self.request(
"{0}/oauth/access_token".format(self.version), args)
def extend_access_token(self, app_id, app_secret):
"""
Extends the expiration time of a valid OAuth access token. See
<https://developers.facebook.com/roadmap/offline-access-removal/
#extend_token>
<https://developers.facebook.com/docs/facebook-login/access-tokens/
expiration-and-extension>
"""
args = {
@ -321,13 +342,14 @@ class GraphAPI(object):
"fb_exchange_token": self.access_token,
}
return self.request("oauth/access_token", args=args)
return self.request("{0}/oauth/access_token".format(self.version),
args=args)
def debug_access_token(self, token, app_id, app_secret):
"""
Gets information about a user access token issued by an app. See
<https://developers.facebook.com/docs/facebook-login/access-tokens
#debug>
<https://developers.facebook.com/docs/facebook-login/
access-tokens/debugging-and-error-handling>
We can generate the app access token by concatenating the app
id and secret: <https://developers.facebook.com/docs/
@ -336,9 +358,9 @@ class GraphAPI(object):
"""
args = {
"input_token": token,
"access_token": "%s|%s" % (app_id, app_secret)
"access_token": "{0}|{1}".format(app_id, app_secret)
}
return self.request("/debug_token", args=args)
return self.request(self.version + "/" + "debug_token", args=args)
class GraphAPIError(Exception):
@ -382,10 +404,8 @@ def get_user_from_cookie(cookies, app_id, app_secret):
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/.
Read more about Facebook authentication at
https://developers.facebook.com/docs/facebook-login.
"""
cookie = cookies.get("fbsr_" + app_id, "")
@ -435,7 +455,7 @@ def parse_signed_request(signed_request, app_secret):
return False
# HMAC can only handle ascii (byte) strings
# http://bugs.python.org/issue5285
# https://bugs.python.org/issue5285
app_secret = app_secret.encode('ascii')
payload = payload.encode('ascii')
@ -449,7 +469,7 @@ def parse_signed_request(signed_request, app_secret):
def auth_url(app_id, canvas_url, perms=None, **kwargs):
url = "https://www.facebook.com/dialog/oauth?"
url = FACEBOOK_OAUTH_DIALOG_URL
kvps = {'client_id': app_id, 'redirect_uri': canvas_url}
if perms:
kvps['scope'] = ",".join(perms)

View file

@ -1,12 +1,12 @@
#!/usr/bin/env python
#
# Copyright 2014 Martey Dodoo
# Copyright 2015 Mobolic
#
# 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
# https://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
@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
__version__ = "1.0.0-alpha"
__version__ = "3.0.0-alpha"

View file

@ -603,14 +603,21 @@ class PrettyMetadata(object):
provider = 'Last.fm'
return provider
def get_provider_link(self):
def get_provider_link(self, provider=None):
provider_link = ''
if self.parameters['thetvdb_url']:
if provider:
if provider == 'plexweb':
provider_link = self.get_plex_url()
else:
provider_link = self.parameters.get(provider + '_url', '')
elif self.parameters['thetvdb_url']:
provider_link = self.parameters['thetvdb_url']
elif self.parameters['themoviedb_url']:
provider_link = self.parameters['themoviedb_url']
elif self.parameters['imdb_url']:
provider_link = self.parameters['imdb_url']
elif self.self.parameters['tvmaze_url']:
provider_link = self.parameters['tvmaze_url']
elif self.parameters['lastfm_url']:
provider_link = self.parameters['lastfm_url']
return provider_link
@ -1307,8 +1314,9 @@ class FACEBOOK(Notifier):
'group_id': '',
'incl_subject': 1,
'incl_card': 0,
'incl_description': 1,
'incl_pmslink': 0
'movie_provider': '',
'tv_provider': '',
'music_provider': ''
}
def _get_authorization(self, app_id='', app_secret='', redirect_uri=''):
@ -1356,12 +1364,12 @@ class FACEBOOK(Notifier):
return plexpy.CONFIG.FACEBOOK_TOKEN
def _post_facebook(self, message=None, attachment=None):
def _post_facebook(self, **data):
if self.config['group_id']:
api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.5')
try:
api.put_wall_post(profile_id=self.config['group_id'], message=message, attachment=attachment)
api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data)
logger.info(u"PlexPy Notifiers :: {name} notification sent.".format(name=self.NAME))
return True
except Exception as e:
@ -1376,41 +1384,29 @@ class FACEBOOK(Notifier):
if not subject or not body:
return
attachment = {}
if self.config['incl_subject']:
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
else:
text = body.encode("utf-8")
data = {'message': text}
if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata
pretty_metadata = PrettyMetadata(kwargs['parameters'])
media_type = pretty_metadata.media_type
poster_url = pretty_metadata.get_poster_url()
plex_url = pretty_metadata.get_plex_url()
provider_link = pretty_metadata.get_provider_link()
caption = pretty_metadata.get_caption()
title = pretty_metadata.get_title('\xc2\xb7'.decode('utf8'))
description = pretty_metadata.get_description()
# Build Facebook post attachment
if self.config['incl_pmslink']:
attachment['link'] = plex_url
attachment['caption'] = 'View on Plex Web'
elif provider_link:
attachment['link'] = provider_link
attachment['caption'] = caption
if pretty_metadata.media_type == 'movie':
provider = self.config['movie_provider']
elif pretty_metadata.media_type in ('show', 'season', 'episode'):
provider = self.config['tv_provider']
elif pretty_metadata.media_type in ('artist', 'album', 'track'):
provider = self.config['music_provider']
else:
attachment['link'] = poster_url
provider = None
data['link'] = pretty_metadata.get_provider_link(provider)
attachment['picture'] = poster_url
attachment['name'] = title
if self.config['incl_description'] or media_type in ('artist', 'album', 'track'):
attachment['description'] = description
else:
attachment['description'] = ' '
if self.config['incl_subject']:
return self._post_facebook(subject + '\r\n' + body, attachment=attachment)
else:
return self._post_facebook(body, attachment=attachment)
return self._post_facebook(**data)
def return_config_options(self):
config_option = [{'label': 'Instructions',
@ -1476,18 +1472,38 @@ class FACEBOOK(Notifier):
'description': 'Include an info card with a poster and metadata with the notifications.',
'input_type': 'checkbox'
},
{'label': 'Include Plot Summaries',
'value': self.config['incl_description'],
'name': 'facebook_incl_description',
'description': 'Include a plot summary for movies and TV shows on the info card.',
'input_type': 'checkbox'
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'facebook_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.',
'input_type': 'select',
'select_options': {'': '',
'trakt': 'Trakt.tv',
'plexweb': 'Plex Web'
}
},
{'label': 'Include Link to Plex Web',
'value': self.config['incl_pmslink'],
'name': 'facebook_incl_pmslink',
'description': 'Include a link to the media in Plex Web on the info card.<br>'
'If disabled, the link will go to IMDB, TVDB, TMDb, or Last.fm instead, if available.',
'input_type': 'checkbox'
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'facebook_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.',
'input_type': 'select',
'select_options': {'': '',
'thetvdb': 'TheTVDB',
'tvmaze': 'TVmaze',
'imdb': 'IMDB',
'trakt': 'Trakt.tv',
'plexweb': 'Plex Web'
}
},
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'facebook_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.',
'input_type': 'select',
'select_options': {'': '',
'lastfm': 'Last.fm',
'plexweb': 'Plex Web'
}
}
]