async def _sub(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not context.args: await update.effective_message.reply_text("Укажи cups л🔥гин") return if len(context.args) > 1: await update.effective_message.reply_text( "Подписаться можно только на 1 cups л🔥гин") return login = context.chat_data.pop('battle_login', None) if login: battle_subs = context.bot_data.get('battle_subs', dict()) context.bot_data['battle_subs'] = battle_subs chats = battle_subs.get(login, set()) battle_subs[login] = chats chats.discard(update.effective_chat.id) login = context.args[0] context.chat_data['battle_login'] = login battle_subs = context.bot_data.get('battle_subs', dict()) context.bot_data['battle_subs'] = battle_subs chats = battle_subs.get('battle_subs', set()) battle_subs[login] = chats chats.add(update.effective_chat.id) await update.effective_message.reply_markdown( f"Подписка на системные игры `{login}` установлена")
async def select_feature(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Select a feature to update for the person.""" buttons = [[ InlineKeyboardButton(text="Name", callback_data=str(NAME)), InlineKeyboardButton(text="Age", callback_data=str(AGE)), InlineKeyboardButton(text="Done", callback_data=str(END)), ]] keyboard = InlineKeyboardMarkup(buttons) # If we collect features for a new person, clear the cache and save the gender if not context.user_data.get(START_OVER): context.user_data[FEATURES] = {GENDER: update.callback_query.data} text = "Please select a feature to update." await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) # But after we do that, we need to send a new message else: text = "Got it! Please select a feature to update." await update.message.reply_text(text=text, reply_markup=keyboard) context.user_data[START_OVER] = False return SELECTING_FEATURE
async def select_gender(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Choose to add mother or father.""" level = update.callback_query.data context.user_data[CURRENT_LEVEL] = level text = "Please choose, whom to add." male, female = _name_switcher(level) buttons = [ [ InlineKeyboardButton(text=f"Add {male}", callback_data=str(MALE)), InlineKeyboardButton(text=f"Add {female}", callback_data=str(FEMALE)), ], [ InlineKeyboardButton(text="Show data", callback_data=str(SHOWING)), InlineKeyboardButton(text="Back", callback_data=str(END)), ], ] keyboard = InlineKeyboardMarkup(buttons) await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_GENDER
async def end_second_level(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Return to top level conversation.""" context.user_data[START_OVER] = True await start(update, context) return END
async def _task_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query # CallbackQueries need to be answered, even if no notification to the user is needed # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery await query.answer() task_id = query.data.split()[1] task = allcups.task(task_id) contest = allcups.contest(task['contest']['slug']) context.chat_data['contest_slug'] = task['contest']['slug'] context.chat_data['task_id'] = task_id info_txt = msg_formatter.format_chat_info(contest, task) await query.edit_message_text(info_txt, parse_mode='markdown')
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Select an action: Adding parent/child or show data.""" text = ( "You may choose to add a family member, yourself, show the gathered data, or end the " "conversation. To abort, simply type /stop.") buttons = [ [ InlineKeyboardButton(text="Add family member", callback_data=str(ADDING_MEMBER)), InlineKeyboardButton(text="Add yourself", callback_data=str(ADDING_SELF)), ], [ InlineKeyboardButton(text="Show data", callback_data=str(SHOWING)), InlineKeyboardButton(text="Done", callback_data=str(END)), ], ] keyboard = InlineKeyboardMarkup(buttons) # If we're starting over we don't need to send a new message if context.user_data.get(START_OVER): await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) else: await update.message.reply_text( "Hi, I'm Family Bot and I'm here to help you gather information about your family." ) await update.message.reply_text(text=text, reply_markup=keyboard) context.user_data[START_OVER] = False return SELECTING_ACTION
async def list_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query await query.answer() # Get the data from the callback_data. # If you're using a type checker like MyPy, you'll have to use typing.cast # to make the checker get the expected type of the callback_data number, number_list = cast(Tuple[int, List[int]], query.data) # append the number to the list number_list.append(number) await query.edit_message_text( text=f"So far you've selected {number_list}. Choose the next item:", reply_markup=build_keyboard(number_list), ) # we can delete the data stored for the query, because we've replaced the buttons context.drop_callback_data(query)
async def regular_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Ask the user for info about the selected predefined choice.""" text = update.message.text context.user_data["choice"] = text await update.message.reply_text( f"Your {text.lower()}? Yes, I would love to hear about that!") return TYPING_REPLY
async def _unsub(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: login = context.chat_data.pop('battle_login', None) if login: battle_subs = context.bot_data.get('battle_subs', dict()) context.bot_data['battle_subs'] = battle_subs chats = battle_subs.get(login, set()) battle_subs[login] = chats chats.discard(update.effective_chat.id) await update.effective_message.reply_text( "Подписка на системные игры отключена")
async def ask_for_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Prompt user to input data for selected feature.""" context.user_data[CURRENT_FEATURE] = update.callback_query.data text = "Okay, tell me." await update.callback_query.answer() await update.callback_query.edit_message_text(text=text) return TYPING
async def adding_self(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Add information about yourself.""" context.user_data[CURRENT_LEVEL] = SELF text = "Okay, please tell me about yourself." button = InlineKeyboardButton(text="Add info", callback_data=str(MALE)) keyboard = InlineKeyboardMarkup.from_button(button) await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return DESCRIBING_SELF
async def _chat_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: usernames = set(context.args) if not usernames: await update.effective_message.reply_text("Ст🔥ит указ🔥ть ник") return cups_logins = context.chat_data.get('cups_logins', set()) cups_logins.difference_update(usernames) context.chat_data['cups_logins'] = cups_logins msg_txt = msg_formatter.chat_logins(cups_logins) await update.effective_message.reply_markdown(msg_txt)
async def regular_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Ask the user for info about the selected predefined choice.""" text = update.message.text.lower() context.user_data["choice"] = text if context.user_data.get(text): reply_text = ( f"Your {text}? I already know the following about that: {context.user_data[text]}" ) else: reply_text = f"Your {text}? Yes, I would love to hear about that!" await update.message.reply_text(reply_text) return TYPING_REPLY
async def received_information(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Store info provided by user and ask for the next category.""" text = update.message.text category = context.user_data["choice"] context.user_data[category] = text.lower() del context.user_data["choice"] await update.message.reply_text( "Neat! Just so you know, this is what you already told me:" f"{facts_to_str(context.user_data)}" "You can tell me more, or change your opinion on something.", reply_markup=markup, ) return CHOOSING
async def _set_contest(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.TYPING) if len(context.args) != 1: await update.effective_message.reply_text("🔥❓") return slug = context.args[0] contest = allcups.contest(slug) if contest is None: logger.warning(f"There is no contest with slug `{slug}`") await update.effective_message.reply_markdown( f"There is no c🔥ntest with slug `{slug}`") return context.chat_data['contest_slug'] = slug context.chat_data.pop('task_id', None) info_txt = msg_formatter.format_chat_info(contest, None) await update.effective_message.reply_markdown(info_txt)
async def games_notifications(context: ContextTypes.DEFAULT_TYPE) -> None: battles = allcups.battles() now = datetime.now(timezone.utc) # sent_battle_ids = context.bot_data.get('sent_battle_ids', dict()) # context.bot_data['sent_battle_ids'] = sent_battle_ids last_battle_ts = context.bot_data.get('last_battle_ts', dict()) context.bot_data['last_battle_ts'] = last_battle_ts # battle_updates = context.bot_data.get('battle_updates', dict()) # context.bot_data['battle_updates'] = battle_updates # battle_last_id = context.bot_data.get('battle_last_id', dict()) # context.bot_data['battle_last_id'] = battle_last_id context.bot_data.pop('battle_last_id', None) context.bot_data.pop('sent_battle_ids', None) for b in battles: end_date = datetime.fromisoformat(b['finish_date']) if now > end_date and b['slug'] != 'coderoyale': continue for r in b['rounds']: start_date = datetime.fromisoformat(r['start_date']) end_date = datetime.fromisoformat(r['finish_date']) if now < start_date or now > end_date: continue for t in r['tasks']: name = f"{b['name']}: {r['name']}: {t['name']}" last_ts = last_battle_ts.get(t['id'], None) last = datetime.fromtimestamp( last_ts, timezone.utc) if last_ts else None # sent_ids = sent_battle_ids.get(t['id'], set()) # battle_update = battle_updates.get(t['id'], None) # task_battles = allcups.battles_bot(t['id'], since=last) task_battles = allcups.battles_bot(t['id']) if not task_battles: continue ts = datetime.fromisoformat( task_battles[0]['updated_at']).timestamp() last_battle_ts[t['id']] = ts if last_ts is None: continue scores = allcups.task_leaderboard(t['id']) lb_scores = {} for s in scores: lb_scores[s['user']['login']] = { 'rank': s['rank'], 'score': s['score'], } for battle in task_battles[::-1]: if datetime.fromisoformat(battle['updated_at']) <= last: continue # if battle['id'] in sent_ids: # continue if battle['status'] != 'DONE': continue await _process_battle_results(battle, name, lb_scores, context)
async def _plot_logins(cups_logins, update: Update, context: ContextTypes.DEFAULT_TYPE, relative_login=None, relative_rank=None, display_rank=False, plot_type='step') -> None: task = allcups.task(context.chat_data['task_id']) display_field = 'rank' if display_rank else 'score' # context.bot_data.pop('history', None) history = context.bot_data.get('history', {}) context.bot_data['history'] = history task_history = history.get(context.chat_data['task_id'], []) history[context.chat_data['task_id']] = task_history ts = datetime.fromisoformat(task['start_date']) finish_date = datetime.fromisoformat(task['finish_date']) time_step = timedelta(minutes=15) now = datetime.now(timezone.utc) end = min(finish_date, now) if task_history: ts = datetime.fromtimestamp(task_history[-1]['ts'], timezone.utc) # TODO: Remove in future. while ts > finish_date: task_history.pop() ts = datetime.fromtimestamp(task_history[-1]['ts'], timezone.utc) time_limit = timedelta(days=2) # ts = max(now - time_limit, ts) ts += time_step while ts <= end: scores = allcups.task_leaderboard(context.chat_data['task_id'], ts) lb = [{ 'rank': s['rank'], 'login': s['user']['login'], 'score': s['score'] } for s in scores] task_history.append({'ts': ts.timestamp(), 'leaderboard': lb}) ts += time_step plt.clf() fig, ax = plt.subplots(1, 1, figsize=(15, 7)) # ax.tick_params(axis='x', rotation=0, labelsize=12) ax.tick_params(axis='y', rotation=0, labelsize=12, labelcolor='tab:red') myFmt = mdates.DateFormatter('%b %d %H:%M') ax.xaxis.set_major_formatter(myFmt) ax.grid(alpha=.9) task_history = [ h for h in task_history if now - datetime.fromtimestamp(h['ts'], timezone.utc) < time_limit ] dates = [ datetime.fromtimestamp(h['ts'], timezone.utc) for h in task_history ] plot_data = {} ls = set(cups_logins) if relative_login is not None: ls.add(relative_login) for login in ls: pd = [] plot_data[login] = pd for h in task_history: point = None for s in h['leaderboard']: if s['login'].lower() == login.lower(): point = s[display_field] break pd.append(point) if relative_login is not None and relative_login in plot_data: relative_data = list(plot_data[relative_login]) for login in cups_logins: login_data = plot_data[login] for i, rel_d in enumerate(relative_data): if login_data[i] is None or rel_d is None: login_data[i] = None else: login_data[i] -= rel_d plt.axhline(y=0.0, color='darkviolet', linestyle='--', label=relative_login) if relative_rank is not None: relative_data = [ h['leaderboard'][relative_rank - 1][display_field] if relative_rank <= len(h['leaderboard']) else None for h in task_history ] for login in cups_logins: login_data = plot_data[login] for i, rel_d in enumerate(relative_data): if login_data[i] is None or rel_d is None: login_data[i] = None else: login_data[i] -= rel_d plt.axhline(y=0.0, color='darkviolet', linestyle='--', label=f'rank={relative_rank}') for login in cups_logins: if relative_login is not None and login.lower( ) == relative_login.lower(): continue if plot_type == 'lines': plt.plot(dates, plot_data[login], label=login) else: plt.step(dates, plot_data[login], where='mid', label=login) plt.grid(color='0.95') plt.legend(fontsize=16, bbox_to_anchor=(1, 1), loc="upper left") if display_field == 'rank': plt.gca().invert_yaxis() plot_file = BytesIO() fig.tight_layout() fig.savefig(plot_file, format='png') plt.clf() plt.close(fig) plot_file.seek(0) await update.effective_message.reply_photo(plot_file, caption="🔥")
async def _process_battle_results(battle, name, lb_scores, context: ContextTypes.DEFAULT_TYPE) -> None: battle_subs = context.bot_data.get('battle_subs', dict()) logins = [r['user__login'] for r in battle['user_results']] for login in logins: if login not in battle_subs: continue scores = [] sorted_results = list( sorted(battle['user_results'], key=lambda x: x['score'], reverse=True)) win = False solution = -1 for i, r in enumerate(sorted_results): if login == r['user__login']: win = (i + 1) <= (len(sorted_results) // 2) solution = r['solution_id'] score = { 'rank': i + 1, 'login': r['user__login'], 'sub_flag': login == r['user__login'], 'score': r['score'], 'id': r['solution_id'], 'language': r['language__name'], } if r['user__login'] in lb_scores: score['lb_score'] = lb_scores[r['user__login']]['score'] score['lb_rank'] = lb_scores[r['user__login']]['rank'] else: score['lb_score'] = "-" score['lb_rank'] = "-" scores.append(score) msg_txt = msg_formatter.format_game(battle, name, scores, lb_scores[login], win, solution) replay_url = "https://cups.online" + battle['visualizer_url'] + "?" for br in battle['user_results']: replay_url += f"&player-names=" + urllib.parse.quote( br['user__login']) # replay_url += f"&player-names=" + urllib.parse.quote(names.get_name()) replay_url += f"&client-ids=" + urllib.parse.quote( str(br['solution__external_id'])) replay_url += f"&replay=" + urllib.parse.quote( battle['battle_result_file']) reply_markup = InlineKeyboardMarkup( [[InlineKeyboardButton(text='Watch Replay', url=replay_url)]]) blocked = [] for chat_id in battle_subs[login]: try: await context.bot.send_message(chat_id=chat_id, text=msg_txt, parse_mode='markdown', reply_markup=reply_markup, read_timeout=5, write_timeout=5, connect_timeout=5, pool_timeout=5) except Forbidden as e: logger.warning( f"Bot blocked by '{chat_id}' - removing subscription.") blocked.append(chat_id) context.application.chat_data[chat_id].pop( 'battle_login', None) except Exception as e: logger.warning(f"Error sending game subscription: {e}") time.sleep(1) if blocked: battle_subs[login] = [ s for s in battle_subs[login] if s not in blocked ] context.bot_data['battle_subs'] = battle_subs