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)
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')
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']))
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)
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' )
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' )
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()
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)
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] )
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)
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))
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.' )
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)
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)
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()
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 ]
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'])
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)
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)
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)
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)
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))
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)
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)
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)
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()
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')
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)
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'])
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()