def get_subject_timetable(subject, subtype, attendance, language_code): """get a timetable for specified subject""" # dict of {weekday: {'online': tt1, 'offline': tt2}} tt = SERVER.get_subject_timetable(subject, subtype, attendance) # no timetable for this subject if not tt: return get_text('no_subject_timetable_header_text', language_code=language_code).text() weekday_list = [] for weekday, dct in tt.items(): # get subheader weekday_name = get_text(f'{weekday}_timetable_text', language_code=language_code).text() # make timetable subject_timetable = __put_together(template=SUBJECT_TEMPLATE_WITH_PARITY, language_code=language_code, **dct) if not subject_timetable: continue day_template = get_text('subject_day_template_text', language_code=language_code).text({ consts.TIMETABLE: subject_timetable, consts.WEEKDAY: weekday_name, }) weekday_list.append(day_template) header = get_text('subject_timetable_header_text', language_code=language_code).text() return header + '\n'.join(weekday_list)
def report_sent(update: Update, context: CallbackContext): """take message to report and send it to all admins""" language_code = update.effective_user.language_code chat_id = update.effective_chat.id data = { 'user': mention_html(update.effective_user.id, update.effective_user.first_name), } for admin_id in database.get_all_admins_chat_ids(): cf.send_message( context=context, chat_id=admin_id, text=get_text('report_template_text', language_code).text(data), ) context.bot.forward_message( chat_id=admin_id, from_chat_id=chat_id, message_id=update.message.message_id, ) cf.send_message( context=context, chat_id=chat_id, text=get_text('report_sent_text', language_code).text(), ) return consts.MAIN_STATE
def subject_keyboard(subject, page, attendance, language_code): """Make keyboard for subject pages: main and timetable page""" # current page, attendance and subject current_state = { consts.PAGE: page, consts.ATTENDANCE: attendance, consts.SUBJECT: subject } # invert states not_page = util.to_not_help_page(page) not_attendance = util.to_not_attendance(attendance) attendance_callback = buttons.SUBJECT % dict(current_state, attendance=not_attendance) page_callback = buttons.SUBJECT % dict(current_state, page=not_page) keyboard = [[ # buttons names and callback according to current states InlineKeyboardButton( text=get_text(f'subject_{not_page}_page_button', language_code).text(), callback_data=page_callback, ), InlineKeyboardButton( text=get_text(f'subject_{not_attendance}_attendance_button', language_code).text(), callback_data=attendance_callback, ), ]] return InlineKeyboardMarkup(inline_keyboard=keyboard)
def pretty_deadlines(deadlines, language_code): lst = [] for subject, dl in deadlines: lst.append( get_text('dl_template_text', language_code).text({ consts.DEADLINE: re.sub('\n', ' ', dl), consts.SUBJECT: subject, })) if not lst: return get_text('no_deadlines_text', language_code).text() return '\n\n'.join(lst)
def timetable_keyboard(weekday, attendance, week_parity, language_code): """ Make weekday timetable key board 3 rows of buttons: 2 for weekdays and 1 to change attendance and week_parity """ # for weekday buttons current_state is the same except weekday current_state = { consts.ATTENDANCE: attendance, consts.WEEK_PARITY: week_parity, consts.WEEKDAY: weekday, } # invert attendance and week_parity not_attendance = util.to_not_attendance(attendance) not_week_parity = util.to_not_week_parity(week_parity) # for attendance and week parity buttons callback changing respectively, weekday stays the same attendance_callback = buttons.TIMETABLE_BUTTON % dict( current_state, attendance=not_attendance) week_parity_callback = buttons.TIMETABLE_BUTTON % dict( current_state, week_parity=not_week_parity) keyboard = [ [ make_timetable_button(consts.MONDAY, current_state, language_code), make_timetable_button(consts.TUESDAY, current_state, language_code), make_timetable_button(consts.WEDNESDAY, current_state, language_code), make_timetable_button(consts.THURSDAY, current_state, language_code), make_timetable_button(consts.FRIDAY, current_state, language_code), make_timetable_button(consts.SATURDAY, current_state, language_code), ], [ # buttons names and callback according to current states InlineKeyboardButton( text=get_text(f'timetable_{not_week_parity}_week_button', language_code).text(), callback_data=week_parity_callback, ), InlineKeyboardButton( text=get_text(f'timetable_{not_attendance}_attendance_button', language_code).text(), callback_data=attendance_callback, ), ] ] return InlineKeyboardMarkup(inline_keyboard=keyboard)
def __user_time_input_chg(update: Update, context: CallbackContext, validation, attr_name, error_state): """change mailing parameters with validation""" language_code = update.effective_user.language_code new_info = update.message.text user_id = update.effective_user.id chat_id = update.effective_chat.id # validate new_info. if False send error message and try again if validation(new_info): # update database database.set_user_attrs(user_id=user_id, attrs={attr_name: new_info}) # reset mailing job jobs.reset_mailing_job(context, user_id, chat_id, language_code) # get and send mailing page text, reply_markup = __get_mailing_page(user_id, language_code) cf.send_message( context=context, chat_id=chat_id, text=text, reply_markup=reply_markup, ) return consts.PARAMETERS_MAIN_STATE cf.send_message( context=context, chat_id=chat_id, text=get_text('format_error_parameters_text', language_code).text(), ) return error_state
def unknown_callback(update: Update, context: CallbackContext): """handles unknown callbacks""" language_code = update.effective_user.language_code cf.send_message(context=context, chat_id=update.effective_chat.id, text=get_text('unknown_callback_text', language_code).text())
def get_deadlines(day: str, utcoffset, language_code): """request deadlines from server and returns them as readable text""" today = tm.get_today(utcoffset) # str_day: date in readable format; day: datetime.date for requested day if day == consts.TODAY: day = today str_day = day.strftime(consts.DEADLINE_DAY_FORMAT) else: str_day = day day = datetime.strptime(day, consts.DEADLINE_DAY_FORMAT) day = day.replace(year=today.year) deadlines, weekday = SERVER.get_deadlines(day.toordinal()) template = get_text('deadline_text', language_code).text({ consts.DATE: str_day, consts.WEEKDAY: weekday, consts.DEADLINE: pretty_deadlines(deadlines, language_code), }) return template
def cancel_callback(update: Update, context: CallbackContext): """manage cancel button""" data, language_code = cf.manage_callback_query(update) parsed_data = data.split('_') if parsed_data[0] == consts.CANCEL: cf.edit_message( update=update, text=get_text('cancel_main_text', update.effective_user.language_code).text(), ) return consts.MAIN_STATE cf.send_message( context=context, chat_id=update.effective_chat.id, text=get_text('wrong_callback_text', language_code).text(), )
def admin_ls(page_number, language_code): """list all users""" # get page of users (left/right bound) page_number = int(page_number) users = database.get_all_users() count = len(users) page_number = min((count - 1) // USERS_ON_PAGE, page_number) lb = page_number * USERS_ON_PAGE rb = lb + USERS_ON_PAGE text = get_text('ls_admin_text', language_code).text({ consts.USERS: '\n'.join(map(lambda pair: mention_html(pair[0], pair[1]), users[lb:rb])), consts.LB: lb + 1, consts.RB: min(rb, count), consts.TOTAL: count }) # detect page type if lb == 0 and rb >= count: page_type = consts.SINGLE_PAGE else: page_type = ( consts.FIRST_PAGE if lb == 0 else (consts.LAST_PAGE if rb >= count else consts.MIDDLE_PAGE) ) reply_markup = keyboard.admin_ls(page_number, page_type, language_code) return text, consts.MAIN_STATE, reply_markup
def admin_notify(update: Update, context: CallbackContext): """sends provided text to specified users""" language_code = update.effective_user.language_code user_nick = context.chat_data.pop('notify_username_admin', None) disable_notification = context.chat_data.pop('n_mode', None) notification_text = update.message.text # send notification if user_nick is not None: cf.send_notification( context=context, user_nick=user_nick, text=notification_text, disable_notification=disable_notification, language_code=language_code, ) else: cf.send_notification_to_all( context=context, sender_id=update.effective_user.id, text=notification_text, disable_notification=disable_notification, language_code=language_code, ) # notify sender that everything is ok cf.send_message( context=context, chat_id=update.effective_chat.id, text=get_text('notification_sent_notify_admin_text', language_code).text() ) return consts.MAIN_STATE
def timetable_args_error(context: CallbackContext, chat_id, error_type, language_code): """send argument error message""" cf.send_message( context=context, chat_id=chat_id, text=get_text('timetable_args_error_text', language_code).text({'error_type': error_type}), )
def error(update: Update, context: CallbackContext): language_code = update.effective_user.language_code cf.send_message( context=context, chat_id=update.effective_chat.id, text=get_text(f'{name}_parameters_error_text', language_code).text(), )
def admin_request_notify(context: CallbackContext, args, language_code): """send request for notification""" ret_lvl = consts.MAIN_STATE reply_markup = None if len(args) == 1: text = get_text('no_args_notify_admin_text', language_code).text() else: params = args[1].split('=') n_mode = False if len(args) >= 3: tr_flag, val = args[2].split('=') if tr_flag != '-s': text = get_text('invalid_flag_admin_text', language_code).text() return text, ret_lvl, reply_markup if val not in {'1', '0'}: text = get_text('invalid_flag_value_admin_text', language_code).text() return text, ret_lvl, reply_markup n_mode = val == '1' context.chat_data['n_mode'] = n_mode ret_lvl = consts.ADMIN_NOTIFY_STATE reply_markup = keyboard.cancel_operation(consts.ADMIN_NOTIFY_STATE)(language_code) if params[0] == '--all': text = get_text('all_users_notify_admin_text', language_code).text() elif params[0] == '--user': if len(params) == 2 and database.has_user(params[1]): context.chat_data['notify_username_admin'] = params[1] text = get_text('user_notify_admin_text', language_code).text() else: text = get_text('invalid_username_admin_text', language_code).text() else: ret_lvl, reply_markup = consts.MAIN_STATE, None text = get_text('invalid_flag_admin_text', language_code).text() return text, ret_lvl, reply_markup
def help_cmd(update: Update, context: CallbackContext): """help command callback""" language_code = update.effective_user.language_code cf.send_message( context=context, chat_id=update.effective_chat.id, text=get_text('help_main_text', language_code).text(), reply_markup=keyboard.help_keyboard(consts.MAIN_PAGE, language_code), )
def inner(language_code): keyboard = [ [ InlineKeyboardButton( text=get_text(buttons.CANCEL, language_code).text(), callback_data=buttons.CANCEL_CALLBACK % {'data': data}, ) ], ] return InlineKeyboardMarkup(inline_keyboard=keyboard)
def admin(update: Update, context: CallbackContext): """ admin's control panel current functions: '/admin [-ls]' - list of all users '/admin [-n <--user=user_nick | --all> [--s=<0 | 1>] ]' - send a notification to the specified user or to all users. Flag --s (silence) is set to 0 by default '/admin [-m |-um <--user=user_nick | --all>]' - mute/unmute reports from user """ language_code = update.effective_user.language_code args = context.args ret_lvl = consts.MAIN_STATE reply_markup = None # unauthorized user tries to access admin panel if not database.get_user_attr('admin', user_id=update.effective_user.id): text = get_text('unauthorized_user_admin_text', language_code).text() # empty args elif len(args) == 0: text = get_text('no_args_admin_text', language_code).text() # notifications elif args[0] == '-n': text, ret_lvl, reply_markup = admin_request_notify(context, args, language_code) # list of the users elif args[0] == '-ls': text, ret_lvl, reply_markup = admin_ls(0, language_code) # mute/unmute users elif args[0] == '-m' or args[0] == '-um': text, ret_lvl = admin_mute(args, language_code) else: text = get_text('invalid_flag_admin_text', language_code).text() cf.send_message( context=context, chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup, ) return ret_lvl
def report(update: Update, context: CallbackContext): """will wait for message to report if unmuted""" language_code = update.effective_user.language_code if database.get_user_attr(consts.MUTED, update.effective_user.id): text = get_text('cannot_send_report_text', language_code).text() ret_lvl = consts.MAIN_STATE reply_markup = None else: text = get_text('report_text', language_code).text() ret_lvl = consts.REPORT_MESSAGE_STATE reply_markup = keyboard.cancel_operation( consts.REPORT_STATE)(language_code) cf.send_message( context=context, chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup, ) return ret_lvl
def get_weekday_timetable(weekday: str, subject_names, attendance, week_parity, language_code, footer=None) -> str: """ makes timetable for specified day (subject_names: all user's subjects or subtypes) """ # checks if it is sunday if weekday == consts.SUNDAY: return get_text('today_sunday_text', language_code=language_code).text() # get text templates template = get_text('timetable_text', language_code) template.add_global_vars({ consts.WEEKDAY: get_text(f'{weekday}_timetable_text', language_code).text(), consts.WEEK_PARITY: get_text(f'{week_parity}_week_timetable_text', language_code=language_code).text(), consts.FOOTER: footer or '', }) # get timetables as dict subject_tt: dict = SERVER.get_weekday_timetable( weekday=weekday, valid_subject_names=subject_names, attendance=attendance, week_parity=week_parity ) online_tt, offline_tt = subject_tt['online'], subject_tt['offline'] # no subjects that day if not online_tt and not offline_tt: happy_text = get_text('happy_timetable_text', language_code=language_code).text() return template.text({consts.TIMETABLE: happy_text}) # make full timetable weekday_timetable = __put_together( online=online_tt, offline=offline_tt, template=SUBJECT_TEMPLATE, language_code=language_code, ) return template.text({consts.TIMETABLE: weekday_timetable})
def __get_mailing_page(user_id, language_code): """get mailing page attrs""" attrs = database.get_user_parameters(user_id) text = get_text('mailing_parameters_text', language_code).text( cf.pretty_user_parameters(attrs, language_code), ) reply_markup = keyboard.mailing_keyboard( mailing_status=attrs[consts.MAILING_STATUS], notification_status=attrs[consts.NOTIFICATION_STATUS], language_code=language_code, ) return text, reply_markup
def __return_callback(update: Update, language_code): """show main parameters page""" user_id = update.effective_user.id cf.edit_message( update=update, text=get_text('main_parameters_text', language_code).text( cf.pretty_user_parameters(database.get_user_parameters(user_id), language_code)), reply_markup=keyboard.parameters_keyboard(language_code), ) return consts.PARAMETERS_MAIN_STATE
def help_callback(update: Update, data: list, language_code): """change help page""" if data[1] in {consts.MAIN_PAGE, consts.ADDITIONAL_PAGE}: text = get_text(f'help_{data[1]}_text', language_code).text() else: raise ValueError(f'Invalid help callback: {data[0]}') cf.edit_message( update=update, text=text, reply_markup=keyboard.help_keyboard(data[1], language_code), )
def get_subject_info(subject, user_id, page, language_code, request: dict = None): """ Returns subject info and attendance as tuple - subject: name of the subject without subtype - user_id: id to get user parameters from database - page: main or timetable - request: dict with 2 values - attendance and subtype. By default user parameters are taken from database, but if the request has values, bot uses them """ if request is None: request = {} # default parameters subtype, attendance = database.get_user_attrs([subject, consts.ATTENDANCE], user_id=user_id).values() # substitutes with request parameters if exists attendance = request.get(consts.ATTENDANCE) or attendance subtype = request.get(consts.SUBTYPE) or subtype # select page to return if page == consts.TIMETABLE_PAGE: timetable = tt.get_subject_timetable(subject, subtype, attendance, language_code) return get_text('subject_timetable_text', language_code).text({ consts.TIMETABLE: timetable, }), attendance elif page == consts.MAIN_PAGE: return get_text(f'{subject}_subject_text', language_code).text({ consts.SUBTYPE: subtype, consts.ATTENDANCE: (attendance if attendance != consts.ATTENDANCE_BOTH else consts.ALL), }), attendance else: raise ValueError(f'Invalid subject page type: {page}')
def admin_mute(args, language_code): """mute/unmute users""" if len(args) < 2: text = get_text('empty_user_id_admin_text', language_code).text() else: if args[0] != '-m' and args[0] != '-um': raise ValueError(f'Invalid flag: {args[0]}') flag = consts.MUTE if args[0] == '-m' else consts.UNMUTE target = args[1].split('=') if target[0] == '--all': text = get_text(f'{flag}_all_users_admin_text', language_code).text() database.set_attr_to_all(consts.MUTED, flag == consts.MUTE) elif target[0] == '--user': if len(target) == 2 and database.has_user(target[1]): database.set_user_attrs(user_nick=target, attrs={consts.MUTED: flag == consts.MUTE}) text = get_text(f'{flag}_user_admin_text', language_code).text() else: text = get_text('invalid_username_admin_text', language_code).text() else: text = get_text('invalid_flag_admin_text', language_code).text() return text, consts.MAIN_STATE
def make_timetable_button(weekday, current_state, language_code): """ Make timetable button according to current page state current_state: dict with 3 parameters: attendance, week_parity, weekday """ return InlineKeyboardButton( text=get_text(buttons.WEEKDAY_BUTTON % { consts.WEEKDAY: weekday }, language_code).text(), callback_data=buttons.TIMETABLE_BUTTON % dict(current_state, weekday=weekday), )
def error_callback(update: Update, context: CallbackContext): """ Error callback function notifies user that error occurred, sends feedback to all admins """ language_code = update.effective_user.language_code # notify user cf.send_message(context=context, chat_id=update.effective_chat.id, text=get_text('error_handler_user_text', language_code).text()) # make logs logger.error( f'{str(context.error)}\n\n{"".join(traceback.format_tb(sys.exc_info()[2]))}' ) # collect data about error data = {'error': str(context.error)} if update.effective_user: data['user'] = mention_html(update.effective_user.id, update.effective_user.first_name) else: data['user'] = '******' if update.callback_query: data[ 'info'] = f'type = query\ncontent = "{update.callback_query.data}"' else: data['info'] = f'type = message\ncontent = "{update.message.text}"' text = get_text('error_handler_dev_text', language_code) # send collected data to all admins for dev_id in database.get_all_admins_chat_ids(): cf.send_message( context=context, chat_id=dev_id, text=text.text(data), )
def parameters(update: Update, context: CallbackContext): """start parameters conversation""" language_code = update.effective_user.language_code user_id = update.effective_user.id cf.send_message( context=context, chat_id=update.effective_chat.id, text=get_text('main_parameters_text', language_code).text( cf.pretty_user_parameters(database.get_user_parameters(user_id), language_code), ), reply_markup=keyboard.parameters_keyboard(language_code), ) return consts.PARAMETERS_MAIN_STATE
def inner(update: Update, context: CallbackContext): language_code = update.effective_user.language_code chat_id = update.effective_chat.id text = get_text(f'{name}_text', language_code).text() send_message( context=context, chat_id=chat_id, text=text, reply_markup=reply_markup, ) return ret_state
def pretty_user_parameters(user_parameters: dict, language_code): """make parameters readable for users""" retval = {} for attr_name, attr_value in user_parameters.items(): # attrs without modifications if (attr_name == consts.USERNAME and attr_value) or (attr_name == consts.MAILING_TIME): retval[attr_name] = attr_value continue # add sign to utcoffset elif attr_name == consts.UTCOFFSET: retval[attr_name] = (str(attr_value) if attr_value < 0 else f'+{attr_value}') continue # get readable values text = get_text(f'{attr_name}_{attr_value}_user_data_text', language_code).text() # attach group's number to general name if attr_name == consts.ENG and attr_value != consts.ALL: text = get_text('eng_std_user_data_text', language_code).text({consts.ENG: text}) retval[attr_name] = text return retval
def doc(update: Update, context: CallbackContext): """ show documentation if argument specified, shows docs for command shows special docs for admins """ language_code = update.effective_user.language_code args = context.args if_admin = database.get_user_attr('admin', user_id=update.effective_user.id) if len(args) > 2: text = get_text('quantity_error_doc_text', language_code).text() else: if len(args) == 0: text = get_text('doc_text', language_code).text({ 'command': consts.ALL, 'admin': if_admin }) else: if args[0] not in consts.DOC_COMMANDS: text = get_text('wrong_command_error_doc_text', language_code).text() else: text = get_text('doc_text', language_code).text({ 'command': args[0], 'admin': if_admin }) if not if_admin and args[0] == 'admin': text += get_text('doc_unavailable_text', language_code).text() cf.send_message( context=context, chat_id=update.effective_chat.id, text=text, )