Exemple #1
0
    def subscribe_users(
        self, message: Dict[str, Any], dest_stream: str,
        users: List[Union[str, Tuple[str, Optional[int]]]]
    ) -> Union[Response, Iterable[Response]]:
        # First, get all the ids of the users whose ids we do not already know.
        user_ids: Optional[
            List[int]] = self.client.get_user_ids_from_display_names(
                map(
                    lambda o: o[0] if isinstance(o, tuple) else o,
                    filter(
                        lambda o: isinstance(o, str) or
                        (isinstance(o, tuple) and o[1] is None), users)))
        if user_ids is None:
            return Response.build_message(
                message, 'error: could not get the user ids.')

        user_ids.extend(
            map(
                lambda t: cast(int, t[1]),
                filter(
                    lambda o: isinstance(o, tuple) and isinstance(o[1], int),
                    users)))

        if not self.client.subscribe_users(user_ids, dest_stream):
            return Response.error(message)

        return Response.ok(message)
Exemple #2
0
    def handle_message(
        self,
        message: Dict[str, Any],
        **kwargs: Any
    ) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        failed: List[str] = []

        stream_tuples: Optional[List[Any]] = split(
            message['command'], converter = [lambda t: split(
                t, sep = ',', exact_split = 2, discard_empty = False
            )]
        )
        if stream_tuples is None or None in stream_tuples:
            return Response.error(message)

        for stream, desc in stream_tuples:
            if not stream:
                failed.append('one empty stream name')
                continue
            result: Dict[str, Any] = self.client.add_subscriptions(
                streams = [{'name': stream, 'description': desc}]
            )
            if result['result'] != 'success':
                failed.append(f'stream: {stream}, description: {desc}')

        if not failed:
            return Response.ok(message)

        response: str = 'Failed to create the following streams:\n' + '\n'.join(failed)

        return Response.build_message(message, response, msg_type = 'private')
Exemple #3
0
    def handle_message(self, message: Dict[str, Any],
                       **kwargs: Any) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        handlers: List[logging.Handler] = logging.getLogger().handlers
        if not handlers or len(handlers) > 1:
            return Response.build_message(message,
                                          'Cannot determine the logfile.')

        if not isinstance(handlers[0], logging.FileHandler):
            return Response.build_message(message, 'No logfile in use.')

        # Upload the logfile. (see https://zulip.com/api/upload-file)
        with open(handlers[0].baseFilename, 'rb') as lf:
            result: Dict[str, Any] = self.client.call_endpoint('user_uploads',
                                                               method='POST',
                                                               files=[lf])

        if result['result'] != 'success':
            return Response.build_message(message,
                                          'Could not upload the logfile.')

        return Response.build_message(message,
                                      '[logfile]({})'.format(result['uri']))
Exemple #4
0
    def handle_message(
        self,
        message: Dict[str, Any],
        **kwargs: Any
    ) -> Union[Response, Iterable[Response]]:
        result: Optional[Tuple[str, CommandParser.Opts, CommandParser.Args]]
        result_sql: List[Tuple[Any, ...]]

        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        # Get command and parameters.
        result = self.command_parser.parse(message['command'])
        if result is None:
            return Response.command_not_found(message)
        command, _, args = result

        if command == 'list':
            result_sql = self._db.execute(self._list_sql)
            response: str = 'Alert word or phrase | Emoji\n---- | ----'
            for (phrase, emoji) in result_sql:
                response += '\n`{0}` | {1} :{1}:'.format(phrase, emoji)
            return Response.build_message(message, response)

        # Use lowercase -> no need for case insensitivity.
        alert_phrase: str = args.alert_phrase.lower()

        if command == 'add':
            # Add binding to database or update it.
            self._db.execute(self._update_sql, alert_phrase, args.emoji, commit = True)
        elif command == 'remove':
            self._db.execute(self._remove_sql, alert_phrase, commit = True)

        return Response.ok(message)
Exemple #5
0
    def _move_topic(
        self,
        message: Dict[str, Any],
        dest: str,
        count: int
    ) -> Union[Response, Iterable[Response]]:
        if count < 1:
            return Response.build_message(message, 'Error: Message count must be >= 1.')

        # Get the stream id ...
        stream_id: int = message['stream_id']
        # ... and the topic of the current message.
        topic: str = message['subject']

        # Get the message that is count "hops" before the given message
        # in this topic.
        request: Dict[str, Any] = {
            'anchor': 'newest',
            'num_before': count + 1,
            'num_after': 0,
            'narrow': [
                {'operator': 'stream', 'operand': stream_id},
                {'operator': 'topic', 'operand': topic},
                {'operator': 'near', 'operand': str(message['id'])}
            ]
        }
        result = self.client.get_messages(request)
        if result['result'] != 'success':
            return Response.error(message)
        if len(result['messages']) < 2:
            return Response.build_message(message, 'No message to move.')
        first_message: Dict[str, Any] = result['messages'][0]

        # Move message (and all following in the same topic) to the new topic.
        request = {
            'message_id': first_message['id'],
            'topic': dest,
            'stream_id': stream_id,
            'send_notification_to_old_thread': False,
            'send_notification_to_new_thread': False,
            'propagate_mode': 'change_later'
        }
        result = self.client.update_message(request)
        if result['result'] != 'success':
            return Response.error(message)

        # Remove requesting message.
        self.client.delete_message(message['id'])

        # Get current stream name.
        stream_name: Optional[str] = self.client.get_stream_name(stream_id)
        from_loc: str = (stream_name if stream_name is not None else "unknown") + ">" + topic
        to_loc: str = (stream_name if stream_name is not None else "unknown") + ">" + dest

        return Response.build_message(
            first_message,
            self.msg_template_topic.format(first_message['sender_full_name'], from_loc, to_loc),
            msg_type = 'private'
        )
Exemple #6
0
    def _move_stream(
        self,
        message: Dict[str, Any],
        dest: str
    ) -> Union[Response, Iterable[Response]]:
        # Get destination stream name.
        dest_stream: Optional[str] = Regex.get_stream_name(dest)
        if dest_stream is None:
            return Response.command_not_found(message)

        # Get destination stream id.
        result: Dict[str, Any] = self.client.get_stream_id(dest_stream)
        if result['result'] != 'success':
            return Response.error(message)
        dest_stream_id: int = result['stream_id']

        # Get the stream id ...
        src_stream_id: int = message['stream_id']
        # ... and the topic of the current message.
        topic: str = message['subject']

        # Get message which started the topic.
        request: Dict[str, Any] = {
            'anchor': 'oldest',
            'num_before': 0,
            'num_after': 1,
            'narrow': [
                { 'operator': 'stream', 'operand': src_stream_id },
                { 'operator': 'topic', 'operand': topic }
            ]
        }
        result = self.client.get_messages(request)
        if result['result'] != 'success':
            return Response.error(message)
        first_message: Dict[str, Any] = result['messages'][0]

        # Move message (and all following in the same topic) = move topic.
        request = {
            'message_id': first_message['id'],
            'topic': topic,
            'stream_id': dest_stream_id,
            'send_notification_to_old_thread': False,
            'propagate_mode': 'change_all'
        }
        result = self.client.update_message(request)
        if result['result'] != 'success':
            return Response.error(message)

        # Remove requesting message.
        self.client.delete_message(message['id'])

        return Response.build_message(
            first_message,
            self.msg_template_stream.format(first_message['sender_full_name'], topic, dest_stream),
            msg_type = 'private'
        )
Exemple #7
0
    def handle_message(self, message: Dict[str, Any],
                       **kwargs: Any) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        # Ask the parent process to restart.
        os.kill(os.getpid(), signal.SIGUSR1)

        # dead code
        return Response.none()
Exemple #8
0
 def _unclaim(
     self,
     message: Dict[str, Any],
     group_id: str,
     message_id: str
 ) -> Union[Response, Iterable[Response]]:
     try:
         msg_id: int = int(message_id)
     except ValueError:
         return Response.build_message(message, f'{message_id} is not an integer.')
     self._db.execute(
         self._unclaim_msg_from_group_sql, msg_id, group_id, commit = True
     )
     return Response.ok(message)
Exemple #9
0
 def _unsubscribe(
     self,
     user_id: int,
     group_id: str,
     message: Optional[Dict[str, Any]] = None
 ) -> Union[Response, Iterable[Response]]:
     """Unsubscribe a user from a group."""
     self._db.execute(self._unsubscribe_user_sql, user_id, group_id, commit = True)
     if message is not None:
         return Response.ok(message)
     return Response.build_message(
         message = None, content = f'Unsubscribed from group {group_id}.',
         msg_type = 'private', to = [user_id]
     )
Exemple #10
0
    def subscribe_user_emails(
            self, message: Dict[str, Any], dest_stream: str,
            user_emails: List[str]) -> Union[Response, Iterable[Response]]:
        user_ids: Optional[List[int]] = self.client.get_user_ids_from_emails(
            user_emails)
        if user_ids is None:
            return Response.build_message(
                message, 'error: could not get the user ids.')

        if not self.client.subscribe_users(
                user_ids, dest_stream, allow_private_streams=True):
            return Response.error(message)

        return Response.ok(message)
Exemple #11
0
    def handle_message(self, message: Dict[str, Any],
                       **kwargs: Any) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        stream_regexes: Optional[List[Any]] = split(
            message['command'], converter=[validate_and_return_regex])
        if stream_regexes is None or None in stream_regexes:
            return Response.build_message(
                message, 'Found invalid regular expressions.')

        response: List[str] = []

        for stream_regex in stream_regexes:
            streams: List[str] = self.client.get_streams_from_regex(
                stream_regex)
            removed: int = 0

            for stream in streams:
                result: Dict[str, Any] = self.client.get_stream_id(stream)
                if result['result'] != 'success':
                    continue
                stream_id: int = result['stream_id']

                # Check if stream is empty.
                result = self.client.get_messages({
                    'anchor':
                    'oldest',
                    'num_before':
                    0,
                    'num_after':
                    1,
                    'narrow': [{
                        'operator': 'stream',
                        'operand': stream_id
                    }]
                })
                if result['result'] != 'success' or result['messages']:
                    continue

                # Archive the stream: https://zulip.com/help/archive-a-stream
                result = self.client.delete_stream(stream_id)
                if result['result'] == 'success':
                    removed += 1

            response.append('"%s" - found %d matching streams, removed %d' %
                            (stream_regex, len(streams), removed))

        return Response.build_message(message, '\n'.join(response))
Exemple #12
0
    def _remove(
        self,
        message: Dict[str, Any],
        group_id: str,
    ) -> Union[Response, Iterable[Response]]:
        msg_success: bool = self._announcements_remove_group(group_id)

        self._db.execute(self._remove_sql, group_id, commit = True)

        if msg_success:
            return Response.ok(message)

        return Response.build_message(
            message, 'Group removed, but removal failed for some announcement messages.'
        )
Exemple #13
0
    def _list(
        self,
        message: Dict[str, Any]
    ) -> Union[Response, Iterable[Response]]:
        """Command `group list`."""
        response: str = 'Group Id | Emoji | Streams | ClaimedBy\n---- | ---- | ---- | ----'

        for (group_id, emoji, streams) in self._db.execute(self._list_sql):
            streams_concat: str = ', '.join(
                '"{}"'.format(s) for s in streams.split('\n')
            )
            claims: str = ', '.join([
                self.message_link.format(msg_id)
                for (msg_id,) in self._db.execute(self._get_claims_for_group, group_id)
            ])
            response += '\n{0} | {1} :{1}: | `{2}` | {3}'.format(
                group_id, emoji, streams_concat, claims
            )

        response += '\n\nMessages claimed for all groups: ' + ', '.join(
            self.message_link.format(msg_id)
            for msg_id, in self._db.execute(self._get_claims_for_all_sql)
        )

        return Response.build_message(message, response)
Exemple #14
0
 def _unannounce(
     self,
     message: Dict[str, Any],
     message_id: str
 ) -> Union[Response, Iterable[Response]]:
     self._db.execute(self._unclaim_msg_for_all_sql, message_id, commit = True)
     return Response.ok(message)
Exemple #15
0
    def handle_reaction_event(
        self,
        event: Dict[str, Any],
    ) -> Union[Response, Iterable[Response]]:
        group_id: Optional[str] = self._get_group_id_from_emoji_event(
            event['message_id'], event['emoji_name']
        )

        if group_id is None:
            return Response.none()
        if event['op'] == 'add':
            return self._subscribe(event['user_id'], group_id)
        if event['op'] == 'remove':
            return self._unsubscribe(event['user_id'], group_id)

        return Response.none()
Exemple #16
0
    def handle_event(self, event: Dict[str, Any],
                     **kwargs: Any) -> Union[Response, Iterable[Response]]:
        if not self._bindings:
            return Response.none()

        # Get message content.
        # Replace markdown links by their textual representation.
        # Convert to lowercase.
        content: str = self._markdown_links\
            .sub(r'\1', event['message']['content'])\
            .lower()

        return [
            Response.build_reaction(message=event['message'], emoji=emoji)
            for pattern, emoji in self._bindings
            if randint(1, 6) == 3 and pattern.search(content) is not None
        ]
Exemple #17
0
    def _help_command(self, message: Dict[str, Any],
                      command: str) -> Union[Response, Iterable[Response]]:
        info_tuple: Optional[Tuple[str, str, str]] = None

        for ituple in self.help_info:
            if ituple[0] == command:
                info_tuple = ituple
                break
        if info_tuple is None:
            return Response.command_not_found(message)

        help_message: str = '\n'.join(info_tuple[1:])

        return Response.build_message(message,
                                      help_message,
                                      msg_type='private',
                                      to=message['sender_email'])
Exemple #18
0
    def subscribe_all_users(
        self,
        message: Dict[str, Any],
        dest_stream: str,
    ) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        result: Dict[str, Any] = self.client.get_users()
        if result['result'] != 'success':
            return Response.error(message)
        user_ids: List[int] = [user['user_id'] for user in result['members']]

        if not self.client.subscribe_users(user_ids, dest_stream):
            return Response.error(message)

        return Response.ok(message)
Exemple #19
0
    def handle_message(self, message: Dict[str, Any],
                       **kwargs: Any) -> Union[Response, Iterable[Response]]:
        result: Optional[Tuple[str, CommandParser.Opts, CommandParser.Args]]

        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        result = self.command_parser.parse(message['command'])
        if result is None:
            return Response.command_not_found(message)
        command, _, args = result

        if command == 'list':
            response: str = 'Key | Value\n ---- | ----'
            for key, value in self._db.execute(self._list_sql):
                response += f'\n{key} | {value}'
            return Response.build_message(message, response)

        if command == 'remove':
            self._db.execute(self._remove_sql, args.key, commit=True)
            return Response.ok(message)

        if command == 'set':
            try:
                self._db.execute(self._update_sql,
                                 args.key,
                                 args.value,
                                 commit=True)
            except Exception as e:
                logging.exception(e)
                return Response.build_message(message, 'Failed: %s' % str(e))
            return Response.ok(message)

        return Response.command_not_found(message)
Exemple #20
0
    def handle_message(self, message: Dict[str, Any],
                       **kwargs: Any) -> Union[Response, Iterable[Response]]:
        result_sql: List[Tuple[Any, ...]]

        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        try:
            if message['command'] == 'list':
                result_sql = self._db.execute(self._list_sql)
            else:
                result_sql = self._db.execute(message['command'])
        except Exception as e:
            return Response.build_message(message, str(e))

        result: str = '```text\n' + '\n'.join(map(str, result_sql)) + '\n```'

        return Response.build_message(message, result)
Exemple #21
0
    def handle_message(
        self,
        message: Dict[str, Any],
        **kwargs: Any
    ) -> Union[Response, Iterable[Response]]:
        result: Optional[Tuple[str, CommandParser.Opts, CommandParser.Args]]

        # Get command and parameters.
        result = self.command_parser.parse(message['command'])
        if result is None:
            return Response.command_not_found(message)
        command, _, args = result

        if command == 'subscribe':
            return self._subscribe(message['sender_id'], args.group_id, message)
        if command == 'unsubscribe':
            return self._unsubscribe(message['sender_id'], args.group_id, message)

        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        if command == 'list':
            return self._list(message)
        if command == 'announce':
            if message['type'] != 'stream':
                return Response.build_message(message, 'Claim only stream messages.')
            return self._announce(message)
        if command == 'unannounce':
            return self._unannounce(message, args.message_id)
        if command == 'claim':
            if message['type'] != 'stream':
                return Response.build_message(message, 'Claim only stream messages.')
            return self._claim(message, args.group_id)
        if command == 'unclaim':
            return self._unclaim(message, args.group_id, args.message_id)
        if command == 'add':
            return self._add(message, args.group_id, args.emoji)
        if command == 'remove':
            return self._remove(message, args.group_id)
        if command in ['add_streams', 'remove_streams']:
            return self._change_streams(message, args.group_id, command, args.streams)

        return Response.command_not_found(message)
Exemple #22
0
    def subscribe_streams(
            self, message: Dict[str, Any], dest_stream: str,
            streams: List[str]) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        failed: List[str] = []

        for stream in streams:
            if not self.client.subscribe_all_from_stream_to_stream(
                    stream, dest_stream, None):
                failed.append(stream)

        if not failed:
            return Response.ok(message)

        return Response.build_message(
            message,
            'Failed to subscribe the following streams:\n' + '\n'.join(failed))
Exemple #23
0
    def _change_streams(
        self,
        message: Dict[str, Any],
        group_id: str,
        command: str,
        change_stream_regs: List[str]
    ) -> Union[Response, Iterable[Response]]:
        """Command `group (add_streams|remove_streams) <id> <stream>...`."""
        # Validate the regexes.
        for reg in change_stream_regs:
            try:
                re.compile(reg)
            except re.error as e:
                return Response.build_message(message, 'invalid regex: %s\n%s', reg, str(e))

        result_sql: List[Tuple[Any, ...]] = self._db.execute(
            self._get_streams_sql, group_id, commit = True
        )
        if not result_sql:
            return Response.build_message(message, f'Group {group_id} does not exist.')

        # Current stream patterns.
        stream_list: List[str] = result_sql[0][0].split('\n')
        # The string containing the new list of stream patterns (newline separated).
        # The patterns have to be non-empty.
        new_streams: str = '\n'.join(filter(
            bool,
            set(stream_list + change_stream_regs) if command == 'add_streams' else
            [s for s in stream_list if s not in change_stream_regs]
        ))

        try:
            self._db.execute(self._update_streams_sql, new_streams, group_id, commit = True)
        except Exception as e:
            logging.exception(e)
            return Response.build_message(message, str(e))

        # Subscribe the group subscribers to the new streams.
        self._subscribe_users_to_stream_regexes(
            self._get_group_subscribers([group_id]), change_stream_regs
        )

        return Response.ok(message)
Exemple #24
0
    def handle_message(self, message: Dict[str, Any],
                       **kwargs: Any) -> Union[Response, Iterable[Response]]:
        result: Optional[Tuple[str, CommandParser.Opts, CommandParser.Args]]

        result = self.command_parser.parse(message['command'])
        if result is None:
            return Response.command_not_found(message)
        command, _, args = result

        if command == 'streams':
            return self.subscribe_streams(message, args.dest_stream,
                                          args.streams)
        if command == 'users':
            return self.subscribe_users(message, args.dest_stream, args.users)
        if command == 'user_emails':
            return self.subscribe_user_emails(message, args.dest_stream,
                                              args.user_emails)
        if command == 'all_users':
            return self.subscribe_all_users(message, args.dest_stream)

        return Response.command_not_found(message)
Exemple #25
0
    def _claim(
        self,
        message: Dict[str, Any],
        group_id: Optional[str],
    ) -> Union[Response, Iterable[Response]]:
        """Command `group claim [id]`."""
        if group_id:
            self._db.execute(self._claim_group_sql, message['id'], group_id, commit = True)
        else:
            self._db.execute(self._claim_all_sql, message['id'], commit = True)

        return Response.ok(message)
Exemple #26
0
    def _announce(
        self,
        message: Dict[str, Any]
    ) -> Union[Response, Iterable[Response]]:
        table: str = '\n'.join(
            self._announcement_msg_table_row_fmt % (group_id, emoji)
            for group_id, emoji, _ in self._db.execute(self._list_sql)
        )

        # Remove the requesting message.
        self.client.delete_message(message['id'])

        # Send own message.
        result: Dict[str, Any] = self.client.send_response(
            Response.build_message(message, self._announcement_msg.format(table))
        )
        if result['result'] != 'success':
            return Response.none()

        # Insert the id of the bot's message into the database.
        try:
            self._db.execute(self._claim_all_sql, result['id'], commit = True)
        except Exception as e:
            return Response.build_message(message, str(e))

        # Get all the currently existant emojis.
        result_sql: List[Tuple[Any, ...]] = self._db.execute(self._get_all_emojis_sql)
        if not result_sql:
            return Response.none()

        # React with all those emojis on this message.
        for emoji in map(lambda t: cast(str, t[0]), result_sql):
            self.client.send_response(Response.build_reaction_from_id(result['id'], emoji))

        return Response.none()
Exemple #27
0
    def handle_message(
        self,
        message: Dict[str, Any],
        **kwargs: Any
    ) -> Union[Response, Iterable[Response]]:
        if not self.client.user_is_privileged(message['sender_id']):
            return Response.admin_err(message)

        failed: List[str] = []

        stream_tuples: Optional[List[Any]] = split(
            message['command'], converter = [lambda t: split(t, sep = ',', exact_split = 2)]
        )
        if stream_tuples is None or None in stream_tuples:
            return Response.error(message)

        for old, new in stream_tuples:
            # Used for error messages.
            line: str = f'{old} -> {new}'

            try:
                old_id: int = self.client.get_stream_id(old)['stream_id']
            except Exception as e:
                logging.exception(e)
                failed.append(line)
                continue

            result: Dict[str, Any] = self.client.update_stream(
                {'stream_id': old_id, 'new_name': '"{}"'.format(new)}
            )
            if result['result'] != 'success':
                failed.append(line)

        if not failed:
            return Response.ok(message)

        response: str = 'Failed to perform the following renamings:\n' + '\n'.join(failed)

        return Response.build_message(message, response, msg_type = 'private')
Exemple #28
0
 def event_callback(self, event: Dict[str, Any]) -> None:
     """Process one event."""
     try:
         if self.is_responsible(event):
             self.client.send_responses(self.handle_event(event))
     except Exception as e:
         logging.exception(e)
         if event['type'] == 'message':
             try:
                 self.client.send_responses(
                     Response.exception(event['message']))
             except Exception as e2:
                 logging.exception(e2)
Exemple #29
0
    def _help_overview(
            self, message: Dict[str,
                                Any]) -> Union[Response, Iterable[Response]]:
        # Get the command names.
        help_message: str = '\n'.join(
            map(lambda tuple: '- ' + tuple[0], self.help_info))

        return Response.build_message(message,
                                      self._help_overview_template.format(
                                          message['sender_full_name'],
                                          help_message),
                                      msg_type='private',
                                      to=message['sender_email'])
Exemple #30
0
    def handle_stream_event(
        self,
        event: Dict[str, Any],
    ) -> Union[Response, Iterable[Response]]:
        for stream in event['streams']:
            # Get all the groups this stream belongs to.
            group_ids: List[str] = self._get_group_ids_from_stream(stream['name'])
            # Get all user ids to subscribe to this new stream ...
            user_ids: List[int] = self._get_group_subscribers(group_ids)
            # ... and subscribe them.
            self.client.subscribe_users(user_ids, stream['name'])

        return Response.none()