async def __process_timer_deletion(data: dict, channel_id: str, user_id: str): timer_name = data['actions'][0]['value'] base_url = f"http://{config.DB_URL}/timer/" if not timer_name: await container.slacker.post_to_channel(channel_id=channel_id, text=TIMER_NAME_NOT_SPECIFIED) return answer = try_request(container.logger, r.get, base_url + "exists", params={'timer_name': timer_name, 'username': user_id}) if answer.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=INTERNAL_ERROR) return if not answer.unwrap().json(): await container.slacker.post_to_channel(channel_id=channel_id, text=TIMER_NOT_FOUND) return answer = try_request(container.logger, r.delete, base_url, params={'timer_name': timer_name, 'username': user_id}) if answer.is_ok(): text = TIMER_DELETED else: text = TIMER_NOT_DELETED await container.slacker.post_to_channel(channel_id=channel_id, text=text.format(timer_name))
async def update_timers_once(logger: Logger, ui_service: str, db_service: str): """ Updates timers that are older than N minutes ago and send user a message about it """ db_base_url = f"http://{db_service}/timer/" ui_base_url = f"http://{ui_service}/internal/" # get timers older than n_minutes time_border = (datetime.utcnow() - timedelta(minutes=OVERDUE_MINUTES)).isoformat() overdue_timers = (try_request(logger, r.get, db_base_url + "overdue", params={ "time_border": time_border }).map_or([], lambda x: x.json())) INFLUX_API_WRITE( Point("digestbot").field("overdue_timers", len(overdue_timers)).time(datetime.utcnow())) # update each timer and notify the timer creator now = datetime.utcnow() for timer in overdue_timers: new_start = datetime.fromisoformat(timer['next_start']) delta = timedelta(seconds=timer['delta']) # update time with deltas while new_start < now: new_start += delta new_timer = Timer( channel_id=timer['channel_id'], username=timer['username'], timer_name=timer['timer_name'], delta=delta, next_start=new_start, top_command=timer['top_command'], ) # update timer in DB and notify the user try_request(logger, r.patch, db_base_url + "next_start", data=json.dumps(new_timer.dict(), cls=TimerEncoder)) text = f"""Due to bot being offline or other reasons timer {new_timer.timer_name} of user <@{new_timer.username}> missed it's tick. Timer's new next start is: {new_timer.next_start.strftime('%Y-%m-%d %H:%M:%S')}.""" try_request(logger, r.post, ui_base_url + "message", data=json.dumps({ "channel_id": new_timer.channel_id, "text": text })) logger.debug(f"{len(overdue_timers)} overdue timers updated.")
async def send_initial_message(user_id: str, channel_id: str) -> None: base_url = f"http://{config.DB_URL}/preset/" answer = try_request(container.logger, r.get, base_url, params={ "user_id": user_id, "include_global": False }) if answer.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=answer.unwrap_err()) return answer = answer.unwrap() presets = answer.json() for x in presets: x['text_channel_ids'] = ", ".join(f"<#{c}>" for c in x.get('channel_ids')) template = container.jinja_env.get_template("preset_list.json") result = template.render(presets=presets) await container.slacker.post_to_channel(channel_id=channel_id, blocks=result, ephemeral=True, user_id=user_id)
async def report_statistics(logger: Logger, db_service: str): db_base_url = f"http://{db_service}/timer/count" timers_total = try_request(logger, r.get, db_base_url).map(lambda x: x.json()) if timers_total.is_ok(): INFLUX_API_WRITE( Point("digestbot").field( "timers_total", timers_total.unwrap()).time(datetime.utcnow()))
async def crawl_messages_once(slacker: Slacker, logger: Logger) -> None: base_url = f"http://{config.DB_URL}/" # get messages and insert them into database ch_info = await slacker.get_channels_list() or [] if ch_info: for ch_id, ch_name in ch_info: logger.debug(f"Channel: {ch_name}") prev_date = datetime.now() - timedelta( days=config.MESSAGE_DELTA_DAYS) messages = await slacker.get_channel_messages(ch_id, prev_date) if messages: try_request(logger, r.put, base_url + "message/", data=json.dumps([x.dict() for x in messages], cls=TimerEncoder)) logger.info( f"Messages from {len(ch_info)} channels parsed and sent to the database." ) channels_point = Point("workspace").field("channels", len(ch_info)).time( datetime.utcnow()) # update messages without permalinks answer = try_request(logger, r.get, base_url + "message/linkless") if answer.is_err(): return answer = answer.value empty_links_messages = [Message(**x) for x in answer.json()] if empty_links_messages: messages = await slacker.update_permalinks( messages=empty_links_messages) answer = try_request(logger, r.patch, base_url + "message/links", data=json.dumps([x.dict() for x in messages], cls=TimerEncoder)) if answer.is_ok(): logger.debug(f"Updated permalinks for {len(messages)} messages.") linkless_messages_point = Point("workspace").field( "linkless_messages", len(empty_links_messages)).time(datetime.utcnow()) INFLUX_API_WRITE([linkless_messages_point, channels_point])
def get_user_presets(user_id: str) -> Optional[List]: # get presets available for the user return try_request(container.logger, r.get, f"http://{config.DB_URL}/preset/", params={ 'user_id': user_id, 'include_global': "true" }).map(lambda x: x.json()).value
async def post_top_message(channel_id: str, request_parameters: dict): base_url = f"http://{config.DB_URL}/message/top" answer = try_request(container.logger, r.get, base_url, params=request_parameters) if answer.is_err(): answer = [MESSAGE_HANDLING_ERROR] elif not (y := answer.unwrap().json()): answer = [NO_MESSAGES_TO_PRINT]
async def ignore_interaction(data: dict): action_id = data.get("actions", [{}])[0].get("action_id", "") channel_id = data.get('channel', {}).get("id", "") user_id = data.get("user", {}).get("id", "") base_url = f"http://{config.DB_URL}/ignore/" if action_id == "ignore_user_add": field = "selected_user" op = r.put message = "User <@{0}> successfully added to the ignore list." elif action_id == "ignore_user_remove": field = "value" op = r.delete message = "User <@{0}> successfully removed from the ignore list." else: container.logger.warning(f"Unknown preset interaction message: {data}") return ignore_user = data.get("actions", [{}])[0].get(field, "") answer = try_request(container.logger, op, base_url, params={ 'author_id': user_id, 'ignore_id': ignore_user }) answer = answer.map(lambda *x: message.format(ignore_user)).value await container.slacker.post_to_channel(channel_id=channel_id, text=answer) ignored_count = try_request( container.logger, r.get, f"http://{config.DB_URL}/ignore/count").map(lambda x: x.json()) if ignored_count.is_ok(): INFLUX_API_WRITE( Point("digestbot").field("ignored_total", ignored_count.unwrap()).time( datetime.utcnow()))
async def send_initial_message(user_id: str, channel_id: str) -> None: base_url = f"http://{config.DB_URL}/ignore/" answer = try_request(container.logger, r.get, base_url, params={"author_id": user_id}) if answer.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=answer.unwrap_err()) return answer = answer.unwrap() ignore_list = answer.json() template = container.jinja_env.get_template("ignore.json") result = template.render(ignore_list=ignore_list) await container.slacker.post_to_channel(channel_id=channel_id, blocks=result, ephemeral=True, user_id=user_id)
async def __process_preset_deletion(data: dict, channel_id: str, user_id: str): preset_name = data.get("actions", [{}])[0].get("value", "") base_url = f"http://{config.DB_URL}/preset/" if preset_name is None: await container.slacker.post_to_channel(channel_id=channel_id, text=PRESET_NOT_SPECIFIED) return answer = try_request(container.logger, r.delete, base_url, params={ 'name': preset_name, 'user_id': user_id }) if answer.is_ok(): text = PRESET_DELETED.format(preset_name) else: text = PRESET_DELETION_FAILED.format(preset_name) await container.slacker.post_to_channel(channel_id=channel_id, text=text)
async def send_initial_message(user_id: str, channel_id: str) -> None: base_url = f"http://{config.DB_URL}/timer/" answer = try_request(container.logger, r.get, base_url, params={"username": user_id}) if answer.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=DATABASE_INTERACTION_ERROR) return answer = answer.unwrap() timers = answer.json() timers = [Timer( channel_id=x['channel_id'], username=x['username'], timer_name=x['timer_name'], delta=timedelta(seconds=x['delta']), next_start=datetime.fromisoformat(x['next_start']), top_command=x['top_command'] ) for x in timers] template = container.jinja_env.get_template("timer_list.json") result = template.render(timers=timers) await container.slacker.post_to_channel(channel_id=channel_id, blocks=result, ephemeral=True, user_id=user_id)
async def preset_interaction(data: dict): action_id = data.get("actions", [{}])[0].get("action_id", "") channel_id = data.get('channel', {}).get("id", "") user_id = data.get("user", {}).get("id", "") if action_id == "preset_new": await __show_preset_creation(data) elif data.get("type", "") == "view_submission" and \ data.get("view", {}).get("callback_id", "") == "preset_new_submission": await __process_preset_creation(data, user_id) elif action_id == "preset_delete": await __process_preset_deletion(data, channel_id, user_id) else: container.logger.warning(f"Unknown preset interaction message: {data}") preset_count = try_request( container.logger, r.get, f"http://{config.DB_URL}/preset/count").map(lambda x: x.json()) if preset_count.is_ok(): INFLUX_API_WRITE( Point("digestbot").field("presets_total", preset_count.unwrap()).time( datetime.utcnow()))
async def __process_timer_creation(data: dict, channel_id: str, user_id: str): def __parse_amount_unit(_amount: str, _unit: str) -> Result[int, str]: """Return seconds from parsed period""" multipliers = { "hour": 60 * 60, "day": 60 * 60 * 24, "week": 60 * 60 * 24 * 7 } _amount = int(_amount) if _unit not in multipliers: capture_message(f"Received unknown unit type: {_unit}") return Err("Internal error.") return Ok(_amount * multipliers[_unit]) values = data['state']['values'] # parse common params timer_parameters = top_parser( amount=values['timer_message_amount']['timer_message_amount'], sorting_type=values['timer_sorting_selector']['timer_sorting_selector'], preset=values['timer_preset_selector']['timer_preset_selector'], user_id=user_id ) if timer_parameters.is_err(): # error returned await container.slacker.post_to_channel(channel_id=channel_id, text=timer_parameters.unwrap_err()) return timer_parameters = timer_parameters.unwrap() # parse message period amount = values['timer_message_period_picker']['timer_period_amount']['selected_option']['value'] unit = values['timer_message_period_picker']['timer_period_unit']['selected_option']['value'] result = __parse_amount_unit(amount, unit) if result.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=result.unwrap_err()) return timer_parameters['message_period_seconds'] = result.unwrap() # parse timer period amount = values['timer_period_picker']['timer_period_amount']['selected_option']['value'] unit = values['timer_period_picker']['timer_period_unit']['selected_option']['value'] result = __parse_amount_unit(amount, unit) if result.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=result.unwrap_err()) return timer_delta = timedelta(seconds=result.unwrap()) # find user's timezone user_info = await container.slacker.get_user_info(user_id=user_id) tz_offset = user_info.get('tz_offset', 0) # parse timer initial date start_time = values['timer_begin_picker']['delta_timepicker']['selected_time'] start_date = values['timer_begin_picker']['delta_datepicker']['selected_date'] selected_datetime = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M") selected_datetime -= timedelta(seconds=tz_offset) if selected_datetime < datetime.utcnow(): await container.slacker.post_to_channel(channel_id=channel_id, text=TIMER_START_ERROR_FUTURE) return new_timer = Timer( channel_id=channel_id, username=user_id, timer_name=str(uuid.uuid4()), delta=timer_delta, next_start=selected_datetime, top_command=json.dumps(timer_parameters) ) data = json.dumps(new_timer.dict(), cls=TimerEncoder) answer = try_request(container.logger, r.post, f"http://{config.DB_URL}/timer/", data=data) if answer.is_err(): await container.slacker.post_to_channel(channel_id=channel_id, text=TIMER_CREATION_FAILED) return INFLUX_API_WRITE(Point("digestbot").field("timer_created", 1).time(datetime.utcnow())) await container.slacker.post_to_channel( channel_id=channel_id, text=TIMER_CREATED.format(new_timer.timer_name, new_timer.next_start.isoformat()) ) return
async def process_timers(logger: Logger, ui_service: str, db_service: str): db_base_url = f"http://{db_service}/timer/" ui_base_url = f"http://{ui_service}/internal/" while True: time_border = (datetime.utcnow() - timedelta(minutes=OVERDUE_MINUTES)).isoformat() # get nearest timer to execute answer = try_request(logger, r.get, db_base_url + "nearest", params={ "time_border": time_border }).map(lambda x: x.json()) if answer.is_err() or answer.value is None: await asyncio.sleep(300) continue nearest_timer: dict = answer.value # sleep until that time if time - current_time > 0 run_time = datetime.fromisoformat(nearest_timer['next_start']) now = datetime.utcnow() run_delta = run_time - now run_delta = run_delta.total_seconds() if run_delta > 5: await asyncio.sleep( min(run_delta - 3, 300)) # if nearest in 1 year and someone will add another continue # let's get timer again just in case if user deleted it already # run top command and return result to the user request_parameters = json.loads(nearest_timer['top_command']) message_period = timedelta( seconds=request_parameters['message_period_seconds']) after_ts = str( time.mktime((datetime.utcnow() - message_period).timetuple())) request_parameters['after_ts'] = after_ts next_time = datetime.fromisoformat( nearest_timer['next_start']) + timedelta( seconds=nearest_timer['delta']) request_parameters['next_time'] = next_time request_parameters['user_id'] = nearest_timer['username'] # post top request try_request(logger, r.post, ui_base_url + "top", data=json.dumps( { "channel_id": nearest_timer['channel_id'], "request_parameters": request_parameters }, cls=TimerEncoder)) new_timer = Timer( channel_id=nearest_timer['channel_id'], username=nearest_timer['username'], timer_name=nearest_timer['timer_name'], delta=nearest_timer['delta'], next_start=next_time, top_command=nearest_timer['top_command'], ) try_request(logger, r.patch, db_base_url + "next_start", data=json.dumps(new_timer.dict(), cls=TimerEncoder)) INFLUX_API_WRITE( Point("digestbot").field("timers_processed", 1).time(datetime.utcnow()))
async def __process_preset_creation(data: dict, user_id: str): base_url = f"http://{config.DB_URL}/preset/" # get preset name and channels data = data.get("view", {}).get("state", {}).get("values", {}) preset_name = data.get("preset_name", {}).get("title", {}).get("value", "") channels = data.get("channels_selector", {}).get("channels", {}).get("selected_channels", []) # check that preset_name does not exist and channels are not empty answer = try_request(container.logger, r.get, base_url, params={ "user_id": user_id, "include_global": False }) if answer.is_err(): await container.slacker.post_to_channel( channel_id=user_id, text=DATABASE_INTERACTION_ERROR) return answer = answer.unwrap() if preset_name in {x.get("name", "") for x in answer.json()}: await container.slacker.post_to_channel(channel_id=user_id, text=PRESET_ALREADY_EXISTS) return if not channels: await container.slacker.post_to_channel( channel_id=user_id, text=NO_CHANNELS_PASSED_MESSAGE) return # check whether user will override global presets answer = try_request(container.logger, r.get, base_url, params={"include_global": True}) if answer.is_err(): await container.slacker.post_to_channel( channel_id=user_id, text=DATABASE_INTERACTION_ERROR) return answer = answer.unwrap() user_answer = "" if preset_name in {x.get("name", "") for x in answer.json()}: user_answer += PRESET_OVERRIDE_WARNING_MESSAGE user_answer += "\n" answer = try_request(container.logger, r.put, base_url, params={ 'user_id': user_id, 'name': preset_name }, data=json.dumps(channels)) if answer.is_ok(): user_answer += PRESET_CREATED.format(preset_name) INFLUX_API_WRITE( Point("digestbot").field("preset_created", 1).time(datetime.utcnow())) else: user_answer += PRESET_NOT_CREATED.format(preset_name) await container.slacker.post_to_channel(channel_id=user_id, text=user_answer)