def get_image(url): if not url: return url local_path = Avatar.get_path(url) size = 0 with ignored(FileNotFoundError): size = os.stat(local_path).st_size if size == 0: log.debug('Getting: {}'.format(url)) image_data = Downloader(url).get_bytes() # Save original size at canonical URI with open(local_path, 'wb') as fd: fd.write(image_data) # Append '.100px' to filename and scale image there. input_stream = Gio.MemoryInputStream.new_from_data( image_data, None) try: pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale( input_stream, 100, 100, True, None) pixbuf.savev(local_path + '.100px', 'png', [], []) except GLib.GError: log.error('Failed to scale image: {}'.format(url)) return local_path
def parsetime(t): """Parse an ISO 8601 datetime string and return seconds since epoch. This accepts either a naive (i.e. timezone-less) string or a timezone aware string. The timezone must start with a + or - and must be followed by exactly four digits. This string is parsed and converted to UTC. This value is then converted to an integer seconds since epoch. """ with _c_locale(): # In Python 3.2, strptime() is implemented in Python, so in order to # parse the UTC timezone (e.g. +0000), you'd think we could just # append %z on the format. We can't rely on it though because of the # non-ISO 8601 formats that some APIs use (I'm looking at you Twitter # and Facebook). We'll use a regular expression to tear out the # timezone string and do the conversion ourselves. tz_offset = None def capture_tz(match_object): nonlocal tz_offset tz_string = match_object.group('tz') if tz_string is not None: # It's possible that we'll see more than one substring # matching the timezone pattern. It should be highly unlikely # so we won't test for that here, at least not now. # # The tz_offset is positive, so it must be subtracted from the # naive datetime in order to return it to UTC. E.g. # # 13:00 -0400 is 17:00 +0000 # or # 1300 - (-0400 / 100) if tz_offset is not None: # This is not the first time we're seeing a timezone. raise ValueError('Unsupported time string: {0}'.format(t)) tz_offset = timedelta(hours=int(tz_string) / 100) # Return the empty string so as to remove the timezone pattern # from the string we're going to parse. return '' # Parse the time string, calling capture_tz() for each timezone match # group we find. The callback itself will ensure we see no more # than one timezone string. naive_t = re.sub(r'[ ]*(?P<tz>[-+]\d{4})', capture_tz, t) if tz_offset is None: # No timezone string was found. tz_offset = timedelta() for parser in PARSERS: with ignored(ValueError): parsed_dt = parser(naive_t) break else: # Nothing matched. raise ValueError('Unsupported time string: {0}'.format(t)) # We must have gotten a valid datetime. Normalize out the timezone # offset and convert it to Epoch seconds. Use timegm() to give us # UTC-based conversion from a struct_time to seconds-since-epoch. utc_dt = parsed_dt - tz_offset timetup = utc_dt.timetuple() return int(timegm(timetup))
def notify(title, message, icon_uri='', pixbuf=None): """Display the message along with sender's name and avatar.""" if not (title and message): return notification = Notify.Notification.new(title, message, 'friends') with ignored(GObject.GError): pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( Avatar.get_image(icon_uri), 48, 48) if pixbuf is not None: notification.set_icon_from_pixbuf(pixbuf) if _notify_can_append: notification.set_hint_string('x-canonical-append', 'allowed') with ignored(GObject.GError): # Most likely we've spammed more than 50 notificatons, # not much we can do about that. notification.show()
def Refresh(self): """Download new messages from each connected protocol.""" self._unread_count = 0 log.debug('Refresh requested') # account.protocol() starts a new thread and then returns # immediately, so there is no delay or blocking during the # execution of this method. for account in self.accounts.values(): with ignored(NotImplementedError): account.protocol('receive')
def send_thread(self, message_id, message): """Send a reply message to message_id. This method takes care to prepend the @mention to the start of your tweet if you forgot it. Without this, Twitter will just consider it a regular message, and it won't be part of any conversation. """ with ignored(FriendsError): sender = '@{}'.format(self._fetch_cell(message_id, 'sender_nick')) if message.find(sender) < 0: message = sender + ' ' + message url = self._api_base.format(endpoint='statuses/update') tweet = self._get_url( url, dict(in_reply_to_status_id=message_id, status=message)) return self._publish_tweet(tweet, stream='reply_to/{}'.format(message_id))
def setup(model, param): """Continue friends-dispatcher init after the DeeModel has synced.""" # mhr3 says that we should not let a Dee.SharedModel exceed 8mb in # size, because anything larger will have problems being transmitted # over DBus. I have conservatively calculated our average row length # to be 500 bytes, which means that we shouldn't let our model exceed # approximately 16,000 rows. However, that seems like a lot to me, so # I'm going to set it to 2,000 for now and we can tweak this later if # necessary. Do you really need more than 2,000 tweets in memory at # once? What are you doing with all these tweets? prune_model(2000) # This builds two different indexes of our persisted Dee.Model # data for the purposes of faster duplicate checks. initialize_caches() # Exception indicates that lock was already released, which is harmless. with ignored(RuntimeError): # Allow publishing. _publish_lock.release()
def contacts(self): # https://dev.twitter.com/docs/api/1.1/get/friends/ids contacts = self._get_url(self._api_base.format(endpoint='friends/ids')) # Twitter uses a dict with 'ids' key, Identica returns the ids directly. with ignored(TypeError): contacts = contacts['ids'] log.debug('Found {} contacts'.format(len(contacts))) for contact_id in contacts: contact_id = str(contact_id) if not self._previously_stored_contact(contact_id): # https://dev.twitter.com/docs/api/1.1/get/users/show full_contact = self._get_url( url=self._api_base.format(endpoint='users/show') + '?user_id=' + contact_id) user_nickname = full_contact.get('screen_name', '') self._push_to_eds( uid=contact_id, name=full_contact.get('name'), nick=user_nickname, link=self._user_home.format(user_id=user_nickname)) return len(contacts)
def initialize(console=False, debug=False, filename=None): """Initialize the Friends service logger. :param console: Add a console logger. :type console: bool :param debug: Set the log level to DEBUG instead of INFO. :type debug: bool :param filename: Alternate file to log messages to. :type filename: string """ # Start by ensuring that the directory containing the log file exists. if filename is None: filename = LOG_FILENAME with ignored(FileExistsError): os.makedirs(os.path.dirname(filename)) # Install a rotating log file handler. XXX There should be a # configuration file rather than hard-coded values. text_handler = logging.handlers.RotatingFileHandler(filename, maxBytes=20971520, backupCount=5) # Use str.format() style format strings. text_formatter = logging.Formatter(LOG_FORMAT, style='{') text_handler.setFormatter(text_formatter) log = logging.getLogger() log.addHandler(text_handler) if debug: log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) if console: console_handler = logging.StreamHandler() console_formatter = logging.Formatter(CSL_FORMAT, style='{') console_handler.setFormatter(console_formatter) log.addHandler(console_handler)
def Do(self, action, account_id='', arg='', success=STUB, failure=STUB): """Performs an arbitrary operation with an optional argument. This is how the client initiates retweeting, liking, searching, etc. See Dispatcher.Upload for an example of how to use the callbacks. example: import dbus obj = dbus.SessionBus().get_object(DBUS_INTERFACE, '/com/canonical/friends/Dispatcher') service = dbus.Interface(obj, DBUS_INTERFACE) service.Do('like', '3', 'post_id') # Likes that FB post. service.Do('search', '', 'search terms') # Searches all accounts. service.Do('list', '6', 'list_id') # Fetch a single list. """ if account_id: accounts = [self.accounts.get(int(account_id))] if None in accounts: message = 'Could not find account: {}'.format(account_id) failure(message) log.error(message) return else: accounts = list(self.accounts.values()) called = False for account in accounts: log.debug('{}: {} {}'.format(account.id, action, arg)) args = (action, arg) if arg else (action, ) # Not all accounts are expected to implement every action. with ignored(NotImplementedError): account.protocol(*args, success=success, failure=failure) called = True if not called: failure('No accounts supporting {} found.'.format(action))
def delete_contacts(self): """Remove all synced contacts from this social network.""" self._prepare_eds_connections(allow_creation=False) with ignored(GLib.GError, AttributeError): return self._eds_source.remove_sync(None)
'Avatar', ] import os import logging from gi.repository import Gio, GLib, GdkPixbuf from tempfile import gettempdir from hashlib import sha1 from friends.utils.http import Downloader from friends.errors import ignored CACHE_DIR = os.path.join(gettempdir(), 'friends-avatars') with ignored(FileExistsError): os.makedirs(CACHE_DIR) log = logging.getLogger(__name__) class Avatar: @staticmethod def get_path(url): return os.path.join(CACHE_DIR, sha1(url.encode('utf-8')).hexdigest()) @staticmethod def get_image(url): if not url: return url local_path = Avatar.get_path(url)
def main(): global log global yappi if args.list_protocols: from friends.utils.manager import protocol_manager for name in sorted(protocol_manager.protocols): cls = protocol_manager.protocols[name] package, dot, class_name = cls.__name__.rpartition('.') print(class_name) return # Disallow multiple instances of friends-dispatcher bus = dbus.SessionBus() obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') iface = dbus.Interface(obj, 'org.freedesktop.DBus') if DBUS_INTERFACE in iface.ListNames(): sys.exit('friends-dispatcher is already running! Abort!') if args.performance: with ignored(ImportError): import yappi yappi.start() # Initialize the logging subsystem. gsettings = Gio.Settings.new('com.canonical.friends') initialize(console=args.console, debug=args.debug or gsettings.get_boolean('debug')) log = logging.getLogger(__name__) log.info('Friends backend dispatcher starting') # ensure friends-service is available to provide the Dee.SharedModel server = bus.get_object('com.canonical.Friends.Service', '/com/canonical/friends/Service') # Determine which messages to notify for. notify_level = gsettings.get_string('notifications') if notify_level == 'all': Base._do_notify = lambda protocol, stream: True elif notify_level == 'none': Base._do_notify = lambda protocol, stream: False else: Base._do_notify = lambda protocol, stream: stream in ( 'mentions', 'private', ) Dispatcher(gsettings, loop) # Don't initialize caches until the model is synchronized Model.connect('notify::synchronized', setup) with ignored(KeyboardInterrupt): log.info('Starting friends-dispatcher main loop') loop.run() log.info('Stopped friends-dispatcher main loop') # This bit doesn't run until after the mainloop exits. if args.performance and yappi is not None: yappi.print_stats(sys.stdout, yappi.SORTTYPE_TTOT)
loop = GLib.MainLoop() from friends.errors import ignored # Short-circuit everything else if we are going to enter test-mode. from friends.utils.options import Options args = Options().parser.parse_args() if args.test: from friends.service.mock_service import Dispatcher from friends.tests.mocks import populate_fake_data populate_fake_data() Dispatcher() with ignored(KeyboardInterrupt): loop.run() sys.exit(0) # Continue with normal loading... from friends.service.dispatcher import Dispatcher, DBUS_INTERFACE from friends.utils.base import Base, initialize_caches, _publish_lock from friends.utils.model import Model, prune_model from friends.utils.logging import initialize # Optional performance profiling module. yappi = None # Logger must be initialized before it can be used. log = None