1 #2

Merged
rejnronuzz merged 6 commits from main into dev 2026-03-09 06:19:03 +00:00
12 changed files with 539 additions and 651 deletions
Showing only changes of commit 7b45fdd8f1 - Show all commits

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
.env
bot.log
__pycache__/
*.pyc
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
.vscode/
.idea/
*.swp
*.swo
*.log

View File

@@ -1,88 +1,120 @@
import discord import discord
from discord.ext import commands, tasks from discord.ext import commands
from discord import app_commands from discord import app_commands
import asyncio import asyncio
import logging import logging
from datetime import datetime, timezone from datetime import datetime
from typing import Optional from typing import Optional
import config import config
from utils.database import FunchosaDatabase from utils.database import FunchosaDatabase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def build_funchosa_embed(message_data: dict) -> discord.Embed:
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
)
if message_data.get('attachments'):
links = [
f"[вложение {i}]({att['url']})"
for i, att in enumerate(message_data['attachments'][:3], 1)
]
embed.add_field(name="вложения", value="\n".join(links), inline=False)
return embed
def build_funchosa_view() -> discord.ui.View:
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="подавай еще, раб",
custom_id="another_random",
style=discord.ButtonStyle.secondary
))
return view
class FunchosaView(discord.ui.View):
def __init__(self, db: FunchosaDatabase, message_url: str):
super().__init__(timeout=None)
self.db = db
self.add_item(discord.ui.Button(
label="перейти к сообщению",
url=message_url,
style=discord.ButtonStyle.link
))
@discord.ui.button(label="подавай еще, раб", custom_id="another_random", style=discord.ButtonStyle.secondary)
async def another_random(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.defer()
message_data = await self.db.get_random_message()
if not message_data:
await interaction.followup.send("помоему чет поломалось. меня пингани", ephemeral=True)
return
embed = build_funchosa_embed(message_data)
view = FunchosaView(self.db, message_data['message_url'])
await interaction.followup.send(embed=embed, view=view)
class FunchosaParser(commands.Cog): class FunchosaParser(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.db = FunchosaDatabase() self.db = FunchosaDatabase()
self.target_channel_id = config.FUNCHOSA_CHANNEL_ID self.target_channel_id = config.FUNCHOSA_CHANNEL_ID
self.is_parsing = False self.is_parsing = False
self.parsed_count = 0 self.parsed_count = 0
self.rate_limit_delay = 0.5
async def cog_load(self): async def cog_load(self):
await self.db.init_db() await self.db.init_db()
logger.info("[FunchosaParser] cog initialized") logger.info("FunchosaParser initialized")
await self.bot.wait_until_ready()
@commands.Cog.listener() await self.auto_parse_on_startup()
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): async def auto_parse_on_startup(self):
if self.is_parsing:
logger.warning("Parsing already in progress, skipping startup parse")
return
channel = self.bot.get_channel(self.target_channel_id)
if not channel:
logger.warning("Channel with id %s not found", self.target_channel_id)
return
try: 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() status = await self.db.get_parsing_status()
logger.info(f"[FunchosaParser] parsing status; firsttry = {not status['first_parse_done']}") is_first = not status['first_parse_done']
limit = None if is_first else 250
logger.info("Starting %s parse", "full" if is_first else "incremental")
if self.is_parsing: count = await self._parse_history(channel, limit=limit)
logger.warning("[FunchosaParser] parsing already in progress")
return
logger.info("[FunchosaParser] starting to parse") last_id = await self.db.get_last_message_in_db()
await self.db.update_parsing_status(first_parse_done=True, last_parsed_message_id=last_id)
if not status['first_parse_done']: logger.info("Parsing finished, %d new messages", count)
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: except Exception as e:
logger.error(f"[FunchosaParser] err when parsing: {e}", exc_info=True) logger.error("Error during startup parse: %s", e, exc_info=True)
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message): async def on_message(self, message):
if message.author.bot: if message.author.bot:
return return
if self.target_channel_id and message.channel.id == self.target_channel_id: if self.target_channel_id and message.channel.id == self.target_channel_id:
await self._save_message(message) await self._save_message(message)
@@ -91,16 +123,11 @@ class FunchosaParser(commands.Cog):
if await self.db.message_exists(message.id): if await self.db.message_exists(message.id):
return return
attachments_data = [] attachments_data = [
attachment_urls = [] {'url': a.url, 'filename': a.filename}
for a in message.attachments
for attachment in message.attachments: if a.url.lower().endswith(('png', 'jpg', 'jpeg', 'gif', 'webp'))
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_data = {
'message_id': message.id, 'message_id': message.id,
@@ -110,83 +137,58 @@ class FunchosaParser(commands.Cog):
'content': message.content, 'content': message.content,
'timestamp': message.created_at.isoformat(), 'timestamp': message.created_at.isoformat(),
'message_url': message.jump_url, 'message_url': message.jump_url,
'has_attachments': len(message.attachments) > 0, 'has_attachments': bool(message.attachments),
'attachment_urls': ','.join(attachment_urls), 'attachments': attachments_data,
'attachments': attachments_data
} }
saved = await self.db.save_message(message_data) saved = await self.db.save_message(message_data)
if saved: if saved:
self.parsed_count += 1 self.parsed_count += 1
if self.parsed_count % 50 == 0: if self.parsed_count % 50 == 0:
logger.info(f"[FunchosaParser] saved messages total: {self.parsed_count}") logger.info("Saved %d messages so far", self.parsed_count)
except Exception as e: except Exception as e:
logger.error(f"[FunchosaParser] err when saving msg: {e}") logger.error("Error saving message: %s", e)
async def _parse_history(self, channel, limit=None):
self.is_parsing = True
count = 0
skipped = 0
async def _parse_history(self, channel, limit=None, after_message=None):
try: try:
self.is_parsing = True logger.info("Parsing history of #%s (limit=%s)", channel.name, limit)
count = 0
skipped = 0
batch_size = 0
logger.info(f"[FunchosaParser] starting to parse {channel.name}") async for message in channel.history(limit=limit, oldest_first=True):
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: if message.author.bot:
continue continue
if await self.db.message_exists(message.id): if await self.db.message_exists(message.id):
skipped += 1 skipped += 1
batch_size += 1
if batch_size >= 100:
logger.info(f"[FunchosaParser] batch: +{count} новых, -{skipped} skipped")
batch_size = 0
continue continue
await self._save_message(message) await self._save_message(message)
count += 1 count += 1
batch_size += 1
await asyncio.sleep(self.rate_limit_delay) if (count + skipped) % 100 == 0:
logger.info("Progress: +%d new, -%d skipped", count, skipped)
if batch_size >= 100: logger.info("Parse done: %d new, %d skipped", count, skipped)
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 return count
except Exception as e: except Exception as e:
logger.error(f"[FunchosaParser] err when parsing history: {e}", exc_info=True) logger.error("Error parsing history: %s", e, exc_info=True)
return 0 return 0
finally: finally:
self.is_parsing = False self.is_parsing = False
@commands.hybrid_command() @commands.hybrid_command()
@app_commands.describe( @app_commands.describe(number="номер сообщения из базы; optional")
number="номер сообщения из базы; optional"
)
async def funchosarand(self, ctx, number: Optional[int] = None): async def funchosarand(self, ctx, number: Optional[int] = None):
await ctx.defer() await ctx.defer()
if number: if number:
message_data = await self.db.get_message_by_number(number) message_data = await self.db.get_message_by_number(number)
if not message_data: if not message_data:
await ctx.send(f"сообщение с номером {number} не найдено в базе ||соси черт||") await ctx.send(f"сообщение с номером {number} не найдено в базе")
return return
else: else:
message_data = await self.db.get_random_message() message_data = await self.db.get_random_message()
@@ -194,119 +196,16 @@ class FunchosaParser(commands.Cog):
await ctx.send("помоему чет поломалось. меня пингани") await ctx.send("помоему чет поломалось. меня пингани")
return return
embed = discord.Embed( embed = build_funchosa_embed(message_data)
description=message_data['content'] or "*[без текста]*", view = FunchosaView(self.db, message_data['message_url'])
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) 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() @commands.hybrid_command()
async def funchosainfo(self, ctx): async def funchosainfo(self, ctx):
total = await self.db.get_total_count() total = await self.db.get_total_count()
status = await self.db.get_parsing_status() status = await self.db.get_parsing_status()
embed = discord.Embed( embed = discord.Embed(title="фунчоза.статы", color=discord.Color.green())
title="фунчоза.статы",
color=discord.Color.green()
)
embed.add_field(name="сообщений в базе", value=f"**{total}**", inline=True) embed.add_field(name="сообщений в базе", value=f"**{total}**", inline=True)
if status['last_parsed_message_id']: if status['last_parsed_message_id']:
@@ -318,5 +217,6 @@ class FunchosaParser(commands.Cog):
await ctx.send(embed=embed) await ctx.send(embed=embed)
async def setup(bot): async def setup(bot):
await bot.add_cog(FunchosaParser(bot)) await bot.add_cog(FunchosaParser(bot))

View File

@@ -1,5 +1,9 @@
import discord import logging
from discord.ext import commands from discord.ext import commands
import config
logger = logging.getLogger(__name__)
class HelpCog(commands.Cog): class HelpCog(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
@@ -7,21 +11,9 @@ class HelpCog(commands.Cog):
@commands.command(name="help") @commands.command(name="help")
async def help(self, ctx): async def help(self, ctx):
help_text = """ await ctx.send(config.HELP_TEXT)
комманды: logger.debug("Help requested by %s", ctx.author.name)
```
uptime : сколько времени прошло с запуска бота
funchosarand <id> : рандомная пикча из фунчозы либо по айдишнику в базе
funchosainfo : фунчоза.статы
kitty : рандомная пикча кошечки из [thecatapi](https://thecatapi.com/)
```
префикс: `!`
в лс отпишите по предложениям че в бота докинуть
"""
await ctx.send(help_text)
async def setup(bot): async def setup(bot):
await bot.add_cog(HelpCog(bot)) await bot.add_cog(HelpCog(bot))

View File

@@ -6,82 +6,83 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Kitty(commands.Cog, name="Котики"): class Kitty(commands.Cog, name="Котики"):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.api_key = os.environ.get('CAT_API_KEY') self.api_key = os.environ.get('CAT_API_KEY')
self.search_url = "https://api.thecatapi.com/v1/images/search" self.search_url = "https://api.thecatapi.com/v1/images/search"
self.session: aiohttp.ClientSession | None = None
if not self.api_key: if not self.api_key:
logger.warning("[Kitty] no api key found") logger.warning("No CAT_API_KEY found, using unauthenticated requests")
async def cog_load(self):
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["x-api-key"] = self.api_key
self.session = aiohttp.ClientSession(headers=headers)
async def cog_unload(self):
if self.session:
await self.session.close()
async def _fetch_random_cat(self): 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 = { params = {
'size': 'med', 'size': 'med',
'mime_types': 'jpg,png', 'mime_types': 'jpg,png',
'format': 'json', 'format': 'json',
'has_breeds': 'true', 'has_breeds': 'true',
'order': 'RANDOM', 'order': 'RANDOM',
'limit': 1 'limit': 1,
} }
try: try:
async with aiohttp.ClientSession() as session: async with self.session.get(self.search_url, params=params) as response:
async with session.get(self.search_url, headers=headers, params=params) as response: if response.status != 200:
if response.status == 200: error_text = await response.text()
data = await response.json() logger.error("API error %s: %s", response.status, error_text)
if data and isinstance(data, list): return None
logger.info(f"[Kitty] API response received")
return data[0] data = await response.json()
else: if not data or not isinstance(data, list):
logger.error(f"[Kitty] api err: {response.status}") logger.error("Unexpected API response format: %s", data)
return None
return data[0]
try:
error_text = await response.text()
logger.error(f"[Kitty] api error text: {error_text}")
except:
pass
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
logger.error(f"[Kitty] client error when contacting api: {e}") logger.error("HTTP client error: %s", e)
except Exception as e: except Exception as e:
logger.error(f"[Kitty] err: {e}") logger.error("Unexpected error fetching cat: %s", e)
return None return None
@commands.hybrid_command(name="kitty", description="kitty") @commands.hybrid_command(name="kitty", description="kitty")
async def kitty(self, ctx): async def kitty(self, ctx):
await ctx.defer() await ctx.defer()
logger.info(f"[Kitty] kitty request from {ctx.author.name} ({ctx.author.id})") logger.info("Kitty request from %s (%s)", ctx.author.name, ctx.author.id)
cat_data = await self._fetch_random_cat() cat_data = await self._fetch_random_cat()
if not cat_data: if not cat_data:
logger.warning("[Kitty] cat_data = null")
await ctx.send("помоему чет поломалось. меня пингани ||not cat_data||") await ctx.send("помоему чет поломалось. меня пингани ||not cat_data||")
return return
image_url = cat_data.get('url') image_url = cat_data.get('url')
if not image_url: if not image_url:
logger.error("[Kitty] no image url")
await ctx.send("помоему чет поломалось. меня пингани ||no image url||") await ctx.send("помоему чет поломалось. меня пингани ||no image url||")
return return
breeds = cat_data.get('breeds')
breed_name = breeds[0].get('name') if breeds else None
breeds_info = cat_data.get('breeds') if breed_name:
if breeds_info and len(breeds_info) > 0: logger.info("Breed found: %s", breed_name)
breed = breeds_info[0] await ctx.send(f"random kitty of the day\n[{breed_name}]({image_url})")
if breed.get('name'): else:
caption = f"{breed['name']}" await ctx.send(f"random kitty of the day\n{image_url}")
logger.info(f"[Kitty] Breed found: {breed['name']}")
logger.info("Successfully sent kitty to %s", ctx.author.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): async def setup(bot):
await bot.add_cog(Kitty(bot)) await bot.add_cog(Kitty(bot))

View File

@@ -1,33 +1,29 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord import app_commands
import config import config
from utils.data_manager import save_message_id, load_message_id from utils.data_manager import save_message_id, load_message_id
import logging import logging
# Создаем логгер для этого модуля
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RoleManager(commands.Cog): class RoleManager(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.role_message_id = None self.role_message_id = None
self.CHANNEL_ID = config.CHANNEL_ID self.CHANNEL_ID = config.CHANNEL_ID
self.REACTION_ROLES = config.REACTION_ROLES self.REACTION_ROLES = config.REACTION_ROLES
self._ready = False
async def cog_load(self): async def cog_load(self):
self.role_message_id = load_message_id() self.role_message_id = load_message_id()
if self.role_message_id: if self.role_message_id:
logger.info(f"[RoleManager] initialized role msg with id: '{self.role_message_id}'") logger.info("Initialized role message with id: %s", self.role_message_id)
else: else:
logger.info('[RoleManager] no role msg found') logger.info("No role message found")
@commands.Cog.listener() await self.bot.wait_until_ready()
async def on_ready(self): if self.role_message_id:
if not self._ready and self.role_message_id:
await self.check_and_sync_roles() await self.check_and_sync_roles()
self._ready = True
@commands.Cog.listener() @commands.Cog.listener()
async def on_raw_reaction_add(self, payload): async def on_raw_reaction_add(self, payload):
@@ -55,22 +51,21 @@ class RoleManager(commands.Cog):
role_id = self.REACTION_ROLES[emoji] role_id = self.REACTION_ROLES[emoji]
role = guild.get_role(role_id) role = guild.get_role(role_id)
if not role: if not role:
logger.warning(f"[RoleManager] role with id '{role_id}' not found") logger.warning("Role with id %s not found", role_id)
return return
try: try:
if add_role: if add_role:
await member.add_roles(role) await member.add_roles(role)
logger.info(f"[RoleManager] gave role '{role.name}' to '{member.name}'") logger.info("Gave role '%s' to '%s'", role.name, member.name)
else: else:
await member.remove_roles(role) await member.remove_roles(role)
logger.info(f"[RoleManager] removed role '{role.name}' from user '{member.name}'") logger.info("Removed role '%s' from '%s'", role.name, member.name)
except discord.Forbidden: except discord.Forbidden:
logger.error(f"[RoleManager] not enough rights to give role '{role.name}'") logger.error("Missing permissions to manage role '%s'", role.name)
except Exception as e: except Exception as e:
logger.error(f"[RoleManager] err: '{e}'") logger.error("Unexpected error in handle_reaction: %s", e)
async def check_and_sync_roles(self): async def check_and_sync_roles(self):
if not self.role_message_id: if not self.role_message_id:
@@ -78,70 +73,68 @@ class RoleManager(commands.Cog):
try: try:
channel = await self.bot.fetch_channel(self.CHANNEL_ID) 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) message = await channel.fetch_message(self.role_message_id)
for reaction in message.reactions: for reaction in message.reactions:
emoji = str(reaction.emoji) emoji = str(reaction.emoji)
if emoji not in self.REACTION_ROLES:
continue
if emoji in self.REACTION_ROLES: role = message.guild.get_role(self.REACTION_ROLES[emoji])
role_id = self.REACTION_ROLES[emoji] if not role:
role = message.guild.get_role(role_id) logger.warning("Role with id %s not found during sync", self.REACTION_ROLES[emoji])
continue
if not role: async for user in reaction.users():
logger.warning(f"[RoleManager] role with id '{role_id}' not found") if user.bot:
continue continue
member = message.guild.get_member(user.id)
async for user in reaction.users(): if member and role not in member.roles:
if user.bot: await member.add_roles(role)
continue logger.info("Sync gave role '%s' to '%s'", role.name, member.name)
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: except discord.NotFound:
logger.warning("[RoleManager] role msg not found") logger.warning("Role message not found during sync")
except discord.Forbidden: except discord.Forbidden:
logger.error("[RoleManager] no rights to get channel or message") logger.error("Missing permissions to fetch channel or message")
except Exception as e: except Exception as e:
logger.error(f"[RoleManager] sync err: '{e}'") logger.error("Unexpected sync error: %s", e)
@commands.hybrid_command() @commands.hybrid_command()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def create_role_message(self, ctx): async def create_role_message(self, ctx):
embed = discord.Embed( message = await ctx.send(config.ROLE_MESSAGE_TEXT)
title="ле", for emoji in self.REACTION_ROLES:
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) await message.add_reaction(emoji)
self.role_message_id = message.id self.role_message_id = message.id
save_message_id(message.id) save_message_id(message.id)
logger.info(f"[RoleManager] created new role message with id: '{message.id}'") logger.info("Created new role message with id: %s", message.id)
@commands.hybrid_command() @commands.hybrid_command()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def sync_roles(self, ctx): async def update_role_message(self, ctx):
await self.check_and_sync_roles() if not self.role_message_id:
logger.info("[RoleManager] manual sync triggered by admin") return
try:
channel = await self.bot.fetch_channel(self.CHANNEL_ID)
message = await channel.fetch_message(self.role_message_id)
await message.edit(content=config.ROLE_MESSAGE_TEXT)
existing = [str(r.emoji) for r in message.reactions]
for emoji in self.REACTION_ROLES:
if emoji not in existing:
await message.add_reaction(emoji)
logger.info("Role message updated by %s", ctx.author.name)
except discord.NotFound:
logger.warning("Role message not found during update")
except discord.Forbidden:
logger.error("Missing permissions to edit role message")
async def setup(bot): async def setup(bot):
await bot.add_cog(RoleManager(bot)) await bot.add_cog(RoleManager(bot))

View File

@@ -7,50 +7,59 @@ from typing import Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class StatusRotator(commands.Cog): class StatusRotator(commands.Cog):
def __init__(self, bot): def __init__(self, bot, *, status_file: str = 'data/statuses.json', interval: float = 1.0):
self.bot = bot self.bot = bot
self.statuses: list[str] = [] self.statuses: list[str] = []
self.current_index = 0 self.current_index = 0
self.status_file = 'data/statuses.json' self.status_file = status_file
self.rotate_status.change_interval(minutes=interval)
async def cog_load(self): async def cog_load(self):
await self.load_statuses() await self.load_statuses()
self.rotate_status.start() self.rotate_status.start()
logger.info("[StatusRotator] status rotator initialized") logger.info("Status rotator initialized with %d statuses", len(self.statuses))
async def cog_unload(self):
self.rotate_status.cancel()
async def load_statuses(self): async def load_statuses(self):
try: try:
with open(self.status_file, 'r', encoding='utf-8') as f: with open(self.status_file, 'r', encoding='utf-8') as f:
data = json.load(f) data = json.load(f)
self.statuses = data.get('statuses', []) self.statuses = data.get('statuses', [])
logger.info("Loaded %d statuses", len(self.statuses))
logger.info(f"[StatusRotator] loaded {len(self.statuses)} statuses")
except FileNotFoundError: except FileNotFoundError:
logger.error(f"[StatusRotator] file {self.status_file} notfound") logger.error("Status file not found: %s", self.status_file)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.error(f"[StatusRotaror] err while parsing JSON") logger.error("Failed to parse JSON in: %s", self.status_file)
def get_random_status(self) -> str: def get_next_status(self) -> Optional[str]:
return random.choice(self.statuses) if not self.statuses:
return None
def get_next_status(self) -> str:
status = self.statuses[self.current_index] status = self.statuses[self.current_index]
self.current_index = (self.current_index + 1) % len(self.statuses) self.current_index = (self.current_index + 1) % len(self.statuses)
return status return status
async def update_status(self, status_text: Optional[str] = None): def get_random_status(self) -> Optional[str]:
if status_text is None: if not self.statuses:
status_text = self.get_random_status() return None
return random.choice(self.statuses)
activity = discord.Game(name=status_text) async def update_status(self, status_text: Optional[str] = None):
if not self.statuses:
logger.warning("No statuses loaded, skipping update")
return
if status_text is None:
status_text = self.get_next_status()
try: try:
await self.bot.change_presence(activity=activity) await self.bot.change_presence(activity=discord.Game(name=status_text))
logger.debug(f"[StatusRotator] status updated: {status_text}") logger.debug("Status updated: %s", status_text)
except Exception as e: except Exception as e:
logger.error(f"[StatusRotator] err while updating status: {e}") logger.error("Failed to update status: %s", e)
@tasks.loop(minutes=1.0) @tasks.loop(minutes=1.0)
async def rotate_status(self): async def rotate_status(self):

View File

@@ -1,46 +1,47 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
import datetime
def pluralize(n: int, one: str, few: str, many: str) -> str:
if 11 <= n % 100 <= 14:
return f"{n} {many}"
r = n % 10
if r == 1:
return f"{n} {one}"
if 2 <= r <= 4:
return f"{n} {few}"
return f"{n} {many}"
class UptimeSimple(commands.Cog): class UptimeSimple(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.start_time = None self.start_time = discord.utils.utcnow()
@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") @commands.command(name="uptime")
async def uptime(self, ctx): async def uptime(self, ctx):
if self.start_time is None: delta = discord.utils.utcnow() - self.start_time
await ctx.send("ебать у тебя тайминги кнш") seconds = int(delta.total_seconds())
return minutes, secs = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
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 = [] parts = []
if days:
parts.append(pluralize(days, "день", "дня", "дней"))
if hours:
parts.append(pluralize(hours, "час", "часа", "часов"))
if minutes:
parts.append(pluralize(minutes, "минуту", "минуты", "минут"))
if secs or not parts:
parts.append(pluralize(secs, "секунду", "секунды", "секунд"))
if days > 0: embed = discord.Embed(
parts.append(f"{days} дня") description="бот работает уже: " + " ".join(parts),
if hours > 0: color=discord.Color.green()
parts.append(f"{hours} часа") )
if minutes > 0: await ctx.send(embed=embed)
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): async def setup(bot):
await bot.add_cog(UptimeSimple(bot)) await bot.add_cog(UptimeSimple(bot))

View File

@@ -5,6 +5,7 @@ load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN') TOKEN = os.getenv('DISCORD_TOKEN')
CHANNEL_ID = 1454107749028855971 # roles channel CHANNEL_ID = 1454107749028855971 # roles channel
roles_message_id = 1454128857102680187
FUNCHOSA_CHANNEL_ID = 1379127661095551048 FUNCHOSA_CHANNEL_ID = 1379127661095551048
REACTION_ROLES = { REACTION_ROLES = {

View File

@@ -1 +0,0 @@
{"message_id": 1454128857102680187}

39
main.py
View File

@@ -13,13 +13,25 @@ logging.basicConfig(
] ]
) )
logger = logging.getLogger(__name__)
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
intents.reactions = True intents.reactions = True
intents.members = True intents.members = True
intents.guilds = True intents.guilds = True
intents.messages = True intents.messages = True
intents.voice_states = True
COGS = [
#! cogs to load
'cogs.role_manager',
'cogs.status_rotator',
'cogs.funchosa_parser',
'cogs.uptime',
'cogs.help',
'cogs.kitty',
]
class Bot(commands.Bot): class Bot(commands.Bot):
def __init__(self): def __init__(self):
@@ -30,28 +42,25 @@ class Bot(commands.Bot):
) )
async def setup_hook(self): async def setup_hook(self):
# ! load cogs for cog in COGS:
await self.load_extension('cogs.role_manager') try:
await self.load_extension('cogs.status_rotator') await self.load_extension(cog)
await self.load_extension('cogs.funchosa_parser') logger.info("Loaded cog: %s", cog)
await self.load_extension('cogs.uptime') except Exception as e:
await self.load_extension('cogs.help') logger.error("Failed to load cog %s: %s", cog, e, exc_info=True)
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() await self.tree.sync()
async def on_ready(self): async def on_ready(self):
print(f"bot initialized succesfully with user '{self.user}'") if not hasattr(self, '_ready'):
print(f"user.id: '{self.user.id}'") self._ready = True
print('initialization (probably) complete; further is logs.') logger.info("Bot ready: %s (id: %s)", self.user, self.user.id)
print('\n*------*\n')
async def main(): async def main():
bot = Bot() bot = Bot()
await bot.start(config.TOKEN) await bot.start(config.TOKEN)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@@ -1,16 +1,15 @@
import json import json
import os import os
from config import roles_message_id
MESSAGE_ID_FILE = 'data/message_id.json' #ts for roles
def save_message_id(message_id): def save_message_id(message_id):
os.makedirs('data', exist_ok=True) os.makedirs('data', exist_ok=True)
with open(MESSAGE_ID_FILE, 'w') as f: with open(roles_message_id, 'w') as f:
json.dump({'message_id': message_id}, f) json.dump({'message_id': message_id}, f)
def load_message_id(): def load_message_id():
try: try:
with open(MESSAGE_ID_FILE, 'r') as f: with open(roles_message_id, 'r') as f:
data = json.load(f) data = json.load(f)
return data.get('message_id') return data.get('message_id')
except FileNotFoundError: except FileNotFoundError:

View File

@@ -1,211 +1,164 @@
import aiosqlite import aiosqlite
import asyncio
from datetime import datetime
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FunchosaDatabase: class FunchosaDatabase:
def __init__(self, db_path='data/funchosa.db'): def __init__(self, db_path='data/funchosa.db'):
self.db_path = db_path self.db_path = db_path
self._conn: aiosqlite.Connection | None = None
async def init_db(self): async def init_db(self):
async with aiosqlite.connect(self.db_path) as db: self._conn = await aiosqlite.connect(self.db_path)
await db.execute(''' self._conn.row_factory = aiosqlite.Row
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(''' await self._conn.executescript('''
CREATE TABLE IF NOT EXISTS attachments ( CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER, message_id BIGINT UNIQUE NOT NULL,
url TEXT UNIQUE NOT NULL, channel_id BIGINT NOT NULL,
filename TEXT, author_id BIGINT NOT NULL,
FOREIGN KEY (message_id) REFERENCES messages (id) author_name TEXT NOT NULL,
) content TEXT,
''') timestamp TIMESTAMP NOT NULL,
message_url TEXT NOT NULL,
has_attachments BOOLEAN DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
await db.execute(''' CREATE TABLE IF NOT EXISTS attachments (
CREATE TABLE IF NOT EXISTS parsing_status ( id INTEGER PRIMARY KEY AUTOINCREMENT,
id INTEGER PRIMARY KEY CHECK (id = 1), message_id INTEGER,
first_parse_done BOOLEAN DEFAULT 0, url TEXT UNIQUE NOT NULL,
last_parsed_message_id BIGINT, filename TEXT,
last_parse_time TIMESTAMP FOREIGN KEY (message_id) REFERENCES messages (id)
) );
''')
await db.execute('CREATE INDEX IF NOT EXISTS idx_message_id ON messages(message_id)') CREATE TABLE IF NOT EXISTS parsing_status (
await db.execute('CREATE INDEX IF NOT EXISTS idx_author_id ON messages(author_id)') id INTEGER PRIMARY KEY CHECK (id = 1),
await db.execute('CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp)') first_parse_done BOOLEAN DEFAULT 0,
last_parsed_message_id BIGINT,
last_parse_time TIMESTAMP
);
await db.commit() CREATE INDEX IF NOT EXISTS idx_message_id ON messages(message_id);
logger.info("[FunchosaDatabase] funchosa db initialized") CREATE INDEX IF NOT EXISTS idx_author_id ON messages(author_id);
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
''')
await self._conn.commit()
logger.info("Database initialized")
async def get_parsing_status(self): async def close(self):
async with aiosqlite.connect(self.db_path) as db: if self._conn:
cursor = await db.execute( await self._conn.close()
'SELECT first_parse_done, last_parsed_message_id FROM parsing_status WHERE id = 1'
)
result = await cursor.fetchone()
if result: def _parse_message_row(self, row) -> dict:
return { message = dict(row)
'first_parse_done': bool(result[0]), if message.get('attachment_urls_list'):
'last_parsed_message_id': result[1] urls = message['attachment_urls_list'].split(',')
} filenames = (message['attachment_filenames'] or '').split(',')
else: message['attachments'] = [
await db.execute( {'url': url, 'filename': filename}
'INSERT INTO parsing_status (id, first_parse_done, last_parsed_message_id) VALUES (1, 0, NULL)' for url, filename in zip(urls, filenames)
) ]
await db.commit() else:
return { message['attachments'] = []
'first_parse_done': False, return message
'last_parsed_message_id': None
} async def get_parsing_status(self) -> dict:
cursor = await self._conn.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]}
await self._conn.execute(
'INSERT INTO parsing_status (id, first_parse_done, last_parsed_message_id) VALUES (1, 0, NULL)'
)
await self._conn.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 def update_parsing_status(self, first_parse_done=False, last_parsed_message_id=None):
async with aiosqlite.connect(self.db_path) as db: await self._conn.execute('''
await db.execute(''' UPDATE parsing_status
UPDATE parsing_status SET first_parse_done = ?,
SET first_parse_done = ?, last_parsed_message_id = ?,
last_parsed_message_id = ?, last_parse_time = CURRENT_TIMESTAMP
last_parse_time = CURRENT_TIMESTAMP WHERE id = 1
WHERE id = 1 ''', (first_parse_done, last_parsed_message_id))
''', (first_parse_done, last_parsed_message_id)) await self._conn.commit()
await db.commit()
async def get_last_message_in_db(self): async def get_last_message_in_db(self):
async with aiosqlite.connect(self.db_path) as db: cursor = await self._conn.execute(
cursor = await db.execute( 'SELECT message_id FROM messages ORDER BY message_id DESC LIMIT 1'
'SELECT message_id FROM messages ORDER BY message_id DESC LIMIT 1' )
) result = await cursor.fetchone()
result = await cursor.fetchone() return result[0] if result else None
return result[0] if result else None
async def save_message(self, message_data): async def save_message(self, message_data: dict) -> bool:
async with aiosqlite.connect(self.db_path) as db: cursor = await self._conn.execute('''
cursor = await db.execute(''' INSERT OR IGNORE INTO messages
INSERT OR IGNORE INTO messages (message_id, channel_id, author_id, author_name, content,
(message_id, channel_id, author_id, author_name, content, timestamp, message_url, has_attachments)
timestamp, message_url, has_attachments, attachment_urls) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (
''', ( message_data['message_id'],
message_data['message_id'], message_data['channel_id'],
message_data['channel_id'], message_data['author_id'],
message_data['author_id'], message_data['author_name'],
message_data['author_name'], message_data['content'],
message_data['content'], message_data['timestamp'],
message_data['timestamp'], message_data['message_url'],
message_data['message_url'], message_data['has_attachments'],
message_data['has_attachments'], ))
message_data['attachment_urls']
))
if cursor.rowcount > 0: if cursor.rowcount > 0:
message_db_id = cursor.lastrowid message_db_id = cursor.lastrowid
for attachment in message_data.get('attachments', []):
await self._conn.execute('''
INSERT OR IGNORE INTO attachments (message_id, url, filename)
VALUES (?, ?, ?)
''', (message_db_id, attachment['url'], attachment['filename']))
await self._conn.commit()
return True
return False
if message_data['attachments']: async def message_exists(self, message_id: int) -> bool:
for attachment in message_data['attachments']: cursor = await self._conn.execute(
await db.execute(''' 'SELECT 1 FROM messages WHERE message_id = ? LIMIT 1', (message_id,)
INSERT OR IGNORE INTO attachments )
(message_id, url, filename) return await cursor.fetchone() is not None
VALUES (?, ?, ?)
''', (
message_db_id,
attachment['url'],
attachment['filename']
))
await db.commit() async def get_random_message(self) -> dict | None:
return True cursor = await self._conn.execute('''
return False 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()
return self._parse_message_row(row) if row else None
async def message_exists(self, message_id): async def get_message_by_number(self, number: int) -> dict | None:
async with aiosqlite.connect(self.db_path) as db: cursor = await self._conn.execute('''
cursor = await db.execute( SELECT m.*,
'SELECT 1 FROM messages WHERE message_id = ? LIMIT 1', GROUP_CONCAT(a.url) as attachment_urls_list,
(message_id,) GROUP_CONCAT(a.filename) as attachment_filenames
) FROM messages m
result = await cursor.fetchone() LEFT JOIN attachments a ON m.id = a.message_id
return result is not None WHERE m.id = ?
GROUP BY m.id
''', (number,))
row = await cursor.fetchone()
return self._parse_message_row(row) if row else None
async def get_random_message(self): async def get_total_count(self) -> int:
async with aiosqlite.connect(self.db_path) as db: cursor = await self._conn.execute('SELECT COUNT(*) FROM messages')
cursor = await db.execute(''' result = await cursor.fetchone()
SELECT m.*, return result[0] if result else 0
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