Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWalter Bender <walter.bender@gmail.com>2013-03-20 23:22:47 (GMT)
committer Walter Bender <walter.bender@gmail.com>2013-03-20 23:22:47 (GMT)
commitf59d9fdf2c0e9244243bf069d709c4a0705d1dcf (patch)
tree8638e6570896174058eb8894ad19c56c8e0ecfe8
parent8583d4db1ca32fcdb2b356f6e44839c3728e2a0b (diff)
add first pass of twitter code
-rw-r--r--configure.ac3
-rw-r--r--extensions/web/Makefile.am2
-rw-r--r--extensions/web/twitter/Makefile.am6
-rw-r--r--extensions/web/twitter/__init__.py0
-rw-r--r--extensions/web/twitter/icons/Makefile.am9
-rw-r--r--extensions/web/twitter/icons/twitter-refresh-insensitive.svg55
-rw-r--r--extensions/web/twitter/icons/twitter-refresh.svg54
-rw-r--r--extensions/web/twitter/icons/twitter-share-insensitive.svg34
-rw-r--r--extensions/web/twitter/icons/twitter-share.svg32
-rw-r--r--extensions/web/twitter/twitter/Makefile.am11
-rw-r--r--extensions/web/twitter/twitter/__init__.py0
-rw-r--r--extensions/web/twitter/twitter/twr_account.py98
-rw-r--r--extensions/web/twitter/twitter/twr_error.py49
-rw-r--r--extensions/web/twitter/twitter/twr_oauth.py97
-rw-r--r--extensions/web/twitter/twitter/twr_object.py122
-rw-r--r--extensions/web/twitter/twitter/twr_search.py79
-rw-r--r--extensions/web/twitter/twitter/twr_status.py175
-rw-r--r--extensions/web/twitter/twitter/twr_timeline.py106
-rw-r--r--extensions/web/twitter/twitter_online_account.py246
19 files changed, 1177 insertions, 1 deletions
diff --git a/configure.ac b/configure.ac
index b235d47..ec28dbd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -70,6 +70,9 @@ extensions/web/Makefile
extensions/web/facebook/Makefile
extensions/web/facebook/facebook/Makefile
extensions/web/facebook/icons/Makefile
+extensions/web/twitter/Makefile
+extensions/web/twitter/twitter/Makefile
+extensions/web/twitter/icons/Makefile
extensions/Makefile
Makefile
po/Makefile.in
diff --git a/extensions/web/Makefile.am b/extensions/web/Makefile.am
index ef6340d..7a7bd10 100644
--- a/extensions/web/Makefile.am
+++ b/extensions/web/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = facebook
+SUBDIRS = facebook twitter
sugardir = $(pkgdatadir)/extensions/web
sugar_PYTHON = \
diff --git a/extensions/web/twitter/Makefile.am b/extensions/web/twitter/Makefile.am
new file mode 100644
index 0000000..8346b41
--- /dev/null
+++ b/extensions/web/twitter/Makefile.am
@@ -0,0 +1,6 @@
+SUBDIRS = twitter icons
+
+sugardir = $(pkgdatadir)/extensions/web/twitter
+sugar_PYTHON = \
+ __init__.py \
+ twitter_online_account.py
diff --git a/extensions/web/twitter/__init__.py b/extensions/web/twitter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/web/twitter/__init__.py
diff --git a/extensions/web/twitter/icons/Makefile.am b/extensions/web/twitter/icons/Makefile.am
new file mode 100644
index 0000000..140e112
--- /dev/null
+++ b/extensions/web/twitter/icons/Makefile.am
@@ -0,0 +1,9 @@
+icondir = $(pkgdatadir)/extensions/web/twitter/icons
+
+icon_DATA = \
+ twitter-refresh.svg \
+ twitter-refresh-insensitive.svg \
+ twitter-share.svg \
+ twitter-share-insensitive.svg
+
+EXTRA_DIST = $(icon_DATA)
diff --git a/extensions/web/twitter/icons/twitter-refresh-insensitive.svg b/extensions/web/twitter/icons/twitter-refresh-insensitive.svg
new file mode 100644
index 0000000..d4d4747
--- /dev/null
+++ b/extensions/web/twitter/icons/twitter-refresh-insensitive.svg
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="Layer_1"
+ xml:space="preserve"><metadata
+ id="metadata3090"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs3088" /><rect
+ width="29.428036"
+ height="29.428036"
+ ry="1.9073727"
+ x="22.862722"
+ y="20.390898"
+ id="rect3071" /><g
+ transform="matrix(0.1,0,0,-0.1,27.576741,43.231715)"
+ id="g3017"
+ style="fill:#808080;fill-opacity:1"><g
+ transform="scale(0.1,0.1)"
+ id="g3019"
+ style="fill:#808080;fill-opacity:1"><path
+ d="m 2000,1432.96 c -73.58,-32.64 -152.67,-54.69 -235.66,-64.61 84.7,50.78 149.77,131.19 180.41,227.01 -79.29,-47.03 -167.1,-81.17 -260.57,-99.57 -74.84,79.75 -181.48,129.57 -299.5,129.57 -226.6,0 -410.328,-183.71 -410.328,-410.31 0,-32.16 3.628,-63.48 10.625,-93.51 -341.016,17.11 -643.368,180.47 -845.739,428.72 -35.324,-60.6 -55.5583,-131.09 -55.5583,-206.29 0,-142.36 72.4373,-267.95 182.5433,-341.53 -67.262,2.13 -130.535,20.59 -185.8519,51.32 -0.0391,-1.71 -0.0391,-3.42 -0.0391,-5.16 0,-198.803 141.441,-364.635 329.145,-402.342 -34.426,-9.375 -70.676,-14.395 -108.098,-14.395 -26.441,0 -52.145,2.578 -77.203,7.364 C 276.391,476.219 427.926,357.578 607.48,354.281 467.051,244.219 290.129,178.621 97.8828,178.621 64.7617,178.621 32.0977,180.57 0,184.359 181.586,67.9414 397.27,0 628.988,0 1383.72,0 1796.45,625.238 1796.45,1167.47 c 0,17.79 -0.41,35.48 -1.2,53.08 80.18,57.86 149.74,130.12 204.75,212.41"
+ id="path3021"
+ style="fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g><polygon
+ points="22.727,18.311 22.727,35.914 43.449,35.914 43.449,8.928 32.113,8.928 "
+ transform="translate(-18.517759,0.67441702)"
+ id="polygon3077"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:3;stroke-opacity:1" /><polyline
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:2.98670006;stroke-opacity:1"
+ points="22.727,18.311 32.113,18.311 32.113,8.928 "
+ transform="translate(-18.517759,0.67441702)"
+ id="polyline3079" /><g
+ transform="translate(-1.3804087,0.67441702)"
+ id="g3081"
+ style="stroke:#808080;stroke-opacity:1"><line
+ x1="38.325649"
+ x2="38.325649"
+ y1="6"
+ y2="18.164"
+ style="fill:none;stroke:#808080;stroke-width:2.98670006;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+ id="line3083" /><polyline
+ transform="matrix(-1,0,0,1,55.03865,0)"
+ points=" 11.583,12.083 16.713,6 21.843,12.083 "
+ id="polyline20"
+ style="fill:none;stroke:#808080;stroke-width:2.98670006;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/extensions/web/twitter/icons/twitter-refresh.svg b/extensions/web/twitter/icons/twitter-refresh.svg
new file mode 100644
index 0000000..95592e0
--- /dev/null
+++ b/extensions/web/twitter/icons/twitter-refresh.svg
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="Layer_1"
+ xml:space="preserve"><metadata
+ id="metadata3090"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs3088" /><g
+ transform="translate(0.61959131,0.67441702)"
+ id="g3138"><rect
+ width="29.428036"
+ height="29.428036"
+ ry="1.9073727"
+ x="22.243132"
+ y="19.71648"
+ id="rect3071" /><g
+ transform="matrix(0.1,0,0,-0.1,26.95715,42.557298)"
+ id="g3017"><g
+ transform="scale(0.1,0.1)"
+ id="g3019"><path
+ d="m 2000,1432.96 c -73.58,-32.64 -152.67,-54.69 -235.66,-64.61 84.7,50.78 149.77,131.19 180.41,227.01 -79.29,-47.03 -167.1,-81.17 -260.57,-99.57 -74.84,79.75 -181.48,129.57 -299.5,129.57 -226.6,0 -410.328,-183.71 -410.328,-410.31 0,-32.16 3.628,-63.48 10.625,-93.51 -341.016,17.11 -643.368,180.47 -845.739,428.72 -35.324,-60.6 -55.5583,-131.09 -55.5583,-206.29 0,-142.36 72.4373,-267.95 182.5433,-341.53 -67.262,2.13 -130.535,20.59 -185.8519,51.32 -0.0391,-1.71 -0.0391,-3.42 -0.0391,-5.16 0,-198.803 141.441,-364.635 329.145,-402.342 -34.426,-9.375 -70.676,-14.395 -108.098,-14.395 -26.441,0 -52.145,2.578 -77.203,7.364 C 276.391,476.219 427.926,357.578 607.48,354.281 467.051,244.219 290.129,178.621 97.8828,178.621 64.7617,178.621 32.0977,180.57 0,184.359 181.586,67.9414 397.27,0 628.988,0 1383.72,0 1796.45,625.238 1796.45,1167.47 c 0,17.79 -0.41,35.48 -1.2,53.08 80.18,57.86 149.74,130.12 204.75,212.41"
+ id="path3021"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g><polygon
+ points="43.449,8.928 32.113,8.928 22.727,18.311 22.727,35.914 43.449,35.914 "
+ transform="translate(-19.13735,0)"
+ id="polygon3077"
+ style="fill:#ffffff;stroke:#010101;stroke-width:3" /><polyline
+ id="polyline3079"
+ transform="translate(-19.13735,0)"
+ points="22.727,18.311 32.113,18.311 32.113,8.928 "
+ style="fill:none;stroke:#010101;stroke-width:2.98670006" /><g
+ transform="translate(-2,0)"
+ id="g3081"><line
+ id="line3083"
+ style="fill:none;stroke:#ffffff;stroke-width:2.98670006;stroke-linecap:round;stroke-linejoin:round"
+ y2="18.164"
+ y1="6"
+ x2="38.325649"
+ x1="38.325649" /><polyline
+ style="fill:none;stroke:#ffffff;stroke-width:2.98670006;stroke-linecap:round;stroke-linejoin:round"
+ id="polyline20"
+ points=" 11.583,12.083 16.713,6 21.843,12.083 "
+ transform="matrix(-1,0,0,1,55.03865,0)" /></g></g></svg> \ No newline at end of file
diff --git a/extensions/web/twitter/icons/twitter-share-insensitive.svg b/extensions/web/twitter/icons/twitter-share-insensitive.svg
new file mode 100644
index 0000000..909373f
--- /dev/null
+++ b/extensions/web/twitter/icons/twitter-share-insensitive.svg
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="Layer_1"
+ xml:space="preserve"><metadata
+ id="metadata13"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs11" /><rect
+ width="54"
+ height="54"
+ ry="3.5"
+ x="0.5"
+ y="0.5"
+ id="rect3" /><g
+ transform="matrix(0.2,0,0,-0.2,7.5,43.7536)"
+ id="g3017"
+ style="fill:#808080;fill-opacity:1"><g
+ transform="scale(0.1,0.1)"
+ id="g3019"
+ style="fill:#808080;fill-opacity:1"><path
+ d="m 2000,1432.96 c -73.58,-32.64 -152.67,-54.69 -235.66,-64.61 84.7,50.78 149.77,131.19 180.41,227.01 -79.29,-47.03 -167.1,-81.17 -260.57,-99.57 -74.84,79.75 -181.48,129.57 -299.5,129.57 -226.6,0 -410.328,-183.71 -410.328,-410.31 0,-32.16 3.628,-63.48 10.625,-93.51 -341.016,17.11 -643.368,180.47 -845.739,428.72 -35.324,-60.6 -55.5583,-131.09 -55.5583,-206.29 0,-142.36 72.4373,-267.95 182.5433,-341.53 -67.262,2.13 -130.535,20.59 -185.8519,51.32 -0.0391,-1.71 -0.0391,-3.42 -0.0391,-5.16 0,-198.803 141.441,-364.635 329.145,-402.342 -34.426,-9.375 -70.676,-14.395 -108.098,-14.395 -26.441,0 -52.145,2.578 -77.203,7.364 C 276.391,476.219 427.926,357.578 607.48,354.281 467.051,244.219 290.129,178.621 97.8828,178.621 64.7617,178.621 32.0977,180.57 0,184.359 181.586,67.9414 397.27,0 628.988,0 1383.72,0 1796.45,625.238 1796.45,1167.47 c 0,17.79 -0.41,35.48 -1.2,53.08 80.18,57.86 149.74,130.12 204.75,212.41"
+ id="path3021"
+ style="fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></svg> \ No newline at end of file
diff --git a/extensions/web/twitter/icons/twitter-share.svg b/extensions/web/twitter/icons/twitter-share.svg
new file mode 100644
index 0000000..e2cf3ec
--- /dev/null
+++ b/extensions/web/twitter/icons/twitter-share.svg
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="Layer_1"
+ xml:space="preserve"><metadata
+ id="metadata13"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs11" /><rect
+ width="54"
+ height="54"
+ ry="3.5"
+ x="0.5"
+ y="0.5"
+ id="rect3" /><g
+ transform="matrix(0.2,0,0,-0.2,7.5,43.7536)"
+ id="g3017"><g
+ transform="scale(0.1,0.1)"
+ id="g3019"><path
+ d="m 2000,1432.96 c -73.58,-32.64 -152.67,-54.69 -235.66,-64.61 84.7,50.78 149.77,131.19 180.41,227.01 -79.29,-47.03 -167.1,-81.17 -260.57,-99.57 -74.84,79.75 -181.48,129.57 -299.5,129.57 -226.6,0 -410.328,-183.71 -410.328,-410.31 0,-32.16 3.628,-63.48 10.625,-93.51 -341.016,17.11 -643.368,180.47 -845.739,428.72 -35.324,-60.6 -55.5583,-131.09 -55.5583,-206.29 0,-142.36 72.4373,-267.95 182.5433,-341.53 -67.262,2.13 -130.535,20.59 -185.8519,51.32 -0.0391,-1.71 -0.0391,-3.42 -0.0391,-5.16 0,-198.803 141.441,-364.635 329.145,-402.342 -34.426,-9.375 -70.676,-14.395 -108.098,-14.395 -26.441,0 -52.145,2.578 -77.203,7.364 C 276.391,476.219 427.926,357.578 607.48,354.281 467.051,244.219 290.129,178.621 97.8828,178.621 64.7617,178.621 32.0977,180.57 0,184.359 181.586,67.9414 397.27,0 628.988,0 1383.72,0 1796.45,625.238 1796.45,1167.47 c 0,17.79 -0.41,35.48 -1.2,53.08 80.18,57.86 149.74,130.12 204.75,212.41"
+ id="path3021"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></svg> \ No newline at end of file
diff --git a/extensions/web/twitter/twitter/Makefile.am b/extensions/web/twitter/twitter/Makefile.am
new file mode 100644
index 0000000..e36c09e
--- /dev/null
+++ b/extensions/web/twitter/twitter/Makefile.am
@@ -0,0 +1,11 @@
+sugardir = $(pkgdatadir)/extensions/web/twitter/twitter
+sugar_PYTHON = \
+ __init__.py \
+ twr_account.py \
+ twr_error.py \
+ twr_oauth.py \
+ twr_object.py \
+ twr_search.py \
+ twr_status.py \
+ twr_timeline.py
+
diff --git a/extensions/web/twitter/twitter/__init__.py b/extensions/web/twitter/twitter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/web/twitter/twitter/__init__.py
diff --git a/extensions/web/twitter/twitter/twr_account.py b/extensions/web/twitter/twitter/twr_account.py
new file mode 100644
index 0000000..ecdeba7
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_account.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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 urllib
+import time
+import random
+import hashlib
+import hmac
+import binascii
+
+
+class TwrAccount:
+
+ @classmethod
+ def set_secrets(cls, c_key, c_secret, a_key, a_secret):
+ cls._consumer_key = c_key
+ cls._consumer_secret = c_secret
+ cls._access_key = a_key
+ cls._access_secret = a_secret
+
+ @classmethod
+ def _oauth_signature(cls, method, url, params):
+ recipe = (
+ method,
+ TwrAccount._percent(url),
+ TwrAccount._percent(TwrAccount._string_params(params)))
+
+ raw = '&'.join(recipe)
+ key = '%s&%s' % (TwrAccount._percent(cls._consumer_secret),
+ TwrAccount._percent(cls._access_secret))
+
+ hashed = hmac.new(key, raw, hashlib.sha1)
+ signature = binascii.b2a_base64(hashed.digest())[:-1]
+
+ return signature
+
+ @classmethod
+ def authorization_header(cls, method, url, request_params):
+ oauth_params = {
+ 'oauth_nonce': TwrAccount._nonce(),
+ 'oauth_timestamp': TwrAccount._timestamp(),
+ 'oauth_consumer_key': cls._consumer_key,
+ 'oauth_version': '1.0',
+ 'oauth_token': cls._access_key,
+ 'oauth_signature_method': 'HMAC-SHA1'}
+
+ params = dict(oauth_params.items() + request_params)
+ params['oauth_signature'] = cls._oauth_signature(method, url, params)
+
+ header = 'OAuth %s' % ', '.join(['%s="%s"' % \
+ (k, TwrAccount._percent(v)) \
+ for k, v in sorted(params.iteritems())])
+
+ return header
+
+ @staticmethod
+ def _percent(string):
+ return urllib.quote(str(string), safe='~')
+
+ @staticmethod
+ def _utf8(string):
+ return str(string).encode("utf-8")
+
+ @staticmethod
+ def _nonce(length=8):
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+ @staticmethod
+ def _timestamp():
+ return int(time.time())
+
+ @staticmethod
+ def _string_params(params):
+ key_values = [(TwrAccount._percent(TwrAccount._utf8(k)), \
+ TwrAccount._percent(TwrAccount._utf8(v))) \
+ for k, v in params.items()]
+ key_values.sort()
+
+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
diff --git a/extensions/web/twitter/twitter/twr_error.py b/extensions/web/twitter/twitter/twr_error.py
new file mode 100644
index 0000000..c787371
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_error.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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.
+
+
+class TwrStatusNotCreated(Exception):
+ pass
+
+
+class TwrStatusAlreadyCreated(Exception):
+ pass
+
+
+class TwrStatusNotFound(Exception):
+ pass
+
+
+class TwrStatusError(Exception):
+ pass
+
+
+class TwrTimelineError(Exception):
+ pass
+
+
+class TwrOauthError(Exception):
+ pass
+
+
+class TwrSearchError(Exception):
+ pass
diff --git a/extensions/web/twitter/twitter/twr_oauth.py b/extensions/web/twitter/twitter/twr_oauth.py
new file mode 100644
index 0000000..e551546
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_oauth.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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 urlparse import parse_qsl
+
+import twr_error
+from twr_object import TwrObject
+
+
+class TwrOauth(GObject.GObject):
+
+ REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
+ AUTHORIZATION_URL = 'https://api.twitter.com/oauth/'\
+ 'authorize?oauth_token=%s'
+ ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
+
+ __gsignals__ = {
+ 'request-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'request-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str])),
+ 'access-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'access-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str]))}
+
+ def request_token(self):
+ GObject.idle_add(self._get,
+ self.REQUEST_TOKEN_URL,
+ [],
+ self.__completed_cb,
+ self.__failed_cb,
+ 'request-downloaded',
+ 'request-downloaded-failed')
+
+ def access_token(self, verifier):
+ params = [('oauth_callback', ('oob')),
+ ('oauth_verifier', (verifier))]
+
+ GObject.idle_add(self._post,
+ self.ACCESS_TOKEN_URL,
+ params,
+ None,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'access-downloaded',
+ 'access-downloaded-failed')
+
+ def _get(self, url, params,
+ completed_cb, failed_cb, completed_data, failed_data):
+
+ object = TwrObject()
+ object.connect('transfer-completed', completed_cb, completed_data)
+ object.connect('transfer-failed', failed_cb, failed_data)
+ object.request('GET', url, params)
+
+ def _post(self, url, params, filepath,
+ completed_cb, failed_cb, completed_data, failed_data):
+
+ object = TwrObject()
+ object.connect('transfer-completed', completed_cb, completed_data)
+ object.connect('transfer-failed', failed_cb, failed_data)
+ object.request('POST', url, params, filepath)
+
+ def __completed_cb(self, object, data, signal):
+ try:
+ info = dict(parse_qsl(data))
+
+ if isinstance(info, dict) and ('errors' in info.keys()):
+ raise twr_error.TwrOauthError(str(info['errors']))
+
+ self.emit(signal, info)
+ except Exception, e:
+ print 'TwrOauth.__completed_cb crashed with %s' % str(e)
+
+ def __failed_cb(self, object, message, signal):
+ self.emit(signal, message)
diff --git a/extensions/web/twitter/twitter/twr_object.py b/extensions/web/twitter/twitter/twr_object.py
new file mode 100644
index 0000000..5acb4cd
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_object.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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 pycurl
+import urllib
+
+from gi.repository import GObject
+
+from twr_account import TwrAccount
+
+
+class TwrObject(GObject.GObject):
+
+ __gsignals__ = {
+ 'transfer-completed': (GObject.SignalFlags.RUN_FIRST, None, ([str])),
+ 'transfer-progress': (GObject.SignalFlags.RUN_FIRST, None, \
+ ([float, float, str])),
+ 'transfer-failed': (GObject.SignalFlags.RUN_FIRST, None, ([str])),
+ 'transfer-started': (GObject.SignalFlags.RUN_FIRST, None, ([]))}
+
+ def _gen_header(self, method, url, params=[]):
+ authorization = TwrAccount.authorization_header(method, url, params)
+ headers = ['Host: api.twitter.com',
+ 'Authorization: %s' % authorization]
+
+ return headers
+
+ def _update_cb(self, down_total, down_done, up_total, up_done, states):
+
+ if 2 in states:
+ return
+
+ total = up_total
+ done = up_done
+ mode = 'upload'
+
+ if 1 in states:
+ total = down_total
+ done = down_done
+ mode = 'download'
+
+ if total == 0:
+ return
+
+ if 0 not in states:
+ self.emit('transfer-started')
+ states.append(0)
+
+ self.emit('transfer-progress', total, done, mode)
+
+ state = states[-1]
+ if total == done and state in states and len(states) == state + 1:
+ states.append(state + 1)
+
+ def request(self, method, url, params, filepath=None):
+ c = pycurl.Curl()
+
+ if method == 'POST':
+ c.setopt(c.POST, 1)
+ c.setopt(c.HTTPHEADER, self._gen_header(method, url))
+
+ if filepath is not None:
+ params += [("media", (c.FORM_FILE, filepath))]
+
+ if params is not None:
+ c.setopt(c.HTTPPOST, params)
+ else:
+ c.setopt(c.POSTFIELDS, '')
+ else:
+ c.setopt(c.HTTPGET, 1)
+ c.setopt(c.HTTPHEADER, self._gen_header(method, url, params))
+ url += '?%s' % urllib.urlencode(params)
+
+ # XXX hack to trace transfer states
+ states = []
+
+ def pre_update_cb(*args):
+ args = list(args) + [states]
+ self._update_cb(*args)
+
+ #XXX hack to write multiple responses
+ buffer = []
+
+ def __write_cb(data):
+ buffer.append(data)
+
+ c.setopt(c.URL, url)
+ c.setopt(c.NOPROGRESS, 0)
+ c.setopt(c.PROGRESSFUNCTION, pre_update_cb)
+ c.setopt(c.WRITEFUNCTION, __write_cb)
+ #c.setopt(c.VERBOSE, True)
+
+ try:
+ c.perform()
+ except pycurl.error, e:
+ self.emit('transfer-failed', str(e))
+ else:
+ code = c.getinfo(c.HTTP_CODE)
+ if code != 200:
+ self.emit('transfer-failed', 'HTTP code %s' % code)
+ finally:
+ self.emit('transfer-completed', ''.join(buffer))
+ c.close()
diff --git a/extensions/web/twitter/twitter/twr_search.py b/extensions/web/twitter/twitter/twr_search.py
new file mode 100644
index 0000000..f2d9440
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_search.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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
+
+from gi.repository import GObject
+
+import twr_error
+from twr_object import TwrObject
+
+
+class TwrSearch(GObject.GObject):
+
+ TWEETS_URL = 'https://api.twitter.com/1.1/search/tweets.json'
+
+ __gsignals__ = {
+ 'tweets-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'tweets-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str]))}
+
+ def tweets(self, q, count=None, since_id=None, max_id=None):
+ params = [('q', (q))]
+
+ if count is not None:
+ params += [('count', (count))]
+ if since_id is not None:
+ params += [('since_id', (since_id))]
+ if max_id is not None:
+ params += [('max_id', (max_id))]
+
+ GObject.idle_add(self._get,
+ self.TWEETS_URL,
+ params,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'tweets-downloaded',
+ 'tweets-downloaded-failed')
+
+ def _get(self, url, params, completed_cb, failed_cb,
+ completed_data, failed_data):
+
+ object = TwrObject()
+ object.connect('transfer-completed', completed_cb, completed_data)
+ object.connect('transfer-failed', failed_cb, failed_data)
+ object.request('GET', url, params)
+
+ def __completed_cb(self, object, data, signal):
+ try:
+ info = json.loads(data)
+
+ if isinstance(info, dict) and ('errors' in info.keys()):
+ raise twr_error.TwrSearchError(str(info['errors']))
+
+ self.emit(signal, info)
+ except Exception, e:
+ print 'TwrSearch.__completed_cb crashed with %s' % str(e)
+
+ def __failed_cb(self, object, message, signal):
+ self.emit(signal, message)
diff --git a/extensions/web/twitter/twitter/twr_status.py b/extensions/web/twitter/twitter/twr_status.py
new file mode 100644
index 0000000..7ba6ea9
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_status.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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
+
+from gi.repository import GObject
+
+import twr_error
+from twr_object import TwrObject
+
+
+class TwrStatus(GObject.GObject):
+ UPDATE_URL = 'https://api.twitter.com/1.1/statuses/update.json'
+ UPDATE_WITH_MEDIA_URL = 'https://api.twitter.com/1.1/statuses/'\
+ 'update_with_media.json'
+ SHOW_URL = 'https://api.twitter.com/1.1/statuses/show.json'
+ RETWEET_URL = 'https://api.twitter.com/1.1/statuses/retweet/%s.json'
+ RETWEETS_URL = 'https://api.twitter.com/1.1/statuses/retweets/%s.json'
+ DESTROY_URL = 'https://api.twitter.com/1.1/statuses/destroy/%s.json'
+
+ __gsignals__ = {
+ 'status-updated': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'status-updated-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str])),
+ 'status-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'status-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str])),
+ 'status-destroyed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'status-destroyed-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str])),
+ 'retweet-created': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'retweet-created-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str])),
+ 'retweets-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'retweets-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str]))}
+
+ def __init__(self, status_id=None):
+ GObject.GObject.__init__(self)
+ self._status_id = status_id
+
+ def update(self, status, reply_status_id=None):
+ self._update(self.UPDATE_URL,
+ status,
+ None,
+ reply_status_id)
+
+ def update_with_media(self, status, filepath, reply_status_id=None):
+ self._update(self.UPDATE_WITH_MEDIA_URL,
+ status,
+ filepath,
+ reply_status_id)
+
+ def _update(self, url, status, filepath=None, reply_status_id=None):
+ self._check_is_not_created()
+
+ params = [('status', (status))]
+ if reply_status_id is not None:
+ params += [('in_reply_to_status_id', (reply_status_id))]
+
+ GObject.idle_add(self._post,
+ url,
+ params,
+ filepath,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'status-updated',
+ 'status-updated-failed')
+
+ def show(self):
+ self._check_is_created()
+ GObject.idle_add(self._get,
+ self.SHOW_URL,
+ [('id', (self._status_id))],
+ self.__completed_cb,
+ self.__failed_cb,
+ 'status-downloaded',
+ 'status-downloaded-failed')
+
+ def destroy(self):
+ self._check_is_created()
+ GObject.idle_add(self._post,
+ self.DESTROY_URL % self._status_id,
+ None,
+ None,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'status-destroyed',
+ 'status-destroyed-failed')
+
+ def retweet(self):
+ self._check_is_created()
+ GObject.idle_add(self._post,
+ self.RETWEET_URL % self._status_id,
+ None,
+ None,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'retweet-created',
+ 'retweet-created-failed')
+
+ def retweets(self):
+ self._check_is_created()
+ GObject.idle_add(self._get,
+ self.RETWEETS_URL % self._status_id,
+ [],
+ self.__completed_cb,
+ self.__failed_cb,
+ 'retweets-downloaded',
+ 'retweets-downloaded-failed')
+
+ def _check_is_not_created(self):
+ if self._status_id is not None:
+ raise twr_error.TwrStatusAlreadyCreated('Status already created')
+
+ def _check_is_created(self):
+ if self._status_id is None:
+ raise twr_error.TwrStatusNotCreated('Status not created')
+
+ def _get(self, url, params,
+ completed_cb, failed_cb, completed_data, failed_data):
+
+ object = TwrObject()
+ object.connect('transfer-completed', completed_cb, completed_data)
+ object.connect('transfer-failed', failed_cb, failed_data)
+ object.request('GET', url, params)
+
+ def _post(self, url, params, filepath,
+ completed_cb, failed_cb, completed_data, failed_data):
+
+ object = TwrObject()
+ object.connect('transfer-completed', completed_cb, completed_data)
+ object.connect('transfer-failed', failed_cb, failed_data)
+ object.request('POST', url, params, filepath)
+
+ def __completed_cb(self, object, data, signal):
+ try:
+ info = json.loads(data)
+
+ if 'errors' in info.keys():
+ raise twr_error.TwrStatusError(str(info['errors']))
+
+ if self._status_id is None and 'id_str' in info.keys():
+ self._status_id = str(info['id_str'])
+
+ self.emit(signal, info)
+ except Exception, e:
+ print '__completed_cb crashed with %s' % str(e)
+
+ def __failed_cb(self, object, message, signal):
+ self.emit(signal, message)
diff --git a/extensions/web/twitter/twitter/twr_timeline.py b/extensions/web/twitter/twitter/twr_timeline.py
new file mode 100644
index 0000000..3b3dfc9
--- /dev/null
+++ b/extensions/web/twitter/twitter/twr_timeline.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Martin Abente Lahaye. - tch@sugarlabs.org
+
+#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
+
+from gi.repository import GObject
+
+import twr_error
+from twr_object import TwrObject
+
+
+class TwrTimeline(TwrObject):
+
+ MENTIONS_TIMELINE_URL = 'https://api.twitter.com/1.1/statuses/'\
+ 'mentions_timeline.json'
+ HOME_TIMELINE_URL = 'https://api.twitter.com/1.1/statuses/'\
+ 'home_timeline.json'
+
+ __gsignals__ = {
+ 'mentions-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'mentions-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str])),
+ 'timeline-downloaded': (GObject.SignalFlags.RUN_FIRST,
+ None, ([object])),
+ 'timeline-downloaded-failed': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str]))}
+
+ def mentions_timeline(self, count=None, since_id=None, max_id=None):
+ params = self._params(count, since_id, max_id)
+
+ GObject.idle_add(self._get,
+ self.MENTIONS_TIMELINE_URL,
+ params,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'mentions-downloaded',
+ 'mentions-downloaded-failed')
+
+ def home_timeline(self, count=None, since_id=None,
+ max_id=None, exclude_replies=None):
+ params = self._params(count, since_id, max_id, exclude_replies)
+
+ GObject.idle_add(self._get,
+ self.HOME_TIMELINE_URL,
+ params,
+ self.__completed_cb,
+ self.__failed_cb,
+ 'timeline-downloaded',
+ 'timeline-downloaded-failed')
+
+ def _params(self, count=None, since_id=None,
+ max_id=None, exclude_replies=None):
+ params = []
+
+ if count is not None:
+ params += [('count', (count))]
+ if since_id is not None:
+ params += [('since_id', (since_id))]
+ if max_id is not None:
+ params += [('max_id', (max_id))]
+ if exclude_replies is not None:
+ params += [('exclude_replies', (exclude_replies))]
+
+ return params
+
+ def _get(self, url, params, completed_cb, failed_cb,
+ completed_data, failed_data):
+
+ object = TwrObject()
+ object.connect('transfer-completed', completed_cb, completed_data)
+ object.connect('transfer-failed', failed_cb, failed_data)
+ object.request('GET', url, params)
+
+ def __completed_cb(self, object, data, signal):
+ try:
+ info = json.loads(data)
+
+ if isinstance(info, dict) and ('errors' in info.keys()):
+ raise twr_error.TwrTimelineError(str(info['errors']))
+
+ self.emit(signal, info)
+ except Exception, e:
+ print 'TwrTimeline.__completed_cb crashed with %s' % str(e)
+
+ def __failed_cb(self, object, message, signal):
+ self.emit(signal, message)
diff --git a/extensions/web/twitter/twitter_online_account.py b/extensions/web/twitter/twitter_online_account.py
new file mode 100644
index 0000000..ae404ed
--- /dev/null
+++ b/extensions/web/twitter/twitter_online_account.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales, Martin Abente
+
+#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
+import json
+
+from twitter.twr_account import TwrAccount
+from twitter.twr_status import TwrStatus
+
+from gi.repository import Gtk
+from gi.repository import GdkPixbuf
+from gi.repository import GConf
+from gi.repository import GObject
+
+from sugar3.datastore import datastore
+from sugar3.graphics.alert import NotifyAlert
+from sugar3.graphics.icon import Icon
+
+from jarabe.journal import journalwindow
+
+from jarabe.web import online_account
+
+ACCOUNT_NEEDS_ATTENTION = 0
+ACCOUNT_ACTIVE = 1
+ONLINE_ACCOUNT_NAME = _('Twitter')
+COMMENTS = 'comments'
+COMMENT_IDS = 'twr_comment_ids'
+
+class TwitterOnlineAccount(online_account.OnlineAccount):
+
+ CONSUMER_TOKEN_KEY = "/desktop/sugar/collaboration/twitter_consumer_token"
+ CONSUMER_SECRET_KEY = "/desktop/sugar/collaboration/twitter_consumer_secret"
+ ACCESS_TOKEN_KEY = "/desktop/sugar/collaboration/twitter_access_token"
+ ACCESS_SECRET_KEY = "/desktop/sugar/collaboration/twitter_access_secret"
+
+ def __init__(self):
+ online_account.OnlineAccount.__init__(self)
+ self._client = GConf.Client.get_default()
+ ctoken, csecret, atoken, asecret = self._access_tokens()
+ TwrAccount.set_secrets(ctoken, csecret, atoken, asecret)
+ self._alert = None
+
+ def get_description(self):
+ return ONLINE_ACCOUNT_NAME
+
+ def is_configured(self):
+ return self._access_tokens() is not None
+
+ def is_active(self):
+ # No expiration date
+ return self._access_tokens()[0] is not None
+
+ def get_share_menu(self, journal_entry_metadata):
+ twr_share_menu = _TwitterShareMenu(journal_entry_metadata,
+ self.is_active())
+ self._connect_transfer_signals(twr_share_menu)
+ return twr_share_menu
+
+ def get_refresh_button(self):
+ twr_refresh_button = _TwitterRefreshButton(self.is_active())
+ self._connect_transfer_signals(twr_refresh_button)
+ return twr_refresh_button
+
+ def _connect_transfer_signals(self, transfer_widget):
+ transfer_widget.connect('transfer-state-changed',
+ self._transfer_state_changed_cb)
+
+ def _transfer_state_changed_cb(self, widget, state_message):
+ logging.debug('_transfer_state_changed_cb')
+
+ # First, remove any existing alert
+ if self._alert is None:
+ self._alert = NotifyAlert()
+ self._alert.props.title = ONLINE_ACCOUNT_NAME
+ self._alert.connect('response', self._alert_response_cb)
+ journalwindow.get_journal_window().add_alert(self._alert)
+ self._alert.show()
+
+ logging.debug(state_message)
+ self._alert.props.msg = state_message
+
+ def _alert_response_cb(self, alert, response_id):
+ journalwindow.get_journal_window().remove_alert(alert)
+ self._alert = None
+
+ def _access_tokens(self):
+ return (self._client.get_string(self.CONSUMER_TOKEN_KEY),
+ self._client.get_string(self.CONSUMER_SECRET_KEY),
+ self._client.get_string(self.ACCESS_TOKEN_KEY),
+ self._client.get_string(self.ACCESS_SECRET_KEY))
+
+
+class _TwitterShareMenu(online_account.OnlineShareMenu):
+ __gtype_name__ = 'JournalTwitterMenu'
+
+ def __init__(self, metadata, is_active):
+ online_account.OnlineShareMenu.__init__(self, ONLINE_ACCOUNT_NAME)
+
+ if is_active:
+ icon_name = 'twitter-share'
+ else:
+ icon_name = 'twitter-share-insensitive'
+ self.set_image(Icon(icon_name=icon_name,
+ icon_size=Gtk.IconSize.MENU))
+ self.show()
+ self._metadata = metadata
+ self._comment = '%s: %s' % (self._get_metadata_by_key('title'),
+ self._get_metadata_by_key('description'))
+
+ self.connect('activate', self._twitter_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 _twitter_share_menu_cb(self, menu_item):
+ logging.debug('_twitter_share_menu_cb')
+
+ self.emit('transfer-state-changed', _('Download started'))
+ tmp_file = tempfile.mktemp()
+ self._image_file_from_metadata(tmp_file)
+
+ tweet = TwrStatus()
+ tweet.update_with_media(self._comment, tmp_file)
+ # TODO: Get the twr_object_id to save in the Journal
+
+ 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 _TwitterRefreshButton(online_account.OnlineRefreshButton):
+ def __init__(self, is_active):
+ online_account.OnlineRefreshButton.__init__(
+ self, 'twitter-refresh-insensitive')
+
+ self._metadata = None
+ self._is_active = is_active
+ self.set_tooltip(_('Twitter refresh'))
+ self.set_sensitive(False)
+ self.connect('clicked', self._twr_refresh_button_clicked_cb)
+ self.show()
+
+ def set_metadata(self, metadata):
+ self._metadata = metadata
+ if self._is_active:
+ if self._metadata:
+ if 'twr_object_id' in self._metadata:
+ self.set_sensitive(True)
+ self.set_icon_name('twitter-refresh')
+ else:
+ self.set_sensitive(False)
+ self.set_icon_name('twitter-refresh-insensitive')
+
+ def _twr_refresh_button_clicked_cb(self, button):
+ logging.debug('_twr_refresh_button_clicked_cb')
+
+ if self._metadata is None:
+ logging.debug('_twr_refresh_button_clicked_cb called without metadata')
+ return
+
+ if 'twr_object_id' not in self._metadata:
+ logging.debug('_twr_refresh_button_clicked_cb called without twr_object_id in metadata')
+ return
+
+ self.emit('transfer-state-changed', _('Download started'))
+ # To Do: filter tweets to find replys to this object id
+ # Fix Me: Twr.Status is not the correct function call
+ tweet = twitter.TwrStatus(self._metadata['twr_object_id'])
+ tweet.connect('comments-downloaded',
+ self._twr_comments_downloaded_cb)
+ tweet.connect('comments-download-failed',
+ self._twr_comments_download_failed_cb)
+ tweet.connect('transfer-state-changed',
+ self._transfer_state_changed_cb)
+ GObject.idle_add(tweet.refresh_comments)
+
+ def _twr_comments_downloaded_cb(self, tweet, comments):
+ logging.debug('_twr_comments_downloaded_cb')
+
+ ds_object = datastore.get(self._metadata['uid'])
+ if not COMMENTS in ds_object.metadata:
+ ds_comments = []
+ else:
+ ds_comments = json.loads(ds_object.metadata[COMMENTS])
+ if not COMMENT_IDS in ds_object.metadata:
+ ds_comment_ids = []
+ else:
+ ds_comment_ids = json.loads(ds_object.metadata[COMMENT_IDS])
+ new_comment = False
+ for comment in comments:
+ if comment['id'] not in ds_comment_ids:
+ # TODO: get avatar icon and add it to icon_theme
+ ds_comments.append({'from': comment['from'],
+ 'message': comment['message'],
+ 'icon': 'twitter-share'})
+ ds_comment_ids.append(comment['id'])
+ new_comment = True
+ if new_comment:
+ ds_object.metadata[COMMENTS] = json.dumps(ds_comments)
+ ds_object.metadata[COMMENT_IDS] = json.dumps(ds_comment_ids)
+ self.emit('comments-updated')
+
+ datastore.write(ds_object, update_mtime=False)
+
+ def _twr_comments_download_failed_cb(self, tweet, failed_reason):
+ logging.debug('_twr_comments_download_failed_cb: %s' % (failed_reason))
+
+def get_account():
+ return TwitterOnlineAccount()