From d6642549c4f58b4e31abd05e85d09dcd31c74048 Mon Sep 17 00:00:00 2001 From: timotheyca Date: Fri, 15 Apr 2022 16:08:01 +0300 Subject: [PATCH] role granting --- v6d3losyash/config.py | 5 + v6d3losyash/run-bot.py | 230 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 222 insertions(+), 13 deletions(-) diff --git a/v6d3losyash/config.py b/v6d3losyash/config.py index d420e7a..09253e8 100644 --- a/v6d3losyash/config.py +++ b/v6d3losyash/config.py @@ -1,7 +1,12 @@ import os +from v6d0auth.config import root + guild = int(os.getenv('v6guild', 541241763042689025)) emoji = int(os.getenv('v6emoji', 586669134406877270)) role = int(os.getenv('v6role', 643896112977018880)) message = int(os.getenv('v6message', 825385619097518110)) channel = int(os.getenv('v6channel', 876814972968116224)) +role_channel = int(os.getenv('v6channel', 964261739980025966)) +myroot = root / 'v6d3losyash' +myroot.mkdir(exist_ok=True) diff --git a/v6d3losyash/run-bot.py b/v6d3losyash/run-bot.py index 3e58404..24372a2 100644 --- a/v6d3losyash/run-bot.py +++ b/v6d3losyash/run-bot.py @@ -1,7 +1,9 @@ import asyncio from io import StringIO +from typing import Union, Optional import discord +from ptvp35 import KVJson, Db from v6d1tokens.client import request_token from v6d2ctx.serve import serve @@ -21,6 +23,7 @@ client = discord.Client( ), ) ESCAPED = '`_*\'"\\' +nest_db = Db(config.myroot / 'nest.db', kvrequest_type=KVJson) def escape(s: str): @@ -32,30 +35,220 @@ def escape(s: str): return res.getvalue() +lock = asyncio.Lock() + + @client.event async def on_ready(): print("ready") await client.change_presence(activity=discord.Game( name='феноменально', )) + async with lock: + await state.reload() -@client.event -async def on_raw_reaction_add(payload: discord.RawReactionActionEvent): - emoji: discord.Emoji = payload.emoji - if emoji.id != config.emoji: - return - if payload.message_id != config.message: - return +class SimpleEmoji: + def __init__(self, ref: Union[str, int]): + self.ref = ref + + def __hash__(self): + return hash(self.ref) + + def __eq__(self, other): + if isinstance(other, SimpleEmoji): + return self.ref == other.ref + return NotImplemented + + @classmethod + def of(cls, emoji: Union[str, discord.Emoji, discord.PartialEmoji]) -> 'SimpleEmoji': + if isinstance(emoji, discord.Emoji): + return cls(emoji.id) + elif isinstance(emoji, str): + return cls(emoji) + elif isinstance(emoji, discord.PartialEmoji): + if emoji.id is None: + return cls(emoji.name) + else: + return cls(emoji.id) + else: + raise TypeError + + def to(self) -> Union[str, discord.Emoji]: + if isinstance(self.ref, str): + return self.ref + else: + return client.get_emoji(self.ref) + + +class RoleGrant: + def __init__(self, role: int, emoji: SimpleEmoji): + self.role = role + self.emoji = emoji + + def format(self) -> str: + guild: discord.Guild = client.get_guild(config.guild) + return f'{self.emoji.to()} {guild.get_role(self.role)}' + + +class MessageDescription: + def __init__(self, key: str, description: str, *roles: RoleGrant): + self.key = key + self._description = description + self.roles = roles + self.mapping: dict[SimpleEmoji, int] = {grant.emoji: grant.role for grant in roles} + + def description(self): + return self._description + '\n' + '\n'.join(map(RoleGrant.format, self.roles)) + + +class ChannelDescription: + def __init__(self, *mds: MessageDescription): + self.mds = mds + self.mapping: dict[str, MessageDescription] = {md.key: md for md in mds} + + +def role_channel() -> discord.TextChannel: guild: discord.Guild = client.get_guild(config.guild) - member: discord.Member = guild.get_member(payload.user_id) + channel: discord.TextChannel = guild.get_channel(config.role_channel) + return channel + + +class State: + def __init__(self, cd: ChannelDescription): + self.defmap: dict[str, int] = {} + self.defrev: dict[int, str] = {} + self.cd = cd + + async def _load(self): + self.defmap = nest_db.get('state-map', {}) + self.defrev.clear() + for md in self.cd.mds: + msg = await self._get(md.key, md.description()) + for reaction in msg.reactions: + emoji = SimpleEmoji.of(reaction.emoji) + if emoji not in md.mapping: + await reaction.clear() + for emoji in md.mapping.keys(): + await msg.add_reaction(emoji.to()) + + async def _get_raw(self, key: str, content: str) -> discord.Message: + channel = role_channel() + if key in self.defmap: + try: + msg = await channel.fetch_message(self.defmap[key]) + if msg.content != content: + await msg.edit(content=content) + return msg + except discord.NotFound: + pass + return await channel.send(content) + + async def _get(self, key: str, content: str) -> discord.Message: + msg = await self._get_raw(key, content) + self.defmap[key] = msg.id + self.defrev[msg.id] = key + return msg + + def _reset_defmap(self): + self.defmap = {key: msid for msid, key in self.defrev.items()} + + async def _clear_channel(self): + msg: discord.Message + async for msg in role_channel().history(limit=100): + if msg.author.id == client.user.id and msg.id not in self.defrev: + await msg.delete() + + async def _save(self): + self._reset_defmap() + await self._clear_channel() + await nest_db.set('state-map', self.defmap) + + async def reload(self): + await self._load() + await self._save() + + async def _role_member( + self, msid: int, member_id: int, emoji: SimpleEmoji + ) -> tuple[Optional[discord.Role], Optional[discord.Member]]: + key = self.defrev[msid] + md = self.cd.mapping[key] + role_id = md.mapping.get(emoji) + if role_id is None: + channel = role_channel() + message: discord.Message = await channel.fetch_message(msid) + await message.clear_reaction(emoji.to()) + return None, None + guild: discord.Guild = await client.fetch_guild(config.guild) + role: discord.Role = guild.get_role(role_id) + member: discord.Member = await guild.fetch_member(member_id) + return role, member + + async def role_grant(self, msid: int, member_id: int, emoji: SimpleEmoji): + role, member = await self._role_member(msid, member_id, emoji) + if role is None or member is None: + return + await member.add_roles(role, reason='emojis go yes') + await log(f'emoji {emoji.to()} +role {role} member {member}') + + async def role_revoke(self, msid: int, member_id: int, emoji: SimpleEmoji): + role, member = await self._role_member(msid, member_id, emoji) + if role is None or member is None: + return + await member.remove_roles(role, reason='emojis go no') + await log(f'emoji {emoji.to()} -role {role} member {member}') + + +state = State( + ChannelDescription( + MessageDescription( + 'games', + 'Игровые роли', + RoleGrant(541263011822698506, SimpleEmoji(541257134407417864)), + RoleGrant(542056704842661899, SimpleEmoji('🦆')), + RoleGrant(541349294586724365, SimpleEmoji('⛏')), + RoleGrant(541263005971644419, SimpleEmoji('🌈')), + RoleGrant(934467294371938355, SimpleEmoji('🎩')), + RoleGrant(933103254752088104, SimpleEmoji('🐟')), + ) + ) +) + + +async def log(msg: str): + guild: discord.Guild = client.get_guild(config.guild) + channel: discord.TextChannel = guild.get_channel(config.channel) + await channel.send(msg) + + +async def grant_citizenship(member_id: int): + guild: discord.Guild = client.get_guild(config.guild) + member: discord.Member = guild.get_member(member_id) role = guild.get_role(config.role) if role in member.roles: return await member.add_roles(role, reason='феноменально') + await log(f'{escape(str(member))} {member.id} <:Jesus:586669134406877270>') - channel: discord.TextChannel = guild.get_channel(config.channel) - await channel.send(f'{escape(str(member))} {member.id} <:Jesus:586669134406877270>') + +@client.event +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: + 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)) + + +@client.event +async def on_raw_reaction_remove(payload: discord.RawReactionActionEvent): + if payload.user_id == client.user.id: + return + emoji: discord.PartialEmoji = payload.emoji + if payload.message_id in state.defrev: + await state.role_revoke(payload.message_id, payload.user_id, SimpleEmoji.of(emoji)) @client.event @@ -73,12 +266,23 @@ async def on_member_remove(member: discord.Member): if guild.id != config.guild: return channel: discord.TextChannel = guild.get_channel(config.channel) - await channel.send(f'{escape(str(member))} {member.id} left (joined {member.joined_at})') + message = await channel.send(f'{escape(str(member))} {member.id} left (joined {member.joined_at})') + await asyncio.sleep(1) + entry: discord.AuditLogEntry + async for entry in guild.audit_logs(action=discord.AuditLogAction.kick): + if entry.target.id == member.id: + await message.reply(f'latest kick: {entry.created_at} {entry.reason}') + break + async for entry in guild.audit_logs(action=discord.AuditLogAction.ban): + if entry.target.id == member.id: + await message.reply(f'latest ban: {entry.created_at} {entry.reason}') + break async def main(): - await client.login(token) - await client.connect() + async with nest_db: + await client.login(token) + await client.connect() if __name__ == '__main__':