Merge pull request #11 from Klemek/dev

v1.11
This commit is contained in:
Klemek
2021-04-06 23:03:21 +02:00
committed by GitHub
18 changed files with 278 additions and 20 deletions
+7
View File
@@ -1,3 +1,4 @@
![Python Version >= 3.7](https://img.shields.io/badge/python-%3E=3.7%20-blue)
[![Scc Count Badge](https://sloc.xyz/github/klemek/discord-analyst/?category=code)](https://github.com/boyter/scc/#badges-beta)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/Klemek/discord-analyst.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/discord-analyst/context:python)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/Klemek/discord-analyst.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/discord-analyst/alerts/)
@@ -17,6 +18,9 @@
* %freq - frequency analysis
* %compo - composition analysis
* %pres - presence analysis
* %first - read first message
* %rand - read a random message
* %last - read last message
* %emojis - rank emotes by their usage
* arguments:
* <n> - top <n> emojis, default is 20
@@ -96,6 +100,9 @@ python3 src/main.py
## Changelog
* **v1.11**
* more scans `%first`, `%rand`, `%last`
* streak computing in `%pres`
* **v1.10**
* multithreading for queries
* bug fix
+1
View File
@@ -3,3 +3,4 @@ from .frequency import Frequency
from .composition import Composition
from .presence import Presence
from .counter import Counter
from .history import History
+1 -1
View File
@@ -49,7 +49,7 @@ class Counter:
else:
output += f"**#{i + 1}**"
sum = val_sum(self.usages)
output += f" {name} - {plural(sum, counted)} ({percent(sum/total_usage)}) (last {from_now(self.last_used)})"
output += f" {name} - {plural(sum, counted)} ({percent(sum/total_usage)}, last {from_now(self.last_used)})"
top_item = top_key(self.usages)
if top_item != 0 and transform is not None:
if self.usages[top_item] == sum:
+35 -5
View File
@@ -2,7 +2,16 @@ from typing import List
from datetime import timedelta
import calendar
from utils import str_date, str_datetime, from_now, plural, percent, precise, top_key
from utils import (
str_date,
str_datetime,
from_now,
plural,
percent,
precise,
top_key,
mention,
)
class Frequency:
@@ -16,8 +25,19 @@ class Frequency:
self.busiest_day_count = 0
self.busiest_hour = None
self.busiest_hour_count = 0
self.streaks = []
self.last_author = None
self.last_streak_start = None
self.last_streak_author = None
self.longest_streak = None
self.longest_streak_start = None
self.longest_streak_author = None
def to_string(self) -> List[str]:
def to_string(
self,
*,
member_specific: bool,
) -> List[str]:
delta = self.dates[-1] - self.dates[0]
total_msg = len(self.dates)
busiest_weekday = top_key(self.week)
@@ -31,14 +51,24 @@ class Frequency:
n_hours = delta.days
if self.dates[0].hour <= busiest_hour and self.dates[-1].hour >= busiest_hour:
n_hours += 1
return [
ret = [
f"- **earliest message**: {str_datetime(self.dates[0])} ({from_now(self.dates[0])})",
f"- **latest message**: {str_datetime(self.dates[-1])} ({from_now(self.dates[-1])})",
f"- **messages/day**: {precise(total_msg/delta.days, precision=3)}",
f"- **busiest day of week**: {calendar.day_name[busiest_weekday]} (~{precise(self.week[busiest_weekday]/n_weekdays, precision=3)} msg, {percent(self.week[busiest_weekday]/total_msg)})",
f"- **busiest day ever**: {str_date(self.busiest_day)} ({from_now(self.busiest_day)}) ({self.busiest_day_count} msg)",
f"- **busiest day ever**: {str_date(self.busiest_day)} ({from_now(self.busiest_day)}, {self.busiest_day_count} msg)",
f"- **messages/hour**: {precise(total_msg*3600/delta.total_seconds(), precision=3)}",
f"- **busiest hour of day**: {busiest_hour:0>2}:00 (~{precise(self.day[busiest_hour]/n_hours, precision=3)} msg, {percent(self.day[busiest_hour]/total_msg)})",
f"- **busiest hour ever**: {str_datetime(self.busiest_hour)} ({from_now(self.busiest_hour)}) ({self.busiest_hour_count} msg)",
f"- **busiest hour ever**: {str_datetime(self.busiest_hour)} ({from_now(self.busiest_hour)}, {self.busiest_hour_count} msg)",
f"- **longest break**: {plural(round(self.longest_break.total_seconds()/3600), 'hour')} ({plural(self.longest_break.days,'day')}) from {str_datetime(self.longest_break_start)} ({from_now(self.longest_break_start)})",
f"- **avg. streak**: {precise(sum(self.streaks)/len(self.streaks), precision=3)} msg",
]
if member_specific:
ret += [
f"- **longest streak**: {self.longest_streak:,} msg from {str_datetime(self.longest_streak_start)} ({from_now(self.longest_streak_start)})"
]
else:
ret += [
f"- **longest streak**: {mention(self.longest_streak_author)} ({self.longest_streak:,} msg from {str_datetime(self.longest_streak_start)}, {from_now(self.longest_streak_start)})"
]
return ret
+39
View File
@@ -0,0 +1,39 @@
from typing import List
import random
# Custom libs
from utils import mention, from_now, str_datetime, message_link
class History:
def __init__(self):
self.messages = []
def to_string(self, *, type: str) -> List[str]:
if len(self.messages) == 0:
return ["There was no messages matching your filters"]
message = None
intro = None
if type == "first":
self.messages.sort(key=lambda m: m.created_at)
message = self.messages[0]
intro = f"First message out of {len(self.messages):,}"
elif type == "last":
self.messages.sort(key=lambda m: m.created_at, reverse=True)
message = self.messages[0]
intro = f"Last message out of {len(self.messages):,}"
elif type == "random":
message = random.choice(self.messages)
intro = f"Random message out of {len(self.messages):,}"
text = ["> " + line for line in message.content.splitlines()]
if message.attachment:
text += ["> <image>" if message.image else "> <attachment>"]
return [
intro,
f"{str_datetime(message.created_at)} ({from_now(message.created_at)}) {mention(message.author)} said:",
*text,
f"<{message_link(message)}>",
]
+6 -5
View File
@@ -1,4 +1,4 @@
from typing import Union, Tuple
from typing import Union, Tuple, Any
import discord
from . import MessageLog
@@ -9,7 +9,8 @@ FORMAT = 3
class ChannelLogs:
def __init__(self, channel: Union[discord.TextChannel, dict]):
def __init__(self, channel: Union[discord.TextChannel, dict], guild: Any):
self.guild = guild
if isinstance(channel, discord.TextChannel):
self.id = channel.id
self.name = channel.name
@@ -27,7 +28,7 @@ class ChannelLogs:
if channel["last_message_id"] is not None
else None
)
self.messages = [MessageLog(message) for message in channel["messages"]]
self.messages = [MessageLog(message, self) for message in channel["messages"]]
def is_format(self):
return self.format == FORMAT
@@ -44,7 +45,7 @@ class ChannelLogs:
oldest_first=True,
):
self.last_message_id = message.id
m = MessageLog(message)
m = MessageLog(message, self)
await m.load(message)
self.messages.insert(0, m)
yield len(self.messages), False
@@ -64,7 +65,7 @@ class ChannelLogs:
):
done += 1
last_message_id = message.id
m = MessageLog(message)
m = MessageLog(message, self)
await m.load(message)
self.messages += [m]
yield len(self.messages), False
+5 -2
View File
@@ -49,6 +49,7 @@ class Worker:
class GuildLogs:
def __init__(self, guild: discord.Guild):
self.id = guild.id
self.guild = guild
self.log_file = os.path.join(LOG_DIR, f"{guild.id}.logz")
self.channels = {}
@@ -104,7 +105,9 @@ class GuildLogs:
return CANCELLED, 0
await code_message(progress, "Reading saved history (4/4)...")
t0 = datetime.now()
self.channels = {int(id): ChannelLogs(channels[id]) for id in channels}
self.channels = {
int(id): ChannelLogs(channels[id], self) for id in channels
}
# remove invalid format
self.channels = {
id: self.channels[id]
@@ -154,7 +157,7 @@ class GuildLogs:
for channel in target_channels:
if channel.id not in self.channels or fresh:
loading_new += 1
self.channels[channel.id] = ChannelLogs(channel)
self.channels[channel.id] = ChannelLogs(channel, self)
workers += [Worker(self.channels[channel.id], channel)]
warning_msg = "(this might take a while)"
if len(target_channels) > 5 and loading_new > 5:
+3 -2
View File
@@ -1,4 +1,4 @@
from typing import Union
from typing import Union, Any
import discord
from datetime import datetime
@@ -9,7 +9,8 @@ EMBED_IMAGES = ["image", "gifv"]
class MessageLog:
def __init__(self, message: Union[discord.Message, dict]):
def __init__(self, message: Union[discord.Message, dict], channel: Any):
self.channel = channel
if isinstance(message, discord.Message):
self.id = message.id
self.created_at = message.created_at
+27 -1
View File
@@ -1,6 +1,11 @@
import sys
from miniscord import Bot
import logging
if sys.version_info < (3, 7):
print("Please upgrade your Python version to 3.7.0 or higher")
sys.exit(1)
from utils import emojis
from scanners import (
EmotesScanner,
@@ -13,6 +18,9 @@ from scanners import (
MessagesScanner,
ChannelsScanner,
ReactionsScanner,
FirstScanner,
RandomScanner,
LastScanner,
)
from logs import GuildLogs
@@ -24,7 +32,7 @@ emojis.load_emojis()
bot = Bot(
"Discord Analyst",
"1.10",
"1.11",
alias="%",
)
@@ -36,6 +44,24 @@ bot.register_command(
"cancel: stop current analysis",
"```\n" + "%cancel: Stop current analysis\n" + "```",
)
bot.register_command(
"last",
lambda *args: LastScanner().compute(*args),
"last: read last message",
LastScanner.help(),
)
bot.register_command(
"rand(om)?",
lambda *args: RandomScanner().compute(*args),
"rand: read a random message",
RandomScanner.help(),
)
bot.register_command(
"first",
lambda *args: FirstScanner().compute(*args),
"first: read first message",
FirstScanner.help(),
)
bot.register_command(
"mentioned",
lambda *args: MentionedScanner().compute(*args),
+3
View File
@@ -8,3 +8,6 @@ from .mentioned_scanner import MentionedScanner
from .messages_scanner import MessagesScanner
from .channels_scanner import ChannelsScanner
from .reactions_scanner import ReactionsScanner
from .first_scanner import FirstScanner
from .last_scanner import LastScanner
from .random_scanner import RandomScanner
+19
View File
@@ -0,0 +1,19 @@
from typing import List
# Custom libs
from .history_scanner import HistoryScanner
class FirstScanner(HistoryScanner):
@staticmethod
def help() -> str:
return super(FirstScanner, FirstScanner).help(
cmd="first", text="Read first message"
)
def __init__(self):
super().__init__(help=FirstScanner.help())
def get_results(self, intro: str) -> List[str]:
return self.history.to_string(type="first")
+17 -1
View File
@@ -44,7 +44,9 @@ class FrequencyScanner(Scanner):
def get_results(self, intro: str) -> List[str]:
FrequencyScanner.compute_results(self.freq)
res = [intro]
res += self.freq.to_string()
res += self.freq.to_string(
member_specific=self.member_specific,
)
return res
@staticmethod
@@ -64,6 +66,20 @@ class FrequencyScanner(Scanner):
):
impacted = True
freq.dates += [message.created_at]
if message.author == freq.last_author:
freq.streaks[-1] += 1
else:
if len(freq.streaks) > 0 and (
freq.longest_streak is None
or freq.streaks[-1] > freq.longest_streak
):
freq.longest_streak = freq.streaks[-1]
freq.longest_streak_start = freq.last_streak_start
freq.longest_streak_author = freq.last_streak_author
freq.streaks += [1]
freq.last_streak_start = message.created_at
freq.last_streak_author = message.author
freq.last_author = message.author
return impacted
@staticmethod
+1 -1
View File
@@ -63,7 +63,7 @@ class FullScanner(Scanner):
FrequencyScanner.compute_results(self.freq)
res = [intro]
res += ["__Frequency__:"]
res += self.freq.to_string()
res += self.freq.to_string(member_specific=self.member_specific)
res += ["__Composition__:"]
res += self.compo.to_string(self.msg_count)
res += ["__Presence__:"]
+70
View File
@@ -0,0 +1,70 @@
from abc import ABC, abstractmethod
from typing import List
import discord
# Custom libs
from .scanner import Scanner
from data_types import History
from logs import ChannelLogs, MessageLog
from utils import COMMON_HELP_ARGS
class HistoryScanner(Scanner, ABC):
@staticmethod
def help(*, cmd: str, text: str) -> str:
return (
"```\n"
+ f"%{cmd}: {text}\n"
+ "arguments:\n"
+ COMMON_HELP_ARGS
+ "* all/everyone - include bots\n"
+ "Example: %{cmd} #mychannel1 @user\n"
+ "```"
)
def __init__(self, *, help: str):
super().__init__(
has_digit_args=True,
valid_args=["all", "everyone"],
help=help,
intro_context="",
)
async def init(self, message: discord.Message, *args: str) -> bool:
self.history = History()
self.all_messages = "all" in args or "everyone" in args
return True
def compute_message(self, channel: ChannelLogs, message: MessageLog):
return HistoryScanner.analyse_message(
channel,
message,
self.history,
self.raw_members,
all_messages=self.all_messages,
)
@abstractmethod
def get_results(self, intro: str):
pass
@staticmethod
def analyse_message(
channel: ChannelLogs,
message: MessageLog,
history: History,
raw_members: List[int],
*,
all_messages: bool,
) -> bool:
impacted = False
# If author is included in the selection (empty list is all)
if (
(not message.bot or all_messages)
and len(raw_members) == 0
or message.author in raw_members
) and (message.content or message.attachment):
impacted = True
history.messages += [message]
return impacted
+19
View File
@@ -0,0 +1,19 @@
from typing import List
# Custom libs
from .history_scanner import HistoryScanner
class LastScanner(HistoryScanner):
@staticmethod
def help() -> str:
return super(LastScanner, LastScanner).help(
cmd="last", text="Read last message"
)
def __init__(self):
super().__init__(help=LastScanner.help())
def get_results(self, intro: str) -> List[str]:
return self.history.to_string(type="last")
+19
View File
@@ -0,0 +1,19 @@
from typing import List
# Custom libs
from .history_scanner import HistoryScanner
class RandomScanner(HistoryScanner):
@staticmethod
def help() -> str:
return super(RandomScanner, RandomScanner).help(
cmd="rand", text="Read a random message"
)
def __init__(self):
super().__init__(help=RandomScanner.help())
def get_results(self, intro: str) -> List[str]:
return self.history.to_string(type="random")
+2 -2
View File
@@ -5,7 +5,7 @@ import logging
import re
import discord
from utils import no_duplicate, get_intro, delta, deltas, mention, channel_mention
from utils import no_duplicate, get_intro, delta
from logs import GuildLogs, ChannelLogs, MessageLog, ALREADY_RUNNING, CANCELLED
@@ -173,5 +173,5 @@ class Scanner(ABC):
pass
@abstractmethod
def get_results(self, intro: str):
def get_results(self, intro: str) -> List[str]:
pass
+4
View File
@@ -51,6 +51,10 @@ def channel_mention(channel_id: int) -> str:
return f"<#{channel_id}>"
def message_link(message: discord.Message) -> str:
return f"https://discord.com/channels/{message.channel.guild.id}/{message.channel.id}/{message.id}"
class FakeMessage:
def __init__(self, id: int):
self.id = id