async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str): """Gets local weather forecast for the next three days from [wttr.in](http://wttr.in/). See help of the `weather` command for possible location types and options.""" async with ctx.typing(): try: units_arg = re.search(self.wttr_units_regex, location).group(1) except AttributeError: units_arg = "" if units_arg.lower() == "f": units = "u" elif units_arg.lower() == "c": units = "m" else: units = "" loc = self.wttr_units_regex.sub("", location).strip() embed = cmn.embed_factory(ctx) embed.title = f"Weather Forecast for {loc}" embed.description = "Data from [wttr.in](http://wttr.in/)." embed.colour = cmn.colours.good loc = loc.replace(" ", "+") async with self.session.get( f"http://wttr.in/{loc}_{units}pnFQ.png") as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) data = io.BytesIO(await resp.read()) embed.set_image(url="attachment://wttr_forecast.png") await ctx.send(embed=embed, file=discord.File(data, "wttr_forecast.png"))
async def get_metar_taf_data(self, airport: str, hours: int, taf: bool) -> List[str]: url = ( f"https://www.aviationweather.gov/metar/data?ids={airport}&format=raw&hours={hours}" f"&taf={'on' if taf else 'off'}&layout=off") async with self.session.get(url) as r: if r.status != 200: raise cmn.BotHTTPError(r) page = await r.text() # pare down to just the data page = page.split("<!-- Data starts here -->")[1].split( "<!-- Data ends here -->")[0].strip() # split at <hr>s data = re.split(r"<hr.*>", page, maxsplit=len(airport)) parsed = [] for sec in data: if sec.strip(): for line in sec.split("\n"): line = line.strip() # remove HTML stuff line = line.replace("<code>", "").replace("</code>", "") line = line.replace("<strong>", "").replace("</strong>", "") line = line.replace("<br/>", "\n").replace(" ", " ") line = line.strip("\n") parsed.append(line) return parsed
async def hamstudy_get_pools(self): async with self.session.get("https://hamstudy.org/pools/") as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) else: pools_dict = json.loads(await resp.read()) pools = dict() for ls in pools_dict.values(): for pool in ls: pools[pool["id"]] = pool return pools
async def _band_conditions(self, ctx: commands.Context): """Gets a solar conditions report.""" async with ctx.typing(): embed = cmn.embed_factory(ctx) embed.title = "Current Solar Conditions" embed.colour = cmn.colours.good async with self.session.get( "http://www.hamqsl.com/solarsun.php") as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) data = io.BytesIO(await resp.read()) embed.set_image(url="attachment://condx.png") await ctx.send(embed=embed, file=discord.File(data, "condx.png"))
async def _grayline(self, ctx: commands.Context): """Gets a map of the current greyline, where HF propagation is the best.""" async with ctx.typing(): embed = cmn.embed_factory(ctx) embed.title = "Current Greyline Conditions" embed.colour = cmn.colours.good async with self.session.get(self.gl_url) as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) data = io.BytesIO(await resp.read()) embed.set_image(url="attachment://greyline.jpg") await ctx.send(embed=embed, file=discord.File(data, "greyline.jpg"))
async def tex(self, ctx: commands.Context, *, expr: str): """Renders a LaTeX expression. In paragraph mode by default. To render math, add `$` around math expressions. """ payload = { "format": "png", "code": self.template.replace("#CONTENT#", expr), "quality": 50 } with ctx.typing(): # ask rTeX to render our expression async with self.session.post(urljoin(opt.rtex_instance, "api/v2"), json=payload) as r: if r.status != 200: raise cmn.BotHTTPError(r) render_result = await r.json() if render_result["status"] != "success": embed = cmn.embed_factory(ctx) embed.title = "LaTeX Rendering Failed!" embed.description = ( "Here are some common reasons:\n" "• Did you forget to use math mode? Surround math expressions with `$`," " like `$x^3$`.\n" "• Are you using a command from a package? It might not be available.\n" "• Are you including the document headers? We already did that for you." ) embed.colour = cmn.colours.bad await ctx.send(embed=embed) return # if rendering went well, download the file given in the response async with self.session.get( urljoin(opt.rtex_instance, "api/v2/" + render_result["filename"])) as r: png_buffer = BytesIO(await r.read()) embed = cmn.embed_factory(ctx) embed.title = "LaTeX Expression" embed.description = "Rendered by [rTeX](https://rtex.probablyaweb.site/)." embed.set_image(url="attachment://tex.png") await ctx.send(file=discord.File(png_buffer, "tex.png"), embed=embed)
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = "", element: str = ""): """Gets a random question from [HamStudy's](https://hamstudy.org) question pools.""" with ctx.typing(): embed = cmn.embed_factory(ctx) country = country.lower() level = level.lower() element = element.upper() if country in study.pool_names.keys(): if level in study.pool_names[country].keys(): pool_name = study.pool_names[country][level] elif level in ("random", "r"): # select a random level in that country pool_name = random.choice( list(study.pool_names[country].values())) else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad for cty in study.pool_names: levels = "`, `".join(study.pool_names[cty].keys()) embed.add_field( name= f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) embed.add_field( name="**Random**", value= "To select a random pool or country, use `random` or `r`" ) await ctx.send(embed=embed) return elif country in ("random", "r"): # select a random country and level country = random.choice(list(study.pool_names.keys())) pool_name = random.choice( list(study.pool_names[country].values())) else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad for cty in study.pool_names: levels = "`, `".join(study.pool_names[cty].keys()) embed.add_field( name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) embed.add_field( name="**Random**", value= "To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return pools = await self.hamstudy_get_pools() pool_matches = [ p for p in pools.keys() if "_".join(p.split("_")[:-1]) == pool_name ] if len(pool_matches) > 0: if len(pool_matches) == 1: pool = pool_matches[0] else: # look at valid_from and expires dates to find the correct one for p in pool_matches: valid_from = datetime.fromisoformat( pools[p]["valid_from"][:-1]) expires = datetime.fromisoformat( pools[p]["expires"][:-1]) if valid_from < datetime.utcnow() < expires: pool = p break else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad for cty in study.pool_names: levels = "`, `".join(study.pool_names[cty].keys()) embed.add_field( name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) embed.add_field( name="**Random**", value= "To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return pool_meta = pools[pool] async with self.session.get( f"https://hamstudy.org/pools/{pool}") as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) pool = json.loads(await resp.read())["pool"] # Select a question if element: els = [el["id"] for el in pool] if element in els: pool_section = pool[els.index(element)]["sections"] else: embed.title = "Element Not Found!" embed.description = f"Possible Elements for Country `{country}` and Level `{level}` are:" embed.colour = cmn.colours.bad embed.description += "\n\n" + "`" + "`, `".join(els) + "`" await ctx.send(embed=embed) return else: pool_section = random.choice(pool)["sections"] pool_questions = random.choice(pool_section)["questions"] question = random.choice(pool_questions) answers = question['answers'] answers_str = "" answers_str_bolded = "" for letter, ans in answers.items(): answers_str += f"{self.choices[letter]} {ans}\n" if letter == question["answer"]: answers_str_bolded += f"{self.choices[letter]} **{ans}**\n" else: answers_str_bolded += f"{self.choices[letter]} {ans}\n" embed.title = f"{study.pool_emojis[country]} {pool_meta['class']} {question['id']}" embed.description = self.source embed.add_field(name="Question", value=question["text"], inline=False) embed.add_field(name="Answers", value=answers_str, inline=False) embed.add_field( name="To Answer", value= ("Answer with reactions below. If not answered within 5 minutes," " the answer will be revealed."), inline=False) if "image" in question: image_url = f"https://hamstudy.org/images/{pool_meta['year']}/{question['image']}" embed.set_image(url=image_url) q_msg = await ctx.send(embed=embed) for i in range(len(answers)): await cmn.add_react(q_msg, list(self.choices.values())[i]) await cmn.add_react(q_msg, cmn.emojis.question) def check(reaction, user): return (user.id != self.bot.user.id and reaction.message.id == q_msg.id and (str(reaction.emoji) in self.choices.values() or str(reaction.emoji) == cmn.emojis.question)) try: reaction, user = await self.bot.wait_for("reaction_add", timeout=300.0, check=check) except asyncio.TimeoutError: embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False) embed.set_field_at( 2, name="Answer", value= (f"{cmn.emojis.clock} " f"**Timed out!** The correct answer was {self.choices[question['answer']]}" )) embed.colour = cmn.colours.timeout await q_msg.edit(embed=embed) else: if str(reaction.emoji) == cmn.emojis.question: embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False) embed.set_field_at( 2, name="Answer", value= f"The correct answer was {self.choices[question['answer']]}", inline=False) embed.add_field(name="Answer Requested By", value=str(user), inline=False) embed.colour = cmn.colours.timeout await q_msg.edit(embed=embed) else: answers_str_checked = "" chosen_ans = self.choices_inv[str(reaction.emoji)] for letter, ans in answers.items(): answers_str_checked += f"{self.choices[letter]}" if letter == question["answer"] == chosen_ans: answers_str_checked += f"{cmn.emojis.check_mark} **{ans}**\n" elif letter == question["answer"]: answers_str_checked += f" **{ans}**\n" elif letter == chosen_ans: answers_str_checked += f"{cmn.emojis.x} {ans}\n" else: answers_str_checked += f" {ans}\n" if self.choices[question["answer"]] == str(reaction.emoji): embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False) embed.set_field_at( 2, name="Answer", value=( f"{cmn.emojis.check_mark} " f"**Correct!** The answer was {reaction.emoji}")) embed.add_field(name="Answered By", value=str(user), inline=False) embed.colour = cmn.colours.good await q_msg.edit(embed=embed) else: embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False) embed.set_field_at( 2, name="Answer", value= (f"{cmn.emojis.x} **Incorrect!** The correct answer was " f"{self.choices[question['answer']]}, not {reaction.emoji}" )) embed.add_field(name="Answered By", value=str(user), inline=False) embed.colour = cmn.colours.bad await q_msg.edit(embed=embed)
async def _ae7q_call(self, ctx: commands.Context, callsign: str): """Looks up the history of a callsign on [ae7q.com](http://ae7q.com/).""" with ctx.typing(): callsign = callsign.upper() desc = "" base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" embed = cmn.embed_factory(ctx) async with self.session.get(base_url + callsign) as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) page = await resp.text() soup = BeautifulSoup(page, features="html.parser") tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] table = tables[0] # find the first table in the page, and use it to make a description if len(table[0]) == 1: for row in table: desc += " ".join(row.getText().split()) desc += "\n" desc = desc.replace(callsign, f"`{callsign}`") table = tables[1] table_headers = table[0].find_all("th") first_header = "".join( table_headers[0].strings) if len(table_headers) > 0 else None # catch if the wrong table was selected if first_header is None or first_header != "Entity Name": embed.title = f"AE7Q History for {callsign}" embed.colour = cmn.colours.bad embed.url = base_url + callsign embed.description = desc embed.description += f"\nNo records found for `{callsign}`" await ctx.send(embed=embed) return table = await process_table(table[1:]) embed = cmn.embed_factory(ctx) embed.title = f"AE7Q History for {callsign}" embed.colour = cmn.colours.good embed.url = base_url + callsign # add the first three rows of the table to the embed for row in table[0:3]: header = f"**{row[0]}** ({row[1]})" # **Name** (Applicant Type) body = (f"Class: *{row[2]}*\n" f"Region: *{row[3]}*\n" f"Status: *{row[4]}*\n" f"Granted: *{row[5]}*\n" f"Effective: *{row[6]}*\n" f"Cancelled: *{row[7]}*\n" f"Expires: *{row[8]}*") embed.add_field(name=header, value=body, inline=False) if len(table) > 3: desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..." embed.description = desc await ctx.send(embed=embed)
async def _ae7q_licensee(self, ctx: commands.Context, licensee_id: str): """Looks up the history of a licensee ID on [ae7q.com](http://ae7q.com/).""" with ctx.typing(): licensee_id = licensee_id.upper() base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" embed = cmn.embed_factory(ctx) async with self.session.get(base_url + licensee_id) as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) page = await resp.text() soup = BeautifulSoup(page, features="html.parser") tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] if not len(tables): embed.title = f"AE7Q History for Licensee {licensee_id}" embed.colour = cmn.colours.bad embed.url = base_url + licensee_id embed.description = f"No records found for Licensee `{licensee_id}`" await ctx.send(embed=embed) return table = tables[0] table_headers = table[0].find_all("th") first_header = "".join( table_headers[0].strings) if len(table_headers) > 0 else None # catch if the wrong table was selected if first_header is None or not first_header.startswith("With FCC"): embed.title = f"AE7Q History for Licensee {licensee_id}" embed.colour = cmn.colours.bad embed.url = base_url + licensee_id embed.description = f"No records found for Licensee `{licensee_id}`" await ctx.send(embed=embed) return table = await process_table(table[2:]) embed = cmn.embed_factory(ctx) embed.title = f"AE7Q History for Licensee {licensee_id}" embed.colour = cmn.colours.good embed.url = base_url + licensee_id # add the first three rows of the table to the embed for row in table[0:3]: header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type) body = (f"Name: *{row[2]}*\n" f"Class: *{row[4]}*\n" f"Region: *{row[1]}*\n" f"Status: *{row[5]}*\n" f"Granted: *{row[6]}*\n" f"Effective: *{row[7]}*\n" f"Cancelled: *{row[8]}*\n" f"Expires: *{row[9]}*") embed.add_field(name=header, value=body, inline=False) if len(table) > 3: embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..." await ctx.send(embed=embed)
async def _ae7q_trustee(self, ctx: commands.Context, callsign: str): """Looks up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/).""" with ctx.typing(): callsign = callsign.upper() desc = "" base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" embed = cmn.embed_factory(ctx) if not callsign.isalnum(): embed = cmn.embed_factory(ctx) embed.title = "AE7Q Trustee History for Callsign" embed.colour = cmn.colours.bad embed.description = "Not a valid callsign!" await ctx.send(embed=embed) return async with self.session.get(base_url + callsign) as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) page = await resp.text() soup = BeautifulSoup(page, features="html.parser") tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] try: table = tables[2] if len(tables[0][0]) == 1 else tables[1] except IndexError: embed.title = f"AE7Q Trustee History for {callsign}" embed.colour = cmn.colours.bad embed.url = base_url + callsign embed.description = desc embed.description += f"\nNo records found for `{callsign}`" await ctx.send(embed=embed) return table_headers = table[0].find_all("th") first_header = "".join( table_headers[0].strings) if len(table_headers) > 0 else None # catch if the wrong table was selected if first_header is None or not first_header.startswith("With"): embed.title = f"AE7Q Trustee History for {callsign}" embed.colour = cmn.colours.bad embed.url = base_url + callsign embed.description = desc embed.description += f"\nNo records found for `{callsign}`" await ctx.send(embed=embed) return table = await process_table(table[2:]) embed = cmn.embed_factory(ctx) embed.title = f"AE7Q Trustee History for {callsign}" embed.colour = cmn.colours.good embed.url = base_url + callsign # add the first three rows of the table to the embed for row in table[0:3]: header = f"**{row[0]}** ({row[3]})" # **Name** (Applicant Type) body = (f"Name: *{row[2]}*\n" f"Region: *{row[1]}*\n" f"Status: *{row[4]}*\n" f"Granted: *{row[5]}*\n" f"Effective: *{row[6]}*\n" f"Cancelled: *{row[7]}*\n" f"Expires: *{row[8]}*") embed.add_field(name=header, value=body, inline=False) if len(table) > 3: desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..." embed.description = desc await ctx.send(embed=embed)
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = ""): """Gets a random question from [HamStudy's](https://hamstudy.org) question pools.""" with ctx.typing(): embed = cmn.embed_factory(ctx) country = country.lower() level = level.lower() if country in study.pool_names.keys(): if level in study.pool_names[country].keys(): pool_name = study.pool_names[country][level] elif level in ("random", "r"): # select a random level in that country pool_name = random.choice(list(study.pool_names[country].values())) else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad for cty in study.pool_names: levels = "`, `".join(study.pool_names[cty].keys()) embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return elif country in ("random", "r"): # select a random country and level country = random.choice(list(study.pool_names.keys())) pool_name = random.choice(list(study.pool_names[country].values())) else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad for cty in study.pool_names: levels = "`, `".join(study.pool_names[cty].keys()) embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return pools = await self.hamstudy_get_pools() pool_matches = [p for p in pools.keys() if "_".join(p.split("_")[:-1]) == pool_name] if len(pool_matches) > 0: if len(pool_matches) == 1: pool = pool_matches[0] else: # look at valid_from and expires dates to find the correct one for p in pool_matches: valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1]) expires = datetime.fromisoformat(pools[p]["expires"][:-1]) if valid_from < datetime.utcnow() < expires: pool = p break else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad for cty in study.pool_names: levels = "`, `".join(study.pool_names[cty].keys()) embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return pool_meta = pools[pool] async with self.session.get(f"https://hamstudy.org/pools/{pool}") as resp: if resp.status != 200: raise cmn.BotHTTPError(resp) pool = json.loads(await resp.read())["pool"] # Select a question pool_section = random.choice(pool)["sections"] pool_questions = random.choice(pool_section)["questions"] question = random.choice(pool_questions) embed.title = f"{study.pool_emojis[country]} {pool_meta['class']} {question['id']}" embed.description = self.source embed.add_field(name="Question:", value=question["text"], inline=False) embed.add_field(name="Answers:", value=(f"**{cmn.emojis.a}** {question['answers']['A']}" f"\n**{cmn.emojis.b}** {question['answers']['B']}" f"\n**{cmn.emojis.c}** {question['answers']['C']}" f"\n**{cmn.emojis.d}** {question['answers']['D']}"), inline=False) embed.add_field(name="To Answer:", value=("Answer with reactions below. If not answered within 10 minutes," " the answer will be revealed."), inline=False) if "image" in question: image_url = f"https://hamstudy.org/images/{pool_meta['year']}/{question['image']}" embed.set_image(url=image_url) q_msg = await ctx.send(embed=embed) await cmn.add_react(q_msg, cmn.emojis.a) await cmn.add_react(q_msg, cmn.emojis.b) await cmn.add_react(q_msg, cmn.emojis.c) await cmn.add_react(q_msg, cmn.emojis.d) def check(reaction, user): return (user.id != self.bot.user.id and reaction.message.id == q_msg.id and str(reaction.emoji) in self.choices.keys()) try: reaction, user = await self.bot.wait_for("reaction_add", timeout=600.0, check=check) except asyncio.TimeoutError: embed.remove_field(2) embed.add_field(name="Answer:", value=f"Timed out! The correct answer was **{question['answer']}**.") await q_msg.edit(embed=embed) else: if self.choices[str(reaction.emoji)] == question["answer"]: embed.remove_field(2) embed.add_field(name="Answer:", value=f"Correct! The answer was **{question['answer']}**.") embed.colour = cmn.colours.good await q_msg.edit(embed=embed) else: embed.remove_field(2) embed.add_field(name="Answer:", value=f"Incorrect! The correct answer was **{question['answer']}**.") embed.colour = cmn.colours.bad await q_msg.edit(embed=embed)