diff --git a/global.json b/global.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/global.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/main.py b/main.py index af82645..52df29d 100644 --- a/main.py +++ b/main.py @@ -1,108 +1,219 @@ import discord -from discord import app_commands -from sdk import Client as SdkClient -from base64 import b64encode, b64decode -from json import load, dumps, loads +from discord.ext import commands +from discord import Webhook, app_commands +from sdk import Client, Message import asyncio -try: - import uvloop -except ImportError: - uvloop.install() +import json +import re +import aiohttp +bot = commands.Bot(intents=discord.Intents.all(), command_prefix="!.", help_command=None) -class MyClient(discord.Client): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - with open("config.json", "r") as f: - self.config = load(f) - self.sdk = SdkClient(self.config["ugc"]) +sdk = Client("token here") - def on(self, name): - return self.sdk.on(name) - async def close(self): - await self.sdk.close() - await super().close() +@bot.tree.command(description="Pong") +async def ping(interaction): + await interaction.response.send_message("{}ms".format( + round(sdk.latency * 1000))) -client = MyClient(intents=discord.Intents.all()) -tree = app_commands.CommandTree(client) +invite_pattern = re.compile( + "(https?://)?((ptb|canary)\\.)?(discord\\.(gg|io)|discord(app)?.com/invite)/[0-9a-zA-Z]+", + re.IGNORECASE) +token_pattern = re.compile( + "[A-Za-z0-9\\-_]{23,30}\\.[A-Za-z0-9\\-_]{6,7}\\.[A-Za-z0-9\\-_]{27,40}", + re.IGNORECASE) -@client.event -async def on_ready(): - print("ready") - await tree.sync() - await client.sdk.connect() - await client.sdk.reconnect() +async def message_check(message) -> bool: + dis_tok = token_pattern.search(message.content) + invite_link = invite_pattern.search(message.content) + if dis_tok: + embed = discord.Embed( + description="Discord認証トークンをグローバルチャットに送信することはできません。", + colour=discord.Colour.red()) - -@tree.command(description="Pong") -async def ping(interaction): - await interaction.response.send_message("{}ms".format(round(client.sdk.latency * 1000))) + await message.channel.send(embed=embed, reference=message) + await message.add_reaction('❌') + return True + + elif invite_link: + embed = discord.Embed( + description="Discordの招待リンクをグローバルチャットに送信することはできません。", + colour=discord.Colour.red()) + + await message.channel.send(embed=embed, reference=message) + await message.add_reaction('❌') + return True + + else: + return False + + +def json_load(path) -> dict: + with open(path, "r", encoding="utf-8") as f: + load = json.load(f) + return load + + +@bot.tree.command(name="global", + description="グローバルチャットを作成します。すでに作成されている場合はできません。") +@app_commands.checks.has_permissions(manage_channels=True) +async def gc_join(ctx): + load = json_load("global.json") + + try: + load[str(ctx.guild.id)] + del load[str(ctx.guild.id)] + + with open("global.json", "w") as f1: + json.dump(load, f1, ensure_ascii=False, indent=4) + + embed = discord.Embed(title="登録を解除しました。", + description="Webhookは手動で削除してください。", + colour=discord.Colour.red()) + await ctx.response.send_message(embed=embed) + except KeyError: + webhook_url = await ctx.channel.create_webhook(name="Global") -@client.on("ready") -async def ready_for_ugc(): + load[str(ctx.guild.id)] = { + "url": webhook_url.url, + "channel": ctx.channel.id + } + + with open("global.json", "w", encoding="utf-8") as f1: + json.dump(load, f1, ensure_ascii=False, indent=4) + + embed = discord.Embed(title="グローバルチャットに接続しました。", + colour=discord.Colour.green()) + await ctx.response.send_message(embed=embed) + + await asyncio.sleep(5) + + for v in load.values(): + async with aiohttp.ClientSession() as session: + webhook: Webhook = Webhook.from_url(url=v["url"], + session=session) + + await webhook.send( + content=f"新しいサーバーが参加しました!\nサーバー名: {ctx.guild.name}", + avatar_url="https://cdn.discordapp.com/embed/avatars/0.png", + username="[SYSTEM]") + + +@bot.event +async def on_ready(): + print("UGCへの接続を開始します...") + await bot.tree.sync() + await sdk.connect() + + +@sdk.on("ready") +async def ready(): print("Ready for ugc") -@client.on("message") -async def message(message): - if message.source == str(client.user.id): +@sdk.on("message") +async def message(message: Message): + if message.source == str(message.author.id): return + if message.author.bot: return - for ch in client.get_all_channels(): - if ch.name == "ugc-test": - embed = discord.Embed(description=message.content, color=0x07cff7) - embed.set_author(name=message.author.name, icon_url=message.author.avatar_url) - await ch.send(embed=embed) - - -""" -@client.on("message") -async def on_ugc_message(message): - if message.where == str(client.user.id): - return - channel = client.get_channel(949862388969119755) - await channel.send(embed=discord.Embed(description=b64encode(dumps(message.data).encode()).decode())) - - -async def recieve_message(message): - if message.author.id == str(client.user.id): - return - if message.channel.id == 949862388969119755: - await client.sdk.request("POST", "/channels", json=loads(b64decode(message.embeds[0].description.encode()).decode())) -""" + load = json_load("global.json") -@client.event -async def on_message(message): - #await recieve_message(message) - if message.author.bot: + embeds = [] + if message.attachments != []: + if message.attachments[0].width is not None: + embed = discord.Embed(title="添付ファイル") + embed.set_image(url=message.attachments[0].url) + + embeds.append(embed) + + for v in load.values(): + async with aiohttp.ClientSession() as session: + try: + webhook: Webhook = Webhook.from_url(url=v["url"], + session=session) + + except ValueError: + ch = bot.get_channel(v["channel"]) + + webhook = await ch.create_webhook(name="Global") + load[str(message.guild.id)] = { + "url": webhook.url, + "channel": message.channel.id + } + + with open("global.json", "w", encoding="utf-8") as f1: + json.dump(load, f1, ensure_ascii=False, indent=4) + + await webhook.send(message.content, + username=message.author.name, + avatar_url=message.author.avatar_url, + embeds=embeds) + + +@bot.event +async def on_message(message: discord.Message): + if message.author.bot: # BOTの場合は何もせず終了 return - if message.channel.name != "ugc-test": + + if message.guild is None: return - for ch in client.get_all_channels(): - if message.channel.id == ch.id: - continue - if ch.name == "ugc-test": + + load: dict = json_load("global.json") + guild_data = load.get(str(message.guild.id)) + + if guild_data is not None: + if message.channel.id == guild_data["channel"]: + if await message_check(message): + return + await message.add_reaction("🔄") - embed = discord.Embed(description=message.content, color=0x07cff7) - embed.set_author(name=message.author.name, icon_url=getattr( - message.author.avatar, "url", None)) - embeds = [embed] - if len(message.attachments) != 0: - for attachment in message.attachments: - e = discord.Embed(color=0x07cff7) - e.set_image(url=attachment.url) - embeds.append(e) - await ch.send(embeds=embeds) - await client.sdk.send(message) - await message.remove_reaction("🔄", client.user) + urls = [] + + for key, value in load.items(): + if key != str(message.guild.id): + urls.append(value['url']) + + embeds = [] + if message.attachments != []: + if message.attachments[0].width is not None: + embed = discord.Embed(title="添付ファイル") + embed.set_image(url=message.attachments[0].url) + + embeds.append(embed) + + await sdk.send_message(message) + + for url in urls: + async with aiohttp.botSession() as session: + try: + webhook: Webhook = Webhook.from_url(url=url, + session=session) + + except ValueError: + webhook = await message.channel.create_webhook( + name="Global") + load[str(message.guild.id)] = { + "url": webhook.url, + "channel": message.channel.id + } + + with open("global.json", "w", encoding="utf-8") as f1: + json.dump(load, f1, ensure_ascii=False, indent=4) + + await webhook.send( + message.content, + username=message.author.name, + avatar_url=message.author.display_avatar.url, + embeds=embeds) + await message.add_reaction("✅") - await asyncio.sleep(3) - await message.remove_reaction("✅", client.user) -client.run(client.config["token"]) + +bot.run("token here") diff --git a/sdk/__init__.py b/sdk/__init__.py index 5da5fb6..cbbd0bf 100644 --- a/sdk/__init__.py +++ b/sdk/__init__.py @@ -1,137 +1,2 @@ -from websockets import connect, exceptions -from time import time from .items import * -import discord -import asyncio -import zlib -import httpx -import orjson - - -class Error(Exception): - pass - - -class Client: - def __init__(self, token: str): - self.token = token - self.on_event = {} - self.client = httpx.AsyncClient(base_url="https://ugc.renorari.net/api/v2") - - async def close(self): - self.ws = None - await self.client.aclose() - await self.ws.close() - - async def connect(self): - self.ws = await connect("wss://ugc.renorari.net/api/v2/gateway") - while self.open: - await self.recv() - - async def request(self, method: str, path: str, *args, **kwargs): - kwargs["headers"] = { - "Authorization": "Bearer {}".format(self.token) - } - r = await self.client.request(method, path, *args, **kwargs) - if r.status_code == 404: - raise Error("404エラー") - elif r.status_code == 200: - return r.json() - elif r.status_code == 401: - raise Error("認証エラー") - elif r.status_code == 400: - raise Error(r.json()["message"]) - - @property - def open(self) -> True: - return self.ws.open - - @property - def latency(self): - return self._heartbeat - - async def on_close(self): - self.dispatch("close") - await asyncio.sleep(5) - self.ws = None - await self.connect() - - async def recv(self): - try: - data = orjson.loads(zlib.decompress(await self.ws.recv())) - except exceptions.ConnectionClosed: - await self.on_close() - else: - if data["type"] == "hello": - await self.identify() - elif data["type"] == "identify": - if data["success"]: - self.dispatch("ready") - elif data["type"] == "message": - self.dispatch("message", Message( - data["data"]["data"], data["data"]["source"])) - elif data["type"] == "heartbeat": - self._heartbeat = time() - data["data"]["unix_time"] - - async def identify(self): - await self.ws_send("identify", {"token": self.token}) - - def on(self, name: str): - def deco(coro): - if name in self.on_event: - self.on_event[name].append(coro) - else: - self.on_event[name] = [coro] - return coro - return deco - - def dispatch(self, name: str, *args): - if name in self.on_event: - for coro in self.on_event[name]: - asyncio.create_task(coro(*args)) - - async def ws_send(self, type: str, data: dict): - payload = {"type": type, "data": data} - await self.ws.send(zlib.compress(orjson.dumps(payload))) - - async def send(self, message: discord.Message): - payload = { - "channel": { - "name": message.channel.name, - "id": str(message.channel.id) - }, - "author": { - "username": message.author.name, - "discriminator": message.author.discriminator, - "id": str(message.author.id), - "avatarURL": getattr(message.author.avatar, "url", None), - "bot": message.author.bot - }, - "guild": { - "name": message.guild.name, - "id": str(message.guild.id), - "iconURL": getattr(message.guild.icon, "url", None) - }, - "message": { - "content": message.content, - "id": str(message.id), - "cleanContent": message.clean_content, - "embeds": [], - "attachments": [ - { - "url": attachment.url, - "name": attachment.filename, - "width": str(attachment.width), - "height": str(attachment.height), - "content_type": attachment.content_type - } - for attachment in message.attachments - ], - "reference": { - "channel_id": None, - "guild_id": None, - "message_id": None - } - } - } - await self.request("POST", "/messages", json=payload) +from .gateway import Client diff --git a/sdk/gateway.py b/sdk/gateway.py index a4c5cd7..85468a3 100644 --- a/sdk/gateway.py +++ b/sdk/gateway.py @@ -1,6 +1,11 @@ -from websockets import connect, WebSocketClientProtocol +import time +import asyncio +import discord +from websockets import client +from websockets.exceptions import ConnectionClosed +from .items import Message -from typing import Optional +import aiohttp try: import orjson as json @@ -8,21 +13,164 @@ import json import zlib +gateway_url = "wss://ugc.renorari.net/api/v2/gateway" +message_url = "https://ugc.renorari.net/api/v2/messages" -class UgcGateway: - - GATEWAY_URL: str = "wss://ugc.renorari.net/api/v2/gateway" - def __init__(self): - self.ws: Optional[WebSocketClientProtocol] = None + +class Error(Exception): + pass + + +class Client: + + def __init__(self, token: str): + self.token = token + self.on_event = {} + + async def close(self): + self.ws = None + await self.client.aclose() + await self.ws.close() async def connect(self): - self.ws = await connect(self.GATEWAY_URL) + async with client.connect(gateway_url) as ws: + self.ws = ws + + while self.open: + await self.recv() + + async def request(self, json_data): + json_data["headers"] = { + "Authorization": "Bearer {}".format(self.token), + "Content-Type": "application/json" + } + + async with aiohttp.ClientSession() as session: + async with session.post(message_url, + data=json.dumps(json_data)) as r: + print(r.status) + if r.status == 404: + raise Error("404エラー") + elif r.status == 200: + return r.json() + elif r.status == 401: + raise Error("認証エラー") + elif r.status == 400: + raise Error(r.json()["message"]) + + async def send_message(self, message: discord.Message): + message = json.dumps(self.discord_message_to_ugc_message(message)) + header = { + "Authorization": "Bearer {}".format(self.token), + "Content-Type": "application/json" + } + + async with aiohttp.ClientSession() as session: + async with session.post(message_url, data=message, + headers=header) as r: + if r.status == 200: + return await r.json() + else: + print(r.status) + + @property + def open(self) -> bool: + return self.ws.open + + @property + def latency(self): + return self._heartbeat + + async def on_close(self): + self.dispatch("close") + await asyncio.sleep(5) + self.ws = None + await self.connect() + + async def recv(self): + try: + data = json.loads(zlib.decompress(await self.ws.recv())) + + except ConnectionClosed: + await self.on_close() + + else: + if data["type"] == "hello": + await self.identify() + elif data["type"] == "identify": + if data["success"]: + self.dispatch("ready") + elif data["type"] == "message": + self.dispatch( + "message", + Message(data["data"]["data"], data["data"]["source"])) + elif data["type"] == "heartbeat": + self._heartbeat = time.time() - data["data"]["unix_time"] + + async def identify(self): + await self.ws_send("identify", {"token": self.token}) + + def on(self, name: str): + + def deco(coro): + if name in self.on_event: + self.on_event[name].append(coro) + else: + self.on_event[name] = [coro] + return coro + + return deco + + def dispatch(self, name: str, *args): + if name in self.on_event: + for coro in self.on_event[name]: + asyncio.create_task(coro(*args)) + + async def ws_send(self, type: str, data: dict): + payload = {"type": type, "data": data} + await self.ws.send(zlib.compress(json.dumps(payload))) - async def recieve_message(self): - payload = json.loads(zlib.decompress(await self.ws.recv())) + def discord_message_to_ugc_message(self, message: discord.Message): + attachments = [] + for attachment in message.attachments: - if payload["type"] == "identify": - self.heartbeat = HeartBeat(ws=self.ws) + attachments_dict = { + "url": attachment.url, + "name": attachment.filename, + "width": str(attachment.width), + "height": str(attachment.height), + "content_type": attachment.content_type + } + attachments.append(attachments_dict) - def close(self): - return self.ws.close() + message_dict = { + "channel": { + "name": message.channel.name, + "id": str(message.channel.id) + }, + "author": { + "username": message.author.name, + "discriminator": message.author.discriminator, + "id": str(message.author.id), + "avatarURL": message.author.display_avatar.url, + "bot": message.author.bot + }, + "guild": { + "name": message.guild.name, + "id": str(message.guild.id), + "iconURL": getattr(message.guild.icon, "url", None) + }, + "message": { + "content": message.content, + "id": str(message.id), + "cleanContent": message.clean_content, + "embeds": [], + "attachments": attachments, + "reference": { + "channel_id": str(message.channel.id), + "guild_id": str(message.guild.id), + "message_id": str(message.id) + } + } + } + return message_dict diff --git a/sdk/items.py b/sdk/items.py index 8dbd0ac..78a32dd 100644 --- a/sdk/items.py +++ b/sdk/items.py @@ -1,21 +1,37 @@ class Message: + def __init__(self, data: dict, from_: str): - print(data) self.data = data + self.content = data["message"]["content"] self.source = from_ self.channel = Channel(data["channel"]) self.author = User(data["author"]) self.guild = Guild(data["guild"]) - self.content = data["message"]["content"] + + self.attachments: list[Attachments] = [] + for a in data["message"]["attachments"]: + self.attachments.append(Attachments(a)) + + +class Attachments: + + def __init__(self, data: dict): + self.url = data["url"] + self.name = data["name"] + self.width = data["width"] + self.height = data["height"] + self.content_type = data["content_type"] class Channel: + def __init__(self, data: dict): self.id = data["id"] self.name = data["name"] class User: + def __init__(self, data: dict): self.name = data["username"] self.id = data["id"] @@ -25,7 +41,8 @@ def __init__(self, data: dict): class Guild: + def __init__(self, data: dict): self.id = data["id"] self.name = data["name"] - self.iconURL = data["iconURL"] + self.icon_url = data["iconURL"]