diff --git a/cogs/__init__.py b/cogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cogs/__pycache__/__init__.cpython-314.pyc b/cogs/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..bfcae80 Binary files /dev/null and b/cogs/__pycache__/__init__.cpython-314.pyc differ diff --git a/cogs/__pycache__/funchosa_parser.cpython-314.pyc b/cogs/__pycache__/funchosa_parser.cpython-314.pyc new file mode 100644 index 0000000..c4a0f04 Binary files /dev/null and b/cogs/__pycache__/funchosa_parser.cpython-314.pyc differ diff --git a/cogs/__pycache__/help.cpython-314.pyc b/cogs/__pycache__/help.cpython-314.pyc new file mode 100644 index 0000000..36af0a6 Binary files /dev/null and b/cogs/__pycache__/help.cpython-314.pyc differ diff --git a/cogs/__pycache__/kitty.cpython-314.pyc b/cogs/__pycache__/kitty.cpython-314.pyc new file mode 100644 index 0000000..20a776a Binary files /dev/null and b/cogs/__pycache__/kitty.cpython-314.pyc differ diff --git a/cogs/__pycache__/muter.cpython-314.pyc b/cogs/__pycache__/muter.cpython-314.pyc new file mode 100644 index 0000000..b0f8db3 Binary files /dev/null and b/cogs/__pycache__/muter.cpython-314.pyc differ diff --git a/cogs/__pycache__/role_manager.cpython-314.pyc b/cogs/__pycache__/role_manager.cpython-314.pyc new file mode 100644 index 0000000..703a2b1 Binary files /dev/null and b/cogs/__pycache__/role_manager.cpython-314.pyc differ diff --git a/cogs/__pycache__/status_rotator.cpython-314.pyc b/cogs/__pycache__/status_rotator.cpython-314.pyc new file mode 100644 index 0000000..1b6ce4e Binary files /dev/null and b/cogs/__pycache__/status_rotator.cpython-314.pyc differ diff --git a/cogs/__pycache__/uptime.cpython-314.pyc b/cogs/__pycache__/uptime.cpython-314.pyc new file mode 100644 index 0000000..b0a1423 Binary files /dev/null and b/cogs/__pycache__/uptime.cpython-314.pyc differ diff --git a/cogs/funchosa_parser.py b/cogs/funchosa_parser.py new file mode 100644 index 0000000..080d4ab --- /dev/null +++ b/cogs/funchosa_parser.py @@ -0,0 +1,322 @@ +import discord +from discord.ext import commands, tasks +from discord import app_commands +import asyncio +import logging +from datetime import datetime, timezone +from typing import Optional +import config +from utils.database import FunchosaDatabase + +logger = logging.getLogger(__name__) + +class FunchosaParser(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db = FunchosaDatabase() + + self.target_channel_id = config.FUNCHOSA_CHANNEL_ID + self.is_parsing = False + self.parsed_count = 0 + self.rate_limit_delay = 0.5 + + async def cog_load(self): + await self.db.init_db() + logger.info("[FunchosaParser] cog initialized") + + @commands.Cog.listener() + async def on_ready(self): + await asyncio.sleep(10) + + if not self.is_parsing: + await self.auto_parse_on_startup() + + async def auto_parse_on_startup(self): + try: + if not self.target_channel_id: + logger.warning("[FunchosaParser] no id channel ") + return + + channel = self.bot.get_channel(self.target_channel_id) + if not channel: + logger.warning(f"[FunchosaParser] no channel with id {self.target_channel_id} found") + return + + status = await self.db.get_parsing_status() + logger.info(f"[FunchosaParser] parsing status; firsttry = {not status['first_parse_done']}") + + if self.is_parsing: + logger.warning("[FunchosaParser] parsing already in progress") + return + + logger.info("[FunchosaParser] starting to parse") + + if not status['first_parse_done']: + logger.info("[FunchosaParser] first try, parse all") + count = await self._parse_history(channel, limit=None) + + last_message_id = await self.db.get_last_message_in_db() + await self.db.update_parsing_status( + first_parse_done=True, + last_parsed_message_id=last_message_id + ) + + logger.info(f"[FunchosaParser] parsing finished, total msg count: {count}") + else: + logger.info("[FunchosaParser] NOTfirst try, parse first 250") + count = await self._parse_history(channel, limit=250) + + if count > 0: + new_last_message_id = await self.db.get_last_message_in_db() + await self.db.update_parsing_status( + first_parse_done=True, + last_parsed_message_id=new_last_message_id + ) + + logger.info(f"[FunchosaParser] parsing finished, total msg count: {count}") + + except Exception as e: + logger.error(f"[FunchosaParser] err when parsing: {e}", exc_info=True) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + if self.target_channel_id and message.channel.id == self.target_channel_id: + await self._save_message(message) + + async def _save_message(self, message): + try: + if await self.db.message_exists(message.id): + return + + attachments_data = [] + attachment_urls = [] + + for attachment in message.attachments: + if attachment.url.lower().endswith(('png', 'jpg', 'jpeg', 'gif', 'webp')): + attachments_data.append({ + 'url': attachment.url, + 'filename': attachment.filename + }) + attachment_urls.append(attachment.url) + + message_data = { + 'message_id': message.id, + 'channel_id': message.channel.id, + 'author_id': message.author.id, + 'author_name': str(message.author), + 'content': message.content, + 'timestamp': message.created_at.isoformat(), + 'message_url': message.jump_url, + 'has_attachments': len(message.attachments) > 0, + 'attachment_urls': ','.join(attachment_urls), + 'attachments': attachments_data + } + + saved = await self.db.save_message(message_data) + if saved: + self.parsed_count += 1 + if self.parsed_count % 50 == 0: + logger.info(f"[FunchosaParser] saved messages total: {self.parsed_count}") + + except Exception as e: + logger.error(f"[FunchosaParser] err when saving msg: {e}") + + async def _parse_history(self, channel, limit=None, after_message=None): + try: + self.is_parsing = True + count = 0 + skipped = 0 + batch_size = 0 + + logger.info(f"[FunchosaParser] starting to parse {channel.name}") + + oldest_first = not limit or limit < 0 + + parse_kwargs = { + 'limit': abs(limit) if limit else None, + 'oldest_first': oldest_first, + } + + if after_message: + parse_kwargs['after'] = after_message + + async for message in channel.history(**parse_kwargs): + if message.author.bot: + continue + + if await self.db.message_exists(message.id): + skipped += 1 + batch_size += 1 + + if batch_size >= 100: + logger.info(f"[FunchosaParser] batch: +{count} новых, -{skipped} skipped") + batch_size = 0 + + continue + + await self._save_message(message) + count += 1 + batch_size += 1 + + await asyncio.sleep(self.rate_limit_delay) + + if batch_size >= 100: + logger.info(f"[FunchosaParser] batch: +{count} новых, -{skipped} skipped") + batch_size = 0 + + logger.info(f"[FunchosaParser] parsing done. total new: {count}, total skipped: {skipped}") + return count + + except Exception as e: + logger.error(f"[FunchosaParser] err when parsing history: {e}", exc_info=True) + return 0 + finally: + self.is_parsing = False + + @commands.hybrid_command() + @app_commands.describe( + number="номер сообщения из базы; optional" + ) + async def funchosarand(self, ctx, number: Optional[int] = None): + await ctx.defer() + + if number: + message_data = await self.db.get_message_by_number(number) + if not message_data: + await ctx.send(f"сообщение с номером {number} не найдено в базе ||соси черт||") + return + else: + message_data = await self.db.get_random_message() + if not message_data: + await ctx.send("помоему чет поломалось. меня пингани") + return + + embed = discord.Embed( + description=message_data['content'] or "*[без текста]*", + color=discord.Color.blue(), + timestamp=datetime.fromisoformat(message_data['timestamp']) + ) + + embed.set_author( + name='random фунчоза of the day' + ) + + + attachments_text = [] + if message_data.get('attachments'): + for i, att in enumerate(message_data['attachments'][:3], 1): + attachments_text.append(f"[вложение {i}]({att['url']})") + + embed.add_field( + name="info", + value=( + f"автор: <@{message_data['author_id']}>\n" + f"дата: {message_data['timestamp'].replace('T', ' ')[:19]}\n" + f"номер в базе: {message_data['id']}" + ), + inline=False + ) + + if attachments_text: + embed.add_field( + name="вложения", + value="\n".join(attachments_text), + inline=False + ) + + view = discord.ui.View() + view.add_item( + discord.ui.Button( + label="перейти к сообщению", + url=message_data['message_url'], + style=discord.ButtonStyle.link + ) + ) + + view.add_item( + discord.ui.Button( + label="подавай еще, раб", + custom_id="another_random", + style=discord.ButtonStyle.secondary + ) + ) + + await ctx.send(embed=embed, view=view) + + @commands.Cog.listener() + async def on_interaction(self, interaction: discord.Interaction): + if not interaction.data or 'custom_id' not in interaction.data: + return + + if interaction.data['custom_id'] == "another_random": + await interaction.response.defer() + + message_data = await self.db.get_random_message() + if not message_data: + await interaction.followup.send("помоему чет поломалось. меня пингани", ephemeral=True) + return + + embed = discord.Embed( + description=message_data['content'] or "*[без текста]*", + color=discord.Color.blue(), + timestamp=datetime.fromisoformat(message_data['timestamp']) + ) + + embed.set_author( + name='random фунчоза of the day' + ) + + embed.add_field( + name="info", + value=( + f"автор: <@{message_data['author_id']}>\n" + f"дата: {message_data['timestamp'].replace('T', ' ')[:19]}\n" + f"номер в базе: {message_data['id']}" + ), + inline=False + ) + + view = discord.ui.View() + view.add_item( + discord.ui.Button( + label="перейти к сообщению", + url=message_data['message_url'], + style=discord.ButtonStyle.link + ) + ) + view.add_item( + discord.ui.Button( + label="подавай еще, раб", + custom_id="another_random", + style=discord.ButtonStyle.secondary + ) + ) + + await interaction.followup.send(embed=embed, view=view) + + @commands.hybrid_command() + async def funchosainfo(self, ctx): + total = await self.db.get_total_count() + status = await self.db.get_parsing_status() + + embed = discord.Embed( + title="фунчоза.статы", + color=discord.Color.green() + ) + + embed.add_field(name="сообщений в базе", value=f"**{total}**", inline=True) + + if status['last_parsed_message_id']: + embed.add_field( + name="последнее сообщение", + value=f"id: `{status['last_parsed_message_id']}`", + inline=False + ) + + await ctx.send(embed=embed) + +async def setup(bot): + await bot.add_cog(FunchosaParser(bot)) \ No newline at end of file diff --git a/cogs/help.py b/cogs/help.py new file mode 100644 index 0000000..b451eaf --- /dev/null +++ b/cogs/help.py @@ -0,0 +1,27 @@ +import discord +from discord.ext import commands + +class HelpCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command(name="help") + async def help(self, ctx): + help_text = """ +комманды: + +``` +uptime : сколько времени прошло с запуска бота +funchosarand : рандомная пикча из фунчозы либо по айдишнику в базе +funchosainfo : фунчоза.статы +kitty : рандомная пикча кошечки из [thecatapi](https://thecatapi.com/) +``` + +префикс: `!` +в лс отпишите по предложениям че в бота докинуть + """ + + await ctx.send(help_text) + +async def setup(bot): + await bot.add_cog(HelpCog(bot)) \ No newline at end of file diff --git a/cogs/kitty.py b/cogs/kitty.py new file mode 100644 index 0000000..f3a6441 --- /dev/null +++ b/cogs/kitty.py @@ -0,0 +1,87 @@ +import discord +from discord.ext import commands +import aiohttp +import os +import logging + +logger = logging.getLogger(__name__) + +class Kitty(commands.Cog, name="Котики"): + def __init__(self, bot): + self.bot = bot + self.api_key = os.environ.get('CAT_API_KEY') + self.search_url = "https://api.thecatapi.com/v1/images/search" + + if not self.api_key: + logger.warning("[Kitty] no api key found") + + async def _fetch_random_cat(self): + headers = {"Content-Type": "application/json"} + + if self.api_key and self.api_key != "DEMO-API-KEY": + headers["x-api-key"] = self.api_key + else: + headers["x-api-key"] = "DEMO-API-KEY" + + params = { + 'size': 'med', + 'mime_types': 'jpg,png', + 'format': 'json', + 'has_breeds': 'true', + 'order': 'RANDOM', + 'limit': 1 + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(self.search_url, headers=headers, params=params) as response: + if response.status == 200: + data = await response.json() + if data and isinstance(data, list): + logger.info(f"[Kitty] API response received") + return data[0] + else: + logger.error(f"[Kitty] api err: {response.status}") + + try: + error_text = await response.text() + logger.error(f"[Kitty] api error text: {error_text}") + except: + pass + except aiohttp.ClientError as e: + logger.error(f"[Kitty] client error when contacting api: {e}") + except Exception as e: + logger.error(f"[Kitty] err: {e}") + return None + + @commands.hybrid_command(name="kitty", description="kitty") + async def kitty(self, ctx): + await ctx.defer() + logger.info(f"[Kitty] kitty request from {ctx.author.name} ({ctx.author.id})") + + cat_data = await self._fetch_random_cat() + + if not cat_data: + logger.warning("[Kitty] cat_data = null") + await ctx.send("помоему чет поломалось. меня пингани ||not cat_data||") + return + + image_url = cat_data.get('url') + if not image_url: + logger.error("[Kitty] no image url") + await ctx.send("помоему чет поломалось. меня пингани ||no image url||") + return + + + breeds_info = cat_data.get('breeds') + if breeds_info and len(breeds_info) > 0: + breed = breeds_info[0] + if breed.get('name'): + caption = f"{breed['name']}" + logger.info(f"[Kitty] Breed found: {breed['name']}") + + await ctx.send(f"random kitty of the day\n[{caption}]({image_url})") + logger.info(f"[Kitty] succesfully send kitty to {ctx.author.name}") + +async def setup(bot): + await bot.add_cog(Kitty(bot)) \ No newline at end of file diff --git a/cogs/quote.py b/cogs/quote.py new file mode 100644 index 0000000..e69de29 diff --git a/cogs/role_manager.py b/cogs/role_manager.py new file mode 100644 index 0000000..17424ce --- /dev/null +++ b/cogs/role_manager.py @@ -0,0 +1,147 @@ +import discord +from discord.ext import commands +from discord import app_commands +import config +from utils.data_manager import save_message_id, load_message_id +import logging + +# Создаем логгер для этого модуля +logger = logging.getLogger(__name__) + +class RoleManager(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.role_message_id = None + self.CHANNEL_ID = config.CHANNEL_ID + self.REACTION_ROLES = config.REACTION_ROLES + self._ready = False + + async def cog_load(self): + self.role_message_id = load_message_id() + if self.role_message_id: + logger.info(f"[RoleManager] initialized role msg with id: '{self.role_message_id}'") + else: + logger.info('[RoleManager] no role msg found') + + @commands.Cog.listener() + async def on_ready(self): + if not self._ready and self.role_message_id: + await self.check_and_sync_roles() + self._ready = True + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload): + await self.handle_reaction(payload, add_role=True) + + @commands.Cog.listener() + async def on_raw_reaction_remove(self, payload): + await self.handle_reaction(payload, add_role=False) + + async def handle_reaction(self, payload, add_role=True): + if payload.message_id != self.role_message_id: + return + + emoji = str(payload.emoji) + if emoji not in self.REACTION_ROLES: + return + + guild = self.bot.get_guild(payload.guild_id) + if not guild: + return + + member = guild.get_member(payload.user_id) + if not member or member.bot: + return + + role_id = self.REACTION_ROLES[emoji] + role = guild.get_role(role_id) + + if not role: + logger.warning(f"[RoleManager] role with id '{role_id}' not found") + return + + try: + if add_role: + await member.add_roles(role) + logger.info(f"[RoleManager] gave role '{role.name}' to '{member.name}'") + else: + await member.remove_roles(role) + logger.info(f"[RoleManager] removed role '{role.name}' from user '{member.name}'") + except discord.Forbidden: + logger.error(f"[RoleManager] not enough rights to give role '{role.name}'") + except Exception as e: + logger.error(f"[RoleManager] err: '{e}'") + + async def check_and_sync_roles(self): + if not self.role_message_id: + return + + try: + channel = await self.bot.fetch_channel(self.CHANNEL_ID) + if not channel: + logger.warning(f"[RoleManager] channel with id '{self.CHANNEL_ID}' not found") + return + + message = await channel.fetch_message(self.role_message_id) + + for reaction in message.reactions: + emoji = str(reaction.emoji) + + if emoji in self.REACTION_ROLES: + role_id = self.REACTION_ROLES[emoji] + role = message.guild.get_role(role_id) + + if not role: + logger.warning(f"[RoleManager] role with id '{role_id}' not found") + continue + + async for user in reaction.users(): + if user.bot: + continue + + member = message.guild.get_member(user.id) + if member and role not in member.roles: + await member.add_roles(role) + logger.info(f"[RoleManager] gave role '{role.name}' to user '{member.name}'") + + except discord.NotFound: + logger.warning("[RoleManager] role msg not found") + except discord.Forbidden: + logger.error("[RoleManager] no rights to get channel or message") + except Exception as e: + logger.error(f"[RoleManager] sync err: '{e}'") + + @commands.hybrid_command() + @commands.has_permissions(administrator=True) + async def create_role_message(self, ctx): + embed = discord.Embed( + title="ле", + description="пикните там роли снизу по реактам,\nшоб если куда то шли играть с фаршем >3 человек то сразу можно было ее пингануть\n\n" + "react_index = {\n" + " '💩': гревдиггер\n" + " '🤙': бара наверн хз\n" + " '🤕': пох ваще за любой движ\n" + " '🇺🇦': мтк\n" + "}\n\n" + "естессно кто будет спамить пингом ролей тот будет опущен и закинут в таймаут\n" + "если бот в оффе роли выдадутся когда я врублю его снова", + color=0x00ff00 + ) + + message = await ctx.send(embed=embed) + + for emoji in self.REACTION_ROLES.keys(): + await message.add_reaction(emoji) + + self.role_message_id = message.id + save_message_id(message.id) + logger.info(f"[RoleManager] created new role message with id: '{message.id}'") + + @commands.hybrid_command() + @commands.has_permissions(administrator=True) + async def sync_roles(self, ctx): + await self.check_and_sync_roles() + logger.info("[RoleManager] manual sync triggered by admin") + +async def setup(bot): + await bot.add_cog(RoleManager(bot)) \ No newline at end of file diff --git a/cogs/status_rotator.py b/cogs/status_rotator.py new file mode 100644 index 0000000..60021e0 --- /dev/null +++ b/cogs/status_rotator.py @@ -0,0 +1,65 @@ +import discord +from discord.ext import commands, tasks +import json +import random +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +class StatusRotator(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.statuses: list[str] = [] + self.current_index = 0 + self.status_file = 'data/statuses.json' + + async def cog_load(self): + await self.load_statuses() + self.rotate_status.start() + logger.info("[StatusRotator] status rotator initialized") + + async def load_statuses(self): + try: + with open(self.status_file, 'r', encoding='utf-8') as f: + data = json.load(f) + self.statuses = data.get('statuses', []) + + logger.info(f"[StatusRotator] loaded {len(self.statuses)} statuses") + + except FileNotFoundError: + logger.error(f"[StatusRotator] file {self.status_file} notfound") + except json.JSONDecodeError: + logger.error(f"[StatusRotaror] err while parsing JSON") + + def get_random_status(self) -> str: + return random.choice(self.statuses) + + def get_next_status(self) -> str: + status = self.statuses[self.current_index] + self.current_index = (self.current_index + 1) % len(self.statuses) + return status + + async def update_status(self, status_text: Optional[str] = None): + if status_text is None: + status_text = self.get_random_status() + + activity = discord.Game(name=status_text) + + try: + await self.bot.change_presence(activity=activity) + logger.debug(f"[StatusRotator] status updated: {status_text}") + except Exception as e: + logger.error(f"[StatusRotator] err while updating status: {e}") + + @tasks.loop(minutes=1.0) + async def rotate_status(self): + await self.update_status() + + @rotate_status.before_loop + async def before_rotate_status(self): + await self.bot.wait_until_ready() + + +async def setup(bot): + await bot.add_cog(StatusRotator(bot)) \ No newline at end of file diff --git a/cogs/uptime.py b/cogs/uptime.py new file mode 100644 index 0000000..018a30b --- /dev/null +++ b/cogs/uptime.py @@ -0,0 +1,46 @@ +import discord +from discord.ext import commands +import datetime + +class UptimeSimple(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.start_time = None + + @commands.Cog.listener() + async def on_ready(self): + if self.start_time is None: + self.start_time = datetime.datetime.now(datetime.timezone.utc) + + @commands.command(name="uptime") + async def uptime(self, ctx): + if self.start_time is None: + await ctx.send("ебать у тебя тайминги кнш") + return + + current_time = datetime.datetime.now(datetime.timezone.utc) + uptime = current_time - self.start_time + seconds = int(uptime.total_seconds()) + + days = seconds // 86400 + hours = (seconds % 86400) // 3600 + minutes = (seconds % 3600) // 60 + secs = seconds % 60 + + result = "бот работает уже: " + parts = [] + + if days > 0: + parts.append(f"{days} дня") + if hours > 0: + parts.append(f"{hours} часа") + if minutes > 0: + parts.append(f"{minutes} минут") + if secs > 0 or not parts: + parts.append(f"{secs} секунд") + + result += " ".join(parts) + await ctx.send(result) + +async def setup(bot): + await bot.add_cog(UptimeSimple(bot)) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..102baf0 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +TOKEN = os.getenv('DISCORD_TOKEN') +CHANNEL_ID = 1454107749028855971 # roles channel +FUNCHOSA_CHANNEL_ID = 1379127661095551048 + +REACTION_ROLES = { + '💩': 1454112057329717434, + '🤙': 1454112345109299281, + '🤕': 1454112388662956093, + '🇺🇦': 1454113041305305200, +} \ No newline at end of file diff --git a/data/funchosa.db b/data/funchosa.db new file mode 100644 index 0000000..aa55521 Binary files /dev/null and b/data/funchosa.db differ diff --git a/data/message_id.json b/data/message_id.json new file mode 100644 index 0000000..178aefd --- /dev/null +++ b/data/message_id.json @@ -0,0 +1 @@ +{"message_id": 1454128857102680187} \ No newline at end of file diff --git a/data/muter.m4a b/data/muter.m4a new file mode 100644 index 0000000..7e98d10 Binary files /dev/null and b/data/muter.m4a differ diff --git a/data/statuses.json b/data/statuses.json new file mode 100644 index 0000000..2aa0c40 --- /dev/null +++ b/data/statuses.json @@ -0,0 +1,88 @@ +{ + "statuses": [ + "смотрит порно", + "я все слышу", + "тима не срал целый год", + "тише пиздюк", + "ДРОЧИЛ 10 ЧАСОВ", + "muzk>sparta ezzzzzz", + "ээээя хз че писать в лс отпишите че сюда закинуть", + "нах ты это читаешь", + "'зашел чипсы взять а у меня евреи печень украли' - мартиниус водолазиссимус", + "футанари домми момми", + "ало гандон хватит в блюлок в роблоксе играть", + "Какой же чёрт на стрелке, пиздец просто.", + "random фунчоза of the day", + "Однажды Эрнест Хемингуэй поспорил, что напишет самый короткий рассказ, способный растрогать любого. Он выиграл спор:", + "айсберг группы femboy", + "мой папа это карп, а мама слон. зеленый? иди нахуй.", + "мне кажется нам стоит забанить тимоху", + "этому человеку только что пришла записка от картеля", + "-левое яйцо реджронуза\n-это правое не?", + "чо там\nя не вижу", + "КУПЛЮ ХАЗIК IЗ ФIЛЬТРАМI", + "руни ало", + "на сегодня хватит интернета с меня", + "бля клэр обскур", + "привет острый китайский снек латяо", + "will you ever fucking shut up", + "музык если и сосал...", + "discord.ext.commands.errors.CommandNotFound: Command Nigger is not found", + "го барку заебал", + "плииз спид ай нид тхис", + "надпись на курточке пизда", + "у меня батя таджик", + "я не могу дышааать", + "ну-ну, соплячок", + "я тебе блять яйца оторву", + "ЧТО ТАКОЕ ФУТАНАРИ, И ЗАЧЕМ ТЫ ПОСТОЯННО ГУГЛИШЬ ПИЩЕВУЮ ДОБАВКУ E621", + "договорнячок)))", + "ветеран вазовский сражается под пидорославией за последний шприц героина", + "jesse, play FEMTANYL even if we scare the hoes", + "its over", + "its over? i didnt hear no bell", + "привет это зомбальный", + "всем привет это я джордж флойд", + "посмотрите на этот чииииизбургеер из пабааааааа", + "Убил человека х3", + "нехватка боеприпасов 90%", + "КТО ПЕРЕКЛЮЧИЛ ВЕЧЕР С СОЛОВЬЕВЫМ", + "сво? гойда? зов?", + "Я ТВОЙ РОТ ШАТАЛ БЛЯ ищи в гугле самый мощный нация", + "STALCRAFT ГОВНО ЕБАНОЕ", + "BAROTRAUMA ГОВНО ЕБАНОЕ", + "F&HUNGER ГОВНО ЕБАНОЕ", + "My spaces? Liminal. My angels? Biblically accurate.", + "Patrick!!! Where are my antipsychotics!!", + "Say the line, peajack.", + "Отсыпь шмали мне Черт ебучий", + "в этой шараге хулиганю Я!!!!!!", + "привет! я чижик!", + "I see you've fall for the old Jewish trick of using evidence to make a point", + "Im out! 1.3 Seconds", + "(I think i need a lobotomy)", + "Я не расист, я убиваю все виды людей!", + "why did spongebob do that", + "O LORD GIVE GAY PORN", + "candy crush saga > vinland saga", + "МУЧИТЕЛЬНО НО ОКЕЙ", + "Mood atm Mischievous", + "YOUR TONE", + "Хрюкни", + "тимоха, че ты творишь нахуй!", + "Скоро стану шлюхой с прицепом. Желаю вам того же!", + "Обед, Уютненько. Подписаться", + "а ведь в одной из этих активностей, (которые кстати меняются 5 минут) есть дискорд нитро.", + "я ниче не хочу я нехочун", + "сьешь еще этих мягких французских булок", + "ебанина какая то", + "pattern recognition for avoiding predators, yeeeees", + "легализуйте каннибализм", + "некоторые шалости уголовно наказуемы", + "ЧЕ ЧЕ ОПА НИХУЯ", + "import во мне дохуя, Java передо мной стоит и ;;;; дрочит свои. Я говорю: 'старина, сьеби нахуй'. Даю просто invalid syntax и все. - Python 3.9.5", + "я хотел бы влится в семью дилары и бустера", + "кеичи, ты проебал в маджонг. твое наказание - обрезание", + "отдых анапа 2007" + ] +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..48d2305 --- /dev/null +++ b/main.py @@ -0,0 +1,57 @@ +import discord +from discord.ext import commands +import config +import asyncio +import logging + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('bot.log', encoding='utf-8'), + logging.StreamHandler() + ] +) + +intents = discord.Intents.default() +intents.message_content = True +intents.reactions = True +intents.members = True +intents.guilds = True +intents.messages = True +intents.voice_states = True + +class Bot(commands.Bot): + def __init__(self): + super().__init__( + command_prefix='!', + intents=intents, + help_command=None, + ) + + async def setup_hook(self): + # ! load cogs + await self.load_extension('cogs.role_manager') + await self.load_extension('cogs.status_rotator') + await self.load_extension('cogs.funchosa_parser') + await self.load_extension('cogs.uptime') + await self.load_extension('cogs.help') + await self.load_extension('cogs.kitty') + #await self.load_extension('cogs.muter') # ass + # adding new modules: + # await self.load_extension('cogs.whyrureadingts') + + await self.tree.sync() + + async def on_ready(self): + print(f"bot initialized succesfully with user '{self.user}'") + print(f"user.id: '{self.user.id}'") + print('initialization (probably) complete; further is logs.') + print('\n*------*\n') + +async def main(): + bot = Bot() + await bot.start(config.TOKEN) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f788b88 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv>=1.0.0 +discord.py>=2.3.0 +aiofiles +aiosqlite \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__pycache__/__init__.cpython-314.pyc b/utils/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..a808d83 Binary files /dev/null and b/utils/__pycache__/__init__.cpython-314.pyc differ diff --git a/utils/__pycache__/data_manager.cpython-314.pyc b/utils/__pycache__/data_manager.cpython-314.pyc new file mode 100644 index 0000000..1940905 Binary files /dev/null and b/utils/__pycache__/data_manager.cpython-314.pyc differ diff --git a/utils/__pycache__/database.cpython-314.pyc b/utils/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000..71e9ec6 Binary files /dev/null and b/utils/__pycache__/database.cpython-314.pyc differ diff --git a/utils/data_manager.py b/utils/data_manager.py new file mode 100644 index 0000000..6ecc9c0 --- /dev/null +++ b/utils/data_manager.py @@ -0,0 +1,19 @@ +import json +import os + +MESSAGE_ID_FILE = 'data/message_id.json' #ts for roles + +def save_message_id(message_id): + os.makedirs('data', exist_ok=True) + with open(MESSAGE_ID_FILE, 'w') as f: + json.dump({'message_id': message_id}, f) + +def load_message_id(): + try: + with open(MESSAGE_ID_FILE, 'r') as f: + data = json.load(f) + return data.get('message_id') + except FileNotFoundError: + return None + except json.JSONDecodeError: + return None \ No newline at end of file diff --git a/utils/database.py b/utils/database.py new file mode 100644 index 0000000..bd05cc7 --- /dev/null +++ b/utils/database.py @@ -0,0 +1,211 @@ +import aiosqlite +import asyncio +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +class FunchosaDatabase: + def __init__(self, db_path='data/funchosa.db'): + self.db_path = db_path + + async def init_db(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id BIGINT UNIQUE NOT NULL, + channel_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + author_name TEXT NOT NULL, + content TEXT, + timestamp TIMESTAMP NOT NULL, + message_url TEXT NOT NULL, + has_attachments BOOLEAN DEFAULT 0, + attachment_urls TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + await db.execute(''' + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER, + url TEXT UNIQUE NOT NULL, + filename TEXT, + FOREIGN KEY (message_id) REFERENCES messages (id) + ) + ''') + + await db.execute(''' + CREATE TABLE IF NOT EXISTS parsing_status ( + id INTEGER PRIMARY KEY CHECK (id = 1), + first_parse_done BOOLEAN DEFAULT 0, + last_parsed_message_id BIGINT, + last_parse_time TIMESTAMP + ) + ''') + + await db.execute('CREATE INDEX IF NOT EXISTS idx_message_id ON messages(message_id)') + await db.execute('CREATE INDEX IF NOT EXISTS idx_author_id ON messages(author_id)') + await db.execute('CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp)') + + await db.commit() + logger.info("[FunchosaDatabase] funchosa db initialized") + + async def get_parsing_status(self): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + 'SELECT first_parse_done, last_parsed_message_id FROM parsing_status WHERE id = 1' + ) + result = await cursor.fetchone() + + if result: + return { + 'first_parse_done': bool(result[0]), + 'last_parsed_message_id': result[1] + } + else: + await db.execute( + 'INSERT INTO parsing_status (id, first_parse_done, last_parsed_message_id) VALUES (1, 0, NULL)' + ) + await db.commit() + return { + 'first_parse_done': False, + 'last_parsed_message_id': None + } + + async def update_parsing_status(self, first_parse_done=False, last_parsed_message_id=None): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + UPDATE parsing_status + SET first_parse_done = ?, + last_parsed_message_id = ?, + last_parse_time = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (first_parse_done, last_parsed_message_id)) + await db.commit() + + async def get_last_message_in_db(self): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + 'SELECT message_id FROM messages ORDER BY message_id DESC LIMIT 1' + ) + result = await cursor.fetchone() + return result[0] if result else None + + async def save_message(self, message_data): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(''' + INSERT OR IGNORE INTO messages + (message_id, channel_id, author_id, author_name, content, + timestamp, message_url, has_attachments, attachment_urls) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + message_data['message_id'], + message_data['channel_id'], + message_data['author_id'], + message_data['author_name'], + message_data['content'], + message_data['timestamp'], + message_data['message_url'], + message_data['has_attachments'], + message_data['attachment_urls'] + )) + + if cursor.rowcount > 0: + message_db_id = cursor.lastrowid + + if message_data['attachments']: + for attachment in message_data['attachments']: + await db.execute(''' + INSERT OR IGNORE INTO attachments + (message_id, url, filename) + VALUES (?, ?, ?) + ''', ( + message_db_id, + attachment['url'], + attachment['filename'] + )) + + await db.commit() + return True + return False + + async def message_exists(self, message_id): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + 'SELECT 1 FROM messages WHERE message_id = ? LIMIT 1', + (message_id,) + ) + result = await cursor.fetchone() + return result is not None + + async def get_random_message(self): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(''' + SELECT m.*, + GROUP_CONCAT(a.url) as attachment_urls_list, + GROUP_CONCAT(a.filename) as attachment_filenames + FROM messages m + LEFT JOIN attachments a ON m.id = a.message_id + GROUP BY m.id + ORDER BY RANDOM() + LIMIT 1 + ''') + + row = await cursor.fetchone() + if not row: + return None + + columns = [description[0] for description in cursor.description] + message = dict(zip(columns, row)) + + if message['attachment_urls_list']: + urls = message['attachment_urls_list'].split(',') + filenames = message['attachment_filenames'].split(',') if message['attachment_filenames'] else [] + message['attachments'] = [ + {'url': url, 'filename': filename} + for url, filename in zip(urls, filenames) + ] + else: + message['attachments'] = [] + + return message + + async def get_total_count(self): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute('SELECT COUNT(*) FROM messages') + result = await cursor.fetchone() + return result[0] if result else 0 + + async def get_message_by_number(self, number): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(''' + SELECT m.*, + GROUP_CONCAT(a.url) as attachment_urls_list, + GROUP_CONCAT(a.filename) as attachment_filenames + FROM messages m + LEFT JOIN attachments a ON m.id = a.message_id + WHERE m.id = ? + GROUP BY m.id + ''', (number,)) + + row = await cursor.fetchone() + if not row: + return None + + columns = [description[0] for description in cursor.description] + message = dict(zip(columns, row)) + + if message.get('attachment_urls_list'): + urls = message['attachment_urls_list'].split(',') + filenames = message['attachment_filenames'].split(',') if message['attachment_filenames'] else [] + message['attachments'] = [ + {'url': url, 'filename': filename} + for url, filename in zip(urls, filenames) + ] + else: + message['attachments'] = [] + + return message \ No newline at end of file