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 #!/usr/bin/env python
# #
# Copyright 2010 Facebook # Copyright 2010 Facebook
# Copyright 2015 Mobolic
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
# a copy of the License at # 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 # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 This client library is designed to support the Graph API and the
official Facebook JavaScript SDK, which is the canonical way to official Facebook JavaScript SDK, which is the canonical way to
implement Facebook authentication. Read more about the Graph API at implement Facebook authentication. Read more about the Graph API at
http://developers.facebook.com/docs/api. You can download the Facebook https://developers.facebook.com/docs/graph-api.
JavaScript SDK at http://github.com/facebook/connect-js/.
""" """
@ -33,9 +33,9 @@ import json
import re import re
try: try:
from urllib.parse import parse_qs, urlencode from urllib.parse import parse_qs, urlencode, urlparse
except ImportError: except ImportError:
from urlparse import parse_qs from urlparse import parse_qs, urlparse
from urllib import urlencode from urllib import urlencode
from . import version from . import version
@ -43,15 +43,16 @@ from . import version
__version__ = version.__version__ __version__ = version.__version__
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
VALID_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5"] 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): class GraphAPI(object):
"""A client for the Facebook Graph API. """A client for the Facebook Graph API.
See http://developers.facebook.com/docs/api for complete https://developers.facebook.com/docs/graph-api
documentation for the API.
The Graph API is made up of the objects in Facebook (e.g., people, The Graph API is made up of the objects in Facebook (e.g., people,
pages, events, photos) and the connections between them (e.g., pages, events, photos) and the connections between them (e.g.,
@ -65,11 +66,11 @@ class GraphAPI(object):
friends = graph.get_connections(user["id"], "friends") friends = graph.get_connections(user["id"], "friends")
You can see a list of all of the objects and connections supported 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 You can obtain an access token via OAuth or by using the Facebook
JavaScript SDK. See 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 If you are using the JavaScript SDK, you can use the
get_user_from_cookie() method below to get the OAuth access token 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, 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. # 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.access_token = access_token
self.timeout = timeout self.timeout = timeout
self.proxies = proxies self.proxies = proxies
self.session = session or requests.Session()
if version: if version:
version_regex = re.compile("^\d\.\d$") version_regex = re.compile("^\d\.\d$")
@ -101,9 +103,16 @@ class GraphAPI(object):
else: else:
self.version = "v" + default_version 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): def get_object(self, id, **args):
"""Fetches the given object from the graph.""" """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): def get_objects(self, ids, **args):
"""Fetches all of the given object from the graph. """Fetches all of the given object from the graph.
@ -114,10 +123,39 @@ class GraphAPI(object):
args["ids"] = ",".join(ids) args["ids"] = ",".join(ids)
return self.request(self.version + "/", args) 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): def get_connections(self, id, connection_name, **args):
"""Fetches the connections for given object.""" """Fetches the connections for given object."""
return self.request( 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): def put_object(self, parent_object, connection_name, **data):
"""Writes the given object to the graph, connected to the given parent. """Writes the given object to the graph, connected to the given parent.
@ -133,41 +171,17 @@ class GraphAPI(object):
post = feed["data"][0] post = feed["data"][0]
graph.put_object(post["id"], "comments", message="First!") graph.put_object(post["id"], "comments", message="First!")
See http://developers.facebook.com/docs/api#publishing for all Certain operations require extended permissions. See
of the supported writeable objects. https://developers.facebook.com/docs/facebook-login/permissions
for details about permissions.
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" assert self.access_token, "Write operations require an access token"
return self.request( return self.request(
self.version + "/" + parent_object + "/" + connection_name, "{0}/{1}/{2}".format(self.version, parent_object, connection_name),
post_args=data, post_args=data,
method="POST") 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): def put_comment(self, object_id, message):
"""Writes the given comment on the given post.""" """Writes the given comment on the given post."""
return self.put_object(object_id, "comments", message=message) return self.put_object(object_id, "comments", message=message)
@ -178,11 +192,11 @@ class GraphAPI(object):
def delete_object(self, id): def delete_object(self, id):
"""Deletes the object with the given ID from the graph.""" """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): def delete_request(self, user_id, request_id):
"""Deletes the Request with the given ID for the given user.""" """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): def put_photo(self, image, album_path="me/photos", **kwargs):
""" """
@ -193,7 +207,7 @@ class GraphAPI(object):
""" """
return self.request( return self.request(
self.version + "/" + album_path, "{0}/{1}".format(self.version, album_path),
post_args=kwargs, post_args=kwargs,
files={"source": image}, files={"source": image},
method="POST") method="POST")
@ -202,12 +216,12 @@ class GraphAPI(object):
"""Fetches the current version number of the Graph API being used.""" """Fetches the current version number of the Graph API being used."""
args = {"access_token": self.access_token} args = {"access_token": self.access_token}
try: try:
response = requests.request("GET", response = self.session.request(
"https://graph.facebook.com/" + "GET",
self.version + "/me", FACEBOOK_GRAPH_URL + self.version + "/me",
params=args, params=args,
timeout=self.timeout, timeout=self.timeout,
proxies=self.proxies) proxies=self.proxies)
except requests.HTTPError as e: except requests.HTTPError as e:
response = json.loads(e.read()) response = json.loads(e.read())
raise GraphAPIError(response) raise GraphAPIError(response)
@ -228,26 +242,30 @@ class GraphAPI(object):
arguments. arguments.
""" """
args = args or {} if args is None:
args = dict()
if post_args is not None: if post_args is not None:
method = "POST" method = "POST"
# Add `access_token` to post_args or args if it has not already been
# included.
if self.access_token: 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 post_args["access_token"] = self.access_token
else: elif "access_token" not in args:
args["access_token"] = self.access_token args["access_token"] = self.access_token
try: try:
response = requests.request(method or "GET", response = self.session.request(
"https://graph.facebook.com/" + method or "GET",
path, FACEBOOK_GRAPH_URL + path,
timeout=self.timeout, timeout=self.timeout,
params=args, params=args,
data=post_args, data=post_args,
proxies=self.proxies, proxies=self.proxies,
files=files) files=files)
except requests.HTTPError as e: except requests.HTTPError as e:
response = json.loads(e.read()) response = json.loads(e.read())
raise GraphAPIError(response) raise GraphAPIError(response)
@ -275,21 +293,23 @@ class GraphAPI(object):
raise GraphAPIError(result) raise GraphAPIError(result)
return result return result
def fql(self, query): def get_app_access_token(self, app_id, app_secret, offline=False):
"""FQL query.
Example query: "SELECT affiliations FROM user WHERE uid = me()"
""" """
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): return self.request("{0}/oauth/access_token".format(self.version),
"""Get the application's access token as a string.""" args=args)["access_token"]
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( def get_access_token_from_code(
self, code, redirect_uri, app_id, app_secret): self, code, redirect_uri, app_id, app_secret):
@ -305,13 +325,14 @@ class GraphAPI(object):
"client_id": app_id, "client_id": app_id,
"client_secret": app_secret} "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): def extend_access_token(self, app_id, app_secret):
""" """
Extends the expiration time of a valid OAuth access token. See Extends the expiration time of a valid OAuth access token. See
<https://developers.facebook.com/roadmap/offline-access-removal/ <https://developers.facebook.com/docs/facebook-login/access-tokens/
#extend_token> expiration-and-extension>
""" """
args = { args = {
@ -321,13 +342,14 @@ class GraphAPI(object):
"fb_exchange_token": self.access_token, "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): def debug_access_token(self, token, app_id, app_secret):
""" """
Gets information about a user access token issued by an app. See Gets information about a user access token issued by an app. See
<https://developers.facebook.com/docs/facebook-login/access-tokens <https://developers.facebook.com/docs/facebook-login/
#debug> access-tokens/debugging-and-error-handling>
We can generate the app access token by concatenating the app We can generate the app access token by concatenating the app
id and secret: <https://developers.facebook.com/docs/ id and secret: <https://developers.facebook.com/docs/
@ -336,9 +358,9 @@ class GraphAPI(object):
""" """
args = { args = {
"input_token": token, "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): 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 requests to the Graph API. If the user is not logged in, we
return None. return None.
Download the official Facebook JavaScript SDK at Read more about Facebook authentication at
http://github.com/facebook/connect-js/. Read more about Facebook https://developers.facebook.com/docs/facebook-login.
authentication at
http://developers.facebook.com/docs/authentication/.
""" """
cookie = cookies.get("fbsr_" + app_id, "") cookie = cookies.get("fbsr_" + app_id, "")
@ -435,7 +455,7 @@ def parse_signed_request(signed_request, app_secret):
return False return False
# HMAC can only handle ascii (byte) strings # HMAC can only handle ascii (byte) strings
# http://bugs.python.org/issue5285 # https://bugs.python.org/issue5285
app_secret = app_secret.encode('ascii') app_secret = app_secret.encode('ascii')
payload = payload.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): 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} kvps = {'client_id': app_id, 'redirect_uri': canvas_url}
if perms: if perms:
kvps['scope'] = ",".join(perms) kvps['scope'] = ",".join(perms)

View file

@ -1,12 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Copyright 2014 Martey Dodoo # Copyright 2015 Mobolic
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
# a copy of the License at # 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 # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 # License for the specific language governing permissions and limitations
# under the License. # 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' provider = 'Last.fm'
return provider return provider
def get_provider_link(self): def get_provider_link(self, provider=None):
provider_link = '' 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'] provider_link = self.parameters['thetvdb_url']
elif self.parameters['themoviedb_url']: elif self.parameters['themoviedb_url']:
provider_link = self.parameters['themoviedb_url'] provider_link = self.parameters['themoviedb_url']
elif self.parameters['imdb_url']: elif self.parameters['imdb_url']:
provider_link = 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']: elif self.parameters['lastfm_url']:
provider_link = self.parameters['lastfm_url'] provider_link = self.parameters['lastfm_url']
return provider_link return provider_link
@ -1307,8 +1314,9 @@ class FACEBOOK(Notifier):
'group_id': '', 'group_id': '',
'incl_subject': 1, 'incl_subject': 1,
'incl_card': 0, 'incl_card': 0,
'incl_description': 1, 'movie_provider': '',
'incl_pmslink': 0 'tv_provider': '',
'music_provider': ''
} }
def _get_authorization(self, app_id='', app_secret='', redirect_uri=''): def _get_authorization(self, app_id='', app_secret='', redirect_uri=''):
@ -1356,12 +1364,12 @@ class FACEBOOK(Notifier):
return plexpy.CONFIG.FACEBOOK_TOKEN return plexpy.CONFIG.FACEBOOK_TOKEN
def _post_facebook(self, message=None, attachment=None): def _post_facebook(self, **data):
if self.config['group_id']: if self.config['group_id']:
api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.5') api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.5')
try: 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)) logger.info(u"PlexPy Notifiers :: {name} notification sent.".format(name=self.NAME))
return True return True
except Exception as e: except Exception as e:
@ -1376,41 +1384,29 @@ class FACEBOOK(Notifier):
if not subject or not body: if not subject or not body:
return 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'): if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata # Grab formatted metadata
pretty_metadata = PrettyMetadata(kwargs['parameters']) 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 pretty_metadata.media_type == 'movie':
if self.config['incl_pmslink']: provider = self.config['movie_provider']
attachment['link'] = plex_url elif pretty_metadata.media_type in ('show', 'season', 'episode'):
attachment['caption'] = 'View on Plex Web' provider = self.config['tv_provider']
elif provider_link: elif pretty_metadata.media_type in ('artist', 'album', 'track'):
attachment['link'] = provider_link provider = self.config['music_provider']
attachment['caption'] = caption
else: else:
attachment['link'] = poster_url provider = None
data['link'] = pretty_metadata.get_provider_link(provider)
attachment['picture'] = poster_url return self._post_facebook(**data)
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)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Instructions', config_option = [{'label': 'Instructions',
@ -1476,18 +1472,38 @@ class FACEBOOK(Notifier):
'description': 'Include an info card with a poster and metadata with the notifications.', 'description': 'Include an info card with a poster and metadata with the notifications.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Include Plot Summaries', {'label': 'Movie Link Source',
'value': self.config['incl_description'], 'value': self.config['movie_provider'],
'name': 'facebook_incl_description', 'name': 'facebook_movie_provider',
'description': 'Include a plot summary for movies and TV shows on the info card.', 'description': 'Select the source for movie links on the info cards. Leave blank for default.',
'input_type': 'checkbox' 'input_type': 'select',
'select_options': {'': '',
'trakt': 'Trakt.tv',
'plexweb': 'Plex Web'
}
}, },
{'label': 'Include Link to Plex Web', {'label': 'TV Show Link Source',
'value': self.config['incl_pmslink'], 'value': self.config['tv_provider'],
'name': 'facebook_incl_pmslink', 'name': 'facebook_tv_provider',
'description': 'Include a link to the media in Plex Web on the info card.<br>' 'description': 'Select the source for tv show links on the info cards. Leave blank for default.',
'If disabled, the link will go to IMDB, TVDB, TMDb, or Last.fm instead, if available.', 'input_type': 'select',
'input_type': 'checkbox' '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'
}
} }
] ]