Exemple #1
0
    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
Exemple #2
0
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))
Exemple #3
0
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()
Exemple #4
0
    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')
Exemple #5
0
    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))
Exemple #6
0
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()
Exemple #7
0
    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)
Exemple #8
0
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)
Exemple #9
0
    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))
Exemple #10
0
 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)
Exemple #11
0
    '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)
Exemple #12
0
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)
Exemple #13
0
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