190 lines
7.5 KiB
Python
190 lines
7.5 KiB
Python
from threading import Thread
|
|
from slack_bolt import App
|
|
from slack_bolt.adapter.socket_mode import SocketModeHandler
|
|
|
|
import logging
|
|
from rich.rule import Rule
|
|
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)
|
|
# models = models
|
|
self.callback = callback
|
|
|
|
def start(self):
|
|
message_helpers.init(self.client)
|
|
missed_messages, missed_reactions = message_helpers.get_past_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("Handling message with {} url(s)".format(len(message.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, article, say=message_helpers.say_substitute):
|
|
# extra={"markup": True}
|
|
# self.logger.info(Rule(url[:min(len(url), 30)]))
|
|
thread = article.slack_thread.execute()[0]
|
|
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
|
|
# self.logger.info(Rule(f"Fully handled (success={status})"))
|
|
|
|
|
|
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 {all_threads}/{fully_processed} threads. {fully_unprocessed} threads have 0 replies. Article-objects to verify: {articles_unprocessed}", extra={"markup": True})
|
|
|
|
|
|
|
|
|
|
|
|
class BotRunner(Thread):
|
|
"""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 launch(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) |