Skip to content

Commit fbb8dcc

Browse files
authored
Merge pull request #309 from rHomelab/feat/report_cog
2 parents 02fb998 + 1f35fce commit fbb8dcc

File tree

2 files changed

+157
-124
lines changed

2 files changed

+157
-124
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -323,15 +323,16 @@ Allows roles to be applied and removed using reactions.
323323

324324
This cog will allow members to send a report into a channel where it can be reviewed and actioned upon by moderators.
325325

326-
- `[p]reports logchannel #admin-log` - For reports to be able to be taken, a log channel must be set which will receive an embed upon a user using the report command.
326+
The `report` and `emergency` commands have cooldowns defined; if a user attempts to use one of these commands more than once within a 30 second period, they will be rate limited and receive a message informing them of this.
327327

328-
- `[p]reports channel [allow|deny] [channel]` - Disallow the `report`/`emergency` commands to be used in certain channels
328+
- `[p]reports confirmation <true|false>` - Sets whether the bot will send users a confirmation/copy of their report.
329+
- `[p]reports logchannel #admin-log` - Set the channnel to which reports will be sent. ⚠️ The cog will not function without this.
330+
- `[p]reports status` - Output the cog's configurationn status.
331+
- `[p]report <message>` - Sends a report with the given message.
332+
- `[p]emergency <message>` - Sends a report with the given message, mentioning (@'ing) all users in the configured `logchannel` who are in either an online or idle state.
329333

330-
- `[p]reports confirm [true|false]` - When a report is issued, this sets whether the bot will DM the user with confirmation or not
331-
332-
- `[p]report [message]` - A report can be sent to the logchannel for any moderators to see and action upon when they are ready.
333-
334-
- `[p]emergency [message]` - An emergency can be requested which will ping all members in the configured logchannel if they are online.
334+
> [!TIP]
335+
> The `report` and `emergency` commands are also implemented as slash commands.
335336
336337
### role\_welcome
337338

report/report.py

Lines changed: 149 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""discord red-bot report cog"""
22

33
import logging
4+
from typing import Literal, TypeAlias
45

56
import discord
67
from redbot.core import Config, checks, commands
@@ -9,6 +10,9 @@
910

1011
logger = logging.getLogger("red.rhomelab.report")
1112

13+
TextLikeChannnel: TypeAlias = discord.VoiceChannel | discord.StageChannel | discord.TextChannel | discord.Thread
14+
GuildChannelOrThread: TypeAlias = "discord.guild.GuildChannel | discord.Thread"
15+
1216

1317
class ReportCog(commands.Cog):
1418
"""Report Cog"""
@@ -23,13 +27,11 @@ def __init__(self, bot: Red):
2327
default_guild_settings = {
2428
"logchannel": None,
2529
"confirmations": True,
26-
# {"id": str, "allowed": bool} bool defaults to True
27-
"channels": [],
2830
}
2931

3032
self.config.register_guild(**default_guild_settings)
3133

32-
def _is_valid_channel(self, channel: "discord.guild.GuildChannel | None"):
34+
def _is_valid_channel(self, channel: "GuildChannelOrThread | None") -> TextLikeChannnel | Literal[False]:
3335
if channel is not None and not isinstance(channel, (discord.ForumChannel, discord.CategoryChannel)):
3436
return channel
3537
return False
@@ -41,179 +43,209 @@ async def _reports(self, ctx: commands.Context):
4143
pass
4244

4345
@_reports.command("logchannel")
46+
@commands.guild_only()
4447
async def reports_logchannel(self, ctx: commands.GuildContext, channel: discord.TextChannel):
45-
"""Sets the channel to post the reports
48+
"""Sets the channel to post the reports.
4649
4750
Example:
4851
- `[p]reports logchannel <channel>`
4952
- `[p]reports logchannel #admin-log`
5053
"""
54+
if channel.permissions_for(ctx.me).send_messages is False:
55+
await ctx.send("❌ I do not have permission to send messages in that channel.")
56+
return
5157
await self.config.guild(ctx.guild).logchannel.set(channel.id)
52-
await ctx.send(f"Reports log message channel set to `{channel.name}`")
58+
await ctx.send(f"Reports log message channel set to {channel.mention}")
5359

54-
@_reports.command("confirm")
60+
@_reports.command("confirmation")
61+
@commands.guild_only()
5562
async def reports_confirm(self, ctx: commands.GuildContext, option: str):
56-
"""Changes if confirmations should be sent to reporters upon a report/emergency.
63+
"""Whether a confirmation should be sent to reporters.
5764
5865
Example:
5966
- `[p]reports confirm <True|False>`
6067
"""
6168
try:
6269
confirmation = strtobool(option)
6370
except ValueError:
64-
await ctx.send("Invalid option. Use: `[p]reports confirm <True|False>`")
71+
await ctx.send("Invalid option. Use: `[p]reports confirm <True|False>`")
6572
return
6673
await self.config.guild(ctx.guild).confirmations.set(confirmation)
67-
await ctx.send(f"Send report confirmations: `{confirmation}`")
74+
await ctx.send(f"✅ Report confirmations {'enabled' if confirmation else 'disabled'}")
75+
76+
@_reports.command("status")
77+
@commands.guild_only()
78+
async def reports_status(self, ctx: commands.GuildContext):
79+
"""Status of the cog."""
80+
reports_channel_id = await self.config.guild(ctx.guild).logchannel()
81+
report_confirmations = await self.config.guild(ctx.guild).confirmations()
82+
83+
if reports_channel_id:
84+
reports_channel = ctx.guild.get_channel(reports_channel_id)
85+
if reports_channel:
86+
reports_channel = reports_channel.mention
87+
else:
88+
reports_channel = f"Set to channel ID {reports_channel_id}, but channel could not be found!"
89+
else:
90+
reports_channel = "Unset"
91+
92+
try:
93+
await ctx.send(
94+
embed=discord.Embed(colour=await ctx.embed_colour())
95+
.add_field(name="Reports Channel", value=reports_channel)
96+
.add_field(name="Report Confirmations", value=report_confirmations)
97+
)
98+
except discord.Forbidden:
99+
await ctx.send("I need the `Embed links` permission to send status.")
68100

69-
@commands.command("report")
101+
@commands.hybrid_command("report")
102+
@commands.cooldown(1, 30.0, commands.BucketType.user)
70103
@commands.guild_only()
71104
async def cmd_report(self, ctx: commands.GuildContext, *, message: str):
72-
"""Sends a report to the mods for possible intervention
105+
"""Send a report to the mods.
73106
74107
Example:
75108
- `[p]report <message>`
76109
"""
77-
pre_check = await self.enabled_channel_check(ctx)
78-
if not pre_check:
79-
return
80-
81-
# Pre-emptively delete the message for privacy reasons
82-
await ctx.message.delete()
110+
await self.do_report(ctx.channel, ctx.message, message, False, ctx.interaction)
83111

84-
log_id = await self.config.guild(ctx.guild).logchannel()
85-
log = None
86-
if log_id:
87-
log = ctx.guild.get_channel(log_id)
88-
else:
89-
logger.warning(f"No log channel set for guild {ctx.guild}")
90-
if not log:
91-
# Failed to get the channel
92-
logger.warning(f"Failed to get log channel {log_id}, in guild {ctx.guild}")
93-
return
94-
95-
data = self.make_report_embed(ctx, message, emergency=False)
96-
if log_channel := self._is_valid_channel(log):
97-
await log_channel.send(embed=data)
98-
else:
99-
logger.warning(f"Failed to get log channel {log_id}, is a invalid channel")
100-
101-
confirm = await self.config.guild(ctx.guild).confirmations()
102-
if confirm:
103-
report_reply = self.make_reporter_reply(ctx, message, False)
104-
try:
105-
await ctx.author.send(embed=report_reply)
106-
except discord.Forbidden:
107-
pass
112+
@cmd_report.error
113+
async def on_cmd_report_error(self, ctx: commands.GuildContext, error):
114+
if isinstance(error, commands.CommandOnCooldown):
115+
if ctx.interaction is not None:
116+
await ctx.interaction.response.send_message(str(error), ephemeral=True)
117+
else:
118+
await ctx.message.delete()
119+
await ctx.author.send(f"You are on cooldown. Try again in <t:{error.retry_after}:R>")
108120

109-
@commands.command("emergency")
121+
@commands.hybrid_command("emergency")
122+
@commands.cooldown(1, 30.0, commands.BucketType.user)
110123
@commands.guild_only()
111124
async def cmd_emergency(self, ctx: commands.GuildContext, *, message: str):
112-
"""Pings the mods with a report for possible intervention
125+
"""Pings the mods with a high-priority report.
113126
114127
Example:
115128
- `[p]emergency <message>`
116129
"""
117-
pre_check = await self.enabled_channel_check(ctx)
118-
if not pre_check:
119-
return
130+
await self.do_report(ctx.channel, ctx.message, message, True, ctx.interaction)
120131

121-
# Pre-emptively delete the message for privacy reasons
122-
await ctx.message.delete()
132+
@cmd_report.error
133+
async def on_cmd_emergency_error(self, ctx: commands.GuildContext, error):
134+
if isinstance(error, commands.CommandOnCooldown):
135+
if ctx.interaction is not None:
136+
await ctx.interaction.response.send_message(str(error), ephemeral=True)
137+
else:
138+
await ctx.message.delete()
139+
await ctx.author.send(f"You are on cooldown. Try again in <t:{error.retry_after}:R>")
123140

124-
log_id = await self.config.guild(ctx.guild).logchannel()
141+
async def get_log_channel(self, guild: discord.Guild) -> TextLikeChannnel | None:
142+
"""Gets the log channel for the guild"""
143+
log_id = await self.config.guild(guild).logchannel()
125144
log = None
126-
if log_id:
127-
log = ctx.guild.get_channel(log_id)
128-
else:
129-
logger.warning(f"No log channel set for guild {ctx.guild}")
145+
if not log_id:
146+
logger.warning(f"No log channel set for guild {guild}")
147+
return
148+
149+
log = guild.get_channel(log_id)
130150
if not log:
131151
# Failed to get the channel
132-
logger.warning(f"Failed to get log channel {log_id}, in guild {ctx.guild}")
152+
logger.warning(f"Failed to get log channel {log_id} in guild {guild}")
133153
return
134154

135-
data = self.make_report_embed(ctx, message, emergency=True)
136-
if channel := self._is_valid_channel(log):
137-
mod_pings = " ".join([i.mention for i in channel.members if not i.bot and str(i.status) in ["online", "idle"]])
138-
if not mod_pings: # If no online/idle mods
139-
mod_pings = " ".join([i.mention for i in channel.members if not i.bot])
140-
await channel.send(content=mod_pings, embed=data)
141-
142-
confirm = await self.config.guild(ctx.guild).confirmations()
143-
if confirm:
144-
report_reply = self.make_reporter_reply(ctx, message, True)
145-
try:
146-
await ctx.author.send(embed=report_reply)
147-
except discord.Forbidden:
148-
pass
155+
if log_channel := self._is_valid_channel(log):
156+
return log_channel
149157
else:
150158
logger.warning(f"Failed to get log channel {log_id}, is a invalid channel")
151-
152-
@_reports.command("channel")
153-
async def reports_channel(self, ctx: commands.GuildContext, rule: str, channel: discord.TextChannel):
154-
"""Allows/denies the use of reports/emergencies in specific channels
155-
156-
Example:
157-
- `[p]reports channel <allow|deny> <channel>`
158-
- `[p]reports channel deny #general
159-
"""
160-
supported_rules = ("deny", "allow")
161-
if rule.lower() not in supported_rules:
162-
await ctx.send("Rule argument must be `allow` or `deny`")
163159
return
164160

165-
bool_conversion = bool(supported_rules.index(rule.lower()))
166-
167-
async with self.config.guild(ctx.guild).channels() as channels:
168-
data = [c for c in channels if c["id"] == str(channel.id)]
169-
if data:
170-
data[0]["allowed"] = bool_conversion
171-
else:
172-
channels.append(
173-
{
174-
"id": str(channel.id),
175-
"allowed": bool_conversion,
176-
}
161+
async def do_report(
162+
self,
163+
channel: "discord.guild.GuildChannel | discord.Thread",
164+
message: discord.Message,
165+
report_body: str,
166+
emergency: bool,
167+
interaction: discord.Interaction | None,
168+
):
169+
"""Sends a report to the mods for possible intervention"""
170+
# Pre-emptively delete the message for privacy reasons
171+
if interaction is None:
172+
await message.delete()
173+
174+
log_channel = await self.get_log_channel(channel.guild)
175+
if log_channel is None:
176+
if channel.guild.owner is not None:
177+
report_msg = f"\nUser report: {report_body}" if report_body else ""
178+
await channel.guild.owner.send(
179+
f"⚠️ User {message.author.mention} attempted to make a report in {channel.jump_url}, "
180+
+ "but the cog is misconfigured. Please check the logs."
181+
+ report_msg
177182
)
183+
return
178184

179-
await ctx.send("Reports {} in {}".format("allowed" if bool_conversion else "denied", channel.mention))
180-
181-
async def enabled_channel_check(self, ctx: commands.GuildContext) -> bool:
182-
"""Checks that reports/emergency commands are enabled in the current channel"""
183-
async with self.config.guild(ctx.guild).channels() as channels:
184-
channel = [c for c in channels if c["id"] == str(ctx.channel.id)]
185+
embed = await self.make_report_embed(channel, message, report_body, emergency)
186+
msg_body = None
187+
if isinstance(channel, TextLikeChannnel):
188+
# Ping online and idle mods or all mods if none with such a status are found.
189+
if emergency:
190+
channel_members = [
191+
channel.guild.get_member(i.id) if isinstance(i, discord.ThreadMember) else i for i in channel.members
192+
]
193+
msg_body = " ".join(
194+
[
195+
i.mention
196+
for i in channel_members
197+
if i is not None and not i.bot and i.status in [discord.Status.online, discord.Status.idle]
198+
]
199+
or [i.mention for i in channel_members if i is not None and not i.bot]
200+
)
185201

186-
if channel:
187-
return channel[0]["allowed"]
202+
await log_channel.send(content=msg_body, embed=embed)
188203

189-
# Insert an entry for this channel if it doesn't exist
190-
channels.append({"id": str(ctx.channel.id), "allowed": True})
191-
return True
204+
confirm = await self.config.guild(channel.guild).confirmations()
205+
if confirm:
206+
report_reply = self.make_reporter_reply(channel.guild, channel, report_body, emergency)
207+
try:
208+
if interaction is not None:
209+
await interaction.response.send_message(embed=report_reply, ephemeral=True)
210+
else:
211+
await message.author.send(embed=report_reply)
212+
except discord.Forbidden:
213+
logger.warning(f"Failed to send report confirmation to {message.author.global_name} ({message.author.id})")
214+
pass
192215

193-
def make_report_embed(self, ctx: commands.GuildContext, message: str, emergency: bool) -> discord.Embed:
194-
"""Construct the embed to be sent"""
195-
return (
216+
async def make_report_embed(
217+
self, channel: GuildChannelOrThread, message: discord.Message, report_body: str, emergency: bool
218+
) -> discord.Embed:
219+
embed = (
196220
discord.Embed(
197221
colour=discord.Colour.red() if emergency else discord.Colour.orange(),
198-
description=escape(message or "<no message>"),
199222
)
200-
.set_author(name="Report", icon_url=ctx.author.display_avatar.url)
201-
.add_field(name="Reporter", value=ctx.author.mention)
202-
.add_field(name="Channel", value=ctx.channel.mention)
203-
.add_field(name="Timestamp", value=f"<t:{int(ctx.message.created_at.timestamp())}:F>")
223+
.set_author(name="Report", icon_url=message.author.display_avatar.url)
224+
.add_field(name="Reporter", value=message.author.mention)
225+
.add_field(name="Timestamp", value=f"<t:{int(message.created_at.timestamp())}:F>")
204226
)
205227

206-
def make_reporter_reply(self, ctx: commands.GuildContext, message: str, emergency: bool) -> discord.Embed:
228+
if isinstance(channel, TextLikeChannnel):
229+
last_msg = [msg async for msg in channel.history(limit=1, before=message.created_at)][0] # noqa: RUF015
230+
embed.add_field(name="Context Region", value=last_msg.jump_url if last_msg else "No messages found")
231+
else:
232+
embed.add_field(name="Channel", value=message.channel.mention) # type: ignore
233+
234+
embed.add_field(name="Report Content", value=escape(report_body or "<no message>"))
235+
return embed
236+
237+
def make_reporter_reply(
238+
self, guild: discord.Guild, channel: GuildChannelOrThread, report_body: str, emergency: bool
239+
) -> discord.Embed:
207240
"""Construct the reply embed to be sent"""
241+
guild_icon = guild.icon
208242
return (
209243
discord.Embed(
210244
colour=discord.Colour.red() if emergency else discord.Colour.orange(),
211-
description=escape(message or "<no message>"),
212245
)
213-
.set_author(name="Report Received", icon_url=ctx.author.display_avatar.url)
214-
.add_field(name="Server", value=ctx.guild.name)
215-
.add_field(name="Channel", value=ctx.channel.mention)
216-
.add_field(name="Timestamp", value=f"<t:{int(ctx.message.created_at.timestamp())}:F>")
246+
.set_author(name="Report Received", icon_url=guild_icon.url if guild_icon else None)
247+
.add_field(name="Report Origin", value=channel.mention)
248+
.add_field(name="Report Content", value=escape(report_body or "<no message>"))
217249
)
218250

219251

0 commit comments

Comments
 (0)