from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler import logging import configuration from . import message_helpers config = configuration.parsed["SLACK"] models = configuration.models class BotApp(App): logger = logging.getLogger(__name__) def __init__(self, callback, *args, **kwargs): super().__init__(*args, **kwargs) self.callback = callback def start(self): message_helpers.init(self.client) missed_messages, missed_reactions = message_helpers.get_unhandled_messages() [self.handle_incoming_message(m) for m in missed_messages] [self.handle_incoming_reaction(r) for r in missed_reactions] # self.react_missed_reactions(missed_reactions) # self.react_missed_messages(missed_messages) self.startup_status() def handle_incoming_reaction(self, reaction): if isinstance(reaction, dict): #else: the reaction is already being passed as a model # CAUTION: filter for 'changed reactions' those are nasty (usually when adding an url) reaction = message_helpers.reaction_dict_to_model(reaction) thread = reaction.message.thread article_object = thread.article if not article_object is None: reaction = reaction.type status = 1 if reaction == "white_check_mark" else -1 # self.logger.info(f"Applying reaction {reaction} to its root message.") article_object.verified = status article_object.save() def handle_incoming_message(self, message): """Reacts to all messages inside channel archiving. Must then distinguish between threaded replies and new requests and react accordingly""" if isinstance(message, dict): #else: the message is already being passed as a model # CAUTION: filter for 'changed messages' those are nasty (usually when adding an url) if message.get("subtype", "not bad") == "message_changed": return False message = message_helpers.message_dict_to_model(message) # First check: belongs to thread? is_threaded = message.thread.message_count > 1 and message != message.thread.initiator_message if is_threaded: self.incoming_thread_message(message) else: self.incoming_channel_message(message) def incoming_thread_message(self, message): if message.user.user_id == config["bot_id"]: return True # ignore the files uploaded by the bot. We handled them already! thread = message.thread if thread.is_fully_processed: return True self.logger.info("Receiving thread-message") self.respond_thread_message(message) def incoming_channel_message(self, message): self.logger.info(f"Handling message {message} ({len(message.urls)} urls)") if not message.urls: # no urls in a root-message => IGNORE message.is_processed_override = True message.save() return # ensure thread is still empty, this is a scenario encountered only in testing, but let's just filter it if message.thread.message_count > 1: self.logger.info("Discarded message because it is actually processed.") return if len(message.urls) > 1: message_helpers.say_substitute("Only the first url is being handled. Please send any subsequent url as a separate message", thread_ts=message.thread.slack_ts) self.callback(message) # for url in message.urls: # self.callback(url, message) # stop here! def respond_thread_message(self, message, say=message_helpers.say_substitute): thread = message.thread article = thread.article if message.perma_link: # file upload means new data fname = message_helpers.save_as_related_file(message.perma_link, article) say("File was saved as 'related file' under `{}`.".format(fname), thread_ts=thread.slack_ts ) else: # either a pointer to a new file (too large to upload), or trash success = message_helpers.react_file_path_message(message.text, article) if success: say("File was saved as 'related file'", thread_ts=thread.slack_ts) else: self.logger.error("User replied to thread {} but the response did not contain a file/path".format(thread)) say("Cannot process response without associated file.", thread_ts=thread.slack_ts ) def respond_channel_message(self, thread, say=message_helpers.say_substitute): article = thread.article answers = article.slack_info for a in answers: if a["file_path"]: try: # either, a["file_path"] does not exist, or the upload resulted in an error self.client.files_upload( channels = config["archive_id"], initial_comment = f"<@{config['responsible_id']}> \n {a['reply_text']}", file = a["file_path"], thread_ts = thread.slack_ts ) status = True except: say( "File {} could not be uploaded.".format(a), thread_ts=thread.slack_ts ) status = False else: # anticipated that there is no file! say( f"<@{config['responsible_id']}> \n {a['reply_text']}", thread_ts=thread.slack_ts ) status = True def startup_status(self): threads = [t for t in models.Thread.select()] all_threads = len(threads) fully_processed = len([t for t in threads if t.is_fully_processed]) fully_unprocessed = len([t for t in threads if t.message_count == 1]) articles_unprocessed = len(models.ArticleDownload.select().where(models.ArticleDownload.verified < 1)) self.logger.info(f"[bold]STATUS[/bold]: Fully processed {fully_processed}/{all_threads} threads. {fully_unprocessed} threads have 0 replies. Article-objects to verify: {articles_unprocessed}", extra={"markup": True}) class BotRunner(): """Stupid encapsulation so that we can apply the slack decorators to the BotApp""" def __init__(self, callback, *args, **kwargs) -> None: self.bot_worker = BotApp(callback, token=config["auth_token"]) @self.bot_worker.event(event="message", matchers=[message_helpers.is_message_in_archiving]) def handle_incoming_message(message, say): return self.bot_worker.handle_incoming_message(message) @self.bot_worker.event(event="reaction_added", matchers=[message_helpers.is_reaction_in_archiving]) def handle_incoming_reaction(event, say): return self.bot_worker.handle_incoming_reaction(event) # target = self.launch # super().__init__(target=target) def start(self): self.bot_worker.start() SocketModeHandler(self.bot_worker, config["app_token"]).start() # def respond_to_message(self, message): # self.bot_worker.handle_incoming_message(message)