Merge pull request #1 from Klemek/dev

v1.1
This commit is contained in:
Klemek
2019-11-14 21:07:49 +01:00
committed by GitHub
5 changed files with 81 additions and 46 deletions
+8
View File
@@ -43,3 +43,11 @@ You will need:
``` ```
python3 bot.py python3 bot.py
``` ```
## Changelog
* **v1.1**:
* coma separator for big numbers
* history loading by chunks for big channels (performance increase)
* bug fix
* **v1.0**: stable release
+28 -9
View File
@@ -8,7 +8,7 @@ import emotes
import help import help
from utils import debug from utils import debug
VERSION = "1.0" VERSION = "1.1"
t0 = datetime.now() t0 = datetime.now()
# Loading token # Loading token
@@ -18,6 +18,25 @@ token = os.getenv('DISCORD_TOKEN')
client = discord.Client() client = discord.Client()
async def info(message, args):
"""
Computes the %info command
:param message: message sent
:type message: :class:`discord.Message`
:param args: arguments of the command
:type args: list[:class:`str`]
"""
await message.channel.send(f"```Discord Analyst v{VERSION} started at {t0:%Y-%m-%d %H:%M}```")
COMMANDS = {
"%help": help.compute,
"%emotes": emotes.compute,
"%info": info
}
@client.event @client.event
async def on_ready(): async def on_ready():
""" """
@@ -47,6 +66,13 @@ async def on_message(message):
if message.author == client.user: if message.author == client.user:
return return
args = message.content.split(" ")
if len(args) < 1 or args[0] not in COMMANDS:
return
debug(message, f"command '{message.content}'")
# Check if bot can respond on current channel or DM user # Check if bot can respond on current channel or DM user
permissions = message.channel.permissions_for(message.guild.me) permissions = message.channel.permissions_for(message.guild.me)
if not permissions.send_messages: if not permissions.send_messages:
@@ -58,14 +84,7 @@ async def on_message(message):
return return
# Redirect to the correct command # Redirect to the correct command
args = message.content.split(" ") await COMMANDS[args[0]](message, args)
if args[0] == "%info":
debug(message, f"command '{message.content}'")
await message.channel.send(f"Discord Analyst v{VERSION} started at {t0.isoformat()}")
if args[0] == "%help":
await help.compute(message, args)
if args[0] == "%emotes":
await emotes.compute(message, args)
# Launch client # Launch client
+44 -35
View File
@@ -6,6 +6,10 @@ import re
import help import help
from utils import debug, aggregate, no_duplicate from utils import debug, aggregate, no_duplicate
# CONSTANTS
CHUNK_SIZE = 10000
# MAIN # MAIN
@@ -18,7 +22,6 @@ async def compute(message, args):
:param args: arguments of the command :param args: arguments of the command
:type args: list[:class:`str`] :type args: list[:class:`str`]
""" """
debug(message, f"command '{message.content}'")
guild = message.guild guild = message.guild
@@ -81,6 +84,7 @@ class Emote:
:ivar last_used: date of last use :ivar last_used: date of last use
:vartype last_used: datetime :vartype last_used: datetime
""" """
def __init__(self, emoji): def __init__(self, emoji):
self.emoji = emoji self.emoji = emoji
self.usages = 0 self.usages = 0
@@ -157,32 +161,37 @@ async def analyse_channel(channel, emotes, members, progress, nm0, nc):
nm = 0 nm = 0
nmm = 0 nmm = 0
try: try:
# Read ALL messages from the channel (pretty long : 300 msg/s) messages = [None]
async for m in channel.history(limit=None): while len(messages) >= CHUNK_SIZE or messages[-1] is None:
# If author is not bot or included in the selection (empty list is all) messages = await channel.history(limit=CHUNK_SIZE, before=messages[-1]).flatten()
if not m.author.bot and (len(members) == 0 or m.author in members): for m in messages:
# Find all emotes un the current message in the form "<:emoji:123456789>" # If author is not bot or included in the selection (empty list is all)
# Filter for known emotes if not m.author.bot and (len(members) == 0 or m.author in members):
found = [name for name in re.findall(r"(<:\w+:\d+>)", m.content) if name in emotes] # Find all emotes un the current message in the form "<:emoji:123456789>"
# For each emote, update its usage # Filter for known emotes
for name in found: found = [name for name in re.findall(r"(<:\w+:\d+>)", m.content) if name in emotes]
emotes[name].usages += 1 # For each emote, update its usage
emotes[name].update_use(m.created_at) for name in found:
# Count this message as impacted emotes[name].usages += 1
nmm += 1 emotes[name].update_use(m.created_at)
# If we include all members, get reactions # Count this message as impacted
if len(members) == 0: nmm += 1
# For each reaction of this message, test if known emote and update when it's the case # For each reaction of this message, test if known emote and update when it's the case
for reaction in m.reactions: for reaction in m.reactions:
name = str(reaction.emoji) name = str(reaction.emoji)
# reaction.emoji can be only str, we don't want that # reaction.emoji can be only str, we don't want that
if not (isinstance(reaction.emoji, str)) and name in emotes: if not (isinstance(reaction.emoji, str)) and name in emotes:
emotes[name].reactions += reaction.count if len(members) == 0:
emotes[name].update_use(m.created_at) emotes[name].reactions += reaction.count
# Count this message as treated and show progress every 1k messages emotes[name].update_use(m.created_at)
nm += 1 """ else:
if (nm0 + nm) % 1000 == 0: users = await reaction.users().flatten()
await progress.edit(content=f"```{(nm0 + nm) // 1000}k messages and {nc} channels analysed```") for member in members:
if member in users:
emotes[name].reactions += 1
emotes[name].update_use(m.created_at)"""
nm += len(messages)
await progress.edit(content=f"```{nm0 + nm:,} messages and {nc} channels analysed```")
return nm, nmm return nm, nmm
except discord.errors.HTTPException: except discord.errors.HTTPException:
# When an exception occurs (like Forbidden) sent -1 # When an exception occurs (like Forbidden) sent -1
@@ -252,29 +261,29 @@ def get_intro(emotes, full, channels, members, nmm, nc):
if len(members) == 0: if len(members) == 0:
# Full scan of the server # Full scan of the server
if full: if full:
return f"{len(emotes)} emotes in this server ({nc} channels, {nmm} messages):" return f"{len(emotes)} emotes in this server ({nc} channels, {nmm:,} messages):"
elif len(channels) < 5: elif len(channels) < 5:
return f"{aggregate([c.mention for c in channels])} emotes usage in {nmm} messages:" return f"{aggregate([c.mention for c in channels])} emotes usage in {nmm:,} messages:"
else: else:
return f"These {len(channels)} channels emotes usage in {nmm} messages:" return f"These {len(channels)} channels emotes usage in {nmm:,} messages:"
elif len(members) < 5: elif len(members) < 5:
if full: if full:
return f"{aggregate([m.mention for m in members])} emotes usage in {nmm} messages:" return f"{aggregate([m.mention for m in members])} emotes usage in {nmm:,} messages:"
elif len(channels) < 5: elif len(channels) < 5:
return f"{aggregate([m.mention for m in members])} on {aggregate([c.mention for c in channels])} " \ return f"{aggregate([m.mention for m in members])} on {aggregate([c.mention for c in channels])} " \
f"emotes usage in {nmm} messages:" f"emotes usage in {nmm:,} messages:"
else: else:
return f"{aggregate([m.mention for m in members])} on these {len(channels)} channels " \ return f"{aggregate([m.mention for m in members])} on these {len(channels)} channels " \
f"emotes usage in {nmm} messages:" f"emotes usage in {nmm:,} messages:"
else: else:
if full: if full:
return f"These {len(members)} members emotes usage in {nmm} messages:" return f"These {len(members)} members emotes usage in {nmm:,} messages:"
elif len(channels) < 5: elif len(channels) < 5:
return f"These {len(members)} members on {aggregate([c.mention for c in channels])} " \ return f"These {len(members)} members on {aggregate([c.mention for c in channels])} " \
f"emotes usage in {nmm} messages:" f"emotes usage in {nmm:,} messages:"
else: else:
return f"These {len(members)} members on these {len(channels)} channels " \ return f"These {len(members)} members on these {len(channels)} channels " \
f"emotes usage in {nmm} messages:" f"emotes usage in {nmm:,} messages:"
def get_place(i): def get_place(i):
@@ -308,7 +317,7 @@ def get_usage(emote):
elif emote.usages == 1: elif emote.usages == 1:
return "1 time " return "1 time "
else: else:
return f"{emote.usages} times " return f"{emote.usages:,} times "
def get_reactions(emote): def get_reactions(emote):
@@ -323,7 +332,7 @@ def get_reactions(emote):
elif emote.reactions == 1: elif emote.reactions == 1:
return "and 1 reaction " return "and 1 reaction "
else: else:
return f"and {emote.reactions} reactions " return f"and {emote.reactions:,} reactions "
def get_life(emote, show_life): def get_life(emote, show_life):
@@ -377,6 +386,6 @@ def get_total(emotes, nmm):
nu += emotes[name].usages nu += emotes[name].usages
nr += emotes[name].reactions nr += emotes[name].reactions
if nr > 0: if nr > 0:
return f"Total: {nu} times ({round(nu / nmm, 4)} / message) and {nr} reactions" return f"Total: {nu:,} times ({nu / nmm:.4f} / message) and {nr:,} reactions"
else: else:
return f"Total: {nu} times ({round(nu / nmm, 4)} / message)" return f"Total: {nu:,} times ({nu / nmm:.4f} / message)"
-2
View File
@@ -1,4 +1,3 @@
from utils import debug
async def compute(message, args): async def compute(message, args):
@@ -10,7 +9,6 @@ async def compute(message, args):
:param args: arguments of the command :param args: arguments of the command
:type args: list[str] :type args: list[str]
""" """
debug(message, f"command '{message.content}'")
# Select correct response to send # Select correct response to send
+1
View File
@@ -1,5 +1,6 @@
# DISCORD API # DISCORD API
def debug(message, txt): def debug(message, txt):
""" """
Print a log with the context of the current event Print a log with the context of the current event