Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9975414
Initial slash commands (dm messages not funcitonal yet)
rhit-daughewd Dec 16, 2025
bcef56d
Mod combined implementation
rhit-daughewd Jan 27, 2026
e12f231
Slash refactoring: removed duplicated logic and limited to user facin…
rhit-daughewd Jan 31, 2026
ebfe657
Help slash and formatting
rhit-daughewd Feb 6, 2026
03105ab
Invoke Command
rhit-daughewd Feb 18, 2026
62bf7d6
Merge branch 'dev' into Slash-Commands
rhit-daughewd Feb 19, 2026
2e0ebb7
BaseCmds is already Cog, this is redundant
Borketh Feb 24, 2026
d022fb6
Merge branch 'dev' into Slash-Commands
Borketh Feb 24, 2026
70e529c
update nextcord
Borketh Feb 25, 2026
d19b5ef
help subcommands
Borketh Feb 25, 2026
103d11c
subcommands example
Borketh Feb 25, 2026
781986e
oh come on read the code and stop blindly using AI
Borketh Feb 25, 2026
7b594e7
imports
Borketh Feb 25, 2026
feb9cc6
fix that typo
Borketh Feb 25, 2026
32817bf
restore text command for docsearch and add reply_generic to bot for f…
Borketh Feb 25, 2026
7eeb332
ephemeral is the terminology discord and other bots use. let's keep t…
Borketh Feb 25, 2026
525f973
fix: Formatting and Subcommands
rhit-daughewd Mar 15, 2026
8022ae4
Fix: corrected group call
rhit-daughewd Mar 15, 2026
126b184
Black Formatting
rhit-daughewd Mar 15, 2026
35d2650
Fix: duplicate welcome
rhit-daughewd Mar 15, 2026
8093400
fix black line length PR bloat
Borketh Mar 25, 2026
b1cf20f
Fix: Display name consistency
rhit-daughewd Mar 25, 2026
2acc2d3
Merge branch 'Slash-Commands' of https://github.com/satisfactorymoddi…
rhit-daughewd Mar 25, 2026
9882546
Merge branch 'dev' into Slash-Commands
rhit-daughewd Apr 7, 2026
402f5d1
Poetry Lock
rhit-daughewd Apr 8, 2026
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
40 changes: 33 additions & 7 deletions fred/cogs/crashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@
from asyncio import Task, TaskGroup
from os.path import split
from pathlib import Path
from typing import AsyncIterator, IO, Type, Coroutine, Generator, Optional, Any, Final, TypedDict, AsyncGenerator
from typing import (
AsyncIterator,
IO,
Type,
Coroutine,
Generator,
Optional,
Any,
Final,
TypedDict,
AsyncGenerator,
)
from urllib.parse import urlparse
from zipfile import ZipFile

Expand Down Expand Up @@ -197,7 +208,11 @@ def replace_response_value_with_captured(m) -> str:
return f"{{Group {group} not captured in crash regex!}}"
return match.group(group)

response = re2.sub(r"{(\d+)}", replace_response_value_with_captured, str(crash["response"]))
response = re2.sub(
r"{(\d+)}",
replace_response_value_with_captured,
str(crash["response"]),
)
yield CrashResponse(name=crash["name"], value=response, inline=True)

async def detect_and_fetch_pastebin_content(self, text: str) -> str:
Expand All @@ -219,15 +234,21 @@ async def process_text(self, text: str, filename="") -> list[CrashResponse]:
responses.extend(await self.process_text(await self.detect_and_fetch_pastebin_content(text)))

if match := await safe_search(
r"([^\n]*Critical error:.*Engine exit[^\n]*\))", text, flags=re2.I | re2.M | re2.S
r"([^\n]*Critical error:.*Engine exit[^\n]*\))",
text,
flags=re2.I | re2.M | re2.S,
):
filename = os.path.basename(filename)
crash = match.group(1)
responses.append(
CrashResponse(
name=f"Crash found in {filename}",
value="It has been attached to this message.",
attachment=File(io.StringIO(crash), filename="Abridged " + filename, force_close=True),
attachment=File(
io.StringIO(crash),
filename="Abridged " + filename,
force_close=True,
),
)
)

Expand Down Expand Up @@ -284,7 +305,8 @@ def _ext_filter(ext: str) -> bool:

async def _obtain_attachments(self, message: Message) -> AsyncGenerator[tuple[str, IO | Exception], None, None]:
cdn_links = re2.findall(
r"(https://(?:cdn.discordapp.com|media.discordapp.net)/attachments/\S+)", message.content
r"(https://(?:cdn.discordapp.com|media.discordapp.net)/attachments/\S+)",
message.content,
)

yield bool(cdn_links or message.attachments)
Expand Down Expand Up @@ -349,7 +371,8 @@ async def process_message(self, message: Message) -> bool:
self.logger.exception(file_or_exc)
responses.append(
CrashResponse(
name="Download failed", value=f"Could not obtain file '{name}' due to `{file_or_exc}`"
name="Download failed",
value=f"Could not obtain file '{name}' due to `{file_or_exc}`",
)
)
continue
Expand Down Expand Up @@ -582,7 +605,10 @@ def _get_fg_log_details(log_file: IO[bytes]):
logger.info("Didn't find all four pieces of information normally found in a log!")
logger.debug(json.dumps(info, indent=2))

mod_loader_logs = filter(lambda l: re2.search("LogSatisfactoryModLoader", l), map(lambda b: b.decode(), lines))
mod_loader_logs = filter(
lambda l: re2.search("LogSatisfactoryModLoader", l),
map(lambda b: b.decode(), lines),
)

for line in mod_loader_logs:
if match := re2.search(r"(?<=v\.)(?P<sml>[\d.]+)", line):
Expand Down
8 changes: 6 additions & 2 deletions fred/cogs/levelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ async def validate_role(self):
if not role:
logpayload["role_id"] = role_id
logger.warning(
"Could not validate someone's level role because the role isn't in the main guild", extra=logpayload
"Could not validate someone's level role because the role isn't in the main guild",
extra=logpayload,
)
return
self.DB_user.rank_role_id = role_id
Expand All @@ -80,7 +81,10 @@ async def validate_role(self):
for member_role in self.member.roles:
if config.RankRoles.fetch_by_role(member_role.id) is not None: # i.e. member_role is a rank role
logpayload["role_id"] = member_role.id
logger.info("Removing a mismatched level role from someone", extra=logpayload)
logger.info(
"Removing a mismatched level role from someone",
extra=logpayload,
)
await self.member.remove_roles(member_role)
logpayload["role_id"] = role.id
logger.info("Removing a mismatched level role from someone", logpayload)
Expand Down
5 changes: 4 additions & 1 deletion fred/cogs/mediaonly.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ async def _process_message(self, message: Message, *, thread: bool) -> bool:
ctx = await self.bot.get_context(message)

if await common.l4_only(ctx):
self.logger.info("Message doesn't contain media but the author is a T3", extra=common.message_info(message))
self.logger.info(
"Message doesn't contain media but the author is a T3",
extra=common.message_info(message),
)
return False

if thread:
Expand Down
5 changes: 4 additions & 1 deletion fred/cogs/webhooklistener.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ def do_POST(self):

# Return error if the payload type is that other weird format instead of a normal json
if content_type != "application/json":
logger.error("POST request has invalid content_type", extra={"content_type": content_type})
logger.error(
"POST request has invalid content_type",
extra={"content_type": content_type},
)
self.send_error(400, "Bad Request", "Expected a JSON request")
return

Expand Down
10 changes: 8 additions & 2 deletions fred/cogs/welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ async def on_member_join(self, member: Member):

async def send_welcome_message(self, member: Member | User):
if welcome := config.Misc.fetch("welcome_message"):
self.logger.info("Sending the welcome message to a new member", extra=common.user_info(member))
self.logger.info(
"Sending the welcome message to a new member",
extra=common.user_info(member),
)
await self.bot.send_safe_direct_message(member, welcome)

if info := config.Misc.fetch("latest_info"):
self.logger.info("Sending the latest information to a new member", extra=common.user_info(member))
self.logger.info(
"Sending the latest information to a new member",
extra=common.user_info(member),
)
info = f"Here's the latest information :\n\n{info}"
await self.bot.send_safe_direct_message(member, info)
11 changes: 10 additions & 1 deletion fred/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@
from typing import Optional, Any, TypedDict

import nextcord
from sqlobject import SQLObject, IntCol, BoolCol, JSONCol, BigIntCol, StringCol, FloatCol, sqlhub
from sqlobject import (
SQLObject,
IntCol,
BoolCol,
JSONCol,
BigIntCol,
StringCol,
FloatCol,
sqlhub,
)
from sqlobject.dberrors import DuplicateEntryError


Expand Down
27 changes: 24 additions & 3 deletions fred/fred.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@ async def safe_send(

return await to.send(content=content, files=files, **kwargs)

async def reply_generic(
self,
target: nextcord.Message | nextcord.Interaction | commands.Context,
content: Optional[str] = None,
propagate_reply: bool = True,
**kwargs,
) -> nextcord.Message:
if isinstance(target, nextcord.Message):
return await self.reply_to_msg(target, content, propagate_reply, **kwargs)
if isinstance(target, nextcord.Interaction):
return await target.send(content, **kwargs)
if isinstance(target, commands.Context):
return await self.reply_to_msg(target.message, content, propagate_reply, **kwargs)
raise TypeError(f"Unsupported type {type(target)}")

async def reply_to_msg(
self,
message: nextcord.Message,
Expand Down Expand Up @@ -275,7 +290,7 @@ def check(message2: nextcord.Message):
await self.reply_to_msg(message, "Timed out and aborted after 120 seconds.")
raise asyncio.TimeoutError

return response.content, response.attachments[0] if response.attachments else None
return response.content, (response.attachments[0] if response.attachments else None)

async def reply_yes_or_no(self, message: nextcord.Message, question: Optional[str] = None, **kwargs) -> bool:
response, _ = await self.reply_question(message, question, **kwargs)
Expand All @@ -300,15 +315,21 @@ async def on_message(self, message: nextcord.Message):
self.logger.info("Processing a DM", extra=common.message_info(message))
if message.content.lower() == "start":
config.Users.fetch(message.author.id).accepts_dms = True
self.logger.info("A user now accepts to receive DMs", extra=common.message_info(message))
self.logger.info(
"A user now accepts to receive DMs",
extra=common.message_info(message),
)
await self.reply_to_msg(
message,
"You will now receive direct messages from me again! If you change your mind, send a message that says `stop`.",
)
return
elif message.content.lower() == "stop":
config.Users.fetch(message.author.id).accepts_dms = False
self.logger.info("A user now refuses to receive DMs", extra=common.message_info(message))
self.logger.info(
"A user now refuses to receive DMs",
extra=common.message_info(message),
)
await self.reply_to_msg(
message,
"You will no longer receive direct messages from me! To resume, send a message that says `start`.",
Expand Down
Loading
Loading