diff --git a/constitution b/constitution index e09f773..27c1811 160000 --- a/constitution +++ b/constitution @@ -1 +1 @@ -Subproject commit e09f773881751bde0b1daba4d626d681026c9c55 +Subproject commit 27c18118c58abecf4a91e4bcab41099a54afeb5b diff --git a/requirements.txt b/requirements.txt index 3183717..bcd22bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -ptvp35 @ git+https://gitea.parrrate.ru/PTV/ptvp35.git@87ba808c2af1be87f4fbb9d9b3b97ba748cb9fae -v6d0auth @ git+https://gitea.parrrate.ru/PTV/v6d0auth.git@324236f435c92756aefe22877a97a906c462ef2c -v6d1tokens @ git+https://gitea.parrrate.ru/PTV/v6d1tokens.git@96567a0cb0c3cb60f20647518df5370df6dc6664 -v6d2ctx @ git+https://gitea.parrrate.ru/PTV/v6d2ctx.git@4a821aa168a83924934b2ab833d283226eb307bb -rainbowadn @ git+https://gitea.parrrate.ru/PTV/rainbowadn.git@e9fba7b064902ceedee0dd5578cb47030665a6aa +ptvp35 @ git+https://gitea.parrrate.ru/PTV/ptvp35.git@e760fca39e2070b9959aeb95b53e59e895f1ad57 +v6d0auth @ git+https://gitea.parrrate.ru/PTV/v6d0auth.git@c718d4d1422945a756213d22d9e26aa24babe0f6 +v6d1tokens @ git+https://gitea.parrrate.ru/PTV/v6d1tokens.git@9ada50f111bd6e9a49c9c6683fa7504fee030056 +v6d2ctx @ git+https://gitea.parrrate.ru/PTV/v6d2ctx.git@18001ff3403646db46f36175a824e571c5734fd6 +rainbowadn @ git+https://gitea.parrrate.ru/PTV/rainbowadn.git@fc1d11f4b53ac4653ffac1bbcad130855e1b7f10 diff --git a/v6d3losyash/config.py b/v6d3losyash/config.py index 09253e8..3a79f30 100644 --- a/v6d3losyash/config.py +++ b/v6d3losyash/config.py @@ -2,6 +2,8 @@ import os from v6d0auth.config import root +__all__ = ('guild', 'emoji', 'role', 'message', 'channel', 'role_channel', 'myroot') + guild = int(os.getenv('v6guild', 541241763042689025)) emoji = int(os.getenv('v6emoji', 586669134406877270)) role = int(os.getenv('v6role', 643896112977018880)) diff --git a/v6d3losyash/run-bot.py b/v6d3losyash/run-bot.py index 9e81d18..2a911b3 100644 --- a/v6d3losyash/run-bot.py +++ b/v6d3losyash/run-bot.py @@ -1,14 +1,15 @@ import asyncio +import json import re from io import StringIO from pathlib import Path -from typing import Optional, Union +from typing import AsyncIterable, Iterable, Optional, Union import discord -from ptvp35 import Db, KVJson -from v6d1tokens.client import request_token -from v6d2ctx.serve import serve +from ptvp35 import * +from v6d1tokens.client import * +from v6d2ctx.serve import * from v6d3losyash import config loop = asyncio.new_event_loop() @@ -27,6 +28,7 @@ client = discord.Client( ) ESCAPED = '`_*\'"\\' nest_db = Db(config.myroot / 'nest.db', kvfactory=KVJson()) +spam_db = Db(config.myroot / 'spam.db', kvfactory=KVJson()) def escape(s: str): @@ -39,11 +41,14 @@ def escape(s: str): class Spam: - def __init__(self) -> None: + def __init__(self, mode: str, channel: int) -> None: self.spammed = False self.root = Path(__file__).parent / '../constitution' + self.path = self.root / f'{mode}.md' + self.channel = channel + self.lock = asyncio.Lock() - def format_segment(self, segment: str) -> str: + def _format_segment(self, segment: str) -> str: segment = re.sub( r'\[[\d.xX]*?\]', lambda m: f'\u001b[0;1;36m{m[0]}\u001b[0m', @@ -69,22 +74,85 @@ class Spam: lambda m: f'\u001b[0;{m[1]}m{m[2]}\u001b[0m', segment, ) - return segment.strip() + return segment - def ru_segments(self) -> list[str]: - return list(map(self.format_segment, (self.root / 'ru.md').read_text().split('=' * 80))) + def format_segment(self, segment: str) -> tuple[str, list[discord.Embed], dict]: + segment, *sflagsl = segment.split('+flags') + flags = {} + for sflags in sflagsl: + flags.update(json.loads(sflags)) + segment, *esegments = segment.split('+embed') + segment = self._format_segment(segment) + embeds = [] + for esegment in esegments: + ejson: dict = json.loads(esegment) + embed = discord.Embed( + colour=ejson.get('colour'), + title=ejson.get('title'), + type=ejson.get('type', 'rich'), + url=ejson.get('url'), + description=ejson.get('description'), + ) + embeds.append(embed) + fjson: dict + for fjson in ejson.get('fields', []): + embed.add_field(name=fjson['name'], value=fjson['value'], inline=fjson.get('inline', False)) + return segment.strip(), embeds, flags - async def spam(self) -> None: + def _ru_segments(self) -> Iterable[tuple[str, list[discord.Embed], dict]]: + return map(self.format_segment, self.path.read_text().split('=' * 80)) + + async def _ru_segments_checked(self) -> AsyncIterable[tuple[str, list[discord.Embed], dict]]: + for i, (segment, embeds, flags) in enumerate(await asyncio.to_thread(self._ru_segments)): + match flags: + case {'fix': int() as fix}: + assert i == fix, (i, fix) + yield segment, embeds, flags + + async def ru_segments(self) -> Iterable[tuple[str, list[discord.Embed], dict]]: + return [t async for t in self._ru_segments_checked()] + + async def _spam(self) -> None: if self.spammed: return try: print('spamming') guild = await client.fetch_guild(config.guild) - channel = await guild.fetch_channel(1056432869834240080) + for role in await guild.fetch_roles(): + print(role.colour, role) + channel = await guild.fetch_channel(self.channel) assert isinstance(channel, discord.abc.Messageable) - segments = await asyncio.to_thread(self.ru_segments) - for segment in segments: - await channel.send(segment.strip()) + + async def fetch(msid: int) -> discord.Message | None: + try: + return await channel.fetch_message(msid) + except discord.NotFound: + return None + broken_order = False + for i, (segment, embeds, flags) in enumerate(await self.ru_segments()): + dbkey = (channel.id, i) + message = None + messageid: int | None + if ( + (messageid := spam_db.get(dbkey, None)) is None + or + (message := await fetch(messageid)) is None + or broken_order + ): + if broken_order and message is not None: + await message.delete() + message = await channel.send(content=segment.strip(), embeds=embeds) + await spam_db.set(dbkey, message.id) + broken_order = True + else: + await message.edit(content=segment.strip(), embeds=embeds) + emojiid: int + for emojiid in flags.get('emojis', []): + emoji = await guild.fetch_emoji(emojiid) + await message.add_reaction(emoji) + if (custom_key := flags.get('key', None)) is not None: + await spam_db.set(custom_key, message.id) + except: from traceback import print_exc print_exc() @@ -92,9 +160,14 @@ class Spam: print('spammed') self.spammed = True + async def spam(self) -> None: + async with self.lock: + await self._spam() + lock = asyncio.Lock() -spam = Spam() +spam_ru = Spam('ru', 1056432869834240080) +spam_en = Spam('en', 1057006937360842942) @client.event @@ -103,10 +176,12 @@ async def on_ready(): await client.change_presence(activity=discord.Game( name='феноменально', )) - task = asyncio.create_task(spam.spam()) + task_ru = asyncio.create_task(spam_ru.spam()) + task_en = asyncio.create_task(spam_en.spam()) async with lock: await state.reload() - await task + await task_ru + await task_en class SimpleEmoji: @@ -321,7 +396,11 @@ async def on_raw_reaction_add(payload: discord.RawReactionActionEvent): if payload.user_id == client.user.id: return emoji: discord.PartialEmoji = payload.emoji - if emoji.id == config.emoji and payload.message_id == config.message: + if emoji.id == config.emoji and payload.message_id in [ + config.message, + spam_db.get('jesus', 'not an id'), + spam_db.get('jensus', 'not an id'), + ]: await grant_citizenship(payload.user_id) if payload.message_id in state.defrev: await state.role_grant(payload.message_id, payload.user_id, SimpleEmoji.of(emoji)) @@ -372,7 +451,7 @@ async def on_member_remove(member: discord.Member): async def main(): - async with nest_db: + async with nest_db, spam_db: await client.login(token) await client.connect()