class NoContestsAvailableException(Exception): onlineJudges = OnlineJudges() def __init__(self, oj=None): self.oj = oj def __str__(self): if self.oj is None: return 'Sorry, there are not upcoming contests currently available.' return 'Sorry, there are not upcoming contests from %s currently available.' % self.onlineJudges.formal_names[ self.oj]
class ProblemCog(commands.Cog): problems_by_points = {'dmoj':{}, 'codeforces':{}, 'atcoder':{}, 'peg':{}} dmoj_problems = None cf_problems = None at_problems = None cses_problems = {} # peg_problems = {} szkopul_problems = {} dmoj_sessions = {} cf_sessions = {} dmoj_user_suggests = {} cf_user_suggests = {} szkopul_page = 1 language = Language() onlineJudges = OnlineJudges() def __init__(self, bot): self.bot = bot self.refresh_dmoj_problems.start() self.refresh_cf_problems.start() self.refresh_atcoder_problems.start() self.refresh_cses_problems.start() # self.refresh_peg_problems.start() self.refresh_szkopul_problems.start() self.logout_offline.start() def parse_dmoj_problems(self, problems): if problems is not None: self.dmoj_problems = problems self.problems_by_points['dmoj'] = {} for name, details in problems.items(): if details['points'] not in self.problems_by_points['dmoj']: self.problems_by_points['dmoj'][details['points']] = {} self.problems_by_points['dmoj'][details['points']][name] = details def parse_cf_problems(self, cf_data): if cf_data is not None: try: self.cf_problems = cf_data['result']['problems'] self.problems_by_points['codeforces'] = {} for details in self.cf_problems: if 'points' in details.keys(): if details['points'] not in self.problems_by_points['codeforces']: self.problems_by_points['codeforces'][details['points']] = [] self.problems_by_points['codeforces'][details['points']].append(details) except KeyError: pass def parse_atcoder_problems(self, problems): if problems is not None: self.atcoder_problems = problems self.problems_by_points['atcoder'] = {} for details in problems: if details['point']: if details['point'] not in self.problems_by_points['atcoder']: self.problems_by_points['atcoder'][details['point']] = [] self.problems_by_points['atcoder'][details['point']].append(details) def parse_cses_problems(self, problems): if problems.status_code == 200: soup = bs.BeautifulSoup(problems.text, 'lxml') task_lists = soup.findAll('ul', attrs={'class' : 'task-list'}) task_groups = soup.findAll('h2') self.cses_problems = {} for index in range(1, len(task_groups)): tasks = task_lists[index].findAll('li', attrs={'class' : 'task'}) for task in tasks: name = task.find('a').contents[0] url = 'https://cses.fi' + task.find('a').attrs['href'] id = url.split('/')[-1] rate = task.find('span', attrs={'class' : 'detail'}).contents[0] group = task_groups[index].contents[0] cses_data = { 'id': id, 'name': name, 'url': url, 'rate': rate, 'group': group } self.cses_problems[id] = cses_data # def parse_peg_problems(self, problems): # if problems.status_code == 200: # soup = bs.BeautifulSoup(problems.text, 'lxml') # table = soup.find('table', attrs={'class' : 'nicetable stripes'}).findAll('tr') # self.peg_problems = {} # self.problems_by_points['peg'] = {} # for prob in range(1, len(table)): # values = table[prob].findAll('td') # name = values[0].find('a').contents[0] # url = 'https://wcipeg.com/problem/' + values[1].contents[0] # points_value = values[2].contents[0] # partial = 'p' in points_value # points = int(points_value.replace('p', '')) # p_users = values[3].find('a').contents[0] # ac_rate = values[4].contents[0] # date = values[5].contents[0] # peg_data = { # 'name': name, # 'url': url, # 'partial': partial, # 'points': points, # 'users': p_users, # 'ac_rate': ac_rate, # 'date': date # } # self.peg_problems[name] = peg_data # if points not in self.problems_by_points['peg']: # self.problems_by_points['peg'][points] = [] # self.problems_by_points['peg'][points].append(peg_data) def parse_szkopul_problems(self): problems = requests.get('https://szkopul.edu.pl/problemset/?page=%d' % self.szkopul_page) if problems.status_code == 200: soup = bs.BeautifulSoup(problems.text, 'lxml') rows = soup.findAll('tr') if self.szkopul_page == 1: self.szkopul_problems = {} if len(rows) == 1: return for row in rows: data = row.findAll('td') if data == []: continue id = data[0].contents[0] title = data[1].find('a').contents[0] url = 'https://szkopul.edu.pl' + data[1].find('a').attrs['href'] tags = [] for tag in data[2].findAll('a'): tags.append(tag.contents[0]) submitters = data[3].contents[0] problem_data = { 'id': id, 'title': title, 'url': url, 'tags': tags, 'submitters': submitters } if int(submitters) > 0: problem_data['percent_correct'] = data[4].contents[0] problem_data['average'] = data[5].contents[0] self.szkopul_problems[id] = problem_data self.szkopul_page += 1 def embed_dmoj_problem(self, name, prob, suggested=False): embed = discord.Embed() url = 'https://dmoj.ca/problem/' + name embed.set_thumbnail(url='https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/dmoj-thumbnail.png') embed.add_field(name='Points', value=prob['points'], inline=False) embed.add_field(name='Partials', value=('Yes' if prob['partial'] else 'No'), inline=False) embed.add_field(name='Group', value=prob['group'], inline=False) return ('[:thumbsup: SUGGESTED] ' if suggested else '') + prob['name'], url, embed def embed_cf_problem(self, prob, suggested=False): embed = discord.Embed() url = 'https://codeforces.com/problemset/problem/' + str(prob['contestId']) + '/' + str(prob['index']) embed.set_thumbnail(url='https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/cf-thumbnail.png') embed.add_field(name='Type', value=prob['type'], inline=False) if 'points' in prob.keys(): embed.add_field(name='Points', value=prob['points'], inline=False) if 'rating' in prob.keys(): embed.add_field(name='Rating', value=prob['rating'], inline=False) embed.add_field(name='Tags', value='||'+', '.join(prob['tags'])+'||', inline=False) return ('[:thumbsup: SUGGESTED] ' if suggested else '') + prob['name'], url, embed def embed_atcoder_problem(self, prob): embed = discord.Embed() url = 'https://atcoder.jp/contests/' + prob['contest_id'] + '/tasks/' + prob['id'] embed.set_thumbnail(url='https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/at-thumbnail.png') if prob['point']: embed.add_field(name='Points', value=prob['point'], inline=False) embed.add_field(name='Solver Count', value=prob['solver_count'], inline=False) return prob['title'], url, embed def embed_cses_problem(self, prob): embed = discord.Embed() embed.set_thumbnail(url='https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/cses-thumbnail.png') embed.add_field(name='Success Rate', value=prob['rate'], inline=False) embed.add_field(name='Group', value='||' + prob['group'] + '||', inline=False) return prob['name'], prob['url'], embed # def embed_peg_problem(self, prob): # embed = discord.Embed() # embed.set_thumbnail(url='https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/peg-thumbnail.png') # embed.add_field(name='Points', value=prob['points'], inline=False) # embed.add_field(name='Partials', value=('Yes' if prob['partial'] else 'No'), inline=False) # embed.add_field(name='Users', value=prob['users'], inline=False) # embed.add_field(name='AC Rate', value=prob['ac_rate'], inline=False) # embed.add_field(name='Date Added', value=prob['date'], inline=False) # return prob['name'], prob['url'], embed def embed_szkopul_problem(self, prob): embed = discord.Embed() embed.set_thumbnail(url='https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/szkopul-thumbnail.png') if len(prob['tags']) > 0: embed.add_field(name='Tags', value=', '.join(prob['tags']), inline=False) embed.add_field(name='Submitters', value=prob['submitters'], inline=False) if 'percent_correct' in prob: embed.add_field(name='% Correct', value=prob['percent_correct'], inline=False) if 'average' in prob: embed.add_field(name='Average', value=prob['average'], inline=False) return prob['title'], prob['url'], embed def get_problem(self, oj, contest_id=None, problem_id=None, szkopul_url=''): oj = self.onlineJudges.get_oj(oj) if oj != 'szkopul' and problem_id is None: raise ProblemNotFoundException if oj == 'dmoj': if problem_id not in self.dmoj_problems.keys(): raise ProblemNotFoundException title, description, embed = self.embed_dmoj_problem(problem_id, self.dmoj_problems[problem_id]) embed.title = title embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() return embed elif oj == 'codeforces': if contest_id is None: raise ProblemNotFoundException def is_problem(prob): return prob['contestId'] == int(contest_id) and prob['index'] == problem_id problist = list(filter(is_problem, self.cf_problems)) if len(problist) == 0: raise ProblemNotFoundException title, description, embed = self.embed_cf_problem(problist[0]) embed.title = title embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() return embed elif oj == 'atcoder': if contest_id is None: raise ProblemNotFoundException def is_problem(prob): return prob['contest_id'] == contest_id and prob['id'] == problem_id problist = list(filter(is_problem, self.atcoder_problems)) if len(problist) == 0: raise ProblemNotFoundException title, description, embed = self.embed_atcoder_problem(problist[0]) embed.title = title embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() return embed # elif oj == 'peg': # def is_problem(prob): # return prob['url'] == 'https://wcipeg.com/problem/' + problem_id # problist = list(filter(is_problem, list(self.peg_problems.values()))) # if len(problist) == 0: # raise ProblemNotFoundException # title, description, embed = self.embed_peg_problem(problist[0]) # embed.title = title # embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) # embed.timestamp = datetime.utcnow() # return embed elif oj == 'cses': if problem_id not in self.cses_problems.keys(): raise CSESProblemNotFoundException title, description, embed = self.embed_cses_problem(self.cses_problems[problem_id]) embed.title = title embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() return embed elif oj == 'szkopul': if szkopul_url == '': raise ProblemNotFoundException def is_problem(prob): return prob['url'] == szkopul_url problist = list(filter(is_problem, list(self.szkopul_problems.values()))) if len(problist) == 0: raise ProblemNotFoundException title, description, embed = self.embed_szkopul_problem(problist[0]) embed.title = title embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() return embed def get_problem_from_url(self, url): components = url.split('/') if url[:24] == 'https://dmoj.ca/problem/' and len(components) == 5: return self.get_problem('dmoj', problem_id=components[4]) elif url[:42] == 'https://codeforces.com/problemset/problem/' and len(components) == 7: return self.get_problem('codeforces', components[5], components[6]) elif url[:28] == 'https://atcoder.jp/contests/' and len(components) == 7 and components[5] == 'tasks': return self.get_problem('atcoder', components[4], components[6]) elif url[:27] == 'https://wcipeg.com/problem/' and len(components) == 5: return self.get_problem('peg', problem_id=components[4]) elif url[:32] == 'https://cses.fi/problemset/task/' and len(components) == 6: return self.get_problem('cses', problem_id=components[5]) elif url[:42] == 'https://szkopul.edu.pl/problemset/problem/': return self.get_problem('szkopul', szkopul_url=url) else: raise InvalidURLException def get_random_problem(self, oj=None, points=None, maximum=None, iden=None): if oj is None: oj = rand.choice(self.onlineJudges.judges) oj = self.onlineJudges.get_oj(oj) if oj == 'cses' and points is not None: raise InvalidParametersException(cses=True) temp_dmoj_problems = {} temp_cf_problems = [] user_data = query.get_user(iden) suggestions_on = False if iden is not None: suggestions_on = user_data[iden]['can_suggest'] and points is None and (( oj == 'dmoj' and user_data[iden]['dmoj'] is not None ) or ( oj == 'codeforces' and user_data[iden]['codeforces'] is not None )) if suggestions_on: if oj == 'dmoj': if iden not in self.dmoj_user_suggests.keys(): self.dmoj_user_suggests[iden] = DMOJUserSuggester(user_data[iden]['dmoj']) points, maximum = self.dmoj_user_suggests[iden].get_pp_range() elif oj == 'codeforces': if iden not in self.cf_user_suggests.keys(): self.cf_user_suggests[iden] = CodeforcesUserSuggester(user_data[iden]['codeforces']) points, maximum = self.cf_user_suggests[iden].get_pp_range() if not user_data[iden]['can_repeat']: if oj == 'dmoj' and user_data[iden]['dmoj'] is not None: user_response = json_get('https://dmoj.ca/api/user/info/%s' % user_data[iden]['dmoj']) if user_response is not None: if points is None: for name, prob in list(self.dmoj_problems.items()): if name not in user_response['solved_problems']: temp_dmoj_problems[name] = prob else: temp_dmoj_problems['dmoj'] = {} for point in list(self.problems_by_points['dmoj']): temp_dmoj_problems['dmoj'][point] = {} for name, prob in list(self.problems_by_points['dmoj'][point].items()): if name not in user_response['solved_problems']: temp_dmoj_problems['dmoj'][point][name] = prob if temp_dmoj_problems == {}: raise InvalidParametersException() elif oj == 'codeforces' and user_data[iden]['codeforces'] is not None: response = requests.get('https://codeforces.com/api/user.status?handle=' + user_data[iden]['codeforces']) if response.status_code != 200 or response.json()['status'] != 'OK': return None solved = [] for sub in response.json()['result']: if sub['verdict'] == 'OK': if 'contestId' in sub['problem']: solved.append((sub['problem']['contestId'], sub['problem']['index'])) elif 'problemsetName' in sub['problem']: solved.append((sub['problem']['problemsetName'], sub['problem']['index'])) if points is None: temp_cf_problems = list(filter(lambda prob: (prob.get('contestId', prob.get('problemsetName')), prob['index']) not in solved, self.cf_problems)) else: temp_cf_problems = {'codeforces': {}} for point in list(self.problems_by_points['codeforces']): temp_cf_problems['codeforces'][point] = list(filter(lambda prob: (prob['contestId'], prob['index']) not in solved, self.problems_by_points['codeforces'][point])) if temp_cf_problems == [] or (type(temp_cf_problems) is dict and temp_cf_problems['codeforces'] == {}): raise InvalidParametersException() if temp_dmoj_problems != {}: problem_list = temp_dmoj_problems elif temp_cf_problems != []: problem_list = temp_cf_problems elif points is None: if oj == 'dmoj': problem_list = self.dmoj_problems elif oj == 'codeforces': problem_list = self.cf_problems else: problem_list = self.problems_by_points if points is not None: if not points.isdigit(): raise InvalidQueryException() points = int(points) if maximum is not None: if not maximum.isdigit(): raise InvalidQueryException() maximum = int(maximum) possibilities = [] for point in list(problem_list[oj].keys()): if point >= points and point <= maximum: possibilities.append(point) if len(possibilities) == 0: if suggestions_on and oj == 'dmoj': while len(possibilities) == 0: self.dmoj_user_suggests[iden].expand_pp_range() points, maximum = map(int, self.dmoj_user_suggests[iden].get_pp_range()) for point in list(problem_list[oj].keys()): if point >= points and point <= maximum: possibilities.append(point) if points <= 1 and maximum >= 50 and len(possibilities) == 0: raise InvalidParametersException() elif suggestions_on and oj == 'codeforces': while len(possibilities) == 0: self.cf_user_suggests[iden].expand_pp_range() points, maximum = map(int, self.cf_user_suggests[iden].get_pp_range()) for point in list(problem_list[oj].keys()): if point >= points and point <= maximum: possibilities.append(point) if points <= 1 and maximum >= 50 and len(possibilities) == 0: raise InvalidParametersException() else: raise InvalidParametersException() points = rand.choice(possibilities) if oj == 'dmoj': if not self.dmoj_problems: raise OnlineJudgeHTTPException('DMOJ') if points is None: name, prob = rand.choice(list(problem_list.items())) elif points in problem_list['dmoj'] and len(problem_list['dmoj'][points]) > 0: name, prob = rand.choice(list(problem_list['dmoj'][points].items())) else: raise InvalidParametersException() if iden is not None: user_data[iden]['last_dmoj_problem'] = name query.update_user(iden, 'last_dmoj_problem', name) return self.embed_dmoj_problem(name, prob, suggestions_on) elif oj == 'codeforces': if not self.cf_problems: raise OnlineJudgeHTTPException('Codeforces') return if points is None: prob = rand.choice(problem_list) elif points in problem_list['codeforces']: prob = rand.choice(problem_list['codeforces'][points]) else: raise InvalidParametersException() return self.embed_cf_problem(prob, suggestions_on) elif oj == 'atcoder': if not self.atcoder_problems: raise OnlineJudgeHTTPException('AtCoder') if points is None: prob = rand.choice(self.atcoder_problems) elif points in self.problems_by_points['atcoder']: prob = rand.choice(self.problems_by_points['atcoder'][points]) else: raise InvalidParametersException() return self.embed_atcoder_problem(prob) # elif oj == 'peg': # if not self.peg_problems: # raise OnlineJudgeHTTPException('WCIPEG') # if points is None: # prob = rand.choice(list(self.peg_problems.values())) # elif points in self.problems_by_points['peg']: # prob = rand.choice(list(self.problems_by_points['peg'][points])) # else: # raise InvalidParametersException() # return self.embed_peg_problem(prob) elif oj == 'cses': prob = rand.choice(list(self.cses_problems.values())) return self.embed_cses_problem(prob) elif oj == 'szkopul': prob = rand.choice(list(self.szkopul_problems.values())) return self.embed_szkopul_problem(prob) else: raise NoSuchOJException(oj) def check_existing_user(self, user): query.insert_ignore_user(user.id) def check_existing_server(self, server): query.insert_ignore_server(server.id) @commands.command(aliases=['r']) @commands.bot_has_permissions(embed_links=True) async def random(self, ctx, oj=None, points=None, maximum=None): self.check_existing_user(ctx.message.author) if isinstance(oj, str) and (oj.lower() == 'peg' or oj.lower() == 'wcipeg'): await ctx.send(ctx.message.author.display_name + ', Notice: Support for WCIPEG has been discontinued as **PEG Judge shut down at the end of July 2020**\nhttps://wcipeg.com/announcement/9383') return try: title, description, embed = self.get_random_problem(oj, points, maximum, ctx.message.author.id) embed.title = title embed.description = description + ' (searched in %ss)' % str(round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() # if rand.randint(0, 30) == 0 and isinstance(oj, str) and (oj.lower() == 'dmoj' or oj.lower() == 'codeforces' or oj.lower() == 'cf'): # prefix = await self.bot.command_prefix(self.bot, ctx.message) # await ctx.send('Pro tip: Try out the new command, `%stogglesuggest` to turn on personalised suggested problems for DMOJ and Codeforces!' % prefix) await ctx.send('Requested problem for ' + ctx.message.author.display_name, embed=embed) except IndexError: await ctx.send(ctx.message.author.display_name + ', No problem was found. This may be due to the bot updating the problem cache. Please wait a moment, then try again.') except NoSuchOJException: await ctx.send(ctx.message.author.display_name + ', Invalid query. The online judge must be one of the following: %s.' % str(self.onlineJudges)) except InvalidParametersException as e: await ctx.send(ctx.message.author.display_name + ', ' + str(e)) except OnlineJudgeHTTPException as e: await ctx.send(ctx.message.author.display_name + ', There seems to be a problem with %s. Please try again later :shrug:' % str(e)) except InvalidQueryException: await ctx.send(ctx.message.author.display_name + ', Invalid query. Make sure your points are positive integers.') @commands.command(aliases=['toggleRepeat']) async def togglerepeat(self, ctx): self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) for account in self.onlineJudges.accounts: if user_data[ctx.message.author.id][account] is not None: user_data[ctx.message.author.id]['can_repeat'] = not user_data[ctx.message.author.id]['can_repeat'] query.update_user(ctx.message.author.id, 'can_repeat', user_data[ctx.message.author.id]['can_repeat']) prefix = await self.bot.command_prefix(self.bot, ctx.message) if user_data[ctx.message.author.id]['can_repeat']: await ctx.send(ctx.message.author.display_name + ', random problems will now contain already solved problems') else: await ctx.send(ctx.message.author.display_name + ', random problems will no longer contain already solved problems') return await ctx.send(ctx.message.author.display_name + ', You are not linked to any accounts') @commands.command(aliases=['toggleSuggest']) async def togglesuggest(self, ctx): self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) for account in self.onlineJudges.accounts: if user_data[ctx.message.author.id][account] is not None: user_data[ctx.message.author.id]['can_suggest'] = not user_data[ctx.message.author.id]['can_suggest'] query.update_user(ctx.message.author.id, 'can_suggest', user_data[ctx.message.author.id]['can_suggest']) prefix = await self.bot.command_prefix(self.bot, ctx.message) if user_data[ctx.message.author.id]['can_suggest']: await ctx.send(ctx.message.author.display_name + ', random problems will now be suggested based on your existing solves') else: await ctx.send(ctx.message.author.display_name + ', random problems will no longer be suggested based on your existing solves') return await ctx.send(ctx.message.author.display_name + ', You are not linked to any accounts') @commands.command(aliases=['toggleCountry']) async def togglecountry(self, ctx, code=''): try: country_object = Country(code) self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) prev_country = user_data[ctx.message.author.id]['country'] user_data[ctx.message.author.id]['country'] = country_object.country query.update_user(ctx.message.author.id, 'country', user_data[ctx.message.author.id]['country']) if prev_country is not None and prev_country != country_object.country: await ctx.send(ctx.message.author.display_name + ', Changed your country from %s to %s.' % (str(Country(prev_country)), str(country_object))) else: await ctx.send(ctx.message.author.display_name + ', Set your country to %s.' % str(country_object)) except InvalidCountryException: prefix = await self.bot.command_prefix(self.bot, ctx.message) await ctx.send(ctx.message.author.display_name + ', Sorry, could not find that country. Search for a country using the name (e.g. `%stogglecountry Finland`, `%stogglecountry "United States"`) or the 2 character ISO code (e.g. `%stogglecountry FI`))' % (prefix, prefix, prefix)) @commands.command(aliases=['u', 'profile', 'whois']) @commands.bot_has_permissions(embed_links=True) async def user(self, ctx, user: discord.User=None): if user is None: user = ctx.message.author self.check_existing_user(user) user_data = query.get_user(user.id) embed = discord.Embed(title=user.display_name) embed.timestamp = datetime.utcnow() empty = True if user_data[user.id]['dmoj'] is not None: embed.add_field(name='DMOJ', value='https://dmoj.ca/user/%s' % user_data[user.id]['dmoj'], inline=False) empty = False if user_data[user.id]['codeforces'] is not None: embed.add_field(name='Codeforces', value='https://codeforces.com/profile/%s' % user_data[user.id]['codeforces'], inline=False) empty = False if user_data[user.id]['country'] is not None: embed.add_field(name='Country', value=str(Country(user_data[user.id]['country'])), inline=False) empty = False if empty: embed.description = 'No accounts linked...' await ctx.send('Requested profile by ' + ctx.message.author.display_name, embed=embed) @commands.command(aliases=['s']) async def submit(self, ctx, problem, lang, *, source=None): if ctx.message.author.id not in self.dmoj_sessions.keys(): prefix = await self.bot.command_prefix(self.bot, ctx.message) await ctx.send(ctx.message.author.display_name + ', You are not logged in to a DMOJ account with submission permissions (this could happen if you last logged in a long time ago or have recently gone offline). Please use command `%sconnect dmoj <token>` (your DMOJ API token can be found by going to https://dmoj.ca/edit/profile/ and selecting the __Generate__ or __Regenerate__ option next to API Token). Note: The connect command will ONLY WORK IN DIRECT MESSAGE. Please do not share this token with anyone else.' % prefix) return user_session = self.dmoj_sessions[ctx.message.author.id] if not self.language.languageExists(lang): await ctx.send(ctx.message.author.display_name + ', That language is not available. The available languages are as followed: ```%s```' % ', '.join(self.language.getLanguages())) return try: if source is None and len(ctx.message.attachments) > 0: f = requests.get(ctx.message.attachments[0].url) source = f.content self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) if problem == '^' and user_data[ctx.message.author.id]['last_dmoj_problem'] is not None: problem = user_data[ctx.message.author.id]['last_dmoj_problem'] id = user_session.submit(problem, self.language.getId(lang), source) response = user_session.getTestcaseStatus(id) responseText = str(response) if len(responseText) > 1950: responseText = responseText[1950:] + '\n(Result cut off to fit message length limit)' await ctx.send(ctx.message.author.display_name + ', ' + responseText + '\nTrack your submission here: https://dmoj.ca/submission/' + str(id)) except InvalidSessionException: await ctx.send(ctx.message.author.display_name + ', Failed to connect, or problem not available. Make sure you are submitting to a valid problem, check your authentication, and try again.') except: await ctx.send(ctx.message.author.display_name + ', Error submitting to the problem. Report this using command `$suggest Submission to DMOJ failed`.') @tasks.loop(seconds=30) async def logout_offline(self): for guild in self.bot.guilds: for member in guild.members: if member.id in self.dmoj_sessions.keys() and member.status == discord.Status.offline: await member.send('Attention! You have been logged out of the account %s due to being offline (Note that your account will still be linked to your Discord account, but will now be unable to submit to problems)' % self.dmoj_sessions.pop(member.id)) @logout_offline.before_loop async def logout_offline_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=3) async def refresh_dmoj_problems(self): self.parse_dmoj_problems(json_get('https://dmoj.ca/api/problem/list')) @refresh_dmoj_problems.before_loop async def refresh_dmoj_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=3) async def refresh_cf_problems(self): self.parse_cf_problems(json_get('https://codeforces.com/api/problemset.problems')) @refresh_cf_problems.before_loop async def refresh_cf_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=3) async def refresh_atcoder_problems(self): self.parse_atcoder_problems(json_get('https://kenkoooo.com/atcoder/resources/merged-problems.json')) @refresh_atcoder_problems.before_loop async def refresh_atcoder_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=3) async def refresh_cses_problems(self): self.parse_cses_problems(requests.get('https://cses.fi/problemset/list/')) @refresh_cses_problems.before_loop async def refresh_cses_problems_before(self): await self.bot.wait_until_ready() # @tasks.loop(hours=3) # async def refresh_peg_problems(self): # self.parse_peg_problems(requests.get('https://wcipeg.com/problems/show%3D999999')) # @refresh_peg_problems.before_loop # async def refresh_peg_problems_before(self): # await self.bot.wait_until_ready() @tasks.loop(hours=3) async def refresh_szkopul_problems(self): self.parse_szkopul_problems() @refresh_szkopul_problems.before_loop async def refresh_szkopul_problems_before(self): await self.bot.wait_until_ready() @commands.command() @commands.guild_only() async def tea(self, ctx, user: discord.User=None): if user is None: self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) if user_data[ctx.message.author.id]['tea'] == 1: await ctx.send(ctx.message.author.display_name + ', You have 1 cup of :tea:.') else: await ctx.send(ctx.message.author.display_name + ', You have ' + str(user_data[ctx.message.author.id]['tea']) + ' cups of :tea:.') return if user.id == ctx.message.author.id: await ctx.send(ctx.message.author.display_name + ', Sorry, cannot send :tea: to yourself!') return elif user.id == self.bot.user.id: await ctx.send(ctx.message.author.display_name + ', Thanks for the :tea:!') return self.check_existing_user(user) user_data = query.get_user(user.id) query.update_user(user.id, 'tea', user_data[user.id]['tea']+1) await ctx.send(ctx.message.author.display_name + ', sent a cup of :tea: to ' + user.mention)
import re import discord from discord.ext import commands, tasks import random as rand import yaml import cogs.dblapi as dblapi import cogs.feedback as feedback import cogs.problems_rankings as problems_rankings import cogs.contests as contests import cogs.searcher as searcher from connector import mySQLConnection as query from utils.onlinejudges import OnlineJudges, NoSuchOJException onlineJudges = OnlineJudges() is_ascii = lambda s: re.match('^[\x00-\x7F]+$', s) != None try: config_file = open('config.yml') except FileNotFoundError: config_file = open('example_config.yml') finally: config = yaml.load(config_file, Loader=yaml.FullLoader) prefix = config['bot']['prefix'] bot_token = config['bot']['token'] DEBUG = config['bot']['debug'] dbl_tokens = {} if 'dbl' in config: for dbl, dbl_token in list(config['dbl'].items()): dbl_tokens[dbl] = dbl_token bot_id = config['bot']['id']
class ContestCog(commands.Cog): fetch_time = 0 dmoj_contests = [] cf_contests = [] atcoder_contests = [] dmoj_contest_titles = [] cf_contest_titles = [] atcoder_contest_titles = [] contest_objects = [] onlineJudges = OnlineJudges() def __init__(self, bot): self.bot = bot with open('data/contests.json', 'r', encoding='utf8', errors='ignore') as f: prev_contest_data = json.load(f) self.contest_cache = [] for data in prev_contest_data: self.contest_cache.append(Contest(data)) self.refresh_contests.start() def get_random_contests(self, number): if len(self.contest_cache) == 0: raise NoContestsAvailableException() rand.shuffle(self.contest_cache) result = [] for i in range(min(number, len(self.contest_cache))): result.append(self.contest_cache[i]) return self.embed_multiple_contests(result) def get_contests_of_oj(self, oj): result = [] for contest_object in self.contest_cache: if oj == contest_object.asdict()['oj']: result.append(contest_object) if len(result) == 0: raise NoContestsAvailableException(oj) return self.embed_multiple_contests(result, oj) def reset_contest(self, oj): if oj == 'dmoj': self.dmoj_contests = [] self.dmoj_contest_titles = [] elif oj == 'codeforces': self.cf_contests = [] self.cf_contest_titles = [] elif oj == 'atcoder': self.atcoder_contests = [] self.atcoder_contest_titles = [] def set_time(self): self.fetch_time = time() def parse_dmoj_contests(self, contests): if contests is not None: for contest in range(len(contests)): name, details = list(contests.items())[contest] if datetime.strptime(details['start_time'].replace(':', ''), '%Y-%m-%dT%H%M%S%z') > datetime.now( pytz.utc): spec = json_get('https://dmoj.ca/api/contest/info/' + name) url = 'https://dmoj.ca/contest/' + name contest_data = { 'title': ':trophy: %s' % details['name'], 'description': url, 'oj': 'dmoj', 'thumbnail': 'https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/dmoj-thumbnail.png', 'Start Time': datetime.strptime( details['start_time'].replace(':', ''), '%Y-%m-%dT%H%M%S%z').strftime('%Y-%m-%d %H:%M:%S'), 'End Time': datetime.strptime( details['end_time'].replace(':', ''), '%Y-%m-%dT%H%M%S%z').strftime('%Y-%m-%d %H:%M:%S') } if details['time_limit']: contest_data['Time Limit'] = details['time_limit'] if len(details['labels']) > 0: contest_data['Labels'] = ', '.join(details['labels']) contest_data['Rated'] = 'Yes' if spec['is_rated'] else 'No' contest_data['Format'] = spec['format']['name'] if contest_data['title'] not in self.dmoj_contest_titles: self.dmoj_contest_titles.append(contest_data['title']) self.dmoj_contests.append(Contest(contest_data)) self.dmoj_contests = list(set(self.dmoj_contests)) def parse_cf_contests(self, contests): if contests is not None and contests['status'] == 'OK': for contest in range(len(contests.get('result', []))): details = contests['result'][contest] if details['phase'] == 'BEFORE': url = 'https://codeforces.com/contest/' + str( details['id']) contest_data = { 'title': ':trophy: %s' % details['name'], 'description': url, 'oj': 'codeforces', 'thumbnail': 'https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/cf-thumbnail.png', 'Type': details['type'], 'Start Time': datetime.utcfromtimestamp( details['startTimeSeconds']).strftime( '%Y-%m-%d %H:%M:%S'), 'Time Limit': '%s:%s:%s' % (str(details['durationSeconds'] // (24 * 3600)).zfill(2), str(details['durationSeconds'] % (24 * 3600) // 3600).zfill(2), str(details['durationSeconds'] % 3600 // 60).zfill(2)) } if contest_data['title'] not in self.cf_contest_titles: self.cf_contest_titles.append(contest_data['title']) self.cf_contests.append(Contest(contest_data)) self.cf_contests = list(set(self.cf_contests)) def parse_atcoder_contests(self, contests): if contests is not None: for contest in range(len(contests)): details = contests[contest] if details['startTimeSeconds'] > time(): url = 'https://atcoder.jp/contests/' + details['id'] contest_data = { 'title': ':trophy: %s' % details['title'].replace( '\n', '').replace('\t', '').replace('◉', ''), 'description': url, 'oj': 'atcoder', 'thumbnail': 'https://raw.githubusercontent.com/kevinjycui/Practice-Bot/master/assets/at-thumbnail.png', 'Start Time': datetime.utcfromtimestamp( details['startTimeSeconds']).strftime( '%Y-%m-%d %H:%M:%S'), 'Time Limit': '%s:%s:%s' % (str(details['durationSeconds'] // (24 * 3600)).zfill(2), str(details['durationSeconds'] % (24 * 3600) // 3600).zfill(2), str(details['durationSeconds'] % 3600 // 60).zfill(2)), 'Rated Range': details['ratedRange'] } if contest_data[ 'title'] not in self.atcoder_contest_titles: self.atcoder_contest_titles.append( contest_data['title']) self.atcoder_contests.append(Contest(contest_data)) self.atcoder_contests = list(set(self.atcoder_contests)) def embed_contest(self, contest): embed = discord.Embed(title=contest.asdict()['title'], description=contest.asdict()['description']) embed.set_thumbnail(url=contest.asdict()['thumbnail']) for key in list(contest.asdict().keys()): if key not in ('title', 'description', 'thumbnail', 'oj'): embed.add_field(name=key, value=contest.asdict()[key], inline=False) return embed def embed_multiple_contests(self, contests, oj=None, new=False): if len(contests) == 0: return None if len(contests) == 1: return self.embed_contest(contests[0]) if oj is not None: embed = discord.Embed(title='%d %s%s Contests' % (len(contests), ' New' if new else '', self.onlineJudges.formal_names[oj])) embed.set_thumbnail(url=self.onlineJudges.thumbnails[oj]) else: embed = discord.Embed(title='%d%s Contests' % (len(contests), ' New' if new else '')) for contest in sorted(contests): embed.add_field(name=contest.asdict()['title'], value=contest.asdict()['description'], inline=(len(contests) > 6)) return embed def generate_stream(self): self.contest_objects = list( set(self.dmoj_contests + self.cf_contests + self.atcoder_contests)) def update_contest_cache(self): with open('data/contests.json', 'w') as json_file: prev_contest_data = [] for contest in self.contest_cache: prev_contest_data.append(contest.asdict()) json.dump(prev_contest_data, json_file) @commands.command(aliases=['c']) @commands.bot_has_permissions(embed_links=True) async def contests(self, ctx, numstr='1'): try: if numstr == 'all': number = len(self.contest_cache) contestList = self.get_random_contests(number) elif numstr.isdigit(): number = int(numstr) contestList = self.get_random_contests(number) elif self.onlineJudges.oj_exists(numstr): oj = self.onlineJudges.get_oj(numstr) if oj not in self.onlineJudges.contest_judges: await ctx.send( ctx.message.author.display_name + ', Sorry, contests for that site are not available yet or contests are not applicable to that site.' ) return contestList = self.get_contests_of_oj(oj) await ctx.send( ctx.message.author.display_name + ', Here are %d upcoming contest(s). Last fetched, %d minutes ago' % (1 if numstr == '1' else len(contestList.fields), (time() - self.fetch_time) // 60), embed=contestList) except NoContestsAvailableException as e: await ctx.send(ctx.message.author.display_name + ', ' + str(e)) except NoSuchOJException: await ctx.send( ctx.message.author.display_name + ', Invalid query. The online judge must be one of the following: %s.' % str(self.onlineJudges)) @commands.command() @commands.has_permissions(manage_channels=True) @commands.guild_only() async def sub(self, ctx, channel: discord.TextChannel = None): determiner = 'That' if channel is None: channel = ctx.message.channel determiner = 'This' if query.exists('subscriptions_contests', 'channel_id', channel.id): await ctx.send( ctx.message.author.display_name + ', %s channel is already subscribed to contest notifications.' % determiner) return query.sub_channel(channel.id) await ctx.send(ctx.message.author.display_name + ', ' + channel.mention + ' subscribed to contest notifications.') @commands.command() @commands.guild_only() async def subs(self, ctx): clist = ctx.message.author.display_name + ', Contest notification channels in this server:\n' for text_channel in ctx.message.guild.text_channels: if query.exists('subscriptions_contests', 'channel_id', text_channel.id): clist += text_channel.mention + '\n' if clist == ctx.message.author.display_name + ', Contest notification channels in this server:\n': await ctx.send( ctx.message.author.display_name + ', There are no channels subscribed to contest notifications in this server :slight_frown:' ) else: await ctx.send(clist) @commands.command() @commands.has_permissions(manage_channels=True) @commands.guild_only() async def unsub(self, ctx, channel: discord.TextChannel = None): determiner = 'That' if channel is None: channel = ctx.message.channel determiner = 'This' if not query.exists('subscriptions_contests', 'channel_id', channel.id): await ctx.send( ctx.message.author.display_name + ', %s channel is already not subscribed to contest notifications.' % determiner) return query.unsub_channel(channel.id) await ctx.send(ctx.message.author.display_name + ', ' + channel.mention + ' is no longer a contest notification channel.') def is_upcoming(self, contest): return datetime.strptime( contest.asdict()['Start Time'], '%Y-%m-%d %H:%M:%S') > datetime.now() - timedelta(days=7) @tasks.loop(minutes=5) async def refresh_contests(self): try: self.reset_contest('dmoj') self.parse_dmoj_contests( json_get('https://dmoj.ca/api/contest/list')) except: pass try: self.reset_contest('codeforces') self.parse_cf_contests( json_get('https://codeforces.com/api/contest.list')) except: pass try: self.reset_contest('atcoder') self.parse_atcoder_contests( json_get('https://atcoder-api.appspot.com/contests')) except: pass self.set_time() self.generate_stream() new_contests = list( set(self.contest_objects).difference(set(self.contest_cache))) for channel_id in query.get_all_subs(): try: channel = self.bot.get_channel(channel_id) await channel.send( embed=self.embed_multiple_contests(new_contests, new=True)) except: pass self.contest_cache = list( filter( self.is_upcoming, list(set(self.contest_objects).union(set( self.contest_cache))))) self.update_contest_cache() @refresh_contests.before_loop async def check_contests_before(self): await self.bot.wait_until_ready()
class ProblemCog(commands.Cog): problems_by_points = {'dmoj': {}, 'codeforces': {}, 'atcoder': {}} dmoj_problems = {} cf_problems = None at_problems = None cses_problems = [] szkopul_problems = {} leetcode_problems = [] leetcode_problems_paid = [] dmoj_sessions = {} cf_sessions = {} dmoj_user_suggests = {} cf_user_suggests = {} szkopul_page = 1 language = Language() onlineJudges = OnlineJudges() statuses = { 'dmoj': 0, 'codeforces': 0, 'atcoder': 0, 'cses': 0, 'szkopul': 0, 'leetcode': 0 } fetch_times = { 'dmoj': 0, 'codeforces': 0, 'atcoder': 0, 'cses': 0, 'szkopul': 0, 'leetcode': 0 } def __init__(self, bot): self.bot = bot self.refresh_dmoj_problems.start() self.refresh_cf_problems.start() self.refresh_atcoder_problems.start() self.refresh_leetcode_problems.start() self.refresh_cses_problems.start() self.refresh_szkopul_problems.start() @commands.command() async def oj(self, ctx, oj: str = ''): if oj == '': status_list = '```' for oj in self.onlineJudges.problem_judges: status_list += '%s status: %s. Last fetched, %d minutes ago\n' % ( self.onlineJudges.formal_names[oj], 'OK' if self.statuses[oj] == 1 else 'Unable to connect', (time() - self.fetch_times[oj]) // 60) await ctx.send(status_list[:-1] + '```') else: try: oj = self.onlineJudges.get_oj(oj) if oj not in self.onlineJudges.problem_judges: raise NoSuchOJException(oj) await ctx.send( '```%s status: %s. Last fetched, %d minutes ago```' % (self.onlineJudges.formal_names[oj], 'OK' if self.statuses[oj] == 1 else 'Unable to connect', (time() - self.fetch_times[oj]) // 60)) except NoSuchOJException: await ctx.send( ctx.message.author.display_name + ', Sorry, no online judge found. Search only for online judges used for getting problems this bot ' + self.onlineJudges.problem_judges_str()) async def parse_dmoj_problems(self): try: problem_req = await webc.webget_json( 'https://dmoj.ca/api/v2/problems') problems = problem_req['data']['objects'] self.statuses['dmoj'] = 1 self.dmoj_problems = {} for problem in problems: self.dmoj_problems[problem['code']] = problem self.problems_by_points['dmoj'] = {} for name, details in self.dmoj_problems.items(): if details['points'] not in self.problems_by_points['dmoj']: self.problems_by_points['dmoj'][details['points']] = {} self.problems_by_points['dmoj'][ details['points']][name] = details except Exception as e: self.statuses['dmoj'] = 0 raise e def parse_cf_problems(self): try: problems = requests.get( 'https://codeforces.com/api/problemset.problems').json() self.statuses['codeforces'] = 1 self.cf_problems = problems['result']['problems'] self.problems_by_points['codeforces'] = {} for details in self.cf_problems: if 'rating' in details.keys(): if details['rating'] not in self.problems_by_points[ 'codeforces']: self.problems_by_points['codeforces'][ details['rating']] = [] self.problems_by_points['codeforces'][ details['rating']].append(details) except Exception as e: self.statuses['codeforces'] = 0 raise e def parse_atcoder_problems(self): try: problems = requests.get( 'https://kenkoooo.com/atcoder/resources/merged-problems.json' ).json() self.statuses['atcoder'] = 1 self.atcoder_problems = problems self.problems_by_points['atcoder'] = {} for details in problems: if details['point']: if details['point'] not in self.problems_by_points[ 'atcoder']: self.problems_by_points['atcoder'][ details['point']] = [] self.problems_by_points['atcoder'][ details['point']].append(details) except Exception as e: self.statuses['atcoder'] = 0 raise e def parse_cses_problems(self): try: problems = requests.get('https://cses.fi/problemset/list/').text self.statuses['cses'] = 1 self.cses_problems = [] soup = bs.BeautifulSoup(problems, 'lxml') task_lists = soup.find_all('ul', attrs={'class': 'task-list'}) task_groups = soup.find_all('h2') for index in range(1, len(task_groups)): tasks = task_lists[index].find_all('li', attrs={'class': 'task'}) for task in tasks: name = task.find('a').contents[0] url = 'https://cses.fi' + task.find('a').attrs['href'] id = url.split('/')[-1] rate = task.find('span', attrs={ 'class': 'detail' }).contents[0] group = task_groups[index].contents[0] cses_data = { 'id': id, 'name': name, 'url': url, 'rate': rate, 'group': group } self.cses_problems.append(cses_data) except Exception as e: self.statuses['cses'] = 0 raise e def parse_szkopul_problems(self): try: problems = requests.get( 'https://szkopul.edu.pl/problemset/?page=%d' % self.szkopul_page).text self.statuses['szkopul'] = 1 soup = bs.BeautifulSoup(problems, 'lxml') rows = soup.find_all('tr') if len(rows) == 1: self.szkopul_problems = [ p for p in self.szkopul_problems if p['updated'] ] return if self.szkopul_page == 1: for problem in self.szkopul_problems: problem['updated'] = False for row in rows: data = row.find_all('td') if data == []: continue id = data[0].contents[0] title = data[1].find('a').contents[0] url = 'https://szkopul.edu.pl' + data[1].find( 'a').attrs['href'] tags = [] for tag in data[2].find_all('a'): tags.append(tag.contents[0]) submitters = data[3].contents[0] problem_data = { 'id': id, 'title': title, 'url': url, 'tags': tags, 'submitters': submitters, 'updated': True } if int(submitters) > 0: problem_data['percent_correct'] = data[4].contents[0] problem_data['average'] = data[5].contents[0] self.szkopul_problems[id] = problem_data self.szkopul_page += 1 except Exception as e: self.statuses['szkopul'] = 0 raise e def parse_leetcode_problems(self): try: problems = requests.get( 'https://leetcode.com/api/problems/algorithms/').json() self.statuses['leetcode'] = 1 self.leetcode_problems_paid = [] self.leetcode_problems = [] problemlist = problems['stat_status_pairs'] for problem in problemlist: id = problem['stat']['frontend_question_id'] title = problem['stat']['question__title'] url = 'https://leetcode.com/problems/' + problem['stat'][ 'question__title_slug'] total_acs = problem['stat']['total_acs'] total_submitted = problem['stat']['total_submitted'] level = problem['difficulty']['level'] paid = problem['paid_only'] problem_data = { 'id': id, 'title': title, 'url': url, 'total_acs': total_acs, 'total_submitted': total_submitted, 'level': level, 'paid': paid } if paid: self.leetcode_problems_paid.append(problem_data) else: self.leetcode_problems.append(problem_data) except Exception as e: self.statuses['leetcode'] = 0 raise e def embed_dmoj_problem(self, name, prob, suggested=False): embed = discord.Embed() embed.colour = self.onlineJudges.colours['dmoj'] url = 'https://dmoj.ca/problem/' + name embed.set_thumbnail(url=self.onlineJudges.thumbnails['dmoj']) embed.add_field(name='Points', value=prob['points'], inline=False) embed.add_field(name='Partials', value=('Yes' if prob['partial'] else 'No'), inline=False) embed.add_field(name='Group', value=prob['group'], inline=False) embed.add_field(name='Types', value='||' + ', '.join(prob['types']) + '||', inline=False) return ('[:thumbsup: SUGGESTED] ' if suggested else '') + prob['name'], url, embed def embed_cf_problem(self, prob, suggested=False): embed = discord.Embed() embed.colour = self.onlineJudges.colours['codeforces'] url = 'https://codeforces.com/problemset/problem/' + str( prob['contestId']) + '/' + str(prob['index']) embed.set_thumbnail(url=self.onlineJudges.thumbnails['codeforces']) embed.add_field(name='Type', value=prob['type'], inline=False) if 'points' in prob.keys(): embed.add_field(name='Points', value=prob['points'], inline=False) if 'rating' in prob.keys(): embed.add_field(name='Rating', value=prob['rating'], inline=False) embed.add_field(name='Tags', value='||' + ', '.join(prob['tags']) + '||', inline=False) return ('[:thumbsup: SUGGESTED] ' if suggested else '') + prob['name'], url, embed def embed_atcoder_problem(self, prob): embed = discord.Embed() embed.colour = self.onlineJudges.colours['atcoder'] url = 'https://atcoder.jp/contests/' + prob[ 'contest_id'] + '/tasks/' + prob['id'] embed.set_thumbnail(url=self.onlineJudges.thumbnails['atcoder']) if prob['point']: embed.add_field(name='Points', value=prob['point'], inline=False) embed.add_field(name='Solver Count', value=prob['solver_count'], inline=False) return prob['title'], url, embed def embed_cses_problem(self, prob): embed = discord.Embed() embed.colour = self.onlineJudges.colours['cses'] embed.set_thumbnail(url=self.onlineJudges.thumbnails['cses']) embed.add_field(name='Success Rate', value=prob['rate'], inline=False) embed.add_field(name='Group', value='||' + prob['group'] + '||', inline=False) return prob['name'], prob['url'], embed def embed_szkopul_problem(self, prob): embed = discord.Embed() embed.colour = self.onlineJudges.colours['szkopul'] embed.set_thumbnail(url=self.onlineJudges.thumbnails['szkopul']) if len(prob['tags']) > 0: embed.add_field(name='Tags', value=', '.join(prob['tags']), inline=False) embed.add_field(name='Submitters', value=prob['submitters'], inline=False) if 'percent_correct' in prob: embed.add_field(name='% Correct', value=prob['percent_correct'], inline=False) if 'average' in prob: embed.add_field(name='Average', value=prob['average'], inline=False) return prob['title'], prob['url'], embed def embed_leetcode_problem(self, prob): embed = discord.Embed() embed.colour = self.onlineJudges.colours['leetcode'] embed.set_thumbnail(url=self.onlineJudges.thumbnails['leetcode']) embed.add_field(name='Total ACs', value=prob['total_acs'], inline=False) embed.add_field(name='Total Submitted', value=prob['total_submitted'], inline=False) embed.add_field(name='Level', value=prob['level'], inline=False) embed.add_field(name='Paid?', value='Yes' if prob['paid'] else 'No', inline=False) return prob['title'], prob['url'], embed async def get_random_problem(self, oj=None, points=None, maximum=None, iden=None, paid=False): if oj is None: oj = rand.choice(self.onlineJudges.problem_judges) oj = self.onlineJudges.get_oj(oj) if oj == 'cses' and points is not None: raise InvalidParametersException(cses=True) elif oj == 'szkopul' and points is not None: raise InvalidParametersException(szkopul=True) temp_dmoj_problems = {} temp_cf_problems = [] user_data = query.get_user(iden) suggestions_on = False if iden is not None: suggestions_on = user_data[iden][ 'can_suggest'] and points is None and ( (oj == 'dmoj' and user_data[iden]['dmoj'] is not None) or (oj == 'codeforces' and user_data[iden]['codeforces'] is not None)) if suggestions_on: if oj == 'dmoj': if iden not in self.dmoj_user_suggests.keys(): self.dmoj_user_suggests[iden] = DMOJUserSuggester( user_data[iden]['dmoj']) await self.dmoj_user_suggests[iden].update_pp_range() points, maximum = self.dmoj_user_suggests[ iden].get_pp_range() elif oj == 'codeforces': if iden not in self.cf_user_suggests.keys(): self.cf_user_suggests[iden] = CodeforcesUserSuggester( user_data[iden]['codeforces']) await self.cf_user_suggests[iden].update_pp_range() points, maximum = self.cf_user_suggests[iden].get_pp_range( ) if not user_data[iden]['can_repeat']: if oj == 'dmoj' and user_data[iden]['dmoj'] is not None: user_response = await webc.webget_json( 'https://dmoj.ca/api/user/info/%s' % user_data[iden]['dmoj']) if points is None: for name, prob in list(self.dmoj_problems.items()): if name not in user_response['solved_problems']: temp_dmoj_problems[name] = prob else: temp_dmoj_problems['dmoj'] = {} for point in list(self.problems_by_points['dmoj']): temp_dmoj_problems['dmoj'][point] = {} for name, prob in list( self.problems_by_points['dmoj'] [point].items()): if name not in user_response[ 'solved_problems']: temp_dmoj_problems['dmoj'][point][ name] = prob if temp_dmoj_problems == {}: raise InvalidParametersException() elif oj == 'codeforces' and user_data[iden][ 'codeforces'] is not None: response = await webc.webget_json( 'https://codeforces.com/api/user.status?handle=' + user_data[iden]['codeforces']) if response['status'] != 'OK': return None solved = [] for sub in response['result']: if sub['verdict'] == 'OK': if 'contestId' in sub['problem']: solved.append((sub['problem']['contestId'], sub['problem']['index'])) elif 'problemsetName' in sub['problem']: solved.append( (sub['problem']['problemsetName'], sub['problem']['index'])) if points is None: temp_cf_problems = list( filter( lambda prob: (prob.get( 'contestId', prob.get('problemsetName')), prob['index']) not in solved, self.cf_problems)) else: temp_cf_problems = {'codeforces': {}} for point in list( self.problems_by_points['codeforces']): temp_cf_problems['codeforces'][point] = list( filter( lambda prob: (prob['contestId'], prob[ 'index']) not in solved, self.problems_by_points['codeforces'] [point])) if temp_cf_problems == [] or ( type(temp_cf_problems) is dict and temp_cf_problems['codeforces'] == {}): raise InvalidParametersException() if temp_dmoj_problems != {}: problem_list = temp_dmoj_problems elif temp_cf_problems != []: problem_list = temp_cf_problems elif points is None: if oj == 'dmoj': problem_list = self.dmoj_problems elif oj == 'codeforces': problem_list = self.cf_problems else: problem_list = self.problems_by_points if points is not None: if not points.isdigit(): raise InvalidQueryException() points = int(points) if maximum is not None: if not maximum.isdigit(): raise InvalidQueryException() maximum = int(maximum) if oj != 'leetcode': possibilities = [] for point in list(problem_list[oj].keys()): if point >= points and point <= maximum: possibilities.append(point) if len(possibilities) == 0: if suggestions_on and oj == 'dmoj': while len(possibilities) == 0: self.dmoj_user_suggests[iden].expand_pp_range() points, maximum = map( int, self.dmoj_user_suggests[iden].get_pp_range()) for point in list(problem_list[oj].keys()): if point >= points and point <= maximum: possibilities.append(point) if points <= 1 and maximum >= 50 and len( possibilities) == 0: raise InvalidParametersException() elif suggestions_on and oj == 'codeforces': while len(possibilities) == 0: self.cf_user_suggests[iden].expand_pp_range() points, maximum = map( int, self.cf_user_suggests[iden].get_pp_range()) for point in list(problem_list[oj].keys()): if point >= points and point <= maximum: possibilities.append(point) if points <= 1 and maximum >= 50 and len( possibilities) == 0: raise InvalidParametersException() else: raise InvalidParametersException() points = rand.choice(possibilities) if oj == 'dmoj': if not self.dmoj_problems: raise OnlineJudgeHTTPException('DMOJ') if points is None: name, prob = rand.choice(list(problem_list.items())) elif points in problem_list['dmoj'] and len( problem_list['dmoj'][points]) > 0: name, prob = rand.choice( list(problem_list['dmoj'][points].items())) else: raise InvalidParametersException() if iden is not None: user_data[iden]['last_dmoj_problem'] = name query.update_user(iden, 'last_dmoj_problem', name) return self.embed_dmoj_problem(name, prob, suggestions_on) elif oj == 'codeforces': if not self.cf_problems: raise OnlineJudgeHTTPException('Codeforces') return if points is None: prob = rand.choice(problem_list) elif points in problem_list['codeforces']: prob = rand.choice(problem_list['codeforces'][points]) else: raise InvalidParametersException() return self.embed_cf_problem(prob, suggestions_on) elif oj == 'atcoder': if not self.atcoder_problems: raise OnlineJudgeHTTPException('AtCoder') if points is None: prob = rand.choice(self.atcoder_problems) elif points in self.problems_by_points['atcoder']: prob = rand.choice(self.problems_by_points['atcoder'][points]) else: raise InvalidParametersException() return self.embed_atcoder_problem(prob) elif oj == 'cses': prob = rand.choice(self.cses_problems) return self.embed_cses_problem(prob) elif oj == 'szkopul': prob = rand.choice(list(self.szkopul_problems.values())) return self.embed_szkopul_problem(prob) elif oj == 'leetcode': if points is not None: if points not in (1, 2, 3): raise InvalidParametersException(leetcode=True) if maximum is not None: if maximum not in (1, 2, 3) or maximum < points: raise InvalidParametersException(leetcode=True) else: points = rand.randint(points, maximum) def is_level(prob): return prob['level'] == points if paid: prob = rand.choice( list( filter( is_level, self.leetcode_problems + self.leetcode_problems_paid))) else: prob = rand.choice( list(filter(is_level, self.leetcode_problems))) elif paid: prob = rand.choice(self.leetcode_problems + self.leetcode_problems_paid) else: prob = rand.choice(self.leetcode_problems) return self.embed_leetcode_problem(prob) else: raise NoSuchOJException(oj) def check_existing_user(self, user): query.insert_ignore_user(user.id) def check_existing_server(self, server): query.insert_ignore_server(server.id) @commands.command(aliases=['r']) @commands.bot_has_permissions(embed_links=True) async def random(self, ctx, oj=None, points=None, maximum=None): self.check_existing_user(ctx.message.author) if isinstance(oj, str) and (oj.lower() == 'peg' or oj.lower() == 'wcipeg'): await ctx.send( ctx.message.author.display_name + ', Notice: Support for WCIPEG has been discontinued as **PEG Judge shut down at the end of July 2020**\nhttps://wcipeg.com/announcement/9383' ) return try: title, description, embed = await self.get_random_problem( oj, points, maximum, ctx.message.author.id) embed.title = title embed.description = description + ' (searched in %ss)' % str( round(self.bot.latency, 3)) embed.timestamp = datetime.utcnow() await ctx.send('Requested problem for ' + ctx.message.author.display_name, embed=embed) except IndexError: await ctx.send( ctx.message.author.display_name + ', No problem was found. This may be due to the bot updating the problem cache. Please wait a moment, then try again.' ) except NoSuchOJException: await ctx.send( ctx.message.author.display_name + ', Invalid query. The online judge must be one of the following: %s.' % self.onlineJudges.problem_judges_str()) except InvalidParametersException as e: await ctx.send(ctx.message.author.display_name + ', ' + str(e)) except OnlineJudgeHTTPException as e: await ctx.send( ctx.message.author.display_name + ', There seems to be a problem with %s. Please try again later :shrug:' % str(e)) except InvalidQueryException: await ctx.send( ctx.message.author.display_name + ', Invalid query. Make sure your points are positive integers.' ) @commands.command(aliases=['toggleRepeat', 'tr']) async def togglerepeat(self, ctx): self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) for account in self.onlineJudges.accounts: if user_data[ctx.message.author.id][account] is not None: user_data[ctx.message.author.id]['can_repeat'] = not user_data[ ctx.message.author.id]['can_repeat'] query.update_user( ctx.message.author.id, 'can_repeat', user_data[ctx.message.author.id]['can_repeat']) prefix = await self.bot.command_prefix(self.bot, ctx.message) if user_data[ctx.message.author.id]['can_repeat']: await ctx.send( ctx.message.author.display_name + ', random problems will now contain already solved problems' ) else: await ctx.send( ctx.message.author.display_name + ', random problems will no longer contain already solved problems' ) return await ctx.send(ctx.message.author.display_name + ', You are not linked to any accounts') @commands.command(aliases=['toggleSuggest', 'ts']) async def togglesuggest(self, ctx): self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) for account in self.onlineJudges.accounts: if user_data[ctx.message.author.id][account] is not None: user_data[ ctx.message.author.id]['can_suggest'] = not user_data[ ctx.message.author.id]['can_suggest'] query.update_user( ctx.message.author.id, 'can_suggest', user_data[ctx.message.author.id]['can_suggest']) prefix = await self.bot.command_prefix(self.bot, ctx.message) if user_data[ctx.message.author.id]['can_suggest']: await ctx.send( ctx.message.author.display_name + ', random problems will now be suggested based on your existing solves' ) else: await ctx.send( ctx.message.author.display_name + ', random problems will no longer be suggested based on your existing solves' ) return await ctx.send(ctx.message.author.display_name + ', You are not linked to any accounts') @commands.command(aliases=['toggleCountry', 'togglecountry', 'setCountry']) async def setcountry(self, ctx, code=''): try: country_object = Country(code) self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) prev_country = user_data[ctx.message.author.id]['country'] user_data[ ctx.message.author.id]['country'] = country_object.country query.update_user(ctx.message.author.id, 'country', user_data[ctx.message.author.id]['country']) if prev_country is not None and prev_country != country_object.country: await ctx.send( ctx.message.author.display_name + ', Changed your country from %s to %s.' % (str(Country(prev_country)), str(country_object))) else: await ctx.send(ctx.message.author.display_name + ', Set your country to %s.' % str(country_object)) except InvalidCountryException: prefix = await self.bot.command_prefix(self.bot, ctx.message) await ctx.send( ctx.message.author.display_name + ', Sorry, could not find that country. Search for a country using the name (e.g. `%stogglecountry Finland`, `%stogglecountry "United States"`) or the 2 character ISO code (e.g. `%stogglecountry FI`))' % (prefix, prefix, prefix)) @commands.command(aliases=['u', 'profile', 'whois']) @commands.bot_has_permissions(embed_links=True) async def user(self, ctx, user: discord.User = None): if user is None: user = ctx.message.author self.check_existing_user(user) user_data = query.get_user(user.id) embed = discord.Embed(title=user.display_name) embed.timestamp = datetime.utcnow() embed.colour = discord.Colour(int('0000ff', 16)) empty = True if user_data[user.id]['dmoj'] is not None: embed.add_field(name='DMOJ', value='https://dmoj.ca/user/%s' % user_data[user.id]['dmoj'], inline=False) empty = False if user_data[user.id]['codeforces'] is not None: embed.add_field(name='Codeforces', value='https://codeforces.com/profile/%s' % user_data[user.id]['codeforces'], inline=False) empty = False if user_data[user.id]['country'] is not None: embed.add_field(name='Country', value=str(Country(user_data[user.id]['country'])), inline=False) if empty: embed.description = 'No accounts linked...' elif user.id == ctx.message.author.id: embed.add_field( name='Can repeat', value=str(user_data[user.id]['can_repeat'] == 1) + ' (If true, problems you have already solved on sites where your account is linked will show up when you request for random problems)', inline=False) embed.add_field( name='Can suggest', value=str(user_data[user.id]['can_suggest'] == 1) + ' (If true, suggested problems based on your points on sites where your account is linked will show up when you request for random problems)', inline=False) await ctx.send('Requested profile by ' + ctx.message.author.display_name, embed=embed) @commands.command(aliases=['si']) @commands.bot_has_permissions(embed_links=True) async def serverinfo(self, ctx): if ctx.message.guild is None: await ctx.send( ctx.message.author.display_name + ', You can only request for server info within a server!') return query.insert_ignore_server(ctx.message.guild.id) server_data = query.get_server(ctx.message.guild.id) embed = discord.Embed(title=ctx.message.guild.name) embed.timestamp = datetime.utcnow() embed.set_thumbnail(url=ctx.message.guild.icon_url) embed.colour = discord.Colour(int('ffd300', 16)) embed.add_field( name='Nickname sync', value=str(server_data[ctx.message.guild.id]['nickname_sync'] == 1) + ' (If true, nicknames will be set automatically based on the sync source)', inline=False) embed.add_field( name='Role sync', value=str(server_data[ctx.message.guild.id]['role_sync'] == 1) + ' (If true, roles will be set automatically based on the sync source)', inline=False) if server_data[ctx.message.guild.id]['nickname_sync'] or server_data[ ctx.message.guild.id]['role_sync']: embed.add_field( name='Sync source', value=server_data[ctx.message.guild.id]['sync_source'], inline=False) embed.add_field( name='Join message', value=str(server_data[ctx.message.guild.id]['join_message'] == 1) + ' (If true, bot will send a default join message whenever a new member joins your server)', inline=False) prefix = await self.bot.command_prefix(self.bot, ctx.message) embed.add_field(name='Server prefix', value=prefix, inline=False) clist = '' for text_channel in ctx.message.guild.text_channels: if query.exists('subscriptions_contests', 'channel_id', text_channel.id): clist += text_channel.mention + '\n' embed.add_field(name='Contest notification channel(s)', value='None' if clist == '' else clist, inline=False) await ctx.send(ctx.message.author.display_name + ', Here is your requested info!', embed=embed) @commands.command(aliases=['s']) async def submit(self, ctx, problem, lang, *, source=None): if ctx.message.author.id not in self.dmoj_sessions.keys(): prefix = await self.bot.command_prefix(self.bot, ctx.message) await ctx.send( ctx.message.author.display_name + ', You are not logged in to a DMOJ account with submission permissions (this could happen if you last logged in a long time ago or have recently gone offline). Please use command `%sconnect dmoj <token>` (your DMOJ API token can be found by going to https://dmoj.ca/edit/profile/ and selecting the __Generate__ or __Regenerate__ option next to API Token). Note: The connect command will ONLY WORK IN DIRECT MESSAGE. Please do not share this token with anyone else.' % prefix) return user_session = self.dmoj_sessions[ctx.message.author.id] if not self.language.languageExists(lang): await ctx.send( ctx.message.author.display_name + ', That language is not available. The available languages are as followed: ```%s```' % ', '.join(self.language.getLanguages())) return try: if source is None and len(ctx.message.attachments) > 0: f = webc.webget_text(ctx.message.attachments[0].url) source = f.content self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) if problem == '^' and user_data[ ctx.message.author.id]['last_dmoj_problem'] is not None: problem = user_data[ctx.message.author.id]['last_dmoj_problem'] id = await user_session.submit(problem, self.language.getId(lang), source) response = await user_session.getTestcaseStatus(id) responseText = str(response) if len(responseText) > 1950: responseText = responseText[ 1950:] + '\n(Result cut off to fit message length limit)' await ctx.send( ctx.message.author.display_name + ', ' + responseText + '\nTrack your submission here: https://dmoj.ca/submission/' + str(id)) except InvalidDMOJSessionException: await ctx.send( ctx.message.author.display_name + ', Failed to connect, or problem not available. Make sure you are submitting to a valid problem, check your authentication, and try again.' ) @commands.Cog.listener() async def on_member_update(self, before, after): if str(after.status) == 'offline' and str( before.status ) != 'offline' and after.id in self.dmoj_sessions.keys(): await after.send( 'Attention! You have been logged out of the account %s due to being offline (Note that your account will still be linked to your Discord account, but will now be unable to submit to problems)' % self.dmoj_sessions.pop(after.id)) @tasks.loop(hours=23) async def refresh_dmoj_problems(self): await self.parse_dmoj_problems() self.fetch_times['dmoj'] = time() @refresh_dmoj_problems.before_loop async def refresh_dmoj_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=25) async def refresh_cf_problems(self): self.parse_cf_problems() self.fetch_times['codeforces'] = time() @refresh_cf_problems.before_loop async def refresh_cf_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=26) async def refresh_atcoder_problems(self): self.parse_atcoder_problems() self.fetch_times['atcoder'] = time() @refresh_atcoder_problems.before_loop async def refresh_atcoder_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=24 * 7) async def refresh_cses_problems(self): self.parse_cses_problems() self.fetch_times['cses'] = time() @refresh_cses_problems.before_loop async def refresh_cses_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=1, minutes=27) async def refresh_szkopul_problems(self): self.parse_szkopul_problems() self.fetch_times['szkopul'] = time() @refresh_szkopul_problems.before_loop async def refresh_szkopul_problems_before(self): await self.bot.wait_until_ready() @tasks.loop(hours=28) async def refresh_leetcode_problems(self): self.parse_leetcode_problems() self.fetch_times['leetcode'] = time() @refresh_leetcode_problems.before_loop async def refresh_leetcode_problems_before(self): await self.bot.wait_until_ready() @commands.command() @commands.guild_only() async def tea(self, ctx, user: discord.User = None): if user is None: self.check_existing_user(ctx.message.author) user_data = query.get_user(ctx.message.author.id) if user_data[ctx.message.author.id]['tea'] == 1: await ctx.send(ctx.message.author.display_name + ', You have 1 cup of :tea:.') else: await ctx.send(ctx.message.author.display_name + ', You have ' + str(user_data[ctx.message.author.id]['tea']) + ' cups of :tea:.') return if user.id == ctx.message.author.id: await ctx.send(ctx.message.author.display_name + ', Sorry, cannot send :tea: to yourself!') return elif user.id == self.bot.user.id: await ctx.send(ctx.message.author.display_name + ', Thanks for the :tea:!') return self.check_existing_user(user) user_data = query.get_user(user.id) query.update_user(user.id, 'tea', user_data[user.id]['tea'] + 1) await ctx.send(ctx.message.author.display_name + ', sent a cup of :tea: to ' + user.mention)
class ContestCog(commands.Cog): fetch_time = 0 dmoj_contests = [] cf_contests = [] atcoder_contests = [] leetcode_contests = [] codechef_contests = [] topcoder_contests = [] contest_objects = [] onlineJudges = OnlineJudges() def __init__(self, bot): self.bot = bot if not os.path.isfile('data/contests.json'): with open('data/contests.json', 'w+', encoding='utf8', errors='ignore') as f: json.dump([], f) with open('data/contests.json', 'r', encoding='utf8', errors='ignore') as f: prev_contest_data = json.load(f) self.contest_cache = [] for data in prev_contest_data: self.contest_cache.append(Contest(data)) self.refresh_contests.start() def get_random_contests(self, number): upcoming_contests = list(filter(self.is_upcoming, self.contest_cache)) if len(upcoming_contests) == 0: raise NoContestsAvailableException() rand.shuffle(upcoming_contests) result = [] for i in range(min(number, len(upcoming_contests))): result.append(upcoming_contests[i]) return self.embed_multiple_contests(result) def get_contests_of_oj(self, oj): upcoming_contests = list(filter(self.is_upcoming, self.contest_cache)) result = [] for contest_object in upcoming_contests: if oj == contest_object.asdict()['oj']: result.append(contest_object) if len(result) == 0: raise NoContestsAvailableException(oj) return self.embed_multiple_contests(result, oj) def reset_contest(self, oj): if oj == 'dmoj': self.dmoj_contests = [] elif oj == 'codeforces': self.cf_contests = [] elif oj == 'atcoder': self.atcoder_contests = [] elif oj == 'codechef': self.codechef_contests = [] elif oj == 'topcoder': self.topcoder_contests = [] def set_time(self): self.fetch_time = time() def parse_dmoj_contests(self): contest_req = requests.get('https://dmoj.ca/api/v2/contests').json() contests = contest_req['data']['objects'] for details in contests: name = details['key'] if datetime.strptime(details['start_time'].replace(':', ''), '%Y-%m-%dT%H%M%S%z').timestamp() > time(): spec = requests.get('https://dmoj.ca/api/v2/contest/' + name).json()['data']['object'] url = 'https://dmoj.ca/contest/' + name contest_data = { 'title': ':trophy: %s' % details['name'], 'description': url, 'oj': 'dmoj', 'Start Time': datetime.strptime( details['start_time'].replace(':', ''), '%Y-%m-%dT%H%M%S%z').strftime('%Y-%m-%d %H:%M:%S%z'), 'End Time': datetime.strptime( details['end_time'].replace(':', ''), '%Y-%m-%dT%H%M%S%z').strftime('%Y-%m-%d %H:%M:%S%z') } if spec['time_limit'] is not None: contest_data['Window'] = '%d:%d:%d' % ( spec['time_limit'] // (60 * 60), spec['time_limit'] % (60 * 60) // 60, spec['time_limit'] % 60) if len(spec['tags']) > 0: contest_data['Tags'] = ', '.join(spec['tags']) contest_data['Rated'] = 'Yes' if spec['is_rated'] else 'No' contest_data['Format'] = spec['format']['name'] self.dmoj_contests.append(Contest(contest_data)) def parse_cf_contests(self): contests = requests.get( 'https://codeforces.com/api/contest.list').json() for contest in range(len(contests.get('result', []))): details = contests['result'][contest] if details['phase'] == 'BEFORE': url = 'https://codeforces.com/contest/' + str(details['id']) contest_data = { 'title': ':trophy: %s' % details['name'], 'description': url, 'oj': 'codeforces', 'Type': details['type'], 'Start Time': datetime.utcfromtimestamp( details['startTimeSeconds']).strftime( '%Y-%m-%d %H:%M:%S%z'), 'Duration': '%s:%s:%s' % (str(details['durationSeconds'] // (24 * 3600)).zfill(2), str(details['durationSeconds'] % (24 * 3600) // 3600).zfill(2), str(details['durationSeconds'] % 3600 // 60).zfill(2)) } self.cf_contests.append(Contest(contest_data)) def parse_atcoder_contests(self): contests = requests.get('https://atcoder.jp/contests/?lang=en').text soup = bs.BeautifulSoup(contests, 'lxml') for contest in soup.find_all('table')[ 1 + len(soup.find_all('div', attrs={'id': 'contest-table-action'}) )].find('tbody').find_all('tr'): details = contest.find_all('td') if datetime.strptime(details[0].find('a').find('time').contents[0], '%Y-%m-%d %H:%M:%S%z').timestamp() > time(): contest_data = { 'title': ':trophy: %s' % details[1].find('a').contents[0], 'description': 'https://atcoder.jp' + details[1].find('a')['href'], 'oj': 'atcoder', 'Start Time': datetime.strptime( details[0].find('a').find('time').contents[0], '%Y-%m-%d %H:%M:%S%z').strftime('%Y-%m-%d %H:%M:%S%z'), 'Duration': details[2].contents[0] + ':00', 'Rated Range': details[3].contents[0] } self.atcoder_contests.append(Contest(contest_data)) def parse_external_contest_api(self): contests = requests.get('https://kontests.net/api/v1/all').json() for contest in contests: if contest['site'] == 'LeetCode': self.parse_leetcode_contest(contest) elif contest['site'] == 'CodeChef': self.parse_codechef_contest(contest) elif contest['site'] == 'TopCoder': self.parse_topcoder_contest(contest) def parse_leetcode_contest(self, contest): if contest['status'] == 'BEFORE': contest_data = { 'title': ':trophy: %s' % contest['name'], 'description': contest['url'], 'oj': 'leetcode', 'Start Time': datetime.strptime( contest['start_time'].split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M:%S') + '+0000', 'End Time': datetime.strptime( contest['end_time'].split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M:%S') + '+0000', 'Duration': '%s:%s:%s' % (str(int(float(contest['duration'])) // (24 * 3600)).zfill(2), str(int(float(contest['duration'])) % (24 * 3600) // 3600).zfill(2), str(int(float(contest['duration'])) % 3600 // 60).zfill(2)) } self.leetcode_contests.append(Contest(contest_data)) def parse_codechef_contest(self, contest): if contest['status'] == 'BEFORE': contest_data = { 'title': ':trophy: %s' % contest['name'], 'description': contest['url'].split('?')[0], 'oj': 'codechef', 'Start Time': datetime.strptime( contest['start_time'].split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M:%S') + '+0000', 'End Time': datetime.strptime( contest['end_time'].split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M:%S') + '+0000', 'Duration': '%s:%s:%s' % (str(int(float(contest['duration'])) // (24 * 3600)).zfill(2), str(int(float(contest['duration'])) % (24 * 3600) // 3600).zfill(2), str(int(float(contest['duration'])) % 3600 // 60).zfill(2)) } self.codechef_contests.append(Contest(contest_data)) def parse_topcoder_contest(self, contest): start_time = datetime.strptime( contest['start_time'].split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M:%S') + '+0000' if contest['status'] == 'BEFORE' and datetime.strptime( start_time, '%Y-%m-%d %H:%M:%S%z').timestamp() - time( ) <= 60 * 60 * 24 * 7: contest_data = { 'title': ':trophy: %s' % contest['name'], 'description': contest['url'], 'oj': 'topcoder', 'Start Time': start_time, 'End Time': datetime.strptime( contest['end_time'].split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M:%S') + '+0000', 'Duration': '%s:%s:%s' % (str(int(float(contest['duration'])) // (24 * 3600)).zfill(2), str(int(float(contest['duration'])) % (24 * 3600) // 3600).zfill(2), str(int(float(contest['duration'])) % 3600 // 60).zfill(2)) } self.topcoder_contests.append(Contest(contest_data)) def embed_contest(self, contest): embed = discord.Embed(title=contest.asdict()['title'], description=contest.asdict()['description']) embed.set_thumbnail( url=self.onlineJudges.thumbnails[contest.asdict()['oj']]) embed.colour = self.onlineJudges.colours[contest.asdict()['oj']] for key in list(contest.asdict().keys()): if key not in ('title', 'description', 'oj'): embed.add_field(name=key, value=contest.asdict()[key], inline=False) return embed def embed_multiple_contests(self, contests, oj=None, new=False): if len(contests) == 0: return None if len(contests) == 1: return self.embed_contest(contests[0]) if oj is not None: embed = discord.Embed(title='%d %s%s Contests' % (len(contests), ' New' if new else '', self.onlineJudges.formal_names[oj])) embed.set_thumbnail(url=self.onlineJudges.thumbnails[oj]) embed.colour = self.onlineJudges.colours[oj] else: embed = discord.Embed(title='%d%s Contests' % (len(contests), ' New' if new else '')) for contest in sorted(contests): embed.add_field(name=contest.asdict()['title'], value=contest.asdict()['description'], inline=(len(contests) > 6)) return embed def generate_stream(self): self.contest_objects = list( set(self.dmoj_contests + self.cf_contests + self.atcoder_contests + self.leetcode_contests + self.codechef_contests + self.topcoder_contests)) def update_contest_cache(self): with open('data/contests.json', 'w') as json_file: prev_contest_data = [] for contest in self.contest_cache: prev_contest_data.append(contest.asdict()) json.dump(prev_contest_data, json_file) @commands.command(aliases=['c']) @commands.bot_has_permissions(embed_links=True) async def contests(self, ctx, numstr='1'): try: if numstr == 'all': number = len(self.contest_cache) contestList = self.get_random_contests(number) elif numstr.isdigit(): number = int(numstr) contestList = self.get_random_contests(number) elif self.onlineJudges.oj_exists(numstr): oj = self.onlineJudges.get_oj(numstr) if oj not in self.onlineJudges.contest_judges: await ctx.send( ctx.message.author.display_name + ', Sorry, contests for that site are not available yet or contests are not applicable to that site.' ) return contestList = self.get_contests_of_oj(oj) await ctx.send( ctx.message.author.display_name + ', Here are some upcoming contest(s). Last fetched, %d minutes ago' % ((time() - self.fetch_time) // 60), embed=contestList) except NoContestsAvailableException as e: await ctx.send(ctx.message.author.display_name + ', ' + str(e)) except NoSuchOJException: await ctx.send( ctx.message.author.display_name + ', Invalid query. The online judge must be one of the following: %s.' % self.onlineJudges.contest_judges_str()) @commands.command() @commands.has_permissions(manage_channels=True) @commands.guild_only() async def sub(self, ctx, channel: discord.TextChannel = None, *args): determiner = 'That' if channel is None: channel = ctx.message.channel determiner = 'This' exists = query.exists('subscriptions_contests', 'channel_id', channel.id) old_sub_int = None old_sub_bin = None ojs = [] for arg in args: try: oj = self.onlineJudges.get_oj(arg) if oj not in self.onlineJudges.contest_judges: raise NoSuchOJException ojs.append(oj) except NoSuchOJException: await ctx.send( ctx.message.author.display_name + ', Invalid query. The online judges must be one of the following: %s.' % self.onlineJudges.contest_judges_str()) return sub_bin = '0' * len(self.onlineJudges.contest_judges) if exists: old_sub_int = query.get_subbed_ojs(channel.id) old_sub_bin = '{0:b}'.format(old_sub_int).zfill( len(self.onlineJudges.contest_judges)) sub_bin = old_sub_bin selected = 'the selected ' if len(ojs) == 0: sub_bin = '1' * len(self.onlineJudges.contest_judges) selected = 'all ' sub_bin_mutable = list(sub_bin) for oj in ojs: sub_bin_mutable[self.onlineJudges.contest_judges.index(oj)] = '1' sub_bin = ''.join(sub_bin_mutable) sub_int = int(sub_bin, 2) if exists and sub_int == old_sub_int: await ctx.send( ctx.message.author.display_name + ', %s channel is already subscribed to %scontest notifications.' % (determiner, selected)) return if not exists: query.sub_channel(channel.id) query.update_subbed_ojs(channel.id, sub_int) await ctx.send(ctx.message.author.display_name + ', ' + channel.mention + ' subscribed to %scontest notifications.' % selected) @commands.command() @commands.guild_only() async def subs(self, ctx): clist = ctx.message.author.display_name + ', Contest notification channels in this server:\n' for text_channel in ctx.message.guild.text_channels: if query.exists('subscriptions_contests', 'channel_id', text_channel.id): sub_bin = '{0:b}'.format(query.get_subbed_ojs( text_channel.id)).zfill( len(self.onlineJudges.contest_judges)) ojs = [] for i, b in enumerate(sub_bin): if b == '1': ojs.append(self.onlineJudges.contest_judges[i]) clist += text_channel.mention + ' `' + ', '.join(ojs) + '`\n' if clist == ctx.message.author.display_name + ', Contest notification channels in this server:\n': await ctx.send( ctx.message.author.display_name + ', There are no channels subscribed to contest notifications in this server :slight_frown:' ) else: await ctx.send(clist) @commands.command() @commands.has_permissions(manage_channels=True) @commands.guild_only() async def unsub(self, ctx, channel: discord.TextChannel = None, *args): determiner = 'That' if channel is None: channel = ctx.message.channel determiner = 'This' exists = query.exists('subscriptions_contests', 'channel_id', channel.id) if not exists: await ctx.send( ctx.message.author.display_name + ', %s channel is already not subscribed to contest notifications.' % determiner) return old_sub_int = None old_sub_bin = None ojs = [] for arg in args: try: oj = self.onlineJudges.get_oj(arg) if oj not in self.onlineJudges.contest_judges: raise NoSuchOJException ojs.append(oj) except NoSuchOJException: await ctx.send( ctx.message.author.display_name + ', Invalid query. The online judges must be one of the following: %s.' % self.onlineJudges.contest_judges_str()) return sub_bin = '1' * len(self.onlineJudges.contest_judges) if exists: old_sub_int = query.get_subbed_ojs(channel.id) old_sub_bin = '{0:b}'.format(old_sub_int).zfill( len(self.onlineJudges.contest_judges)) sub_bin = old_sub_bin if len(ojs) == 0: sub_bin = '0' * len(self.onlineJudges.contest_judges) sub_bin_mutable = list(sub_bin) for oj in ojs: sub_bin_mutable[self.onlineJudges.contest_judges.index(oj)] = '0' sub_bin = ''.join(sub_bin_mutable) sub_int = int(sub_bin, 2) if sub_int == old_sub_int: await ctx.send( ctx.message.author.display_name + ', %s channel is already not subscribed to the selected contest notifications.' % determiner) return if sub_int == 0: query.unsub_channel(channel.id) await ctx.send(ctx.message.author.display_name + ', ' + channel.mention + ' is no longer a contest notification channel.') else: query.update_subbed_ojs(channel.id, sub_int) await ctx.send( ctx.message.author.display_name + ', ' + channel.mention + ' has been unsubscribed from contest notifications from the selected online judges' ) def is_upcoming(self, contest): if '+' in contest.asdict()['Start Time']: return datetime.strptime(contest.asdict()['Start Time'], '%Y-%m-%d %H:%M:%S%z') > datetime.now( pytz.UTC) return datetime.strptime(contest.asdict()['Start Time'], '%Y-%m-%d %H:%M:%S') > datetime.now() def is_recent(self, contest): if '+' in contest.asdict()['Start Time']: return datetime.strptime(contest.asdict()['Start Time'], '%Y-%m-%d %H:%M:%S%z') > datetime.now( pytz.UTC) - timedelta(days=7) return datetime.strptime( contest.asdict()['Start Time'], '%Y-%m-%d %H:%M:%S') > datetime.now() - timedelta(days=7) @tasks.loop(minutes=7) async def refresh_contests(self): try: self.reset_contest('dmoj') self.parse_dmoj_contests() except: pass try: self.reset_contest('codeforces') self.parse_cf_contests() except: pass try: self.reset_contest('atcoder') self.parse_atcoder_contests() except: pass try: self.reset_contest('leetcode') self.reset_contest('codechef') self.reset_contest('topcoder') self.parse_external_contest_api() except: pass self.set_time() self.generate_stream() new_contests = list( set(self.contest_objects).difference(set(self.contest_cache))) for channel_id in query.get_all_subs(): sub_bin = '{0:b}'.format(query.get_subbed_ojs(channel_id)).zfill( len(self.onlineJudges.contest_judges)) channel_subbed = [] for new_contest in new_contests: if sub_bin[self.onlineJudges.contest_judges.index( new_contest.asdict()['oj'])] == '1': channel_subbed.append(new_contest) try: channel = self.bot.get_channel(channel_id) await channel.send(embed=self.embed_multiple_contests( channel_subbed, new=True)) except: pass self.contest_cache = list( filter( self.is_recent, list(set(self.contest_objects).union(set( self.contest_cache))))) self.update_contest_cache() @refresh_contests.before_loop async def check_contests_before(self): await self.bot.wait_until_ready()