async def me(self, ctx, arg: str = None): settings = cf_common.user_db.get_reminder_settings(ctx.guild.id) if settings is None: await ctx.send(embed=discord_common.embed_alert( 'To use this command, reminder settings must be set by an admin' )) return _, role_id, _ = settings role = ctx.guild.get_role(int(role_id)) if role is None: await ctx.send(embed=discord_common.embed_alert( 'The role set for reminders is no longer available')) return if arg is None: if role in ctx.author.roles: await ctx.send(embed=discord_common.embed_neutral( 'You are already subscribed to contest reminders')) return await ctx.author.add_roles( role, reason='User subscribed to contest reminders') await ctx.send(embed=discord_common.embed_success( 'Successfully subscribed to contest reminders')) elif arg == 'not': if role not in ctx.author.roles: await ctx.send(embed=discord_common.embed_neutral( 'You are not subscribed to contest reminders')) return await ctx.author.remove_roles( role, reason='User unsubscribed from contest reminders') await ctx.send(embed=discord_common.embed_success( 'Successfully unsubscribed from contest reminders'))
async def settings(self, ctx): """Shows the role, channel and before time settings.""" settings = cf_common.user_db.get_reminder_settings(ctx.guild.id) if settings is None: await ctx.send( embed=discord_common.embed_neutral('Reminder not set')) return channel_id, role_id, before = settings channel_id, role_id, before = int(channel_id), int( role_id), json.loads(before) channel, role = ctx.guild.get_channel(channel_id), ctx.guild.get_role( role_id) if channel is None: await ctx.send(embed=discord_common.embed_alert( 'The channel set for reminders is no longer available')) return if role is None: await ctx.send(embed=discord_common.embed_alert( 'The role set for reminders is no longer available')) return before_str = ', '.join(str(before_mins) for before_mins in before) embed = discord_common.embed_success('Current reminder settings') embed.add_field(name='Channel', value=channel.mention) embed.add_field(name='Role', value=role.mention) embed.add_field(name='Before', value=f'At {before_str} mins before contest') await ctx.send(embed=embed)
async def _show_ranklist(self, channel, contest_id: int, handles: [str], ranklist, vc: bool = False, delete_after: float = None): contest = cf_common.cache2.contest_cache.get_contest(contest_id) if ranklist is None: raise ContestCogError('No ranklist to show') handle_standings = [] for handle in handles: try: standing = ranklist.get_standing_row(handle) except rl.HandleNotPresentError: continue # Database has correct handle ignoring case, update to it # TODO: It will throw an exception if this row corresponds to a team. At present ranklist doesnt show teams. # It should be fixed in https://github.com/cheran-senthil/TLE/issues/72 handle = standing.party.members[0].handle if vc and standing.party.participantType != 'VIRTUAL': continue handle_standings.append((handle, standing)) if not handle_standings: error = f'None of the handles are present in the ranklist of `{contest.name}`' if vc: await channel.send(embed=discord_common.embed_alert(error), delete_after=delete_after) return raise ContestCogError(error) handle_standings.sort(key=lambda data: data[1].rank) deltas = None if ranklist.is_rated: deltas = [ranklist.get_delta(handle) for handle, standing in handle_standings] problem_indices = [problem.index for problem in ranklist.problems] pages = self._make_standings_pages(contest, problem_indices, handle_standings, deltas) paginator.paginate(self.bot, channel, pages, wait_time=_STANDINGS_PAGINATE_WAIT_TIME, delete_after=delete_after)
async def cog_command_error(self, ctx, error): if isinstance(error, GraphCogError): await ctx.send(embed=discord_common.embed_alert(error)) error.handled = True return await cf_common.cf_handle_error_handler(ctx, error) await cf_common.run_handle_coro_error_handler(ctx, error)
async def remove(self, ctx, original_message_id: int): """Remove a particular message from the starboard database.""" rc = cf_common.user_db.remove_starboard_message(original_msg_id=original_message_id) if rc: await ctx.send(embed=discord_common.embed_success('Successfully removed')) else: await ctx.send(embed=discord_common.embed_alert('Not found in database'))
async def cog_command_error(self, ctx, error): if isinstance( error, (ContestCogError, rl.RanklistError, cache_system2.CacheError)): await ctx.send(embed=discord_common.embed_alert(error)) error.handled = True return await cf_common.resolve_handle_error_handler(ctx, error)
async def ratedvc(self, ctx, contest_id: int, *members: discord.Member): ratedvc_channel_id = cf_common.user_db.get_rated_vc_channel( ctx.guild.id) if not ratedvc_channel_id or ctx.channel.id != ratedvc_channel_id: raise ContestCogError( 'You must use this command in ratedvc channel.') if not members: raise ContestCogError('Missing members') contest = cf_common.cache2.contest_cache.get_contest(contest_id) try: (await cf.contest.ratingChanges(contest_id=contest_id ))[_MIN_RATED_CONTESTANTS_FOR_RATED_VC - 1] except (cf.RatingChangesUnavailableError, IndexError): error = ( f'`{contest.name}` was not rated for at least {_MIN_RATED_CONTESTANTS_FOR_RATED_VC} contestants' ' or the ratings changes are not published yet.') raise ContestCogError(error) ongoing_vc_member_ids = _get_ongoing_vc_participants() this_vc_member_ids = {str(member.id) for member in members} intersection = this_vc_member_ids & ongoing_vc_member_ids if intersection: busy_members = ", ".join([ ctx.guild.get_member(int(member_id)).mention for member_id in intersection ]) error = f'{busy_members} are registered in ongoing ratedvcs.' raise ContestCogError(error) handles = cf_common.members_to_handles(members, ctx.guild.id) visited_contests = await cf_common.get_visited_contests(handles) if contest_id in visited_contests: raise ContestCogError( f'Some of the handles: {", ".join(handles)} have submissions in the contest' ) start_time = time.time() finish_time = start_time + contest.durationSeconds + _RATED_VC_EXTRA_TIME cf_common.user_db.create_rated_vc(contest_id, start_time, finish_time, ctx.guild.id, [member.id for member in members]) title = f'Starting {contest.name} for:' msg = "\n".join( f'[{discord.utils.escape_markdown(handle)}]({cf.PROFILE_BASE_URL}{handle})' for handle in handles) embed = discord_common.cf_color_embed(title=title, description=msg, url=contest.url) await ctx.send(embed=embed) embed = discord_common.embed_alert( f'You have {int(finish_time - start_time) // 60} minutes to complete the vc!' ) embed.set_footer(text='GL & HF') await ctx.send(embed=embed)
async def withdraw(self, ctx): active = cf_common.user_db.check_duel_withdraw(ctx.author.id) if not active: raise DuelCogError( f'{ctx.author.mention}, you are not challenging anyone.') duelid, challengee = active challengee = ctx.guild.get_member(challengee) cf_common.user_db.cancel_duel(duelid, Duel.WITHDRAWN) message = f'{ctx.author.mention} withdrew a challenge to `{challengee.mention}`.' embed = discord_common.embed_alert(message) await ctx.send(embed=embed)
async def decline(self, ctx): active = cf_common.user_db.check_duel_decline(ctx.author.id) if not active: raise DuelCogError( f'{ctx.author.mention}, you are not being challenged!') duelid, challenger = active challenger = ctx.guild.get_member(challenger) cf_common.user_db.cancel_duel(duelid, Duel.DECLINED) message = f'`{ctx.author.mention}` declined a challenge by {challenger.mention}.' embed = discord_common.embed_alert(message) await ctx.send(embed=embed)
async def here(self, ctx, role: discord.Role, *before: int): """Sets reminder channel to current channel, role to the given role, and reminder times to the given values in minutes.""" if not role.mentionable: await ctx.send(embed=discord_common.embed_alert( 'The role for reminders must be mentionable')) return if not before or any(before_mins <= 0 for before_mins in before): return cf_common.user_db.set_reminder_settings(ctx.guild.id, ctx.channel.id, role.id, json.dumps(before)) await ctx.send(embed=discord_common.embed_success( 'Reminder settings saved successfully')) self._reschedule_tasks(ctx.guild.id)
async def _watch_rated_vc(self, vc_id: int): vc = cf_common.user_db.get_rated_vc(vc_id) channel_id = cf_common.user_db.get_rated_vc_channel(vc.guild_id) if channel_id is None: raise ContestCogError('No Rated VC channel') channel = self.bot.get_channel(int(channel_id)) member_ids = cf_common.user_db.get_rated_vc_user_ids(vc_id) handles = [cf_common.user_db.get_handle(member_id, channel.guild.id) for member_id in member_ids] handle_to_member_id = {handle : member_id for handle, member_id in zip(handles, member_ids)} now = time.time() ranklist = await cf_common.cache2.ranklist_cache.generate_vc_ranklist(vc.contest_id, handle_to_member_id) async def has_running_subs(handle): return [sub for sub in await cf.user.status(handle=handle) if sub.verdict == 'TESTING' and sub.problem.contestId == vc.contest_id and sub.relativeTimeSeconds <= vc.finish_time - vc.start_time] running_subs_flag = any([await has_running_subs(handle) for handle in handles]) if running_subs_flag: msg = 'Some submissions are still being judged' await channel.send(embed=discord_common.embed_alert(msg), delete_after=_WATCHING_RATED_VC_WAIT_TIME) if now < vc.finish_time or running_subs_flag: # Display current standings await channel.send(embed=self._make_contest_embed_for_vc_ranklist(ranklist, vc.start_time, vc.finish_time), delete_after=_WATCHING_RATED_VC_WAIT_TIME) await self._show_ranklist(channel, vc.contest_id, handles, ranklist=ranklist, vc=True, delete_after=_WATCHING_RATED_VC_WAIT_TIME) return rating_change_by_handle = {} RatingChange = namedtuple('RatingChange', 'handle oldRating newRating') for handle, member_id in zip(handles, member_ids): delta = ranklist.delta_by_handle.get(handle) if delta is None: # The user did not participate. cf_common.user_db.remove_last_ratedvc_participation(member_id) continue old_rating = cf_common.user_db.get_vc_rating(member_id) new_rating = old_rating + delta rating_change_by_handle[handle] = RatingChange(handle=handle, oldRating=old_rating, newRating=new_rating) cf_common.user_db.update_vc_rating(vc_id, member_id, new_rating) cf_common.user_db.finish_rated_vc(vc_id) await channel.send(embed=self._make_vc_rating_changes_embed(channel.guild, vc.contest_id, rating_change_by_handle)) await self._show_ranklist(channel, vc.contest_id, handles, ranklist=ranklist, vc=True)
async def challenge(self, ctx, opponent: discord.Member, *args): """Challenge another server member to a duel. Problem difficulty will be the lesser of duelist ratings minus 400. You can alternatively specify a different rating. The duel will be unrated if specified rating is above the default value or tags are used to choose a problem. The challenge expires if ignored for 5 minutes.""" challenger_id = ctx.author.id challengee_id = opponent.id await cf_common.resolve_handles( ctx, self.converter, ('!' + str(ctx.author), '!' + str(opponent))) userids = [challenger_id, challengee_id] handles = [ cf_common.user_db.get_handle(userid, ctx.guild.id) for userid in userids ] submissions = [ await cf.user.status(handle=handle) for handle in handles ] if not cf_common.user_db.is_duelist(challenger_id): raise DuelCogError( f'{ctx.author.mention}, you are not a registered duelist!') if not cf_common.user_db.is_duelist(challengee_id): raise DuelCogError( f'{opponent.mention} is not a registered duelist!') if challenger_id == challengee_id: raise DuelCogError( f'{ctx.author.mention}, you cannot challenge yourself!') if cf_common.user_db.check_duel_challenge(challenger_id): raise DuelCogError( f'{ctx.author.mention}, you are currently in a duel!') if cf_common.user_db.check_duel_challenge(challengee_id): raise DuelCogError(f'{opponent.mention} is currently in a duel!') tags = cf_common.parse_tags(args, prefix='+') bantags = cf_common.parse_tags(args, prefix='~') rating = cf_common.parse_rating(args) users = [cf_common.user_db.fetch_cf_user(handle) for handle in handles] lowest_rating = min(user.rating or 0 for user in users) suggested_rating = max( round(lowest_rating, -2) + _DUEL_RATING_DELTA, 500) rating = round(rating, -2) if rating else suggested_rating unofficial = rating > suggested_rating or tags or bantags dtype = DuelType.UNOFFICIAL if unofficial else DuelType.OFFICIAL solved = { sub.problem.name for subs in submissions for sub in subs if sub.verdict != 'COMPILATION_ERROR' } seen = { name for userid in userids for name, in cf_common.user_db.get_duel_problem_names(userid) } def get_problems(rating): return [ prob for prob in cf_common.cache2.problem_cache.problems if prob.rating == rating and prob.name not in solved and prob.name not in seen and not any( cf_common.is_contest_writer(prob.contestId, handle) for handle in handles) and not cf_common.is_nonstandard_problem(prob) and prob. matches_all_tags(tags) and not prob.matches_any_tag(bantags) ] for problems in map(get_problems, range(rating, 400, -100)): if problems: break rstr = f'{rating} rated ' if rating else '' if not problems: raise DuelCogError( f'No unsolved {rstr}problems left for {ctx.author.mention} vs {opponent.mention}.' ) problems.sort(key=lambda problem: cf_common.cache2.contest_cache. get_contest(problem.contestId).startTimeSeconds) choice = max(random.randrange(len(problems)) for _ in range(2)) problem = problems[choice] issue_time = datetime.datetime.now().timestamp() duelid = cf_common.user_db.create_duel(challenger_id, challengee_id, issue_time, problem, dtype) ostr = 'an **unofficial**' if unofficial else 'a' await ctx.send( f'{ctx.author.mention} is challenging {opponent.mention} to {ostr} {rstr}duel!' ) await asyncio.sleep(_DUEL_EXPIRY_TIME) if cf_common.user_db.cancel_duel(duelid, Duel.EXPIRED): message = f'{ctx.author.mention}, your request to duel {opponent.mention} has expired!' embed = discord_common.embed_alert(message) await ctx.send(embed=embed)
async def cog_command_error(self, ctx, error): if isinstance(error, StarboardCogError): await ctx.send(embed=discord_common.embed_alert(error)) error.handled = True
async def resolve_handle_error_handler(ctx, error): if isinstance(error, ResolveHandleError): await ctx.send(embed=discord_common.embed_alert(error)) error.handled = True
async def run_handle_coro_error_handler(ctx, error): if isinstance(error, RunHandleCoroFailedError): await ctx.send(embed=discord_common.embed_alert(error)) error.handled = True
async def cf_handle_error_handler(ctx, error): if isinstance(error, CodeforcesHandleError): await ctx.send(embed=discord_common.embed_alert(error)) error.handled = True
async def ranklist(self, ctx, contest_id: int, *handles: str): """Shows ranklist for the contest with given contest id. If handles contains '+server', all server members are included. No handles defaults to '+server'. """ contest = cf_common.cache2.contest_cache.get_contest(contest_id) wait_msg = None try: ranklist = cf_common.cache2.ranklist_cache.get_ranklist(contest) deltas_status = 'Predicted' except cache_system2.RanklistNotMonitored: if contest.phase == 'BEFORE': raise ContestCogError( f'Contest `{contest.id} | {contest.name}` has not started') wait_msg = await ctx.send('Please wait...') ranklist = await cf_common.cache2.ranklist_cache.generate_ranklist( contest.id, fetch_changes=True) deltas_status = 'Final' handles = set(handles) if not handles: handles.add('+server') if '+server' in handles: handles.remove('+server') guild_handles = [ handle for discord_id, handle in cf_common.user_db.get_handles_for_guild(ctx.guild.id) ] handles.update(guild_handles) handles = await cf_common.resolve_handles(ctx, self.member_converter, handles, maxcnt=100) handle_standings = [] for handle in handles: try: standing = ranklist.get_standing_row(handle) except rl.HandleNotPresentError: continue handle_standings.append((handle, standing)) if not handle_standings: msg = f'None of the handles are present in the ranklist of `{contest.name}`' await ctx.send(embed=discord_common.embed_alert(msg)) return handle_standings.sort(key=lambda data: data[1].rank) deltas = None if ranklist.is_rated: deltas = [ ranklist.get_delta(handle) for handle, standing in handle_standings ] problem_indices = [problem.index for problem in ranklist.problems] pages = self._make_standings_pages(contest, problem_indices, handle_standings, deltas) embed = discord_common.cf_color_embed(title=contest.name, url=contest.url) phase = contest.phase.capitalize().replace('_', ' ') embed.add_field(name='Phase', value=phase) if ranklist.is_rated: embed.add_field(name='Deltas', value=deltas_status) if wait_msg: try: await wait_msg.delete() except: pass await ctx.send(embed=embed) paginator.paginate(self.bot, ctx.channel, pages, wait_time=_STANDINGS_PAGINATE_WAIT_TIME)
async def _rating_hist(self, ctx, ratings, mode, binsize, title): if mode not in ('log', 'normal'): await ctx.send(embed=discord_common.embed_alert( 'Mode should be either `log` or `normal`.')) return ratings = [max(r, 0) for r in ratings] assert 100 % binsize == 0 # because bins is semi-hardcoded bins = 39 * 100 // binsize colors = [] low, high = 0, binsize * bins for rank in cf.RATED_RANKS: for r in range(max(rank.low, low), min(rank.high, high), binsize): colors.append('#' + '%06x' % rank.color_embed) assert len( colors) == bins, f'Expected {bins} colors, got {len(colors)}' height = [0] * bins for r in ratings: height[r // binsize] += 1 csum = 0 cent = [] users = sum(height) for h in height: csum += h cent.append(round(100 * csum / users)) x = [k * binsize for k in range(bins)] label = [f'{r} ({c})' for r, c in zip(x, cent)] l, r = 0, bins - 1 while not height[l]: l += 1 while not height[r]: r -= 1 x = x[l:r + 1] cent = cent[l:r + 1] label = label[l:r + 1] colors = colors[l:r + 1] height = height[l:r + 1] plt.clf() fig = plt.figure(figsize=(15, 5)) plt.xticks(rotation=45) plt.xlim(l * binsize - binsize // 2, r * binsize + binsize // 2) plt.bar(x, height, binsize * 0.9, color=colors, linewidth=0, tick_label=label, log=(mode == 'log')) plt.xlabel('Rating') plt.ylabel('Number of users') discord_file = _get_current_figure_as_file() embed = discord_common.cf_color_embed(title=title) discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file) plt.close(fig)