Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRaul 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)
commit7757e04f8bc92f616108e2b4d138272810028f3f (patch)
treeed0b8cdeb1cb0d316b86034e2256e9741c239cf6
parent9227fd84f4399b4a1181aed0dc465754c30350e7 (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.ac1
-rw-r--r--extensions/cpsection/Makefile.am2
-rw-r--r--extensions/cpsection/facebook/Makefile.am7
-rw-r--r--extensions/cpsection/facebook/__init__.py21
-rw-r--r--extensions/cpsection/facebook/model.py30
-rw-r--r--extensions/cpsection/facebook/view.py77
-rw-r--r--src/jarabe/journal/journaltoolbox.py12
-rw-r--r--src/jarabe/journal/palettes.py35
-rw-r--r--src/jarabe/util/Makefile.am6
-rw-r--r--src/jarabe/util/facebook.py294
-rw-r--r--src/jarabe/util/facebook_online_account.py227
-rw-r--r--src/jarabe/util/online_account.py48
-rw-r--r--src/jarabe/util/online_accounts_manager.py41
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()]