upload from local

upload muzovkantv2 from local storage
This commit is contained in:
2026-03-08 14:35:33 +05:00
committed by GitHub
parent 86361adabe
commit 5f00de56b9
29 changed files with 1089 additions and 0 deletions

0
cogs/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

322
cogs/funchosa_parser.py Normal file
View File

@@ -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))

27
cogs/help.py Normal file
View File

@@ -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 <id> : рандомная пикча из фунчозы либо по айдишнику в базе
funchosainfo : фунчоза.статы
kitty : рандомная пикча кошечки из [thecatapi](https://thecatapi.com/)
```
префикс: `!`
в лс отпишите по предложениям че в бота докинуть
"""
await ctx.send(help_text)
async def setup(bot):
await bot.add_cog(HelpCog(bot))

87
cogs/kitty.py Normal file
View File

@@ -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))

0
cogs/quote.py Normal file
View File

147
cogs/role_manager.py Normal file
View File

@@ -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))

65
cogs/status_rotator.py Normal file
View File

@@ -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))

46
cogs/uptime.py Normal file
View File

@@ -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))

15
config.py Normal file
View File

@@ -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,
}

BIN
data/funchosa.db Normal file

Binary file not shown.

1
data/message_id.json Normal file
View File

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

BIN
data/muter.m4a Normal file

Binary file not shown.

88
data/statuses.json Normal file
View File

@@ -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"
]
}

57
main.py Normal file
View File

@@ -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())

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
python-dotenv>=1.0.0
discord.py>=2.3.0
aiofiles
aiosqlite

0
utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

19
utils/data_manager.py Normal file
View File

@@ -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

211
utils/database.py Normal file
View File

@@ -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