예제 #1
0
def request_clock_out_description(update: Update, context: CallbackContext):
    chat_id = update.effective_chat.id
    try:
        user_info = get_user_info(chat_id)
        msg_id = int(user_info['msg_reply_id'])
        clock_out_date = user_info['clock_out_date']
    except KeyError:
        return

    if not update.effective_message.reply_to_message or update.effective_message.reply_to_message.message_id != msg_id:
        try:
            update.effective_message.bot.delete_message(chat_id=chat_id,
                                                        message_id=msg_id)
        except BadRequest:
            logger.warn(
                'The message to be replied has been deleted by the user, but we\'ll try again'
            )
        msg = send_markup_msg(update,
                              strings()['feedback:exit:insist_reply'],
                              ForceReply(), True)
        set_clock_out_msg_reply_id(chat_id, msg.message_id)
        raise DispatcherHandlerStop
    else:
        replied_entry_description(chat_id, update.effective_message.text,
                                  clock_out_date)
        send_markup_msg(update,
                        strings()['feedback:exit:acknowledgment'],
                        options_kbd(strings()))
        remove_msg_reply_id(chat_id)
        raise DispatcherHandlerStop
예제 #2
0
def request_language_setup(update: Update, context: CallbackContext) -> None:
    setup_state, _ = get_user_states(str(update.effective_chat.id))
    if setup_state is not SetupState.LANGUAGE_NOT_SET:
        logger.error('request_language_setup being called when state is not LANGUAGE_NOT_SET')
        return None

    if update.callback_query is None or update.callback_query.data not in _callback_query_options:
        reply_markup = InlineKeyboardMarkup(_lang_inline_keyboard_buttons)
        send_markup_msg(update, strings()["language:choose"], reply_markup)
    else:
        query = update.callback_query

        lang = Language(query.data.split("#")[1])
        if lang is Language.ENGLISH:
            set_strings(force_lang=Language.ENGLISH)
        else:
            set_strings(force_lang=Language.PORTUGUESE)

        set_language(chat_id=str(update.callback_query.message.chat.id),
                     lang=lang,
                     next_state=SetupState.TIMEZONE_NOT_SET)

        try:
            set_timezone(str(update.effective_chat.id), int(os.environ.get('TIMEZONE_SECONDS_OFFSET', None)),
                         SetupState.NONE)
            send_markup_msg(update, strings()['setup:complete'], options_kbd(strings()))
        except Exception as e:
            logger.warn(e)
            logger.warn('TIMEZONE_SECONDS_OFFSET not present or in wrong format.\n'
                        'Switching to fallback mode with google maps api.')
            edit_message(update, strings()['language:set'])
            send_message(update, strings()['timezone:request'])

    raise DispatcherHandlerStop
예제 #3
0
def validate_edited_registries(update: Update, context: CallbackContext):
    try:
        chat_id = update.effective_chat.id
        user_info = get_user_info(chat_id)
        editing_day = datetime.fromisoformat(user_info['editing_day'])
        offset = user_info['utc_delta_seconds']
        _now = datetime.utcnow() + timedelta(seconds=int(offset))

        split_message = (update.effective_message.text + '\n').split('\n')
        i = (list(g) for _, g in groupby(split_message, key=''.__ne__))
        chunks = [a + b for a, b in zip(i, i)]
        last_clock_out = editing_day - timedelta(microseconds=1)
        entries = []
        for chunk in chunks:
            _e, last_clock_out = parse_and_validate_chunk(chunk, last_clock_out, editing_day, chat_id)
            entries = [*entries, *_e]

        # delete before inserting new ones of that day
        delete_that_day_entries(chat_id, editing_day)
        list(map(lambda _e: create_full_entry(**_e), entries))
        send_markdown_msg(update, strings()['edit:done'])
        _cancel_edit(chat_id)
        raise DispatcherHandlerStop
    except DispatcherHandlerStop:
        raise
    except Exception as e:
        logger.error(f"User sent data in wrong format {e}")
        msg = "\n".join([
            strings()['edit:request:entry:wrong_format'],
            strings()['edit:request:model'],
            strings()['edit:suggest:cancel'],
        ])
        send_markdown_msg(update, msg)
    finally:
        raise DispatcherHandlerStop
예제 #4
0
def options_inline_kdb():
    _inline_keyboard_buttons = [
        [InlineKeyboardButton(strings()['button:edit'], callback_data='option#edit')],
        [InlineKeyboardButton(strings()['button:help'], callback_data='option#help')],
        [InlineKeyboardButton(strings()['button:source'], callback_data='option#source')],
        [InlineKeyboardButton(strings()['button:delete_all'], callback_data='option#delete')],
    ]
    return InlineKeyboardMarkup(_inline_keyboard_buttons)
예제 #5
0
def help_inline_kdb():
    _inline_keyboard_buttons = [
        [InlineKeyboardButton(strings()['button:help:clockin'], callback_data='help#clockin')],
        [InlineKeyboardButton(strings()['button:help:report'], callback_data='help#report')],
        [InlineKeyboardButton(strings()['button:help:edit'], callback_data='help#edit')],
        [InlineKeyboardButton(strings()['button:help:deleteall'], callback_data='help#deleteall')],
        [InlineKeyboardButton(strings()['button:help:issue'], callback_data='help#issue')],
    ]
    return InlineKeyboardMarkup(_inline_keyboard_buttons)
예제 #6
0
def start(update: Update, context: CallbackContext):
    _, chat_state = get_user_states(str(update.effective_message.chat_id))
    if chat_state in _sensitive_states:
        logger.debug(f"Not letting user perform a restart because they're in {chat_state} state")
        send_markup_msg(update, strings()['start:prohibit'], options_kbd(strings()))
    else:
        create_or_reset_user(str(update.message.chat_id))
        request_language_setup(update, context)
    raise DispatcherHandlerStop
예제 #7
0
def handle_chosen_option(update: Update, context: CallbackContext):
    if update.callback_query.data == "option#edit":
        return edit_choice_selector(update, context)

    elif update.callback_query.data == "option#help":
        return edit_message(update, strings()['help'], True, help_inline_kdb())

    elif update.callback_query.data == "option#source":
        return edit_message(update, strings()['source'], True)

    elif update.callback_query.data == "option#delete":
        return delete(update, context)
예제 #8
0
def clockin(update: Update, context: CallbackContext) -> None:
    chat_id = str(update.message.chat_id)
    _date = clock_in(chat_id)
    entries = today_entries(chat_id)
    if len(entries) % 2 == 0:
        msg = send_markup_msg(update,
                              strings()['feedback:exit'], ForceReply(), True)
        set_unreported_clock_out(chat_id, msg.message_id, _date)
        raise DispatcherHandlerStop
    else:
        send_message(update, strings()['feedback:entrance'])
        raise DispatcherHandlerStop
예제 #9
0
def show_report(update: Update, context: CallbackContext) -> None:
    msg = generate_report(update, context)
    if msg is None:
        edit_message(update, strings()['report:choice:empty'])
        raise DispatcherHandlerStop
    try:
        edit_message(update, msg, True)
    except BadRequest:
        edit_message(update, strings()['report:choice:too_many_chars'])
        send_document(update, bytes(remove_regxx(msg), 'utf-8'),
                      f"{update.callback_query.data}.txt")
    finally:
        raise DispatcherHandlerStop
def generate_reply(update: Update, context: CallbackContext, clocked_in_date: str) -> None:
    chat_id = update.effective_chat.id
    that_day_entries = _that_day_entries(chat_id, clocked_in_date)

    compiled_entries = [
        strings()['request:warning'],
        datetime.fromisoformat(clocked_in_date).strftime('%d/%m/%Y') + '\n',
        compile_entries(that_day_entries) + '\n',
        strings()['request:missing_entry']
    ]

    msg = send_markup_msg(update, "\n".join(compiled_entries), ForceReply(), True)
    set_msg_reply_id(chat_id, msg.message_id)
예제 #11
0
def edit_entry_request(update: Update, context: CallbackContext, entries: List[dict]):
    chat_id = update.effective_chat.id
    compiled_entries = compile_entries(entries)
    if len(compiled_entries) == 0:
        send_markdown_msg(update, strings()['edit:request:empty'])
    else:
        send_markdown_msg(update, compiled_entries)
        send_markdown_msg(update, strings()['edit:request:not_empty'])
    send_markdown_msg(update, strings()['edit:request:instructions:1'])
    send_markdown_msg(update, strings()['edit:request:model'])
    send_markdown_msg(update, strings()['edit:request:instructions:2'])
    set_chat_state(chat_id, ChatState.AWAITING_EDITED_REGISTRIES)
    raise DispatcherHandlerStop
예제 #12
0
def handle_help(update: Update, context: CallbackContext):
    if update.callback_query.data == 'help#clockin':
        edit_message(update, strings()['help:clockin'], True)

    elif update.callback_query.data == 'help#report':
        edit_message(update, strings()['help:report'], True)

    elif update.callback_query.data == 'help#edit':
        edit_message(update, strings()['help:edit'], True)

    elif update.callback_query.data == 'help#deleteall':
        edit_message(update, strings()['help:deleteall'], True)

    elif update.callback_query.data == 'help#issue':
        edit_message(update, strings()['help:issue'], True)

    raise DispatcherHandlerStop
예제 #13
0
def edit_choice_selector(update: Update, context: CallbackContext):
    kbd = [
        [InlineKeyboardButton(strings()['button:today'], callback_data='edit#today')],
        [InlineKeyboardButton(strings()['button:yesterday'], callback_data='edit#yesterday')],
        [InlineKeyboardButton(strings()['button:other'], callback_data='edit#other')],
    ]
    try:
        edit_message(update,
                     strings()['edit:choose:message'],
                     True,
                     InlineKeyboardMarkup(kbd))
    except BadRequest as e:
        send_markup_msg(
            update,
            strings()['edit:choose:message'],
            InlineKeyboardMarkup(kbd),
            True,
        )
    raise DispatcherHandlerStop
예제 #14
0
def validate_picked_day(update: Update, context: CallbackContext):
    try:
        chat_id = update.effective_chat.id
        day, month, year = map(int, update.effective_message.text.split('/'))
        _date = datetime.combine(date(year=year, month=month, day=day), time())
        entries = that_day_entries(chat_id, _date.isoformat())
        set_edit_day(chat_id, _date.isoformat())
        edit_entry_request(update, context, entries)
    except DispatcherHandlerStop:
        raise
    except Exception as e:
        logger.error(e)
        msg = "\n".join([
            strings()['edit:request:date:wrong_format'],
            strings()['edit:request:date_model'],
            strings()['edit:suggest:cancel'],
        ])
        send_markdown_msg(update, msg)
    finally:
        raise DispatcherHandlerStop
예제 #15
0
 class InlineKeyboardOptions(Enum):
     TODAY = strings()['button:today']
     YESTERDAY = strings()['button:yesterday']
     WEEK = strings()['button:report:week']
     MONTH = strings()['button:report:month']
     LAST_WEEK = strings()['button:report:last_week']
     LAST_MONTH = strings()['button:report:last_month']
def ensure_valid_clocked_in(update: Update, context: CallbackContext):
    # if the chat_state is clocked_in but there are no records today, this means that
    # the user forgot to clock out the other day.
    chat_id = update.effective_chat.id
    if len(today_entries(chat_id)) == 0:
        user_info = get_user_info(chat_id)
        clocked_in_date = datetime.fromisoformat(user_info['clocked_in_date'])

        # we are in an inconsistent state
        # check if there's any msg waiting for a reply
        # if not, create one
        try:
            msg_id = int(user_info['msg_reply_id'])
            if not update.effective_message.reply_to_message \
                    or update.effective_message.reply_to_message.message_id != msg_id:
                try:
                    update.effective_message.bot.delete_message(chat_id=chat_id, message_id=msg_id)
                except BadRequest:
                    logger.warn('The message to be replied has been deleted by the user, but we\'ll try again')
                generate_reply(update, context, clocked_in_date.isoformat())
                raise DispatcherHandlerStop

            else:
                msg = update.effective_message.text
                time = msg.split('\n')[0]
                if len(time) != 5:  # hh:mm
                    raise ValueError('It should be in format hh:mm')

                hour, minute = int(time.split(':')[0]), int(time.split(':')[1])
                clock_out_date = clocked_in_date.replace(hour=hour, minute=minute,
                                                         microsecond=clocked_in_date.microsecond + 1).isoformat()
                description = msg.split('\n')[0].strip()

                if len(description) == 0:
                    raise ValueError('Description should not be empty')

                if clock_out_date < clocked_in_date.isoformat():
                    raise ValueError('Clock out date must be greater than the clock in date')

                create_full_entry(chat_id, clock_out_date, description)
                remove_msg_reply_id(chat_id)
                send_markup_msg(update, strings()['request:acknowledgment'], options_kbd(strings()))
                raise DispatcherHandlerStop
        except DispatcherHandlerStop:
            raise DispatcherHandlerStop
        except Exception as e:
            logger.error(e)
            generate_reply(update, context, clocked_in_date.isoformat())
            raise DispatcherHandlerStop
        finally:
            raise DispatcherHandlerStop
예제 #17
0
def user_selected_edit_day(update: Update, context: CallbackContext):
    chat_id = update.effective_chat.id
    user_info = get_user_info(chat_id)
    offset = user_info['utc_delta_seconds']
    today = datetime.combine(datetime.utcnow() + timedelta(seconds=int(offset)), time())
    if update.callback_query.data == 'edit#today':
        entries = today_entries(chat_id)
        if len(entries) % 2 != 0:
            send_markdown_msg(update, strings()['edit:incomplete_day'])
            raise DispatcherHandlerStop
        set_edit_day(chat_id, today.isoformat())
        edit_entry_request(update, context, entries)

    elif update.callback_query.data == 'edit#yesterday':
        entries = yesterday_entries(chat_id)
        set_edit_day(chat_id, (today - timedelta(days=1)).isoformat())
        edit_entry_request(update, context, entries)

    elif update.callback_query.data == 'edit#other':
        send_markdown_msg(update, strings()['edit:request_day'])
        set_chat_state(chat_id, ChatState.AWAITING_EDIT_DAY)

    raise DispatcherHandlerStop
예제 #18
0
def request_timezone_setup(update: Update, context: CallbackContext) -> None:
    state, _ = get_user_states(str(update.effective_chat.id))
    if state is not SetupState.TIMEZONE_NOT_SET:
        logger.error(
            'request_timezone_setup being called when state is not TIMEZONE_NOT_SET'
        )
        return

    if update.message is not None:
        try:
            tz = get_address_and_timezone_by_name(update.message.text)
            is_negative = "-" if tz['offset'] < 0 else ""
            pretty_offset = is_negative + str(
                timedelta(seconds=abs(tz['offset'])))[:-3]
            _inline_keyboard_buttons = [[
                InlineKeyboardButton(
                    strings()['button:yes'],
                    callback_data=f"timezone#yes#{pretty_offset}"),
                InlineKeyboardButton(strings()['button:no'],
                                     callback_data='timezone#no'),
            ]]
            reply_markup = InlineKeyboardMarkup(_inline_keyboard_buttons)
            tz_msg = f"regx*x{tz['timezone_id']}regx*x \nregx*xUTC{pretty_offset}regx*x\n"
            full_msg = strings()["timezone:location:confirm"].format(tz_msg)
            send_markup_msg(update, full_msg, reply_markup, True)
        except LocationNotFoundException:
            send_message(update, strings()['timezone:location:not_found'])
    else:
        query = update.callback_query
        if not any(cb_option in query.data
                   for cb_option in _callback_query_options):
            send_message(update, strings()['timezone:request'])
        else:
            if 'timezone#no' in query.data:
                send_message(update, strings()['timezone:request'])
            else:
                pretty_offset = query.data.split('#')[-1]
                hours, minutes = map(int, pretty_offset.split(':'))
                minutes = minutes if hours >= 0 else minutes * (-1)
                offset = (hours * 60 * 60) + (minutes * 60)
                set_timezone(str(update.effective_chat.id), offset,
                             SetupState.NONE)
                send_markup_msg(update,
                                strings()['setup:complete'],
                                options_kbd(strings()))
    raise DispatcherHandlerStop
예제 #19
0
def choose_report(update: Update, context: CallbackContext) -> None:
    class InlineKeyboardOptions(Enum):
        TODAY = strings()['button:today']
        YESTERDAY = strings()['button:yesterday']
        WEEK = strings()['button:report:week']
        MONTH = strings()['button:report:month']
        LAST_WEEK = strings()['button:report:last_week']
        LAST_MONTH = strings()['button:report:last_month']

    inline_keyboard_buttons = [
        [
            InlineKeyboardButton(InlineKeyboardOptions.TODAY.value,
                                 callback_data="report#today")
        ],
        [
            InlineKeyboardButton(InlineKeyboardOptions.YESTERDAY.value,
                                 callback_data="report#yesterday")
        ],
        [
            InlineKeyboardButton(InlineKeyboardOptions.WEEK.value,
                                 callback_data="report#week")
        ],
        [
            InlineKeyboardButton(InlineKeyboardOptions.MONTH.value,
                                 callback_data="report#month")
        ],
        [
            InlineKeyboardButton(InlineKeyboardOptions.LAST_WEEK.value,
                                 callback_data="report#last_week")
        ],
        [
            InlineKeyboardButton(InlineKeyboardOptions.LAST_MONTH.value,
                                 callback_data="report#last_month")
        ],
    ]

    reply_markup = InlineKeyboardMarkup(inline_keyboard_buttons)
    send_markup_msg(update, strings()["report:period:choose"], reply_markup)
    raise DispatcherHandlerStop
예제 #20
0
def delete(update: Update, context: CallbackContext):
    _del_inline_keyboard_buttons = [
        [
            InlineKeyboardButton(strings()['button:yes'],
                                 callback_data='delete#yes')
        ],
        [
            InlineKeyboardButton(strings()['button:no'],
                                 callback_data='delete#no')
        ],
    ]

    _callback_query_options = ['delete#yes', 'delete#no']

    chat_id = update.effective_chat.id
    if update.callback_query is None or update.callback_query.data not in _callback_query_options:
        reply_markup = InlineKeyboardMarkup(_del_inline_keyboard_buttons)
        send_markup_msg(update,
                        strings()["delete:confirm"], reply_markup, True)
        raise DispatcherHandlerStop
    else:
        if update.callback_query.data == 'delete#yes':
            deleted_user_entries = db.delete_user_entries(chat_id)
            deleted_user_info = db.delete_user_info(chat_id)
            if not deleted_user_entries and not deleted_user_info:
                update.effective_message.delete()
                send_markup_msg(
                    update,
                    strings()['delete:nothing'],
                    ReplyKeyboardMarkup([['/start']],
                                        resize_keyboard=True,
                                        one_time_keyboard=True))
            else:
                update.effective_message.delete()
                send_markup_msg(
                    update,
                    strings()['delete:success'],
                    ReplyKeyboardMarkup([['/start']],
                                        resize_keyboard=True,
                                        one_time_keyboard=True))
            raise DispatcherHandlerStop
        else:
            edit_message(update, strings()['delete:cancelled'])
            raise DispatcherHandlerStop
예제 #21
0
def request_start(update: Update, context: CallbackContext):
    send_message(update, strings()["request:start"])
    raise DispatcherHandlerStop
예제 #22
0
def report_compiler(_entries: list):
    if len(_entries) == 0:
        return None

    # Converting entry iso dates to datetime objects
    def _ec(_e):
        _e['date'] = datetime.fromisoformat(_e['date'])

    # map just creates a map object, you have to iterate over it to actually do the mapping
    list(map(_ec, _entries))

    # Sorting entries based on date
    entries = list(sorted(_entries, key=lambda k: k['date']))

    # Ensuring an even length to loop through
    # Screw the last clock in event that has no clock out yet
    final = len(entries) if len(entries) % 2 == 0 else len(entries) - 1
    entries = entries[:final]

    if len(entries) == 0:
        return None

    # Setup
    total_accum = timedelta(seconds=0)
    day_accum = timedelta(seconds=0)
    paragraphs = []
    phrases = []
    curr_day: datetime = entries[0]['date']

    def insert_day_total():
        phrases.insert(
            0,
            f"regx*x{curr_day.strftime('%d/%m/%Y')} - {strings()[curr_day.strftime('%A')]}regx*x"
        )
        phrases.append(
            f"regx_xregx_x{strings()['report:total_of_day']}: "
            f"regx*x{seconds_to_str(day_accum.total_seconds())}regx*xregx_xregx_x\n\n"
        )

    # The loop.
    for i in range(0, final, 2):
        f_entry = entries[i]
        s_entry = entries[i + 1]

        if curr_day.day != f_entry['date'].day:
            insert_day_total()
            paragraph = "\n".join(phrases)
            paragraphs.append(paragraph)
            # cleanup
            day_accum = timedelta(seconds=0)
            curr_day = f_entry['date']
            phrases = []

        partial_accum = s_entry['date'] - f_entry['date']
        day_accum += partial_accum
        total_accum += partial_accum

        description = s_entry[
            'description'] if 'description' in s_entry else strings(
            )['report:description:not_found']
        phrase = f"{f_entry['date'].strftime('%H:%M')} - {s_entry['date'].strftime('%H:%M')} => " \
                 f"regx*x{seconds_to_str(partial_accum.total_seconds())}regx*x\n" \
                 f"regx_x{description}regx_x"
        phrases.append(phrase)

    insert_day_total()
    paragraph = "\n".join(phrases)
    paragraphs.append(paragraph)

    paragraphs.append(
        f"{strings()['report:total_of_period']}: "
        f"regx*x{seconds_to_str(total_accum.total_seconds())}regx*x\n\n")

    return "\n\n".join(paragraphs)
예제 #23
0
def help_message(update: Update, context: CallbackContext):
    send_markup_msg(update, strings()['help'], help_inline_kdb(), True)
    raise DispatcherHandlerStop
예제 #24
0
def options(update: Update, context: CallbackContext):
    send_markup_msg(update, strings()['options:choose'], options_inline_kdb())
    raise DispatcherHandlerStop
예제 #25
0
def default(update: Update, context: CallbackContext):
    send_markup_msg(update, strings()['default'], options_kbd(strings()))
    raise DispatcherHandlerStop
예제 #26
0
def cancel_edit(update: Update, context: CallbackContext):
    _cancel_edit(update.effective_chat.id)
    send_markdown_msg(update, strings()['cancelled'])
    raise DispatcherHandlerStop