@@ -18,13 +18,24 @@
|
|||||||
* %freq - frequency analysis
|
* %freq - frequency analysis
|
||||||
* %compo - composition analysis
|
* %compo - composition analysis
|
||||||
* %pres - presence analysis
|
* %pres - presence analysis
|
||||||
* %first - read first message
|
|
||||||
* %rand - read a random message
|
|
||||||
* %last - read last message
|
|
||||||
* %find - find specific words or phrases
|
|
||||||
* %repeat - repeat last analysis (adding supplied arguments)
|
* %repeat - repeat last analysis (adding supplied arguments)
|
||||||
* %mobile - fix @invalid-user for last command but mentions users
|
* %mobile - fix @invalid-user for last command but mentions users
|
||||||
* %gdpr - displays GDPR information
|
* %gdpr - displays GDPR information
|
||||||
|
* %find - find specific words or phrases (you can use quotes to add spaces in queries, backticks define regexes)
|
||||||
|
* arguments:
|
||||||
|
* top - rank users for these queries
|
||||||
|
* %first - read first message (add text to filter like %find)
|
||||||
|
* arguments:
|
||||||
|
* image - pull an image instead of a message
|
||||||
|
* spoiler:allow/only - allow spoiler images
|
||||||
|
* %rand - read a random message (add text to filter like %find)
|
||||||
|
* arguments:
|
||||||
|
* image - pull an image instead of a message
|
||||||
|
* spoiler:allow/only - allow spoiler images
|
||||||
|
* %last - read last message (add text to filter like %find)
|
||||||
|
* arguments:
|
||||||
|
* image - pull an image instead of a message
|
||||||
|
* spoiler:allow/only - allow spoiler images
|
||||||
* %emojis - rank emojis by their usage
|
* %emojis - rank emojis by their usage
|
||||||
* arguments:
|
* arguments:
|
||||||
* <n> - top <n> emojis, default is 20
|
* <n> - top <n> emojis, default is 20
|
||||||
@@ -61,6 +72,7 @@
|
|||||||
* all/everyone - include bots messages
|
* all/everyone - include bots messages
|
||||||
* fast: only read cache
|
* fast: only read cache
|
||||||
* fresh: does not read cache
|
* fresh: does not read cache
|
||||||
|
* nsfw:allow/only - allow messages from nsfw channels
|
||||||
* mobile/mention: mentions users (fix @invalid-user bug)
|
* mobile/mention: mentions users (fix @invalid-user bug)
|
||||||
|
|
||||||
(Sample dates: 2020 / 2021-11 / 2021-06-28 / 2020-06-28T23:00 / today / week / 8days / 1y)
|
(Sample dates: 2020 / 2021-11 / 2021-06-28 / 2020-06-28T23:00 / today / week / 8days / 1y)
|
||||||
@@ -113,6 +125,12 @@ python3 src/main.py
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
* **v1.15**
|
||||||
|
* `nsfw:allow/only` filter nsfw channels
|
||||||
|
* `%find` can use regexes
|
||||||
|
* `%first`, `%rand` and `%last` can be filter with specific keywords
|
||||||
|
* `%first`, `%rand` and `%last` can pull images
|
||||||
|
* bug fix
|
||||||
* **v1.14**
|
* **v1.14**
|
||||||
* `mobile/mention` arg to fix mobile bug
|
* `mobile/mention` arg to fix mobile bug
|
||||||
* `%repeat`, `%mobile` to repeat commands
|
* `%repeat`, `%mobile` to repeat commands
|
||||||
|
|||||||
@@ -3,13 +3,81 @@ import random
|
|||||||
|
|
||||||
# Custom libs
|
# Custom libs
|
||||||
|
|
||||||
from utils import mention, from_now, str_datetime, message_link
|
from utils import (
|
||||||
|
mention,
|
||||||
|
from_now,
|
||||||
|
str_datetime,
|
||||||
|
message_link,
|
||||||
|
SPLIT_TOKEN,
|
||||||
|
FilterLevel,
|
||||||
|
should_allow_spoiler,
|
||||||
|
)
|
||||||
|
|
||||||
|
MAX_RANDOM_TRIES = 100
|
||||||
|
|
||||||
|
|
||||||
class History:
|
class History:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.messages = []
|
self.messages = []
|
||||||
|
|
||||||
|
async def to_string_image(self, *, type: str, spoiler: FilterLevel) -> List[str]:
|
||||||
|
if len(self.messages) == 0:
|
||||||
|
return ["There was no messages matching your filters"]
|
||||||
|
message = None
|
||||||
|
intro = None
|
||||||
|
real_message = None
|
||||||
|
if type == "first":
|
||||||
|
self.messages.sort(key=lambda m: m.created_at)
|
||||||
|
index = 0
|
||||||
|
while real_message is None and index < len(self.messages):
|
||||||
|
message = self.messages[index]
|
||||||
|
real_message = await message.fetch()
|
||||||
|
if real_message is not None and not should_allow_spoiler(
|
||||||
|
real_message, spoiler
|
||||||
|
):
|
||||||
|
real_message = None
|
||||||
|
index += 1
|
||||||
|
intro = f"First image out of {len(self.messages):,}"
|
||||||
|
elif type == "last":
|
||||||
|
self.messages.sort(key=lambda m: m.created_at, reverse=True)
|
||||||
|
index = 0
|
||||||
|
while real_message is None and index < len(self.messages):
|
||||||
|
message = self.messages[index]
|
||||||
|
real_message = await message.fetch()
|
||||||
|
if real_message is not None and not should_allow_spoiler(
|
||||||
|
real_message, spoiler
|
||||||
|
):
|
||||||
|
real_message = None
|
||||||
|
index += 1
|
||||||
|
intro = f"Last image out of {len(self.messages):,}"
|
||||||
|
elif type == "random":
|
||||||
|
intro = f"Random image out of {len(self.messages):,}"
|
||||||
|
tries = 0
|
||||||
|
while real_message is None and tries < MAX_RANDOM_TRIES:
|
||||||
|
message = random.choice(self.messages)
|
||||||
|
real_message = await message.fetch()
|
||||||
|
if real_message is not None and not should_allow_spoiler(
|
||||||
|
real_message, spoiler
|
||||||
|
):
|
||||||
|
real_message = None
|
||||||
|
tries += 1
|
||||||
|
|
||||||
|
if real_message is None:
|
||||||
|
return ["There was no messages matching your filters"]
|
||||||
|
image = "<Error>"
|
||||||
|
if len(real_message.attachments) > 0:
|
||||||
|
image = real_message.attachments[0].url
|
||||||
|
elif len(real_message.embeds) > 0:
|
||||||
|
image = real_message.embeds[0].url
|
||||||
|
|
||||||
|
return [
|
||||||
|
intro,
|
||||||
|
f"{str_datetime(message.created_at)} ({from_now(message.created_at)}) {mention(message.author)} sent:",
|
||||||
|
f"<{message_link(message)}>",
|
||||||
|
SPLIT_TOKEN,
|
||||||
|
image,
|
||||||
|
]
|
||||||
|
|
||||||
def to_string(self, *, type: str) -> List[str]:
|
def to_string(self, *, type: str) -> List[str]:
|
||||||
if len(self.messages) == 0:
|
if len(self.messages) == 0:
|
||||||
return ["There was no messages matching your filters"]
|
return ["There was no messages matching your filters"]
|
||||||
|
|||||||
+12
-11
@@ -1,16 +1,13 @@
|
|||||||
from typing import Union, Tuple, Any
|
from typing import Union, Tuple, Any
|
||||||
import discord
|
import discord
|
||||||
from discord import message
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from . import MessageLog
|
from . import MessageLog
|
||||||
from utils import FakeMessage
|
from utils import serialize, FakeMessage
|
||||||
|
|
||||||
CHUNK_SIZE = 2000
|
CHUNK_SIZE = 2000
|
||||||
FORMAT = 3
|
FORMAT = 3
|
||||||
|
|
||||||
NOT_SERIALIZED = ["channel", "guild", "start_date"]
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelLogs:
|
class ChannelLogs:
|
||||||
def __init__(self, channel: Union[discord.TextChannel, dict], guild: Any):
|
def __init__(self, channel: Union[discord.TextChannel, dict], guild: Any):
|
||||||
@@ -50,11 +47,17 @@ class ChannelLogs:
|
|||||||
def is_format(self):
|
def is_format(self):
|
||||||
return self.format == FORMAT
|
return self.format == FORMAT
|
||||||
|
|
||||||
|
def preload(self, channel: discord.TextChannel):
|
||||||
|
self.name = channel.name
|
||||||
|
self.channel = channel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nsfw(self):
|
||||||
|
self.channel.nsfw
|
||||||
|
|
||||||
async def load(
|
async def load(
|
||||||
self, channel: discord.TextChannel, start_date: datetime, stop_date: datetime
|
self, channel: discord.TextChannel, start_date: datetime, stop_date: datetime
|
||||||
) -> Tuple[int, int]:
|
) -> Tuple[int, int]:
|
||||||
self.name = channel.name
|
|
||||||
self.channel = channel
|
|
||||||
is_empty = self.last_message_id is None
|
is_empty = self.last_message_id is None
|
||||||
try:
|
try:
|
||||||
if is_empty:
|
if is_empty:
|
||||||
@@ -110,7 +113,7 @@ class ChannelLogs:
|
|||||||
tmp_message_id = self.last_message_id
|
tmp_message_id = self.last_message_id
|
||||||
async for message in channel.history(
|
async for message in channel.history(
|
||||||
limit=CHUNK_SIZE,
|
limit=CHUNK_SIZE,
|
||||||
after=FakeMessage(self.last_message_id),
|
after=FakeMessage(self.first_message_id),
|
||||||
oldest_first=True,
|
oldest_first=True,
|
||||||
):
|
):
|
||||||
last_message_date = message.created_at
|
last_message_date = message.created_at
|
||||||
@@ -119,7 +122,7 @@ class ChannelLogs:
|
|||||||
await m.load(message)
|
await m.load(message)
|
||||||
self.messages.insert(0, m)
|
self.messages.insert(0, m)
|
||||||
yield len(self.messages), False
|
yield len(self.messages), False
|
||||||
except discord.errors.HTTPException:
|
except discord.errors.HTTPException as e:
|
||||||
yield -1, True
|
yield -1, True
|
||||||
return # When an exception occurs (like Forbidden)
|
return # When an exception occurs (like Forbidden)
|
||||||
self.start_date = (
|
self.start_date = (
|
||||||
@@ -128,8 +131,6 @@ class ChannelLogs:
|
|||||||
yield len(self.messages), True
|
yield len(self.messages), True
|
||||||
|
|
||||||
def dict(self) -> dict:
|
def dict(self) -> dict:
|
||||||
channel = dict(self.__dict__)
|
channel = serialize(self, not_serialized=["channel", "guild", "start_date"])
|
||||||
for key in NOT_SERIALIZED:
|
|
||||||
channel.pop(key, None)
|
|
||||||
channel["messages"] = [message.dict() for message in self.messages]
|
channel["messages"] = [message.dict() for message in self.messages]
|
||||||
return channel
|
return channel
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ class GuildLogs:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
total_chan = len(target_channels)
|
total_chan = len(target_channels)
|
||||||
|
for channel in target_channels:
|
||||||
|
self.channels[channel.id].preload(channel)
|
||||||
else:
|
else:
|
||||||
if not self.locked and not self.lock():
|
if not self.locked and not self.lock():
|
||||||
return ALREADY_RUNNING, 0
|
return ALREADY_RUNNING, 0
|
||||||
@@ -231,6 +233,7 @@ class GuildLogs:
|
|||||||
if channel.id not in self.channels or fresh:
|
if channel.id not in self.channels or fresh:
|
||||||
loading_new += 1
|
loading_new += 1
|
||||||
self.channels[channel.id] = ChannelLogs(channel, self)
|
self.channels[channel.id] = ChannelLogs(channel, self)
|
||||||
|
self.channels[channel.id].preload(channel)
|
||||||
workers += [
|
workers += [
|
||||||
Worker(self.channels[channel.id], channel, start_date, stop_date)
|
Worker(self.channels[channel.id], channel, start_date, stop_date)
|
||||||
]
|
]
|
||||||
|
|||||||
+10
-12
@@ -1,16 +1,13 @@
|
|||||||
from typing import Union, Any
|
from typing import Optional, Union, Any
|
||||||
import discord
|
import discord
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from utils import is_extension
|
from utils import is_extension, serialize
|
||||||
|
|
||||||
IMAGE_FORMAT = [".gif", ".gifv", ".png", ".jpg", ".jpeg", ".bmp"]
|
IMAGE_FORMAT = [".gif", ".gifv", ".png", ".jpg", ".jpeg", ".bmp"]
|
||||||
EMBED_IMAGES = ["image", "gifv"]
|
EMBED_IMAGES = ["image", "gifv"]
|
||||||
|
|
||||||
|
|
||||||
NOT_SERIALIZED = ["channel"]
|
|
||||||
|
|
||||||
|
|
||||||
class MessageLog:
|
class MessageLog:
|
||||||
def __init__(self, message: Union[discord.Message, dict], channel: Any):
|
def __init__(self, message: Union[discord.Message, dict], channel: Any):
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
@@ -80,12 +77,13 @@ class MessageLog:
|
|||||||
async for user in reaction.users():
|
async for user in reaction.users():
|
||||||
self.reactions[str(reaction.emoji)] += [user.id]
|
self.reactions[str(reaction.emoji)] += [user.id]
|
||||||
|
|
||||||
|
async def fetch(self) -> Optional[discord.Message]:
|
||||||
|
try:
|
||||||
|
return await self.channel.channel.fetch_message(self.id)
|
||||||
|
except (discord.NotFound, discord.Forbidden, discord.HTTPException):
|
||||||
|
return None
|
||||||
|
|
||||||
def dict(self) -> dict:
|
def dict(self) -> dict:
|
||||||
message = dict(self.__dict__)
|
return serialize(
|
||||||
for key in NOT_SERIALIZED:
|
self, not_serialized=["channel"], dates=["created_at", "edited_at"]
|
||||||
message.pop(key, None)
|
|
||||||
message["created_at"] = self.created_at.isoformat()
|
|
||||||
message["edited_at"] = (
|
|
||||||
self.edited_at.isoformat() if self.edited_at is not None else None
|
|
||||||
)
|
)
|
||||||
return message
|
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ emojis.load_emojis()
|
|||||||
|
|
||||||
bot = Bot(
|
bot = Bot(
|
||||||
"Discord Analyst",
|
"Discord Analyst",
|
||||||
"1.14",
|
"1.15",
|
||||||
alias="%",
|
alias="%",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ bot.register_command(
|
|||||||
scanners.LastScanner.help(),
|
scanners.LastScanner.help(),
|
||||||
)
|
)
|
||||||
bot.register_command(
|
bot.register_command(
|
||||||
"rand(om)?",
|
"(rand(om)?|mood)",
|
||||||
lambda *args: scanners.RandomScanner().compute(*args),
|
lambda *args: scanners.RandomScanner().compute(*args),
|
||||||
"rand: read a random message",
|
"rand: read a random message",
|
||||||
scanners.RandomScanner.help(),
|
scanners.RandomScanner.help(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from collections import defaultdict
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import Dict, List
|
from typing import Dict, List, Optional, Tuple
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import discord
|
import discord
|
||||||
|
import re
|
||||||
|
|
||||||
# Custom libs
|
# Custom libs
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ class FindScanner(Scanner):
|
|||||||
def help() -> str:
|
def help() -> str:
|
||||||
return generate_help(
|
return generate_help(
|
||||||
"find",
|
"find",
|
||||||
"Find specific words or phrases (you can use quotes to add spaces in queries)",
|
"Find specific words or phrases (you can use quotes to add spaces in queries, backticks define regexes)",
|
||||||
args=[
|
args=[
|
||||||
"top - rank users for these queries",
|
"top - rank users for these queries",
|
||||||
"all/everyone - include bots",
|
"all/everyone - include bots",
|
||||||
@@ -43,17 +44,21 @@ class FindScanner(Scanner):
|
|||||||
self.top = "top" in args or len(self.other_args) == 1
|
self.top = "top" in args or len(self.other_args) == 1
|
||||||
if len(self.other_args) == 0:
|
if len(self.other_args) == 0:
|
||||||
await message.channel.send(
|
await message.channel.send(
|
||||||
"You need to add a query to find (you can use quotes to add spaces in queries)",
|
"You need to add a query to find (you can use quotes to add spaces in queries, backticks define regexes)",
|
||||||
reference=message,
|
reference=message,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
self.queries = [
|
||||||
|
(query, query.strip("`") if re.match(r"^`.*`$", query) else None)
|
||||||
|
for query in self.other_args
|
||||||
|
]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def compute_message(self, channel: ChannelLogs, message: MessageLog):
|
def compute_message(self, channel: ChannelLogs, message: MessageLog):
|
||||||
return FindScanner.analyse_message(
|
return FindScanner.analyse_message(
|
||||||
message,
|
message,
|
||||||
self.matches,
|
self.matches,
|
||||||
self.other_args,
|
self.queries,
|
||||||
self.raw_members,
|
self.raw_members,
|
||||||
all_messages=self.all_messages,
|
all_messages=self.all_messages,
|
||||||
top=self.top,
|
top=self.top,
|
||||||
@@ -77,7 +82,9 @@ class FindScanner(Scanner):
|
|||||||
res += [
|
res += [
|
||||||
self.matches[match].to_string(
|
self.matches[match].to_string(
|
||||||
matches.index(match),
|
matches.index(match),
|
||||||
f'"{escape_text(match)}"',
|
f'"{escape_text(match)}"'
|
||||||
|
if len(match.strip("`")) == len(match)
|
||||||
|
else match,
|
||||||
total_usage=self.msg_count,
|
total_usage=self.msg_count,
|
||||||
ranking=False,
|
ranking=False,
|
||||||
transform=lambda id: f" by {mention(id)}",
|
transform=lambda id: f" by {mention(id)}",
|
||||||
@@ -97,7 +104,7 @@ class FindScanner(Scanner):
|
|||||||
def analyse_message(
|
def analyse_message(
|
||||||
message: MessageLog,
|
message: MessageLog,
|
||||||
matches: Dict[str, Counter],
|
matches: Dict[str, Counter],
|
||||||
queries: List[str],
|
queries: List[Tuple[str, Optional[str]]],
|
||||||
raw_members: List[int],
|
raw_members: List[int],
|
||||||
*,
|
*,
|
||||||
all_messages: bool,
|
all_messages: bool,
|
||||||
@@ -113,10 +120,15 @@ class FindScanner(Scanner):
|
|||||||
impacted = True
|
impacted = True
|
||||||
content = message.content.lower()
|
content = message.content.lower()
|
||||||
for query in queries:
|
for query in queries:
|
||||||
count = content.count(query.lower())
|
if query[1] is not None:
|
||||||
|
count = len(re.findall(query[1], message.content))
|
||||||
|
else:
|
||||||
|
count = content.count(query[0].lower())
|
||||||
if top:
|
if top:
|
||||||
if count > 0:
|
if count > 0:
|
||||||
matches[message.author].update_use(count, message.created_at)
|
matches[message.author].update_use(count, message.created_at)
|
||||||
else:
|
else:
|
||||||
matches[query].update_use(count, message.created_at, message.author)
|
matches[query[0]].update_use(
|
||||||
|
count, message.created_at, message.author
|
||||||
|
)
|
||||||
return impacted
|
return impacted
|
||||||
|
|||||||
@@ -9,10 +9,22 @@ from utils import generate_help
|
|||||||
class FirstScanner(HistoryScanner):
|
class FirstScanner(HistoryScanner):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def help() -> str:
|
def help() -> str:
|
||||||
return generate_help("first", "Read first message")
|
return generate_help(
|
||||||
|
"first",
|
||||||
|
"Read first message (add text to filter like %find)",
|
||||||
|
args=[
|
||||||
|
"image - pull an image instead of a message",
|
||||||
|
"spoiler:allow/only - allow spoiler images",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(help=FirstScanner.help())
|
super().__init__(help=FirstScanner.help())
|
||||||
|
|
||||||
def get_results(self, intro: str) -> List[str]:
|
async def get_results(self, intro: str) -> List[str]:
|
||||||
return self.history.to_string(type="first")
|
if self.images_only:
|
||||||
|
return await self.history.to_string_image(
|
||||||
|
type="first", spoiler=self.spoiler
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.history.to_string(type="first")
|
||||||
|
|||||||
@@ -1,26 +1,46 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List
|
from typing import List, Tuple, Optional
|
||||||
import discord
|
import discord
|
||||||
|
import re
|
||||||
|
|
||||||
# Custom libs
|
# Custom libs
|
||||||
|
|
||||||
from .scanner import Scanner
|
from .scanner import Scanner
|
||||||
from data_types import History
|
from data_types import History
|
||||||
from logs import ChannelLogs, MessageLog
|
from logs import ChannelLogs, MessageLog
|
||||||
|
from utils import FilterLevel
|
||||||
|
|
||||||
|
|
||||||
class HistoryScanner(Scanner, ABC):
|
class HistoryScanner(Scanner, ABC):
|
||||||
def __init__(self, *, help: str):
|
def __init__(self, *, help: str):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
has_digit_args=True,
|
has_digit_args=True,
|
||||||
valid_args=["all", "everyone"],
|
valid_args=["all", "everyone", "spoiler", "spoiler:allow", "spoiler:only"],
|
||||||
help=help,
|
help=help,
|
||||||
intro_context="",
|
intro_context="",
|
||||||
|
all_args=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def init(self, message: discord.Message, *args: str) -> bool:
|
async def init(self, message: discord.Message, *args: str) -> bool:
|
||||||
self.history = History()
|
self.history = History()
|
||||||
self.all_messages = "all" in args or "everyone" in args
|
self.all_messages = "all" in args or "everyone" in args
|
||||||
|
self.images_only = "image" in args
|
||||||
|
if "spoiler" in args or "spoiler:allow" in args:
|
||||||
|
self.spoiler = FilterLevel.ALLOW
|
||||||
|
elif "spoiler:only" in args:
|
||||||
|
self.spoiler = FilterLevel.ONLY
|
||||||
|
else:
|
||||||
|
self.spoiler = FilterLevel.NONE
|
||||||
|
if not self.images_only:
|
||||||
|
self.queries = [
|
||||||
|
(
|
||||||
|
query.lower(),
|
||||||
|
query.strip("`") if re.match(r"^`.*`$", query) else None,
|
||||||
|
)
|
||||||
|
for query in self.other_args
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.queries = []
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def compute_message(self, channel: ChannelLogs, message: MessageLog):
|
def compute_message(self, channel: ChannelLogs, message: MessageLog):
|
||||||
@@ -30,6 +50,8 @@ class HistoryScanner(Scanner, ABC):
|
|||||||
self.history,
|
self.history,
|
||||||
self.raw_members,
|
self.raw_members,
|
||||||
all_messages=self.all_messages,
|
all_messages=self.all_messages,
|
||||||
|
queries=self.queries,
|
||||||
|
images_only=self.images_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -44,14 +66,27 @@ class HistoryScanner(Scanner, ABC):
|
|||||||
raw_members: List[int],
|
raw_members: List[int],
|
||||||
*,
|
*,
|
||||||
all_messages: bool,
|
all_messages: bool,
|
||||||
|
queries: List[Tuple[str, Optional[str]]],
|
||||||
|
images_only: bool,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
impacted = False
|
impacted = False
|
||||||
# If author is included in the selection (empty list is all)
|
# If author is included in the selection (empty list is all)
|
||||||
if (
|
if (
|
||||||
(not message.bot or all_messages)
|
(
|
||||||
and len(raw_members) == 0
|
(not message.bot or all_messages)
|
||||||
or message.author in raw_members
|
and len(raw_members) == 0
|
||||||
) and (message.content or message.attachment):
|
or message.author in raw_members
|
||||||
|
)
|
||||||
|
and (message.content or message.attachment)
|
||||||
|
and (not images_only or message.image)
|
||||||
|
):
|
||||||
|
content = message.content.lower()
|
||||||
|
for query in queries:
|
||||||
|
if query[1] is not None:
|
||||||
|
if not re.match(query[1], message.content):
|
||||||
|
return False
|
||||||
|
elif not query[0] in content:
|
||||||
|
return False
|
||||||
impacted = True
|
impacted = True
|
||||||
history.messages += [message]
|
history.messages += [message]
|
||||||
return impacted
|
return impacted
|
||||||
|
|||||||
@@ -9,10 +9,20 @@ from utils import generate_help
|
|||||||
class LastScanner(HistoryScanner):
|
class LastScanner(HistoryScanner):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def help() -> str:
|
def help() -> str:
|
||||||
return generate_help("last", "Read last message")
|
return generate_help(
|
||||||
|
"last",
|
||||||
|
"Read last message (add text to filter like %find)",
|
||||||
|
args=[
|
||||||
|
"image - pull an image instead of a message",
|
||||||
|
"spoiler:allow/only - allow spoiler images",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(help=LastScanner.help())
|
super().__init__(help=LastScanner.help())
|
||||||
|
|
||||||
def get_results(self, intro: str) -> List[str]:
|
async def get_results(self, intro: str) -> List[str]:
|
||||||
return self.history.to_string(type="last")
|
if self.images_only:
|
||||||
|
return await self.history.to_string_image(type="last", spoiler=self.spoiler)
|
||||||
|
else:
|
||||||
|
return self.history.to_string(type="last")
|
||||||
|
|||||||
@@ -9,10 +9,22 @@ from utils import generate_help
|
|||||||
class RandomScanner(HistoryScanner):
|
class RandomScanner(HistoryScanner):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def help() -> str:
|
def help() -> str:
|
||||||
return generate_help("rand", "Read a random message")
|
return generate_help(
|
||||||
|
"rand",
|
||||||
|
"Read a random message (add text to filter like %find)",
|
||||||
|
args=[
|
||||||
|
"image - pull an image instead of a message",
|
||||||
|
"spoiler:allow/only - allow spoiler images",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(help=RandomScanner.help())
|
super().__init__(help=RandomScanner.help())
|
||||||
|
|
||||||
def get_results(self, intro: str) -> List[str]:
|
async def get_results(self, intro: str) -> List[str]:
|
||||||
return self.history.to_string(type="random")
|
if self.images_only:
|
||||||
|
return await self.history.to_string_image(
|
||||||
|
type="random", spoiler=self.spoiler
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.history.to_string(type="random")
|
||||||
|
|||||||
+62
-14
@@ -4,6 +4,7 @@ from datetime import datetime
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import discord
|
import discord
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
from utils import (
|
from utils import (
|
||||||
@@ -15,6 +16,8 @@ from utils import (
|
|||||||
RELATIVE_REGEX,
|
RELATIVE_REGEX,
|
||||||
parse_time,
|
parse_time,
|
||||||
command_cache,
|
command_cache,
|
||||||
|
FilterLevel,
|
||||||
|
SPLIT_TOKEN,
|
||||||
)
|
)
|
||||||
from logs import (
|
from logs import (
|
||||||
GuildLogs,
|
GuildLogs,
|
||||||
@@ -27,7 +30,17 @@ from logs import (
|
|||||||
|
|
||||||
|
|
||||||
class Scanner(ABC):
|
class Scanner(ABC):
|
||||||
VALID_ARGS = ["me", "here", "fast", "fresh", "mobile", "mention"]
|
VALID_ARGS = [
|
||||||
|
"me",
|
||||||
|
"here",
|
||||||
|
"fast",
|
||||||
|
"fresh",
|
||||||
|
"mobile",
|
||||||
|
"mention",
|
||||||
|
"nsfw",
|
||||||
|
"nsfw:allow",
|
||||||
|
"nsfw:only",
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -139,6 +152,30 @@ class Scanner(ABC):
|
|||||||
|
|
||||||
self.mention_users = "mention" in args or "mobile" in args
|
self.mention_users = "mention" in args or "mobile" in args
|
||||||
|
|
||||||
|
# nsfw filter
|
||||||
|
if "nsfw" in args or "nsfw:allow" in args:
|
||||||
|
self.nsfw = FilterLevel.ALLOW
|
||||||
|
elif "nsfw:only" in args:
|
||||||
|
self.nsfw = FilterLevel.ONLY
|
||||||
|
else:
|
||||||
|
self.nsfw = FilterLevel.NONE
|
||||||
|
|
||||||
|
# fix nsfw filter if channel specified
|
||||||
|
if not self.full and any(channel.nsfw for channel in self.channels):
|
||||||
|
self.nsfw = FilterLevel.ALLOW
|
||||||
|
elif all(channel.nsfw for channel in self.channels):
|
||||||
|
self.nsfw = FilterLevel.ONLY
|
||||||
|
|
||||||
|
# filter nsfw channels
|
||||||
|
if self.nsfw == FilterLevel.NONE:
|
||||||
|
self.channels = list(
|
||||||
|
filter(lambda channel: not channel.nsfw, self.channels)
|
||||||
|
)
|
||||||
|
elif self.nsfw == FilterLevel.ONLY:
|
||||||
|
self.channels = list(
|
||||||
|
filter(lambda channel: channel.nsfw, self.channels)
|
||||||
|
)
|
||||||
|
|
||||||
if not await self.init(message, *args):
|
if not await self.init(message, *args):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -220,18 +257,20 @@ class Scanner(ABC):
|
|||||||
await progress.edit(content="```Computing results...```")
|
await progress.edit(content="```Computing results...```")
|
||||||
# Display results
|
# Display results
|
||||||
t0 = datetime.now()
|
t0 = datetime.now()
|
||||||
results = self.get_results(
|
intro = get_intro(
|
||||||
get_intro(
|
self.intro_context,
|
||||||
self.intro_context,
|
self.full,
|
||||||
self.full,
|
self.channels,
|
||||||
self.channels,
|
self.members,
|
||||||
self.members,
|
self.msg_count,
|
||||||
self.msg_count,
|
self.chan_count,
|
||||||
self.chan_count,
|
self.start_date,
|
||||||
self.start_date,
|
self.stop_date,
|
||||||
self.stop_date,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if inspect.iscoroutinefunction(self.get_results):
|
||||||
|
results = await self.get_results(intro)
|
||||||
|
else:
|
||||||
|
results = self.get_results(intro)
|
||||||
logging.info(
|
logging.info(
|
||||||
f"scan {guild.id} > results in {delta(t0):,}ms"
|
f"scan {guild.id} > results in {delta(t0):,}ms"
|
||||||
)
|
)
|
||||||
@@ -244,7 +283,7 @@ class Scanner(ABC):
|
|||||||
)
|
)
|
||||||
for r in results:
|
for r in results:
|
||||||
if r:
|
if r:
|
||||||
if len(response + "\n" + r) > 2000:
|
if isinstance(r, int) and r == SPLIT_TOKEN:
|
||||||
await message.channel.send(
|
await message.channel.send(
|
||||||
response,
|
response,
|
||||||
reference=message if first else None,
|
reference=message if first else None,
|
||||||
@@ -252,7 +291,16 @@ class Scanner(ABC):
|
|||||||
)
|
)
|
||||||
first = False
|
first = False
|
||||||
response = ""
|
response = ""
|
||||||
response += "\n" + r
|
elif isinstance(r, str):
|
||||||
|
if len(response + "\n" + r) > 2000:
|
||||||
|
await message.channel.send(
|
||||||
|
response,
|
||||||
|
reference=message if first else None,
|
||||||
|
allowed_mentions=allowed_mentions,
|
||||||
|
)
|
||||||
|
first = False
|
||||||
|
response = ""
|
||||||
|
response += "\n" + r
|
||||||
if len(response) > 0:
|
if len(response) > 0:
|
||||||
await message.channel.send(
|
await message.channel.send(
|
||||||
response,
|
response,
|
||||||
|
|||||||
+46
-1
@@ -1,3 +1,4 @@
|
|||||||
|
from enum import Enum
|
||||||
from typing import Callable, List, Dict, Union, Optional, Any
|
from typing import Callable, List, Dict, Union, Optional, Any
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@@ -17,6 +18,7 @@ COMMON_HELP_ARGS = [
|
|||||||
"<date2> - filter before <date2>",
|
"<date2> - filter before <date2>",
|
||||||
"fast - only read cache",
|
"fast - only read cache",
|
||||||
"fresh - does not read cache (long)",
|
"fresh - does not read cache (long)",
|
||||||
|
"nsfw:allow/only - allow messages from nsfw channels",
|
||||||
"mobile/mention - mentions users (fix @invalid-user bug)",
|
"mobile/mention - mentions users (fix @invalid-user bug)",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ def generate_help(
|
|||||||
replace_args=[],
|
replace_args=[],
|
||||||
):
|
):
|
||||||
arg_list = "* " + "\n* ".join(
|
arg_list = "* " + "\n* ".join(
|
||||||
replace_args + COMMON_HELP_ARGS[len(replace_args) :] + args
|
args + replace_args + COMMON_HELP_ARGS[len(replace_args) :]
|
||||||
)
|
)
|
||||||
return f"""```
|
return f"""```
|
||||||
%{cmd}: {info}
|
%{cmd}: {info}
|
||||||
@@ -49,6 +51,15 @@ def deltas(t0: datetime):
|
|||||||
return (datetime.now() - t0).total_seconds()
|
return (datetime.now() - t0).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
class FilterLevel(Enum):
|
||||||
|
NONE = 0
|
||||||
|
ALLOW = 1
|
||||||
|
ONLY = 2
|
||||||
|
|
||||||
|
|
||||||
|
SPLIT_TOKEN = 1152317803
|
||||||
|
|
||||||
|
|
||||||
# DISCORD API
|
# DISCORD API
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +100,25 @@ class FakeMessage:
|
|||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
|
|
||||||
|
def is_image_spoiler(message: discord.Message) -> bool:
|
||||||
|
if len(message.attachments) > 0:
|
||||||
|
return message.attachments[0].is_spoiler()
|
||||||
|
elif len(message.embeds) > 0:
|
||||||
|
return re.match(r"||[^|]*http[^|]||", message.content.lower()) is not None
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def should_allow_spoiler(message: discord.Message, spoiler: FilterLevel) -> bool:
|
||||||
|
is_spoiler = is_image_spoiler(message)
|
||||||
|
return (
|
||||||
|
not is_spoiler
|
||||||
|
and spoiler <= FilterLevel.ALLOW
|
||||||
|
or is_spoiler
|
||||||
|
and spoiler >= FilterLevel.ALLOW
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# FILE
|
# FILE
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +167,21 @@ def val_sum(d: Dict[Any, int]) -> int:
|
|||||||
return sum(d.values())
|
return sum(d.values())
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(
|
||||||
|
obj: Any, *, not_serialized: List[str] = [], dates: List[str] = []
|
||||||
|
) -> Dict:
|
||||||
|
output = dict(obj.__dict__)
|
||||||
|
for key in not_serialized:
|
||||||
|
output.pop(key, None)
|
||||||
|
for key in dates:
|
||||||
|
if output[key]:
|
||||||
|
try:
|
||||||
|
output[key] = getattr(obj, key).isoformat()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
# MESSAGE FORMATTING
|
# MESSAGE FORMATTING
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user