diff --git a/requirements.txt b/requirements.txt index bd857ae..b5ad337 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pandas -# discord.py -nextcord +discord.py +# nextcord nltk python-dotenv @@ -9,3 +9,4 @@ beautifulsoup4 requests lxml +rich \ No newline at end of file diff --git a/src/kwaylon/jokes/base.py b/src/kwaylon/jokes/base.py index a74fcb0..4bfb646 100644 --- a/src/kwaylon/jokes/base.py +++ b/src/kwaylon/jokes/base.py @@ -2,7 +2,8 @@ import logging import random import re -from nextcord import Message, Client +# from nextcord import Message, Client +from discord import Message, Client LOGGER = logging.getLogger(__name__) diff --git a/src/kwaylon/jokes/jokes.py b/src/kwaylon/jokes/jokes.py index d0b58ea..b02c896 100644 --- a/src/kwaylon/jokes/jokes.py +++ b/src/kwaylon/jokes/jokes.py @@ -2,8 +2,9 @@ import logging import re from random import choice -from nextcord import Client, Message -from nextcord import utils +# from nextcord import Client, Message +# from nextcord import utils +from discord import Client, Message, utils from . import base, helpers diff --git a/src/kwaylon/kwaylon.py b/src/kwaylon/kwaylon.py index 483e212..ebc1e4a 100644 --- a/src/kwaylon/kwaylon.py +++ b/src/kwaylon/kwaylon.py @@ -1,165 +1,168 @@ -import asyncio -import logging -import re -import sqlite3 -from datetime import datetime, timedelta -from pathlib import Path -from typing import List - -import pandas as pd -from nextcord import Client, Message, TextChannel -from nextcord import RawReactionActionEvent, Emoji -from nextcord import utils - -from . import jokes -from .reactions import ReactionData - -LIL_STINKY_ID = 704043422276780072 - -LOGGER = logging.getLogger(__name__) - - -class Kwaylon(Client): - def __init__(self, limit: int = 5000, days: int = 30, db_path: Path = None, *args, **kwargs): - super().__init__(*args, **kwargs) - if db_path is None: - self.db_path = Path(__file__).parents[2] / 'data' / 'messages.db' - else: - self.db_path = db_path - - self.limit, self.days = limit, days - self.jokes = list(jokes.collect_jokes()) - self.lock = asyncio.Lock() - - self.most_regex = re.compile('^most\s*(?P\S+)', re.IGNORECASE) - - def text_channels(self) -> List[TextChannel]: - return [chan for chan in self.get_all_channels() if isinstance(chan, TextChannel)] - - def robotics_facility(self) -> TextChannel: - for chan in self.text_channels(): - if chan.name == 'robotics-facility' and chan.guild.name == 'Family Dinner': - return chan - - def kaylon_emoji(self) -> Emoji: - return utils.get(self.emojis, name='kaylon') - - async def handle_ready(self): - async def alive(): - channel: TextChannel = self.robotics_facility() - await channel.send('https://tenor.com/view/terminator-im-back-gif-19144173') - await channel.send(self.kaylon_emoji()) - - # await alive() - - try: - self.data = ReactionData(self.db_path) - LOGGER.info(f'{self.data.row_count():d} reactions in {self.db_path}') - except sqlite3.Error as e: - LOGGER.exception(e) - LOGGER.error(f'self.db_path: {self.db_path}') - - async def handle_message(self, message: Message): - if message.author != self.user: - await self.read_command(message) - await self.respond_to_joke(message) - await self.respond_to_emoji(message) - - async def read_command(self, message: Message): - for mention in message.mentions: - if mention.id == self.user.id and 'read' in message.content: - days = get_days(message.content) or self.days - await self.data.scan_messages(client=self, limit=self.limit, days=days) - - async def respond_to_emoji(self, message: Message): - if (most_match := self.most_regex.match(message.content)): - emoji_ref = most_match.group('emoji') - emoji_name = get_emoji_name(emoji_ref) - LOGGER.info(f'Most {emoji_name}') - - async with message.channel.typing(): - with self.data.connect() as con: - days = get_days(message.content) or 14 - - if days >= 1000: - await message.reply( - f'https://tenor.com/view/i-hate-you-anakin-darth-vader-vader-star-wars-gif-13071041' - ) - return - - df = self.data.read_emoji(emoji_name, con=con, days=days) - con.close() - - if df.shape[0] > 0: - LOGGER.info(f'{df.shape[0]} messages with {emoji_ref} after filtering') - - if 'leaderboard' in message.content: - LOGGER.info(f'Building leaderboard') - res = f'{emoji_ref} totals, past {days} days\n' - if (board := await self.leaderboard(df)) is not None: - res += board - await message.reply(res) - - else: - if len(message.mentions) > 0: - df = df[df['auth_id'].isin([m.id for m in message.mentions])] - - most = df.sort_values('count').iloc[-1] - msg = await self.fetch_message(most) - await message.reply(f'Most {emoji_ref} in past {days} days\n{msg.jump_url}') - - # else: - # await message.reply(f"NObody (in the past {days} days)...gah, leave me alone!") - - LOGGER.info(f'Done') - - async def respond_to_joke(self, message: Message): - for joke in self.jokes: - if (joke_match := joke.scan(message)): - LOGGER.info(f'{joke.__class__.__name__} detected: {message.content}, {joke_match.group()}') - await joke.respond(message, self, joke_match) - - async def leaderboard(self, df: pd.DataFrame) -> str: - df = df.groupby('auth_id').sum() - counts = df['count'].sort_values(ascending=False) - counts.index = [(await self.fetch_user(idx)).display_name for idx in counts.index] - - width = max([len(str(s)) for s in counts.index]) - - res = '\n'.join( - f"`{str(name).ljust(width + 1)}with {cnt:<2.0f} total`" - for name, cnt in counts.iteritems() - ) - return res - - async def handle_raw_reaction(self, payload: RawReactionActionEvent): - LOGGER.info(payload) - - guild = await self.fetch_guild(payload.guild_id) - channel = await guild.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - - async with self.lock: - with self.data.connect() as con: - self.data.add_reactions_from_message(message, con) - con.close() - - async def fetch_message(self, row: pd.Series): - guild = await self.fetch_guild(row['guild_id']) - channel = await guild.fetch_channel(row['channel_id']) - return await channel.fetch_message(row['msg_id']) - - async def scan_messages(self, **kwargs): - async with self.lock: - await self.data.scan_messages(client=self, **kwargs) - - -def get_emoji_name(string: str) -> str: - if (m := re.search('<:(?P\w+):(?P\d+)>', string)): - string = m.group('name') - return string.lower().strip() - - -def get_days(input_str): - if (m := re.search('(?P\d+) days', input_str)): - return int(m.group('days')) +import asyncio +import logging +import re +import sqlite3 +from pathlib import Path +from typing import List + +import pandas as pd +# from nextcord import Client, Message, TextChannel +# from nextcord import RawReactionActionEvent, Emoji +# from nextcord import utils + +from discord import Client, Message, TextChannel +from discord import RawReactionActionEvent, Emoji +from discord import utils + +from . import jokes +from .reactions import ReactionData + +LIL_STINKY_ID = 704043422276780072 + +LOGGER = logging.getLogger(__name__) + + +class Kwaylon(Client): + def __init__(self, limit: int = 5000, days: int = 30, db_path: Path = None, *args, **kwargs): + super().__init__(*args, **kwargs) + if db_path is None: + self.db_path = Path(__file__).parents[2] / 'data' / 'messages.db' + else: + self.db_path = db_path + + self.limit, self.days = limit, days + self.jokes = list(jokes.collect_jokes()) + self.lock = asyncio.Lock() + + self.most_regex = re.compile('^most\s*(?P\S+)', re.IGNORECASE) + + def text_channels(self) -> List[TextChannel]: + return [chan for chan in self.get_all_channels() if isinstance(chan, TextChannel)] + + def robotics_facility(self) -> TextChannel: + for chan in self.text_channels(): + if chan.name == 'robotics-facility' and chan.guild.name == 'Family Dinner': + return chan + + def kaylon_emoji(self) -> Emoji: + return utils.get(self.emojis, name='kaylon') + + async def handle_ready(self): + async def alive(): + channel: TextChannel = self.robotics_facility() + await channel.send('https://tenor.com/view/terminator-im-back-gif-19144173') + await channel.send(self.kaylon_emoji()) + + # await alive() + + try: + self.data = ReactionData(self.db_path) + LOGGER.info(f'{self.data.row_count():d} reactions in {self.db_path}') + except sqlite3.Error as e: + LOGGER.exception(e) + LOGGER.error(f'self.db_path: {self.db_path}') + + async def handle_message(self, message: Message): + if message.author != self.user: + await self.read_command(message) + await self.respond_to_joke(message) + await self.respond_to_emoji(message) + + async def read_command(self, message: Message): + for mention in message.mentions: + if mention.id == self.user.id and 'read' in message.content: + days = get_days(message.content) or self.days + await self.data.scan_messages(client=self, limit=self.limit, days=days) + + async def respond_to_emoji(self, message: Message): + if (most_match := self.most_regex.match(message.content)): + emoji_ref = most_match.group('emoji') + emoji_name = get_emoji_name(emoji_ref) + LOGGER.info(f'Most {emoji_name}') + + async with message.channel.typing(): + with self.data.connect() as con: + days = get_days(message.content) or 14 + + if days >= 1000: + await message.reply( + f'https://tenor.com/view/i-hate-you-anakin-darth-vader-vader-star-wars-gif-13071041' + ) + return + + df = self.data.read_emoji(emoji_name, con=con, days=days) + con.close() + + if df.shape[0] > 0: + LOGGER.info(f'{df.shape[0]} messages with {emoji_ref} after filtering') + + if 'leaderboard' in message.content: + LOGGER.info(f'Building leaderboard') + res = f'{emoji_ref} totals, past {days} days\n' + if (board := await self.leaderboard(df)) is not None: + res += board + await message.reply(res) + + else: + if len(message.mentions) > 0: + df = df[df['auth_id'].isin([m.id for m in message.mentions])] + + most = df.sort_values('count').iloc[-1] + msg = await self.fetch_message(most) + await message.reply(f'Most {emoji_ref} in past {days} days\n{msg.jump_url}') + + # else: + # await message.reply(f"NObody (in the past {days} days)...gah, leave me alone!") + + LOGGER.info(f'Done') + + async def respond_to_joke(self, message: Message): + for joke in self.jokes: + if (joke_match := joke.scan(message)): + LOGGER.info(f'{joke.__class__.__name__} detected: {message.content}, {joke_match.group()}') + await joke.respond(message, self, joke_match) + + async def leaderboard(self, df: pd.DataFrame) -> str: + df = df.groupby('auth_id').sum() + counts = df['count'].sort_values(ascending=False) + counts.index = [(await self.fetch_user(idx)).display_name for idx in counts.index] + + width = max([len(str(s)) for s in counts.index]) + + res = '\n'.join( + f"`{str(name).ljust(width + 1)}with {cnt:<2.0f} total`" + for name, cnt in counts.iteritems() + ) + return res + + async def handle_raw_reaction(self, payload: RawReactionActionEvent): + LOGGER.info(payload) + + guild = await self.fetch_guild(payload.guild_id) + channel = await guild.fetch_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + + async with self.lock: + with self.data.connect() as con: + self.data.add_reactions_from_message(message, con) + con.close() + + async def fetch_message(self, row: pd.Series): + guild = await self.fetch_guild(row['guild_id']) + channel = await guild.fetch_channel(row['channel_id']) + return await channel.fetch_message(row['msg_id']) + + async def scan_messages(self, **kwargs): + async with self.lock: + await self.data.scan_messages(client=self, **kwargs) + + +def get_emoji_name(string: str) -> str: + if (m := re.search('<:(?P\w+):(?P\d+)>', string)): + string = m.group('name') + return string.lower().strip() + + +def get_days(input_str): + if (m := re.search('(?P\d+) days', input_str)): + return int(m.group('days')) diff --git a/src/kwaylon/msg.py b/src/kwaylon/msg.py index 2ed7e78..87a4b5d 100644 --- a/src/kwaylon/msg.py +++ b/src/kwaylon/msg.py @@ -2,9 +2,12 @@ import logging from datetime import datetime, timedelta import pandas as pd -from nextcord import Client, Message, Reaction -from nextcord import TextChannel -from nextcord.utils import AsyncIterator +from discord import Client, Message, Reaction, TextChannel + +# from nextcord import Client, Message, Reaction +# from nextcord import TextChannel +# from nextcord.utils import AsyncIterator + LOGGER = logging.getLogger(__name__) @@ -15,7 +18,7 @@ async def message_gen(client: Client, days: int = None, after: datetime = None, around: datetime = None, - oldest_first: bool = True) -> AsyncIterator[Message]: + oldest_first: bool = True): if days is not None: after = (datetime.today() - timedelta(days=days)).astimezone() @@ -38,7 +41,8 @@ async def message_gen(client: Client, async for msg in channel.history(**kwargs): yield msg for thread in channel.threads: - LOGGER.info(f'Thread: {channel.category}: {channel.name}: {thread.name}') + LOGGER.info( + f'Thread: {channel.category}: {channel.name}: {thread.name}') async for msg in thread.history(**kwargs): yield msg else: @@ -60,7 +64,7 @@ def reaction_dict(reaction: Reaction): } -async def reaction_gen(client: Client, **kwargs) -> AsyncIterator[Reaction]: +async def reaction_gen(client: Client, **kwargs): async for msg in message_gen(client=client, **kwargs): for reaction in msg.reactions: yield reaction_dict(reaction) diff --git a/src/kwaylon/reactions.py b/src/kwaylon/reactions.py index 675f6f2..40e42b4 100644 --- a/src/kwaylon/reactions.py +++ b/src/kwaylon/reactions.py @@ -5,7 +5,8 @@ from datetime import datetime, timedelta from pathlib import Path import pandas as pd -from nextcord import Message, Client +# from nextcord import Message, Client +from discord import Message, Client from .msg import reaction_dict, message_gen diff --git a/src/main.py b/src/main.py index b876fe3..6efc45b 100644 --- a/src/main.py +++ b/src/main.py @@ -1,47 +1,53 @@ +#!/usr/bin/env python3 + +import logging import os -import nextcord as discord +from discord import Intents, Message, RawReactionActionEvent from dotenv import load_dotenv -from nextcord import RawReactionActionEvent +from rich.highlighter import NullHighlighter +from rich.logging import RichHandler from kwaylon import Kwaylon if __name__ == '__main__': - import logging + logging.basicConfig( + level=logging.DEBUG, + # https://docs.python.org/3/library/logging.html#logrecord-attributes + format='[magenta]%(name)s[/] [cyan]%(funcName)s[/] %(message)s', + datefmt='%Y-%m-%d %I:%M:%S %p', + handlers=[ + RichHandler( + highlighter=NullHighlighter(), + markup=True, + rich_tracebacks=True, + tracebacks_suppress=['pandas'], + ) + ] + ) - logging.basicConfig(level=logging.INFO) - - client = Kwaylon() + for handler in logging.getLogger('discord.client').handlers: + print(handler) + intents = Intents.default() + intents.message_content = True + client = Kwaylon(intents=intents) @client.event async def on_ready(): await client.handle_ready() - # await client.data.scan_messages( - # client=client, - # limit=50, - # # days=7, - # ) - # chan = await client.fetch_channel(690588413543579649) - # msg = await chan.fetch_message(936684979654623293) - # logging.info(f'Msg: {msg.clean_content}') - # await msg.reply(f'https://tenor.com/view/i-will-orange-county-jack-black-nodding-nod-gif-4984565') - @client.event - async def on_message(message: discord.Message): + async def on_message(message: Message): await client.handle_message(message) - @client.event async def on_raw_reaction_add(payload: RawReactionActionEvent): await client.handle_raw_reaction(payload) - @client.event async def on_raw_reaction_remove(payload: RawReactionActionEvent): await client.handle_raw_reaction(payload) - load_dotenv() client.run(os.getenv('DISCORD_TOKEN'))