Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 89 additions & 14 deletions Bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async def on_ready() -> None:
triggering an HTTPException.

The method also initializes sparPings to enable a 2-hour cooldown for the
spar command, and chunks all guilds (caches them) to speed up operations.
spar command, and chunks (caches) all guilds to speed up operations.
This also allows you to get a good idea of how many unique users are in
all guilds in which Beardless Bot operates.
"""
Expand All @@ -91,7 +91,7 @@ async def on_ready() -> None:
logger.info("Avatar updated!")

if len(BeardlessBot.guilds) == 0:
logger.exception("Bot is in no servers! Add it to a server.")
logger.warning("Bot is in no servers! Add it to a server.")
else:
for guild in BeardlessBot.guilds:
# Do this first so all servers can spar immediately
Expand Down Expand Up @@ -457,6 +457,16 @@ async def cmd_dice(ctx: misc.BotContext) -> int | nextcord.Embed:

@BeardlessBot.command(name="reset")
async def cmd_reset(ctx: misc.BotContext) -> int:
"""
Reset the user to 200 BeardlessBucks and inform them of this.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 1 otherwise.

"""
if misc.ctx_created_thread(ctx):
return -1
await ctx.send(embed=bucks.reset(ctx.author))
Expand All @@ -465,6 +475,18 @@ async def cmd_reset(ctx: misc.BotContext) -> int:

@BeardlessBot.command(name="register")
async def cmd_register(ctx: misc.BotContext) -> int:
"""
Attempt to register the user with 300 BeardlessBucks.

If the user is already registered, send a message to that effect.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 1 otherwise.

"""
if misc.ctx_created_thread(ctx):
return -1
await ctx.send(embed=bucks.register(ctx.author))
Expand All @@ -487,6 +509,16 @@ async def cmd_bucks(ctx: misc.BotContext) -> int | nextcord.Embed:

@BeardlessBot.command(name="hello", aliases=("hi",))
async def cmd_hello(ctx: misc.BotContext) -> int:
"""
Send a random greeting.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 1 otherwise.

"""
if misc.ctx_created_thread(ctx):
return -1
await ctx.send(random.choice(misc.Greetings))
Expand All @@ -495,6 +527,16 @@ async def cmd_hello(ctx: misc.BotContext) -> int:

@BeardlessBot.command(name="source")
async def cmd_source(ctx: misc.BotContext) -> int:
"""
Send an embed containing the source of many of the fun facts.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 1 otherwise.

"""
if misc.ctx_created_thread(ctx):
return -1
source = (
Expand Down Expand Up @@ -534,6 +576,16 @@ async def cmd_random_brawl(

@BeardlessBot.command(name="fact")
async def cmd_fact(ctx: misc.BotContext) -> int:
"""
Send an embed containing a random fun fact.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 1 otherwise.

"""
if misc.ctx_created_thread(ctx):
return -1
await ctx.send(embed=misc.bb_embed(
Expand Down Expand Up @@ -817,6 +869,14 @@ async def cmd_buy(
return 1


@BeardlessBot.command(name="search", aliases=("google", "lmgtfy"))
async def cmd_search(ctx: misc.BotContext, *, searchterm: str = "") -> int:
if misc.ctx_created_thread(ctx):
return -1
await ctx.send(embed=misc.search(searchterm))
return 1


@BeardlessBot.command(name="pins", aliases=("sparpins", "howtospar"))
async def cmd_pins(ctx: misc.BotContext) -> int:
if (
Expand Down Expand Up @@ -1036,6 +1096,17 @@ async def cmd_tweet(ctx: misc.BotContext) -> int:

@BeardlessBot.command(name="reddit")
async def cmd_reddit(ctx: misc.BotContext) -> int:
"""
Send a link to the EggSoup Subreddit, if in the Egg server.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 0 if the command was not
invoked in the Egg server; 1 otherwise.

"""
if misc.ctx_created_thread(ctx) or not ctx.guild:
return -1
if ctx.guild.id == EggGuildId:
Expand All @@ -1046,6 +1117,17 @@ async def cmd_reddit(ctx: misc.BotContext) -> int:

@BeardlessBot.command(name="guide")
async def cmd_guide(ctx: misc.BotContext) -> int:
"""
Send a link to the EggSoup Improvement Guide, if in the Egg server.

Args:
ctx (misc.BotContext): The context in which the command was invoked

Returns:
int: -1 if the message was a thread creation; 0 if the command was not
invoked in the Egg server; 1 otherwise.

"""
if misc.ctx_created_thread(ctx) or not ctx.guild:
return -1
if ctx.guild.id == EggGuildId:
Expand All @@ -1057,14 +1139,6 @@ async def cmd_guide(ctx: misc.BotContext) -> int:
return 0


@BeardlessBot.command(name="search", aliases=("google", "lmgtfy"))
async def cmd_search(ctx: misc.BotContext, *, searchterm: str = "") -> int:
if misc.ctx_created_thread(ctx):
return -1
await ctx.send(embed=misc.search(searchterm))
return 1


# Listeners:


Expand All @@ -1087,12 +1161,12 @@ async def on_command_error(
misc.logException method.

Args:
ctx (misc.botContext): The context in which the command threw an
Exception
ctx (misc.botContext): The context in which the command threw
an Exception
e (commands.errors.CommandError): The Exception that was thrown

Returns:
int: 0 if the Exception was CommandNotFound; otherwise, 0.
int: 0 if the Exception was CommandNotFound; otherwise, 1.

"""
if isinstance(e, commands.CommandNotFound):
Expand Down Expand Up @@ -1186,7 +1260,8 @@ def launch() -> None:
],
)

# HTTPX tends to flood logs with INFO-level calls; set it to >= WARNING
# Tendency to flood logs with INFO-level calls; set them to >= WARNING
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("nextcord").setLevel(logging.WARNING)

launch()
2 changes: 1 addition & 1 deletion bucks.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def deal_to_player(self) -> str:
f" bringing your total to {sum(self.hand)}. "
)
if BlackjackGame.AceVal in self.hand and self.check_bust():
for i, card in enumerate(self.hand):
for i, card in enumerate(self.hand): # pragma: no branch
if card == BlackjackGame.AceVal:
self.hand[i] = 1
self.bet *= -1
Expand Down
11 changes: 2 additions & 9 deletions logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@

from misc import ProfUrl, bb_embed, content_check, fetch_avatar, truncate_time

# TODO: Implement log thread locked/unlocked
# https://github.com/LevBernstein/BeardlessBot/issues/45

MaxPurgedMsgs: Final[int] = 99


Expand Down Expand Up @@ -140,13 +137,9 @@ def log_member_nick_change(
).set_author(
name=after.name, icon_url=fetch_avatar(after),
).add_field(
name="Before:",
value=before.nick if before.nick else before.name,
inline=False,
name="Before:", value=before.nick or before.name, inline=False,
).add_field(
name="After:",
value=after.nick if after.nick else after.name,
inline=False,
name="After:", value=after.nick or after.name, inline=False,
)


Expand Down
28 changes: 24 additions & 4 deletions misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def fetch_avatar(
user: nextcord.Member | nextcord.User | nextcord.ClientUser,
) -> str:
"""
Pull a given user's avatar url.
Pull a given user's avatar url safely.

Args:
user (nextcord.Member or User or ClientUser): The user whose avatar
Expand Down Expand Up @@ -296,6 +296,29 @@ def __init__(self, *, animal: str) -> None:


async def fetch_animal(url: str, *args: str | int) -> str | None:
"""
Pull an animal image URL from a JSON response.

For each arg in *args, move a layer deeper into the JSON response,
eventually returning the image URL. For example, calling fetch_animal
with args=["foo", 0, "bar", "url"] will extract the url from the following
expected JSON object:

{"foo": ["bar": {"url": "animal.url"}]}

This obviously fails horribly if the JSON response in any way does not
match what you are expecting.

Args:
url (str): The API endpoint to call
*args (str | int): The path to take through the JSON response in order
to obtain the animal image URL.

Returns:
str | None: The animal image URL if the called API returned OK;
else, None.

"""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(url)
if r.status_code == Ok:
Expand Down Expand Up @@ -966,8 +989,6 @@ def get_last_numeric_char(duration: str) -> int:
async def process_mute_target(
ctx: BotContext, target: str | None, bot: commands.Bot,
) -> nextcord.Member | None:
# TODO: unit test
# https://github.com/LevBernstein/BeardlessBot/issues/47
assert hasattr(ctx.author, "guild_permissions")
if not ctx.author.guild_permissions.manage_messages:
await ctx.send(Naughty.format(ctx.author.mention))
Expand Down Expand Up @@ -1085,7 +1106,6 @@ async def check_for_spar_channel(ctx: BotContext) -> bool:


# Static embeds.
# TODO: convert these to methods

AdminPermsReasons = (
"Beardless Bot requires permissions in order to do just about anything."
Expand Down
4 changes: 2 additions & 2 deletions resources/images/docstr-coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions resources/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ aiohttp==3.13.2
audioop-lts==0.2.2; python_version>="3.13"
beautifulsoup4==4.14.3
codespell==2.4.1
coverage==7.12.0
coverage==7.13.0
docstr-coverage==2.3.2
flake8==7.3.0
flake8-comprehensions==3.17.0
Expand All @@ -12,14 +12,14 @@ httpx==0.28.1
mypy[faster-cache, reports]==1.19.0
mypy-extensions==1.1.0
nextcord==3.1.1
pytest==9.0.1
pytest==9.0.2
pytest-asyncio==1.3.0
pytest-github-actions-annotate-failures==0.3.0
pytest-httpx==0.36.0
pytest-rerunfailures==16.1
python-dotenv==1.2.1
requests==2.32.5
ruff==0.14.8
ruff==0.14.9
steam==1.4.4
types-aiofiles==25.1.0.20251011
types-beautifulsoup4==4.12.0.20250516
Expand Down
26 changes: 11 additions & 15 deletions unitTests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,27 @@
#output results to stdout and testResults.md

echo "# Beardless Bot Unit Test Results" > testResults.md;
python3 -m coverage run --include=$(echo $PWD)/* --omit=./*env/* \
python3 -m coverage run --branch --include=$(echo $PWD)/* --omit=./*env/* \
-m pytest -W ignore::DeprecationWarning --ignore=./*env/* --tb=line \
--junitxml=junit.xml;
python3 -m coverage report -m --precision=2 |& tee -a testResults.md;
python3 -m coverage xml 1> /dev/null;
python3 -m coverage xml -q;
rm resources/images/coverage.svg 2> /dev/null;
genbadge coverage -i coverage.xml -o \
resources/images/coverage.svg >> /dev/null;
rm .coverage 2> /dev/null;
rm coverage.xml 2> /dev/null;
python3 -m flake8 --exclude="*env/*" --ignore=A003,W191,W503 --statistics \
genbadge coverage -s -i coverage.xml -o resources/images/coverage.svg;
rm .coverage coverage.xml 2> /dev/null;
python3 -m flake8 --exclude="*env/*" --ignore=W191,W503 --statistics \
--exit-zero --output-file flake8stats.txt --min-python-version 3.12.0;
genbadge flake8 -i flake8stats.txt -o \
resources/images/flake8-badge.svg >> /dev/null;
genbadge flake8 -s -i flake8stats.txt -o resources/images/flake8-badge.svg;
rm flake8stats.txt 2> /dev/null;
genbadge tests -i junit.xml -o resources/images/tests.svg >> /dev/null;
genbadge tests -s -i junit.xml -o resources/images/tests.svg;
rm junit.xml 2> /dev/null;
docstr-coverage ./ -e ".*bb_test.py/*|.*env/*" -v 0 --badge \
resources/images/docstr-coverage.svg 2> /dev/null;

#Truncate decimals in coverage badge. Very hacky; I wish
#there was an arg for significant figures.
python3 -c '
import re
with open("resources/images/coverage.svg") as f:
svg = f.read()
with open("resources/images/coverage.svg", "w") as g:
g.write(re.sub(r"((\.\d{2})%+)", ".00%", svg))'
import re, pathlib
p = pathlib.Path("resources/images/coverage.svg")
with p.open("r") as f: svg = f.read()
with p.open("w") as g: g.write(re.sub(r"((\.\d{2})%+)", ".00%", svg))'