Exemple #1
0
    def update(self):
        """Update the database.

        If the database is fresh, it will be initialized. If the database is already up-to-date,
        nothing will be done. It is thus safe to call :meth:`update` without knowing if an update is
        necessary or not.
        """
        # Compatibility for databases without micro_version (obsolete since 0.13.0)
        if not self.r.exists('micro_version') and self.r.exists('Settings'):
            self.r.set('micro_version', 0)

        version = self.r.get('micro_version')

        # If fresh, initialize database
        if not version:
            settings = self.create_settings()
            self.r.oset(settings.id, settings)
            self.r.set('micro_version', 1)
            self.do_update()
            return

        version = int(version)
        r = JSONRedis(self.r.r)
        r.caching = False

        if version < 2:
            settings = r.oget('Settings')
            settings['feedback_url'] = None
            r.oset(settings['id'], settings)
            self.r.set('micro_version', 2)

        self.do_update()
Exemple #2
0
    def do_update(self):
        db_version = self.r.get('version')

        # If fresh, initialize database
        if not db_version:
            self.r.set('version', 5)
            return

        db_version = int(db_version)
        r = JSONRedis(self.r.r)
        r.caching = False

        # Deprecated since 0.12.0
        if db_version < 5:
            users = r.omget(r.lrange('users', 0, -1))
            for user in users:
                user['email'] = None
            r.omset({u['id']: u for u in users})
            r.set('version', 5)
Exemple #3
0
    def __init__(self, redis_url='', email='bot@localhost', smtp_url='',
                 render_email_auth_message=None):
        check_email(email)
        try:
            # pylint: disable=pointless-statement; port errors are only triggered on access
            urlparse(smtp_url).port
        except builtins.ValueError:
            raise ValueError('smtp_url_invalid')

        self.redis_url = redis_url
        try:
            self.r = StrictRedis.from_url(self.redis_url)
        except builtins.ValueError:
            raise ValueError('redis_url_invalid')
        self.r = JSONRedis(self.r, self._encode, self._decode)

        self.types = {'User': User, 'Settings': Settings, 'AuthRequest': AuthRequest}
        self.user = None
        self.users = JSONRedisMapping(self.r, 'users')
        self.email = email
        self.smtp_url = smtp_url
        self.render_email_auth_message = render_email_auth_message
Exemple #4
0
class Application:
    """See :ref:`Application`.

    .. attribute:: user

       Current :class:`User`. ``None`` means anonymous access.

    .. attribute:: users

       Map of all :class:`User` s.

    .. attribute:: redis_url

       See ``--redis-url`` command line option.

    .. attribute:: email

       Sender email address to use for outgoing email. Defaults to ``bot@localhost``.

    .. attribute:: smtp_url

       See ``--smtp-url`` command line option.

    .. attribute:: render_email_auth_message

       Hook function of the form *render_email_auth_message(email, auth_request, auth)*, responsible
       for rendering an email message for the authentication request *auth_request*. *email* is the
       email address to authenticate and *auth* is the secret authentication code.

    .. attribute:: r

       :class:`Redis` database. More precisely a :class:`JSONRedis` instance.
    """

    def __init__(self, redis_url='', email='bot@localhost', smtp_url='',
                 render_email_auth_message=None):
        check_email(email)
        try:
            # pylint: disable=pointless-statement; port errors are only triggered on access
            urlparse(smtp_url).port
        except builtins.ValueError:
            raise ValueError('smtp_url_invalid')

        self.redis_url = redis_url
        try:
            self.r = StrictRedis.from_url(self.redis_url)
        except builtins.ValueError:
            raise ValueError('redis_url_invalid')
        self.r = JSONRedis(self.r, self._encode, self._decode)

        self.types = {'User': User, 'Settings': Settings, 'AuthRequest': AuthRequest}
        self.user = None
        self.users = JSONRedisMapping(self.r, 'users')
        self.email = email
        self.smtp_url = smtp_url
        self.render_email_auth_message = render_email_auth_message

    @property
    def settings(self):
        """App :class:`Settings`."""
        return self.r.oget('Settings')

    def update(self):
        """Update the database.

        If the database is fresh, it will be initialized. If the database is already up-to-date,
        nothing will be done. It is thus safe to call :meth:`update` without knowing if an update is
        necessary or not.
        """
        # Compatibility for databases without micro_version (obsolete since 0.13.0)
        if not self.r.exists('micro_version') and self.r.exists('Settings'):
            self.r.set('micro_version', 0)

        version = self.r.get('micro_version')

        # If fresh, initialize database
        if not version:
            settings = self.create_settings()
            self.r.oset(settings.id, settings)
            self.r.set('micro_version', 1)
            self.do_update()
            return

        version = int(version)
        r = JSONRedis(self.r.r)
        r.caching = False

        if version < 2:
            settings = r.oget('Settings')
            settings['feedback_url'] = None
            r.oset(settings['id'], settings)
            self.r.set('micro_version', 2)

        self.do_update()

    def do_update(self):
        """Subclass API: Perform the database update.

        May be overridden by subclass. Called by :meth:`update`, which takes care of updating (or
        initializing) micro specific data. The default implementation does nothing.
        """
        pass

    def create_settings(self):
        """Subclass API: Create and return the app :class:`Settings`.

        *id* must be set to ``Settings``.

        Must be overridden by subclass. Called by :meth:`update` when initializing the database.
        """
        raise NotImplementedError()

    def authenticate(self, secret):
        """Authenticate an :class:`User` (device) with *secret*.

        The identified user is set as current *user* and returned. If the authentication fails, an
        :exc:`AuthenticationError` is raised.
        """
        id = self.r.hget('auth_secret_map', secret)
        if not id:
            raise AuthenticationError()
        self.user = self.users[id.decode()]
        return self.user

    def login(self, code=None):
        """See :http:post:`/api/login`.

        The logged-in user is set as current *user*.
        """
        if code:
            id = self.r.hget('auth_secret_map', code)
            if not id:
                raise ValueError('code_invalid')
            user = self.users[id.decode()]

        else:
            id = 'User:'******'Guest', email=None,
                        auth_secret=randstr())
            self.r.oset(user.id, user)
            self.r.rpush('users', user.id)
            self.r.hset('auth_secret_map', user.auth_secret, user.id)

            # Promote first user to staff
            if len(self.users) == 1:
                settings = self.settings
                # pylint: disable=protected-access; Settings is a friend
                settings._staff = [user.id]
                self.r.oset(settings.id, settings)

        return self.authenticate(user.auth_secret)

    def get_object(self, id, default=KeyError):
        """Get the :class:`Object` given by *id*.

        *default* is the value to return if no object with *id* is found. If it is an
        :exc:`Exception`, it is raised instead.
        """
        object = self.r.oget(id)
        if object is None:
            object = default
        if isinstance(object, Exception):
            raise object
        return object

    @staticmethod
    def _encode(object):
        try:
            return object.json()
        except AttributeError:
            raise TypeError()

    def _decode(self, json):
        try:
            type = json.pop('__type__')
        except KeyError:
            return json
        type = self.types[type]
        return type(app=self, **json)
Exemple #5
0
 def setUp(self) -> None:
     self.r = JSONRedis(StrictRedis(db=15),
                        encode=Cat.encode,
                        decode=Cat.decode)
     self.r.flushdb()
Exemple #6
0
class JSONRedisTestCase(TestCase):
    def setUp(self) -> None:
        self.r = JSONRedis(Redis(db=15), encode=Cat.encode, decode=Cat.decode)
        self.r.flushdb()
Exemple #7
0
    def do_update(self):
        db_version = self.r.get('version')

        # If fresh, initialize database
        if not db_version:
            self.r.set('version', 5)
            return

        db_version = int(db_version)
        r = JSONRedis(self.r.r)
        r.caching = False

        if db_version < 2:
            users = r.omget(r.lrange('users', 0, -1))
            for user in users:
                user['name'] = 'Guest'
                user['authors'] = [user['id']]
            r.omset({u['id']: u for u in users})
            r.set('version', 2)

        if db_version < 3:
            meetings = r.omget(r.lrange('meetings', 0, -1))
            for meeting in meetings:
                meeting['time'] = None
                meeting['location'] = None

                items = r.omget(r.lrange(meeting['id'] + '.items', 0, -1))
                for item in items:
                    item['duration'] = None
                r.omset({i['id']: i for i in items})
            r.omset({m['id']: m for m in meetings})
            r.set('version', 3)

        if db_version < 4:
            meeting_ids = r.lrange('meetings', 0, -1)
            objects = r.omget(chain(
                ['Settings'],
                r.lrange('users', 0, -1),
                meeting_ids,
                chain.from_iterable(r.lrange(i + b'.items', 0, -1) for i in meeting_ids)
            ))
            for object in objects:
                object['trashed'] = False
            r.omset({o['id']: o for o in objects})
            r.set('version', 4)

        if db_version < 5:
            users = r.omget(r.lrange('users', 0, -1))
            for user in users:
                user['email'] = None
            r.omset({u['id']: u for u in users})
            r.set('version', 5)
Exemple #8
0
    def do_update(self):
        version = self.r.get('version')
        if not version:
            self.r.set('version', 8)
            return

        version = int(version)
        r = JSONRedis(self.r.r)
        r.caching = False

        # Deprecated since 0.14.0
        if version < 7:
            now = time()
            lists = r.omget(r.lrange('lists', 0, -1))
            for lst in lists:
                r.zadd('{}.lists'.format(lst['authors'][0]), {lst['id']: -now})
            r.set('version', 7)

        # Deprecated since 0.23.0
        if version < 8:
            lists = r.omget(r.lrange('lists', 0, -1))
            for lst in lists:
                users_key = '{}.users'.format(lst['id'])
                self.r.zadd(users_key, {lst['authors'][0].encode(): 0})
                events = r.omget(r.lrange('{}.activity.items'.format(lst['id']), 0, -1))
                for event in reversed(events):
                    t = parse_isotime(event['time'], aware=True).timestamp()
                    self.r.zadd(users_key, {event['user'].encode(): -t})
            r.set('version', 8)
Exemple #9
0
    def do_update(self):
        version = self.r.get('version')
        if not version:
            self.r.set('version', 7)
            return

        version = int(version)
        r = JSONRedis(self.r.r)
        r.caching = False

        # Deprecated since 0.3.0
        if version < 2:
            lists = r.omget(r.lrange('lists', 0, -1))
            for lst in lists:
                lst['features'] = []
                items = r.omget(r.lrange('{}.items'.format(lst['id']), 0, -1))
                for item in items:
                    item['checked'] = False
                r.omset({item['id']: item for item in items})
            r.omset({lst['id']: lst for lst in lists})
            r.set('version', 2)

        # Deprecated since 0.5.0
        if version < 3:
            lists = r.omget(r.lrange('lists', 0, -1))
            for lst in lists:
                lst['activity'] = (Activity('{}.activity'.format(lst['id']),
                                            app=self,
                                            subscriber_ids=[]).json())
            r.omset({lst['id']: lst for lst in lists})
            r.set('version', 3)

        # Deprecated since 0.6.0
        if version < 4:
            items = r.omget([
                id for list_id in r.lrange('lists', 0, -1)
                for id in r.lrange('{}.items'.format(list_id.decode()), 0, -1)
            ])
            for item in items:
                item['location'] = None
            r.omset({item['id']: item for item in items})
            r.set('version', 4)

        # Deprecated since 0.7.0
        if version < 5:
            items = r.omget([
                id for list_id in r.lrange('lists', 0, -1)
                for id in r.lrange('{}.items'.format(list_id.decode()), 0, -1)
            ])
            for item in items:
                item['resource'] = None
            r.omset({item['id']: item for item in items})
            r.set('version', 5)

        # Deprecated since 0.11.0
        if version < 6:
            lists = r.omget(r.lrange('lists', 0, -1))
            for lst in lists:
                lst['mode'] = 'collaborate'
            r.omset({lst['id']: lst for lst in lists})
            r.set('version', 6)

        # Deprecated since 0.14.0
        if version < 7:
            now = time()
            lists = r.omget(r.lrange('lists', 0, -1))
            for lst in lists:
                r.zadd('{}.lists'.format(lst['authors'][0]), {lst['id']: -now})
            r.set('version', 7)