예제 #1
0
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))
예제 #2
0
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.")
예제 #3
0
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)
예제 #4
0
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()))
예제 #5
0
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])
예제 #6
0
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
예제 #7
0
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]
예제 #8
0
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()))
예제 #9
0
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)
예제 #10
0
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)
예제 #11
0
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)
예제 #12
0
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()))
예제 #13
0
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
예제 #14
0
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()))
예제 #15
0
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)