From e9505fa08213019cd88caadeda82039b662bb8d1 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 6 Apr 2021 23:01:51 +0200 Subject: [PATCH] history commands --- README.md | 4 ++ src/data_types/__init__.py | 1 + src/data_types/history.py | 39 ++++++++++++++++++ src/logs/channel_logs.py | 11 +++--- src/logs/guild_logs.py | 7 +++- src/logs/message_log.py | 5 ++- src/main.py | 21 ++++++++++ src/scanners/__init__.py | 3 ++ src/scanners/first_scanner.py | 19 +++++++++ src/scanners/history_scanner.py | 70 +++++++++++++++++++++++++++++++++ src/scanners/last_scanner.py | 19 +++++++++ src/scanners/random_scanner.py | 19 +++++++++ src/scanners/scanner.py | 4 +- src/utils/utils.py | 4 ++ 14 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 src/data_types/history.py create mode 100644 src/scanners/first_scanner.py create mode 100644 src/scanners/history_scanner.py create mode 100644 src/scanners/last_scanner.py create mode 100644 src/scanners/random_scanner.py diff --git a/README.md b/README.md index be05a51..a3d936b 100644 --- a/README.md +++ b/README.md @@ -18,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: * - top emojis, default is 20 @@ -98,6 +101,7 @@ python3 src/main.py ## Changelog * **v1.11** + * more scans `%first`, `%rand`, `%last` * streak computing in `%pres` * **v1.10** * multithreading for queries diff --git a/src/data_types/__init__.py b/src/data_types/__init__.py index 38daabd..e4be59f 100644 --- a/src/data_types/__init__.py +++ b/src/data_types/__init__.py @@ -3,3 +3,4 @@ from .frequency import Frequency from .composition import Composition from .presence import Presence from .counter import Counter +from .history import History diff --git a/src/data_types/history.py b/src/data_types/history.py new file mode 100644 index 0000000..27dff33 --- /dev/null +++ b/src/data_types/history.py @@ -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 += ["> " if message.image else "> "] + + return [ + intro, + f"{str_datetime(message.created_at)} ({from_now(message.created_at)}) {mention(message.author)} said:", + *text, + f"<{message_link(message)}>", + ] diff --git a/src/logs/channel_logs.py b/src/logs/channel_logs.py index 91ce4df..615fcd1 100644 --- a/src/logs/channel_logs.py +++ b/src/logs/channel_logs.py @@ -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 diff --git a/src/logs/guild_logs.py b/src/logs/guild_logs.py index ed96ad6..7f3cecb 100644 --- a/src/logs/guild_logs.py +++ b/src/logs/guild_logs.py @@ -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: diff --git a/src/logs/message_log.py b/src/logs/message_log.py index fe733b5..b77ea49 100644 --- a/src/logs/message_log.py +++ b/src/logs/message_log.py @@ -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 diff --git a/src/main.py b/src/main.py index 3b987e9..2f73357 100644 --- a/src/main.py +++ b/src/main.py @@ -18,6 +18,9 @@ from scanners import ( MessagesScanner, ChannelsScanner, ReactionsScanner, + FirstScanner, + RandomScanner, + LastScanner, ) from logs import GuildLogs @@ -41,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), diff --git a/src/scanners/__init__.py b/src/scanners/__init__.py index 510c886..37eaa77 100644 --- a/src/scanners/__init__.py +++ b/src/scanners/__init__.py @@ -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 \ No newline at end of file diff --git a/src/scanners/first_scanner.py b/src/scanners/first_scanner.py new file mode 100644 index 0000000..766b145 --- /dev/null +++ b/src/scanners/first_scanner.py @@ -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") diff --git a/src/scanners/history_scanner.py b/src/scanners/history_scanner.py new file mode 100644 index 0000000..c61872e --- /dev/null +++ b/src/scanners/history_scanner.py @@ -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 diff --git a/src/scanners/last_scanner.py b/src/scanners/last_scanner.py new file mode 100644 index 0000000..7713195 --- /dev/null +++ b/src/scanners/last_scanner.py @@ -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") diff --git a/src/scanners/random_scanner.py b/src/scanners/random_scanner.py new file mode 100644 index 0000000..9ef520b --- /dev/null +++ b/src/scanners/random_scanner.py @@ -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") diff --git a/src/scanners/scanner.py b/src/scanners/scanner.py index 079217b..4a3b749 100644 --- a/src/scanners/scanner.py +++ b/src/scanners/scanner.py @@ -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 diff --git a/src/utils/utils.py b/src/utils/utils.py index e32e4a4..880d892 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -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