diff options
author | Raul Gutierrez Segales <rgs@itevenworks.net> | 2013-02-01 17:47:05 (GMT) |
---|---|---|
committer | Raul Gutierrez Segales <rgs@itevenworks.net> | 2013-02-03 07:37:18 (GMT) |
commit | 7757e04f8bc92f616108e2b4d138272810028f3f (patch) | |
tree | ed0b8cdeb1cb0d316b86034e2256e9741c239cf6 | |
parent | 9227fd84f4399b4a1181aed0dc465754c30350e7 (diff) |
[sharing] Add's 'Share to Facebook' capability
This also sets the foundations for adding sharing
capabilities for other services out there.
I guess this is mainly RFC for now, some thinking
is needed for the whole workflow of sharing out and
into the Journal.
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | extensions/cpsection/Makefile.am | 2 | ||||
-rw-r--r-- | extensions/cpsection/facebook/Makefile.am | 7 | ||||
-rw-r--r-- | extensions/cpsection/facebook/__init__.py | 21 | ||||
-rw-r--r-- | extensions/cpsection/facebook/model.py | 30 | ||||
-rw-r--r-- | extensions/cpsection/facebook/view.py | 77 | ||||
-rw-r--r-- | src/jarabe/journal/journaltoolbox.py | 12 | ||||
-rw-r--r-- | src/jarabe/journal/palettes.py | 35 | ||||
-rw-r--r-- | src/jarabe/util/Makefile.am | 6 | ||||
-rw-r--r-- | src/jarabe/util/facebook.py | 294 | ||||
-rw-r--r-- | src/jarabe/util/facebook_online_account.py | 227 | ||||
-rw-r--r-- | src/jarabe/util/online_account.py | 48 | ||||
-rw-r--r-- | src/jarabe/util/online_accounts_manager.py | 41 |
13 files changed, 795 insertions, 6 deletions
diff --git a/configure.ac b/configure.ac index a4eb829..43056ed 100644 --- a/configure.ac +++ b/configure.ac @@ -52,6 +52,7 @@ data/sugar-emulator.desktop extensions/cpsection/aboutcomputer/Makefile extensions/cpsection/aboutme/Makefile extensions/cpsection/datetime/Makefile +extensions/cpsection/facebook/Makefile extensions/cpsection/frame/Makefile extensions/cpsection/keyboard/Makefile extensions/cpsection/language/Makefile diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am index a92b5dd..0c4e852 100644 --- a/extensions/cpsection/Makefile.am +++ b/extensions/cpsection/Makefile.am @@ -1,5 +1,5 @@ SUBDIRS = aboutme aboutcomputer datetime frame keyboard language \ - modemconfiguration network power updater + modemconfiguration network power updater facebook sugardir = $(pkgdatadir)/extensions/cpsection sugar_PYTHON = __init__.py diff --git a/extensions/cpsection/facebook/Makefile.am b/extensions/cpsection/facebook/Makefile.am new file mode 100644 index 0000000..471c047 --- /dev/null +++ b/extensions/cpsection/facebook/Makefile.am @@ -0,0 +1,7 @@ +sugardir = $(pkgdatadir)/extensions/cpsection/facebook + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py + diff --git a/extensions/cpsection/facebook/__init__.py b/extensions/cpsection/facebook/__init__.py new file mode 100644 index 0000000..28c0cca --- /dev/null +++ b/extensions/cpsection/facebook/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'FacebookConfig' +ICON = 'facebook-share' +TITLE = _('Configure your Facebook account') diff --git a/extensions/cpsection/facebook/model.py b/extensions/cpsection/facebook/model.py new file mode 100644 index 0000000..6eed773 --- /dev/null +++ b/extensions/cpsection/facebook/model.py @@ -0,0 +1,30 @@ +# Copyright (C) 2013 Walter Bender - Raul Gutierrez Segales +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from gettext import gettext as _ +from gi.repository import GConf + +import time + +from jarabe.util import facebook_online_account as fboa + +def save_access_token(access_token, expires_in): + client = GConf.Client.get_default() + client.set_string(fboa.FacebookOnlineAccount.ACCESS_TOKEN_KEY, access_token) + expiry_time = int(time.time()) + expires_in + client.set_int(fboa.FacebookOnlineAccount.ACCESS_TOKEN_KEY_EXPIRATION_DATE, + expiry_time) diff --git a/extensions/cpsection/facebook/view.py b/extensions/cpsection/facebook/view.py new file mode 100644 index 0000000..fa394f7 --- /dev/null +++ b/extensions/cpsection/facebook/view.py @@ -0,0 +1,77 @@ +# Copyright (C) 2013, Walter Bender - Raul Gutierrez Segales +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gi.repository import Gtk +from gi.repository import GObject +from gi.repository import WebKit + +import urllib +import urlparse + +from gettext import gettext as _ + +from jarabe.controlpanel.sectionview import SectionView + +class FacebookConfig(SectionView): + APP_ID = "172917389475707" + REDIRECT_URI = "http://www.sugarlabs.org" + + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self.restart_alerts = alerts + + scrolled = Gtk.ScrolledWindow() + self.add(scrolled) + scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scrolled.show() + + workspace = Gtk.VBox() + scrolled.add_with_viewport(workspace) + workspace.show() + + wkv = WebKit.WebView() + wkv.load_uri(self._auth_url()) + wkv.grab_focus() + wkv.connect('navigation-policy-decision-requested', self._nav_policy_cb) + workspace.add(wkv) + workspace.show_all() + + def undo(self): + pass + + def _auth_url(self): + url = 'http://www.facebook.com/dialog/oauth' + params = [ + ('client_id', self.APP_ID), + ('redirect_uri', self.REDIRECT_URI), + ('response_type', 'token'), + ('scope', 'publish_stream') + ] + + return "%s?%s" % (url, urllib.urlencode(params)) + + def _nav_policy_cb(self, view, frame, req, action, param): + uri = req.get_uri() + if uri is None: + return + + url_o = urlparse.urlparse(uri) + params = urlparse.parse_qs(url_o.fragment) + if params.has_key('access_token') and params.has_key('expires_in'): + self._model.save_access_token(params['access_token'][0], + int(params['expires_in'][0])) diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index f794dd7..71e614c 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -26,6 +26,7 @@ from gi.repository import GObject from gi.repository import Gio from gi.repository import Gtk +from sugar3.datastore import datastore from sugar3.graphics.palette import Palette from sugar3.graphics.toolbarbox import ToolbarBox from sugar3.graphics.toolcombobox import ToolComboBox @@ -37,7 +38,7 @@ from sugar3.graphics.palettemenu import PaletteMenuBox from sugar3.graphics.palettemenu import PaletteMenuItem from sugar3.graphics.icon import Icon from sugar3.graphics.xocolor import XoColor -from sugar3.graphics.alert import Alert +from sugar3.graphics.alert import NotifyAlert, Alert from sugar3.graphics import iconentry from sugar3 import mime @@ -47,7 +48,7 @@ from jarabe.journal import model from jarabe.journal.palettes import ClipboardMenu from jarabe.journal.palettes import VolumeMenu from jarabe.journal import journalwindow - +from jarabe.util import online_accounts_manager as oam _AUTOSEARCH_TIMEOUT = 1000 @@ -386,6 +387,11 @@ class DetailToolbox(ToolbarBox): self._duplicate.connect('clicked', self._duplicate_clicked_cb) self.toolbar.insert(self._duplicate, -1) + self._refresh_buttons = [] + for account in oam.OnlineAccountsManager.configured_accounts(): + self._refresh_buttons.append(account.get_refresh_button()) + self.toolbar.insert(self._refresh_buttons[-1], -1) + separator = Gtk.SeparatorToolItem() self.toolbar.insert(separator, -1) separator.show() @@ -398,6 +404,8 @@ class DetailToolbox(ToolbarBox): def set_metadata(self, metadata): self._metadata = metadata + for refresh_button in self._refresh_buttons: + refresh_button.set_metadata(metadata) self._refresh_copy_palette() self._refresh_duplicate_palette() self._refresh_resume_palette() diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 200240f..0b272f4 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -17,20 +17,23 @@ from gettext import gettext as _ import logging import os +import tempfile from gi.repository import GObject from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GdkPixbuf from gi.repository import GConf from gi.repository import Gio from gi.repository import GLib +from sugar3.datastore import datastore from sugar3.graphics import style from sugar3.graphics.palette import Palette from sugar3.graphics.menuitem import MenuItem from sugar3.graphics.icon import Icon from sugar3.graphics.xocolor import XoColor -from sugar3.graphics.alert import Alert +from sugar3.graphics.alert import Alert, NotifyAlert from sugar3 import mime from jarabe.model import friends @@ -39,7 +42,7 @@ from jarabe.model import mimeregistry from jarabe.journal import misc from jarabe.journal import model from jarabe.journal import journalwindow - +from jarabe.util import online_accounts_manager as oam class ObjectPalette(Palette): @@ -112,6 +115,16 @@ class ObjectPalette(Palette): self.menu.append(menu_item) menu_item.show() + menu_item = MenuItem(_('Share on')) + icon = Icon(icon_name='activity-web', xo_color=color, + icon_size=Gtk.IconSize.MENU) + menu_item.set_image(icon) + self.menu.append(menu_item) + menu_item.show() + share_menu = ShareMenu(metadata) + menu_item.set_submenu(share_menu) + menu_item.show() + menu_item = MenuItem(_('Send to'), 'document-send') self.menu.append(menu_item) menu_item.show() @@ -193,6 +206,24 @@ class ObjectPalette(Palette): mime_type) +class ShareMenu(Gtk.Menu): + __gtype_name__ = 'JournalShareMenu' + + __gsignals__ = { + 'share-error': (GObject.SignalFlags.RUN_FIRST, None, + ([str, str])), + } + + def __init__(self, metadata): + Gtk.Menu.__init__(self) + + self._metadata = metadata + + for account in oam.OnlineAccountsManager.configured_accounts(): + menu = account.get_share_menu(metadata) + self.append(menu) + + class CopyMenu(Gtk.Menu): __gtype_name__ = 'JournalCopyMenu' diff --git a/src/jarabe/util/Makefile.am b/src/jarabe/util/Makefile.am index 3054b5a..7f80330 100644 --- a/src/jarabe/util/Makefile.am +++ b/src/jarabe/util/Makefile.am @@ -5,4 +5,8 @@ sugardir = $(pythondir)/jarabe/util sugar_PYTHON = \ __init__.py \ emulator.py \ - normalize.py + facebook.py \ + facebook_online_account.py \ + normalize.py \ + online_account.py \ + online_accounts_manager.py diff --git a/src/jarabe/util/facebook.py b/src/jarabe/util/facebook.py new file mode 100644 index 0000000..92529dc --- /dev/null +++ b/src/jarabe/util/facebook.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python +# +# Copyright (c) 2012 Raul Gutierrez S. - rgs@itevenworks.net + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +import json +import logging +import pycurl +import time +import urllib + +from gi.repository import GObject + +class FbAccount(): + _access_token = "" + + @classmethod + def set_access_token(cls, access_token): + logging.debug("LOOK = %s" % (access_token)) + cls._access_token = access_token + + @classmethod + def access_token(cls): + return cls._access_token + +class FbObjectNotCreatedException(Exception): + pass + +class FbBadCall(Exception): + pass + +class FbPhoto(GObject.GObject): + PHOTOS_URL = "https://graph.facebook.com/me/photos?access_token=%s" + COMMENTS_URL = "https://graph.facebook.com/%s/comments" + + __gsignals__ = { + 'photo-created': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + 'photo-create-failed': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + 'comment-added': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + 'comment-add-failed': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + 'comments-downloaded': (GObject.SignalFlags.RUN_FIRST, None, ([object])), + 'comments-download-failed': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + 'likes-downloaded': (GObject.SignalFlags.RUN_FIRST, None, ([object])), + } + + def __init__(self, fb_object_id=None): + GObject.GObject.__init__(self) + self.fb_object_id = fb_object_id + + def create(self, image_path): + GObject.idle_add(self._create, image_path) + + def add_comment(self, comment): + self.check_created('add_comment') + GObject.idle_add(self._add_comment, comment) + + def refresh_comments(self): + """ raise an exception if no one is listening """ + self.check_created('refresh_comments') + GObject.idle_add(self._refresh_comments) + + def check_created(self, method_name): + if self.fb_object_id is None: + errmsg = "Need to call create before calling %s" % (method_name) + raise FbObjectNotCreatedException(errmsg) + + def _add_comment(self, comment): + url = self.COMMENTS_URL % (self.fb_object_id) + + response = [] + def write_cb(buf): + response.append(buf) + + res = self._http_call(url, [('message', comment)], write_cb, post=True) + if res == 200: + try: + comment_id = self._id_from_response("".join(response)) + self.emit('comment-added', comment_id) + except FbBadCall as ex: + self.emit('comment-add-failed', str(ex)) + else: + logging.debug("_add_comment failed, HTTP resp code: %d" % (res)) + self.emit('comment-add-failed', "Add comment failed: %d" % (res)) + + def _create(self, image_path): + url = self.PHOTOS_URL % (FbAccount.access_token()) + c = pycurl.Curl() + params = [('source', (c.FORM_FILE, image_path))] + + response = [] + def write_cb(buf): + response.append(buf) + + result = self._http_call(url, params, write_cb, post=True) + if result == 200: + photo_id = self._id_from_response("".join(response)) + self.fb_object_id = photo_id + self.emit('photo-created', photo_id) + else: + logging.debug("_create failed, HTTP resp code: %d" % result) + + if result == 400: + failed_reason = "Expired access token." + elif result == 6: + failed_reason = "Network is down." + failed_reason += \ + "Please connect to the network and try again." + else: + failed_reason = "Failed reason unknown: %s" % (str(result)) + + self.emit('photo-create-failed', failed_reason) + + def _id_from_response(self, response_str): + response_object = json.loads(response_str) + + if not "id" in response_object: + raise FbBadCall(response_str) + + fb_object_id = response_object['id'].encode('ascii', 'replace') + return fb_object_id + + def _refresh_comments(self): + """ this blocks """ + url = self.COMMENTS_URL % (self.fb_object_id) + + logging.debug("_refresh_comments fetching %s" % (url)) + + response_comments = [] + def write_cb(buf): + response_comments.append(buf) + + ret = self._http_call(url, [], write_cb, post=False) + if ret != 200: + logging.debug("_refresh_comments failed, HTTP resp code: %d" % ret) + self.emit('comments-download-failed', + "Comments download failed: %d" % (ret)) + return + + logging.debug("_refresh_comments: %s" % ("".join(response_comments))) + + try: + response_data = json.loads("".join(response_comments)) + if 'data' not in response_data: + logging.debug("No data inside the FB response") + self.emit('comments-download-failed', + "Comments download failed with no data") + return + except Exception as ex: + logging.debug("Couldn't parse FB response: %s" % str(ex)) + self.emit('comments-download-failed', + "Comments download failed: %s" % (str(ex))) + return + + comments = [] + for c in response_data['data']: + comment = {} # this should be an Object + comment['from'] = c['from']['name'] + comment['message'] = c['message'] + comment['created_time'] = c['created_time'] + comment['like_count'] = c['like_count'] + comments.append(comment) + + if len(comments) > 0: + self.emit('comments-downloaded', comments) + else: + self.emit('comments-download-failed', 'No comments found') + + def _http_call(self, url, params, write_cb, post=False): + app_auth_params = [('access_token', FbAccount.access_token())] + + c = pycurl.Curl() + c.setopt(c.WRITEFUNCTION, write_cb) + + if post: + c.setopt(c.POST, 1) + c.setopt(c.HTTPPOST, app_auth_params + params) + else: + c.setopt(c.HTTPGET, 1) + params_str = urllib.urlencode(app_auth_params + params) + url = "%s?%s" % (url, params_str) + + logging.debug("_http_call: %s" % (url)) + + c.setopt(c.URL, url) + c.perform() + result = c.getinfo(c.HTTP_CODE) + c.close() + + return result + + +if __name__ == '__main__': + import sys + if len(sys.argv) != 3: + print "Tests need access_token and an image path!" + exit(1) + + access_token, photo_path = sys.argv[1:3] + FbAccount.set_access_token(access_token) + + +def test_create_photo(loop): + def photo_created_cb(photo, photo_id, loop): + print "Photo created: %s" % (photo_id) + loop.quit() + + photo = FbPhoto() + photo.connect('photo-created', photo_created_cb, loop) + photo.create(photo_path) + +def test_add_comment(loop): + def photo_created_cb(photo, photo_id, loop): + print "Photo created: %s" % (photo_id) + + def comment_added_cb(photo, comment_id, loop): + print "Comment created: %s" % (comment_id) + loop.quit() + return False + + photo = FbPhoto(photo_id) + photo.connect("comment-added", comment_added_cb, loop) + photo.add_comment("this is a test") + return False + + photo = FbPhoto() + photo.connect('photo-created', photo_created_cb, loop) + photo.create(photo_path) + +def test_get_comments(loop): + def photo_created_cb(photo, photo_id, loop): + print "Photo created: %s" % (photo_id) + + def comment_added_cb(photo, comment_id, loop): + print "Comment created: %s" % (comment_id) + + def comments_downloaded_cb(photo, comments, loop): + print "%s comments for photo %s" % \ + (len(comments), photo.fb_object_id) + + for c in comments: + print "Comment from %s with message: %s" % \ + (c["from"], c["message"]) + + loop.quit() + + photo.connect('comments-downloaded', + comments_downloaded_cb, + loop) + photo.refresh_comments() + return False + + photo = FbPhoto(photo_id) + photo.connect("comment-added", comment_added_cb, loop) + photo.add_comment("this is a test") + return False + + photo = FbPhoto() + photo.connect('photo-created', photo_created_cb, loop) + photo.create(photo_path) + + +def timeout_cb(test_name, loop): + print "%s timed out and failed" % (test_name) + loop.quit() + return False + +if __name__ == '__main__': + tests = [eval(t) for t in dir() if t.startswith('test_')] + + for t in tests: + print "\n=== Starting %s (%s) ===" % (t.__name__, time.time()) + loop = GObject.MainLoop() + tid = GObject.timeout_add(30000, timeout_cb, t.__name__, loop) + t(loop) + loop.run() + GObject.source_remove(tid) + print "=== Finished %s (%s) ===\n" % (t.__name__, time.time()) diff --git a/src/jarabe/util/facebook_online_account.py b/src/jarabe/util/facebook_online_account.py new file mode 100644 index 0000000..dcc82ad --- /dev/null +++ b/src/jarabe/util/facebook_online_account.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python +# +# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +from gettext import gettext as _ +import logging +import os +import tempfile +import time + +from gi.repository import Gtk +from gi.repository import GdkPixbuf +from gi.repository import GConf + +from sugar3.datastore import datastore +from sugar3.graphics.alert import Alert, NotifyAlert +from sugar3.graphics.icon import Icon + +from jarabe.journal import journalwindow +from jarabe.util import facebook +from jarabe.util import online_account + +ACCOUNT_NEEDS_ATTENTION = 0 +ACCOUNT_ACTIVE = 1 + + +class FacebookOnlineAccount(online_account.OnlineAccount): + + ACCESS_TOKEN_KEY = "/desktop/sugar/collaboration/facebook_access_token" + ACCESS_TOKEN_KEY_EXPIRATION_DATE = \ + "/desktop/sugar/collaboration/facebook_access_token_expiration_date" + + def __init__(self): + online_account.OnlineAccount.__init__(self) + self._client = GConf.Client.get_default() + facebook.FbAccount.set_access_token(self._access_token()) + + def is_configured(self): + return self._access_token() is not None + + def is_active(self): + expiration_date = \ + self._client.get_int(self.ACCESS_TOKEN_KEY_EXPIRATION_DATE) + return expiration_date != 0 and expiration_date > time.time() + + def get_share_menu(self, journal_entry_metadata): + if self.is_active(): + icon_name = 'facebook-share' + else: + icon_name = 'facebook-share-insensitive' + fb_share_menu = FacebookShareMenu(journal_entry_metadata) + fb_share_menu.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + fb_share_menu.show() + return fb_share_menu + + def get_refresh_button(self): + return FacebookRefreshButton(self.is_active()) + + def _access_token(self): + return self._client.get_string(self.ACCESS_TOKEN_KEY) + + +class FacebookShareMenu(online_account.OnlineShareMenu): + __gtype_name__ = 'JournalFacebookMenu' + + def __init__(self, metadata): + online_account.OnlineShareMenu.__init__(self, _('Facebook')) + + self._metadata = metadata + self._comment = '%s: %s' % (self._get_metadata_by_key('title'), + self._get_metadata_by_key('description')) + + self.connect('activate', self._facebook_share_menu_cb) + + def _get_metadata_by_key(self, key, default_value=''): + if key in self._metadata: + return self._metadata[key] + return default_value + + def _facebook_share_menu_cb(self, menu_item): + logging.debug('_facebook_share_menu_cb') + + tmp_file = tempfile.mktemp() + self._image_file_from_metadata(tmp_file) + + photo = facebook.FbPhoto() + photo.connect('photo-created', self._photo_created_cb, tmp_file) + photo.connect('photo-create-failed', + self._photo_create_failed_cb, + tmp_file) + result = photo.create(tmp_file) + + def _photo_created_cb(self, fb_photo, fb_object_id, tmp_file): + logging.debug("_photo_created_cb") + + if os.path.exists(tmp_file): + os.unlink(tmp_file) + + fb_photo.connect('comment-added', self._comment_added_cb) + fb_photo.connect('comment-add-failed', self._comment_add_failed_cb) + fb_photo.add_comment(self._comment) + + try: + ds_object = datastore.get(self._metadata['uid']) + ds_object.metadata['fb_object_id'] = fb_object_id + datastore.write(ds_object, update_mtime=False) + except Exception as ex: + logging.debug("_photo_created_cb failed to write to datastore: " % \ + str(ex)) + + def _photo_create_failed_cb(self, fb_photo, failed_reason, tmp_file): + logging.debug("_photo_create_failed_cb") + + if os.path.exists(tmp_file): + os.unlink(tmp_file) + + self._fb_notify(failed_reason) + + def _comment_added_cb(self, fb_photo, fb_comment_id): + logging.debug("_comment_added_cb") + self._fb_notify(_('Upload successful')) + + def _comment_add_failed_cb(self, fb_photo, failed_reason): + logging.debug("_comment_add_failed_cb") + self._fb_notify(failed_reason) + + def _fb_notify(self, message): + alert = NotifyAlert() + title_string = _('Facebook') + alert.props.title = title_string + alert.props.msg = message + alert.connect('response', self._facebook_alert_response_cb) + journalwindow.get_journal_window().add_alert(alert) + alert.show() + + def _facebook_alert_response_cb(self, alert, response_id): + journalwindow.get_journal_window().remove_alert(alert) + + def _image_file_from_metadata(self, image_path): + """ Load a pixbuf from a Journal object. """ + pixbufloader = \ + GdkPixbuf.PixbufLoader.new_with_mime_type('image/png') + pixbufloader.set_size(300, 225) + try: + pixbufloader.write(self._metadata['preview']) + pixbuf = pixbufloader.get_pixbuf() + except Exception as ex: + logging.debug("_image_file_from_metadata: %s" % (str(ex))) + pixbuf = None + + pixbufloader.close() + if pixbuf: + pixbuf.savev(image_path, 'png', [], []) + + +class FacebookRefreshButton(online_account.OnlineRefreshButton): + def __init__(self, is_active): + online_account.OnlineRefreshButton.__init__(self, 'facebook-refresh-insensitive') + + self._metadata = None + self._is_active = is_active + self.set_tooltip(_('Facebook refresh')) + self.set_sensitive(False) + self.connect('clicked', self._fb_refresh_button_clicked_cb) + self.show() + + def set_metadata(self, metadata): + self._metadata = metadata + if self._is_active: + if self._metadata and 'fb_object_id' in self._metadata: + self.set_sensitive(True) + self.set_icon_name('facebook-refresh') + + def _fb_refresh_button_clicked_cb(self, button): + logging.debug('_fb_refresh_button_clicked_cb') + + if self._metadata is None: + logging.debug('_fb_refresh_button_clicked_cb called without metadata') + return + + fb_photo = facebook.FbPhoto(self._metadata['fb_object_id']) + fb_photo.connect('comments-downloaded', + self._fb_comments_downloaded_cb) + fb_photo.connect('comments-download-failed', + self._fb_comments_download_failed_cb) + fb_photo.refresh_comments() + + def _fb_comments_downloaded_cb(self, fb_photo, comments): + logging.debug('_fb_comments_downloaded_cb') + + ds_object = datastore.get(self._metadata['uid']) + for comment in comments: + c_str = "%s: %s" % (comment['from'], comment['message']) + ds_object.metadata['description'] += c_str + + datastore.write(ds_object, update_mtime=False) + + def _fb_comments_download_failed_cb(self, fb_photo, failed_reason): + logging.debug('_fb_comments_download_failed_cb: %s' % (failed_reason)) + alert = NotifyAlert() + alert.props.title = _('Comments download') + alert.props.msg = failed_reason + alert.connect('response', self.__fb_refresh_offline_response_cb) + journalwindow.get_journal_window().add_alert(alert) + alert.show() + + def __fb_refresh_offline_response_cb(self, alert, alert_id): + pass diff --git a/src/jarabe/util/online_account.py b/src/jarabe/util/online_account.py new file mode 100644 index 0000000..b908538 --- /dev/null +++ b/src/jarabe/util/online_account.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# +# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +from gi.repository import GObject + +from sugar3.graphics.menuitem import MenuItem +from sugar3.graphics.toolbutton import ToolButton + +class OnlineAccount(GObject.GObject): + def is_configured(self): + raise Exception("Not defined") + + def is_active(self): + raise Exception("Not defined") + + def get_share_menu(self): + raise Exception("Not defined") + + def get_refresh_button(self): + raise Exception("Not defined") + + +class OnlineShareMenu(MenuItem): + pass + + +class OnlineRefreshButton(ToolButton): + def set_metadata(self, metadata): + raise Exception("Not defined") diff --git a/src/jarabe/util/online_accounts_manager.py b/src/jarabe/util/online_accounts_manager.py new file mode 100644 index 0000000..c262968 --- /dev/null +++ b/src/jarabe/util/online_accounts_manager.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# +# Copyright (c) 2013 Walter Bender + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +from gi.repository import GObject + +from jarabe.util import facebook_online_account as fboa + + +class OnlineAccountsManager(GObject.GObject): + @classmethod + def all_accounts(cls): + accounts = [] + accounts.append(fboa.FacebookOnlineAccount()) + return accounts + + @classmethod + def configured_accounts(cls): + return [a for a in cls.all_accounts() if a.is_configured()] + + @classmethod + def active_accounts(cls): + return [a for a in cls.all_accounts() if a.is_active()] |