diff --git a/khweeteur-experimental/__init__.py b/build/lib/khweeteur/__init__.py similarity index 100% rename from khweeteur-experimental/__init__.py rename to build/lib/khweeteur/__init__.py diff --git a/khweeteur-experimental/bitly.py b/build/lib/khweeteur/bitly.py similarity index 100% rename from khweeteur-experimental/bitly.py rename to build/lib/khweeteur/bitly.py diff --git a/khweeteur-experimental/daemon.py b/build/lib/khweeteur/daemon.py similarity index 99% rename from khweeteur-experimental/daemon.py rename to build/lib/khweeteur/daemon.py index cdffb70..a0e2115 100644 --- a/khweeteur-experimental/daemon.py +++ b/build/lib/khweeteur/daemon.py @@ -608,7 +608,7 @@ def retrieve(self, options=None): if __name__ == "__main__": install_excepthook(__version__) - daemon = KhweeteurDaemon('/tmp/khweeteur.pid') + daemon = KhweeteurDaemon('/var/run/khweeteurd/khweeteurd.pid') if len(sys.argv) == 2: if 'start' == sys.argv[1]: daemon.start() diff --git a/khweeteur-experimental/icons/favorite.png b/build/lib/khweeteur/icons/favorite.png similarity index 100% rename from khweeteur-experimental/icons/favorite.png rename to build/lib/khweeteur/icons/favorite.png diff --git a/khweeteur-experimental/icons/general_chat_button.png b/build/lib/khweeteur/icons/general_chat_button.png similarity index 100% rename from khweeteur-experimental/icons/general_chat_button.png rename to build/lib/khweeteur/icons/general_chat_button.png diff --git a/khweeteur-experimental/icons/general_presence_home.png b/build/lib/khweeteur/icons/general_presence_home.png similarity index 100% rename from khweeteur-experimental/icons/general_presence_home.png rename to build/lib/khweeteur/icons/general_presence_home.png diff --git a/khweeteur-experimental/icons/geoloc.png b/build/lib/khweeteur/icons/geoloc.png similarity index 100% rename from khweeteur-experimental/icons/geoloc.png rename to build/lib/khweeteur/icons/geoloc.png diff --git a/khweeteur-experimental/icons/khweeteur.png b/build/lib/khweeteur/icons/khweeteur.png similarity index 100% rename from khweeteur-experimental/icons/khweeteur.png rename to build/lib/khweeteur/icons/khweeteur.png diff --git a/khweeteur-experimental/icons/reply.png b/build/lib/khweeteur/icons/reply.png similarity index 100% rename from khweeteur-experimental/icons/reply.png rename to build/lib/khweeteur/icons/reply.png diff --git a/khweeteur-experimental/icons/retweet.png b/build/lib/khweeteur/icons/retweet.png similarity index 100% rename from khweeteur-experimental/icons/retweet.png rename to build/lib/khweeteur/icons/retweet.png diff --git a/khweeteur-experimental/icons/tasklaunch_sms_chat.png b/build/lib/khweeteur/icons/tasklaunch_sms_chat.png similarity index 100% rename from khweeteur-experimental/icons/tasklaunch_sms_chat.png rename to build/lib/khweeteur/icons/tasklaunch_sms_chat.png diff --git a/khweeteur-experimental/list_model.py b/build/lib/khweeteur/list_model.py similarity index 100% rename from khweeteur-experimental/list_model.py rename to build/lib/khweeteur/list_model.py diff --git a/khweeteur-experimental/list_view.py b/build/lib/khweeteur/list_view.py similarity index 100% rename from khweeteur-experimental/list_view.py rename to build/lib/khweeteur/list_view.py diff --git a/khweeteur-experimental/notifications.py b/build/lib/khweeteur/notifications.py similarity index 100% rename from khweeteur-experimental/notifications.py rename to build/lib/khweeteur/notifications.py diff --git a/khweeteur-experimental/qbadgebutton.py b/build/lib/khweeteur/qbadgebutton.py similarity index 100% rename from khweeteur-experimental/qbadgebutton.py rename to build/lib/khweeteur/qbadgebutton.py diff --git a/khweeteur-experimental/qml_gui.py b/build/lib/khweeteur/qml_gui.py similarity index 100% rename from khweeteur-experimental/qml_gui.py rename to build/lib/khweeteur/qml_gui.py diff --git a/khweeteur-experimental/qwidget_gui.py b/build/lib/khweeteur/qwidget_gui.py similarity index 98% rename from khweeteur-experimental/qwidget_gui.py rename to build/lib/khweeteur/qwidget_gui.py index 4807c4a..7e63f3f 100644 --- a/khweeteur-experimental/qwidget_gui.py +++ b/build/lib/khweeteur/qwidget_gui.py @@ -48,6 +48,7 @@ def __init__(self,parent): @dbus.service.signal(dbus_interface='net.khertan.Khweeteur') def require_update(self,optional=None): self.parent.setAttribute(Qt.WA_Maemo5ShowProgressIndicator , True) + print 'DEBUG : require_update' @dbus.service.signal(dbus_interface='net.khertan.Khweeteur', signature='uussssss') @@ -61,6 +62,7 @@ def post_tweet(self, \ action = '', tweet_id = '0', ): + print 'DEBUG : post_tweet' pass class KhweeteurAbout(QMainWindow): @@ -380,11 +382,12 @@ def listen_dbus(self): dbus.set_default_main_loop(self.dbus_loop) self.bus = dbus.SessionBus() #Connect the new tweet signal - self.bus.add_signal_receiver(self.new_tweets, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='new_tweets') - self.bus.add_signal_receiver(self.stop_spinning, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='refresh_ended') + self.bus.add_signal_receiver(self.new_tweets, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='new_tweets') + self.bus.add_signal_receiver(self.stop_spinning, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='refresh_ended') self.dbus_handler = KhweeteurDBusHandler(self) def stop_spinning(self): + print 'DEBUG : stop_spinning' self.setAttribute(Qt.WA_Maemo5ShowProgressIndicator , False) def new_tweets(self,count,msg): @@ -404,12 +407,16 @@ def new_tweets(self,count,msg): QApplication.processEvents() if self.model.call == msg: + print 'DEBUG : new_tweets model.load' self.model.load(msg) + print 'DEBUG : new_tweet end model.load' + + print 'DEBUG : end new_tweet' @pyqtSlot() def show_search(self): terms = self.sender().text() - print 'show_search %s' % (terms,) + self.tb_search_button.setCounter(0) self.home_button.setChecked(False) self.msg_button.setChecked(False) self.tb_search_button.setChecked(True) diff --git a/khweeteur-experimental/retriever.py b/build/lib/khweeteur/retriever.py similarity index 100% rename from khweeteur-experimental/retriever.py rename to build/lib/khweeteur/retriever.py diff --git a/khweeteur-experimental/settings.py b/build/lib/khweeteur/settings.py similarity index 100% rename from khweeteur-experimental/settings.py rename to build/lib/khweeteur/settings.py diff --git a/khweeteur-experimental/tweetslist.py b/build/lib/khweeteur/tweetslist.py similarity index 100% rename from khweeteur-experimental/tweetslist.py rename to build/lib/khweeteur/tweetslist.py diff --git a/khweeteur-experimental/twitpic.py b/build/lib/khweeteur/twitpic.py similarity index 100% rename from khweeteur-experimental/twitpic.py rename to build/lib/khweeteur/twitpic.py diff --git a/khweeteur-experimental/twitter.py b/build/lib/khweeteur/twitter.py similarity index 100% rename from khweeteur-experimental/twitter.py rename to build/lib/khweeteur/twitter.py diff --git a/build/scripts-2.5/khweeteur b/build/scripts-2.5/khweeteur new file mode 100644 index 0000000..baf2406 --- /dev/null +++ b/build/scripts-2.5/khweeteur @@ -0,0 +1,7 @@ +#!/bin/sh +if [ $# = 1 ] +then + exec python /usr/lib/python2.5/site-packages/khweeteur/daemon.py +else + exec python /usr/lib/python2.5/site-packages/khweeteur/__init__.py +fi diff --git a/icons/hicolor/128x128/apps/khweeteur.png b/icons/hicolor/128x128/apps/khweeteur.png new file mode 100644 index 0000000..f6d3294 Binary files /dev/null and b/icons/hicolor/128x128/apps/khweeteur.png differ diff --git a/icons/hicolor/32x32/apps/khweeteur.png b/icons/hicolor/32x32/apps/khweeteur.png new file mode 100644 index 0000000..5b5c9b9 Binary files /dev/null and b/icons/hicolor/32x32/apps/khweeteur.png differ diff --git a/khweeteur-experimental/qml/khweeteur.png b/icons/hicolor/64x64/apps/khweeteur.png similarity index 100% rename from khweeteur-experimental/qml/khweeteur.png rename to icons/hicolor/64x64/apps/khweeteur.png diff --git a/khweeteur-experimental/daemon.pyo b/khweeteur-experimental/daemon.pyo deleted file mode 100644 index 4343fd5..0000000 Binary files a/khweeteur-experimental/daemon.pyo and /dev/null differ diff --git a/khweeteur-experimental/retriever.pyo b/khweeteur-experimental/retriever.pyo deleted file mode 100644 index ae23fc8..0000000 Binary files a/khweeteur-experimental/retriever.pyo and /dev/null differ diff --git a/khweeteur-experimental/tweetslist.pyo b/khweeteur-experimental/tweetslist.pyo deleted file mode 100644 index 775c99b..0000000 Binary files a/khweeteur-experimental/tweetslist.pyo and /dev/null differ diff --git a/khweeteur-experimental/twitter.pyo b/khweeteur-experimental/twitter.pyo deleted file mode 100644 index 1daf796..0000000 Binary files a/khweeteur-experimental/twitter.pyo and /dev/null differ diff --git a/khweeteur.desktop b/khweeteur.desktop new file mode 100644 index 0000000..4562e00 --- /dev/null +++ b/khweeteur.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Encoding=UTF-8 +Version=1.0 +Type=Application +Name=Khweeteur +Exec=/usr/bin/khweeteur_launch.py +Icon=khweeteur diff --git a/khweeteur.png b/khweeteur.png new file mode 100644 index 0000000..f6d3294 Binary files /dev/null and b/khweeteur.png differ diff --git a/khweeteur.service b/khweeteur.service new file mode 100644 index 0000000..7fb8f07 --- /dev/null +++ b/khweeteur.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=net.khertan.khweeteur +Exec=/usr/bin/khweeteur_launch.py diff --git a/khweeteur/__init__.py b/khweeteur/__init__.py new file mode 100644 index 0000000..982943d --- /dev/null +++ b/khweeteur/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Beno๎t HERVIER +# Licenced under GPLv3 + +'''A Twitter client made with Python and Qt''' + +from qwidget_gui import Khweeteur +import os.path + +if __name__ == '__main__': + from subprocess import Popen + Popen(['/usr/bin/python',os.path.join(os.path.dirname(__file__),'daemon.py'),'start']) + app = Khweeteur() + app.exec_() diff --git a/khweeteur/bitly.py b/khweeteur/bitly.py new file mode 100644 index 0000000..9268cac --- /dev/null +++ b/khweeteur/bitly.py @@ -0,0 +1,213 @@ +#!/usr/bin/python2.4 +# +# Copyright 2009 Empeeric LTD. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import simplejson +import urllib,urllib2 +import urlparse +import string + +BITLY_BASE_URL = "http://api.bit.ly/" +BITLY_API_VERSION = "2.0.1" + +VERBS_PARAM = { + 'shorten':'longUrl', + 'expand':'shortUrl', + 'info':'shortUrl', + 'stats':'shortUrl', + 'errors':'', +} + +class BitlyError(Exception): + '''Base class for bitly errors''' + + @property + def message(self): + '''Returns the first argument used to construct this error.''' + return self.args[0] + +class Api(object): + """ API class for bit.ly """ + def __init__(self, login, apikey): + self.login = login + self.apikey = apikey + self._urllib = urllib2 + + def shorten(self,longURL): + """ + Takes either: + A long URL string and returns shortened URL string + Or a list of long URL strings and returns a list of shortened URL strings. + """ + if not isinstance(longURL, list): + longURL = [longURL] + + for index,url in enumerate(longURL): + if not '://' in url: + longURL[index] = "http://" + url + + request = self._getURL("shorten",longURL) + result = self._fetchUrl(request) + json = simplejson.loads(result) + self._CheckForError(json) + + res = [] + for item in json['results'].values(): + if item['shortKeywordUrl'] == "": + res.append(item['shortUrl']) + else: + res.append(item['shortKeywordUrl']) + + if len(res) == 1: + return res[0] + else: + return res + + def expand(self,shortURL): + """ Given a bit.ly url or hash, return long source url """ + request = self._getURL("expand",shortURL) + result = self._fetchUrl(request) + json = simplejson.loads(result) + self._CheckForError(json) + return json['results'][string.split(shortURL, '/')[-1]]['longUrl'] + + def info(self,shortURL): + """ + Given a bit.ly url or hash, + return information about that page, + such as the long source url + """ + request = self._getURL("info",shortURL) + result = self._fetchUrl(request) + json = simplejson.loads(result) + self._CheckForError(json) + return json['results'][string.split(shortURL, '/')[-1]] + + def stats(self,shortURL): + """ Given a bit.ly url or hash, return traffic and referrer data. """ + request = self._getURL("stats",shortURL) + result = self._fetchUrl(request) + json = simplejson.loads(result) + self._CheckForError(json) + return Stats.NewFromJsonDict(json['results']) + + def errors(self): + """ Get a list of bit.ly API error codes. """ + request = self._getURL("errors","") + result = self._fetchUrl(request) + json = simplejson.loads(result) + self._CheckForError(json) + return json['results'] + + def setUrllib(self, urllib): + '''Override the default urllib implementation. + + Args: + urllib: an instance that supports the same API as the urllib2 module + ''' + self._urllib = urllib + + def _getURL(self,verb,paramVal): + if not isinstance(paramVal, list): + paramVal = [paramVal] + + params = [ + ('version',BITLY_API_VERSION), + ('format','json'), + ('login',self.login), + ('apiKey',self.apikey), + ] + + verbParam = VERBS_PARAM[verb] + if verbParam: + for val in paramVal: + params.append(( verbParam,val )) + + encoded_params = urllib.urlencode(params) + return "%s%s?%s" % (BITLY_BASE_URL,verb,encoded_params) + + def _fetchUrl(self,url): + '''Fetch a URL + + Args: + url: The URL to retrieve + + Returns: + A string containing the body of the response. + ''' + + # Open and return the URL + url_data = self._urllib.urlopen(url).read() + return url_data + + def _CheckForError(self, data): + """Raises a BitlyError if bitly returns an error message. + + Args: + data: A python dict created from the bitly json response + Raises: + BitlyError wrapping the bitly error message if one exists. + """ + # bitly errors are relatively unlikely, so it is faster + # to check first, rather than try and catch the exception + if 'ERROR' in data or data['statusCode'] == 'ERROR': + raise BitlyError, data['errorMessage'] + for key in data['results']: + if type(data['results']) is dict and type(data['results'][key]) is dict: + if 'statusCode' in data['results'][key] and data['results'][key]['statusCode'] == 'ERROR': + raise BitlyError, data['results'][key]['errorMessage'] + +class Stats(object): + '''A class representing the Statistics returned by the bitly api. + + The Stats structure exposes the following properties: + status.user_clicks # read only + status.clicks # read only + ''' + + def __init__(self,user_clicks=None,total_clicks=None): + self.user_clicks = user_clicks + self.total_clicks = total_clicks + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the bitly API + Returns: + A bitly.Stats instance + ''' + return Stats(user_clicks=data.get('userClicks', None), + total_clicks=data.get('clicks', None)) + + +if __name__ == '__main__': + testURL1="www.yahoo.com" + testURL2="www.cnn.com" + a=Api(login="pythonbitly",apikey="R_06871db6b7fd31a4242709acaf1b6648") + short=a.shorten(testURL1) + print "Short URL = %s" % short + urlList=[testURL1,testURL2] + shortList=a.shorten(urlList) + print "Short URL list = %s" % shortList + long=a.expand(short) + print "Expanded URL = %s" % long + info=a.info(short) + print "Info: %s" % info + stats=a.stats(short) + print "User clicks %s, total clicks: %s" % (stats.user_clicks,stats.total_clicks) + errors=a.errors() + print "Errors: %s" % errors diff --git a/khweeteur/daemon.py b/khweeteur/daemon.py new file mode 100644 index 0000000..a0e2115 --- /dev/null +++ b/khweeteur/daemon.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +#import sip +#sip.setapi('QString', 2) +#sip.setapi('QVariant', 2) + +from __future__ import with_statement + +import sys +import time +from PySide.QtCore import QSettings +import atexit +import os +from signal import SIGTERM + +import logging + +from retriever import KhweeteurRefreshWorker +from settings import SUPPORTED_ACCOUNTS +import gobject +gobject.threads_init() +import socket +import pickle +import re + +__version__ = '0.5.0' + +import dbus +from dbus.mainloop.glib import DBusGMainLoop +DBusGMainLoop(set_as_default=True) +import threading + +import twitter +from urllib import urlretrieve +import urllib2 +import pickle +import glob + +try: + from PIL import Image +except: + import Image + +from PySide.QtCore import QSettings + +from threading import Thread + +import logging +import os +import os.path +import dbus +import dbus.service + + +#A hook to catch errors +def install_excepthook(version): + '''Install an excepthook called at each unexcepted error''' + __version__ = version + + def my_excepthook(exctype, value, tb): + '''Method which replace the native excepthook''' + #traceback give us all the errors information message like + # the method, file line ... everything like + # we have in the python interpreter + import traceback + trace_s = ''.join(traceback.format_exception(exctype, value, tb)) + print 'Except hook called : %s' % (trace_s) + formatted_text = "%s Version %s\nTrace : %s" % ('Khweeteur', __version__, trace_s) + logging.error(formatted_text) + + sys.excepthook = my_excepthook + + +class Daemon: + """ + A generic daemon class. + Usage: subclass the Daemon class and override the run() method + """ + + def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file(self.stdin, 'r') + so = file(self.stdout, 'a+') + se = file(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + file(self.pidfile, 'w+').write("%s\n" % pid) + + def delpid(self): + os.remove(self.pidfile) + + def start(self): + """ + Start the daemon + """ + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + try: + os.kill(pid, 0) + message = "pidfile %s already exist. Daemon already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + except OSError, err: + sys.stderr.write('pidfile %s already exist. But daemon is dead.\n' % self.pidfile) + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if not pid: + message = "pidfile %s does not exist. Daemon not running?\n" + sys.stderr.write(message % self.pidfile) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, SIGTERM) + time.sleep(0.1) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print str(err) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """ + + +class KhweeteurDBusHandler(dbus.service.Object): + + def __init__(self): + dbus.service.Object.__init__(self, dbus.SessionBus(), '/net/khertan/Khweeteur') + self.m_id = 0 + + @dbus.service.signal(dbus_interface='net.khertan.Khweeteur', + signature='') + def refresh_ended(self): + pass + + @dbus.service.signal(dbus_interface='net.khertan.Khweeteur', + signature='us') + def new_tweets(self, count, ttype): + logging.debug('New tweet notification ttype : %s (%s)' % (ttype,str(type(ttype)),)) + if ttype in ('Mentions', 'DMs'): + m_bus = dbus.SystemBus() + m_notify = m_bus.get_object('org.freedesktop.Notifications', + '/org/freedesktop/Notifications') + iface = dbus.Interface(m_notify, 'org.freedesktop.Notifications') + m_id = 0 + + if ttype == 'DMs': + msg = 'New DMs' + elif ttype == 'Mentions': + msg = 'New mentions' + else: + msg = 'New tweets' + try: + self.m_id = iface.Notify('Khweeteur', + self.m_id, + 'khweeteur', + msg, + msg, + ['default', 'call'], + {'category': 'khweeteur-new-tweets', + 'desktop-entry': 'khweeteur', + 'dbus-callback-default': 'net.khertan.khweeteur /net/khertan/khweeteur net.khertan.khweeteur show_now', + 'count': count, + 'amount': count}, + -1) + except: + pass + + +class KhweeteurDaemon(Daemon): + + def run(self): + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%a, %d %b %Y %H:%M:%S', + filename='/home/user/.khweeteur.log', + filemode='w') + + self.bus = dbus.SessionBus() + self.bus.add_signal_receiver(self.update, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='require_update') + self.bus.add_signal_receiver(self.post_tweet, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='post_tweet') + self.threads = [] #Here to avoid gc + + #Cache Folder + self.cache_path = os.path.join(os.path.expanduser("~"),\ + '.khweeteur', 'cache') + if not os.path.exists(self.cache_path): + os.makedirs(self.cache_path) + #Post Folder + self.post_path = os.path.join(os.path.expanduser("~"),\ + '.khweeteur', 'topost') + if not os.path.exists(self.post_path): + os.makedirs(self.post_path) + + self.dbus_handler = KhweeteurDBusHandler() + + loop = gobject.MainLoop() + gobject.timeout_add_seconds(1, self.update) + logging.debug('Timer added') + loop.run() + + def post_tweet(self, \ + shorten_url=True,\ + serialize=True,\ + text='',\ + lattitude='', + longitude='', + base_url='', + action='', + tweet_id='', + ): + with open(os.path.join(self.post_path, str(time.time())), 'wb') as fhandle: + post = {'shorten_url': shorten_url, + 'serialize': serialize, + 'text': text, + 'lattitude': lattitude, + 'longitude': longitude, + 'base_url': base_url, + 'action': action, + 'tweet_id': tweet_id,} + logging.debug('%s' % (post.__repr__(),)) + pickle.dump(post, fhandle, pickle.HIGHEST_PROTOCOL) + self.do_posts() + + def get_api(self,account): + api = \ + twitter.Api(username=account['consumer_key'], + password=account['consumer_secret'], + access_token_key=account['token_key'], + access_token_secret=account['token_secret'], + base_url=account['base_url'],) + api.SetUserAgent('Khweeteur') + + return api + + def do_posts(self): + settings = QSettings("Khertan Software", "Khweeteur") + accounts = [] + nb_accounts = settings.beginReadArray('accounts') + for index in range(nb_accounts): + settings.setArrayIndex(index) + accounts.append(dict((key, settings.value(key)) for key in settings.allKeys())) + settings.endArray() + + logging.debug('Number of account : %s' % len(accounts)) + + for item in glob.glob(os.path.join(self.post_path, '*')): + logging.debug('Try to post %s' % (item,)) + try: + with open(item, 'rb') as fhandle: + post = pickle.load(fhandle) + text = post['text'] + if post['shorten_url'] == 1: + urls = re.findall("(?Phttps?://[^\s]+)", text) + if len(urls) > 0: + import bitly + a = bitly.Api(login='pythonbitly', + apikey='R_06871db6b7fd31a4242709acaf1b6648') + + for url in urls: + try: + short_url = a.shorten(url) + text = text.replace(url, short_url) + except: + pass + if post['lattitude'] == '': + post['lattitude'] = None + else: + post['lattitude'] = int(post['lattitude']) + if post['longitude'] == '': + post['longitude'] = None + else: + post['longitude'] = int(post['longitude']) + + #Loop on accounts + for account in accounts: + #Reply + if post['action'] == 'reply': #Reply tweet + if account['base_url'] == post['base_url'] \ + and account['use_for_tweet'] == 'true': + api = self.get_api(account) + if post['serialize'] == 1: + api.PostSerializedUpdates(text, + in_reply_to_status_id=int(post['tweet_id']), + latitude=post['lattitude'], longitude=post['longitude']) + else: + api.PostUpdate(text, + in_reply_to_status_id=int(post['tweet_id']), + latitude=post['lattitude'], longitude=post['longitude']) + logging.debug('Posted reply %s' % (text,)) + elif post['action'] == 'retweet': + #Retweet + if account['base_url'] == post['base_url'] \ + and account['use_for_tweet'] == 'true': + api = self.get_api(account) + api.PostRetweet(tweet_id=int(post['tweet_id'])) + logging.debug('Posted retweet %s' % (post['tweet_id'],)) + elif post['action'] == 'tweet': + #Else "simple" tweet + if account['use_for_tweet'] == 'true': + api = self.get_api(account) + if post['serialize'] == 1: + api.PostSerializedUpdates(text, + latitude=post['lattitude'], longitude=post['longitude']) + else: + api.PostUpdate(text, + latitude=post['lattitude'], longitude=post['longitude']) + logging.debug('Posted %s' % (text,)) + elif post['action'] == 'delete': + if account['base_url'] == post['base_url']: + api = self.get_api(account) + api.DestroyStatus(int(post['tweet_id'])) + path = os.path.join(os.path.expanduser('~'), \ + '.khweeteur', \ + 'cache', \ + 'HomeTimeline', \ + post['tweet_id']) + os.remove(path) + logging.debug('Deleted %s' % (post['tweet_id'],)) + elif post['action'] == 'favorite': + if account['base_url'] == post['base_url']: + api = self.get_api(account) + api.CreateFavorite(int(post['tweet_id'])) + logging.debug('Favorited %s' % (post['tweet_id'],)) + elif post['action'] == 'follow': + if account['base_url'] == post['base_url']: + api = self.get_api(account) + api.CreateFriendship(int(post['tweet_id'])) + logging.debug('Follow %s' % (post['tweet_id'],)) + elif post['action'] == 'unfollow': + if account['base_url'] == post['base_url']: + api = self.get_api(account) + api.DestroyFriendship(int(post['tweet_id'])) + logging.debug('Follow %s' % (post['tweet_id'],)) + else: + logging.error('Unknow action : %s' % post['action']) + + os.remove(item) + + except twitter.TwitterError, err: + if err.message == 'Status is a duplicate': + os.remove(item) + else: + logging.error('Do_posts : %s' % (err.message,)) + except StandardError, err: + logging.error('Do_posts : %s' % (str(err),)) + #Emitting the error will block the other tweet post + #raise #can t post, we will keep the file to do it later + except: + logging.error('Do_posts : Unknow error') + + def post_twitpic(self, file_path, text): + settings = QSettings("Khertan Software", "Khweeteur") + + import twitpic + import oauth2 as oauth + import simplejson + + nb_accounts = settings.beginReadArray('accounts') + for index in range(nb_accounts): + settings.setArrayIndex(index) + if (settings.value('base_url') == SUPPORTED_ACCOUNTS[0]['base_url']) \ + and (settings.value('use_for_tweet') == Qt.CheckState): + api = twitter.Api(username=settings.value('consumer_key'), + password=settings.value('consumer_secret'), + access_token_key=settings.value('token_key'), + access_token_secret=settings.value('token_secret'), + base_url=SUPPORTED_ACCOUNTS[0]['base_url']) + twitpic_client = twitpic.TwitPicOAuthClient( + consumer_key=settings.value('consumer_key'), + consumer_secret=settings.value('consumer_secret'), + access_token=api._oauth_token.to_string(), + service_key='f9b7357e0dc5473df5f141145e4dceb0') + + params = {} + params['media'] = 'file://' + file_path + params['message'] = text + response = twitpic_client.create('upload', params) + + if 'url' in response: + self.post(text=url) + + settings.endArray() + + def update(self, option=None): + settings = QSettings("Khertan Software", "Khweeteur") + logging.debug('Setting loaded') + settings.sync() + + #Verify the default interval + if not settings.contains('refresh_interval'): + refresh_interval = 600 + else: + refresh_interval = int(settings.value('refresh_interval')) * 60 + if refresh_interval < 600: + refresh_interval = 600 + logging.debug('refresh interval loaded') + + self.do_posts() + self.retrieve() + gobject.timeout_add_seconds(refresh_interval, self.update) + return False + + def retrieve(self, options=None): + settings = QSettings("Khertan Software", "Khweeteur") + logging.debug('Setting loaded') + try: + #Re read the settings + settings.sync() + logging.debug('Setting synced') + + #Cleaning old thread reference for keep for gc + for thread in self.threads: + if not thread.isAlive(): + self.threads.remove(thread) + logging.debug('Removed a thread') + + #Remove old tweets in cache according to history prefs + try: + keep = int(settings.value('tweetHistory')) + except: + keep = 60 + + for root, folders, files in os.walk(self.cache_path): + for folder in folders: + statuses = [] + uids = glob.glob(os.path.join(root, folder, '*')) + for uid in uids: + uid = os.path.basename(uid) + try: + pkl_file = open(os.path.join(root, folder, uid), 'rb') + status = pickle.load(pkl_file) + pkl_file.close() + statuses.append(status) + except StandardError, err: + logging.debug('Error in cache cleaning: %s,%s' % (err, os.path.join(root, uid))) + statuses.sort(key=lambda status: status.created_at_in_seconds, reverse=True) + for status in statuses[keep:]: + try: + os.remove(os.path.join(root, folder, str(status.id))) + except StandardError, err: + logging.debug('Cannot remove : %s : %s' % (str(status.id), str(err))) + + nb_searches = settings.beginReadArray('searches') + searches = [] + for index in range(nb_searches): + settings.setArrayIndex(index) + searches.append(settings.value('terms')) + settings.endArray() + + nb_accounts = settings.beginReadArray('accounts') + logging.info('Found %s account' % (str(nb_accounts),)) + for index in range(nb_accounts): + settings.setArrayIndex(index) + #Worker + try: + self.threads.append(KhweeteurRefreshWorker(\ + settings.value('base_url'), + settings.value('consumer_key'), + settings.value('consumer_secret'), + settings.value('token_key'), + settings.value('token_secret'), + 'HomeTimeline', self.dbus_handler)) + except Exception, err: + logging.error('Timeline : %s' % str(err)) + + try: + self.threads.append(KhweeteurRefreshWorker(\ + settings.value('base_url'), + settings.value('consumer_key'), + settings.value('consumer_secret'), + settings.value('token_key'), + settings.value('token_secret'), + 'Mentions', self.dbus_handler)) + except Exception, err: + logging.error('Mentions : %s' % str(err)) + + try: + self.threads.append(KhweeteurRefreshWorker(\ + settings.value('base_url'), + settings.value('consumer_key'), + settings.value('consumer_secret'), + settings.value('token_key'), + settings.value('token_secret'), + 'DMs', self.dbus_handler)) + except Exception, err: + logging.error('DMs : %s' % str(err)) + + #Start searches thread + for terms in searches: + try: + self.threads.append(KhweeteurRefreshWorker(\ + settings.value('base_url'), + settings.value('consumer_key'), + settings.value('consumer_secret'), + settings.value('token_key'), + settings.value('token_secret'), + 'Search:'+terms, self.dbus_handler)) + except Exception, err: + logging.error('Search %s: %s' % (terms,str(err))) + + try: + for idx, thread in enumerate(self.threads): + logging.debug('Try to run Thread : %s' % str(thread)) + try: + self.threads[idx].start() + except RuntimeError, err: + logging.debug('Attempt to start a thread already running : %s' % (str(err),)) + except: + logging.error('Running Thread error') + + settings.endArray() + + while any([thread.isAlive() for thread in self.threads]): + time.sleep(1) + + self.dbus_handler.refresh_ended() + + logging.debug('Finished loop') + + except StandardError, err: + logging.exception(str(err)) + logging.debug(str(err)) + + +if __name__ == "__main__": + install_excepthook(__version__) + daemon = KhweeteurDaemon('/var/run/khweeteurd/khweeteurd.pid') + if len(sys.argv) == 2: + if 'start' == sys.argv[1]: + daemon.start() + elif 'stop' == sys.argv[1]: + daemon.stop() + elif 'restart' == sys.argv[1]: + daemon.restart() + else: + print "Unknown command" + sys.exit(2) + sys.exit(0) + else: + print "usage: %s start|stop|restart" % sys.argv[0] + sys.exit(2) diff --git a/khweeteur/icons/favorite.png b/khweeteur/icons/favorite.png new file mode 100644 index 0000000..9296aff Binary files /dev/null and b/khweeteur/icons/favorite.png differ diff --git a/khweeteur/icons/general_chat_button.png b/khweeteur/icons/general_chat_button.png new file mode 100644 index 0000000..c6726b2 Binary files /dev/null and b/khweeteur/icons/general_chat_button.png differ diff --git a/khweeteur/icons/general_presence_home.png b/khweeteur/icons/general_presence_home.png new file mode 100644 index 0000000..e145762 Binary files /dev/null and b/khweeteur/icons/general_presence_home.png differ diff --git a/khweeteur/icons/geoloc.png b/khweeteur/icons/geoloc.png new file mode 100644 index 0000000..88bd9fb Binary files /dev/null and b/khweeteur/icons/geoloc.png differ diff --git a/khweeteur/icons/khweeteur.png b/khweeteur/icons/khweeteur.png new file mode 100644 index 0000000..036e638 Binary files /dev/null and b/khweeteur/icons/khweeteur.png differ diff --git a/khweeteur/icons/reply.png b/khweeteur/icons/reply.png new file mode 100644 index 0000000..5ba48c7 Binary files /dev/null and b/khweeteur/icons/reply.png differ diff --git a/khweeteur/icons/retweet.png b/khweeteur/icons/retweet.png new file mode 100644 index 0000000..bbfc43d Binary files /dev/null and b/khweeteur/icons/retweet.png differ diff --git a/khweeteur/icons/tasklaunch_sms_chat.png b/khweeteur/icons/tasklaunch_sms_chat.png new file mode 100644 index 0000000..06b5dbc Binary files /dev/null and b/khweeteur/icons/tasklaunch_sms_chat.png differ diff --git a/khweeteur/list_model.py b/khweeteur/list_model.py new file mode 100644 index 0000000..be4d7a9 --- /dev/null +++ b/khweeteur/list_model.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +'''A simple Twitter client made with pyqt4 : QModel''' + +#import sip +#sip.setapi('QString', 2) +#sip.setapi('QVariant', 2) + +import time +import pickle +import datetime +import glob +from notifications import KhweeteurNotification +import os + +SCREENNAMEROLE = 20 +REPLYTOSCREENNAMEROLE = 21 +REPLYTEXTROLE = 22 +REPLYIDROLE = 25 +IDROLE = 23 +ORIGINROLE = 24 +TIMESTAMPROLE = 26 +RETWEETOFROLE = 27 +ISMEROLE = 28 +PROTECTEDROLE = 28 +USERIDROLE = 29 + +from PySide.QtCore import QAbstractListModel,QModelIndex, \ + QThread, \ + Qt, \ + QSettings, \ + QObject, \ + Signal + +from PySide.QtGui import QPixmap + +pyqtSignal = Signal + +class KhweetsModel(QAbstractListModel): + + """ListModel : A simple list : Start_At,TweetId, Users Screen_name, Tweet Text, Profile Image""" + + dataChanged = pyqtSignal(QModelIndex,QModelIndex) + + def __init__(self): + QAbstractListModel.__init__(self) + + # Cache the passed data list as a class member. + + self._items = [] + self._uids = [] + + self._avatars = {} + self.now = time.time() + self.call = None + + def setLimit(self, limit): + self.khweets_limit = limit + + def getCacheFolder(self): + return os.path.join(os.path.expanduser("~"), \ + '.khweeteur','cache', \ + os.path.normcase(unicode(self.call.replace('/', \ + '_'))).encode('UTF-8')) + + def rowCount(self, parent=QModelIndex()): + return len(self._items) + + def refreshTimestamp(self): + self.now = time.time() + self.dataChanged.emit(self.createIndex(0, 0), + self.createIndex(0, + len(self._items))) + + def addStatuses(self, uids): + #Optimization + folder_path = self.getCacheFolder() + pickleload = pickle.load + try: + keys = [] + for uid in uids: + try: + pkl_file = open(os.path.join(folder_path, + str(uid)), 'rb') + status = pickleload(pkl_file) + pkl_file.close() + + #Test if status already exists + if status.id not in self._uids: + self._uids.append(status.id) + self._items.append(status) + + except StandardError, e: + print e + + except StandardError, e: + print "We shouldn't got this error here :", e + import traceback + traceback.print_exc() + + self._items.sort() + self._uids.sort() + self.dataChanged.emit(self.createIndex(0, 0), + self.createIndex(0, + len(self._items))) + + def destroyStatus(self, index): + self._items.pop(index.row()) + self.dataChanged.emit(self.createIndex(0, 0), + self.createIndex(0, + len(self._items))) + + + def load(self,call): + + self.now = time.time() + + if self.call != call: + self._items=[] + self.call = call + + try: + folder = self.getCacheFolder() + uids = glob.glob(folder + u'/*') + pickleload = pickle.load + avatar_path = os.path.join(os.path.expanduser("~"), + '.khweeteur','avatars') + for uid in uids: + if uid not in [status.id for status in self._items]: + pkl_file = open(os.path.join(folder, + str(uid)), 'rb') + status = pickleload(pkl_file) + pkl_file.close() + + #Test if status already exists + if status not in self._items: + self._uids.append(status.id) + self._items.append(status) + if hasattr(status, 'user'): + profile_image = os.path.basename(status.user.profile_image_url.replace('/' + , '_')) + else: + profile_image = '/opt/usr/share/icons/hicolor/64x64/hildon/general_default_avatar.png' + + if profile_image not in self._avatars: + try: + self._avatars[status.user.profile_image_url] = QPixmap(os.path.join(avatar_path, + profile_image)) + except: + pass + + self._items.sort(key=lambda status:status.created_at_in_seconds, reverse=True) + + self.dataChanged.emit(self.createIndex(0, 0), + self.createIndex(0, + len(self._items))) + + except StandardError, e: + print 'unSerialize : ', e + + def data(self, index, role=Qt.DisplayRole): + + if role == Qt.DisplayRole: + status = self._items[index.row()] + try: + if status.truncated: + return status.retweeted_status.text + else: + return status.text + except: + return status.text + elif role == SCREENNAMEROLE: + try: + return self._items[index.row()].user.screen_name + except: + return self._items[index.row()].sender_screen_name + elif role == IDROLE: + return self._items[index.row()].id + elif role == REPLYIDROLE: + try: + return self._items[index.row()].in_reply_to_status_id + except: + return None + elif role == REPLYTOSCREENNAMEROLE: + try: + return self._items[index.row()].in_reply_to_screen_name + except: + return None + elif role == REPLYTEXTROLE: + return self._items[index.row()].in_reply_to_status_text + elif role == ORIGINROLE: + return self._items[index.row()].base_url + elif role == RETWEETOFROLE: + try: + return self._items[index.row()].retweeted_status + except: + return None + elif role == ISMEROLE: + try: + return self._items[index.row()].is_me + except: + return False + + elif role == TIMESTAMPROLE: + return self._items[index.row()].GetRelativeCreatedAt(self.now) + + elif role == PROTECTEDROLE: + return self._items[index.row()].user.protected + + elif role == USERIDROLE: + return self._items[index.row()].user.id + + elif role == Qt.DecorationRole: + try: + return self._avatars[self._items[index.row()].user.profile_image_url] + except: + return None + else: + return None + + def wantsUpdate(self): + #QObject.emit(self, SIGNAL('layoutChanged()')) + self.layoutChanged.emit() diff --git a/khweeteur/list_view.py b/khweeteur/list_view.py new file mode 100644 index 0000000..7e344f8 --- /dev/null +++ b/khweeteur/list_view.py @@ -0,0 +1,649 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +'''A simple Twitter client made with pyqt4 : QListView''' + +import sip +sip.setapi('QString', 2) +sip.setapi('QVariant', 2) + +SCREENNAMEROLE = 20 +REPLYTOSCREENNAMEROLE = 21 +REPLYTEXTROLE = 22 +REPLYIDROLE = 25 +IDROLE = 23 +ORIGINROLE = 24 +TIMESTAMPROLE = 26 +RETWEETOFROLE = 27 +ISMEROLE = 28 + + +from PySide.QtGui import QStyledItemDelegate, \ + QListView, \ + QColor, \ + QAbstractItemView, \ + QFontMetrics, \ + QFont, \ + QStyle +from PySide.QtCore import Qt, \ + QSize, \ + QSettings + +from settings import KhweeteurPref + +class WhiteCustomDelegate(QStyledItemDelegate): + + '''Delegate to do custom draw of the items''' + + def __init__(self, parent): + '''Initialization''' + + QStyledItemDelegate.__init__(self, parent) + + self.bg_color = QColor('#FFFFFF') + self.bg_alternate_color = QColor('#dddddd') + self.user_color = QColor('#7AB4F5') + self.time_color = QColor('#7AB4F5') + self.replyto_color = QColor('#7AB4F5') + + self.text_color = QColor('#000000') + self.separator_color = QColor('#000000') + + +class DefaultCustomDelegate(QStyledItemDelegate): + + '''Delegate to do custom draw of the items''' + + memoized_size = {} + memoized_width = {} + + def __init__(self, parent): + '''Initialization''' + + QStyledItemDelegate.__init__(self, parent) + self.show_avatar = True + self.show_screenname = True + self.show_timestamp = True + self.show_replyto = True + + self.bg_color = QColor('#000000') + self.bg_alternate_color = QColor('#333333') + self.user_color = QColor('#7AB4F5') + self.time_color = QColor('#7AB4F5') + self.replyto_color = QColor('#7AB4F5') + + self.text_color = QColor('#FFFFFF') + self.separator_color = QColor('#000000') + + self.fm = None + self.minifm = None + + self.normFont = None + self.miniFont = None + + def sizeHint(self, option, index): + '''Custom size calculation of our items''' + + uid = str(index.data(role=IDROLE)) + 'x' + \ + str(option.rect.width()) + try: + return self.memoized_size[uid] + except: + size = QStyledItemDelegate.sizeHint(self, option, index) + tweet = index.data(Qt.DisplayRole) + + # One time is enought sizeHint need to be fast + + if not self.fm: + self.fm = QFontMetrics(option.font) + height = self.fm.boundingRect( + 0, + 0, + option.rect.width() - 75, + 800, + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), + tweet, + ).height() + 40 + + if self.show_replyto: + reply_name = index.data(role=REPLYTOSCREENNAMEROLE) + reply_text = index.data(role=REPLYTEXTROLE) + if reply_name and reply_text: + + # One time is enought sizeHint need to be fast + + reply = 'In reply to @' + reply_name + ' : ' \ + + reply_text + if not self.minifm: + if not self.miniFont: + self.miniFont = QFont(option.font) + self.miniFont.setPointSizeF(option.font.pointSizeF() + * 0.80) + self.minifm = QFontMetrics(self.miniFont) + height += self.minifm.boundingRect( + 0, + 0, + option.rect.width() - 75, + 800, + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), + reply, + ).height() + elif reply_name: + reply = 'In reply to @' + reply_name + if not self.minifm: + if not self.miniFont: + self.miniFont = QFont(option.font) + self.miniFont.setPointSizeF(option.font.pointSizeF() + * 0.80) + self.minifm = QFontMetrics(self.miniFont) + height += self.minifm.boundingRect( + 0, + 0, + option.rect.width() - 75, + 800, + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), + reply, + ).height() + + if height < 70: + height = 70 + + self.memoized_size[uid] = QSize(size.width(), height) + return self.memoized_size[uid] + + def paint( + self, + painter, + option, + index, + ): + '''Paint our tweet''' + +# if not USE_PYSIDE: + (x1, y1, x2, y2) = option.rect.getCoords() +# else: + #Work arround Pyside bug #544 +# y1 = option.rect.y() +# y2 = y1 + option.rect.height() +# x1 = option.rect.x() +# x2 = x1 + option.rect.width() +# + # Ugly hack ? + if y1 < 0 and y2 < 0: + return + + if not self.fm: + self.fm = QFontMetrics(option.font) + + model = index.model() + tweet = index.data(Qt.DisplayRole) + is_me = index.data(ISMEROLE) + + # Instantiate font only one time ! + + if not self.normFont: + self.normFont = QFont(option.font) + self.miniFont = QFont(option.font) + self.miniFont.setPointSizeF(option.font.pointSizeF() * 0.80) + + painter.save() + + # Draw alternate ? + + if index.row() % 2 == 0: + painter.fillRect(option.rect, self.bg_color) + else: + painter.fillRect(option.rect, self.bg_alternate_color) + + # highlight selected items + + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + + # Draw icon + + if self.show_avatar: + icon = index.data(Qt.DecorationRole) + if icon != None: + if is_me: + painter.drawPixmap(x2 -60, y1 + 10, 50, 50, icon) + else: + painter.drawPixmap(x1 + 10, y1 + 10, 50, 50, icon) + + # Draw tweet + + painter.setPen(self.text_color) + if is_me: + new_rect = \ + painter.drawText(option.rect.adjusted(4, 5, -70, 0), int(Qt.AlignTop) + | int(Qt.AlignRight) + | int(Qt.TextWordWrap), tweet) + else: + new_rect = \ + painter.drawText(option.rect.adjusted(int(self.show_avatar) + * 70, 5, -4, 0), int(Qt.AlignTop) + | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), tweet) + + # Draw Timeline + + if self.show_timestamp: + time = index.data(role=TIMESTAMPROLE) + painter.setFont(self.miniFont) + painter.setPen(self.time_color) + if is_me: + painter.drawText(option.rect.adjusted(4, 10, -80, -9), + int(Qt.AlignBottom) | int(Qt.AlignRight), + time) + else: + painter.drawText(option.rect.adjusted(70, 10, -10, -9), + int(Qt.AlignBottom) | int(Qt.AlignRight), + time) + + # Draw screenname + + if self.show_screenname: + screenname = index.data(SCREENNAMEROLE) + retweet_of = index.data(RETWEETOFROLE) + if retweet_of: + screenname = '%s : Retweet of %s' % (screenname, retweet_of.user.screen_name) + painter.setFont(self.miniFont) + painter.setPen(self.user_color) + if is_me: + painter.drawText(option.rect.adjusted(4, 10, -70, -9), + int(Qt.AlignBottom) | int(Qt.AlignLeft), + screenname) + else: + painter.drawText(option.rect.adjusted(70, 10, -10, -9), + int(Qt.AlignBottom) | int(Qt.AlignLeft), + screenname) + + # Draw reply + + if self.show_replyto: + reply_name = index.data(role=REPLYTOSCREENNAMEROLE) + reply_text = index.data(role=REPLYTEXTROLE) + if reply_name and reply_text: + reply = 'In reply to ' + reply_name + ' : ' \ + + reply_text + painter.setFont(self.miniFont) + painter.setPen(self.replyto_color) + if is_me: + new_rect = \ + painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + else: + new_rect = \ + painter.drawText(option.rect.adjusted(int(self.show_avatar) + * 70, new_rect.height() + 5, -4, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + elif reply_name: + reply = 'In reply to ' + reply_name + painter.setFont(self.miniFont) + painter.setPen(self.replyto_color) + if is_me: + new_rect = \ + painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + else: + new_rect = \ + painter.drawText(option.rect.adjusted(int(self.show_avatar) + * 70, new_rect.height() + 5, -4, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + + # Draw line + + painter.setPen(self.separator_color) + painter.drawLine(x1, y2, x2, y2) + + painter.restore() + + +class MiniDefaultCustomDelegate(QStyledItemDelegate): + + '''Delegate to do custom draw of the items''' + + memoized_size = {} + memoized_width = {} + + def __init__(self, parent): + '''Initialization''' + + QStyledItemDelegate.__init__(self, parent) + self.show_avatar = True + self.show_screenname = True + self.show_timestamp = True + self.show_replyto = True + + self.bg_color = QColor('#000000') + self.bg_alternate_color = QColor('#333333') + self.user_color = QColor('#7AB4F5') + self.time_color = QColor('#7AB4F5') + self.replyto_color = QColor('#7AB4F5') + + self.text_color = QColor('#FFFFFF') + self.separator_color = QColor('#000000') + + self.fm = None + self.minifm = None + + self.normFont = None + self.miniFont = None + + def sizeHint(self, option, index): + '''Custom size calculation of our items''' + + uid = str(index.data(role=IDROLE)) + 'x' + \ + str(option.rect.width()) + try: + return self.memoized_size[uid] + except: + size = QStyledItemDelegate.sizeHint(self, option, index) + tweet = index.data(Qt.DisplayRole) + + # One time is enought sizeHint need to be fast + + if not self.fm: + self.font = QFont(option.font) + self.font.setPointSizeF(option.font.pointSizeF() + * 0.80) + self.fm = QFontMetrics(self.font) + + height = self.fm.boundingRect( + 0, + 0, + option.rect.width() - 75, + 800, + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), + tweet, + ).height() + 40 + + if self.show_replyto: + reply_name = index.data(role=REPLYTOSCREENNAMEROLE) + reply_text = index.data(role=REPLYTEXTROLE) + if reply_name and reply_text: + + # One time is enought sizeHint need to be fast + + reply = 'In reply to @' + reply_name + ' : ' \ + + reply_text + if not self.minifm: + if not self.miniFont: + self.miniFont = QFont(option.font) + self.miniFont.setPointSizeF(option.font.pointSizeF() + * 0.60) + self.minifm = QFontMetrics(self.miniFont) + height += self.minifm.boundingRect( + 0, + 0, + option.rect.width() - 75, + 800, + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), + reply, + ).height() + elif reply_name: + reply = 'In reply to @' + reply_name + if not self.minifm: + if not self.miniFont: + self.miniFont = QFont(option.font) + self.miniFont.setPointSizeF(option.font.pointSizeF() + * 0.60) + self.minifm = QFontMetrics(self.miniFont) + height += self.minifm.boundingRect( + 0, + 0, + option.rect.width() - 75, + 800, + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), + reply, + ).height() + + if height < 70: + height = 70 + + self.memoized_size[uid] = QSize(size.width(), height) + return self.memoized_size[uid] + + def paint( + self, + painter, + option, + index, + ): + '''Paint our tweet''' + +# if not USE_PYSIDE: + (x1, y1, x2, y2) = option.rect.getCoords() +# else: + #Work arround Pyside bug #544 +# y1 = option.rect.y() +# y2 = y1 + option.rect.height() +# x1 = option.rect.x() +# x2 = x1 + option.rect.width() +# + # Ugly hack ? + if y1 < 0 and y2 < 0: + return + + if not self.fm: + self.font = QFont(option.font) + self.font.setPointSizeF(option.font.pointSizeF() + * 0.80) + self.fm = QFontMetrics(self.font) + + model = index.model() + tweet = index.data(Qt.DisplayRole) + is_me = index.data(ISMEROLE) + + # Instantiate font only one time ! + + if not self.normFont: + self.normFont = QFont(self.font) + self.miniFont = QFont(option.font) + self.miniFont.setPointSizeF(option.font.pointSizeF() * 0.60) + + painter.save() + + # Draw alternate ? + + if index.row() % 2 == 0: + painter.fillRect(option.rect, self.bg_color) + else: + painter.fillRect(option.rect, self.bg_alternate_color) + + # highlight selected items + + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + + # Draw icon + + if self.show_avatar: + icon = index.data(Qt.DecorationRole) + if icon != None: + if is_me: + painter.drawPixmap(x2 -60, y1 + 10, 50, 50, icon) + else: + painter.drawPixmap(x1 + 10, y1 + 10, 50, 50, icon) + + # Draw tweet + painter.setFont(self.normFont) + painter.setPen(self.text_color) + if is_me: + new_rect = \ + painter.drawText(option.rect.adjusted(4, 5, -70, 0), int(Qt.AlignTop) + | int(Qt.AlignRight) + | int(Qt.TextWordWrap), tweet) + else: + new_rect = \ + painter.drawText(option.rect.adjusted(int(self.show_avatar) + * 70, 5, -4, 0), int(Qt.AlignTop) + | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), tweet) + + # Draw Timeline + + if self.show_timestamp: + time = index.data(role=TIMESTAMPROLE) + painter.setFont(self.miniFont) + painter.setPen(self.time_color) + if is_me: + painter.drawText(option.rect.adjusted(4, 10, -80, -9), + int(Qt.AlignBottom) | int(Qt.AlignRight), + time) + else: + painter.drawText(option.rect.adjusted(70, 10, -10, -9), + int(Qt.AlignBottom) | int(Qt.AlignRight), + time) + + # Draw screenname + + if self.show_screenname: + screenname = index.data(SCREENNAMEROLE) + retweet_of = index.data(RETWEETOFROLE) + if retweet_of: + screenname = '%s : Retweet of %s' % (screenname, retweet_of.user.screen_name) + painter.setFont(self.miniFont) + painter.setPen(self.user_color) + if is_me: + painter.drawText(option.rect.adjusted(4, 10, -70, -9), + int(Qt.AlignBottom) | int(Qt.AlignLeft), + screenname) + else: + painter.drawText(option.rect.adjusted(70, 10, -10, -9), + int(Qt.AlignBottom) | int(Qt.AlignLeft), + screenname) + + # Draw reply + + if self.show_replyto: + reply_name = index.data(role=REPLYTOSCREENNAMEROLE) + reply_text = index.data(role=REPLYTEXTROLE) + if reply_name and reply_text: + reply = 'In reply to ' + reply_name + ' : ' \ + + reply_text + painter.setFont(self.miniFont) + painter.setPen(self.replyto_color) + if is_me: + new_rect = \ + painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + else: + new_rect = \ + painter.drawText(option.rect.adjusted(int(self.show_avatar) + * 70, new_rect.height() + 5, -4, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + elif reply_name: + reply = 'In reply to ' + reply_name + painter.setFont(self.miniFont) + painter.setPen(self.replyto_color) + if is_me: + new_rect = \ + painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + else: + new_rect = \ + painter.drawText(option.rect.adjusted(int(self.show_avatar) + * 70, new_rect.height() + 5, -4, 0), + int(Qt.AlignTop) | int(Qt.AlignLeft) + | int(Qt.TextWordWrap), reply) + + # Draw line + + painter.setPen(self.separator_color) + painter.drawLine(x1, y2, x2, y2) + + painter.restore() + + +class CoolWhiteCustomDelegate(DefaultCustomDelegate): + + '''Delegate to do custom draw of the items''' + + def __init__(self, parent): + '''Initialization''' + + DefaultCustomDelegate.__init__(self, parent) + + self.user_color = QColor('#3399cc') + self.replyto_color = QColor('#3399cc') + self.time_color = QColor('#94a1a7') + self.bg_color = QColor('#edf1f2') + self.bg_alternate_color = QColor('#e6eaeb') + self.text_color = QColor('#444444') + self.separator_color = QColor('#c8cdcf') + + +class CoolGrayCustomDelegate(DefaultCustomDelegate): + + '''Delegate to do custom draw of the items''' + + def __init__(self, parent): + '''Initialization''' + + DefaultCustomDelegate.__init__(self, parent) + + self.user_color = QColor('#3399cc') + self.time_color = QColor('#94a1a7') + self.replyto_color = QColor('#94a1a7') + self.bg_color = QColor('#4a5153') + self.bg_alternate_color = QColor('#444b4d') + self.text_color = QColor('#FFFFFF') + self.separator_color = QColor('#333536') + + +class KhweetsView(QListView): + + ''' Model View ''' + + def __init__(self, parent=None): + QListView.__init__(self, parent) + self.setWordWrap(True) + self.refreshCustomDelegate() + self.setEditTriggers(QAbstractItemView.SelectedClicked) + self.setSpacing(0) + self.setUniformItemSizes(False) + self.setResizeMode(QListView.Adjust) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + + def keyPressEvent(self, event): + if event.key() not in (Qt.Key_Up, Qt.Key_Down): + self.parent().switch_tb_edit() + self.parent().tb_text.setFocus() + self.parent().tb_text.keyPressEvent(event) + else: + QListView.keyPressEvent(self, event) + + def refreshCustomDelegate(self): + settings = QSettings("Khertan Software", "Khweeteur") + theme = settings.value('theme') + if theme == KhweeteurPref.WHITETHEME: + self.custom_delegate = WhiteCustomDelegate(self) + elif theme == KhweeteurPref.DEFAULTTHEME: + self.custom_delegate = DefaultCustomDelegate(self) + elif theme == KhweeteurPref.COOLWHITETHEME: + self.custom_delegate = CoolWhiteCustomDelegate(self) + elif theme == KhweeteurPref.COOLGRAYTHEME: + self.custom_delegate = CoolGrayCustomDelegate(self) + elif theme == KhweeteurPref.MINITHEME: + self.custom_delegate = MiniDefaultCustomDelegate(self) + else: + self.custom_delegate = DefaultCustomDelegate(self) + self.setItemDelegate(self.custom_delegate) diff --git a/khweeteur/notifications.py b/khweeteur/notifications.py new file mode 100644 index 0000000..eb980d9 --- /dev/null +++ b/khweeteur/notifications.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +from PySide.QtCore import QObject + +try: + import dbus + import dbus.service + from dbus.mainloop.qt import DBusQtMainLoop +# from dbusobj import KhweeteurDBus + noDBUS = False +except: + noDBUS = True + print 'No dbus try with pynotify' + import pynotify + +class KhweeteurNotification(QObject): + '''Notification class interface''' + def __init__(self): + global noDBUS + QObject.__init__(self) + if not noDBUS: + try: + self.m_bus = dbus.SystemBus() + self.m_notify = self.m_bus.get_object('org.freedesktop.Notifications', + '/org/freedesktop/Notifications') + self.iface = dbus.Interface(self.m_notify, 'org.freedesktop.Notifications') + self.m_id = 0 + except: + noDBUS = True + + def warn(self, message): + '''Display an Hildon banner''' + if not noDBUS: + try: + self.iface.SystemNoteDialog(message,0, 'Nothing') + except: + pass + else: + if pynotify.init("Khweeteur"): + n = pynotify.Notification(message, message) + n.show() + + def info(self, message): + '''Display an information banner''' + if not noDBUS: + if isMAEMO: + try: + self.iface.SystemNoteInfoprint('Khweeteur : '+message) + except: + pass + else: + if pynotify.init("Khweeteur"): + n = pynotify.Notification(message, message) + n.show() + + def notify(self,title, message,category='khweeteur-new-tweets', icon='khweeteur',count=1): + '''Create a notification in the style of email one''' + if not noDBUS: + try: + self.m_id = self.iface.Notify('Khweeteur', + self.m_id, + icon, + title, + message, + ['default','call'], + {'category':category, + 'desktop-entry':'khweeteur', + 'dbus-callback-default':'net.khertan.khweeteur /net/khertan/khweeteur net.khertan.khweeteur show_now', + 'count':count, + 'amount':count}, + -1 + ) + except: + pass + + else: + if pynotify.init("Khweeteur"): + n = pynotify.Notification(title, message) + n.show() + diff --git a/khweeteur-experimental/old/__init__.py b/khweeteur/old/__init__.py similarity index 100% rename from khweeteur-experimental/old/__init__.py rename to khweeteur/old/__init__.py diff --git a/khweeteur-experimental/old/client.py b/khweeteur/old/client.py similarity index 100% rename from khweeteur-experimental/old/client.py rename to khweeteur/old/client.py diff --git a/khweeteur-experimental/old/client2.py b/khweeteur/old/client2.py similarity index 100% rename from khweeteur-experimental/old/client2.py rename to khweeteur/old/client2.py diff --git a/khweeteur-experimental/old/daemon.py b/khweeteur/old/daemon.py similarity index 100% rename from khweeteur-experimental/old/daemon.py rename to khweeteur/old/daemon.py diff --git a/khweeteur-experimental/old/oauth2/__init__.py b/khweeteur/old/oauth2/__init__.py similarity index 100% rename from khweeteur-experimental/old/oauth2/__init__.py rename to khweeteur/old/oauth2/__init__.py diff --git a/khweeteur-experimental/old/oauth2/__init__.pyc b/khweeteur/old/oauth2/__init__.pyc similarity index 100% rename from khweeteur-experimental/old/oauth2/__init__.pyc rename to khweeteur/old/oauth2/__init__.pyc diff --git a/khweeteur-experimental/old/oauth2/__init__.pyo b/khweeteur/old/oauth2/__init__.pyo similarity index 100% rename from khweeteur-experimental/old/oauth2/__init__.pyo rename to khweeteur/old/oauth2/__init__.pyo diff --git a/khweeteur-experimental/old/oauth2/clients/__init__.py b/khweeteur/old/oauth2/clients/__init__.py similarity index 100% rename from khweeteur-experimental/old/oauth2/clients/__init__.py rename to khweeteur/old/oauth2/clients/__init__.py diff --git a/khweeteur-experimental/old/oauth2/clients/__init__.pyc b/khweeteur/old/oauth2/clients/__init__.pyc similarity index 100% rename from khweeteur-experimental/old/oauth2/clients/__init__.pyc rename to khweeteur/old/oauth2/clients/__init__.pyc diff --git a/khweeteur-experimental/old/oauth2/clients/imap.py b/khweeteur/old/oauth2/clients/imap.py similarity index 100% rename from khweeteur-experimental/old/oauth2/clients/imap.py rename to khweeteur/old/oauth2/clients/imap.py diff --git a/khweeteur-experimental/old/oauth2/clients/imap.pyc b/khweeteur/old/oauth2/clients/imap.pyc similarity index 100% rename from khweeteur-experimental/old/oauth2/clients/imap.pyc rename to khweeteur/old/oauth2/clients/imap.pyc diff --git a/khweeteur-experimental/old/oauth2/clients/smtp.py b/khweeteur/old/oauth2/clients/smtp.py similarity index 100% rename from khweeteur-experimental/old/oauth2/clients/smtp.py rename to khweeteur/old/oauth2/clients/smtp.py diff --git a/khweeteur-experimental/old/oauth2/clients/smtp.pyc b/khweeteur/old/oauth2/clients/smtp.pyc similarity index 100% rename from khweeteur-experimental/old/oauth2/clients/smtp.pyc rename to khweeteur/old/oauth2/clients/smtp.pyc diff --git a/khweeteur-experimental/old/objects.py b/khweeteur/old/objects.py similarity index 100% rename from khweeteur-experimental/old/objects.py rename to khweeteur/old/objects.py diff --git a/khweeteur-experimental/old/tweetslist.py b/khweeteur/old/tweetslist.py similarity index 100% rename from khweeteur-experimental/old/tweetslist.py rename to khweeteur/old/tweetslist.py diff --git a/khweeteur-experimental/old/tweetslist.pyo b/khweeteur/old/tweetslist.pyo similarity index 100% rename from khweeteur-experimental/old/tweetslist.pyo rename to khweeteur/old/tweetslist.pyo diff --git a/khweeteur-experimental/old/tweetslist.qml b/khweeteur/old/tweetslist.qml similarity index 100% rename from khweeteur-experimental/old/tweetslist.qml rename to khweeteur/old/tweetslist.qml diff --git a/khweeteur-experimental/old/twitter.py b/khweeteur/old/twitter.py similarity index 100% rename from khweeteur-experimental/old/twitter.py rename to khweeteur/old/twitter.py diff --git a/khweeteur-experimental/old/twitter.pyo b/khweeteur/old/twitter.pyo similarity index 100% rename from khweeteur-experimental/old/twitter.pyo rename to khweeteur/old/twitter.pyo diff --git a/khweeteur/qbadgebutton.py b/khweeteur/qbadgebutton.py new file mode 100644 index 0000000..59b3ff6 --- /dev/null +++ b/khweeteur/qbadgebutton.py @@ -0,0 +1,145 @@ +import sys +from PySide.QtGui import * +from PySide.QtCore import Qt + +class QBadgeButton (QPushButton): + + def __init__ (self, icon = None, text = None, parent = None): + if icon: + QPushButton.__init__(self, icon, text, parent) + elif text: + QPushButton.__init__(self, text, parent) + else: + QPushButton.__init__(self, parent) + + self.badge_counter = 0 + self.badge_size = 50 + + self.redGradient = QRadialGradient(0.0, 0.0, 17.0, self.badge_size - 3, self.badge_size - 3); + self.redGradient.setColorAt(0.0, QColor(0xe0, 0x84, 0x9b)); + self.redGradient.setColorAt(0.5, QColor(0xe9, 0x34, 0x43)); + self.redGradient.setColorAt(1.0, QColor(0xdc, 0x0c, 0x00)); + + def setSize (self, size): + self.badge_size = size + + def setCounter (self, counter): + self.badge_counter = counter + self.update() + + def getCounter (self): + return self.badge_counter + + def paintEvent (self, event): + QPushButton.paintEvent(self, event) + p = QPainter(self) + p.setRenderHint(QPainter.TextAntialiasing) + p.setRenderHint(QPainter.Antialiasing) + + if self.badge_counter > 0: + point = self.rect().topRight() + self.drawBadge(p, point.x()-self.badge_size - 1, point.y() + 1, self.badge_size, str(self.badge_counter), QBrush(self.redGradient)) + + def fillEllipse (self, painter, x, y, size, brush): + path = QPainterPath() + path.addEllipse(x, y, size, size); + painter.fillPath(path, brush); + + def drawBadge(self, painter, x, y, size, text, brush): + painter.setFont(QFont(painter.font().family(), 11, QFont.Bold)) + + while ((size - painter.fontMetrics().width(text)) < 10): + pointSize = painter.font().pointSize() - 1 + weight = QFont.Normal if (pointSize < 8) else QFont.Bold + painter.setFont(QFont(painter.font().family(), painter.font().pointSize() - 1, weight)) + + shadowColor = QColor(0, 0, 0, size) + self.fillEllipse(painter, x + 1, y, size, shadowColor) + self.fillEllipse(painter, x - 1, y, size, shadowColor) + self.fillEllipse(painter, x, y + 1, size, shadowColor) + self.fillEllipse(painter, x, y - 1, size, shadowColor) + + painter.setPen(QPen(Qt.white, 2)); + self.fillEllipse(painter, x, y, size - 3, brush) + painter.drawEllipse(x, y, size - 3, size - 3) + + painter.setPen(QPen(Qt.white, 1)); + painter.drawText(x, y, size - 2, size - 2, Qt.AlignCenter, text); + +class QToolBadgeButton (QToolButton): + + def __init__ (self, parent = None): + QToolButton.__init__(self, parent) + + self.badge_counter = 0 + self.badge_size = 25 + + self.redGradient = QRadialGradient(0.0, 0.0, 17.0, self.badge_size - 3, self.badge_size - 3); + self.redGradient.setColorAt(0.0, QColor(0xe0, 0x84, 0x9b)); + self.redGradient.setColorAt(0.5, QColor(0xe9, 0x34, 0x43)); + self.redGradient.setColorAt(1.0, QColor(0xdc, 0x0c, 0x00)); + + def setSize (self, size): + self.badge_size = size + + def setCounter (self, counter): + self.badge_counter = counter + + def getCounter (self): + return self.badge_counter + + def paintEvent (self, event): + QToolButton.paintEvent(self, event) + p = QPainter(self) + p.setRenderHint(QPainter.TextAntialiasing) + p.setRenderHint(QPainter.Antialiasing) + if self.badge_counter > 0: + point = self.rect().topRight() + self.drawBadge(p, point.x()-self.badge_size, point.y(), self.badge_size, str(self.badge_counter), QBrush(self.redGradient)) + + def fillEllipse (self, painter, x, y, size, brush): + path = QPainterPath() + path.addEllipse(x, y, size, size); + painter.fillPath(path, brush); + + def drawBadge(self, painter, x, y, size, text, brush): + painter.setFont(QFont(painter.font().family(), 11, QFont.Bold)) + + while ((size - painter.fontMetrics().width(text)) < 10): + pointSize = painter.font().pointSize() - 1 + weight = QFont.Normal if (pointSize < 8) else QFont.Bold + painter.setFont(QFont(painter.font().family(), painter.font().pointSize() - 1, weight)) + + shadowColor = QColor(0, 0, 0, size) + self.fillEllipse(painter, x + 1, y, size, shadowColor) + self.fillEllipse(painter, x - 1, y, size, shadowColor) + self.fillEllipse(painter, x, y + 1, size, shadowColor) + self.fillEllipse(painter, x, y - 1, size, shadowColor) + + painter.setPen(QPen(Qt.white, 2)); + self.fillEllipse(painter, x, y, size - 3, brush) + painter.drawEllipse(x, y, size - 2, size - 2) + + painter.setPen(QPen(Qt.white, 1)); + painter.drawText(x, y, size - 2, size - 2, Qt.AlignCenter, text); + +if __name__ == '__main__': + + app = QApplication(sys.argv) + win = QMainWindow() + + toolbar = QToolBar('Toolbar') + win.addToolBar(Qt.BottomToolBarArea, toolbar) + b = QToolBadgeButton(win) + b.setText("test") + b.setCounter(22) + toolbar.addWidget(b) + + w = QBadgeButton(parent=win) + w.setText("test") + w.setCounter(22) + win.setCentralWidget(w) + win.show() + + sys.exit(app.exec_()) + diff --git a/khweeteur-experimental/qml/add.png b/khweeteur/qml/add.png similarity index 100% rename from khweeteur-experimental/qml/add.png rename to khweeteur/qml/add.png diff --git a/khweeteur-experimental/qml/default.png b/khweeteur/qml/default.png similarity index 100% rename from khweeteur-experimental/qml/default.png rename to khweeteur/qml/default.png diff --git a/khweeteur-experimental/qml/fullsize.png b/khweeteur/qml/fullsize.png similarity index 100% rename from khweeteur-experimental/qml/fullsize.png rename to khweeteur/qml/fullsize.png diff --git a/khweeteur-experimental/qml/house.png b/khweeteur/qml/house.png similarity index 100% rename from khweeteur-experimental/qml/house.png rename to khweeteur/qml/house.png diff --git a/khweeteur/qml/khweeteur.png b/khweeteur/qml/khweeteur.png new file mode 100644 index 0000000..c6f7691 Binary files /dev/null and b/khweeteur/qml/khweeteur.png differ diff --git a/khweeteur-experimental/qml/refresh.png b/khweeteur/qml/refresh.png similarity index 100% rename from khweeteur-experimental/qml/refresh.png rename to khweeteur/qml/refresh.png diff --git a/khweeteur-experimental/qml/tweetslist.qml b/khweeteur/qml/tweetslist.qml similarity index 100% rename from khweeteur-experimental/qml/tweetslist.qml rename to khweeteur/qml/tweetslist.qml diff --git a/khweeteur/qml_gui.py b/khweeteur/qml_gui.py new file mode 100644 index 0000000..5cc0497 --- /dev/null +++ b/khweeteur/qml_gui.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +'''A Twitter client made with PySide and QML''' + +__version__ = '0.2.0' + +from PySide.QtGui import * +from PySide.QtCore import * +from PySide.QtDeclarative import * +from PySide.QtMaemo5 import * + +try: + from PySide import QtOpenGL + USE_GL = True +except: + USE_GL = False + print 'Not using Open_GL' + +import os +import sys + +import twitter +from tweetslist import * + +import glob +import pickle +import time + +class Khweeteur(QApplication): + def __init__(self): + QApplication.__init__(self,sys.argv) + self.run() + + def run(self): + self.win = KhweeteurWin() + self.win.showFullScreen() + +class KhweeteurWin(QMainWindow): + def __init__(self,parent=None): + QMainWindow.__init__(self,parent) + self.view = QDeclarativeView() + + self.setAttribute(Qt.WA_Maemo5AutoOrientation, True) + self.setAttribute(Qt.WA_Maemo5StackedWindow, True) + self.setWindowTitle(self.tr('Khweeteur')) + + self.listen() + + controller = Controller() + controller.switch_fullscreen.connect(self.switch_fullscreen) + statusesList = TweetsListModel() + controller.switch_list.connect(statusesList.load_list) + statusesList.load_list('HomeTimeline') + + self.context = self.view.rootContext() + + self.context.setContextProperty('controller', controller) + self.context.setContextProperty('tweetsListModel', statusesList) + + self.buttonList = ToolbarListModel() + self.context.setContextProperty('toolbarListModel', self.buttonList) + + self.view.setSource('qml/tweetslist.qml') + + if USE_GL: + glw = QtOpenGL.QGLWidget() + self.view.setViewport(glw) + self.view.setResizeMode(QDeclarativeView.SizeRootObjectToView) + self.setCentralWidget(self.view) + + def listen(self): + import dbus + import dbus.service + from dbus.mainloop.qt import DBusQtMainLoop + self.dbus_loop = DBusQtMainLoop() + dbus.set_default_main_loop(self.dbus_loop) + self.bus = dbus.SessionBus() #should connect to system bus instead of session because the former is where the incoming signals come from + self.bus.add_signal_receiver(self.new_tweets, path='/net/khertan/Khweeteur/NewTweets', dbus_interface='net.khertan.Khweeteur', signal_name=None) + + def new_tweets(self,count,msg): + print 'New Tweets dbus signal received' + print count,msg + self.buttonList.setCount(msg,count) + #TODO + #Do something with the signal + + + @Slot() + def switch_fullscreen(self): + if self.isFullScreen(): + self.showMaximized() + else: + self.showFullScreen() + +if __name__ == '__main__': + + from subprocess import Popen + Popen(['/usr/bin/python',os.path.join(os.path.dirname(__file__),'daemon.py'),'start']) + app = Khweeteur() + app.exec_() + + \ No newline at end of file diff --git a/khweeteur/qwidget_gui.py b/khweeteur/qwidget_gui.py new file mode 100644 index 0000000..7e63f3f --- /dev/null +++ b/khweeteur/qwidget_gui.py @@ -0,0 +1,773 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +'''A Twitter client made with Python and Qt''' + +__version__ = '0.5.0' + +#import sip +#sip.setapi('QString', 2) +#sip.setapi('QVariant', 2) + +from PySide.QtGui import * +from PySide.QtCore import * +from PySide.QtMaemo5 import * + +import dbus +import dbus.service + +import os +import sys + +from qbadgebutton import QBadgeButton, QToolBadgeButton + +import twitter + +import glob +import pickle +import time + +from list_view import * +from list_model import * + +import re + +from QtMobility.Location import * + +pyqtSignal = Signal +pyqtSlot = Slot + +class KhweeteurDBusHandler(dbus.service.Object): + def __init__(self,parent): + dbus.service.Object.__init__(self, dbus.SessionBus(), '/net/khertan/Khweeteur') + self.parent = parent + + @dbus.service.signal(dbus_interface='net.khertan.Khweeteur') + def require_update(self,optional=None): + self.parent.setAttribute(Qt.WA_Maemo5ShowProgressIndicator , True) + print 'DEBUG : require_update' + + @dbus.service.signal(dbus_interface='net.khertan.Khweeteur', + signature='uussssss') + def post_tweet(self, \ + shorten_url=1,\ + serialize=1,\ + text='',\ + lattitude='0', + longitude='0', + base_url = '', + action = '', + tweet_id = '0', + ): + print 'DEBUG : post_tweet' + pass + +class KhweeteurAbout(QMainWindow): + + '''About Window''' + + def __init__(self, parent=None): + QMainWindow.__init__(self, parent) + self.parent = parent + + self.settings = QSettings() + + try: # Preferences not set yet + if int(self.settings.value('useAutoRotation')) == 2: + self.setAttribute(Qt.WA_Maemo5AutoOrientation, True) + except: + self.setAttribute(Qt.WA_Maemo5AutoOrientation, True) + + self.setAttribute(Qt.WA_Maemo5StackedWindow, True) + self.setWindowTitle(self.tr('Khweeteur About')) + + try: + aboutScrollArea = QScrollArea(self) + aboutScrollArea.setWidgetResizable(True) + awidget = QWidget(aboutScrollArea) + awidget.setMinimumSize(480, 1400) + awidget.setSizePolicy(QSizePolicy.Expanding, + QSizePolicy.Expanding) + aboutScrollArea.setSizePolicy(QSizePolicy.Expanding, + QSizePolicy.Expanding) + + # Kinetic scroller is available on Maemo and should be on meego + + try: + scroller = aboutScrollArea.property('kineticScroller') + scroller.setEnabled(True) + except: + pass + + aboutLayout = QVBoxLayout(awidget) + except: + awidget = QWidget(self) + aboutLayout = QVBoxLayout(awidget) + + aboutIcon = QLabel() + try: + aboutIcon.setPixmap(QIcon.fromTheme('khweeteur' + ).pixmap(128, 128)) + except: + aboutIcon.setPixmap(QIcon(os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'icons', 'khweeteur.png')).pixmap(128, + 128)) + + aboutIcon.setAlignment(Qt.AlignCenter or Qt.AlignHCenter) + aboutIcon.resize(128, 128) + aboutLayout.addWidget(aboutIcon) + + aboutLabel = \ + QLabel(self.tr('''
Khweeteur %s +

An easy to use twitter client +

Licenced under GPLv3 +
By Benoît HERVIER (Khertan) +

Khweeteur try to be simple and fast +
identi.ca and twitter client
+

Features +
Support multiple account +
Notify DMs and Mentions even when not launched +
Reply, Retweet, Follow/Unfollow user, Favorite, Delete your tweet +
Disconnected mode, action will be done when you recover network +
Twitpic upload +
Automated OAuth authentification +

Shortcuts : +
Control-R : Refresh current view +
Control-M : Reply to selected tweet +
Control-Up : To scroll to top +
Control-Bottom : To scroll to bottom +

Thanks to : +
ddoodie on #pyqt +
xnt14 on #maemo +
trebormints on twitter +
moubaildotcom on twitter +
teotwaki on twitter +
Jaffa on maemo.org +
creip on Twitter +
zcopley on #statusnet +
jordan_c on #statusnet +
''') + % __version__) + aboutLayout.addWidget(aboutLabel) + self.bugtracker_button = QPushButton(self.tr('BugTracker')) + self.bugtracker_button.clicked.connect(self.open_bugtracker) + self.website_button = QPushButton(self.tr('Website')) + self.website_button.clicked.connect(self.open_website) + awidget2 = QWidget() + buttonLayout = QHBoxLayout(awidget2) + buttonLayout.addWidget(self.bugtracker_button) + buttonLayout.addWidget(self.website_button) + aboutLayout.addWidget(awidget2) + + try: + awidget.setLayout(aboutLayout) + aboutScrollArea.setWidget(awidget) + self.setCentralWidget(aboutScrollArea) + except: + self.setCentralWidget(awidget) + + self.show() + + def open_website(self): + QDesktopServices.openUrl(QUrl('http://khertan.net/khweeteur')) + + def open_bugtracker(self): + QDesktopServices.openUrl(QUrl('http://khertan.net/khweeteur/bugs' + )) +class Khweeteur(QApplication): + def __init__(self): + QApplication.__init__(self,sys.argv) + self.setOrganizationName("Khertan Software") + self.setOrganizationDomain("khertan.net") + self.setApplicationName("Khweeteur") + + self.run() + + def run(self): + self.win = KhweeteurWin() + self.win.show() + +class KhweeteurWin(QMainWindow): + def __init__(self,parent=None): + QMainWindow.__init__(self,parent) + + self.setAttribute(Qt.WA_Maemo5AutoOrientation, True) + self.setAttribute(Qt.WA_Maemo5StackedWindow, True) + self.setWindowTitle('Khweeteur') + + self.listen_dbus() + + self.view = KhweetsView() + self.model = KhweetsModel() + self.view.setModel(self.model) + self.view.clicked.connect(self.switch_tb_action) + + self.dbus_handler.require_update() + + self.toolbar = QToolBar('Toolbar') + self.addToolBar(Qt.BottomToolBarArea, self.toolbar) + + self.toolbar_mode = 0 #0 - Default , 1 - Edit, 2 - Action + + self.list_tb_action = [] + self.edit_tb_action = [] + self.action_tb_action = [] + + #Switch to edit mode (default) + self.tb_new = QAction(QIcon.fromTheme('khweeteur' + ), 'New', self) + self.tb_new.triggered.connect(self.switch_tb_edit) + self.toolbar.addAction(self.tb_new) + self.list_tb_action.append(self.tb_new) + + #Back button (Edit + Action) + self.tb_back = QAction(QIcon.fromTheme('general_back' + ), 'Back', self) + self.tb_back.triggered.connect(self.switch_tb_default) + self.toolbar.addAction(self.tb_back) + self.edit_tb_action.append(self.tb_back) + self.action_tb_action.append(self.tb_back) + + self.setupMenu() + + #Twitpic button + self.tb_twitpic = QAction(QIcon.fromTheme('tasklaunch_images' + ), 'Twitpic', self) + self.tb_twitpic.triggered.connect(self.do_tb_twitpic) + self.toolbar.addAction(self.tb_twitpic) + self.edit_tb_action.append(self.tb_twitpic) + + #Text field (edit) + self.tb_text = QPlainTextEdit() + self.tb_text_reply_id = 0 + self.tb_text_reply_base_url = '' + self.tb_text.setFixedHeight(66) + self.edit_tb_action.append(self.toolbar.addWidget(self.tb_text)) + + #Char count (Edit) + self.tb_charCounter = QLabel('140') + self.edit_tb_action.append(self.toolbar.addWidget(self.tb_charCounter)) + self.tb_text.textChanged.connect(self.countCharsAndResize) + + #Send tweet (Edit) + self.tb_send = QAction(QIcon.fromTheme('khweeteur' + ), 'Tweet', self) + self.tb_send.triggered.connect(self.do_tb_send) + self.tb_send.setVisible(False) + self.toolbar.addAction(self.tb_send) + self.edit_tb_action.append(self.tb_send) + + #Refresh (Default) + self.tb_update = QAction(QIcon.fromTheme('general_refresh' + ), 'Update', self) + self.tb_update.triggered.connect(self.dbus_handler.require_update) + self.toolbar.addAction(self.tb_update) + self.list_tb_action.append(self.tb_update) + + #Home (Default) + self.home_button = QToolBadgeButton(self) + self.home_button.setText("Home") + self.home_button.setCheckable(True) + self.home_button.setChecked(True) + self.home_button.clicked.connect(self.show_hometimeline) + self.list_tb_action.append(self.toolbar.addWidget(self.home_button)) + + #Mentions (Default) + self.mention_button = QToolBadgeButton(self) + self.mention_button.setText("Mentions") + self.mention_button.setCheckable(True) + self.mention_button.clicked.connect(self.show_mentions) + self.list_tb_action.append(self.toolbar.addWidget(self.mention_button)) + + #DM (Default) + self.msg_button = QToolBadgeButton(self) + self.msg_button.setText("DMs") + self.msg_button.setCheckable(True) + self.msg_button.clicked.connect(self.show_dms) + self.list_tb_action.append(self.toolbar.addWidget(self.msg_button)) + + #Search Button + self.tb_search_menu = QMenu() + self.loadSearchMenu() + + #Search (Default) + self.tb_search_button = QToolBadgeButton(self) + self.tb_search_button.setText("") + self.tb_search_button.setIcon(QIcon.fromTheme('general_search')) + self.tb_search_button.setMenu(self.tb_search_menu) + self.tb_search_button.setPopupMode(QToolButton.InstantPopup) + self.tb_search_button.setCheckable(True) + self.tb_search_button.clicked.connect(self.show_search) + self.list_tb_action.append(self.toolbar.addWidget(self.tb_search_button)) + + #Reply button (Action) + self.tb_reply = QAction('Reply', self) + self.tb_reply.setShortcut('Ctrl+M') + self.toolbar.addAction(self.tb_reply) + self.tb_reply.triggered.connect(self.do_tb_reply) + self.action_tb_action.append(self.tb_reply) + + #Retweet (Action) + self.tb_retweet = QAction('Retweet', self) + self.tb_retweet.setShortcut('Ctrl+P') + self.toolbar.addAction(self.tb_retweet) + self.tb_retweet.triggered.connect(self.do_tb_retweet) + self.action_tb_action.append(self.tb_retweet) + + #Follow (Action) + self.tb_follow = QAction('Follow', self) + self.tb_follow.triggered.connect(self.do_tb_follow) + self.toolbar.addAction(self.tb_follow) + self.action_tb_action.append(self.tb_follow) + + #UnFollow (Action) + self.tb_unfollow = QAction('Unfollow', self) + self.tb_unfollow.triggered.connect(self.do_tb_unfollow) + self.toolbar.addAction(self.tb_unfollow) + self.action_tb_action.append(self.tb_unfollow) + + #Favorite (Action) + self.tb_favorite = QAction('Favorite', self) + self.tb_favorite.triggered.connect(self.do_tb_favorite) + self.toolbar.addAction(self.tb_favorite) + self.action_tb_action.append(self.tb_favorite) + + #Open URLs (Action) + self.tb_urls = QAction('Open URLs', self) + self.tb_urls.setShortcut('Ctrl+O') + self.toolbar.addAction(self.tb_urls) + self.tb_urls.triggered.connect(self.do_tb_openurl) + self.action_tb_action.append(self.tb_urls) + + #Delete (Action) + self.tb_delete = QAction('Delete', self) + self.toolbar.addAction(self.tb_delete) + self.tb_delete.triggered.connect(self.do_tb_delete) + self.action_tb_action.append(self.tb_delete) + + # Actions not in toolbar + + self.tb_scrolltop = QAction('Scroll to top', self) + self.tb_scrolltop.setShortcut(Qt.CTRL + Qt.Key_Up) + self.tb_scrolltop.triggered.connect(self.view.scrollToTop) + self.addAction(self.tb_scrolltop) + + self.tb_scrollbottom = QAction('Scroll to bottom', self) + self.tb_scrollbottom.setShortcut(Qt.CTRL + Qt.Key_Down) + self.tb_scrollbottom.triggered.connect(self.view.scrollToBottom) + self.addAction(self.tb_scrollbottom) + + self.switch_tb_default() + + self.model.load('HomeTimeline') + self.setCentralWidget(self.view) + + QApplication.processEvents() + + self.geolocDoStart() + + def enterEvent(self,event): + """ + Redefine the enter event to refresh recent file list + """ + print 'EnterEvent' + self.model.refreshTimestamp() + + def listen_dbus(self): + from dbus.mainloop.qt import DBusQtMainLoop + self.dbus_loop = DBusQtMainLoop() + dbus.set_default_main_loop(self.dbus_loop) + self.bus = dbus.SessionBus() + #Connect the new tweet signal + self.bus.add_signal_receiver(self.new_tweets, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='new_tweets') + self.bus.add_signal_receiver(self.stop_spinning, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='refresh_ended') + self.dbus_handler = KhweeteurDBusHandler(self) + + def stop_spinning(self): + print 'DEBUG : stop_spinning' + self.setAttribute(Qt.WA_Maemo5ShowProgressIndicator , False) + + def new_tweets(self,count,msg): + print 'New Tweets dbus signal received' + print count,msg + if msg == 'HomeTimeline': + self.home_button.setCounter(self.home_button.getCounter()+count) + QApplication.processEvents() + elif msg == 'Mentions': + self.mention_button.setCounter(self.mention_button.getCounter()+count) + QApplication.processEvents() + elif msg == 'DMs': + self.msg_button.setCounter(self.msg_button.getCounter()+count) + QApplication.processEvents() + elif msg.startswith('Search:'): + self.tb_search_button.setCounter(self.tb_search_button.getCounter()+count) + QApplication.processEvents() + + if self.model.call == msg: + print 'DEBUG : new_tweets model.load' + self.model.load(msg) + print 'DEBUG : new_tweet end model.load' + + print 'DEBUG : end new_tweet' + + @pyqtSlot() + def show_search(self): + terms = self.sender().text() + self.tb_search_button.setCounter(0) + self.home_button.setChecked(False) + self.msg_button.setChecked(False) + self.tb_search_button.setChecked(True) + self.mention_button.setChecked(False) + self.view.scrollToTop() + self.model.load('Search:'+terms) + + @pyqtSlot() + def show_hometimeline(self): + self.home_button.setCounter(0) + self.home_button.setChecked(True) + self.msg_button.setChecked(False) + self.tb_search_button.setChecked(False) + self.mention_button.setChecked(False) + self.view.scrollToTop() + self.model.load('HomeTimeline') + + @pyqtSlot() + def switch_tb_default(self): + print 'Switch tb default' + self.tb_text.setPlainText('') + self.tb_text_reply_id = 0 + self.tb_text_reply_base_url = '' + self.toolbar_mode = 0 + self.switch_tb() + + @pyqtSlot() + def switch_tb_edit(self): + print 'Switch tb edit' + self.toolbar_mode = 1 + self.switch_tb() + + @pyqtSlot() + def switch_tb_action(self): + if self.toolbar_mode != 2: + self.toolbar_mode = 2 + self.switch_tb() + for index in self.view.selectedIndexes(): + isme = self.model.data(index, role=ISMEROLE) + if isme: + self.tb_follow.setVisible(False) + self.tb_unfollow.setVisible(False) + self.tb_delete.setVisible(True) + else: + self.tb_delete.setVisible(False) + self.tb_follow.setVisible(True) + self.tb_unfollow.setVisible(True) + + def switch_tb(self): + mode = self.toolbar_mode + print mode,type(mode) + for item in self.list_tb_action: + item.setVisible(mode == 0) + self.view.setFocus() + for item in self.edit_tb_action: + item.setVisible(mode == 1) + if mode == 1: + self.tb_text.setFocus() + for item in self.action_tb_action: + item.setVisible(mode == 2) + if mode in (1, 2): + self.tb_back.setVisible(True) + + @pyqtSlot() + def do_tb_twitpic(self): + pass + + @pyqtSlot() + def do_tb_openurl(self): + for index in self.view.selectedIndexes(): + status = self.model.data(index) + try: + urls = re.findall("(?Phttps?://[^\s]+)", status) + for url in urls: + QDesktopServices.openUrl(QUrl(url)) + except: + raise + + @pyqtSlot() + def do_tb_send(self): + is_not_reply = self.tb_text_reply_id==0 + self.dbus_handler.post_tweet( \ + 1,#shorten_url=\ + 1,#serialize=\ + self.tb_text.toPlainText(),#text=\ + '' if self.geoloc_source==None else self.geoloc_source[0], #lattitude = + '' if self.geoloc_source==None else self.geoloc_source[1], #longitude = + '' if is_not_reply else self.tb_text_reply_base_url, #base_url + 'tweet' if is_not_reply else 'reply', #action + '' if is_not_reply else str(self.tb_text_reply_id),) + self.switch_tb_default() + self.dbus_handler.require_update() + + @pyqtSlot() + def do_tb_reply(self): + tweet_id = None + for index in self.view.selectedIndexes(): + tweet_id = self.model.data(index, role=IDROLE) + tweet_source = self.model.data(index, role=ORIGINROLE) + tweet_screenname = self.model.data(index, role=SCREENNAMEROLE) + if tweet_id: + self.tb_text.setPlainText('@'+tweet_screenname+self.tb_text.toPlainText()) + self.tb_text_reply_id = tweet_id + self.tb_text_reply_base_url = tweet_source + self.switch_tb_edit() + + @pyqtSlot() + def do_tb_retweet(self): + tweet_id = None + for index in self.view.selectedIndexes(): + tweet_id = self.model.data(index, role=IDROLE) + tweet_source = self.model.data(index, role=ORIGINROLE) + print 'protected ?',self.model.data(index, role=PROTECTEDROLE),type(self.model.data(index, role=PROTECTEDROLE)) + if self.model.data(index, role=PROTECTEDROLE): + screenname = self.model.data(index, role=SCREENNAMEROLE) + QMessageBox.warning(self, + "Khweeteur - Retweet", + "%s protect his tweets you can't retweet them" % screenname, + QMessageBox.Close + ) + + if tweet_id: + self.dbus_handler.post_tweet( \ + 0,#shorten_url=\ + 0,#serialize=\ + '',#text=\ + '', #lattitude = + '', #longitude = + tweet_source, #base_url = + 'retweet', + str(tweet_id), #tweet_id = + ) + self.switch_tb_default() + self.dbus_handler.require_update() + + @pyqtSlot() + def do_tb_delete(self): + tweet_id = None + for index in self.view.selectedIndexes(): + tweet_id = self.model.data(index, role=IDROLE) + tweet_source = self.model.data(index, role=ORIGINROLE) + + if tweet_id: + self.dbus_handler.post_tweet( \ + 0,#shorten_url=\ + 0,#serialize=\ + '',#text=\ + '', #lattitude = + '', #longitude = + tweet_source, #base_url = + 'delete', + str(tweet_id), #tweet_id = + ) + self.switch_tb_default() + self.dbus_handler.require_update() + + @pyqtSlot() + def do_tb_favorite(self): + tweet_id = None + for index in self.view.selectedIndexes(): + tweet_id = self.model.data(index, role=IDROLE) + tweet_source = self.model.data(index, role=ORIGINROLE) + + if tweet_id: + self.dbus_handler.post_tweet( \ + 0,#shorten_url=\ + 0,#serialize=\ + '',#text=\ + '', #lattitude = + '', #longitude = + tweet_source, #base_url = + 'favorite', + str(tweet_id), #tweet_id = + ) + self.switch_tb_default() + self.dbus_handler.require_update() + + @pyqtSlot() + def do_tb_follow(self): + user_id = None + for index in self.view.selectedIndexes(): + user_id = self.model.data(index, role=USERIDROLE) + tweet_source = self.model.data(index, role=ORIGINROLE) + + if user_id: + self.dbus_handler.post_tweet( \ + 0,#shorten_url=\ + 0,#serialize=\ + '',#text=\ + '', #lattitude = + '', #longitude = + tweet_source, #base_url = + 'follow', + str(user_id), #tweet_id = + ) + self.switch_tb_default() + self.dbus_handler.require_update() + + @pyqtSlot() + def do_tb_unfollow(self): + user_id = None + for index in self.view.selectedIndexes(): + user_id = self.model.data(index, role=USERIDROLE) + tweet_source = self.model.data(index, role=ORIGINROLE) + + if user_id: + self.dbus_handler.post_tweet( \ + 0,#shorten_url=\ + 0,#serialize=\ + '',#text=\ + '', #lattitude = + '', #longitude = + tweet_source, #base_url = + 'unfollow', + str(user_id), #tweet_id = + ) + self.switch_tb_default() + self.dbus_handler.require_update() + + @pyqtSlot() + def show_mentions(self): + self.mention_button.setCounter(0) + self.mention_button.setChecked(True) + self.msg_button.setChecked(False) + self.tb_search_button.setChecked(False) + self.home_button.setChecked(False) + self.view.scrollToTop() + self.model.load('Mentions') + + @pyqtSlot() + def show_dms(self): + self.msg_button.setCounter(0) + self.msg_button.setChecked(True) + self.home_button.setChecked(False) + self.tb_search_button.setChecked(False) + self.mention_button.setChecked(False) + self.view.scrollToTop() + self.model.load('DMs') + + @pyqtSlot() + def countCharsAndResize(self): + local_self = self.tb_text + self.tb_charCounter.setText(unicode(140 + - len(local_self.toPlainText()))) + doc = local_self.document() + cursor = local_self.cursorRect() + s = doc.size() + s.setHeight((s.height() + 1) + * (local_self.fontMetrics().lineSpacing() + 1) + - 21) + fr = local_self.frameRect() + cr = local_self.contentsRect() + local_self.setFixedHeight(min(370, s.height() + fr.height() + - cr.height() - 1)) + + def loadSearchMenu(self): + settings = QSettings() + searches = [] + self.tb_search_menu.clear () + self.tb_search_menu.addAction(QIcon.fromTheme('general_add'), 'New', self.newSearchAsk) + + nb_searches = settings.beginReadArray('searches') + for index in range(nb_searches): + settings.setArrayIndex(index) + self.tb_search_menu.addAction(settings.value('terms'), self.show_search) + settings.endArray() + + def newSearchAsk(self): + (search_terms, ok) = QInputDialog.getText(self, + self.tr('Search'), + self.tr('Enter the search keyword(s) :')) + if ok == 1: + #FIXME : Create the search + self.tb_search_menu.addAction(search_terms, self.show_search) + settings = QSettings() + nb_searches = settings.beginWriteArray('searches') + for index,action in enumerate(self.tb_search_menu.actions()): + #pass the first which are the new option + if index==0: + continue + settings.setArrayIndex(index-1) + settings.setValue('terms',action.text()) + settings.endArray() + settings.sync() + self.dbus_handler.require_update() + + def setupMenu(self): + """ + Initialization of the maemo menu + """ + + fileMenu = QMenu(self.tr("&Menu"), self) + self.menuBar().addMenu(fileMenu) + + fileMenu.addAction(self.tr("&Preferences..."), self.showPrefs) + fileMenu.addAction(self.tr("&About"), self.showAbout) + + @pyqtSlot() + def showPrefs(self): + khtsettings = KhweeteurPref(parent=self) + khtsettings.save.connect(self.refreshPrefs) + khtsettings.show() + + @pyqtSlot() + def refreshPrefs(self): + self.view.refreshCustomDelegate() + self.geolocDoStart() + + @pyqtSlot() + def showAbout(self): + if not hasattr(self,'aboutWin'): + self.aboutWin = KhweeteurAbout(self) + self.aboutWin.show() + + settings = QSettings() + + def geolocDoStart(self): + settings = QSettings() + self.geoloc_source = None + if settings.contains('useGPS'): + if settings.value('useGPS') == 'true': + self.geolocStart() + + def geolocStart(self): + '''Start the GPS with a 50000 refresh_rate''' + self.geoloc_coordinates = None + if self.geoloc_source is None: + self.geoloc_source = \ + QGeoPositionInfoSource.createDefaultSource(None) + if self.geoloc_source is not None: + self.geoloc_source.setUpdateInterval(50000) + self.geoloc_source.positionUpdated.connect(self.geolocUpdated) + self.geoloc_source.startUpdates() + + def geolocStop(self): + '''Stop the GPS''' + self.geoloc_coordinates = None + if self.geoloc_source is not None: + self.geoloc_source.stopUpdates() + self.geoloc_source = None + + def geolocUpdated(self, update): + '''GPS Callback on update''' + if update.isValid(): + self.geoloc_coordinates = (update.coordinate().latitude(), + update.coordinate().longitude()) + else: + print 'GPS Update not valid' + +if __name__ == '__main__': + from subprocess import Popen + Popen(['/usr/bin/python',os.path.join(os.path.dirname(__file__),'daemon.py'),'start']) + app = Khweeteur() + app.exec_() diff --git a/khweeteur/retriever.py b/khweeteur/retriever.py new file mode 100644 index 0000000..3d27856 --- /dev/null +++ b/khweeteur/retriever.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +import twitter +import socket +socket.setdefaulttimeout(60) +from urllib import urlretrieve + +import urllib2 +import pickle +try: + from PIL import Image +except: + import Image + +from PySide.QtCore import QSettings + +from threading import Thread + +import logging +import os +import dbus +import dbus.service +import socket + + +class KhweeteurRefreshWorker(Thread): + def __init__(self,base_url, consumer_key, consumer_secret, access_token, access_secret, call, dbus_handler): + Thread.__init__(self, None) + self.api = twitter.Api(username=consumer_key, + password=consumer_secret, + access_token_key=access_token, + access_token_secret=access_secret, + base_url=base_url) + self.api.SetUserAgent('Khweeteur') + self.call = call + self.consumer_key = consumer_key + self.dbus_handler = dbus_handler + socket.setdefaulttimeout(60) + + def send_notification(self,msg,count): + self.dbus_handler.new_tweets(count,msg) + + def getCacheFolder(self): + if not hasattr(self,'folder_path'): + self.folder_path = os.path.join(os.path.expanduser("~"), + '.khweeteur','cache', + os.path.normcase(unicode(self.call.replace('/', + '_'))).encode('UTF-8')) + + if not os.path.isdir(self.folder_path): + try: + os.makedirs(self.folder_path) + except IOError, e: + logging.debug('getCacheFolder:' + e) + + return self.folder_path + + def downloadProfilesImage(self, statuses): + avatar_path = os.path.join(os.path.expanduser("~"), + '.khweeteur','avatars') + if not os.path.exists(avatar_path): + os.makedirs(avatar_path) + + for status in statuses: + if type(status) != twitter.DirectMessage: + cache = os.path.join(avatar_path, os.path.basename(status.user.profile_image_url.replace('/', '_'))) + if not os.path.exists(cache): + try: + urlretrieve(status.user.profile_image_url, + cache) + im = Image.open(cache) + im = im.resize((50, 50)) + im.save(os.path.splitext(cache)[0] + '.png', + 'PNG') +# status.user.profile_image_url = cache + except StandardError, err: + logging.debug('DownloadProfilImage:' + str(err)) + print err + + def removeAlreadyInCache(self, statuses): + # Load cached statuses + try: + folder_path = self.getCacheFolder() + for status in statuses: + if os.path.exists(os.path.join(folder_path, + str(status.id))): + statuses.remove(status) + except StandardError, err: + logging.debug(err) + + def applyOrigin(self, statuses): + for status in statuses: + status.base_url = self.api.base_url + + def getOneReplyContent(self, tid): + #Got from cache + status = None + for root,dirs,files in os.walk(os.path.join(os.path.expanduser("~"),'.khweeteur','cache')): + for folder in dirs: + logging.debug('getOneReplyContent Folder: %s' % (folder,)) + for afile in files: + logging.debug('getOneReplyContent aFile: %s' % (os.path.join(root,afile),)) + if unicode(tid) == afile: + try: + fhandle = open(os.path.join(root,afile), 'rb') + status = pickle.load(fhandle) + fhandle.close() + return status.text + except StandardError,err: + logging.debug('getOneReplyContent:' + err) + + try: + rpath = os.path.join(os.path.expanduser("~"),'.khweeteur','cache','Replies') + if not os.path.exists(rpath): + os.makedirs(rpath) + + status = self.api.GetStatus(tid) + fhandle = open(os.path.join(os.path.join(rpath, + unicode(status.id))), 'wb') + pickle.dump(status, fhandle, pickle.HIGHEST_PROTOCOL) + fhandle.close() + return status.text + except StandardError, err: + logging.debug('getOneReplyContent:' + str(err)) + print err + + def isMe(self,statuses): + try: + me = self.api.VerifyCredentials() + except StandardError, err: + logging.debug('IsMe: %s' % (str(err),)) + for status in statuses: + if status.user.id == me.id: + status.is_me = True + else: + status.is_me = False + + def getRepliesContent(self, statuses): + for status in statuses: + try: + if not hasattr(status, 'in_reply_to_status_id'): + status.in_reply_to_status_text = None + elif not status.in_reply_to_status_text \ + and status.in_reply_to_status_id: + status.in_reply_to_status_text = \ + self.getOneReplyContent(status.in_reply_to_status_id) + except StandardError, err: + logging.debug('getOneReplyContent:' + err) + print err + + def serialize(self, statuses): + folder_path = self.getCacheFolder() + + for status in statuses: + try: + fhandle = open(os.path.join(folder_path, unicode(status.id)),'wb') + pickle.dump(status, fhandle, pickle.HIGHEST_PROTOCOL) + fhandle.close() + except: + logging.debug('Serialization of %s failed' % (status.id,)) + + def run(self): + settings = QSettings("Khertan Software", "Khweeteur") + statuses = [] + + logging.debug('Thread Runned') + try: + since = settings.value(self.consumer_key + '_' + self.call) + + if self.call == 'HomeTimeline': + logging.debug('%s running' % self.call) + statuses = self.api.GetHomeTimeline(since_id=since) + logging.debug('%s finished' % self.call) + elif self.call == 'Mentions': + logging.debug('%s running' % self.call) + statuses = self.api.GetMentions(since_id=since) + logging.debug('%s finished' % self.call) + elif self.call == 'DMs': + logging.debug('%s running' % self.call) + statuses = self.api.GetDirectMessages(since_id=since) + logging.debug('%s finished' % self.call) + #Its a search .... or a list + elif self.call.startswith('Search:'): + logging.debug('%s running' % self.call) + statuses = self.api.GetSearch(since_id=since, term=self.call.split(':')[1]) + logging.debug('%s finished' % self.call) + else: + logging.error('Unknow call : %s' % (self.call,)) + + except StandardError, err: + logging.debug(err) + raise err + + self.removeAlreadyInCache(statuses) + if len(statuses) > 0: + logging.debug('%s start download avatars' % self.call) + self.downloadProfilesImage(statuses) + logging.debug('%s start applying origin' % self.call) + self.applyOrigin(statuses) + logging.debug('%s start getreply' % self.call) + self.getRepliesContent(statuses) + if self.call != 'DMs': + logging.debug('%s start isMe' % self.call) + self.isMe(statuses) + logging.debug('%s start serialize' % self.call) + self.serialize(statuses) + statuses.sort() + statuses.reverse() + settings.setValue(self.consumer_key + \ + '_' + self.call, statuses[0].id) + self.send_notification(self.call,len(statuses)) + settings.sync() + logging.debug('%s refreshed' % self.call) diff --git a/khweeteur/settings.py b/khweeteur/settings.py new file mode 100644 index 0000000..e4d373f --- /dev/null +++ b/khweeteur/settings.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 +'''A simple Twitter client made with pyqt4''' + +import datetime +import httplib2 +import re + +#import sip +#sip.setapi('QString', 2) +#sip.setapi('QVariant', 2) + +from PySide.QtGui import QMainWindow, \ + QSizePolicy, \ + QSpinBox, \ + QVBoxLayout, \ + QDesktopServices, \ + QAbstractItemView, \ + QScrollArea, \ + QListView, \ + QComboBox, \ + QCheckBox, \ + QDialog, \ + QGridLayout, \ + QWidget, \ + QToolBar, \ + QLabel, \ + QPushButton, \ + QInputDialog, \ + QKeySequence, \ + QMenu, \ + QAction, \ + QApplication, \ + QIcon, \ + QMessageBox, \ + QPlainTextEdit + +from PySide.QtCore import Qt, \ + QUrl, \ + QAbstractListModel, \ + QSettings, \ + QModelIndex, \ + Signal + +#Signal = pyqtSignal + +SUPPORTED_ACCOUNTS = [{'name':'Twitter', + 'consumer_key':'uhgjkoA2lggG4Rh0ggUeQ', + 'consumer_secret':'lbKAvvBiyTlFsJfb755t3y1LVwB0RaoMoDwLD14VvU', + 'base_url':'https://api.twitter.com/1', + 'request_token_url':'https://api.twitter.com/oauth/request_token', + 'access_token_url':'https://api.twitter.com/oauth/access_token', + 'authorization_url':'https://api.twitter.com/oauth/authorize'}, + {'name':'Identi.ca', + 'consumer_key':'c7e86efd4cb951871200440ad1774413', + 'consumer_secret':'236fa46bf3f65fabdb1fd34d63c26d28', + 'base_url':'http://identi.ca/api', + 'request_token_url':'http://identi.ca/api/oauth/request_token', + 'access_token_url':'http://identi.ca/api/oauth/access_token', + 'authorization_url':'http://identi.ca/api/oauth/authorize'}, + ] + +import oauth2 as oauth +from notifications import KhweeteurNotification + +try: + from urlparse import parse_qs +except: + from cgi import parse_qs + +from PySide.QtWebKit import * + + +class OAuthView(QWebView): + gotpin = Signal(unicode) + + def __init__(self, parent=None, account_type={}, use_for_tweet=False): + QWebView.__init__(self, parent) + self.loggedIn = False + self.account_type = account_type + self.use_for_tweet = use_for_tweet + self.pin = None + + def open(self, url): + """.""" + self.url = QUrl(url) + self.loadFinished.connect(self._loadFinished) + self.load(self.url) + self.show() + + def createWindow(self, windowType): + """Load links in the same web-view.""" + return self + + def _loadFinished(self): + + regex = re.compile('.*(.*)<') + res = regex.findall(self.page().mainFrame().toHtml()) + if len(res) > 0: + self.pin = res[0] + + self.loggedIn = (self.pin not in (None, '')) + + if self.loggedIn: + self.loadFinished.disconnect(self._loadFinished) + self.gotpin.emit(self.pin) + + +class AccountDlg(QDialog): + """ Find and replace dialog """ + add_account = Signal(dict, bool) + + def __init__(self, parent=None): + QDialog.__init__(self, parent) + self.setWindowTitle("Add account") + + self.accounts_type = QComboBox() + for account_type in SUPPORTED_ACCOUNTS: + self.accounts_type.addItem(account_type['name']) + + self.use_for_tweet = QCheckBox("Use for posting") + + self.add = QPushButton("&Add") + + gridLayout = QGridLayout() + gridLayout.addWidget(self.accounts_type, 0, 0) + gridLayout.addWidget(self.use_for_tweet, 0, 1) + gridLayout.addWidget(self.add, 1, 2) + self.setLayout(gridLayout) + self.add.clicked.connect(self.addit) + + def addit(self): + index = self.accounts_type.currentIndex() + self.add_account.emit(SUPPORTED_ACCOUNTS[index], + self.use_for_tweet.isChecked()) + self.hide() + + +class AccountsModel(QAbstractListModel): + dataChanged = Signal(QModelIndex, QModelIndex) + + def __init__(self): + QAbstractListModel.__init__(self) + self._items = [] + + def set(self, mlist): + self._items = mlist + self.dataChanged.emit(self.createIndex(0, 0), + self.createIndex(0, + len(self._items))) + + def rowCount(self, parent=QModelIndex()): + return len(self._items) + + def data(self, index, role=Qt.DisplayRole): + if role == Qt.DisplayRole: + return self._items[index.row()].name + else: + return None + + +class AccountsView(QListView): + + def __init__(self, parent=None): + QListView.__init__(self, parent) + self.setEditTriggers(QAbstractItemView.SelectedClicked) + + +class KhweeteurAccount(): + + def __init__(self, name='Unknow', consumer_key='', consumer_secret='', token_key='', token_secret='', use_for_tweet=True, base_url=''): + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.token_key = token_key + self.token_secret = token_secret + self.use_for_tweet = use_for_tweet + self.base_url = base_url + self.name = name + +class KhweeteurPref(QMainWindow): + save = Signal() + + DEFAULTTHEME = u'Default' + WHITETHEME = u'White' + COOLWHITETHEME = u'CoolWhite' + COOLGRAYTHEME = u'CoolGray' + MINITHEME = u'MiniDefault' + THEMES = [DEFAULTTHEME, WHITETHEME, COOLWHITETHEME, COOLGRAYTHEME, MINITHEME] + + def __init__(self, parent=None): + ''' Init the GUI Win''' + QMainWindow.__init__(self,parent) + self.parent = parent + + self.settings = QSettings() + + self.setAttribute(Qt.WA_Maemo5AutoOrientation, True) + self.setAttribute(Qt.WA_Maemo5StackedWindow, True) + self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setWindowTitle("Khweeteur Prefs") + + self._setupGUI() + self.loadPrefs() + + def loadPrefs(self): + ''' Load and init default prefs to GUI''' + #load account + self.accounts = [] + nb_accounts = self.settings.beginReadArray('accounts') + for index in range(nb_accounts): + self.settings.setArrayIndex(index) + self.accounts.append(KhweeteurAccount(name=self.settings.value('name'), \ + consumer_key=self.settings.value('consumer_key'), \ + consumer_secret=self.settings.value('consumer_secret'), \ + token_key=self.settings.value('token_key'), \ + token_secret=self.settings.value('token_secret'), \ + use_for_tweet=self.settings.value('use_for_tweet'), \ + base_url=self.settings.value('base_url'))) + self.settings.endArray() + self.accounts_model.set(self.accounts) + + #load other prefs + if self.settings.contains('refresh_interval'): + self.refresh_value.setValue(int(self.settings.value("refreshInterval"))) + else: + self.refresh_value.setValue(10) + + if self.settings.contains("useDaemon"): + self.useNotification_value.setCheckState(Qt.CheckState(int(self.settings.value("useDaemon")))) + else: + self.useNotification_value.setCheckState(Qt.CheckState(2)) + + if self.settings.contains("useSerialization"): + self.useSerialization_value.setCheckState(Qt.CheckState(int(self.settings.value("useSerialization")))) + else: + self.useSerialization_value.setCheckState(Qt.CheckState(2)) + + if self.settings.contains("useBitly"): + self.useBitly_value.setCheckState(Qt.CheckState(int(self.settings.value("useBitly")))) + else: + self.useBitly_value.setCheckState(Qt.CheckState(2)) + + if self.settings.contains("theme"): + if not self.settings.value("theme") in self.THEMES: + self.settings.setValue("theme",KhweeteurPref.DEFAULTTHEME) + else: + self.settings.setValue("theme",KhweeteurPref.DEFAULTTHEME) + + self.theme_value.setCurrentIndex(self.THEMES.index(self.settings.value("theme"))) + + if self.settings.contains("tweetHistory"): + self.history_value.setValue(int(self.settings.value("tweetHistory"))) + else: + self.history_value.setValue(60) + + if self.settings.contains("useGPS"): + self.useGPS_value.setCheckState(Qt.CheckState(int(self.settings.value("useGPS")))) + else: + self.useGPS_value.setCheckState(Qt.CheckState(2)) + + def savePrefs(self): + ''' Save the prefs from the GUI to QSettings''' + self.settings.beginWriteArray("accounts") + for index,account in enumerate(self.accounts): + self.settings.setArrayIndex(index) + self.settings.setValue("name", account.name) + self.settings.setValue("consumer_key", account.consumer_key) + self.settings.setValue("consumer_secret", account.consumer_secret) + self.settings.setValue("token_key", account.token_key) + self.settings.setValue("token_secret", account.token_secret) + self.settings.setValue("use_for_tweet", account.use_for_tweet) + self.settings.setValue("base_url", account.base_url) + self.settings.endArray() + + self.settings.setValue('refreshInterval', self.refresh_value.value()) + self.settings.setValue('useDaemon', self.useNotification_value.checkState()) + self.settings.setValue('useSerialization', self.useSerialization_value.checkState()) + self.settings.setValue('useBitly', self.useBitly_value.checkState()) + self.settings.setValue('theme', self.theme_value.currentText()) + self.settings.setValue('useGPS', self.useGPS_value.checkState()) + self.settings.setValue('tweetHistory', self.history_value.value()) + self.settings.sync() + self.save.emit() + + def add_account(self): + self.dlg = AccountDlg() + self.dlg.add_account.connect(self.do_ask_token) + self.dlg.show() + + def do_verify_pin(self,pincode): + token = oauth.Token(self.oauth_webview.request_token['oauth_token'][0], self.oauth_webview.request_token['oauth_token_secret'][0]) + token.set_verifier(unicode(pincode.strip())) + + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + oauth_consumer = oauth.Consumer(key=self.oauth_webview.account_type['consumer_key'], secret=self.oauth_webview.account_type['consumer_secret']) + oauth_client = oauth.Client(oauth_consumer) + + + try: + oauth_client = oauth.Client(oauth_consumer, token) + resp, content = oauth_client.request(self.oauth_webview.account_type['access_token_url'], method='POST', body='oauth_verifier=%s' % str(pincode.strip())) + access_token = (parse_qs(content)) + + print access_token['oauth_token'][0] + + if resp['status'] == '200': + #Create the account + self.accounts.append(KhweeteurAccount(\ + name=self.oauth_webview.account_type['name'],\ + base_url=self.oauth_webview.account_type['base_url'],\ + consumer_key=self.oauth_webview.account_type['consumer_key'],\ + consumer_secret=self.oauth_webview.account_type['consumer_secret'],\ + token_key=access_token['oauth_token'][0],\ + token_secret=access_token['oauth_token_secret'][0],\ + use_for_tweet=self.oauth_webview.use_for_tweet,\ + )) + self.accounts_model.set(self.accounts) + self.savePrefs() + else: + KhweeteurNotification().warn(self.tr('Invalid respond from %s requesting access token: %s') % (self.oauth_webview.account_type['name'],resp['status'])) + except StandardError, err: + KhweeteurNotification().warn(self.tr('A error occur while requesting temp token : %s' % (err,))) + import traceback + traceback.print_exc() + + self.oauth_win.close() + del self.oauth_win + del self.oauth_webview + + def do_ask_token(self, account_type,use_for_tweet): + print account_type,use_for_tweet + + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + oauth_consumer = oauth.Consumer(key=account_type['consumer_key'], secret=account_type['consumer_secret']) + oauth_client = oauth.Client(oauth_consumer) + + #Crappy hack for fixing oauth_callback not yet supported by the oauth2 lib but requested by identi.ca + body = 'oauth_callback=oob' + + try: + resp, content = oauth_client.request(account_type['request_token_url'], 'POST', body=body) + + if resp['status'] != '200': + KhweeteurNotification().warn(self.tr('Invalid respond from %s requesting temp token: %s') % (account_type['name'],resp['status'])) + else: + request_token = (parse_qs(content)) + + self.oauth_webview = OAuthView(self, account_type, use_for_tweet) + self.oauth_webview.open((QUrl('%s?oauth_token=%s' % (account_type['authorization_url'], request_token['oauth_token'][0])))) + self.oauth_webview.request_token = request_token + + self.oauth_webview.show() + self.oauth_webview.gotpin.connect(self.do_verify_pin) + self.oauth_win = QMainWindow(self) + self.oauth_win.setCentralWidget(self.oauth_webview) + self.oauth_win.setAttribute(Qt.WA_Maemo5AutoOrientation, True) + self.oauth_win.setAttribute(Qt.WA_Maemo5StackedWindow, True) + self.oauth_win.setWindowTitle("Khweeteur OAuth") + self.oauth_win.show() + + except httplib2.ServerNotFoundError,err: + KhweeteurNotification().warn(self.tr('Server not found : %s :') % unicode(err)) + + def delete_account(self, index): + if QMessageBox.question(self,'Delete account', 'Are you sure you want to delete this account ?', QMessageBox.Yes | QMessageBox.Close) == QMessageBox.Yes: + for index in self.accounts_view.selectedIndexes(): + del self.accounts[index.row()] + self.accounts_model.set(self.accounts) + + def closeEvent(self,widget,*args): + ''' close event called when closing window''' + self.savePrefs() + + def _setupGUI(self): + ''' Create the gui content of the window''' + self.scrollArea = QScrollArea(self) + self.scrollArea.setWidgetResizable(True) + self.aWidget = QWidget(self.scrollArea) + self.aWidget.setMinimumSize(480,1000) + self.aWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.scrollArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.scrollArea.setWidget(self.aWidget) + #Available on maemo but should be too on Meego + try: + scroller = self.scrollArea.property("kineticScroller") + scroller.setEnabled(True) + except: + pass + + self._main_layout = QVBoxLayout(self.aWidget) + self._umain_layout = QGridLayout() + self.aWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + self._umain_layout.addWidget(QLabel(self.tr('Refresh Interval (Minutes) :')),3,0) + self.refresh_value = QSpinBox() + self._umain_layout.addWidget(self.refresh_value,3,1) + + self._umain_layout.addWidget(QLabel(self.tr('Number of tweet to keep in the view :')),4,0) + self.history_value = QSpinBox() + self._umain_layout.addWidget(self.history_value,4,1) + + self._umain_layout.addWidget(QLabel(self.tr('Theme :')),5,0) + + self.theme_value = QComboBox() + self._umain_layout.addWidget(self.theme_value,5,1) + for theme in self.THEMES: + self.theme_value.addItem(theme) + + self._umain_layout.addWidget(QLabel(self.tr('Other preferences :')),9,0) + self.useNotification_value = QCheckBox(self.tr('Use Daemon')) + self._umain_layout.addWidget(self.useNotification_value,10,1) + + self.useSerialization_value = QCheckBox(self.tr('Use Serialization')) + self._umain_layout.addWidget(self.useSerialization_value,11,1) + + self.useBitly_value = QCheckBox(self.tr('Use Bit.ly')) + self._umain_layout.addWidget(self.useBitly_value,12,1) + + self.useGPS_value = QCheckBox(self.tr('Use GPS Geopositionning')) + self._umain_layout.addWidget(self.useGPS_value,13,1) + + self._main_layout.addLayout(self._umain_layout) + + self.accounts_model = AccountsModel() + self.accounts_view = AccountsView() + self.accounts_view.clicked.connect(self.delete_account) + self.accounts_view.setModel(self.accounts_model) + self.add_acc_button = QPushButton('Add account') + self.add_acc_button.clicked.connect(self.add_account) + self._main_layout.addWidget(self.add_acc_button) + self._main_layout.addWidget(self.accounts_view) + + self.aWidget.setLayout(self._main_layout) + self.setCentralWidget(self.scrollArea) + +if __name__ == '__main__': + import sys + app = QApplication(sys.argv) + app.setOrganizationName("Khertan Software") + app.setOrganizationDomain("khertan.net") + app.setApplicationName("Khweeteur") + + khtsettings = KhweeteurPref() + khtsettings.show() + sys.exit(app.exec_()) diff --git a/khweeteur/tweetslist.py b/khweeteur/tweetslist.py new file mode 100644 index 0000000..f8f5c2c --- /dev/null +++ b/khweeteur/tweetslist.py @@ -0,0 +1,205 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 Benoรฎt HERVIER +# Licenced under GPLv3 + +'''A Twitter client made with PySide and QML''' + +from PySide.QtGui import * +from PySide.QtCore import * + +import os +import sys +import glob +import pickle +import time +import twitter +import dbus +import dbus.service + +AVATAR_CACHE_FOLDER = '/home/user/.khweeteur/avatars' + +class KhweeteurDBusHandler(dbus.service.Object): + def __init__(self): + dbus.service.Object.__init__(self, dbus.SessionBus(), '/net/khertan/Khweeteur/RequireUpdate') + + @dbus.service.signal(dbus_interface='net.khertan.Khweeteur', + signature='') + def require_update(self): + pass + +class StatusWrapper(QObject): + def __init__(self,status): + QObject.__init__(self) + self._status = status + + def _screen_name(self): + return self._status.user.screen_name + + def _id(self): + return self._status.id + + def _image_url(self): + return self._status.user.profile_image_url + + def _avatar(self): + return os.path.join(AVATAR_CACHE_FOLDER, \ + os.path.basename(self._status.user.profile_image_url.replace('/' , '_'))) + + def _text(self): + return self._status.text + + def _created_at(self): + return self._status.created_at + + def _in_reply_to_screenname(self): + return self._status.in_reply_to_screenname + + @Signal + def changed(self): + pass + + def __cmp__(obj1,obj2): + if obj1._status.created_at == obj2._status.created_at: + return 0 + if obj1._status.created_at > obj2._status.created_at: + return -1 + else: + return 1 + + screen_name = Property(unicode, _screen_name, notify=changed) + id = Property(unicode, _id, notify=changed) + image_url = Property(unicode, _image_url, notify=changed) + avatar = Property(unicode, _avatar, notify=changed) + text = Property(unicode, _text, notify=changed) + created_at = Property(unicode, _created_at, notify=changed) + in_reply_to_screenname = Property(unicode, _in_reply_to_screenname, notify=changed) + +class TweetsListModel(QAbstractListModel): +# dataChanged = Signal(QModelIndex,QModelIndex) + + COLUMNS = ('status',) + + def __init__(self, statuses = []): + QAbstractListModel.__init__(self) + self._statuses = statuses + self.setRoleNames(dict(enumerate(TweetsListModel.COLUMNS))) + + def rowCount(self,parent=QModelIndex): + return len(self._statuses) + + def data(self,index,role): + if index.isValid(): + return self._statuses[index.row()] + else: + return None + + @Slot(unicode) + def load_list(self,tweetlist): + print 'load timeline tweets' + start = time.time() + TIMELINE_PATH = '/home/user/.khweeteur/cache/%s' % (tweetlist) + cach_path = TIMELINE_PATH + uids = glob.glob(cach_path + '/*')[:60] + statuses = [] + for uid in uids: + uid = os.path.basename(uid) + try: + pkl_file = open(os.path.join(cach_path, uid), 'rb') + status = pickle.load(pkl_file) + pkl_file.close() + statuses.append(status) + except: + pass + print time.time() - start + print len(statuses) + self._statuses = [StatusWrapper(status) for status in statuses] + self._statuses.sort() + + #FIXME + #Wait pyside bug is resolved + self.dataChanged.emit(self.createIndex(0, 1), + self.createIndex(0, + len(self._statuses))) + + +class ButtonWrapper(QObject): + def __init__(self,button): + QObject.__init__(self) + self._button = button + + def _label(self): + return self._button['label'] + + def _count(self): + return self._button['count'] + + def _src(self): + return self._button['src'] + + @Signal + def changed(self): + pass + + label = Property(unicode, _label, notify=changed) + src = Property(unicode, _src, notify=changed) + count = Property(int, _count, notify=changed) + +class ToolbarListModel(QAbstractListModel): + COLUMNS = ('button',) + def __init__(self,): + QAbstractListModel.__init__(self) + self._buttons = [] + self._buttons.append(ButtonWrapper({'label':'','src':'refresh.png','count':0})) + self._buttons.append(ButtonWrapper({'label':'Timeline','src':'','count':0})) + self._buttons.append(ButtonWrapper({'label':'Mentions','src':'','count':0})) + self._buttons.append(ButtonWrapper({'label':'DMs','src':'','count':0})) + self.setRoleNames(dict(enumerate(ToolbarListModel.COLUMNS))) + + def rowCount(self,parent=QModelIndex): + return len(self._buttons) + + def data(self,index,role): + if index.isValid(): + return self._buttons[index.row()] + else: + return None + + def setCount(self,msg,count): + for button in self._buttons: + if button._button['label'] == msg: + button._button['count'] = int(count) + + #FIXME + #Wait pyside bug is resolved +# self.dataChanged.emit(self.createIndex(0, 1), +# self.createIndex(0, +# len(self._statuses))) + +class Controller(QObject): + switch_fullscreen = Signal() + switch_list = Signal(unicode) + + def __init__(self): + QObject.__init__(self,None) + self.dbus_handler = KhweeteurDBusHandler() + + @Slot(QObject) + def statusSelected(self, wrapper): + print 'User clicked on:', wrapper._status.id + + @Slot(unicode) + def toolbar_callback(self,name): + print name + if name.endswith('fullsize.png'): + self.switch_fullscreen.emit() + elif name.endswith('Timeline'): + QApplication.processEvents() + self.switch_list.emit('HomeTimeline') + elif name.endswith('Mentions'): + QApplication.processEvents() + self.switch_list.emit('Mentions') + elif name.endswith('refresh.png'): + QApplication.processEvents() + self.dbus_handler.require_update() diff --git a/khweeteur/twitpic.py b/khweeteur/twitpic.py new file mode 100644 index 0000000..564f9ef --- /dev/null +++ b/khweeteur/twitpic.py @@ -0,0 +1,451 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +# python-twitpic - Dead-simple Twitpic image uploader. + +# Copyright (c) 2009, Chris McMichael +# http://chrismcmichael.com/ +# http://code.google.com/p/python-twitpic/ + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author nor the names of its contributors may +# be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +import mimetypes +import os +import urllib +import urllib2 +import re + +from oauth import oauth +from xml.dom import minidom as xml +from xml.parsers.expat import ExpatError + +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +try: + import json +except ImportError: + import simplejson as json + + +class TwitPicError(Exception): + """TwitPic Exception""" + + def __init__(self, reason, response=None): + self.reason = unicode(reason) + self.response = response + + def __str__(self): + return self.reason + + +# Handles Twitter OAuth authentication +class TwitPicOAuthClient(oauth.OAuthClient): + """TwitPic OAuth Client API""" + + SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + STATUS_UPDATE_URL = 'https://api.twitter.com/1/statuses/update.json' + USER_INFO_URL = 'https://api.twitter.com/1/account/verify_credentials.json' + + FORMAT = 'json' + SERVER = 'http://api.twitpic.com' + + GET_URIS = { + 'media_show': ('/2/media/show', ('id',)), + 'faces_show': ('/2/faces/show', ('user')), + 'user_show': ('/2/users/show', ('username',)), # additional optional params + 'comments_show': ('/2/comments/show', ('media_id', 'page')), + 'place_show': ('/2/place/show', ('id',)), + 'places_user_show': ('/2/places/show', ('user',)), + 'events_show': ('/2/events/show', ('user',)), + 'event_show': ('/2/event/show', ('id',)), + 'tags_show': ('/2/tags/show', ('tag',)), + } + + POST_URIS = { + 'upload': ('/2/upload', ('message', 'media')), + 'comments_create': ('/2/comments/create', ('message_id', 'message')), + 'faces_create': ('/2/faces/create', ('media_id', 'top_coord', 'left_coord')), # additional optional params + 'event_create': ('/2/event/create', ('name')), # additional optional params + 'event_add': ('/2/event/add', ('event_id', 'media_id')), # no workie! + 'tags_create': ('/2/tags/create', ('media_id', 'tags')), + } + + PUT_URIS = { + 'faces_edit': ('/2/faces/edit', ('tag_id', 'top_coord', 'left_coord')), + } + + DELETE_URIS = { + 'comments_delete': ('/2/comments/delete', ('comment_id')), + 'faces_delete': ('/2/faces/delete', ('tag_id')), + 'event_delete': ('/2/event/delete', ('event_id')), + 'event_remove': ('/2/event/remove', ('event_id', 'media_id')), + 'tags_delete': ('/2/tags/delete', ('media_id', 'tag_id')), + } + + def __init__(self, consumer_key=None, consumer_secret=None, + service_key=None, access_token=None): + """ + An object for interacting with the Twitpic API. + + The arguments listed below are generally required for most calls. + + Args: + consumer_key: + Twitter API Key [optional] + consumer_secret: + Twitter API Secret [optional] + access_token: + Authorized access_token in string format. [optional] + service_key: + Twitpic service key used to interact with the API. [optional] + + NOTE: + The TwitPic OAuth Client does NOT support fetching + an access_token. Use your favorite Twitter API Client to + retrieve this. + + """ + self.server = self.SERVER + self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + self.service_key = service_key + self.format = self.FORMAT + + if access_token: + self.access_token = oauth.OAuthToken.from_string(access_token) + + def set_comsumer(self, consumer_key, consumer_secret): + self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) + + def set_access_token(self, accss_token): + self.access_token = oauth.OAuthToken.from_string(access_token) + + def set_service_key(self, service_key): + self.service_key = service_key + + def _encode_multipart_formdata(self, fields=None): + BOUNDARY = '-------tHISiStheMulTIFoRMbOUNDaRY' + CRLF = '\r\n' + L = [] + filedata = None + media = fields.get('media', '') + + if media: + filedata = self._get_filedata(media) + del fields['media'] + + if fields: + for (key, value) in fields.items(): + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % str(key)) + L.append('') + L.append(str(value)) + + if filedata: + for (filename, value) in [(media, filedata)]: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="media"; \ + filename="%s"' % (str(filename),)) + L.append('Content-Type: %s' % self._get_content_type(media)) + L.append('') + L.append(value.getvalue()) + + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + + return content_type, body + + def _get_content_type(self, media): + return mimetypes.guess_type(media)[0] or 'application/octet-stream' + + def _get_filedata(self, media): + # Check self.image is an url, file path, or nothing. + prog = re.compile('((https?|ftp|gopher|telnet|file|notes|ms-help):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)') + + if prog.match(media): + return StringIO.StringIO(urllib2.urlopen(media).read()) + elif os.path.exists(media): + return StringIO.StringIO(open(media, 'rb').read()) + else: + return None + + def _post_call(self, method, params, uri, required): + if not self.consumer: + raise TwitPicError("Missing Twitter consumer keys") + if not self.access_token: + raise TwitPicError("Missing access_token") + if not self.service_key: + raise TwitPicError("Missing TwitPic service key") + + for req_param in required: + if req_param not in params: + raise TwitPicError('"' + req_param + '" parameter is not provided.') + + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + self.consumer, + self.access_token, + http_url=self.USER_INFO_URL + ) + + # Sign our request before setting Twitpic-only parameters + oauth_request.sign_request(self.signature_method, self.consumer, self.access_token) + + # Set TwitPic parameters + oauth_request.set_parameter('key', self.service_key) + + for key, value in params.iteritems(): + oauth_request.set_parameter(key, value) + + # Build request body parameters. + params = oauth_request.parameters + content_type, content_body = self._encode_multipart_formdata(params) + + # Get the oauth headers. + oauth_headers = oauth_request.to_header(realm='http://api.twitter.com/') + + # Add the headers required by TwitPic and any additional headers. + headers = { + 'X-Verify-Credentials-Authorization': oauth_headers['Authorization'], + 'X-Auth-Service-Provider': self.USER_INFO_URL, + 'User-Agent': 'Python-TwitPic2', + 'Content-Type': content_type + } + + # Build our url + url = '%s%s.%s' % (self.server, uri, self.format) + + # Make the request. + + req = urllib2.Request(url, content_body, headers) + + try: + # Get the response. + response = urllib2.urlopen(req) + except urllib2.HTTPError, e: + raise TwitPicError(e) + + if self.format == 'json': + return self.parse_json(response.read()) + elif self.format == 'xml': + return self.parse_xml(response.read()) + + def read(self, method, params, format=None): + """ + Use this method for all GET URI calls. + + An access_token or service_key is not required for this method. + + Args: + method: + name that references which GET URI to use. + params: + dictionary of parameters needed for the selected method call. + format: + response format. default is json. options are (xml, json) + + NOTE: + faces_show is actually a POST method. However, since data + is being retrieved and not created, it seemed appropriate + to keep this call under the GET method calls. Tokens and keys + will be required for this call as well. + + """ + uri, required = self.GET_URIS.get(method, (None, None)) + if uri is None: + raise TwitPicError('Unidentified Method: ' + method) + + if format: + self.format = format + + if method == 'faces_show': + return self._post_call(method, params, uri, required) + + for req_param in required: + if req_param not in params: + raise TwitPicError('"' + req_param + '" parameter is not provided.') + + # Build our GET url + request_params = urllib.urlencode(params) + url = '%s%s.%s?%s' % (self.server, uri, self.format, request_params) + + # Make the request. + req = urllib2.Request(url) + + try: + # Get the response. + response = urllib2.urlopen(req) + except urllib2.HTTPError, e: + raise TwitPicError(e) + + if self.format == 'json': + return self.parse_json(response.read()) + elif self.format == 'xml': + return self.parse_xml(response.read()) + + def create(self, method, params, format=None): + """ + Use this method for all POST URI calls. + + Args: + method: + name that references which POST URI to use. + params: + dictionary of parameters needed for the selected method call. + format: + response format. default is json. options are (xml, json) + + NOTE: + You do NOT have to pass the key param (service key). Service key + should have been provided before calling this method. + + """ + if 'key' in params: + raise TwitPicError('"key" parameter should be provided by set_service_key method or initializer method.') + + uri, required = self.POST_URIS.get(method, (None, None)) + + if uri is None: + raise TwitPicError('Unidentified Method: ' + method) + + if format: + self.format = format + + return self._post_call(method, params, uri, required) + + def update(self, method, params, format=None): + """ + Use this method for all PUT URI calls. + + Args: + method: + name that references which PUT URI to use. + params: + dictionary of parameters needed for the selected method call. + format: + response format. default is json. options are (xml, json) + + """ + if 'key' in params: + raise TwitPicError('"key" parameter should be provided by set_service_key method or initializer method.') + + uri, required = self.PUT_URIS.get(method, (None, None)) + + if uri is None: + raise TwitPicError('Unidentified Method: ' + method) + + if format: + self.format = format + + return self._post_call(method, params, uri, required) + + def remove(self, method, params, format=None): + """ + Use this method for all DELETE URI calls. + + Args: + method: + name that references which DELETE URI to use. + params: + dictionary of parameters needed for the selected method call. + format: + response format. default is json. options are (xml, json) + + """ + if 'key' in params: + raise TwitPicError('"key" parameter should be provided by set_service_key method or initializer method.') + + uri, required = self.DELETE_URIS.get(method, (None, None)) + + if uri is None: + raise TwitPicError('Unidentified Method: ' + method) + + if format: + self.format = format + + return self._post_call(method, params, uri, required) + + def parse_xml(self, xml_response): + try: + dom = xml.parseString(xml_response) + node = dom.firstChild + if node.nodeName == 'errors': + return node.firstChild.nodeValue + else: + return dom + except ExpatError, e: + raise TwitPicError('XML Parsing Error: ' + e) + + def parse_json(self, json_response): + try: + result = json.loads(json_response) + if result.has_key('errors'): + return result['errors']['code'] + else: + return result + except ValueError, e: + raise TwitPicError('JSON Parsing Error: ' + e) + +if __name__ == '__main__': + from optparse import OptionParser + optPsr = OptionParser("usage: %prog -k CONSUMER_KEY -s CONSUMER_SECRET -a ACCESS_TOKEN -t SERVICE_KEY -m TWEET -f FILE") + optPsr.add_option('-k', '--consumer_key', type='string', help='Twitter consumer API key') + optPsr.add_option('-s', '--consumer_secret', type='string', help='Twitter consumer API secret') + optPsr.add_option('-a', '--access_token', type='string', help='Twitter Access Token') + optPsr.add_option('-t', '--service_key', type='string', help='Twitpic API key') + optPsr.add_option('-m', '--message', type='string', help='The tweet that belongs to the image.') + optPsr.add_option('-f', '--file', type='string', help='The file upload data.') + (opts, args) = optPsr.parse_args() + + if not opts.consumer_key: + optPsr.error("Missing CONSUMER_KEY") + + if not opts.consumer_secret: + optPsr.error("Missing CONSUMER_SECRET") + + if not opts.access_token: + optPsr.error("Missing ACCESS_TOKEN") + + if not opts.service_key: + optPsr.error("Missing SERVICE_KEY") + + if not opts.message: + optPsr.error("Missing TWEET") + + if not opts.file: + optPsr.error("Missing FILE") + + twitpic = TwitPicOAuthClient( + consumer_key = opts.consumer_key, + consumer_secret = opts.consumer_secret, + access_token = opts.access_token, + service_key = opts.service_key, + ) + + response = twitpic.create('upload', {'media': opts.file, 'message': opts.message }) + print response diff --git a/khweeteur/twitter.py b/khweeteur/twitter.py new file mode 100644 index 0000000..dfb8674 --- /dev/null +++ b/khweeteur/twitter.py @@ -0,0 +1,3597 @@ +#!/usr/bin/python2.4 +# -*- coding: utf-8 -*- +# Copyright 2007 The Python-Twitter Developers +# This version is a fork made by Khertan +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +'''A library that provides a Python interface to the Twitter API''' + +__author__ = 'python-twitter@googlegroups.com' +__version__ = '0.8-khtfork.3' + + +import base64 +import calendar +import datetime +import httplib +import os +import rfc822 +import simplejson +import sys +import tempfile +import textwrap +import time +import urllib +import urllib2 +import urlparse +import gzip +import StringIO + +import oauth2 as oauth + +import socket +socket.setdefaulttimeout(120) + +# parse_qsl moved to urlparse module in v2.6 +try: + from urlparse import parse_qsl, parse_qs +except: + from cgi import parse_qsl, parse_qs + +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + + +CHARACTER_LIMIT = 140 + +# A singleton representing a lazily instantiated FileCache. +DEFAULT_CACHE = object() + +REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' +ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' +AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' +SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + + + +import re, htmlentitydefs + +## +# Removes HTML or XML character references and entities from a text string. +# +# @param text The HTML (or XML) source text. +# @return The plain text, as a Unicode string, if necessary. + +def unescape(text): + def fixup(m): + text = m.group(0) + if text[:2] == "&#": + # character reference + try: + if text[:3] == "&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub("&#?\w+;", fixup, text) + +class TwitterError(Exception): + '''Base class for Twitter errors''' + + @property + def message(self): + '''Returns the first argument used to construct this error.''' + return self.args[0] + + +class Status(object): + '''A class representing the Status structure used by the twitter API. + + The Status structure exposes the following properties: + + status.created_at + status.created_at_in_seconds # read only + status.favorited + status.in_reply_to_screen_name + status.in_reply_to_user_id + status.in_reply_to_status_id + status.truncated + status.source + status.id + status.text + status.location + status.relative_created_at # read only + status.user + ''' + def __init__(self, + created_at=None, + favorited=None, + id=None, + text=None, + location=None, + user=None, + in_reply_to_screen_name=None, + in_reply_to_user_id=None, + in_reply_to_status_id=None, + in_reply_to_status_text=None, + truncated=None, + source=None, + now=None, + origin=None, + retweeted_status=None): + '''An object to hold a Twitter status message. + + This class is normally instantiated by the twitter.Api class and + returned in a sequence. + + Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007" + + Args: + created_at: The time this status message was posted + favorited: Whether this is a favorite of the authenticated user + id: The unique id of this status message + text: The text of this status message + location: the geolocation string associated with this message + relative_created_at: + A human readable string representing the posting time + user: + A twitter.User instance representing the person posting the message + now: + The current time, if the client choses to set it. Defaults to the + wall clock time. + ''' + self.created_at = created_at + self.favorited = favorited + self.id = id + self.text = text + self.location = location + self.user = user + self.now = now + self.in_reply_to_screen_name = in_reply_to_screen_name + self.in_reply_to_user_id = in_reply_to_user_id + self.in_reply_to_status_id = in_reply_to_status_id + self.in_reply_to_status_text = in_reply_to_status_text + self.truncated = truncated + self.source = source + self.origin = origin + self.rel_created_at = None + self.retweeted_status = retweeted_status + + def GetCreatedAt(self): + '''Get the time this status message was posted. + + Returns: + The time this status message was posted + ''' + return self._created_at + + def SetCreatedAt(self, created_at): + '''Set the time this status message was posted. + + Args: + created_at: The time this status message was created + ''' + self._created_at = created_at + + created_at = property(GetCreatedAt, SetCreatedAt, + doc='The time this status message was posted.') + + def GetCreatedAtInSeconds(self): + '''Get the time this status message was posted, in seconds since the epoch. + + Returns: + The time this status message was posted, in seconds since the epoch. + ''' + return calendar.timegm(rfc822.parsedate(self.created_at)) + + created_at_in_seconds = property(GetCreatedAtInSeconds, + doc="The time this status message was " + "posted, in seconds since the epoch") + + def GetFavorited(self): + '''Get the favorited setting of this status message. + + Returns: + True if this status message is favorited; False otherwise + ''' + return self._favorited + + def SetFavorited(self, favorited): + '''Set the favorited state of this status message. + + Args: + favorited: boolean True/False favorited state of this status message + ''' + self._favorited = favorited + + favorited = property(GetFavorited, SetFavorited, + doc='The favorited state of this status message.') + + def GetId(self): + '''Get the unique id of this status message. + + Returns: + The unique id of this status message + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this status message. + + Args: + id: The unique id of this status message + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this status message.') + + def GetInReplyToScreenName(self): + return self._in_reply_to_screen_name + + def SetInReplyToScreenName(self, in_reply_to_screen_name): + self._in_reply_to_screen_name = in_reply_to_screen_name + + in_reply_to_screen_name = property(GetInReplyToScreenName, SetInReplyToScreenName, + doc='') + + def GetInReplyToUserId(self): + return self._in_reply_to_user_id + + def SetInReplyToUserId(self, in_reply_to_user_id): + self._in_reply_to_user_id = in_reply_to_user_id + + in_reply_to_user_id = property(GetInReplyToUserId, SetInReplyToUserId, + doc='') + + def GetInReplyToStatusId(self): + return self._in_reply_to_status_id + + def SetInReplyToStatusId(self, in_reply_to_status_id): + self._in_reply_to_status_id = in_reply_to_status_id + + in_reply_to_status_id = property(GetInReplyToStatusId, SetInReplyToStatusId, + doc='') + + def GetTruncated(self): + return self._truncated + + def SetTruncated(self, truncated): + self._truncated = truncated + + truncated = property(GetTruncated, SetTruncated, + doc='') + + def GetSource(self): + return self._source + + def SetSource(self, source): + self._source = source + + source = property(GetSource, SetSource, + doc='') + + def GetText(self): + '''Get the text of this status message. + + Returns: + The text of this status message. + ''' + return self._text + + def SetText(self, text): + '''Set the text of this status message. + + Args: + text: The text of this status message + ''' + self._text = text + + text = property(GetText, SetText, + doc='The text of this status message') + + def GetLocation(self): + '''Get the geolocation associated with this status message + + Returns: + The geolocation string of this status message. + ''' + return self._location + + def SetLocation(self, location): + '''Set the geolocation associated with this status message + + Args: + location: The geolocation string of this status message + ''' + self._location = location + + location = property(GetLocation, SetLocation, + doc='The geolocation string of this status message') + + def GetRelativeCreatedAt(self,time_now=time.time()): + '''Get a human redable string representing the posting time + + Returns: + A human readable string representing the posting time + ''' + fudge = 1.25 + delta = long(time_now) - long(self.created_at_in_seconds) + + if delta < (1 * fudge): + return 'about a second ago' + elif delta < (60 * (1/fudge)): + return 'about %d seconds ago' % (delta) + elif delta < (60 * fudge): + return 'about a minute ago' + elif delta < (60 * 60 * (1/fudge)): + return 'about %d minutes ago' % (delta / 60) + elif delta < (60 * 60 * fudge) or delta / (60 * 60) == 1: + return 'about an hour ago' + elif delta < (60 * 60 * 24 * (1/fudge)): + return 'about %d hours ago' % (delta / (60 * 60)) + elif delta < (60 * 60 * 24 * fudge) or delta / (60 * 60 * 24) == 1: + return 'about a day ago' + else: + return 'about %d days ago' % (delta / (60 * 60 * 24)) + + relative_created_at = property(GetRelativeCreatedAt, + doc='Get a human readable string representing' + 'the posting time') + + def GetUser(self): + '''Get a twitter.User reprenting the entity posting this status message. + + Returns: + A twitter.User reprenting the entity posting this status message + ''' + return self._user + + def SetUser(self, user): + '''Set a twitter.User reprenting the entity posting this status message. + + Args: + user: A twitter.User reprenting the entity posting this status message + ''' + self._user = user + + user = property(GetUser, SetUser, + doc='A twitter.User reprenting the entity posting this ' + 'status message') + + def GetNow(self): + '''Get the wallclock time for this status message. + + Used to calculate relative_created_at. Defaults to the time + the object was instantiated. + + Returns: + Whatever the status instance believes the current time to be, + in seconds since the epoch. + ''' + if self._now is None: + self._now = time.time() + return self._now + + def SetNow(self, now): + '''Set the wallclock time for this status message. + + Used to calculate relative_created_at. Defaults to the time + the object was instantiated. + + Args: + now: The wallclock time for this instance. + ''' + self._now = now + + now = property(GetNow, SetNow, + doc='The wallclock time for this status instance.') + + def __cmp__(self,other): + if self.id == other.id: + return 0 + if self.created_at_in_seconds > other.created_at_in_seconds: + return -1 + return 1 + + def __lt__(self,other): + return other and self.created_at < other.created_at + + def __le__(self,other): + return other and self.created_at <= other.created_at + + def __gt__(self,other): + return other and self.created_at > other.created_at + + def __ge__(self,other): + return other and self.created_at >= other.created_at + + def __ne__(self, other): + return not self.__eq__(other) + + + def __eq__(self, other): + try: + return other and self.id == other.id + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.Status instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.Status instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.Status instance. + + Returns: + A JSON string representation of this twitter.Status instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.Status instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.Status instance + ''' + data = {} + if self.created_at: + data['created_at'] = self.created_at + if self.favorited: + data['favorited'] = self.favorited + if self.id: + data['id'] = self.id + if self.text: + data['text'] = self.text + if self.location: + data['location'] = self.location + if self.user: + data['user'] = self.user.AsDict() + if self.in_reply_to_screen_name: + data['in_reply_to_screen_name'] = self.in_reply_to_screen_name + if self.in_reply_to_user_id: + data['in_reply_to_user_id'] = self.in_reply_to_user_id + if self.in_reply_to_status_id: + data['in_reply_to_status_id'] = self.in_reply_to_status_id + if self.truncated is not None: + data['truncated'] = self.truncated + if self.favorited is not None: + data['favorited'] = self.favorited + if self.source: + data['source'] = self.source + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the twitter API + Returns: + A twitter.Status instance + ''' + if 'retweeted_status' in data: + retweeted_status = Status.NewFromJsonDict(data['retweeted_status']) + else: + retweeted_status = None + + if 'user' in data: + user = User.NewFromJsonDict(data['user']) + else: + user = None + return Status(created_at=data.get('created_at', None), + favorited=data.get('favorited', None), + id=long(data.get('id', None)), + text=unescape(data.get('text', None)), + location=data.get('location', None), + in_reply_to_screen_name=data.get('in_reply_to_screen_name', None), + in_reply_to_user_id=data.get('in_reply_to_user_id', None), + in_reply_to_status_id=data.get('in_reply_to_status_id', None), + truncated=data.get('truncated', None), + source=data.get('source', None), + user=user, + retweeted_status=retweeted_status) + +class List(object): + '''A class representing the List structure used by the twitter API. + + The List structure exposes the following properties: + + list.id + list.name + list.slug + list.description + list.full_name + list.mode + list.uri + list.member_count + list.subscriber_count + list.following + ''' + def __init__(self, + id=None, + name=None, + slug=None, + description=None, + full_name=None, + mode=None, + uri=None, + member_count=None, + subscriber_count=None, + following=None, + user=None): + self.id = id + self.name = name + self.slug = slug + self.description = description + self.full_name = full_name + self.mode = mode + self.uri = uri + self.member_count = member_count + self.subscriber_count = subscriber_count + self.following = following + self.user = user + + def GetId(self): + '''Get the unique id of this list. + + Returns: + The unique id of this list + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this list. + + Args: + id: + The unique id of this list. + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this list.') + + def GetName(self): + '''Get the real name of this list. + + Returns: + The real name of this list + ''' + return self._name + + def SetName(self, name): + '''Set the real name of this list. + + Args: + name: + The real name of this list + ''' + self._name = name + + name = property(GetName, SetName, + doc='The real name of this list.') + + def GetSlug(self): + '''Get the slug of this list. + + Returns: + The slug of this list + ''' + return self._slug + + def SetSlug(self, slug): + '''Set the slug of this list. + + Args: + slug: + The slug of this list. + ''' + self._slug = slug + + slug = property(GetSlug, SetSlug, + doc='The slug of this list.') + + def GetDescription(self): + '''Get the description of this list. + + Returns: + The description of this list + ''' + return self._description + + def SetDescription(self, description): + '''Set the description of this list. + + Args: + description: + The description of this list. + ''' + self._description = description + + description = property(GetDescription, SetDescription, + doc='The description of this list.') + + def GetFull_name(self): + '''Get the full_name of this list. + + Returns: + The full_name of this list + ''' + return self._full_name + + def SetFull_name(self, full_name): + '''Set the full_name of this list. + + Args: + full_name: + The full_name of this list. + ''' + self._full_name = full_name + + full_name = property(GetFull_name, SetFull_name, + doc='The full_name of this list.') + + def GetMode(self): + '''Get the mode of this list. + + Returns: + The mode of this list + ''' + return self._mode + + def SetMode(self, mode): + '''Set the mode of this list. + + Args: + mode: + The mode of this list. + ''' + self._mode = mode + + mode = property(GetMode, SetMode, + doc='The mode of this list.') + + def GetUri(self): + '''Get the uri of this list. + + Returns: + The uri of this list + ''' + return self._uri + + def SetUri(self, uri): + '''Set the uri of this list. + + Args: + uri: + The uri of this list. + ''' + self._uri = uri + + uri = property(GetUri, SetUri, + doc='The uri of this list.') + + def GetMember_count(self): + '''Get the member_count of this list. + + Returns: + The member_count of this list + ''' + return self._member_count + + def SetMember_count(self, member_count): + '''Set the member_count of this list. + + Args: + member_count: + The member_count of this list. + ''' + self._member_count = member_count + + member_count = property(GetMember_count, SetMember_count, + doc='The member_count of this list.') + + def GetSubscriber_count(self): + '''Get the subscriber_count of this list. + + Returns: + The subscriber_count of this list + ''' + return self._subscriber_count + + def SetSubscriber_count(self, subscriber_count): + '''Set the subscriber_count of this list. + + Args: + subscriber_count: + The subscriber_count of this list. + ''' + self._subscriber_count = subscriber_count + + subscriber_count = property(GetSubscriber_count, SetSubscriber_count, + doc='The subscriber_count of this list.') + + def GetFollowing(self): + '''Get the following status of this list. + + Returns: + The following status of this list + ''' + return self._following + + def SetFollowing(self, following): + '''Set the following status of this list. + + Args: + following: + The following of this list. + ''' + self._following = following + + following = property(GetFollowing, SetFollowing, + doc='The following status of this list.') + + def GetUser(self): + '''Get the user of this list. + + Returns: + The owner of this list + ''' + return self._user + + def SetUser(self, user): + '''Set the user of this list. + + Args: + user: + The owner of this list. + ''' + self._user = user + + user = property(GetUser, SetUser, + doc='The owner of this list.') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.name == other.name and \ + self.slug == other.slug and \ + self.description == other.description and \ + self.full_name == other.full_name and \ + self.mode == other.mode and \ + self.uri == other.uri and \ + self.member_count == other.member_count and \ + self.subscriber_count == other.subscriber_count and \ + self.following == other.following and \ + self.user == other.user + + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.List instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.List instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.List instance. + + Returns: + A JSON string representation of this twitter.List instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.List instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.List instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.name: + data['name'] = self.name + if self.slug: + data['slug'] = self.slug + if self.description: + data['description'] = self.description + if self.full_name: + data['full_name'] = self.full_name + if self.mode: + data['mode'] = self.mode + if self.uri: + data['uri'] = self.uri + if self.member_count is not None: + data['member_count'] = self.member_count + if self.subscriber_count is not None: + data['subscriber_count'] = self.subscriber_count + if self.following is not None: + data['following'] = self.following + if self.user is not None: + data['user'] = self.user + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.List instance + ''' + if 'user' in data: + user = User.NewFromJsonDict(data['user']) + else: + user = None + return List(id=data.get('id', None), + name=data.get('name', None), + slug=data.get('slug', None), + description=data.get('description', None), + full_name=data.get('full_name', None), + mode=data.get('mode', None), + uri=data.get('uri', None), + member_count=data.get('member_count', None), + subscriber_count=data.get('subscriber_count', None), + following=data.get('following', None), + user=user) + +class User(object): + '''A class representing the User structure used by the twitter API. + + The User structure exposes the following properties: + + user.id + user.name + user.screen_name + user.location + user.description + user.profile_image_url + user.profile_background_tile + user.profile_background_image_url + user.profile_sidebar_fill_color + user.profile_background_color + user.profile_link_color + user.profile_text_color + user.protected + user.utc_offset + user.time_zone + user.url + user.status + user.statuses_count + user.followers_count + user.friends_count + user.favourites_count + ''' + def __init__(self, + id=None, + name=None, + screen_name=None, + location=None, + description=None, + profile_image_url=None, + profile_background_tile=None, + profile_background_image_url=None, + profile_sidebar_fill_color=None, + profile_background_color=None, + profile_link_color=None, + profile_text_color=None, + protected=None, + utc_offset=None, + time_zone=None, + followers_count=None, + friends_count=None, + statuses_count=None, + favourites_count=None, + url=None, + status=None): + self.id = id + self.name = name + self.screen_name = screen_name + self.location = location + self.description = description + self.profile_image_url = profile_image_url + self.profile_background_tile = profile_background_tile + self.profile_background_image_url = profile_background_image_url + self.profile_sidebar_fill_color = profile_sidebar_fill_color + self.profile_background_color = profile_background_color + self.profile_link_color = profile_link_color + self.profile_text_color = profile_text_color + self.protected = protected + self.utc_offset = utc_offset + self.time_zone = time_zone + self.followers_count = followers_count + self.friends_count = friends_count + self.statuses_count = statuses_count + self.favourites_count = favourites_count + self.url = url + self.status = status + + + def GetId(self): + '''Get the unique id of this user. + + Returns: + The unique id of this user + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this user. + + Args: + id: The unique id of this user. + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this user.') + + def GetName(self): + '''Get the real name of this user. + + Returns: + The real name of this user + ''' + return self._name + + def SetName(self, name): + '''Set the real name of this user. + + Args: + name: The real name of this user + ''' + self._name = name + + name = property(GetName, SetName, + doc='The real name of this user.') + + def GetScreenName(self): + '''Get the short username of this user. + + Returns: + The short username of this user + ''' + return self._screen_name + + def SetScreenName(self, screen_name): + '''Set the short username of this user. + + Args: + screen_name: the short username of this user + ''' + self._screen_name = screen_name + + screen_name = property(GetScreenName, SetScreenName, + doc='The short username of this user.') + + def GetLocation(self): + '''Get the geographic location of this user. + + Returns: + The geographic location of this user + ''' + return self._location + + def SetLocation(self, location): + '''Set the geographic location of this user. + + Args: + location: The geographic location of this user + ''' + self._location = location + + location = property(GetLocation, SetLocation, + doc='The geographic location of this user.') + + def GetDescription(self): + '''Get the short text description of this user. + + Returns: + The short text description of this user + ''' + return self._description + + def SetDescription(self, description): + '''Set the short text description of this user. + + Args: + description: The short text description of this user + ''' + self._description = description + + description = property(GetDescription, SetDescription, + doc='The short text description of this user.') + + def GetUrl(self): + '''Get the homepage url of this user. + + Returns: + The homepage url of this user + ''' + return self._url + + def SetUrl(self, url): + '''Set the homepage url of this user. + + Args: + url: The homepage url of this user + ''' + self._url = url + + url = property(GetUrl, SetUrl, + doc='The homepage url of this user.') + + def GetProfileImageUrl(self): + '''Get the url of the thumbnail of this user. + + Returns: + The url of the thumbnail of this user + ''' + return self._profile_image_url + + def SetProfileImageUrl(self, profile_image_url): + '''Set the url of the thumbnail of this user. + + Args: + profile_image_url: The url of the thumbnail of this user + ''' + self._profile_image_url = profile_image_url + + profile_image_url= property(GetProfileImageUrl, SetProfileImageUrl, + doc='The url of the thumbnail of this user.') + + def GetProfileBackgroundTile(self): + '''Boolean for whether to tile the profile background image. + + Returns: + True if the background is to be tiled, False if not, None if unset. + ''' + return self._profile_background_tile + + def SetProfileBackgroundTile(self, profile_background_tile): + '''Set the boolean flag for whether to tile the profile background image. + + Args: + profile_background_tile: Boolean flag for whether to tile or not. + ''' + self._profile_background_tile = profile_background_tile + + profile_background_tile = property(GetProfileBackgroundTile, SetProfileBackgroundTile, + doc='Boolean for whether to tile the background image.') + + def GetProfileBackgroundImageUrl(self): + return self._profile_background_image_url + + def SetProfileBackgroundImageUrl(self, profile_background_image_url): + self._profile_background_image_url = profile_background_image_url + + profile_background_image_url = property(GetProfileBackgroundImageUrl, SetProfileBackgroundImageUrl, + doc='The url of the profile background of this user.') + + def GetProfileSidebarFillColor(self): + return self._profile_sidebar_fill_color + + def SetProfileSidebarFillColor(self, profile_sidebar_fill_color): + self._profile_sidebar_fill_color = profile_sidebar_fill_color + + profile_sidebar_fill_color = property(GetProfileSidebarFillColor, SetProfileSidebarFillColor) + + def GetProfileBackgroundColor(self): + return self._profile_background_color + + def SetProfileBackgroundColor(self, profile_background_color): + self._profile_background_color = profile_background_color + + profile_background_color = property(GetProfileBackgroundColor, SetProfileBackgroundColor) + + def GetProfileLinkColor(self): + return self._profile_link_color + + def SetProfileLinkColor(self, profile_link_color): + self._profile_link_color = profile_link_color + + profile_link_color = property(GetProfileLinkColor, SetProfileLinkColor) + + def GetProfileTextColor(self): + return self._profile_text_color + + def SetProfileTextColor(self, profile_text_color): + self._profile_text_color = profile_text_color + + profile_text_color = property(GetProfileTextColor, SetProfileTextColor) + + def GetProtected(self): + return self._protected + + def SetProtected(self, protected): + self._protected = protected + + protected = property(GetProtected, SetProtected) + + def GetUtcOffset(self): + return self._utc_offset + + def SetUtcOffset(self, utc_offset): + self._utc_offset = utc_offset + + utc_offset = property(GetUtcOffset, SetUtcOffset) + + def GetTimeZone(self): + '''Returns the current time zone string for the user. + + Returns: + The descriptive time zone string for the user. + ''' + return self._time_zone + + def SetTimeZone(self, time_zone): + '''Sets the user's time zone string. + + Args: + time_zone: The descriptive time zone to assign for the user. + ''' + self._time_zone = time_zone + + time_zone = property(GetTimeZone, SetTimeZone) + + def GetStatus(self): + '''Get the latest twitter.Status of this user. + + Returns: + The latest twitter.Status of this user + ''' + return self._status + + def SetStatus(self, status): + '''Set the latest twitter.Status of this user. + + Args: + status: The latest twitter.Status of this user + ''' + self._status = status + + status = property(GetStatus, SetStatus, + doc='The latest twitter.Status of this user.') + + def GetFriendsCount(self): + '''Get the friend count for this user. + + Returns: + The number of users this user has befriended. + ''' + return self._friends_count + + def SetFriendsCount(self, count): + '''Set the friend count for this user. + + Args: + count: The number of users this user has befriended. + ''' + self._friends_count = count + + friends_count = property(GetFriendsCount, SetFriendsCount, + doc='The number of friends for this user.') + + def GetFollowersCount(self): + '''Get the follower count for this user. + + Returns: + The number of users following this user. + ''' + return self._followers_count + + def SetFollowersCount(self, count): + '''Set the follower count for this user. + + Args: + count: The number of users following this user. + ''' + self._followers_count = count + + followers_count = property(GetFollowersCount, SetFollowersCount, + doc='The number of users following this user.') + + def GetStatusesCount(self): + '''Get the number of status updates for this user. + + Returns: + The number of status updates for this user. + ''' + return self._statuses_count + + def SetStatusesCount(self, count): + '''Set the status update count for this user. + + Args: + count: The number of updates for this user. + ''' + self._statuses_count = count + + statuses_count = property(GetStatusesCount, SetStatusesCount, + doc='The number of updates for this user.') + + def GetFavouritesCount(self): + '''Get the number of favourites for this user. + + Returns: + The number of favourites for this user. + ''' + return self._favourites_count + + def SetFavouritesCount(self, count): + '''Set the favourite count for this user. + + Args: + count: The number of favourites for this user. + ''' + self._favourites_count = count + + favourites_count = property(GetFavouritesCount, SetFavouritesCount, + doc='The number of favourites for this user.') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.name == other.name and \ + self.screen_name == other.screen_name and \ + self.location == other.location and \ + self.description == other.description and \ + self.profile_image_url == other.profile_image_url and \ + self.profile_background_tile == other.profile_background_tile and \ + self.profile_background_image_url == other.profile_background_image_url and \ + self.profile_sidebar_fill_color == other.profile_sidebar_fill_color and \ + self.profile_background_color == other.profile_background_color and \ + self.profile_link_color == other.profile_link_color and \ + self.profile_text_color == other.profile_text_color and \ + self.protected == other.protected and \ + self.utc_offset == other.utc_offset and \ + self.time_zone == other.time_zone and \ + self.url == other.url and \ + self.statuses_count == other.statuses_count and \ + self.followers_count == other.followers_count and \ + self.favourites_count == other.favourites_count and \ + self.friends_count == other.friends_count and \ + self.status == other.status + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.User instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.User instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.User instance. + + Returns: + A JSON string representation of this twitter.User instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.User instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.User instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.name: + data['name'] = self.name + if self.screen_name: + data['screen_name'] = self.screen_name + if self.location: + data['location'] = self.location + if self.description: + data['description'] = self.description + if self.profile_image_url: + data['profile_image_url'] = self.profile_image_url + if self.profile_background_tile is not None: + data['profile_background_tile'] = self.profile_background_tile + if self.profile_background_image_url: + data['profile_sidebar_fill_color'] = self.profile_background_image_url + if self.profile_background_color: + data['profile_background_color'] = self.profile_background_color + if self.profile_link_color: + data['profile_link_color'] = self.profile_link_color + if self.profile_text_color: + data['profile_text_color'] = self.profile_text_color + if self.protected is not None: + data['protected'] = self.protected + if self.utc_offset: + data['utc_offset'] = self.utc_offset + if self.time_zone: + data['time_zone'] = self.time_zone + if self.url: + data['url'] = self.url + if self.status: + data['status'] = self.status.AsDict() + if self.friends_count: + data['friends_count'] = self.friends_count + if self.followers_count: + data['followers_count'] = self.followers_count + if self.statuses_count: + data['statuses_count'] = self.statuses_count + if self.favourites_count: + data['favourites_count'] = self.favourites_count + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the twitter API + Returns: + A twitter.User instance + ''' + if 'status' in data: + status = Status.NewFromJsonDict(data['status']) + else: + status = None + return User(id=data.get('id', None), + name=data.get('name', None), + screen_name=data.get('screen_name', None), + location=data.get('location', None), + description=data.get('description', None), + statuses_count=data.get('statuses_count', None), + followers_count=data.get('followers_count', None), + favourites_count=data.get('favourites_count', None), + friends_count=data.get('friends_count', None), + profile_image_url=data.get('profile_image_url', None), + profile_background_tile = data.get('profile_background_tile', None), + profile_background_image_url = data.get('profile_background_image_url', None), + profile_sidebar_fill_color = data.get('profile_sidebar_fill_color', None), + profile_background_color = data.get('profile_background_color', None), + profile_link_color = data.get('profile_link_color', None), + profile_text_color = data.get('profile_text_color', None), + protected = data.get('protected', None), + utc_offset = data.get('utc_offset', None), + time_zone = data.get('time_zone', None), + url=data.get('url', None), + status=status) + +class DirectMessage(object): + '''A class representing the DirectMessage structure used by the twitter API. + + The DirectMessage structure exposes the following properties: + + direct_message.id + direct_message.created_at + direct_message.created_at_in_seconds # read only + direct_message.sender_id + direct_message.sender_screen_name + direct_message.recipient_id + direct_message.recipient_screen_name + direct_message.text + ''' + + def __init__(self, + id=None, + created_at=None, + sender_id=None, + sender_screen_name=None, + recipient_id=None, + recipient_screen_name=None, + text=None): + '''An object to hold a Twitter direct message. + + This class is normally instantiated by the twitter.Api class and + returned in a sequence. + + Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007" + + Args: + id: The unique id of this direct message + created_at: The time this direct message was posted + sender_id: The id of the twitter user that sent this message + sender_screen_name: The name of the twitter user that sent this message + recipient_id: The id of the twitter that received this message + recipient_screen_name: The name of the twitter that received this message + text: The text of this direct message + ''' + self.id = id + self.created_at = created_at + self.sender_id = sender_id + self.sender_screen_name = sender_screen_name + self.recipient_id = recipient_id + self.recipient_screen_name = recipient_screen_name + self.text = text + + def GetId(self): + '''Get the unique id of this direct message. + + Returns: + The unique id of this direct message + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this direct message. + + Args: + id: The unique id of this direct message + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this direct message.') + + def GetCreatedAt(self): + '''Get the time this direct message was posted. + + Returns: + The time this direct message was posted + ''' + return self._created_at + + def SetCreatedAt(self, created_at): + '''Set the time this direct message was posted. + + Args: + created_at: The time this direct message was created + ''' + self._created_at = created_at + + created_at = property(GetCreatedAt, SetCreatedAt, + doc='The time this direct message was posted.') + + def GetCreatedAtInSeconds(self): + '''Get the time this direct message was posted, in seconds since the epoch. + + Returns: + The time this direct message was posted, in seconds since the epoch. + ''' + return calendar.timegm(rfc822.parsedate(self.created_at)) + + created_at_in_seconds = property(GetCreatedAtInSeconds, + doc="The time this direct message was " + "posted, in seconds since the epoch") + + def GetRelativeCreatedAt(self,time_now=time.time()): + '''Get a human redable string representing the posting time + + Returns: + A human readable string representing the posting time + ''' + fudge = 1.25 + delta = long(time_now) - long(self.created_at_in_seconds) + + if delta < (1 * fudge): + return 'about a second ago' + elif delta < (60 * (1/fudge)): + return 'about %d seconds ago' % (delta) + elif delta < (60 * fudge): + return 'about a minute ago' + elif delta < (60 * 60 * (1/fudge)): + return 'about %d minutes ago' % (delta / 60) + elif delta < (60 * 60 * fudge) or delta / (60 * 60) == 1: + return 'about an hour ago' + elif delta < (60 * 60 * 24 * (1/fudge)): + return 'about %d hours ago' % (delta / (60 * 60)) + elif delta < (60 * 60 * 24 * fudge) or delta / (60 * 60 * 24) == 1: + return 'about a day ago' + else: + return 'about %d days ago' % (delta / (60 * 60 * 24)) + + relative_created_at = property(GetRelativeCreatedAt, + doc='Get a human readable string representing' + 'the posting time') + + def GetSenderId(self): + '''Get the unique sender id of this direct message. + + Returns: + The unique sender id of this direct message + ''' + return self._sender_id + + def SetSenderId(self, sender_id): + '''Set the unique sender id of this direct message. + + Args: + sender id: The unique sender id of this direct message + ''' + self._sender_id = sender_id + + sender_id = property(GetSenderId, SetSenderId, + doc='The unique sender id of this direct message.') + + def GetSenderScreenName(self): + '''Get the unique sender screen name of this direct message. + + Returns: + The unique sender screen name of this direct message + ''' + return self._sender_screen_name + + def SetSenderScreenName(self, sender_screen_name): + '''Set the unique sender screen name of this direct message. + + Args: + sender_screen_name: The unique sender screen name of this direct message + ''' + self._sender_screen_name = sender_screen_name + + sender_screen_name = property(GetSenderScreenName, SetSenderScreenName, + doc='The unique sender screen name of this direct message.') + + def GetRecipientId(self): + '''Get the unique recipient id of this direct message. + + Returns: + The unique recipient id of this direct message + ''' + return self._recipient_id + + def SetRecipientId(self, recipient_id): + '''Set the unique recipient id of this direct message. + + Args: + recipient id: The unique recipient id of this direct message + ''' + self._recipient_id = recipient_id + + recipient_id = property(GetRecipientId, SetRecipientId, + doc='The unique recipient id of this direct message.') + + def GetRecipientScreenName(self): + '''Get the unique recipient screen name of this direct message. + + Returns: + The unique recipient screen name of this direct message + ''' + return self._recipient_screen_name + + def SetRecipientScreenName(self, recipient_screen_name): + '''Set the unique recipient screen name of this direct message. + + Args: + recipient_screen_name: The unique recipient screen name of this direct message + ''' + self._recipient_screen_name = recipient_screen_name + + recipient_screen_name = property(GetRecipientScreenName, SetRecipientScreenName, + doc='The unique recipient screen name of this direct message.') + + def GetText(self): + '''Get the text of this direct message. + + Returns: + The text of this direct message. + ''' + return self._text + + def SetText(self, text): + '''Set the text of this direct message. + + Args: + text: The text of this direct message + ''' + self._text = text + + text = property(GetText, SetText, + doc='The text of this direct message') + + def __cmp__(self,other): + if self.id == other.id: + return 0 + if self.created_at < other.created_at: + return -1 + return 1 + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.created_at == other.created_at and \ + self.sender_id == other.sender_id and \ + self.sender_screen_name == other.sender_screen_name and \ + self.recipient_id == other.recipient_id and \ + self.recipient_screen_name == other.recipient_screen_name and \ + self.text == other.text + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.DirectMessage instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.DirectMessage instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.DirectMessage instance. + + Returns: + A JSON string representation of this twitter.DirectMessage instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.DirectMessage instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.DirectMessage instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.created_at: + data['created_at'] = self.created_at + if self.sender_id: + data['sender_id'] = self.sender_id + if self.sender_screen_name: + data['sender_screen_name'] = self.sender_screen_name + if self.recipient_id: + data['recipient_id'] = self.recipient_id + if self.recipient_screen_name: + data['recipient_screen_name'] = self.recipient_screen_name + if self.text: + data['text'] = self.text + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the twitter API + Returns: + A twitter.DirectMessage instance + ''' + return DirectMessage(created_at=data.get('created_at', None), + recipient_id=data.get('recipient_id', None), + sender_id=data.get('sender_id', None), + text=data.get('text', None), + sender_screen_name=data.get('sender_screen_name', None), + id=data.get('id', None), + recipient_screen_name=data.get('recipient_screen_name', None)) + +class Api(object): + '''A python interface into the Twitter API + + By default, the Api caches results for 1 minute. + + Example usage: + + To create an instance of the twitter.Api class, with no authentication: + + >>> import twitter + >>> api = twitter.Api() + + To fetch the most recently posted public twitter status messages: + + >>> statuses = api.GetPublicTimeline() + >>> print [s.user.name for s in statuses] + [u'DeWitt', u'Kesuke Miyagi', u'ev', u'Buzz Andersen', u'Biz Stone'] #... + + To fetch a single user's public status messages, where "user" is either + a Twitter "short name" or their user id. + + >>> statuses = api.GetUserTimeline(user) + >>> print [s.text for s in statuses] + + To use authentication, instantiate the twitter.Api class with a + username, password and the oAuth key and secret: + + >>> api = twitter.Api(username='twitter user', password='twitter pass', + access_token_key='the_key_given', + access_token_secret='the_key_secret') + + To fetch your friends (after being authenticated): + + >>> users = api.GetFriends() + >>> print [u.name for u in users] + + To post a twitter status message (after being authenticated): + + >>> status = api.PostUpdate('I love python-twitter!') + >>> print status.text + I love python-twitter! + + There are many other methods, including: + + >>> api.PostUpdates(status) + >>> api.PostDirectMessage(user, text) + >>> api.GetUser(user) + >>> api.GetReplies() + >>> api.GetUserTimeline(user) + >>> api.GetStatus(id) + >>> api.DestroyStatus(id) + >>> api.GetFriendsTimeline(user) + >>> api.GetFriends(user) + >>> api.GetFollowers() + >>> api.GetFeatured() + >>> api.GetDirectMessages() + >>> api.PostDirectMessage(user, text) + >>> api.DestroyDirectMessage(id) + >>> api.DestroyFriendship(user) + >>> api.CreateFriendship(user) + >>> api.GetUserByEmail(email) + >>> api.VerifyCredentials() + ''' + + DEFAULT_CACHE_TIMEOUT = 60 # cache for 1 minute + _API_REALM = 'Twitter API' + + def __init__(self, + username=None, + password=None, + access_token_key=None, + access_token_secret=None, + input_encoding=None, + request_headers=None, + cache=DEFAULT_CACHE, + shortner=None, + base_url=None, + use_gzip_compression=False): + '''Instantiate a new twitter.Api object. + + Args: + username: + The username of the twitter account. [optional] + NOTE: for oAuth based authentication, this is not + optional and the value is the Twitter + Consumer Key value *not* your Twitter ID + password: + The password for the twitter account. [optional] + NOTE: for oAuth based authentication, this is not + optional and the value is the Twitter + Consumer Secret value *not* your Twitter + password + access_token_key: + The oAuth access token key value you retrieved + from running get_access_token.py. + access_token_secret: + The oAuth access token's secret, also retrieved + from the get_access_token.py run. + input_encoding: + The encoding used to encode input strings. [optional] + request_header: + A dictionary of additional HTTP request headers. [optional] + cache: + The cache instance to use. Defaults to DEFAULT_CACHE. + Use None to disable caching. [optional] + shortner: + The shortner instance to use. Defaults to None. + See shorten_url.py for an example shortner. [optional] + base_url: + The base URL to use to contact the Twitter API. + Defaults to https://twitter.com. [optional] + use_gzip_compression: + Set to True to tell enable gzip compression for any call + made to Twitter. Defaults to False. [optional] + ''' + self.SetCache(cache) + self._urllib = urllib2 + self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT + self._input_encoding = input_encoding + self._use_gzip = use_gzip_compression + self._oauth_consumer = None + + self._InitializeRequestHeaders(request_headers) + self._InitializeUserAgent() + self._InitializeDefaultParameters() + + if base_url is None: + self.base_url = 'https://api.twitter.com/1' + else: + self.base_url = base_url + + if username is not None and (access_token_key is None or + access_token_secret is None): + print >> sys.stderr, 'Twitter now requires an oAuth Access Token for API calls.' + print >> sys.stderr, 'If your using this library from a command line utility, please' + print >> sys.stderr, 'run the the included get_access_token.py tool to generate one.' + + raise TwitterError('Twitter requires oAuth Access Token for all API access') + + self.SetCredentials(username, password, access_token_key, access_token_secret) + + def SetCredentials(self, + username, + password, + access_token_key=None, + access_token_secret=None): + '''Set the username and password for this instance + + Args: + username: + The username of the twitter account. + password: + The password for the twitter account. + access_token_key: + The oAuth access token key value you retrieved + from running get_access_token.py. + access_token_secret: + The oAuth access token's secret, also retrieved + from the get_access_token.py run. + ''' + self._username = username + self._password = password + self._access_token_key = access_token_key + self._access_token_secret = access_token_secret + self._oauth_consumer = None + + if username is not None and password is not None and \ + access_token_key is not None and access_token_secret is not None: + self._signature_method_plaintext = oauth.SignatureMethod_PLAINTEXT() + self._signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + + self._oauth_token = oauth.Token(key=access_token_key, secret=access_token_secret) + self._oauth_consumer = oauth.Consumer(key=username, secret=password) + + def ClearCredentials(self): + '''Clear the any credentials for this instance + ''' + self._username = None + self._password = None + self._access_token_key = None + self._access_token_secret = None + self._oauth_consumer = None + + def GetPublicTimeline(self, + since_id=None): + '''Fetch the sequence of public twitter.Status message for all users. + + Args: + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + + Returns: + An sequence of twitter.Status instances, one for each message + ''' + parameters = {} + + if since_id: + parameters['since_id'] = since_id + + url = '%s/statuses/public_timeline.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def FilterPublicTimeline(self, + term, + since_id=None): + '''Filter the public twitter timeline by a given search term on + the local machine. + + Args: + term: + term to search by. + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + + Returns: + A sequence of twitter.Status instances, one for each message + containing the term + ''' + statuses = self.GetPublicTimeline(since_id) + results = [] + + for s in statuses: + if s.text.lower().find(term.lower()) != -1: + results.append(s) + + return results + + def GetSearch(self, + term, + geocode=None, + since_id=None, + per_page=15, + page=1, + lang=None, + show_user="true", + query_users=False): + '''Return twitter search results for a given term. + + Args: + term: + term to search by. + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + geocode: + geolocation information in the form (latitude, longitude, radius) + [optional] + per_page: + number of results to return. Default is 15 [optional] + page: + which page of search results to return + lang: + language for results. Default is English [optional] + show_user: + prefixes screen name in status + query_users: + If set to False, then all users only have screen_name and + profile_image_url available. + If set to True, all information of users are available, + but it uses lots of request quota, one per status. + Returns: + A sequence of twitter.Status instances, one for each message containing + the term + ''' + # Build request parameters + parameters = {} + + if since_id: + parameters['since_id'] = since_id + + if (not term) and (not geocode): + return [] + + parameters['q'] = term #urllib.quote_plus(term) + parameters['show_user'] = show_user + if lang: + parameters['lang'] = lang + parameters['rpp'] = per_page + parameters['page'] = page + + if geocode is not None: + parameters['geocode'] = ','.join(map(unicode, geocode)) + + # Make and send requests + if 'twitter' in self.base_url: + url = 'http://search.twitter.com/search.json' + else: + url = '%s/search.json' % self.base_url + + json = self._FetchUrl(url, post_data=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + results = [] + + for x in data['results']: + temp = Status.NewFromJsonDict(x) + + if query_users: + # Build user object with new request + temp.user = self.GetUser(urllib.quote(x['from_user'])) + else: + temp.user = User(screen_name=x['from_user'], profile_image_url=x['profile_image_url']) + + results.append(temp) + + # Return built list of statuses + return results # [Status.NewFromJsonDict(x) for x in data['results']] + + def GetFriendsTimeline(self, + user=None, + count=None, + since=None, + since_id=None, + retweets=False): + '''Fetch the sequence of twitter.Status messages for a user's friends + + The twitter.Api instance must be authenticated if the user is private. + + Args: + user: + Specifies the ID or screen name of the user for whom to return + the friends_timeline. If unspecified, the username and password + must be set in the twitter.Api instance. [Optional] + count: + Specifies the number of statuses to retrieve. May not be + greater than 200. [Optional] + since: + Narrows the returned results to just those statuses created + after the specified HTTP-formatted date. [Optional] + since_id: + Returns only public statuses with an ID greater than (that is, + more recent than) the specified ID. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each message + ''' + if not user and not self._oauth_consumer: + raise TwitterError("User must be specified if API is not authenticated.") + url = '%s/statuses' % self.base_url + if retweets: + src = 'home_timeline' + else: + src = 'friends_timeline' + + if user: + url = '%s/%s/%s.json' % (url, src, user) + else: + url = '%s/%s.json' % (url, src) + parameters = {} + if count is not None: + try: + if int(count) > 200: + raise TwitterError("'count' may not be greater than 200") + except ValueError: + raise TwitterError("'count' must be an integer") + parameters['count'] = count + if since: + parameters['since'] = since + if since_id: + parameters['since_id'] = since_id + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] + + + def GetUserTimeline(self, + id=None, + user_id=None, + screen_name=None, + since_id=None, + max_id=None, + count=None, + page=None, + include_rts=None, + include_entities=None): + '''Fetch the sequence of public Status messages for a single user. + + The twitter.Api instance must be authenticated if the user is private. + + Args: + id: + Specifies the ID or screen name of the user for whom to return + the user_timeline. [Optional] + user_id: + Specfies the ID of the user for whom to return the + user_timeline. Helpful for disambiguating when a valid user ID + is also a valid screen name. [Optional] + screen_name: + Specfies the screen name of the user for whom to return the + user_timeline. Helpful for disambiguating when a valid screen + name is also a user ID. [Optional] + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns only statuses with an ID less than (that is, older + than) or equal to the specified ID. [Optional] + count: + Specifies the number of statuses to retrieve. May not be + greater than 200. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + include_rts: + If True, the timeline will contain native retweets (if they + exist) in addition to the standard stream of tweets. [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] + + Returns: + A sequence of Status instances, one for each message up to count + ''' + parameters = {} + + if id: + url = '%s/statuses/user_timeline/%s.json' % (self.base_url, id) + elif user_id: + url = '%s/statuses/user_timeline.json?user_id=%d' % (self.base_url, user_id) + elif screen_name: + url = ('%s/statuses/user_timeline.json?screen_name=%s' % (self.base_url, + screen_name)) + elif not self._oauth_consumer: + raise TwitterError("User must be specified if API is not authenticated.") + else: + url = '%s/statuses/user_timeline.json' % self.base_url + + if since_id: + try: + parameters['since_id'] = long(since_id) + except: + raise TwitterError("since_id must be an integer") + + if max_id: + try: + parameters['max_id'] = long(max_id) + except: + raise TwitterError("max_id must be an integer") + + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + if page: + try: + parameters['page'] = int(page) + except: + raise TwitterError("page must be an integer") + + if include_rts: + parameters['include_rts'] = 1 + + if include_entities: + parameters['include_entities'] = 1 + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] + + + def GetHomeTimeline(self, + since_id=None, + max_id=None, + count=None, + page=None, + include_entities=None): + '''Fetch the sequence of home Status messages for authenticated user. + + The twitter.Api instance must be authenticated if the user is private. + + Args: + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns only statuses with an ID less than (that is, older + than) or equal to the specified ID. [Optional] + count: + Specifies the number of statuses to retrieve. May not be + greater than 200. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + include_rts: + If True, the timeline will contain native retweets (if they + exist) in addition to the standard stream of tweets. [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] + + Returns: + A sequence of Status instances, one for each message up to count + ''' + parameters = {} + + url = '%s/statuses/home_timeline.json' % self.base_url + + if since_id: + try: + parameters['since_id'] = long(since_id) + except: + raise TwitterError("since_id must be an integer") + + if max_id: + try: + parameters['max_id'] = long(max_id) + except: + raise TwitterError("max_id must be an integer") + + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + if page: + try: + parameters['page'] = int(page) + except: + raise TwitterError("page must be an integer") + + if include_entities: + parameters['include_entities'] = 1 + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] + + def GetRetweetsForStatus(self,id=None): + '''Return retweet of a status + The twitter.Api instance must be authenticated if the status message is private. + + Args: + id: The numerical ID of the status to retrieve retweets. + + Returns: + A sequence of Status instances, one for each retweet + ''' + if id == None: + raise TwitterError("A status id must be specified.") + elif not self._oauth_consumer: + raise TwitterError("User must be specified if API is not authenticated.") + + url = '%s/statuses/retweets/%id.json' % (self.base_url,id) + + json = self._FetchUrl(url, parameters={'id':id,}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] + + def GetStatus(self, id): + '''Returns a single status message. + + The twitter.Api instance must be authenticated if the status message is private. + + Args: + id: The numerical ID of the status you're trying to retrieve. + + Returns: + A twitter.Status instance representing that status message + ''' + try: + if id: + long(id) + except: + raise TwitterError("id must be an long integer") + url = '%s/statuses/show/%s.json' % (self.base_url, id) + json = self._FetchUrl(url) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return Status.NewFromJsonDict(data) + + def DestroyStatus(self, id): + '''Destroys the status specified by the required ID parameter. + + The twitter.Api instance must be authenticated and thee + authenticating user must be the author of the specified status. + + Args: + id: The numerical ID of the status you're trying to destroy. + + Returns: + A twitter.Status instance representing the destroyed status message + ''' + try: + if id: + long(id) + except: + raise TwitterError("id must be an integer") + url = '%s/statuses/destroy/%s.json' % (self.base_url, id) + pdata = {} + pdata['id']=id + json = self._FetchUrl(url, post_data=pdata) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return Status.NewFromJsonDict(data) + + def PostUpdate(self, status, in_reply_to_status_id=None, latitude=None, longitude=None): + '''Post a twitter status message from the authenticated user. + + The twitter.Api instance must be authenticated. + + Args: + status: + The message text to be posted. Must be less than or equal to + 140 characters. + in_reply_to_status_id: + The ID of an existing status that the status to be posted is + in reply to. This implicitly sets the in_reply_to_user_id + attribute of the resulting status to the user ID of the + message being replied to. Invalid/missing status IDs will be + ignored. [Optional] + Returns: + A twitter.Status instance representing the message posted. + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + url = '%s/statuses/update.json' % self.base_url + + #ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ + if len(status.decode('utf-8')) > CHARACTER_LIMIT: + raise TwitterError("Text must be less than or equal to %d characters. " + "Consider using PostUpdates." % CHARACTER_LIMIT) + + data = {'status': status} + if in_reply_to_status_id: + data['in_reply_to_status_id'] = in_reply_to_status_id + if latitude is not None: + data['lat'] = latitude + data['display_coordinates'] = True + data['geo_enabled'] = True + if longitude is not None: + data['long'] = longitude + +# print data + json = self._FetchUrl(url, post_data=data) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return Status.NewFromJsonDict(data) + + def PostSerializedUpdates(self, status, continuation=None, **kwargs): + '''Post one or more twitter status messages from the authenticated user. + + Unlike api.PostUpdate, this method will post multiple status updates + if the message is longer than 140 characters. + + The twitter.Api instance must be authenticated. + + Args: + status: + The message text to be posted. May be longer than 140 characters. + continuation: + The character string, if any, to be appended to all but the + last message. Note that Twitter strips trailing '...' strings + from messages. Consider using the unicode \u2026 character + (horizontal ellipsis) instead. [Defaults to None] + **kwargs: + See api.PostUpdate for a list of accepted parameters. + Returns: + A of list twitter.Status instance representing the messages posted. + ''' + results = list() + line_length = CHARACTER_LIMIT + lines = textwrap.wrap(status, line_length) + if len(lines) > 9: + line_length = CHARACTER_LIMIT - 6 + lines = textwrap.wrap(status, line_length) + elif len(lines) > 1: + line_length = CHARACTER_LIMIT - 4 + lines = textwrap.wrap(status, line_length) + counter = 1 + tot = len(lines) + print kwargs + if len(lines)==1: + results.append(self.PostUpdate(lines[0], **kwargs)) + else: + for line in lines: + #print 'line',type(unicode(line,'utf-8').encode('utf-8')),line + r = self.PostUpdate(line \ + + ' ' \ + + str(counter) \ + + '/' \ + + str(tot), \ + **kwargs) + results.append(r) + counter = counter + 1 + return results + + def PostUpdates(self, status, continuation=None, **kwargs): + '''Post one or more twitter status messages from the authenticated user. + + Unlike api.PostUpdate, this method will post multiple status updates + if the message is longer than 140 characters. + + The twitter.Api instance must be authenticated. + + Args: + status: + The message text to be posted. May be longer than 140 characters. + continuation: + The character string, if any, to be appended to all but the + last message. Note that Twitter strips trailing '...' strings + from messages. Consider using the unicode \u2026 character + (horizontal ellipsis) instead. [Defaults to None] + **kwargs: + See api.PostUpdate for a list of accepted parameters. + Returns: + A of list twitter.Status instance representing the messages posted. + ''' + results = list() + if continuation is None: + continuation = '' + line_length = CHARACTER_LIMIT - len(continuation) + lines = textwrap.wrap(status, line_length) + for line in lines[0:-1]: + results.append(self.PostUpdate(line + continuation, **kwargs)) + results.append(self.PostUpdate(lines[-1], **kwargs)) + return results + + def PostRetweet(self, tweet_id): + '''Retweet a tweet with the Retweet API + + The twitter.Api instance must be authenticated. + + Args: + id: The numerical ID of the tweet you are retweeting + + Returns: + A twitter.Status instance representing the retweet posted + ''' + if not self._username: + raise TwitterError("The twitter.Api instance must be authenticated.") + try: + if long(tweet_id) <= 0: + raise TwitterError("'id' must be a positive number") + except ValueError: + raise TwitterError("'id' must be an integer") + + data = {'id': tweet_id} + url = '%s/statuses/retweet/%s.json' % (self.base_url,tweet_id) +# url = 'http://api.twitter.com/1/statuses/retweet/%s.json' % tweet_id + json = self._FetchUrl(url, post_data=data) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return Status.NewFromJsonDict(data) + + def GetReplies(self, since=None, since_id=None, page=None): + '''Get a sequence of status messages representing the 20 most recent + replies (status updates prefixed with @username) to the authenticating + user. + + Args: + page: + since: + Narrows the returned results to just those statuses created + after the specified HTTP-formatted date. [optional] + since_id: + Returns only public statuses with an ID greater than (that is, + more recent than) the specified ID. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each reply to the user. + ''' + url = '%s/statuses/replies.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + parameters = {} + if since: + parameters['since'] = since + if since_id: + parameters['since_id'] = since_id + if page: + parameters['page'] = page + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] + + def GetFriends(self, user=None, cursor=-1): + '''Fetch the sequence of twitter.User instances, one for each friend. + + Args: + user: the username or id of the user whose friends you are fetching. If + not specified, defaults to the authenticated user. [optional] + + The twitter.Api instance must be authenticated. + + Returns: + A sequence of twitter.User instances, one for each friend + ''' + if not user and not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + if user: + url = '%s/statuses/friends/%s.json' % (self.base_url, user) + else: + url = '%s/statuses/friends.json' % self.base_url + parameters = {} + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [User.NewFromJsonDict(x) for x in data] + + def GetSubscriptions(self, user, cursor=-1): + '''Fetch the sequence of Lists that the given user is subscribed to + + The twitter.Api instance must be authenticated. + + Args: + user: + The twitter name or id of the user + cursor: + "page" value that Twitter will use to start building the + list sequence from. -1 to start at the beginning. + Twitter will return in the result the values for next_cursor + and previous_cursor. [Optional] + + Returns: + A sequence of twitter.List instances, one for each list + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + + url = '%s/%s/lists/subscriptions.json' % (self.base_url, user) + parameters = {} + parameters['cursor'] = cursor + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + print data + return [List.NewFromJsonDict(x) for x in data['lists']] + + def CreateList(self, user, name, mode=None, description=None): + '''Creates a new list with the give name + + The twitter.Api instance must be authenticated. + + Args: + user: + Twitter name to create the list for + name: + New name for the list + mode: + 'public' or 'private'. + Defaults to 'public'. [Optional] + description: + Description of the list. [Optional] + + Returns: + A twitter.List instance representing the new list + ''' + url = '%s/%s/lists.json' % (self.base_url, user) + parameters = {'name': name} + if mode is not None: + parameters['mode'] = mode + if description is not None: + parameters['description'] = description + json = self._FetchUrl(url, post_data=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def DestroyList(self, user, id): + '''Destroys the list from the given user + + The twitter.Api instance must be authenticated. + + Args: + user: + The user to remove the list from. + id: + The slug or id of the list to remove. + Returns: + A twitter.List instance representing the removed list. + ''' + url = '%s/%s/lists/%s.json' % (self.base_url, user, id) + json = self._FetchUrl(url, post_data={'_method': 'DELETE'}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def CreateSubscription(self, owner, list): + '''Creates a subscription to a list by the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + owner: + User name or id of the owner of the list being subscribed to. + list: + The slug or list id to subscribe the user to + + Returns: + A twitter.List instance representing the list subscribed to + ''' + url = '%s/%s/%s/subscribers.json' % (self.base_url, owner, list) + json = self._FetchUrl(url, post_data={'list_id': list}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def DestroySubscription(self, owner, list): + '''Destroys the subscription to a list for the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + owner: + The user id or screen name of the user that owns the + list that is to be unsubscribed from + list: + The slug or list id of the list to unsubscribe from + + Returns: + A twitter.List instance representing the removed list. + ''' + url = '%s/%s/%s/subscribers.json' % (self.base_url, owner, list) + json = self._FetchUrl(url, post_data={'_method': 'DELETE', 'list_id': list}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def GetLists(self, user, cursor=-1): + '''Fetch the sequence of lists for a user. + + The twitter.Api instance must be authenticated. + + Args: + user: + The twitter name or id of the user whose friends you are fetching. + If the passed in user is the same as the authenticated user + then you will also receive private list data. + cursor: + "page" value that Twitter will use to start building the + list sequence from. -1 to start at the beginning. + Twitter will return in the result the values for next_cursor + and previous_cursor. [Optional] + + Returns: + A sequence of twitter.List instances, one for each list + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + + url = '%s/%s/lists.json' % (self.base_url, user) + parameters = {} + parameters['cursor'] = cursor + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [List.NewFromJsonDict(x) for x in data['lists']] + + def GetFriendIDs(self, user=None, cursor=-1): + '''Returns a list of twitter user id's for every person + the specified user is following. + + Args: + user: + The id or screen_name of the user to retrieve the id list for + [optional] + + Returns: + A list of integers, one for each user id. + ''' + if not user and not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + if user: + url = '%s/friends/ids/%s.json' % (self.base_url, user) + else: + url = '%s/friends/ids.json' % self.base_url + parameters = {} + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return data + + def GetFollowerIDs(self, userid=None, cursor=-1): + '''Fetch the sequence of twitter.User instances, one for each follower + + The twitter.Api instance must be authenticated. + + Returns: + A sequence of twitter.User instances, one for each follower + ''' + url = 'http://twitter.com/followers/ids.json' + parameters = {} + parameters['cursor'] = cursor + if userid: + parameters['user_id'] = userid + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return data + + def GetFollowers(self, page=None): + '''Fetch the sequence of twitter.User instances, one for each follower + + The twitter.Api instance must be authenticated. + + Returns: + A sequence of twitter.User instances, one for each follower + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + url = '%s/statuses/followers.json' % self.base_url + parameters = {} + if page: + parameters['page'] = page + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [User.NewFromJsonDict(x) for x in data] + + def GetFeatured(self): + '''Fetch the sequence of twitter.User instances featured on twitter.com + + The twitter.Api instance must be authenticated. + + Returns: + A sequence of twitter.User instances + ''' + url = '%s/statuses/featured.json' % self.base_url + json = self._FetchUrl(url) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [User.NewFromJsonDict(x) for x in data] + + def GetUser(self, user): + '''Returns a single user. + + The twitter.Api instance must be authenticated. + + Args: + user: The username or id of the user to retrieve. + + Returns: + A twitter.User instance representing that user + ''' + url = '%s/users/show/%s.json' % (self.base_url, user) + json = self._FetchUrl(url) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return User.NewFromJsonDict(data) + + def GetDirectMessages(self, since=None, since_id=None, page=None): + '''Returns a list of the direct messages sent to the authenticating user. + + The twitter.Api instance must be authenticated. + + Args: + since: + Narrows the returned results to just those statuses created + after the specified HTTP-formatted date. [optional] + since_id: + Returns only public statuses with an ID greater than (that is, + more recent than) the specified ID. [Optional] + + Returns: + A sequence of twitter.DirectMessage instances + ''' + url = '%s/direct_messages.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + parameters = {} + if since: + parameters['since'] = since + if since_id: + parameters['since_id'] = since_id + if page: + parameters['page'] = page + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [DirectMessage.NewFromJsonDict(x) for x in data] + + def PostDirectMessage(self, user, text): + '''Post a twitter direct message from the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + user: The ID or screen name of the recipient user. + text: The message text to be posted. Must be less than 140 characters. + + Returns: + A twitter.DirectMessage instance representing the message posted + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + url = '%s/direct_messages/new.json' % self.base_url + data = {'text': text, 'user': user} + json = self._FetchUrl(url, post_data=data) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return DirectMessage.NewFromJsonDict(data) + + def DestroyDirectMessage(self, id): + '''Destroys the direct message specified in the required ID parameter. + + The twitter.Api instance must be authenticated, and the + authenticating user must be the recipient of the specified direct + message. + + Args: + id: The id of the direct message to be destroyed + + Returns: + A twitter.DirectMessage instance representing the message destroyed + ''' + url = '%s/direct_messages/destroy/%s.json' % (self.base_url, id) + json = self._FetchUrl(url, post_data={}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return DirectMessage.NewFromJsonDict(data) + + def CreateFriendship(self, user): + '''Befriends the user specified in the user parameter as the authenticating user. + + The twitter.Api instance must be authenticated. + + Args: + The ID or screen name of the user to befriend. + Returns: + A twitter.User instance representing the befriended user. + ''' + url = '%s/friendships/create/%s.json' % (self.base_url, user) + json = self._FetchUrl(url, post_data={'user':user}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return User.NewFromJsonDict(data) + + def DestroyFriendship(self, user): + '''Discontinues friendship with the user specified in the user parameter. + + The twitter.Api instance must be authenticated. + + Args: + The ID or screen name of the user with whom to discontinue friendship. + Returns: + A twitter.User instance representing the discontinued friend. + ''' + url = '%s/friendships/destroy/%s.json' % (self.base_url, user) + json = self._FetchUrl(url, post_data={'user':user}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return User.NewFromJsonDict(data) + + def CreateFavorite(self, status_id): + '''Favorites the status specified in the status parameter as the authenticating user. + Returns the favorite status when successful. + + The twitter.Api instance must be authenticated. + + Args: + The twitter.Status instance to mark as a favorite. + Returns: + A twitter.Status instance representing the newly-marked favorite. + ''' + url = '%s/favorites/create/%s.json' % (self.base_url, status_id) + json = self._FetchUrl(url, post_data={'id':status_id}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return Status.NewFromJsonDict(data) + + def DestroyFavorite(self, status_id): + '''Un-favorites the status specified in the ID parameter as the authenticating user. + Returns the un-favorited status in the requested format when successful. + + The twitter.Api instance must be authenticated. + + Args: + The twitter.Status to unmark as a favorite. + Returns: + A twitter.Status instance representing the newly-unmarked favorite. + ''' + url = '%s/favorites/destroy/%s.json' % (self.base_url, status_id) + json = self._FetchUrl(url, post_data={'id':status_id}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return Status.NewFromJsonDict(data) + + def GetFavorites(self, + user=None, + page=None): + '''Return a list of Status objects representing favorited tweets. + By default, returns the (up to) 20 most recent tweets for the + authenticated user. + + Args: + user: + The username or id of the user whose favorites you are fetching. + If not specified, defaults to the authenticated user. [optional] + + page: + Retrieves the 20 next most recent favorite statuses. [optional] + ''' + parameters = {} + + if page: + parameters['page'] = page + + if user: + url = '%s/favorites/%s.json' % (self.base_url, user) + elif not user and not self._oauth_consumer: + raise TwitterError("User must be specified if API is not authenticated.") + else: + url = '%s/favorites.json' % self.base_url + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def GetMentions(self, + since_id=None, + max_id=None, + page=None, + include_rts=None): + '''Returns the 20 most recent mentions (status containing @username) + for the authenticating user. + + Args: + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + + max_id: + Returns only statuses with an ID less than + (that is, older than) the specified ID. [optional] + + page: + Retrieves the 20 next most recent replies. [optional] + + Returns: + A sequence of twitter.Status instances, one for each mention of the user. + see: http://apiwiki.twitter.com/REST-API-Documentation#statuses/mentions + ''' + + url = '%s/statuses/mentions.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + parameters = {} + + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if page: + parameters['page'] = page + if include_rts: + parameters['include_rts'] = 1 + + json = self._FetchUrl(url, parameters=parameters) + #print json + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def GetRetweetedByMe(self, + since_id=None, + max_id=None, + page=None): + '''Returns the 20 most recent retweet made by user (status containing @username) + for the authenticating user. + + Args: + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + + max_id: + Returns only statuses with an ID less than + (that is, older than) the specified ID. [optional] + + page: + Retrieves the 20 next most recent replies. [optional] + + Returns: + A sequence of twitter.Status instances, one for each retweet of the user. + see: http://apiwiki.twitter.com/REST-API-Documentation#statuses/mentions + ''' + + url = '%s/statuses/retweeted_by_me.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + parameters = {} + + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if page: + parameters['page'] = page + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def GetRetweetsOfMe(self, + since_id=None, + max_id=None, + page=None): + '''Returns the 20 most recent retweet of tweet made by the user (status containing @username) + for the authenticating user. + + Args: + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + + max_id: + Returns only statuses with an ID less than + (that is, older than) the specified ID. [optional] + + page: + Retrieves the 20 next most recent replies. [optional] + + Returns: + A sequence of twitter.Status instances, one for each retweet of the user. + see: http://apiwiki.twitter.com/REST-API-Documentation#statuses/mentions + ''' + + url = '%s/statuses/retweets_of_me.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + parameters = {} + + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if page: + parameters['page'] = page + parameters['trim_user'] = 'false' + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def GetRetweetedToMe(self, + since_id=None, + max_id=None, + page=None): + '''Returns the 20 most recent retweet to the user (status containing @username) + for the authenticating user. + + Args: + since_id: + Returns only public statuses with an ID greater than + (that is, more recent than) the specified ID. [optional] + + max_id: + Returns only statuses with an ID less than + (that is, older than) the specified ID. [optional] + + page: + Retrieves the 20 next most recent replies. [optional] + + Returns: + A sequence of twitter.Status instances, one for each retweet of the user. + see: http://apiwiki.twitter.com/REST-API-Documentation#statuses/mentions + ''' + + url = '%s/statuses/retweeted_to_me.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + parameters = {} + + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if page: + parameters['page'] = page + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def GetUserByEmail(self, email): + '''Returns a single user by email address. + + Args: + email: The email of the user to retrieve. + Returns: + A twitter.User instance representing that user + ''' + url = '%s/users/show.json?email=%s' % (self.base_url, email) + json = self._FetchUrl(url) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return User.NewFromJsonDict(data) + + def VerifyCredentials(self): + '''Returns a twitter.User instance if the authenticating user is valid. + + Returns: + A twitter.User instance representing that user if the + credentials are valid, None otherwise. + ''' + if not self._oauth_consumer: + raise TwitterError("Api instance must first be given user credentials.") + url = '%s/account/verify_credentials.json' % self.base_url + try: + json = self._FetchUrl(url, no_cache=True) + except urllib2.HTTPError, http_error: + if http_error.code == httplib.UNAUTHORIZED: + return None + else: + raise http_error + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return User.NewFromJsonDict(data) + + def SetCache(self, cache): + '''Override the default cache. Set to None to prevent caching. + + Args: + cache: an instance that supports the same API as the twitter._FileCache + ''' + if cache == DEFAULT_CACHE: + self._cache = _FileCache() + else: + self._cache = cache + + def SetUrllib(self, urllib): + '''Override the default urllib implementation. + + Args: + urllib: an instance that supports the same API as the urllib2 module + ''' + self._urllib = urllib + + def SetCacheTimeout(self, cache_timeout): + '''Override the default cache timeout. + + Args: + cache_timeout: time, in seconds, that responses should be reused. + ''' + self._cache_timeout = cache_timeout + + def SetUserAgent(self, user_agent): + '''Override the default user agent + + Args: + user_agent: a string that should be send to the server as the User-agent + ''' + self._request_headers['User-Agent'] = user_agent + + def SetXTwitterHeaders(self, client, url, version): + '''Set the X-Twitter HTTP headers that will be sent to the server. + + Args: + client: + The client name as a string. Will be sent to the server as + the 'X-Twitter-Client' header. + url: + The URL of the meta.xml as a string. Will be sent to the server + as the 'X-Twitter-Client-URL' header. + version: + The client version as a string. Will be sent to the server + as the 'X-Twitter-Client-Version' header. + ''' + self._request_headers['X-Twitter-Client'] = client + self._request_headers['X-Twitter-Client-URL'] = url + self._request_headers['X-Twitter-Client-Version'] = version + + def SetSource(self, source): + '''Suggest the "from source" value to be displayed on the Twitter web site. + + The value of the 'source' parameter must be first recognized by + the Twitter server. New source values are authorized on a case by + case basis by the Twitter development team. + + Args: + source: + The source name as a string. Will be sent to the server as + the 'source' parameter. + ''' + self._default_params['source'] = source + + def GetRateLimitStatus(self): + '''Fetch the rate limit status for the currently authorized user. + + Returns: + A dictionary containing the time the limit will reset (reset_time), + the number of remaining hits allowed before the reset (remaining_hits), + the number of hits allowed in a 60-minute period (hourly_limit), and the + time of the reset in seconds since The Epoch (reset_time_in_seconds). + ''' + url = '%s/account/rate_limit_status.json' % self.base_url + json = self._FetchUrl(url, no_cache=True) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return data + + def MaximumHitFrequency(self): + '''Determines the minimum number of seconds that a program must wait before + hitting the server again without exceeding the rate_limit imposed for the + currently authenticated user. + + Returns: + The minimum second interval that a program must use so as to not exceed + the rate_limit imposed for the user. + ''' + rate_status = self.GetRateLimitStatus() + reset_time = rate_status.get('reset_time', None) + limit = rate_status.get('remaining_hits', None) + + if reset_time and limit: + # put the reset time into a datetime object + reset = datetime.datetime(*rfc822.parsedate(reset_time)[:7]) + + # find the difference in time between now and the reset time + 1 hour + delta = reset + datetime.timedelta(hours=1) - datetime.datetime.utcnow() + + # determine the minimum number of seconds allowed as a regular interval + max_frequency = int(delta.seconds / limit) + + # return the number of seconds + return max_frequency + + return 0 + + def _BuildUrl(self, url, path_elements=None, extra_params=None): + # Break url into consituent parts + (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) + + # Add any additional path elements to the path + if path_elements: + # Filter out the path elements that have a value of None + p = [i for i in path_elements if i] + if not path.endswith('/'): + path += '/' + path += '/'.join(p) + + # Add any additional query parameters to the query string + if extra_params and len(extra_params) > 0: + extra_query = self._EncodeParameters(extra_params) + # Add it to the existing query + if query: + query += '&' + extra_query + else: + query = extra_query + + # Return the rebuilt URL + return urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) + + def _InitializeRequestHeaders(self, request_headers): + if request_headers: + self._request_headers = request_headers + else: + self._request_headers = {} + + def _InitializeUserAgent(self): + user_agent = 'Khweeteur-python-twitter/%s)' % \ + ( __version__) + self.SetUserAgent(user_agent) + + def _InitializeDefaultParameters(self): + self._default_params = {} + + def _DecompressGzippedResponse(self, response): + raw_data = response.read() + if response.headers.get('content-encoding', None) == 'gzip': + url_data = gzip.GzipFile(fileobj=StringIO.StringIO(raw_data)).read() + else: + url_data = raw_data + return url_data + + def _Encode(self, s): + if self._input_encoding: + return unicode(s, self._input_encoding).encode('utf-8') + else: + return unicode(s).encode('utf-8') + + def _EncodeParameters(self, parameters): + '''Return a string in key=value&key=value form + + Values of None are not included in the output string. + + Args: + parameters: + A dict of (key, value) tuples, where value is encoded as + specified by self._encoding + Returns: + A URL-encoded string in "key=value&key=value" form + ''' + if parameters is None: + return None + else: + return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in parameters.items() if v is not None])) + + def _EncodePostData(self, post_data): + '''Return a string in key=value&key=value form + + Values are assumed to be encoded in the format specified by self._encoding, + and are subsequently URL encoded. + + Args: + post_data: + A dict of (key, value) tuples, where value is encoded as + specified by self._encoding + Returns: + A URL-encoded string in "key=value&key=value" form + ''' + if post_data is None: + return None + else: + return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in post_data.items()])) + + def _CheckForTwitterError(self, data): + """Raises a TwitterError if twitter returns an error message. + + Args: + data: A python dict created from the Twitter json response + Raises: + TwitterError wrapping the twitter error message if one exists. + """ + # Twitter errors are relatively unlikely, so it is faster + # to check first, rather than try and catch the exception + if 'error' in data: + raise TwitterError(data['error']) + + def _FetchUrl(self, + url, + post_data=None, + parameters=None, + no_cache=None, + use_gzip_compression=None): + '''Fetch a URL, optionally caching for a specified time. + + Args: + url: + The URL to retrieve + post_data: + A dict of (str, unicode) key/value pairs. + If set, POST will be used. + parameters: + A dict whose key/value pairs should encoded and added + to the query string. [optional] + no_cache: + If true, overrides the cache on the current request + use_gzip_compression: + If True, tells the server to gzip-compress the response. + It does not apply to POST requests. + Defaults to None, which will get the value to use from + the instance variable self._use_gzip [optional] + + Returns: + A string containing the body of the response. + ''' + # Build the extra parameters dict + extra_params = {} + if self._default_params: + extra_params.update(self._default_params) + if parameters: + extra_params.update(parameters) + + if post_data: + http_method = "POST" + else: + http_method = "GET" + + http_handler = self._urllib.HTTPHandler(debuglevel=0) + https_handler = self._urllib.HTTPSHandler(debuglevel=0) + + opener = self._urllib.OpenerDirector() + opener.add_handler(http_handler) + opener.add_handler(https_handler) + + if use_gzip_compression is None: + use_gzip = self._use_gzip + else: + use_gzip = use_gzip_compression + + # Set up compression + if use_gzip and not post_data: + opener.addheaders.append(('Accept-Encoding', 'gzip')) + + if self._oauth_consumer is not None: + if post_data and http_method == "POST": + parameters = post_data.copy() + + req = oauth.Request.from_consumer_and_token(self._oauth_consumer, + token=self._oauth_token, + http_method=http_method, + http_url=url, parameters=parameters) + + req.sign_request(self._signature_method_hmac_sha1, self._oauth_consumer, self._oauth_token) + + headers = req.to_header() + + if http_method == "POST": + encoded_post_data = req.to_postdata() + else: + encoded_post_data = None + url = req.to_url() + else: + url = self._BuildUrl(url, extra_params=extra_params) + encoded_post_data = self._EncodePostData(post_data) + + # Open and return the URL immediately if we're not going to cache + if encoded_post_data or no_cache or not self._cache or not self._cache_timeout: + response = opener.open(url, encoded_post_data) + url_data = self._DecompressGzippedResponse(response) + opener.close() + else: + # Unique keys are a combination of the url and the oAuth Consumer Key + if self._username: + key = self._username + ':' + url + else: + key = url + + # See if it has been cached before + last_cached = self._cache.GetCachedTime(key) + + # If the cached version is outdated then fetch another and store it + if not last_cached or time.time() >= last_cached + self._cache_timeout: + try: + response = opener.open(url, encoded_post_data) + url_data = self._DecompressGzippedResponse(response) + except urllib2.HTTPError, e: + print 'Errors ',e + opener.close() + self._cache.Set(key, url_data) + else: + url_data = self._cache.Get(key) + + # Always return the latest version + return url_data + +class _FileCacheError(Exception): + '''Base exception class for FileCache related errors''' + +class _FileCache(object): + + DEPTH = 3 + + def __init__(self,root_directory=None): + self._InitializeRootDirectory(root_directory) + + def Get(self,key): + path = self._GetPath(key) + if os.path.exists(path): + return open(path).read() + else: + return None + + def Set(self,key,data): + path = self._GetPath(key) + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + if not os.path.isdir(directory): + raise _FileCacheError('%s exists but is not a directory' % directory) + temp_fd, temp_path = tempfile.mkstemp() + temp_fp = os.fdopen(temp_fd, 'w') + temp_fp.write(data) + temp_fp.close() + if not path.startswith(self._root_directory): + raise _FileCacheError('%s does not appear to live under %s' % + (path, self._root_directory)) + if os.path.exists(path): + os.remove(path) + os.rename(temp_path, path) + + def Remove(self,key): + path = self._GetPath(key) + if not path.startswith(self._root_directory): + raise _FileCacheError('%s does not appear to live under %s' % + (path, self._root_directory )) + if os.path.exists(path): + os.remove(path) + + def GetCachedTime(self,key): + path = self._GetPath(key) + if os.path.exists(path): + return os.path.getmtime(path) + else: + return None + + def _GetUsername(self): + '''Attempt to find the username in a cross-platform fashion.''' + try: + return os.getenv('USER') or \ + os.getenv('LOGNAME') or \ + os.getenv('USERNAME') or \ + os.getlogin() or \ + 'nobody' + except (IOError, OSError), e: + return 'nobody' + + def _GetTmpCachePath(self): + username = self._GetUsername() + cache_directory = 'python.cache_' + username + return os.path.join(tempfile.gettempdir(), cache_directory) + + def _InitializeRootDirectory(self, root_directory): + if not root_directory: + root_directory = self._GetTmpCachePath() + root_directory = os.path.abspath(root_directory) + if not os.path.exists(root_directory): + os.mkdir(root_directory) + if not os.path.isdir(root_directory): + raise _FileCacheError('%s exists but is not a directory' % + root_directory) + self._root_directory = root_directory + + def _GetPath(self,key): + try: + hashed_key = md5(key).hexdigest() + except TypeError: + hashed_key = md5.new(key).hexdigest() + + return os.path.join(self._root_directory, + self._GetPrefix(hashed_key), + hashed_key) + + def _GetPrefix(self,hashed_key): + return os.path.sep.join(hashed_key[0:_FileCache.DEPTH]) + +if __name__ == '__main__': + print 'test' + s=(u'''ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ''').encode('utf-8') + s2=(u'''ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚ ั‚ะตัั‚''').encode('utf-8') + api = Api(username='twitterfdghjuser', password='twittgfjker pass', + access_token_key='the_key_g', + access_token_secret='the_key_secret') + api.PostSerializedUpdates(s) diff --git a/khweeteur_32.png b/khweeteur_32.png new file mode 100644 index 0000000..5b5c9b9 Binary files /dev/null and b/khweeteur_32.png differ diff --git a/khweeteur_64.png b/khweeteur_64.png new file mode 100644 index 0000000..c6f7691 Binary files /dev/null and b/khweeteur_64.png differ diff --git a/khweeteurd b/khweeteurd new file mode 100644 index 0000000..3077550 --- /dev/null +++ b/khweeteurd @@ -0,0 +1,15 @@ +start on stopped rcS + +console output + +respawn + +pre-start script + if [ ! -d /var/run/khweeteurd ] ; then + mkdir /var/run/khweeteurd + chmod 777 /var/run/khweeteurd + fi +end script + +exec /usr/bin/khweeteur -D + diff --git a/scripts/khweeteur b/scripts/khweeteur new file mode 100644 index 0000000..baf2406 --- /dev/null +++ b/scripts/khweeteur @@ -0,0 +1,7 @@ +#!/bin/sh +if [ $# = 1 ] +then + exec python /usr/lib/python2.5/site-packages/khweeteur/daemon.py +else + exec python /usr/lib/python2.5/site-packages/khweeteur/__init__.py +fi diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5ce7708 --- /dev/null +++ b/setup.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +#Khweeteur Setup File + +import imp +import sys +reload(sys).setdefaultencoding("UTF-8") + +try: + from sdist_maemo import sdist_maemo as _sdist_maemo +except: + _sdist_maemo = None + print 'sdist_maemo command not available' + +from distutils.core import setup +import khweeteur + +#Remove pyc and pyo file +import glob,os +for fpath in glob.glob('*/*.py[c|o]'): + os.remove(fpath) + +setup(name='khweeteur', + version=khweeteur.qwidget_gui.__version__, + license='GNU GPLv3', + description="A twitter client for Maemo and MeeGo.", + long_description="Khweeteur is a small twitter client for Maemo and MeeGo. It showing DMs, mentions and the follower timeline in one window, with a subsequent window for each search. Maemo's notification system is supported, as is auto-update and themeing.", + author='Benoรฎt HERVIER', + author_email='khertan@khertan.net', + maintainer=u'Benoรฎt HERVIER', + maintainer_email='khertan@khertan.net', + requires=['imaging','simplejson','conic','PySide','oauth2','PySide.QtMobility'], + url='http://www.khertan.net/khweeteur', + packages= ['khweeteur',], + package_data = {'khweeteur': ['icons/*.png']}, + data_files=[('/usr/share/dbus-1/services', ['khweeteur.service']), + ('/usr/share/applications/hildon/', ['khweeteur.desktop']), + ('/usr/share/pixmaps', ['khweeteur.png','khweeteur_64.png','khweeteur_32.png']), + ('/usr/share/icons/hicolor/128x128/apps', ['khweeteur.png']), + ('/usr/share/icons/hicolor/64x64/apps', ['icons/hicolor/64x64/apps/khweeteur.png']), + ('/usr/share/icons/hicolor/32x32/apps', ['icons/hicolor/32x32/apps/khweeteur.png']), + ('/etc/event.d', ['khweeteurd']),], + scripts=['scripts/khweeteur'], + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: X11 Applications :: Qt", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Programming Language :: Python", + "Operating System :: POSIX :: Linux", + "Operating System :: POSIX :: Other", + "Operating System :: Other OS", + "Intended Audience :: End Users/Desktop",], + cmdclass={'sdist_maemo': _sdist_maemo}, + options = { 'sdist_maemo':{ + 'debian_package':'khweeteur-experimental', + 'buildversion':'1', + 'depends':'python2.5, pyside-mobility, python-pyside, python-oauth2, python-simplejson, python-conic, python-imaging, python-dbus, python-oauth', + 'conflicts':'khweeteur-experimental', + 'XSBC_Bugtracker':'http://khertan.net/khweeteur:bugs', + 'XB_Maemo_Display_Name':'Khweeteur', + 'XB_Maemo_Icon_26':'khweeteur.png', + 'section':'user/network', + 'changelog':'* Complete rewrite', + 'architecture':'any', + 'postinst':"""#!/bin/sh +chmod +x /usr/bin/khweeteur +python -m compileall /usr/lib/python2.5/site-packages/khweeteur +rm -rf /home/user/.khweeteur/ +NOTIFICATIONS_CONF="/etc/hildon-desktop/notification-groups.conf" +NOTIFICATIONS_KEY="khweeteur-new-tweets" +if ! grep -q "$NOTIFICATIONS_KEY" "$NOTIFICATIONS_CONF"; then +echo -n "Updating $NOTIFICATIONS_CONF..." +cat >>$NOTIFICATIONS_CONF << EOF +### BEGIN Added by Khweeteur postinst ### +[khweeteur-new-tweets] +Destination=Khweeteur +Icon=khweeteur +Title-Text-Empty=Khweeteur +Secondary-Text=New tweets available +Text-Domain=khweeteur +LED-Pattern=PatternCommonNotification +### END Added by khweeteur postinst ### +EOF + echo "done." +fi +""", + 'prere':"""#!/bin/sh +rm -rf /usr/lib/python2.5/site-packages/khweeteur/*.pyc""", + 'copyright':'gpl'}, + 'bdist_rpm':{ + 'requires':'python, python-setuptools, pyside-mobility, python-pyside,python-qt4-core, python-qt4-maemo5, python-oauth2, python-simplejson, python-conic, python-imaging', + 'conflicts':'khweeteur-experimental', + 'icon':'khweeteur.png', + 'group':'Network',}} + ) +