Merge branch 'master' into helppfx

This commit is contained in:
0x5c 2021-03-13 18:30:23 -05:00
commit 950840be60
No known key found for this signature in database
GPG Key ID: A57F71C3176B9581
10 changed files with 235 additions and 34 deletions

View File

@ -57,7 +57,9 @@ jobs:
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true [[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
- name: Push images to registry - name: Push images to registry
run: docker push ${{ steps.tag_image.outputs.image_id }} run: |
[[ "${{ steps.tag_image.outputs.version }}" != "dev" ]] && docker push ${{ steps.tag_image.outputs.image_id }}:latest || true
docker push ${{ steps.tag_image.outputs.image_id }}:${{ steps.tag_image.outputs.version }}
- name: Deploy official images - name: Deploy official images
id: deploy_images id: deploy_images

View File

@ -7,12 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- MUF and foF2 maps from [prop.kc2g.com](https://prop.kc2g.com/). - MUF and foF2 maps from [prop.kc2g.com](https://prop.kc2g.com/).
- Commands to show METAR (`?metar`) and TAF (`?taf`) (aeronautical weather conditions).
- The ability to select an element of a pool in `?hamstudy`.
- The ability to answer ❓ to a HamStudy question to get the answer.
- The list of available prefixes to `?help` when there is more than one. - The list of available prefixes to `?help` when there is more than one.
### Changed ### Changed
- New colour theme for `?greyline`. - New colour theme for `?greyline`.
- Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`. - Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`.
- `?ungrid` to `?latlong`. - `?ungrid` to `?latlong`.
- Renamed `?cond` to `?solar`. - Renamed `?cond` to `?solar`.
- Reduced `?hamstudy` timeout to 5 minutes.
### Fixed ### Fixed
- Weird image caching situation for `?greyline` on Discord's side. - Weird image caching situation for `?greyline` on Discord's side.
- The help command was not using the prefix it was invoked with. - The help command was not using the prefix it was invoked with.

View File

@ -32,6 +32,8 @@ $ run.sh
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project. Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project.
All issues and requests related to resources (including maps, band charts, data) should be added in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources).
## Copyright ## Copyright
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2020 Abigail Gold, 0x5c

View File

@ -36,6 +36,7 @@ colours = SimpleNamespace(
good=0x43B581, good=0x43B581,
neutral=0x7289DA, neutral=0x7289DA,
bad=0xF04747, bad=0xF04747,
timeout=0xF26522,
) )
# meow # meow
@ -56,10 +57,12 @@ emojis = SimpleNamespace(
question="", question="",
no_entry="", no_entry="",
bangbang="‼️", bangbang="‼️",
clock="",
a="🇦", a="🇦",
b="🇧", b="🇧",
c="🇨", c="🇨",
d="🇩", d="🇩",
e="🇪",
) )
paths = SimpleNamespace( paths = SimpleNamespace(

View File

@ -44,6 +44,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q 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: async with self.session.get(base_url + callsign) as resp:
if resp.status != 200: if resp.status != 200:
raise cmn.BotHTTPError(resp) raise cmn.BotHTTPError(resp)
@ -110,6 +118,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx) 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: async with self.session.get(base_url + callsign) as resp:
if resp.status != 200: if resp.status != 200:
raise cmn.BotHTTPError(resp) raise cmn.BotHTTPError(resp)
@ -178,6 +194,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Application 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: async with self.session.get(base_url + callsign) as resp:
if resp.status != 200: if resp.status != 200:
raise cmn.BotHTTPError(resp) raise cmn.BotHTTPError(resp)
@ -250,6 +274,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
if not frn.isdecimal():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for FRN"
embed.colour = cmn.colours.bad
embed.description = "Not a valid FRN!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + frn) as resp: async with self.session.get(base_url + frn) as resp:
if resp.status != 200: if resp.status != 200:
raise cmn.BotHTTPError(resp) raise cmn.BotHTTPError(resp)
@ -313,6 +345,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
if not licensee_id.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Licensee"
embed.colour = cmn.colours.bad
embed.description = "Not a valid licensee ID!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + licensee_id) as resp: async with self.session.get(base_url + licensee_id) as resp:
if resp.status != 200: if resp.status != 200:
raise cmn.BotHTTPError(resp) raise cmn.BotHTTPError(resp)

View File

@ -11,6 +11,7 @@ the GNU General Public License, version 2.
import random import random
import re import re
from typing import Union from typing import Union
import pathlib
import discord import discord
import discord.ext.commands as commands import discord.ext.commands as commands
@ -103,6 +104,22 @@ class BaseCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.changelog = parse_changelog() self.changelog = parse_changelog()
commit_file = pathlib.Path("git_commit")
dot_git = pathlib.Path(".git")
if commit_file.is_file():
with commit_file.open() as f:
self.commit = f.readline().strip()[:7]
elif dot_git.is_dir():
head_file = pathlib.Path(dot_git, "HEAD")
if head_file.is_file():
with head_file.open() as hf:
head = hf.readline().split(": ")[1].strip()
branch_file = pathlib.Path(dot_git, head)
if branch_file.is_file():
with branch_file.open() as bf:
self.commit = bf.readline().strip()[:7]
else:
self.commit = ""
@commands.command(name="info", aliases=["about"]) @commands.command(name="info", aliases=["about"])
async def _info(self, ctx: commands.Context): async def _info(self, ctx: commands.Context):
@ -112,7 +129,7 @@ class BaseCog(commands.Cog):
embed.description = info.description embed.description = info.description
embed.add_field(name="Authors", value=", ".join(info.authors)) embed.add_field(name="Authors", value=", ".join(info.authors))
embed.add_field(name="License", value=info.license) embed.add_field(name="License", value=info.license)
embed.add_field(name="Version", value=f"v{info.release}") embed.add_field(name="Version", value=f"v{info.release} {'(`' + self.commit + '`)' if self.commit else ''}")
embed.add_field(name="Contributing", value=info.contributing, inline=False) embed.add_field(name="Contributing", value=info.contributing, inline=False)
embed.add_field(name="Official Server", value=info.bot_server, inline=False) embed.add_field(name="Official Server", value=info.bot_server, inline=False)
embed.set_thumbnail(url=str(self.bot.user.avatar_url)) embed.set_thumbnail(url=str(self.bot.user.avatar_url))
@ -172,8 +189,11 @@ class BaseCog(commands.Cog):
"""Shows how to create a bug report or feature request about the bot.""" """Shows how to create a bug report or feature request about the bot."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
embed.title = "Found a bug? Have a feature request?" embed.title = "Found a bug? Have a feature request?"
embed.description = ("Submit an issue on the [issue tracker]" embed.description = """Submit an issue on the [issue tracker](https://github.com/miaowware/qrm2/issues)!
"(https://github.com/miaowware/qrm2/issues)!")
All issues and requests related to resources (including maps, band charts, data) \
should be added in \
[miaowware/qrm-resources](https://github.com/miaowware/qrm-resources/issues)."""
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="echo", aliases=["e"], category=cmn.cat.admin) @commands.command(name="echo", aliases=["e"], category=cmn.cat.admin)

View File

@ -31,6 +31,14 @@ class QRZCog(commands.Cog):
"""Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page.""" """Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page."""
flags = [f.lower() for f in flags] flags = [f.lower() for f in flags]
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "QRZ Data for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
if keys.qrz_user == "" or keys.qrz_pass == "" or "--link" in flags: if keys.qrz_user == "" or keys.qrz_pass == "" or "--link" in flags:
await ctx.send(f"http://qrz.com/db/{callsign}") await ctx.send(f"http://qrz.com/db/{callsign}")
return return

View File

@ -22,7 +22,8 @@ from resources import study
class StudyCog(commands.Cog): class StudyCog(commands.Cog):
choices = {cmn.emojis.a: "A", cmn.emojis.b: "B", cmn.emojis.c: "C", cmn.emojis.d: "D"} choices = {"A": cmn.emojis.a, "B": cmn.emojis.b, "C": cmn.emojis.c, "D": cmn.emojis.d, "E": cmn.emojis.e}
choices_inv = {y: x for x, y in choices.items()}
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
@ -31,13 +32,14 @@ class StudyCog(commands.Cog):
self.session = aiohttp.ClientSession(connector=bot.qrm.connector) self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.cat.study) @commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.cat.study)
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = ""): 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.""" """Gets a random question from [HamStudy's](https://hamstudy.org) question pools."""
with ctx.typing(): with ctx.typing():
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
country = country.lower() country = country.lower()
level = level.lower() level = level.lower()
element = element.upper()
if country in study.pool_names.keys(): if country in study.pool_names.keys():
if level in study.pool_names[country].keys(): if level in study.pool_names[country].keys():
@ -115,21 +117,37 @@ class StudyCog(commands.Cog):
pool = json.loads(await resp.read())["pool"] pool = json.loads(await resp.read())["pool"]
# Select a question # Select a question
pool_section = random.choice(pool)["sections"] 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"] pool_questions = random.choice(pool_section)["questions"]
question = random.choice(pool_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.title = f"{study.pool_emojis[country]} {pool_meta['class']} {question['id']}"
embed.description = self.source embed.description = self.source
embed.add_field(name="Question:", value=question["text"], inline=False) embed.add_field(name="Question", value=question["text"], inline=False)
embed.add_field(name="Answers:", embed.add_field(name="Answers", value=answers_str, inline=False)
value=(f"**{cmn.emojis.a}** {question['answers']['A']}" embed.add_field(name="To Answer",
f"\n**{cmn.emojis.b}** {question['answers']['B']}" value=("Answer with reactions below. If not answered within 5 minutes,"
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."), " the answer will be revealed."),
inline=False) inline=False)
if "image" in question: if "image" in question:
@ -138,33 +156,61 @@ class StudyCog(commands.Cog):
q_msg = await ctx.send(embed=embed) q_msg = await ctx.send(embed=embed)
await cmn.add_react(q_msg, cmn.emojis.a) for i in range(len(answers)):
await cmn.add_react(q_msg, cmn.emojis.b) await cmn.add_react(q_msg, list(self.choices.values())[i])
await cmn.add_react(q_msg, cmn.emojis.c) await cmn.add_react(q_msg, cmn.emojis.question)
await cmn.add_react(q_msg, cmn.emojis.d)
def check(reaction, user): def check(reaction, user):
return (user.id != self.bot.user.id return (user.id != self.bot.user.id
and reaction.message.id == q_msg.id and reaction.message.id == q_msg.id
and str(reaction.emoji) in self.choices.keys()) and (str(reaction.emoji) in self.choices.values() or str(reaction.emoji) == cmn.emojis.question))
try: try:
reaction, user = await self.bot.wait_for("reaction_add", timeout=600.0, check=check) reaction, user = await self.bot.wait_for("reaction_add", timeout=300.0, check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
embed.remove_field(2) embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.add_field(name="Answer:", value=f"Timed out! The correct answer was **{question['answer']}**.") 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) await q_msg.edit(embed=embed)
else: else:
if self.choices[str(reaction.emoji)] == question["answer"]: if str(reaction.emoji) == cmn.emojis.question:
embed.remove_field(2) embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.add_field(name="Answer:", value=f"Correct! The answer was **{question['answer']}**.") embed.set_field_at(2, name="Answer",
embed.colour = cmn.colours.good 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) await q_msg.edit(embed=embed)
else: else:
embed.remove_field(2) answers_str_checked = ""
embed.add_field(name="Answer:", value=f"Incorrect! The correct answer was **{question['answer']}**.") chosen_ans = self.choices_inv[str(reaction.emoji)]
embed.colour = cmn.colours.bad for letter, ans in answers.items():
await q_msg.edit(embed=embed) 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 hamstudy_get_pools(self): async def hamstudy_get_pools(self):
async with self.session.get("https://hamstudy.org/pools/") as resp: async with self.session.get("https://hamstudy.org/pools/") as resp:

View File

@ -9,7 +9,11 @@ the GNU General Public License, version 2.
import re import re
from typing import List
import aiohttp
from discord import Embed
import discord.ext.commands as commands import discord.ext.commands as commands
import common as cmn import common as cmn
@ -20,8 +24,10 @@ class WeatherCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(aliases=["solar", "bandconditions", "cond", "condx", "conditions"], category=cmn.cat.weather) @commands.command(name="solarweather", aliases=["solar", "bandconditions", "cond", "condx", "conditions"],
category=cmn.cat.weather)
async def solarweather(self, ctx: commands.Context): async def solarweather(self, ctx: commands.Context):
"""Gets a solar weather report.""" """Gets a solar weather report."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -103,6 +109,73 @@ class WeatherCog(commands.Cog):
embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png") embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="metar", category=cmn.cat.weather)
async def metar(self, ctx: commands.Context, airport: str, hours: int = 0):
"""Gets current raw METAR (Meteorological Terminal Aviation Routine Weather Report) for an airport. \
Optionally, a number of hours can be given to show a number of hours of historical METAR data.
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, hours, False))
@commands.command(name="taf", category=cmn.cat.weather)
async def taf(self, ctx: commands.Context, airport: str):
"""Gets forecasted raw TAF (Terminal Aerodrome Forecast) data for an airport. Includes the latest METAR data.
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, 0, True))
async def gen_metar_taf_embed(self, ctx: commands.Context, airport: str, hours: int, taf: bool) -> Embed:
embed = cmn.embed_factory(ctx)
airport = airport.upper()
if re.fullmatch(r"\w(\w|\d){2,3}", airport):
metar = await self.get_metar_taf_data(airport, hours, taf)
if taf:
embed.title = f"Current TAF for {airport}"
elif hours > 0:
embed.title = f"METAR for {airport} for the last {hours} hour{'s' if hours > 1 else ''}"
else:
embed.title = f"Current METAR for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/metar/data)."
embed.colour = cmn.colours.good
data = "\n".join(metar)
embed.description += f"\n\n```\n{data}\n```"
else:
embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad
return embed
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("&nbsp;", " ")
line = line.strip("\n")
parsed.append(line)
return parsed
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
bot.add_cog(WeatherCog(bot)) bot.add_cog(WeatherCog(bot))

View File

@ -11,6 +11,9 @@ the GNU General Public License, version 2.
authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639") authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
description = """A bot with various useful ham radio-related functions, written in Python.""" description = """A bot with various useful ham radio-related functions, written in Python."""
license = "Released under the GNU General Public License v2" license = "Released under the GNU General Public License v2"
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2" contributing = """Check out the [source on GitHub](https://github.com/miaowware/qrm2). Contributions are welcome!
All issues and requests related to resources (including maps, band charts, data) should be added \
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.5.1" release = "2.5.1"
bot_server = "https://discord.gg/Ntbg3J4" bot_server = "https://discord.gg/Ntbg3J4"