diff options
author | Walter 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) |
commit | f59d9fdf2c0e9244243bf069d709c4a0705d1dcf (patch) | |
tree | 8638e6570896174058eb8894ad19c56c8e0ecfe8 | |
parent | 8583d4db1ca32fcdb2b356f6e44839c3728e2a0b (diff) |
add first pass of twitter code
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() |