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
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
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
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)
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)
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
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)
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
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)
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
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
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
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
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
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
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
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
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
def request_start(update: Update, context: CallbackContext): send_message(update, strings()["request:start"]) raise DispatcherHandlerStop
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)
def help_message(update: Update, context: CallbackContext): send_markup_msg(update, strings()['help'], help_inline_kdb(), True) raise DispatcherHandlerStop
def options(update: Update, context: CallbackContext): send_markup_msg(update, strings()['options:choose'], options_inline_kdb()) raise DispatcherHandlerStop
def default(update: Update, context: CallbackContext): send_markup_msg(update, strings()['default'], options_kbd(strings())) raise DispatcherHandlerStop
def cancel_edit(update: Update, context: CallbackContext): _cancel_edit(update.effective_chat.id) send_markdown_msg(update, strings()['cancelled']) raise DispatcherHandlerStop