1
1
"""discord red-bot report cog"""
2
2
3
3
import logging
4
+ from typing import Literal , TypeAlias
4
5
5
6
import discord
6
7
from redbot .core import Config , checks , commands
9
10
10
11
logger = logging .getLogger ("red.rhomelab.report" )
11
12
13
+ TextLikeChannnel : TypeAlias = discord .VoiceChannel | discord .StageChannel | discord .TextChannel | discord .Thread
14
+ GuildChannelOrThread : TypeAlias = "discord.guild.GuildChannel | discord.Thread"
15
+
12
16
13
17
class ReportCog (commands .Cog ):
14
18
"""Report Cog"""
@@ -23,13 +27,11 @@ def __init__(self, bot: Red):
23
27
default_guild_settings = {
24
28
"logchannel" : None ,
25
29
"confirmations" : True ,
26
- # {"id": str, "allowed": bool} bool defaults to True
27
- "channels" : [],
28
30
}
29
31
30
32
self .config .register_guild (** default_guild_settings )
31
33
32
- def _is_valid_channel (self , channel : "discord.guild.GuildChannel | None" ):
34
+ def _is_valid_channel (self , channel : "GuildChannelOrThread | None" ) -> TextLikeChannnel | Literal [ False ] :
33
35
if channel is not None and not isinstance (channel , (discord .ForumChannel , discord .CategoryChannel )):
34
36
return channel
35
37
return False
@@ -41,179 +43,209 @@ async def _reports(self, ctx: commands.Context):
41
43
pass
42
44
43
45
@_reports .command ("logchannel" )
46
+ @commands .guild_only ()
44
47
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.
46
49
47
50
Example:
48
51
- `[p]reports logchannel <channel>`
49
52
- `[p]reports logchannel #admin-log`
50
53
"""
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
51
57
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 } " )
53
59
54
- @_reports .command ("confirm" )
60
+ @_reports .command ("confirmation" )
61
+ @commands .guild_only ()
55
62
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.
57
64
58
65
Example:
59
66
- `[p]reports confirm <True|False>`
60
67
"""
61
68
try :
62
69
confirmation = strtobool (option )
63
70
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>`" )
65
72
return
66
73
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." )
68
100
69
- @commands .command ("report" )
101
+ @commands .hybrid_command ("report" )
102
+ @commands .cooldown (1 , 30.0 , commands .BucketType .user )
70
103
@commands .guild_only ()
71
104
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.
73
106
74
107
Example:
75
108
- `[p]report <message>`
76
109
"""
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 )
83
111
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>" )
108
120
109
- @commands .command ("emergency" )
121
+ @commands .hybrid_command ("emergency" )
122
+ @commands .cooldown (1 , 30.0 , commands .BucketType .user )
110
123
@commands .guild_only ()
111
124
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.
113
126
114
127
Example:
115
128
- `[p]emergency <message>`
116
129
"""
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 )
120
131
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>" )
123
140
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 ()
125
144
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 )
130
150
if not log :
131
151
# 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 } " )
133
153
return
134
154
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
149
157
else :
150
158
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`" )
163
159
return
164
160
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"\n User 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
177
182
)
183
+ return
178
184
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
+ )
185
201
186
- if channel :
187
- return channel [0 ]["allowed" ]
202
+ await log_channel .send (content = msg_body , embed = embed )
188
203
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
192
215
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 = (
196
220
discord .Embed (
197
221
colour = discord .Colour .red () if emergency else discord .Colour .orange (),
198
- description = escape (message or "<no message>" ),
199
222
)
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>" )
204
226
)
205
227
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 :
207
240
"""Construct the reply embed to be sent"""
241
+ guild_icon = guild .icon
208
242
return (
209
243
discord .Embed (
210
244
colour = discord .Colour .red () if emergency else discord .Colour .orange (),
211
- description = escape (message or "<no message>" ),
212
245
)
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>" ))
217
249
)
218
250
219
251
0 commit comments