diff --git a/main.py b/main.py index 9868d78..cc560ae 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,12 @@ import discord -import sys, os, traceback, aiohttp -import update +import sys +import os +import traceback +import aiohttp import function as func from discord.ext import commands +from web import IPCServer from dotenv import load_dotenv from datetime import datetime from voicelink import VoicelinkException @@ -14,26 +17,30 @@ func.settings_setup() class Translator(discord.app_commands.Translator): async def load(self): print("Loaded Translator") - + async def unload(self): print("Unload Translator") - + async def translate(self, string: discord.app_commands.locale_str, locale: discord.Locale, context: discord.app_commands.TranslationContext): if str(locale) in func.local_langs: return func.local_langs[str(locale)].get(string.message, None) return None class Vocard(commands.Bot): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.ipc = IPCServer(self, sercet_key="Vocard") async def on_message(self, message: discord.Message, /) -> None: if message.author.bot or not message.guild: return False - + if self.user.mentioned_in(message) and not message.mention_everyone: await message.channel.send(f"My prefix is `{await self.command_prefix(self, message)}`") await self.process_commands(message) - + async def setup_hook(self): func.langs_setup() for module in os.listdir(func.root_dir + '/cogs'): @@ -44,6 +51,8 @@ class Vocard(commands.Bot): except Exception as e: print(traceback.format_exc()) + await self.ipc.start() + await bot.tree.set_translator(Translator()) await bot.tree.sync() @@ -60,7 +69,7 @@ class Vocard(commands.Bot): error = getattr(exception, 'original', exception) if isinstance(error, (commands.CommandNotFound, aiohttp.client_exceptions.ClientOSError)): return - + elif isinstance(error, (commands.CommandOnCooldown, commands.MissingPermissions, commands.RangeError, commands.BadArgument)): pass @@ -68,25 +77,25 @@ class Vocard(commands.Bot): command = f" Correct Usage: {ctx.prefix}" + (f"{ctx.command.parent.qualified_name} " if ctx.command.parent else "") + f"{ctx.command.name} {ctx.command.signature}" position = command.find(f"<{ctx.current_parameter.name}>") + 1 error = f"```css\n[You are missing argument!]\n{command}\n" + " " * position + "^" * len(ctx.current_parameter.name) + "```" - + elif not issubclass(error.__class__, VoicelinkException): - error = function.get_lang(ctx.guild.id, "unknownException") + function.invite_link - if (guildId := ctx.guild.id) not in function.error_log: - function.error_log[guildId] = {} - function.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc()) - + error = func.get_lang(ctx.guild.id, "unknownException") + func.invite_link + if (guildId := ctx.guild.id) not in func.error_log: + func.error_log[guildId] = {} + func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc()) + try: return await ctx.reply(error, ephemeral=True) except: pass class CommandCheck(discord.app_commands.CommandTree): - + async def interaction_check(self, interaction: discord.Interaction, /) -> bool: if not interaction.guild: await interaction.response.send_message("This command can only be used in guilds!") return False - + return await super().interaction_check(interaction) async def get_prefix(bot, message: discord.Message): @@ -118,10 +127,10 @@ async def app_command_error(interaction: discord.Interaction, error): elif isinstance(error, (discord.app_commands.CommandOnCooldown, discord.app_commands.errors.MissingPermissions)): pass elif not issubclass(error.__class__, VoicelinkException): - error = function.get_lang(interaction.guild_id, "unknownException") + function.invite_link - if (guildId := interaction.guild_id) not in function.error_log: - function.error_log[guildId] = {} - function.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc()) + error = func.get_lang(interaction.guild_id, "unknownException") + func.invite_link + if (guildId := interaction.guild_id) not in func.error_log: + func.error_log[guildId] = {} + func.error_log[guildId][round(datetime.timestamp(datetime.now()))] = str(traceback.format_exc()) try: if interaction.response.is_done(): return await interaction.followup.send(error, ephemeral=True) @@ -131,5 +140,4 @@ async def app_command_error(interaction: discord.Interaction, error): pass if __name__ == "__main__": - update.checkVersion(withMsg=True) bot.run(os.getenv("TOKEN"), log_handler=None) \ No newline at end of file diff --git a/update.py b/update.py deleted file mode 100644 index 997aad2..0000000 --- a/update.py +++ /dev/null @@ -1,59 +0,0 @@ -import requests, zipfile, os, sys, shutil, traceback -from io import BytesIO - -root_dir = os.path.dirname(os.path.abspath(__file__)) -install_pack_dir = os.path.join(root_dir, "Vocard.zip") -__version__ = "v2.5.7" - -def checkVersion(withMsg = False): - resp = requests.get("https://api.github.com/repos/ChocoMeow/Vocard/releases/latest") - version = resp.json().get("name", __version__) - if withMsg: - if version == __version__: - print(f"Your bot is up-to-date! - {version}") - else: - print(f"Your bot is not up-to-date! This latest version is {version} and you are currently running version {__version__}\n. Run `python update.py --start` to update your bot!") - return version - -def downloadFile(version:str = None): - if not version: - version = checkVersion() - print("Downloading Vocard version: " + version) - response = requests.get("https://github.com/ChocoMeow/Vocard/archive/" + version + ".zip") - print("Download Completed") - unZip(response, version) - -def unZip(response, version: str): - print("Installing ...") - zfile = zipfile.ZipFile(BytesIO(response.content)) - zfile.extractall(root_dir) - - version = version.replace("v", "") - source_dir = os.path.join(root_dir, f"Vocard-{version}") - if os.path.exists(source_dir): - for filename in os.listdir(root_dir): - if filename in ["settings.json", ".env", f"Vocard-{version}"]: - continue - filename = os.path.join(root_dir, filename) - if os.path.isdir(filename): - shutil.rmtree(filename) - else: - os.remove(filename) - for filename in os.listdir(source_dir): - shutil.move(os.path.join(source_dir, filename), os.path.join(root_dir, filename)) - os.rmdir(source_dir) - -def start(): - try: - downloadFile() - if os.path.exists(install_pack_dir): - os.remove(install_pack_dir) - print("Update Successfully! Run `python main.py` to start your bot") - except Exception as e: - print(traceback.format_exc()) - -if "--start" in sys.argv: - start() - -if "--check" in sys.argv: - checkVersion(withMsg = True) \ No newline at end of file diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..4bafd1f --- /dev/null +++ b/web/__init__.py @@ -0,0 +1,2 @@ +from .ipc import * +from .objects import * \ No newline at end of file diff --git a/web/ipc/__init__.py b/web/ipc/__init__.py new file mode 100644 index 0000000..8d6d350 --- /dev/null +++ b/web/ipc/__init__.py @@ -0,0 +1,2 @@ +from .server import IPCServer +from .client import IPCClient \ No newline at end of file diff --git a/web/ipc/client.py b/web/ipc/client.py new file mode 100644 index 0000000..96bcc14 --- /dev/null +++ b/web/ipc/client.py @@ -0,0 +1,41 @@ +import websockets, json +from uuid import uuid4 + +class IPCClient: + def __init__(self, + host = "127.0.0.1", + port = 8000, + secret_key = None, + callback = None): + + self.host = host + self.port = port + self.websocket = None + self.secret_key = secret_key + self.callback = callback + self.id = uuid4() + + async def connect(self): + self.websocket = await websockets.connect(f"ws://{self.host}:{self.port}", extra_headers={"Client-Id": str(self.id)}) + await self.start_receiver() + + async def send(self, message, user): + if not self.websocket: + await self.connect() + data = json.loads(message) + + payload = { + "sercet": self.secret_key, + "data": data | {"user_id": user.id, "guild_id": user.guild_id} + } + + await self.websocket.send(json.dumps(payload)) + + async def receive(self): + async for message in self.websocket: + if self.callback: + data = json.loads(message) + self.callback(data) + + async def start_receiver(self): + await self.receive() \ No newline at end of file diff --git a/web/ipc/methods.py b/web/ipc/methods.py new file mode 100644 index 0000000..c8c7026 --- /dev/null +++ b/web/ipc/methods.py @@ -0,0 +1,183 @@ +import json +import voicelink + +from discord import Member +from discord.ext import commands + +def missingPermission(user_id:int): + payload = {"op": "missingPermission", "msg": "Only the DJ or admins may use this funciton!"} + payload["user_id"] = user_id + return payload + +async def initPlayer(player, member: Member, data: dict): + return { + "op": "initPlayer", + "guild_id": player.guild.id, + "user_id": data.get("user_id"), + "users": [{ + "user_id": member.id, + "avatar_url": member.avatar.url, + "name": member.name + } for member in player.channel.members ], + "tracks": [ track.toDict() for track in player.queue._queue ], + "current_queue_position": player.queue._position if player.is_playing else player.queue._position + 1, + "current_position": 0 or player.position if player.is_playing else 0, + "is_playing": player.is_playing, + "is_paused": player.is_paused + } + +async def skipTo(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + index = data.get("index", 1) + if index > 1: + player.queue.skipto(index) + + if player.queue._repeat == 1: + player.queue.set_repeat("off") + await player.stop() + +async def backTo(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + index = data.get("index", 1) + if not player.is_playing: + player.queue.backto(index) + await player.do_next() + else: + player.queue.backto(index + 1) + await player.stop() + +async def moveTrack(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + c = player.queue._position - 1 + position = data.get("position") + new_position = data.get("newPosition") + + moveItem = player.queue._queue[position] + player.queue._queue.remove(moveItem) + player.queue._queue.insert(new_position, moveItem) + return { + "op": "moveTrack", + "position": { + "index": position - c, + "track_id": moveItem.track_id + }, + "newPosition": { + "index": new_position - c + }, + "guild_id": player.guild.id, + "requester_id": member.id, + "skip_users": [member.id] + } + +async def addTracks(player, member: Member, data: dict): + raw_tracks = data.get("tracks", []) + tracks = [voicelink.Track( + track_id=track["track_id"], + info=track["info"], + requester=member + ) for track in raw_tracks] + + await player.add_track(tracks) + + if not player.is_playing: + await player.do_next() + +async def getTracks(player, member: Member, data: dict): + query = data.get("query", None) + if query: + payload = {"op": "getTracks", "user_id": member.id} + tracks = await player.get_tracks(query, requester=member) + if not tracks: + return payload + + payload["tracks"] = [ track.toDict() for track in tracks ] + return payload + +async def shuffleTrack(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + await player.shuffle(data.get("type", "queue"), member) + +async def updateRepeatMode(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + mode = data.get("mode", "off") + if mode.lower() not in ['off', 'track', 'queue']: + mode = "off" + player.queue.set_repeat(mode) + +async def updatePause(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + pause = data.get("pause", True) + if pause: + player.pause_votes.clear() + else: + player.resume_votes.clear() + await player.set_pause(pause, member) + +async def updatePosition(player, member: Member, data: dict): + if not player.is_privileged(member): + return missingPermission(member.id) + + position = data.get("position"); + await player.seek(position, member); + +methods = { + "initPlayer": initPlayer, + "skipTo": skipTo, + "backTo": backTo, + "moveTrack": moveTrack, + "addTracks": addTracks, + "getTracks": getTracks, + "shuffleTrack": shuffleTrack, + "updateRepeat": updateRepeatMode, + "updatePause": updatePause, + "updatePosition": updatePosition, +} + +async def process_methods(websocket, bot: commands.Bot, data: dict) -> None: + method = methods.get(data.get("op", ""), None) + if not method: + return + + guild = None + member = None + guild_id = data.get("guild_id", None) + user_id = data.get("user_id", None) + if guild_id is None: + user = bot.get_user(user_id) + if not user: + return + + for g in user.mutual_guilds: + m = g.get_member(user.id) + if m.voice and m.voice.channel: + guild = g + member = m + + else: + guild = bot.get_guild(guild_id) + member = guild.get_member(user_id) + + if not guild: + return + + player = guild.voice_client + if not player or not Member: + return + try: + resp: dict = await method(player, member, data) + if resp: + await websocket.send(json.dumps(resp)) + except: + return \ No newline at end of file diff --git a/web/ipc/server.py b/web/ipc/server.py new file mode 100644 index 0000000..ad1efdf --- /dev/null +++ b/web/ipc/server.py @@ -0,0 +1,67 @@ +import json + +from websockets.server import serve, WebSocketServerProtocol +from websockets.exceptions import ConnectionClosed +from discord.ext import commands + +from typing import ( + Optional, + Dict, + Any +) + +class IPCServer: + def __init__( + self, + bot: commands.Bot, + host: str = "127.0.0.1", + port: int = 8000, + sercet_key: Optional[str] = None + ): + self.bot = bot + self.host = host + self.port = port + self.sercet_key = sercet_key + + self.user = {} + self.connections = set() + + def is_secure(self, data: dict) -> bool: + if (key := data.get("sercet")): + return str(key) == str(self.sercet_key) + return bool(self.sercet_key is None) + + async def start(self): + try: + print("Starting IPC") + await serve(self.handle_ipc_connection, self.host, self.port) + except Exception as e: + print(e) + + async def handle_ipc_connection(self, websocket: WebSocketServerProtocol): + from .methods import process_methods + + client_id = websocket.request_headers.get("Client-ID", None) + if not client_id: + return + + self.connections.add(websocket) + try: + async for message in websocket: + data: Dict[str, Any] = json.loads(message) + if self.is_secure(data): + action = data.get("data") + await process_methods(websocket, self.bot, action) + + except ConnectionClosed: + pass + + self.connections.remove(websocket) + + async def send(self, payload): + for conn in self.connections: + try: + await conn.send(json.dumps(payload)) + except: + await conn.close() + self.connections.remove(conn) \ No newline at end of file