From e9096da03ffd8c01b6ef02cf1da19f4bd086d220 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Fri, 10 Jun 2022 18:20:45 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[WIP]=20Add:=20=E3=82=B0=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=90=E3=83=AB=E3=83=81=E3=83=A3=E3=83=83=E3=83=88=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 28 +++ Dockerfile | 2 +- create_index.py | 16 ++ locale/en.yml | 1 - locale/ja.yml | 80 ++++++++- poetry.lock | 109 +++++++++++- pyproject.toml | 1 + src/common/confirm.py | 52 ++++++ src/common/embed.py | 46 +++++ src/common/missing.py | 20 +++ src/const/color.py | 1 + src/core.py | 27 +-- src/db/gc_room.py | 17 ++ src/embed.yml | 22 +++ src/exts/ex_events.py | 20 +++ src/exts/global_chat.py | 340 ++++++++++++++++++++++++++++++++++++- src/exts/ping.py | 8 +- src/lang.py | 101 +++++++---- 18 files changed, 834 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 create_index.py create mode 100644 src/common/confirm.py create mode 100644 src/common/embed.py create mode 100644 src/common/missing.py create mode 100644 src/db/gc_room.py create mode 100644 src/embed.yml create mode 100644 src/exts/ex_events.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6963e9b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: "Lint" +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: "Checkout repository" + uses: actions/checkout@v3 + - name: "Setup Python" + uses: actions/setup-python@v3 + with: + python-version: "3.9" + - name: "Install poetry" + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + - name: "Load cached venv" + id: load-cached-venv + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + run: poetry install + if: steps.load-cached-venv.outputs.cache-hit != 'true' + - name: "Lint" + run: poetry run poe lint diff --git a/Dockerfile b/Dockerfile index 12c73b5..57a8929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY . . # -- Run the app ENV environment=production -CMD ["python", "main.py"] \ No newline at end of file +CMD ["python", "main.py"] diff --git a/create_index.py b/create_index.py new file mode 100644 index 0000000..8603e8d --- /dev/null +++ b/create_index.py @@ -0,0 +1,16 @@ +import pymongo +from dotenv import load_dotenv +from os import getenv + +load_dotenv() + +db_client = pymongo.MongoClient(getenv("MONGO_URI")) +db = db_client[getenv("environment", "development")] +db.gc_room.create_index( + [ + ("channels", pymongo.TEXT), + ], + unique=True, +) + +print("Done.") diff --git a/locale/en.yml b/locale/en.yml index 31048f5..b3e2bfb 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -1,7 +1,6 @@ chat_input: ping: _: - name: "ping" description: "Pong! Returns the latency of SevenBot." args: embed: diff --git a/locale/ja.yml b/locale/ja.yml index 0d17599..3eaafb3 100644 --- a/locale/ja.yml +++ b/locale/ja.yml @@ -2,9 +2,85 @@ $schema: "./_scheme.yml" chat_input: ping: _: - name: "ping" description: "Pong! SevenBotの通信速度を返します。" args: embed: title: ":ping_pong: Pong!" - description: "通信速度は`{latency}ms`です。" \ No newline at end of file + description: "通信速度は`{latency}ms`です。" + global: + _: + description: "グローバルチャット。" + activate: + _: + description: "グローバルチャットを有効にします。" + create_confirm: + title: ":x: 不明なグローバルチャット" + description: | + チャット`{name}`が見付かりませんでした。 + 作成しますか? + create_success: + title: ":white_check_mark: 完了" + description: | + グローバルチャット{name}(`{id}`)を作成しました。 + `/global activate {id}`で有効にします。 + join_confirm: + title: ":question: 参加確認" + description: | + グローバルチャット`{name}`(`{id}`)に参加しますか? + >>> {description} + join_password: + title: "パスワードの確認" + fields: + password: + name: "パスワード" + placeholder: "パスワードを入力してください。" + password_failed: + title: ":x: パスワードが違います" + description: "パスワードが違います。" + join_success: + title: ":white_check_mark: 完了" + description: | + グローバルチャット`{name}`(`{id}`)に参加しました。 + joined_announce: + title: ":inbox_tray: 参加" + description: | + {name}がグローバルチャットに参加しました。 + 現在{count}チャンネルが参加しています。 + already_in: + title: ":x: 参加済み" + description: | + このチャンネルは既にグローバルチャット{name}(`{id}`)に参加しています。 + `/global deactivate`で退出できます。 + not_in: + title: ":x: 未参加" + description: | + このチャンネルはグローバルチャットに参加していません。 + `/global activate `で参加できます。 + deactivate_confirm: + title: ":warning: 退出しますか?" + description: | + グローバルチャット{name}(`{id}`)から退出しますか? + deactivated: + title: ":white_check_mark: 完了" + description: | + グローバルチャット{name}(`{id}`)から退出しました。 + create: + title: "グローバルチャットの作成" + fields: + id: + name: "チャットID" + placeholder: "接続に使用するチャットID。" + name: + name: "チャット名" + placeholder: "チャット名。デフォルトではチャットIDが使用されます。" + description: + name: "説明" + placeholder: "チャットの説明。接続時に表示されます。" + password: + name: "パスワード" + placeholder: "チャットのパスワード。省略すると誰でも入れるようになります。" +common: + confirm: "はい" + cancel: "いいえ" + timeouted: "タイムアウトしました。もう一度最初からやり直してください。" + canceled: "キャンセルしました。" diff --git a/poetry.lock b/poetry.lock index e1e9bfc..4c2e6b7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -103,6 +103,22 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "dataclasses-json" +version = "0.5.7" +description = "Easily serialize dataclasses to and from JSON" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.3.0,<4.0.0" +marshmallow-enum = ">=1.5.1,<2.0.0" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["pytest (>=6.2.3)", "ipython", "mypy (>=0.710)", "hypothesis", "portray", "flake8", "simplejson", "types-dataclasses"] + [[package]] name = "discord.py" version = "2.0.0a4227+geee65ac3" @@ -192,6 +208,34 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "marshmallow" +version = "3.15.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = "*" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.4.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.7)"] +lint = ["mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-enum" +version = "1.5.1" +description = "Enum field for Marshmallow" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +marshmallow = ">=2.0.0" + [[package]] name = "mccabe" version = "0.6.1" @@ -232,10 +276,21 @@ python-versions = ">=3.7" name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +category = "main" optional = false python-versions = "*" +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + [[package]] name = "pastel" version = "0.2.1" @@ -312,6 +367,17 @@ snappy = ["python-snappy"] srv = ["dnspython (>=1.16.0,<3.0.0)"] zstd = ["zstandard"] +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + [[package]] name = "pyproject-flake8" version = "0.0.1a4" @@ -363,10 +429,22 @@ python-versions = ">=3.7" name = "typing-extensions" version = "4.2.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "typing-inspect" +version = "0.7.1" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + [[package]] name = "yarl" version = "1.7.2" @@ -382,7 +460,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "f6f725bb9327175d409282b2dc044e0c02a41a2c858aabcde7d902de45903f3c" +content-hash = "bfed4e60fe029a6760a19ed2f02c40056b10728717277165d4109f96fc792c8e" [metadata.files] aiohttp = [ @@ -508,6 +586,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +dataclasses-json = [ + {file = "dataclasses-json-0.5.7.tar.gz", hash = "sha256:c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90"}, + {file = "dataclasses_json-0.5.7-py3-none-any.whl", hash = "sha256:bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd"}, +] "discord.py" = [] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -590,6 +672,14 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +marshmallow = [ + {file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"}, + {file = "marshmallow-3.15.0.tar.gz", hash = "sha256:2aaaab4f01ef4f5a011a21319af9fce17ab13bf28a026d1252adab0e035648d5"}, +] +marshmallow-enum = [ + {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, + {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -663,6 +753,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] pastel = [ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, @@ -772,6 +866,10 @@ pymongo = [ {file = "pymongo-4.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:f0aea377b9dfc166c8fa05bb158c30ee3d53d73f0ed2fc05ba6c638d9563422f"}, {file = "pymongo-4.1.1.tar.gz", hash = "sha256:d7b8f25c9b0043cbaf77b8b895814e33e7a3c807a097377c07e1bd49946030d5"}, ] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] pyproject-flake8 = [ {file = "pyproject-flake8-0.0.1a4.tar.gz", hash = "sha256:8ed9453f1d984cfe94c998f9840275359e29e7f435b8ddd188ae084e2dc1270c"}, {file = "pyproject_flake8-0.0.1a4-py2.py3-none-any.whl", hash = "sha256:1a8f94e18d08677ee780625049d9d00a9ee823661c6606caab8a383351037a75"}, @@ -827,6 +925,11 @@ typing-extensions = [ {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] +typing-inspect = [ + {file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"}, + {file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"}, + {file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"}, +] yarl = [ {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, diff --git a/pyproject.toml b/pyproject.toml index e348ae1..2c89a50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ colorama = "^0.4.4" python-dotenv = "^0.20.0" PyYAML = "^6.0" motor = "^3.0.0" +dataclasses-json = "^0.5.7" [tool.poetry.dev-dependencies] black = "^22.3.0" diff --git a/src/common/confirm.py b/src/common/confirm.py new file mode 100644 index 0000000..f5343c0 --- /dev/null +++ b/src/common/confirm.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from secrets import token_urlsafe + +import discord +from discord.ui import Button, View +from src.lang import text + + +class ConfirmView(View): + def __init__( + self, + lang: str, + cancel_first: bool = False, + button_color: discord.ButtonStyle = discord.ButtonStyle.primary, + ): + super().__init__(timeout=30) + self.value: bool | None = None + self.nonce: str = token_urlsafe(16) + self.interaction: discord.Interaction = None + confirm_button = Button( + label=text(lang, "common.confirm"), + style=button_color, + row=0, + custom_id=self.nonce + ":confirm", + ) + cancel_button = Button( + label=text(lang, "common.cancel"), + style=discord.ButtonStyle.secondary, + row=0, + custom_id=self.nonce + ":cancel", + ) + confirm_button.callback = self.callback + cancel_button.callback = self.callback + + if cancel_first: + self.add_item( + cancel_button, + ) + self.add_item(confirm_button) + else: + self.add_item(confirm_button) + self.add_item(cancel_button) + + async def on_timeout(self) -> None: + self.stop() + + async def callback(self, interaction: discord.Interaction): + self.value = interaction.data["custom_id"].split(":")[1] == "confirm" + + self.interaction = interaction + self.stop() diff --git a/src/common/embed.py b/src/common/embed.py new file mode 100644 index 0000000..06e9c49 --- /dev/null +++ b/src/common/embed.py @@ -0,0 +1,46 @@ +from discord import Embed +import yaml + +from src.const.color import Color + +from .. import lang + + +class LocaleEmbed(Embed): + def __init__(self, data: lang.LocaleGroup, **formats): + self.locale_data = data + super().__init__() + self.title = self.locale_data("title", **formats) + self.description = self.locale_data("description", **formats) + with open("src/embed.yml") as file: + embed_data = yaml.safe_load(file) + try: + for part in self.locale_data.code.split("."): + embed_data = embed_data[part] + except KeyError: + embed_data = {} + color = embed_data.get("color") + if color is None: + pass + elif isinstance(color, int): + self.color = embed_data.get("color") + elif color.startswith("#"): + self.color = int(color[1:], 16) + else: + self.color = Color[color]._value_ + self.set_footer( + text=self.locale_data("footer", None, **formats), icon_url=embed_data.get("footer_icon_url", None) + ) + self.set_thumbnail(url=embed_data.get("thumbnail_url", None)) + if self.locale_data("author.name", embed_data.get("author_name", None)): + self.set_author( + name=self.locale_data("author.name", embed_data.get("author_name", None), **formats), + url=self.locale_data("author.url", embed_data.get("author_url", None), **formats), + icon_url=embed_data.get("author_icon_url", None), + ) + self.set_image(url=embed_data.get("image_url", None)) + for field_name, field_data in self.locale_data("fields", {}).items(): + self.add_field( + name=field_name.format(**formats), + value=field_data.format(**formats), + ) diff --git a/src/common/missing.py b/src/common/missing.py new file mode 100644 index 0000000..13e47f7 --- /dev/null +++ b/src/common/missing.py @@ -0,0 +1,20 @@ +from typing import Any + + +class _MissingSentinel: + __slots__ = () + + def __eq__(self, other): + return False + + def __bool__(self): + return False + + def __hash__(self): + return 0 + + def __repr__(self): + return "..." + + +MISSING: Any = _MissingSentinel() diff --git a/src/const/color.py b/src/const/color.py index e8e9245..ec2c8fe 100644 --- a/src/const/color.py +++ b/src/const/color.py @@ -8,3 +8,4 @@ class Color(Enum): success = discord.Color.green() info = discord.Color.blue() warning = discord.Color.orange() + prompt = discord.Color.dark_gray() diff --git a/src/core.py b/src/core.py index 9d406fa..a34f738 100644 --- a/src/core.py +++ b/src/core.py @@ -1,4 +1,4 @@ -import hashlib +import json import logging import os from base64 import b64decode @@ -10,8 +10,9 @@ if TYPE_CHECKING: from .exts._common import Cog -from .exts._common import CogFlag + from . import lang # noqa: F401 +from .exts._common import CogFlag class SevenBot(commands.Bot): @@ -24,12 +25,13 @@ def __init__(self): strip_after_prefix=True, case_insensitive=True, intents=intents, - application_id=int(b64decode(os.environ["TOKEN"].split(".")[0])) + application_id=int(b64decode(os.environ["TOKEN"].split(".")[0])), ) - self.prev_hash = {} + self.prev_update = {} self.prev_commands = [] self.logger = logging.getLogger("SevenBot") - self.db = motor.AsyncIOMotorClient(os.environ["MONGO_URI"]) + self.db_client = motor.AsyncIOMotorClient(os.environ["MONGO_URI"]) + self.db = self.db_client["production" if self.is_production else "development"] async def on_ready(self) -> None: """bot起動時のイベント""" @@ -50,17 +52,16 @@ async def watch_files(self) -> None: for file in os.listdir("src/exts/"): if not file.endswith(".py") or file.startswith("_"): continue - with open(f"src/exts/{file}", "rb") as f: - file_hash = hashlib.sha256(f.read()).hexdigest() - if file_hash != self.prev_hash.get(file): - if self.prev_hash.get(file) is not None: + update_time = os.path.getmtime(f"src/exts/{file}") + if update_time != self.prev_update.get(file): + if self.prev_update.get(file) is not None: self.logger.info("Reloading %s by auto reloading", file) try: await self.reload_extension(f"src.exts.{file[:-3]}") except commands.errors.ExtensionNotLoaded: await self.load_extension(f"src.exts.{file[:-3]}") await self.sync() - self.prev_hash[file] = file_hash + self.prev_update[file] = update_time except Exception as e: self.logger.exception(e) @@ -77,8 +78,10 @@ def test_guild(self): return discord.Object(int(os.environ["TEST_GUILD"])) async def sync(self) -> None: - current_commands = list(self.tree.walk_commands()) - if [c.to_dict() for c in self.prev_commands] == [c.to_dict() for c in current_commands]: + current_commands = list(self.tree.walk_commands(guild=self.test_guild)) + if sorted([c.to_dict() for c in self.prev_commands], key=lambda c: json.dumps(c)) == sorted( + [c.to_dict() for c in current_commands], key=lambda c: json.dumps(c) + ): self.logger.info("No changes, skipping sync") return self.prev_commands = current_commands diff --git a/src/db/gc_room.py b/src/db/gc_room.py new file mode 100644 index 0000000..ffe2cbf --- /dev/null +++ b/src/db/gc_room.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class GlobalChatRoom: + id: str + name: str + channels: list[int] + owner: int + password: str | None + mute: list[int] + rule: dict[str, str] + slow: int + description: str + antispam: bool diff --git a/src/embed.yml b/src/embed.yml new file mode 100644 index 0000000..901aa1f --- /dev/null +++ b/src/embed.yml @@ -0,0 +1,22 @@ +chat_input: + ping: + embed: + color: sevenbot + global: + activate: + create_confirm: + color: prompt + create_success: + color: success + already_in: + color: error + join_confirm: + color: prompt + password_failed: + color: error + join_success: + color: success + deactivate_confirm: + color: prompt + deactivated: + color: success \ No newline at end of file diff --git a/src/exts/ex_events.py b/src/exts/ex_events.py new file mode 100644 index 0000000..f4a329a --- /dev/null +++ b/src/exts/ex_events.py @@ -0,0 +1,20 @@ +from typing import TYPE_CHECKING +import discord +from discord.ext import commands + +from ._common import Cog + +if TYPE_CHECKING: + from core import SevenBot + + +class ExtraEvents(Cog): + @commands.Cog.listener("on_message") + async def on_valid_message(self, message: discord.Message) -> None: + if message.author.bot: + return + self.bot.dispatch("valid_message", message) + + +async def setup(bot: "SevenBot"): + await bot.load_cog(ExtraEvents) diff --git a/src/exts/global_chat.py b/src/exts/global_chat.py index e413113..48df93e 100644 --- a/src/exts/global_chat.py +++ b/src/exts/global_chat.py @@ -1,7 +1,16 @@ +import asyncio +import hashlib +import re +from secrets import token_urlsafe from typing import TYPE_CHECKING import discord from discord import app_commands +from discord.ui import Modal, TextInput +from discord.ext import commands +from src.db.gc_room import GlobalChatRoom +from src.common.embed import LocaleEmbed +from src.common.confirm import ConfirmView from ._common import Cog, CogFlag @@ -9,17 +18,340 @@ from core import SevenBot +INVITE_PATTERN = re.compile(r"(?:https?://)?discord(?:app\.com/invite|\.gg)/[a-z0-9]+", re.I) +FILE_COLORS = { + "application": discord.Color.blurple(), + "audio": discord.Color.green(), + "font": discord.Color.dark_teal(), + "image": discord.Color.dark_orange(), + "text": discord.Color.dark_purple(), + "video": discord.Color.dark_red(), + "zip": discord.Color.dark_gold(), + "model": discord.Color.dark_magenta(), +} + + class GlobalChat(Cog): flag = CogFlag.production | CogFlag.development group = app_commands.Group(name="global", description="グローバルチャット") - @group.command(name="activate", description="Activate global chat") - async def activate(self, interaction: discord.Interaction): - await interaction.response.defer() + def __init__(self, bot: "SevenBot"): + super().__init__(bot) + self._channels_cache = set() + + async def channels(self): + if self._channels_cache: + return self._channels_cache + async for gc_room in self.bot.db.gc_room.find(): + self._channels_cache.update(gc_room["channels"]) + return self._channels_cache + + @group.command(name="activate", description="グローバルチャットを有効にします。") + @app_commands.describe(gc_id="接続するグローバルチャットの名前。") + @app_commands.rename(gc_id="id") + async def activate(self, interaction: discord.Interaction, gc_id: str = "global"): + await interaction.response.defer(ephemeral=True) + current_room = await self.bot.db.gc_room.find_one({"channels": interaction.channel_id}) + if current_room is not None: + current_room = GlobalChatRoom.from_dict(current_room) + await interaction.followup.send( + embed=LocaleEmbed(interaction.text("already_in"), name=current_room.name, id=current_room.id), + ephemeral=True, + ) + return + gc_room = await self.bot.db.gc_room.find_one({"id": gc_id}) + if gc_room is None: + await self.create_gc_room(interaction, gc_id) + else: + await self.join_gc_room(interaction, GlobalChatRoom.from_dict(gc_room)) @group.command(name="deactivate", description="Deactivate global chat") async def deactivate(self, interaction: discord.Interaction): - await interaction.response.defer() + await interaction.response.defer(ephemeral=True) + current_room = await self.bot.db.gc_room.find_one({"channels": interaction.channel_id}) + if current_room is None: + await interaction.followup.send( + embed=LocaleEmbed(interaction.text("not_in")), + ephemeral=True, + ) + return + current_room = GlobalChatRoom.from_dict(current_room) + confirm = ConfirmView(lang=interaction.locale, button_color=discord.ButtonStyle.danger) + await interaction.followup.send( + embed=LocaleEmbed(interaction.text("deactivate_confirm"), name=current_room.name, id=current_room.id), + view=confirm, + ephemeral=True, + ) + await confirm.wait() + if confirm.value is None: + return await interaction.followup.send( + interaction.text("common.timeouted"), + ephemeral=True, + ) + if not confirm.value: + return await confirm.interaction.response.send_message( + interaction.text("common.canceled"), + ephemeral=True, + ) + await self.bot.db.gc_room.delete_one({"id": current_room.id}) + self._channels_cache.remove(interaction.channel_id) + await interaction.edit_original_message( + embed=LocaleEmbed(interaction.text("deactivated"), name=current_room.name, id=current_room.id), + ephemeral=True, + view=None, + ) + + async def create_gc_room(self, interaction: discord.Interaction, gc_id: str): + confirm = ConfirmView( + lang=interaction.locale, + button_color=discord.ButtonStyle.success, + ) + await interaction.edit_original_message( + embed=LocaleEmbed(interaction.text("create_confirm"), name=gc_id), + view=confirm, + ) + await confirm.wait() + if confirm.value is None: + return await interaction.followup.send( + interaction.text("common.timeouted"), + ephemeral=True, + ) + if not confirm.value: + return await confirm.interaction.response.send_message( + interaction.text("common.canceled"), + ephemeral=True, + ) + modal = CreateModal(confirm.interaction, gc_id) + await confirm.interaction.response.send_modal(modal) + await modal.wait() + modal_interaction = modal.interaction + response = {} + for component in modal_interaction.data["components"]: + response[component["components"][0]["custom_id"].split(":")[1]] = component["components"][0]["value"] + room = GlobalChatRoom( + id=gc_id, + name=response["name"], + channels=[interaction.channel_id], + owner=interaction.user.id, + password=hashlib.sha256(response["password"].encode("utf-8")).hexdigest() if response["password"] else None, + description=response["description"], + mute=[], + rule={}, + slow=0, + antispam=False, + ) + await modal_interaction.response.defer( + ephemeral=True, + ) + await self.bot.db.gc_room.insert_one(room.to_dict()) + self._channels_cache.add(interaction.channel_id) + await modal_interaction.edit_original_message( + embed=LocaleEmbed( + interaction.text("create_success"), + id=gc_id, + name=response["name"], + ), + view=None, + ) + + async def join_gc_room(self, interaction: discord.Interaction, gc_room: GlobalChatRoom): + confirm = ConfirmView( + lang=interaction.locale, + button_color=discord.ButtonStyle.success, + ) + await interaction.edit_original_message( + embed=LocaleEmbed( + interaction.text("join_confirm"), + name=gc_room.name, + id=gc_room.id, + description=gc_room.description, + ), + view=confirm, + ) + await confirm.wait() + if confirm.value is None: + return await interaction.followup.send( + interaction.text("common.timeouted"), + ephemeral=True, + ) + if not confirm.value: + return await confirm.interaction.response.send_message( + interaction.text("common.canceled"), + ephemeral=True, + ) + if gc_room.password: + modal = PasswordModal(confirm.interaction) + await confirm.interaction.response.send_modal(modal) + await modal.wait() + final_interaction = modal.interaction + password = final_interaction.data["components"][0]["components"][0]["value"] + if gc_room.password != hashlib.sha256(password.encode("utf8")).hexdigest(): + await final_interaction.response.send_message( + embed=LocaleEmbed(interaction.text("password_failed")), + ephemeral=True, + ) + return + else: + final_interaction = confirm.interaction + await final_interaction.response.defer( + ephemeral=True, + ) + await self.bot.db.gc_room.update_one( + {"id": gc_room.id}, + {"$addToSet": {"channels": interaction.channel_id}}, + ) + self._channels_cache.add(interaction.channel_id) + embed = LocaleEmbed( + interaction.text("join_announce"), name=interaction.guild.name, count=len(gc_room.channels) + 1 + ) + if interaction.guild.icon: + embed.set_thumbnail(url=interaction.guild.icon.url) + await self.multi_send( + gc_room, + embed=embed, + ) + await final_interaction.response.send_message( + embed=LocaleEmbed(interaction.text("join_success")), + ephemeral=True, + ) + + async def multi_send(self, gc_room: GlobalChatRoom, **kwargs): + def get_coroutine(channel_id: int): + channel = self.bot.get_channel(channel_id) + if channel is None: + return + self.single_send(gc_room, channel, **kwargs) + + await asyncio.gather(filter(lambda c: c, map(get_coroutine, gc_room.channels))) + + async def single_send(self, gc_room: GlobalChatRoom, channel: discord.TextChannel, **kwargs): + webhook = await self.get_webhook(channel, gc_room) + if webhook is None: + return + await webhook.send(**kwargs) + + async def get_webhook(self, channel: discord.TextChannel, gc_room: GlobalChatRoom): + webhook = await channel.webhooks() + name = f"sevenbot-global-webhook-{gc_room.id}" + for w in webhook: + if w.name == name: + return w + try: + return await channel.create_webhook(name=name) + except discord.HTTPException: + return None + + @commands.Cog.listener("on_valid_message") + async def on_message_global(self, message: discord.Message): + if message.channel.id not in await self.channels(): + return + db_data = await self.bot.db.gc_room.find_one({"channel": message.channel.id}) + if db_data is None: + return + gc_room = GlobalChatRoom.from_dict(db_data) + name = message.author.display_name + suffix = f"(From {message.guild.name}, ID: {message.author.id})" + if len(name) + len(suffix) > 32: + name = name[: 32 - len(suffix)] + name += suffix + embeds = [] + for attachment in message.attachments: + if attachment.content_type: + content_type = attachment.content_type + else: + content_type = "application/octet-stream" + embed = discord.Embed( + title=attachment.name + " (" + self.get_size(attachment.size) + ")", + url=attachment.url, + color=FILE_COLORS[content_type.split("/")[0]], + ) + if content_type.startswith("image/"): + embed.set_image(url=attachment.url) + embeds.append(embed) + await self.multi_send_filtered( + gc_room, + content=message.clean_content, + username=name, + avatar_url=message.author.display_avatar, + embeds=embeds, + ) + + def get_size(self, size: int): + if size < 1024: + return f"{size} Bytes" + elif size < 1048576: + return f"{size / 1024} KiB" + elif size < 1073741824: + return f"{size / 1048576} MiB" + else: + return f"{size / 1073741824} GiB" + + async def multi_send_filtered(self, gc_room: GlobalChatRoom, content, **kwargs): + if len(content.splitlines()) > 10: + content = "\n".join(content.splitlines()[:10]) + "\n..." + content = INVITE_PATTERN.sub("", content) + await self.multi_send(gc_room, content=content, **kwargs) + + +class CreateModal(Modal): + def __init__(self, interaction: discord.Interaction, name: str): + command_texts = interaction.text("chat_input.global.activate.create") + super().__init__(title=command_texts("title")) + field_texts = command_texts("fields") + self.nonce: str = token_urlsafe(16) + self.interaction = None + self.add_item( + TextInput( + label=field_texts("name.name"), + placeholder=field_texts("name.placeholder"), + default=name, + custom_id=self.nonce + ":name", + ) + ) + self.add_item( + TextInput( + label=field_texts("description.name"), + placeholder=field_texts("description.placeholder"), + default="", + style=discord.TextStyle.paragraph, + custom_id=self.nonce + ":description", + ) + ) + self.add_item( + TextInput( + label=field_texts("password.name"), + placeholder=field_texts("password.placeholder"), + default="", + custom_id=self.nonce + ":password", + required=False, + ) + ) + + async def on_submit(self, interaction: discord.Interaction) -> None: + self.interaction = interaction + self.stop() + + +class PasswordModal(Modal): + def __init__(self, interaction: discord.Interaction): + command_texts = interaction.text("chat_input.global.activate.join_password") + super().__init__(title=command_texts("title")) + field_texts = command_texts("fields") + self.nonce: str = token_urlsafe(16) + self.interaction = None + self.add_item( + TextInput( + label=field_texts("password.name"), + placeholder=field_texts("password.placeholder"), + default="", + custom_id=self.nonce + ":password", + required=True, + ) + ) + + async def on_submit(self, interaction: discord.Interaction) -> None: + self.interaction = interaction + self.stop() async def setup(bot: "SevenBot"): diff --git a/src/exts/ping.py b/src/exts/ping.py index d657fc9..485ca60 100644 --- a/src/exts/ping.py +++ b/src/exts/ping.py @@ -3,6 +3,8 @@ import discord from discord import app_commands +from src.common.embed import LocaleEmbed + from ._common import Cog, CogFlag from src.const.color import Color @@ -16,10 +18,10 @@ class Ping(Cog): @app_commands.command(name="ping", description="Pong!") async def ping(self, interaction: discord.Interaction): await interaction.response.send_message( - embed=discord.Embed( - title=interaction.text("embed.title"), - description=interaction.text("embed.description", latency=round(self.bot.latency * 1000)), + embed=LocaleEmbed( + interaction.text("embed"), color=Color.sevenbot.value, + latency=round(self.bot.latency * 1000), ), ephemeral=True, ) diff --git a/src/lang.py b/src/lang.py index fe8ec19..58f7e32 100644 --- a/src/lang.py +++ b/src/lang.py @@ -1,9 +1,47 @@ import os +from typing import Any import discord import yaml +from src.common.missing import MISSING -def get_locale(locale: discord.Locale) -> str: + +class LocaleGroup: + def __init__(self, code: str, data: str): + self.code = code + self.data = data + + def __call__(self, code: str, default: Any = MISSING, **formats) -> str: + try: + base = self.data + for part in code.split("."): + base = base[part] + if isinstance(base, str): + return LocaleText(base.format(**formats)) + return LocaleGroup(self.code + "." + code, base) + except KeyError: + if default is MISSING: + return LocaleText(f"*{self.code}.{code}*") + return default + + +class LocaleText(str): + @property + def code(self): + return self.replace("*", "") + + def __call__(self, code: str, default: Any = MISSING, **formats): + if default is not MISSING: + return default + return LocaleText(self[0:-1] + "." + code + self[-1]) + + +def get_texts(locale: str) -> dict: + with open(get_locale(locale), "r", encoding="utf-8") as file: + return yaml.safe_load(file) + + +def get_locale(locale: str) -> str: locale = locale.value.split("-")[0] if os.path.exists(f"locale/{locale}.yml"): return f"locale/{locale}.yml" @@ -11,41 +49,42 @@ def get_locale(locale: discord.Locale) -> str: return "locale/en.yml" -def text(self: discord.Interaction, code: str, **formats) -> str: - try: - with open(get_locale(self.locale), "r", encoding="utf-8") as file: - texts = yaml.safe_load(file) - command = self.command - if isinstance(command, discord.app_commands.Command): - base = texts["chat_input"] - for name in command.qualified_name.split(" "): - base = base[name] - else: # Context - base = texts["context"][str(command)] - for code_key in code.split("."): - base = base[code_key] - return base.format(**formats) - except KeyError: - return f"*{code}*" +def interaction_text(self: discord.Interaction, code: str, **formats) -> str: + texts = get_texts(self.locale) + command = self.command + if code.split(".")[0] in texts: + pass + elif isinstance(command, discord.app_commands.Command): + code = "chat_input." + command.qualified_name.replace(" ", ".") + "." + code + else: # Context + code = "context." + str(command) + "." + code + return text(self.locale, code, **formats) + + +def interaction_guild_text(self: discord.Interaction, code: str, **formats) -> str: + texts = get_texts(self.guild_locale) + command = self.command + if code.split(".")[0] in texts: + pass + elif isinstance(command, discord.app_commands.Command): + code = "chat_input." + command.qualified_name.replace(" ", ".") + "." + code + else: # Context + code = "context." + str(command) + "." + code + return text(self.guild_locale, code, **formats) -def guild_text(self: discord.Interaction, code: str, **formats) -> str: +def text(lang: str, code: str, **formats) -> str: try: - with open(get_locale(self.guild_locale), "r", encoding="utf-8") as file: - texts = yaml.safe_load(file) - command = self.command - if isinstance(command, discord.app_commands.Command): - base = texts["chat_input"] - for name in command.qualified_name.split(" "): - base = base[name] - else: - base = texts["context"][str(self.command)] + texts = get_texts(lang) + base = texts for code_key in code.split("."): base = base[code_key] - return base.format(**formats) + if isinstance(base, str): + return LocaleText(base.format(**formats)) + return LocaleGroup(code, base) except KeyError: - return f"*{code}*" + return LocaleText(f"*{code}*") -discord.Interaction.text = text # Monkey patch -discord.Interaction.guild_text = guild_text +discord.Interaction.text = interaction_text # Monkey patch +discord.Interaction.guild_text = interaction_guild_text From 4b1134ab798b510cff3661591d5e2b0d3e9baae0 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Sat, 11 Jun 2022 14:48:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Add:=20=E6=A9=9F=E8=83=BD=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- create_index.py | 9 ++-- locale/ja.yml | 39 +++++++++----- main.py | 13 ++--- poetry.lock | 26 +++++----- pyproject.toml | 2 +- src/assets/global_chat.png | Bin 0 -> 8980 bytes src/core.py | 2 +- src/db/gc_room.py | 11 +++- src/embed.yml | 11 +++- src/exts/global_chat.py | 102 ++++++++++++++++++++++++------------- src/lang.py | 40 +++++++++++---- 11 files changed, 164 insertions(+), 91 deletions(-) create mode 100644 src/assets/global_chat.png diff --git a/create_index.py b/create_index.py index 8603e8d..7b4b96a 100644 --- a/create_index.py +++ b/create_index.py @@ -7,10 +7,11 @@ db_client = pymongo.MongoClient(getenv("MONGO_URI")) db = db_client[getenv("environment", "development")] db.gc_room.create_index( - [ - ("channels", pymongo.TEXT), - ], + "channels", + unique=True, +) +db.gc_room.create_index( + "name", unique=True, ) - print("Done.") diff --git a/locale/ja.yml b/locale/ja.yml index 3eaafb3..db25e50 100644 --- a/locale/ja.yml +++ b/locale/ja.yml @@ -41,7 +41,7 @@ chat_input: title: ":white_check_mark: 完了" description: | グローバルチャット`{name}`(`{id}`)に参加しました。 - joined_announce: + join_announce: title: ":inbox_tray: 参加" description: | {name}がグローバルチャットに参加しました。 @@ -51,19 +51,6 @@ chat_input: description: | このチャンネルは既にグローバルチャット{name}(`{id}`)に参加しています。 `/global deactivate`で退出できます。 - not_in: - title: ":x: 未参加" - description: | - このチャンネルはグローバルチャットに参加していません。 - `/global activate `で参加できます。 - deactivate_confirm: - title: ":warning: 退出しますか?" - description: | - グローバルチャット{name}(`{id}`)から退出しますか? - deactivated: - title: ":white_check_mark: 完了" - description: | - グローバルチャット{name}(`{id}`)から退出しました。 create: title: "グローバルチャットの作成" fields: @@ -79,6 +66,30 @@ chat_input: password: name: "パスワード" placeholder: "チャットのパスワード。省略すると誰でも入れるようになります。" + deactivate: + deactivate_confirm: + title: ":warning: 退出しますか?" + description: | + グローバルチャット{name}(`{id}`)から退出しますか? + deactivated: + title: ":white_check_mark: 完了" + description: | + グローバルチャット{name}(`{id}`)から退出しました。 + deactivated_deleted: + title: ":white_check_mark: 完了" + description: | + グローバルチャット{name}(`{id}`)から退出しました。 + 参加しているチャンネルが無くなったため、グローバルチャットは削除されました。 + not_in: + title: ":x: 未参加" + description: | + このチャンネルはグローバルチャットに参加していません。 + `/global activate `で参加できます。 + leave_announce: + title: ":outbox_tray: 退出" + description: | + {name}がグローバルチャットから退出しました。 + 現在{count}チャンネルが参加しています。 common: confirm: "はい" cancel: "いいえ" diff --git a/main.py b/main.py index b013ef4..f18ef41 100644 --- a/main.py +++ b/main.py @@ -9,21 +9,14 @@ def setup_logging(): - discord_logger = logging.getLogger("discord") - sevenbot_logger = logging.getLogger("SevenBot") - discord_logger.setLevel(logging.DEBUG) - sevenbot_logger.setLevel(logging.DEBUG) file_handler = logging.FileHandler(filename="sevenbot.log", encoding="utf-8", mode="w") - file_handler.setLevel(logging.INFO) + file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")) - discord_logger.addHandler(file_handler) - sevenbot_logger.addHandler(file_handler) - console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(ColoredFormatter(True)) - discord_logger.addHandler(console_handler) - sevenbot_logger.addHandler(console_handler) + + logging.basicConfig(handlers=[file_handler, console_handler], level=logging.DEBUG) def main(): diff --git a/poetry.lock b/poetry.lock index 4c2e6b7..8be8eb8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -121,7 +121,7 @@ dev = ["pytest (>=6.2.3)", "ipython", "mypy (>=0.710)", "hypothesis", "portray", [[package]] name = "discord.py" -version = "2.0.0a4227+geee65ac3" +version = "2.0.0a4311+g36f039a1" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -129,19 +129,19 @@ python-versions = ">=3.8.0" develop = false [package.dependencies] -aiohttp = ">=3.6.0,<4" +aiohttp = ">=3.7.4,<4" [package.extras] docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions"] -speed = ["orjson (>=3.5.4)"] +speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet"] test = ["coverage", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock"] voice = ["PyNaCl (>=1.3.0,<1.6)"] [package.source] type = "git" url = "https://github.com/Rapptz/discord.py.git" -reference = "eee65ac3" -resolved_reference = "eee65ac39b85edc0353402baa42ca7e72ec286d9" +reference = "36f039a1" +resolved_reference = "36f039a1bffb835a555be8a43976397ba6eb9c76" [[package]] name = "flake8" @@ -210,19 +210,19 @@ plugins = ["setuptools"] [[package]] name = "marshmallow" -version = "3.15.0" +version = "3.16.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -packaging = "*" +packaging = ">=17.0" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (==4.4.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.7)"] -lint = ["mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)"] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.960)", "flake8 (==4.0.1)", "flake8-bugbear (==22.4.25)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.5.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.8)"] +lint = ["mypy (==0.960)", "flake8 (==4.0.1)", "flake8-bugbear (==22.4.25)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -460,7 +460,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "bfed4e60fe029a6760a19ed2f02c40056b10728717277165d4109f96fc792c8e" +content-hash = "a59b9d3977894fdf698cfea7fec2742be7f4b1c235c60515c3f6ccd9a53ca8b0" [metadata.files] aiohttp = [ @@ -673,8 +673,8 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] marshmallow = [ - {file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"}, - {file = "marshmallow-3.15.0.tar.gz", hash = "sha256:2aaaab4f01ef4f5a011a21319af9fce17ab13bf28a026d1252adab0e035648d5"}, + {file = "marshmallow-3.16.0-py3-none-any.whl", hash = "sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1"}, + {file = "marshmallow-3.16.0.tar.gz", hash = "sha256:a762c1d8b2bcb0e5c8e964850d03f9f3bffd6a12b626f3c14b9d6b1841999af5"}, ] marshmallow-enum = [ {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, diff --git a/pyproject.toml b/pyproject.toml index 2c89a50..7fe5374 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["sevenc-nanashi "] [tool.poetry.dependencies] python = "^3.9" -"discord.py" = { git = "https://github.com/Rapptz/discord.py.git", rev = "eee65ac3" } +"discord.py" = { git = "https://github.com/Rapptz/discord.py.git", rev = "36f039a1" } GitPython = "^3.1.27" colorama = "^0.4.4" python-dotenv = "^0.20.0" diff --git a/src/assets/global_chat.png b/src/assets/global_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..fc2c479d62eb2d5cd9ecd04405f9657792511fb7 GIT binary patch literal 8980 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 S6KF6OfTo)6^O~pD@BRyb*4jM) literal 0 HcmV?d00001 diff --git a/src/core.py b/src/core.py index a34f738..1699529 100644 --- a/src/core.py +++ b/src/core.py @@ -20,7 +20,7 @@ def __init__(self): intents = discord.Intents.all() intents.typing = False super().__init__( - command_prefix=["sb#", "sb."], + command_prefix=[], help_command=None, strip_after_prefix=True, case_insensitive=True, diff --git a/src/db/gc_room.py b/src/db/gc_room.py index ffe2cbf..353c2de 100644 --- a/src/db/gc_room.py +++ b/src/db/gc_room.py @@ -1,4 +1,6 @@ from dataclasses import dataclass +from typing import Union + from dataclasses_json import dataclass_json @@ -9,9 +11,16 @@ class GlobalChatRoom: name: str channels: list[int] owner: int - password: str | None + password: Union[str, None] mute: list[int] rule: dict[str, str] slow: int description: str antispam: bool + + def except_channel(self, *channels: int) -> bool: + tmp_channels = self.channels.copy() + for channel in channels: + if channel in tmp_channels: + tmp_channels.remove(channel) + return tmp_channels diff --git a/src/embed.yml b/src/embed.yml index 901aa1f..5af1de9 100644 --- a/src/embed.yml +++ b/src/embed.yml @@ -16,7 +16,16 @@ chat_input: color: error join_success: color: success + join_announce: + color: success + deactivate: deactivate_confirm: color: prompt deactivated: - color: success \ No newline at end of file + color: success + deactivated_deleted: + color: success + leave_announce: + color: error + not_in: + color: error \ No newline at end of file diff --git a/src/exts/global_chat.py b/src/exts/global_chat.py index 48df93e..6bedee3 100644 --- a/src/exts/global_chat.py +++ b/src/exts/global_chat.py @@ -1,8 +1,9 @@ import asyncio import hashlib +import os import re from secrets import token_urlsafe -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import discord from discord import app_commands @@ -29,6 +30,8 @@ "zip": discord.Color.dark_gold(), "model": discord.Color.dark_magenta(), } +SYSTEM_MESSAGE = "System" +LOGO = open("src/assets/global_chat.png", "rb").read() class GlobalChat(Cog): @@ -37,16 +40,16 @@ class GlobalChat(Cog): def __init__(self, bot: "SevenBot"): super().__init__(bot) - self._channels_cache = set() + self.channels_cache = set() async def channels(self): - if self._channels_cache: - return self._channels_cache + if self.channels_cache: + return self.channels_cache async for gc_room in self.bot.db.gc_room.find(): - self._channels_cache.update(gc_room["channels"]) - return self._channels_cache + self.channels_cache.update(gc_room["channels"]) + return self.channels_cache - @group.command(name="activate", description="グローバルチャットを有効にします。") + @group.command(name="activate", description="グローバルチャットに接続します。") @app_commands.describe(gc_id="接続するグローバルチャットの名前。") @app_commands.rename(gc_id="id") async def activate(self, interaction: discord.Interaction, gc_id: str = "global"): @@ -65,7 +68,7 @@ async def activate(self, interaction: discord.Interaction, gc_id: str = "global" else: await self.join_gc_room(interaction, GlobalChatRoom.from_dict(gc_room)) - @group.command(name="deactivate", description="Deactivate global chat") + @group.command(name="deactivate", description="グローバルチャットから切断します。") async def deactivate(self, interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) current_room = await self.bot.db.gc_room.find_one({"channels": interaction.channel_id}) @@ -93,11 +96,35 @@ async def deactivate(self, interaction: discord.Interaction): interaction.text("common.canceled"), ephemeral=True, ) - await self.bot.db.gc_room.delete_one({"id": current_room.id}) - self._channels_cache.remove(interaction.channel_id) + try: + self.channels_cache.remove(interaction.channel_id) + except KeyError: + pass + if len(current_room.channels) == 1: + await self.bot.db.gc_room.delete_one({"id": current_room.id}) + embed = LocaleEmbed(interaction.text("deactivated_deleted"), name=current_room.name, id=current_room.id) + else: + await self.bot.db.gc_room.update_one( + {"id": current_room.id}, {"$pull": {"channels": interaction.channel_id}} + ) + current_room.channels.remove(interaction.channel_id) + announce_embed = LocaleEmbed( + interaction.text("leave_announce"), name=interaction.guild.name, count=len(current_room.channels) + ) + if interaction.guild.icon: + announce_embed.set_thumbnail(url=interaction.guild.icon.url) + await self.multi_send( + current_room, + current_room.channels, + embed=announce_embed, + username=SYSTEM_MESSAGE, + ) + embed = LocaleEmbed(interaction.text("deactivated"), name=current_room.name, id=current_room.id) + webhook = await self.get_webhook(interaction.channel, current_room, create=False) + if webhook is not None: + await webhook.delete() await interaction.edit_original_message( - embed=LocaleEmbed(interaction.text("deactivated"), name=current_room.name, id=current_room.id), - ephemeral=True, + embed=embed, view=None, ) @@ -144,7 +171,7 @@ async def create_gc_room(self, interaction: discord.Interaction, gc_id: str): ephemeral=True, ) await self.bot.db.gc_room.insert_one(room.to_dict()) - self._channels_cache.add(interaction.channel_id) + self.channels_cache.add(interaction.channel_id) await modal_interaction.edit_original_message( embed=LocaleEmbed( interaction.text("create_success"), @@ -200,44 +227,46 @@ async def join_gc_room(self, interaction: discord.Interaction, gc_room: GlobalCh {"id": gc_room.id}, {"$addToSet": {"channels": interaction.channel_id}}, ) - self._channels_cache.add(interaction.channel_id) - embed = LocaleEmbed( - interaction.text("join_announce"), name=interaction.guild.name, count=len(gc_room.channels) + 1 - ) + self.channels_cache.add(interaction.channel_id) + gc_room.channels.append(interaction.channel_id) + embed = LocaleEmbed(interaction.text("join_announce"), name=interaction.guild.name, count=len(gc_room.channels)) if interaction.guild.icon: embed.set_thumbnail(url=interaction.guild.icon.url) await self.multi_send( - gc_room, - embed=embed, + gc_room, gc_room.except_channel(interaction.channel_id), embed=embed, username=SYSTEM_MESSAGE ) - await final_interaction.response.send_message( - embed=LocaleEmbed(interaction.text("join_success")), - ephemeral=True, + await final_interaction.edit_original_message( + embed=LocaleEmbed(interaction.guild_text("join_success"), name=gc_room.name, id=gc_room.id), + view=None, ) - async def multi_send(self, gc_room: GlobalChatRoom, **kwargs): + async def multi_send(self, gc_room: GlobalChatRoom, channels: list[int], **kwargs): def get_coroutine(channel_id: int): channel = self.bot.get_channel(channel_id) if channel is None: return - self.single_send(gc_room, channel, **kwargs) + return self.single_send(gc_room, channel, **kwargs) - await asyncio.gather(filter(lambda c: c, map(get_coroutine, gc_room.channels))) + await asyncio.gather(*filter(lambda c: c, map(get_coroutine, channels))) async def single_send(self, gc_room: GlobalChatRoom, channel: discord.TextChannel, **kwargs): - webhook = await self.get_webhook(channel, gc_room) + webhook = await self.get_webhook(channel, gc_room, create=True) if webhook is None: return - await webhook.send(**kwargs) + await webhook.send(**kwargs, allowed_mentions=discord.AllowedMentions.none(), wait=True) - async def get_webhook(self, channel: discord.TextChannel, gc_room: GlobalChatRoom): + async def get_webhook( + self, channel: discord.TextChannel, gc_room: GlobalChatRoom, create: bool = False + ) -> Optional[discord.Webhook]: webhook = await channel.webhooks() name = f"sevenbot-global-webhook-{gc_room.id}" for w in webhook: if w.name == name: return w + if not create: + return None try: - return await channel.create_webhook(name=name) + return await channel.create_webhook(name=name, avatar=LOGO) except discord.HTTPException: return None @@ -245,15 +274,15 @@ async def get_webhook(self, channel: discord.TextChannel, gc_room: GlobalChatRoo async def on_message_global(self, message: discord.Message): if message.channel.id not in await self.channels(): return - db_data = await self.bot.db.gc_room.find_one({"channel": message.channel.id}) + db_data = await self.bot.db.gc_room.find_one({"channels": message.channel.id}) if db_data is None: return gc_room = GlobalChatRoom.from_dict(db_data) - name = message.author.display_name + name = str(message.author) suffix = f"(From {message.guild.name}, ID: {message.author.id})" - if len(name) + len(suffix) > 32: - name = name[: 32 - len(suffix)] - name += suffix + if len(name + " " + suffix) > 80: + name = name[: 80 - len(suffix)] + name += " " + suffix embeds = [] for attachment in message.attachments: if attachment.content_type: @@ -270,6 +299,7 @@ async def on_message_global(self, message: discord.Message): embeds.append(embed) await self.multi_send_filtered( gc_room, + gc_room.except_channel(message.channel.id), content=message.clean_content, username=name, avatar_url=message.author.display_avatar, @@ -286,11 +316,11 @@ def get_size(self, size: int): else: return f"{size / 1073741824} GiB" - async def multi_send_filtered(self, gc_room: GlobalChatRoom, content, **kwargs): + async def multi_send_filtered(self, gc_room: GlobalChatRoom, channels: list[int], content, **kwargs): if len(content.splitlines()) > 10: content = "\n".join(content.splitlines()[:10]) + "\n..." content = INVITE_PATTERN.sub("", content) - await self.multi_send(gc_room, content=content, **kwargs) + await self.multi_send(gc_room, channels, content=content, **kwargs) class CreateModal(Modal): diff --git a/src/lang.py b/src/lang.py index 58f7e32..62132ce 100644 --- a/src/lang.py +++ b/src/lang.py @@ -1,3 +1,4 @@ +from logging import getLogger import os from typing import Any import discord @@ -6,26 +7,42 @@ from src.common.missing import MISSING +logger = getLogger("lang") + + +class DefaultMap(dict): + def __missing__(self, key): + return key.join("{}") + + class LocaleGroup: - def __init__(self, code: str, data: str): + def __init__(self, code: str, data: str, lang: str): self.code = code self.data = data + self.lang = lang def __call__(self, code: str, default: Any = MISSING, **formats) -> str: try: base = self.data for part in code.split("."): base = base[part] - if isinstance(base, str): - return LocaleText(base.format(**formats)) - return LocaleGroup(self.code + "." + code, base) except KeyError: if default is MISSING: - return LocaleText(f"*{self.code}.{code}*") + logger.warning(f"Missing text in LocaleGroup: {self.lang}/{self.code}.{code}") + return LocaleText(f"*{self.code}.{code}*", self.lang) return default + else: + if isinstance(base, str): + return LocaleText(base.format_map(DefaultMap(formats)), self.lang) + return LocaleGroup(self.code + "." + code, base, self.lang) class LocaleText(str): + def __new__(cls, text: str, lang: str): + obj = str.__new__(cls, text) + obj.lang = lang + return obj + @property def code(self): return self.replace("*", "") @@ -33,7 +50,8 @@ def code(self): def __call__(self, code: str, default: Any = MISSING, **formats): if default is not MISSING: return default - return LocaleText(self[0:-1] + "." + code + self[-1]) + logger.warning(f"Missing text in LocaleText: {self.lang}/{self.code}.{code}") + return LocaleText(f"*{self.code}.{code}*", self.lang) def get_texts(locale: str) -> dict: @@ -79,11 +97,13 @@ def text(lang: str, code: str, **formats) -> str: base = texts for code_key in code.split("."): base = base[code_key] - if isinstance(base, str): - return LocaleText(base.format(**formats)) - return LocaleGroup(code, base) except KeyError: - return LocaleText(f"*{code}*") + logger.warning(f"Missing text: {lang}/{code}") + return LocaleText(f"*{code}*", lang) + else: + if isinstance(base, str): + return LocaleText(base.format_map(DefaultMap(formats)), lang) + return LocaleGroup(code, base, lang) discord.Interaction.text = interaction_text # Monkey patch From 46f1ce27a1ba4440e2dd3a0dac46315fdc2334d1 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Sat, 9 Jul 2022 20:36:20 +0900 Subject: [PATCH 3/3] Add: Add Sending --- emojis/check.png | Bin 0 -> 2766 bytes emojis/clock.png | Bin 0 -> 5600 bytes main.py | 4 +++- poetry.lock | 8 ++++---- pyproject.toml | 2 +- src/core.py | 9 ++++++++- src/exts/global_chat.py | 20 ++++++++++++++------ 7 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 emojis/check.png create mode 100644 emojis/clock.png diff --git a/emojis/check.png b/emojis/check.png new file mode 100644 index 0000000000000000000000000000000000000000..cea572c4f03df2ae82016eb98833f0943e401ff9 GIT binary patch literal 2766 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 WXk7_&BnkOJ1?;SytSa|Y>Hh{UB$gEb literal 0 HcmV?d00001 diff --git a/emojis/clock.png b/emojis/clock.png new file mode 100644 index 0000000000000000000000000000000000000000..e7c32e7feaa3240267c5ec02a1f1420fd6babb5f GIT binary patch literal 5600 zcmbtYhf~wd)BmPmLJ!SQB_O?oDjI6&(vc!vnh~WVRe=ziG(~z>qKJTWlqLuP0@6eU zDN64?NQnZ9K;XsqU-;e3&CPzc-R$kn?QN2|sXikeHyr=~j0RVAEC2w6{Wsy~&X5mj zrLWEwt{`38AWOenL7}d9ae(GcKQ|o8z{k}CXMuCQc`sl9rwRZZF$OxCR$<>ZpQr`$ z=(9e6y@A9Z!0bIn7;qyk##sf&G++g1A$k9DLA%r%_Cy95Rt_g8ohxriAe-bO+7lZ4 z2e|^O`k~V|dtz=!QXpev3cIS-VnwzdfXXy=dwQ1(e72GQX+#vUm;iEl>f!2KbA6L1 zGjkM;of>L*z~Qc0pgjiyOF{WoCr|5~zNy&ZXL}d?n)8N{TtxLwAjd#z8w8H3viz>| z<-$f=JgM3UwHhq21HXtO_*>Q=`RZ;OKA#*t4Ao&lV4-Hv+VNkF)o-|zrAt(xETFgs znp&>8CTmL5o%F}@mYPM3F;io_aMX(QeTzQzYh7Nq(;ae$1ruvIqxAp*na|;`aMI74 z)#b8S|3c+-08m+i0+~yYkY=^lClR*vTjb4ERvo~s+q!r$W(^B;)sQ)U@{9?hZObKJ z=$skIpqMlc``lri46?saJjxD7&2$bwd9{4zrRsa5s(Pua9L(Gx&Zn)o4%Uyrc(sv2
  • &c}=esDcANCchnoMhlEpWk)dwfaS$gOup=o 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 boYHUty3MsW{HP-A|EwA4n(EYOxy1Y*Y%Tk8 literal 0 HcmV?d00001 diff --git a/main.py b/main.py index f18ef41..16afc1a 100644 --- a/main.py +++ b/main.py @@ -16,7 +16,9 @@ def setup_logging(): console_handler.setLevel(logging.INFO) console_handler.setFormatter(ColoredFormatter(True)) - logging.basicConfig(handlers=[file_handler, console_handler], level=logging.DEBUG) + logging.root.setLevel(logging.DEBUG) + logging.root.addHandler(file_handler) + logging.root.addHandler(console_handler) def main(): diff --git a/poetry.lock b/poetry.lock index 8be8eb8..46bc6cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -121,7 +121,7 @@ dev = ["pytest (>=6.2.3)", "ipython", "mypy (>=0.710)", "hypothesis", "portray", [[package]] name = "discord.py" -version = "2.0.0a4311+g36f039a1" +version = "2.0.0a4355+g2b9e43db" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -140,8 +140,8 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"] [package.source] type = "git" url = "https://github.com/Rapptz/discord.py.git" -reference = "36f039a1" -resolved_reference = "36f039a1bffb835a555be8a43976397ba6eb9c76" +reference = "2b9e43db" +resolved_reference = "2b9e43dbf94730a9fd3ef4da005c6e8c5d826e74" [[package]] name = "flake8" @@ -460,7 +460,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "a59b9d3977894fdf698cfea7fec2742be7f4b1c235c60515c3f6ccd9a53ca8b0" +content-hash = "caa3f556caa51acd6c4544edbd80b04a2d39129502dc61e39cced1ed82d33602" [metadata.files] aiohttp = [ diff --git a/pyproject.toml b/pyproject.toml index 7fe5374..cf22876 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["sevenc-nanashi "] [tool.poetry.dependencies] python = "^3.9" -"discord.py" = { git = "https://github.com/Rapptz/discord.py.git", rev = "36f039a1" } +"discord.py" = { git = "https://github.com/Rapptz/discord.py.git", rev = "2b9e43db" } GitPython = "^3.1.27" colorama = "^0.4.4" python-dotenv = "^0.20.0" diff --git a/src/core.py b/src/core.py index 1699529..01f96b1 100644 --- a/src/core.py +++ b/src/core.py @@ -68,7 +68,14 @@ async def watch_files(self) -> None: def run(self) -> None: """SevenBotを起動します。""" self.logger.info("Starting SevenBot...") - super().run(os.environ["TOKEN"]) + super().run(os.environ["TOKEN"], log_handler=None) + + def emoji(self, name: str): + return discord.utils.get(sum([list(g.emojis) for g in self.emoji_guilds], []), name=name) + + @property + def emoji_guilds(self) -> list[discord.Guild]: + return [g for g in self.guilds if str(g.id) in os.environ["EMOJI_GUILDS"].split(",")] def setup(self) -> None: pass diff --git a/src/exts/global_chat.py b/src/exts/global_chat.py index 6bedee3..2b0ba84 100644 --- a/src/exts/global_chat.py +++ b/src/exts/global_chat.py @@ -1,6 +1,5 @@ import asyncio import hashlib -import os import re from secrets import token_urlsafe from typing import TYPE_CHECKING, Optional @@ -41,6 +40,7 @@ class GlobalChat(Cog): def __init__(self, bot: "SevenBot"): super().__init__(bot) self.channels_cache = set() + self.send_semaphore = asyncio.Semaphore(100) async def channels(self): if self.channels_cache: @@ -240,20 +240,23 @@ async def join_gc_room(self, interaction: discord.Interaction, gc_room: GlobalCh view=None, ) - async def multi_send(self, gc_room: GlobalChatRoom, channels: list[int], **kwargs): + async def multi_send(self, gc_room: GlobalChatRoom, channels: list[int], **kwargs) -> list[discord.WebhookMessage]: def get_coroutine(channel_id: int): channel = self.bot.get_channel(channel_id) if channel is None: return return self.single_send(gc_room, channel, **kwargs) - await asyncio.gather(*filter(lambda c: c, map(get_coroutine, channels))) + return await asyncio.gather(*filter(lambda c: c, map(get_coroutine, channels))) - async def single_send(self, gc_room: GlobalChatRoom, channel: discord.TextChannel, **kwargs): + async def single_send( + self, gc_room: GlobalChatRoom, channel: discord.TextChannel, **kwargs + ) -> discord.WebhookMessage: webhook = await self.get_webhook(channel, gc_room, create=True) if webhook is None: return - await webhook.send(**kwargs, allowed_mentions=discord.AllowedMentions.none(), wait=True) + async with self.send_semaphore: + return await webhook.send(**kwargs, allowed_mentions=discord.AllowedMentions.none(), wait=True) async def get_webhook( self, channel: discord.TextChannel, gc_room: GlobalChatRoom, create: bool = False @@ -297,6 +300,7 @@ async def on_message_global(self, message: discord.Message): if content_type.startswith("image/"): embed.set_image(url=attachment.url) embeds.append(embed) + self.bot.loop.create_task(message.add_reaction(self.bot.emoji("clock"))) await self.multi_send_filtered( gc_room, gc_room.except_channel(message.channel.id), @@ -305,6 +309,10 @@ async def on_message_global(self, message: discord.Message): avatar_url=message.author.display_avatar, embeds=embeds, ) + self.bot.loop.create_task(message.remove_reaction(self.bot.emoji("clock"), self.bot.user)) + self.bot.loop.create_task(message.add_reaction(self.bot.emoji("check"))) + await asyncio.sleep(3) + await message.remove_reaction(self.bot.emoji("check"), self.bot.user) def get_size(self, size: int): if size < 1024: @@ -320,7 +328,7 @@ async def multi_send_filtered(self, gc_room: GlobalChatRoom, channels: list[int] if len(content.splitlines()) > 10: content = "\n".join(content.splitlines()[:10]) + "\n..." content = INVITE_PATTERN.sub("", content) - await self.multi_send(gc_room, channels, content=content, **kwargs) + return await self.multi_send(gc_room, channels, content=content, **kwargs) class CreateModal(Modal):