commit fed84a8c0d1bdbde6457b6fa6ac59f6bd7d86123 Author: gsd Date: Fri Mar 3 14:28:32 2023 +0300 first init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3eb60e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +RUN python -m pip install git+https://github.com/Rapptz/discord.py aiohttp +ENV PYTHONUNBUFFERED 1 +WORKDIR /app +COPY ./ ./ +ENTRYPOINT ["python", "bot.py"] diff --git a/admin_ext/kick.py b/admin_ext/kick.py new file mode 100644 index 0000000..9beea94 --- /dev/null +++ b/admin_ext/kick.py @@ -0,0 +1,14 @@ +import discord + +class Extension: + def __init__(self, core): + @core.tree.command(name = "kick", description = "Кикнуть игрока") + @discord.app_commands.describe(profile=core.ANY_INPUT, reason="причина") + async def kick_player( + interaction: discord.Interaction, + profile: str, + reason: str = "" + ): + steam64 = await core.GetSteam64OfDiscord(interaction.user) + player = await core.GetPlayer(profile, steam64, False) + return await interaction.response.send_message(f'{await player.kick(reason)}', ephemeral=False) \ No newline at end of file diff --git a/admin_ext/rcon.py b/admin_ext/rcon.py new file mode 100644 index 0000000..f042725 --- /dev/null +++ b/admin_ext/rcon.py @@ -0,0 +1,19 @@ +import discord +import aiohttp + +class Extension: + def __init__(self, core): + @core.tree.command(name = "rcon", description = "Вызвать команду на сервере") + @discord.app_commands.describe(server="Нужным сервер в формате srv1", command="Команда исполнения") + async def rcon_command( + interaction: discord.Interaction, + server: str, + command: str + ): + steam64 = await core.GetSteam64OfDiscord(interaction.user) + if not server in core.stats.get("servers", {}).keys(): + return await interaction.response.send_message(f"Сервер с таким индификатором не существует, введи существующий из предложенных:\n{' '.join(core.stats.get('servers', {}).keys())}", ephemeral=False) + + async with aiohttp.ClientSession(cookies={"secretkey":core.secret_key, "steam64":steam64}) as session: + async with session.post(f"{core.backend_url}/api/admin/rcon?srv={server}&command={command}", ssl=False) as response: + return await interaction.response.send_message(f'{await response.text()}', ephemeral=False) \ No newline at end of file diff --git a/admin_ext/use.py b/admin_ext/use.py new file mode 100644 index 0000000..9cab75f --- /dev/null +++ b/admin_ext/use.py @@ -0,0 +1,15 @@ +import discord + +class Extension: + def __init__(self, core): + @core.tree.command(name = "use", description = "Использовать команду на игрока") + @discord.app_commands.describe(command="Команда исполнения", profile=core.ANY_INPUT, args="Аргумент если нужен") + async def rcon_use_command( + interaction: discord.Interaction, + command: str, + profile: str, + args: str = "" + ): + steam64 = await core.GetSteam64OfDiscord(interaction.user) + player = await core.GetPlayer(profile, steam64) + return await interaction.response.send_message(f'{await player.rcon(command, args)}', ephemeral=False) \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..1183bd4 --- /dev/null +++ b/bot.py @@ -0,0 +1,88 @@ +import ssl +import discord +from discord import app_commands +import os, sys +import aiohttp +from datetime import datetime +from discord.ext import tasks +from player import * + +#Скрыть сообщение если надо ephemeral=True + + +class NeedDiscordAuthOfSteam(Exception): + pass + + +class DiscordClient(discord.Client): + ANY_INPUT = "ссылка на стим | имя игрока | стимид" + discord2steam_cache = {} + backend_url = "" + secret_key = "" + stats = {} + show_stats_prev = 0 + + def __init__(self, backend_url, secret_key): + self.backend_url = backend_url + self.secret_key = secret_key + ################################################### + super().__init__(intents=discord.Intents.default()) + self.tree = app_commands.CommandTree(self) + self.load_extensions(['user_ext', 'admin_ext', 'other_ext']) + + def load_extensions(self, extensions_path): + if type(extensions_path) == str: + extensions_path = [extensions_path] + for path in extensions_path: + print(f"Load extensions from: {path}") + sys.path.insert(0, path) + for extension in os.listdir(path): + extension, ext = os.path.splitext(extension) + if ext != ".py": + continue + print(f"Loading: {extension}") + __import__(extension).Extension(self) + sys.path.pop(0) + + async def setup_hook(self): + self.stats = await self.GetStats() + print("sync tree") + await self.tree.sync(guild=discord.Object(320241437620830209)) + await self.tree.sync() + + def setup_events(self): + @self.event + async def on_ready(): + print(f'Logged in as {self.user} (ID: {self.user.id})') + + async def GetSteam64OfDiscord(self, user, no_cache = False): + if user.id in self.discord2steam_cache and not no_cache: + return self.discord2steam_cache[user.id] + + async with aiohttp.ClientSession(cookies={"secretkey":self.secret_key}) as session: + async with session.get(f"{self.backend_url}/api/discord?discord_id={user.id}", ssl=False) as response: + steamid_response = await response.json() + if steamid_response != None: + self.discord2steam_cache[user.id] = steamid_response["steam64"] + else: + raise NeedDiscordAuthOfSteam + + return self.discord2steam_cache[user.id] + + async def GetPlayer(self, profile, requester_steam64, load_profile = True): + player = Player(profile, requester_steam64, self.stats) + await player.GetSteamID() + if load_profile: + await player.LoadProfile() + return player + + async def GetStats(self): + async with aiohttp.ClientSession() as session: + async with session.get(f"{os.getenv('BACKEND_URL')}/api/stats", ssl=False) as response: + return await response.json() + +if __name__ == "__main__": + DiscordClient( + os.getenv("BACKEND_URL"), + os.getenv("BACKEND_SECRETKEY") + ).run(os.getenv("DISCORD_TOKEN")) \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..325c218 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + facti13bot_discord_v2: + build: . + container_name: facti13bot_discord_v2 + env_file: + - .env + extra_hosts: + - "tf2.pblr-nyk.pro:192.168.3.3" + hostname: 'discord' + restart: unless-stopped \ No newline at end of file diff --git a/git_build/Dockerfile b/git_build/Dockerfile new file mode 100644 index 0000000..d52b384 --- /dev/null +++ b/git_build/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +RUN python -m pip install git+https://github.com/Rapptz/discord.py aiohttp +RUN cd /tmp && git clone https://git.pblr-nyk.pro/gsd/Facti13.Bot.Discord.V2 && cd Facti13.Bot.Discord.V2 && cp ./ /app +ENV PYTHONUNBUFFERED 1 +WORKDIR /app +ENTRYPOINT ["python", "bot.py"] \ No newline at end of file diff --git a/other_ext/stats_loader.py b/other_ext/stats_loader.py new file mode 100644 index 0000000..7589873 --- /dev/null +++ b/other_ext/stats_loader.py @@ -0,0 +1,6 @@ +from discord.ext import tasks +class Extension: + def __init__(self, core): + @tasks.loop(seconds=60.0) + async def stats_loader(core): + core.stats = await core.GetStats() \ No newline at end of file diff --git a/other_ext/stats_presence.py b/other_ext/stats_presence.py new file mode 100644 index 0000000..fafc4da --- /dev/null +++ b/other_ext/stats_presence.py @@ -0,0 +1,19 @@ +from discord.ext import tasks +import discord +class Extension: + def __init__(self, core): + show_stats_prev = 0 + @tasks.loop(seconds=3) + async def show_stats(core): + if not core.stats['servers'].items(): + print("Stats not be loaded") + return + try: + server = core.stats['servers'].items()[core.show_stats_prev] + except: + show_stats_prev = 0 + server = core.stats['servers'].items()[core.show_stats_prev] + + addr = server['address'].split(":") + act = discord.Streaming(name = f"{server['name']} - {server['player_count']}", url=f"https://{addr[0]}/connect/{addr[1]}") + await core.change_presence(activity=act) \ No newline at end of file diff --git a/player.py b/player.py new file mode 100644 index 0000000..8a11997 --- /dev/null +++ b/player.py @@ -0,0 +1,151 @@ +import aiohttp, os +from datetime import datetime + +class CannotCastToSteamID(Exception): + pass + +class Player: + original_request = "" + requester_steam64 = "" + current = {} + steamid = {} + play_on = {} + def __init__(self, profile, requester_steam64, stats): + self.requester_steam64 = requester_steam64 + self.original_request = profile + self.stats = stats + #потом надо будет сделать что профиль принимает все, а не только стим ид + #api/profile/steam, ток там буква Z поэтому стоит поменять на секрет кей + pass + + async def GetSteamID(self): + self.steamid = await self.GetSteamIDOfProfile(self.original_request) + + async def LoadProfile(self): + self.current = await self.GetProfile(self.steamid.get("steam64")) + + def __str__(self): + if not self.current: + return "profile not load from backend, use GetProfile" + + message = self.current["steamids"]["community_url"] + "\n" + if "play_on" in self.current and self.current["play_on"]: + message += f"Сейчас играет на {self.stats['servers'][self.current['play_on']['server_id']]['name']}\n" + message += "\n" + #тут должна быть последняя причина выхода с сервера, но она не релизована в бекенде + message += "Последняя игра на серверах:\n" + if "lastplay" in self.current and self.current["lastplay"]: + for maps in self.current["lastplay"].values(): + for map_name, last_play in maps.items(): + message += f"{workshopmap2bsp(map_name)} - {utime2human(last_play)}\n" + else: + message += "Не играл у нас\n" + message += "\n" + #Далее игровое время бро + if "gametime" in self.current and self.current["gametime"]: + message += "Статистика по картам:\n" + for maps in self.current["gametime"].values(): + for map_name, play_time in maps.items(): + message += f"{workshopmap2bsp(map_name)} - {human_TIME(play_time)}\n" + message += "\n" + #Далее идут проверка прав + if "permition" in self.current and self.current["permition"]: + message += f"Права: {self.current['permition']['status']} назначены {utime2human(self.current['permition']['u_timestamp']) if self.current['permition']['u_timestamp'] != 0 else 'с момента создания'}\n" + if self.current['permition'].get('amount', 0) and self.current['permition'].get("u_timestamp", 0): + message += f"Кончаются: {utime2human(self.current['permition']['u_timestamp'] + self.current['permition']['amount'])}\n" + message += "\n" + #Далее проверка бана + if "ban" in self.current and self.current["ban"]: + message += "ИМЕЕТСЯ БАН\n" + message += f"Ник: {self.current['ban']['player_name']}\n" + message += f"Причина: {self.current['ban']['ban_reason']}\n" + message += f"Время: {utime2human(self.current['ban']['ban_utime'])}\n" + message += f"Кто забанил: {self.current['ban']['banned_by']} | <@{self.current['ban']['admin_info']['discord_id']}>\n" + if self.current['ban']['active'] == True: + if self.current['ban']['ban_length'] == 0: + message += "Данный бан навсегда!\n" + else: + message += f"Дата разбана: {utime2human(self.current['ban']['ban_utime'] + self.current['ban']['ban_length_seconds'])}\n" + else: + message += f"Кто разбанил: {'бан снялся со временем' if self.current['ban']['unbanned_by_id'] == 'STEAM_0:0:0' else self.current['ban']['unbanned_by_id']}\n" + #Не реализованное получение числа скок был в бане + return message + + async def GetSteamIDOfProfile(self, any:str): + async with aiohttp.ClientSession(cookies={"secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: + async with session.get(f"{os.getenv('BACKEND_URL')}/api/profile/steam?any={any}", ssl=False) as response: + response = await response.json() + if response == None: + raise CannotCastToSteamID + return response + + async def GetProfile(self, steam64): + async with aiohttp.ClientSession(cookies={"secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: + async with session.get(f"{os.getenv('BACKEND_URL')}/api/profile?steam64={steam64}", ssl=False) as response: + return await response.json() + + ############### + #admin commands + ############### + async def kick(self, reason): + async with aiohttp.ClientSession(cookies={ + "secretkey":os.getenv("BACKEND_SECRETKEY"), + "steam64": self.requester_steam64}) as session: + async with session.post(f"{os.getenv('BACKEND_URL')}/api/admin/kick?steam64={self.steamid.get('steam64')}", ssl=False) as response: + result = await response.text() + if response.status == 200: + return "Кикнут с серверов" + if response.status == 404: + return "Игрок не найден на серверах" + if response.status == 403: + return "Это не для тебя и не для таких как ты сделано..." + return "помогите я обосрался" + + async def rcon(self, command, args): + if not self.current: + return "добродей дурачек забыл прогрузить профиль" + + if self.current.get("play_on", {}): + server = self.current['play_on']['server_id'] + player_id = self.current['play_on']['player_id'] + final_command = f"{command} #{player_id} {args}" + async with aiohttp.ClientSession(cookies={ + "secretkey":os.getenv("BACKEND_SECRETKEY"), + "steam64": self.requester_steam64}) as session: + async with session.post(f"{os.getenv('BACKEND_URL')}/api/admin/rcon?srv={server}&command={final_command}", ssl=False) as response: + return await response.text() + else: + return "Игрок не играет на серверах" + + + ############### + #user command + ############### + async def report(self, reason): + async with aiohttp.ClientSession(cookies={ + "secretkey":os.getenv("BACKEND_SECRETKEY"), + "steam64": self.requester_steam64}) as session: + async with session.post(f"{os.getenv('BACKEND_URL')}/api/profile/current/report?steam64={self.steamid.get('steam64')}&text={reason}", ssl=False) as response: + result = int(await response.text()) + if result == 0: + return "Игрок с таким именем не играет на серверах в данный момент" + else: + return f"Падажди, следующий репорт можно отправить только после: {utime2human(result)}" + + +def workshopmap2bsp(map_name): + return map_name.split('/')[-1:][0].split('.ugc')[0] + +def utime2human(utime): + return datetime.fromtimestamp(utime).strftime('%H:%M:%S %d.%m.%Y') + +def human_TIME(seconds): + m, s = divmod(int(seconds), 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + if not d: + return "%d:%02d:%02d" % (h, m, s) + elif d < 2: + return "%d день %d:%02d:%02d" % (d ,h, m, s) + else: + return "%d дней %d:%02d:%02d" % (d ,h, m, s) \ No newline at end of file diff --git a/user_ext/nyk.py b/user_ext/nyk.py new file mode 100644 index 0000000..62bd4a4 --- /dev/null +++ b/user_ext/nyk.py @@ -0,0 +1,9 @@ +import discord + +class Extension: + def __init__(self, core): + @core.tree.command(name = "nyk", description = "Сренькнуть в ответ") + async def nyk( + interaction: discord.Interaction + ): + return await interaction.response.send_message(f'среньк', ephemeral=False) \ No newline at end of file diff --git a/user_ext/profile.py b/user_ext/profile.py new file mode 100644 index 0000000..037456f --- /dev/null +++ b/user_ext/profile.py @@ -0,0 +1,13 @@ +import discord + +class Extension: + def __init__(self, core): + @core.tree.command(name = "profile", description = "Проверить профиль") + @discord.app_commands.describe(profile=core.ANY_INPUT) + async def check_profile( + interaction: discord.Interaction, + profile: str = "" + ): + steam64 = await core.GetSteam64OfDiscord(interaction.user) + player = await core.GetPlayer(profile, steam64) if profile else await core.GetPlayer(steam64, steam64) + return await interaction.response.send_message(f'{player}', ephemeral=False) \ No newline at end of file diff --git a/user_ext/report.py b/user_ext/report.py new file mode 100644 index 0000000..ae7aa2a --- /dev/null +++ b/user_ext/report.py @@ -0,0 +1,14 @@ +import discord + +class Extension: + def __init__(self, core): + @core.tree.command(name = "report", description = "Пожаловать на игрока с сервера") + @discord.app_commands.describe(profile=core.ANY_INPUT) + async def report_player( + interaction: discord.Interaction, + profile: str, + reason: str + ): + steam64 = await core.GetSteam64OfDiscord(interaction.user) + player = await core.GetPlayer(profile, steam64, False) + return await interaction.response.send_message(f'{await player.report(reason)}', ephemeral=False) \ No newline at end of file diff --git a/user_ext/servers.py b/user_ext/servers.py new file mode 100644 index 0000000..32f857a --- /dev/null +++ b/user_ext/servers.py @@ -0,0 +1,53 @@ +import discord + +class Extension: + def __init__(self, core): + @core.tree.command(name = "servers", description = "Показать статистику с серверов") + @discord.app_commands.describe(server = "ид сервера в формате srv1") + async def get_servers( + interaction: discord.Interaction, + server: str = "" + ): + steam64 = await core.GetSteam64OfDiscord(interaction.user) + embed = discord.Embed() + + if server: + if not server in core.stats.get("servers", {}).keys(): + return await interaction.response.send_message(f"Сервер с таким индификатором не существует, введи существующий из предложенных:\n{' '.join(core.stats.get('servers', {}).keys())}", ephemeral=False) + embed.add_field(name=core.stats['servers'][server]['name'], value=str_server(core.stats['servers'][server]), inline=False) + else: + servers = [server for server in core.stats["servers"].values() if server['status'] == True and server['player_count'] > 0] + if servers: + embed.add_field(name = "Где сейчас играют", value = f'{core.stats["statistic"]["player_now"]} карликов', inline=True) + for server in servers: + embed.add_field(name = server["name"], value=str_server(server), inline=False) + + servers = [server for server in core.stats["servers"].values() if server['status'] == True and server['player_count'] == 0] + if servers: + embed.add_field(name = "Пустующие сервера", value = f"{len(servers)} штук", inline=True) + for server in servers: + embed.add_field(name = server["name"], value=str_server(server), inline=False) + + #не забудь потом сделать лямбды дурачек + servers = [server for server in core.stats["servers"].values() if server['status'] == False] + if servers: + embed.add_field(name = "Неработающие сервера", value = "пук", inline=True) + for server in servers: + embed.add_field(name = server["name"], value=str_server(server), inline=False) + + return await interaction.response.send_message(embed=embed, ephemeral=False) + + +def str_server(data): + message = "" + addr = data['address'].split(":") + message += f"https://{addr[0]}/connect/{addr[1]}\n" + if data['status'] == False: + return message + "Сервер не отвечает" + + message += f"Карта: {data['map']}\n" + message += f"Игроков: {data['player_count']}/{data['max_players']}\n" + message += "\n" + for player in data['players']: + message += f"{player['duration']:7} | {player['score']:3} | {player['name']}\n" + return message \ No newline at end of file