thenetcircle/dino

View on GitHub
dino/hooks/status.py

Summary

Maintainability
B
5 hrs
Test Coverage
import logging
import sys

from dino import environ
from dino import utils
from dino.config import SessionKeys
from dino.config import UserKeys
from dino.exceptions import NoSuchUserException


class OnStatusHooks(object):
    logger = logging.getLogger(__name__)

    @staticmethod
    def set_status(arg: tuple) -> None:
        data, activity = arg

        user_id = activity.actor.id
        user_name = environ.env.session.get(SessionKeys.user_name.value, None)
        image = environ.env.session.get(SessionKeys.image.value, '')
        status = activity.verb

        if user_name is None:
            try:
                user_name = utils.get_user_name_for(user_id)
            except NoSuchUserException:
                user_name = str(user_id)

        if not utils.is_valid_id(user_id):
            OnStatusHooks.logger.warning('got invalid user id for activity: {}'.format(str(data)))
            return

        if utils.is_super_user(user_id) or utils.is_global_moderator(user_id):
            OnStatusHooks.log_admin_activity(user_id, user_name, status)

        # mostly for away/back, since they might be called a lot without changing the status,
        # in which case we don't need to send an external event
        status_changed = True

        if status == 'online':
            OnStatusHooks.set_online(user_id, user_name, image)
        elif status == 'invisible':
            stage = 'status'
            if hasattr(activity.actor, 'summary'):
                stage = activity.actor.summary
            OnStatusHooks.set_invisible(user_id, user_name, stage=stage)
        elif status == 'visible':
            OnStatusHooks.set_visible(user_id, user_name)
        elif status == 'offline':
            OnStatusHooks.set_offline(user_id, user_name)
        elif status == 'away':
            status_changed = OnStatusHooks.set_away(user_id)
        elif status == 'back':
            status_changed = OnStatusHooks.set_back(user_id)

        # for Solr
        if status in {"offline", "invisible"}:
            utils.add_last_online_at_to_event(data)
        elif status == "away":
            utils.add_last_active_at_to_event(data)

        if status_changed:
            environ.env.publish(data, external=True)

    @staticmethod
    def log_admin_activity(user_id, user_name, status):
        try:
            info_message = \
                'op {} ({}) requested to change status to {}; user status is currently set to {}'.format(
                    user_id,
                    user_name,
                    status,
                    utils.get_user_status(user_id)
                )
            OnStatusHooks.logger.info(info_message)
        except NoSuchUserException:
            OnStatusHooks.logger.error('no username found for op user {}'.format(user_id))
        except Exception as e:
            OnStatusHooks.logger.error('exception while getting username for op {}: {}'.format(user_id, str(e)))
            OnStatusHooks.logger.exception(e)
            environ.env.capture_exception(sys.exc_info())

    @staticmethod
    def set_offline(user_id: str, user_name: str) -> None:
        OnStatusHooks.logger.info('setting user {} ({}) to offline'.format(
            user_id, user_name,
        ))
        environ.env.db.set_user_offline(user_id)
        activity_json = utils.activity_for_disconnect(user_id, user_name)
        rooms = utils.rooms_for_user(user_id)
        for room_id in rooms:
            environ.env.emit(
                'gn_user_disconnected', activity_json, room=room_id, broadcast=True,
                include_self=False, namespace='/ws')

    @staticmethod
    def set_away(user_id: str) -> bool:
        """
        returns True if we change the status, otherwise False
        """
        user_status = utils.get_user_status(user_id, skip_cache=False)

        if user_status == UserKeys.STATUS_AVAILABLE:
            OnStatusHooks.logger.info(f"setting user {user_id} to away (was online)")
            environ.env.cache.set_user_away(user_id)
            return True
        return False

    @staticmethod
    def set_back(user_id: str) -> bool:
        """
        returns True if we change the status, otherwise False
        """
        user_status = utils.get_user_status(user_id, skip_cache=False)

        if user_status == UserKeys.STATUS_AWAY:
            OnStatusHooks.logger.info(f"setting user {user_id} back to online (was away)")
            environ.env.cache.set_user_online(user_id)
            return True
        return False

    @staticmethod
    def set_visible(user_id: str, user_name: str) -> None:
        user_status = utils.get_user_status(user_id, skip_cache=True)
        if user_status in {UserKeys.STATUS_AVAILABLE, UserKeys.STATUS_CHAT}:
            return

        # status is UserKeys.STATUS_VISIBLE, but is in multicast so the user is online
        if environ.env.cache.user_is_in_multicast(user_id):
            OnStatusHooks.logger.info(
                'setting user {} ({}) to visible (online), was invisible (online)'.format(user_id, user_name))
            OnStatusHooks.set_online(user_id, user_name)

        # otherwise status is UserKeys.STATUS_INVISIBLE, but if not in multicast the user is offline
        # TODO: when choosing to login invisibly, this is called before the user connects to dino, so should NOT do set_offline()
        # TODO: should visible login call set_status with 'online' or 'visible'?
        else:
            OnStatusHooks.logger.info(
                'setting user {} ({}) to visible (offline), was invisible (offline)'.format(user_id, user_name))
            OnStatusHooks.set_offline(user_id, user_name)

    @staticmethod
    def set_invisible(user_id: str, user_name: str, stage: str) -> None:
        user_status = utils.get_user_status(user_id)

        # no need to set it again if already invisible
        if user_status == UserKeys.STATUS_INVISIBLE:
            return

        OnStatusHooks.logger.info('setting user {} ({}) to invisible'.format(
            user_id, user_name,
        ))

        # when logging in as 'invisible', the rest call can happen after ws
        # logs in, in which case we don't want to update last_online
        if stage == 'login':
            is_offline = True
        else:
            is_offline = not utils.user_is_online(user_id)

        environ.env.db.set_user_invisible(user_id, is_offline=is_offline)

        if is_offline:
            # nothing more to do if offline already
            return

        disconnect_activity = utils.activity_for_disconnect(user_id, user_name)

        rooms = utils.rooms_for_user(user_id)
        for room_id in rooms:
            admins_in_room = environ.env.db.get_admins_in_room(room_id, user_id)
            if admins_in_room is None or len(admins_in_room) == 0:
                environ.env.emit('gn_user_disconnected', disconnect_activity, room=room_id, broadcast=True,
                                 include_self=False, namespace='/ws')
                continue

            users_in_room = utils.get_users_in_room(room_id)
            for other_user_id, _ in users_in_room.items():
                if other_user_id in admins_in_room:
                    continue
                environ.env.emit(
                    'gn_user_disconnected', disconnect_activity, room=other_user_id, broadcast=True,
                    include_self=False, namespace='/ws')

            invisible_activity = utils.activity_for_going_invisible(user_id)
            for admin_id in admins_in_room:
                environ.env.emit(
                    'gn_user_invisible', invisible_activity, room=admin_id, broadcast=False, namespace='/ws')

    @staticmethod
    def set_online(user_id: str, user_name: str, image: str = '') -> None:
        was_invisible = utils.user_is_invisible(user_id)
        OnStatusHooks.logger.info('setting user {} ({}) online (was invisible before? {})'.format(
            user_id, user_name, was_invisible
        ))
        environ.env.db.set_user_online(user_id)
        rooms = utils.rooms_for_user(user_id)

        if not was_invisible:
            for room_id in rooms:
                room_name = utils.get_room_name(room_id)
                join_activity = utils.activity_for_user_joined(user_id, user_name, room_id, room_name, image)
                environ.env.emit(
                    'gn_user_joined', join_activity, room=room_id, broadcast=True,
                    include_self=False, namespace='/ws')
            return

        visible_activity = utils.activity_for_going_visible(user_id)
        for room_id in rooms:
            room_name = utils.get_room_name(room_id)
            join_activity = utils.activity_for_user_joined(user_id, user_name, room_id, room_name, image)
            admins_in_room = environ.env.db.get_admins_in_room(room_id, user_id)

            if admins_in_room is None or len(admins_in_room) == 0:
                environ.env.emit(
                    'gn_user_joined', join_activity, room=room_id, broadcast=True,
                    include_self=False, namespace='/ws')
                continue

            users_in_room = utils.get_users_in_room(room_id).copy()
            for user_id_in_room, _ in users_in_room.items():
                if user_id_in_room in admins_in_room:
                    continue
                environ.env.emit(
                    'gn_user_joined', join_activity, room=user_id_in_room, broadcast=True,
                    include_self=False, namespace='/ws')

            for admin_id in admins_in_room:
                environ.env.emit(
                    'gn_user_visible', visible_activity, room=admin_id, broadcast=False, namespace='/ws')


@environ.env.observer.on('on_status')
def _on_status_set_status(arg: tuple) -> None:
    OnStatusHooks.set_status(arg)