├── .github └── workflows │ ├── check_syntax.yml │ ├── dependency-review.yml │ └── python-app.yml ├── .gitignore ├── LICENSE ├── auth.template.json ├── bin ├── libopus-0.x64.dll └── libopus-0.x86.dll ├── cogs ├── RT │ ├── __init__.py │ ├── help.py │ ├── language.py │ ├── news.py │ ├── prefix.py │ ├── rocations.py │ ├── rtrole.py │ └── websockets.py ├── _first.py ├── _sub.py ├── admin │ ├── __init__.py │ ├── database.py │ ├── develop.py │ ├── logger.py │ └── rtlife.py ├── channelplugin │ ├── __init__.py │ ├── autopublic.py │ ├── channel_plugin.py │ └── log.py ├── entertainment │ ├── 6ch.py │ ├── __init__.py │ ├── funp.py │ ├── gamesearch.py │ ├── minesweeper.py │ ├── qr.py │ └── today.py ├── individual │ ├── __init__.py │ ├── _short_url.py │ ├── afk.py │ ├── onlinenotice.py │ ├── person.py │ ├── reprypt.py │ ├── schedule.py │ ├── short_url.py │ ├── tenki.py │ ├── tools.py │ ├── transit.py │ ├── translator.py │ ├── url_checker.py │ └── watch.py ├── music │ ├── __init__.py │ ├── music.py │ ├── player.py │ ├── playlist.py │ └── views.py ├── other │ ├── __init__.py │ ├── github.py │ ├── oldcaptcha │ │ ├── __init__.py │ │ ├── image_captcha.py │ │ ├── web_captcha.py │ │ └── word_captcha.py │ ├── rtfm.py │ ├── token_remover.py │ └── topgg.py ├── serverpanel │ ├── __init__.py │ ├── _oldrole.py │ ├── delay_lottery.py │ ├── free_channel.py │ ├── nickname.py │ ├── original_menu_message.py │ ├── poll.py │ ├── recruitment.py │ ├── role.py │ └── ticket.py ├── serversafety │ ├── __init__.py │ ├── automod │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── data_manager.py │ │ └── modutils.py │ ├── blocker.py │ ├── captcha │ │ ├── __init__.py │ │ ├── click.py │ │ ├── image.py │ │ ├── web.py │ │ └── word.py │ ├── gban.py │ ├── link_blocker.py │ ├── moderation.py │ ├── ngnickname.py │ ├── ngword.py │ └── noicon_notice.py ├── servertool │ ├── __init__.py │ ├── auto_role.py │ ├── bulk.py │ ├── bump.py │ ├── delay_delete.py │ ├── delay_role.py │ ├── force_pinned_message.py │ ├── locker.py │ ├── role_keeper.py │ ├── role_message.py │ ├── thread_manager │ │ ├── __init__.py │ │ ├── constants.py │ │ └── dataclass.py │ ├── voice_channel.py │ └── welcome.py ├── serveruseful │ ├── __init__.py │ ├── channel_status.py │ ├── embed.py │ ├── expander.py │ ├── globalchat.py │ ├── level.py │ ├── original_command.py │ ├── require_send.py │ ├── role_linker.py │ ├── rta.py │ ├── stamp.py │ ├── twitter.py │ ├── vcnt.py │ └── voice_role.py └── tts │ ├── __init__.py │ ├── agents.py │ ├── data │ ├── allowed_characters.csv │ └── avaliable_voices.json │ ├── lib │ └── AquesTalk │ │ └── aquestalk.c │ ├── manager.py │ ├── readme.md │ └── voice.py ├── contributing ├── about_cogs.md ├── about_util.md ├── guide.md ├── readme.md ├── sikumi.md └── syntax.md ├── data ├── __init__.py ├── area_code.json ├── captcha │ ├── SourceHanSans-Normal.otf │ └── readme.md ├── headers.py ├── images │ └── game_maker │ │ ├── ps4_base.png │ │ ├── ps4_mask.png │ │ ├── switch_base.png │ │ └── switch_mask.png ├── julius_dict │ └── README.md └── replies.json ├── log └── readme.md ├── main.py ├── readme.ja.md ├── readme.md ├── requirements.txt ├── run.sh ├── sub.py ├── test_all.py └── util ├── __init__.py ├── bot.py ├── cacher.py ├── checks.py ├── converters.py ├── data_manager.py ├── db.py ├── debug.py ├── dochelp.py ├── docparser.py ├── dpy_monkey.py ├── ext ├── __init__.py ├── on_cog_add.py ├── on_full_reaction.py ├── on_send.py └── view.py ├── lib_data_manager.py ├── markdowns.py ├── minesweeper.py ├── mysql_manager.py ├── olds.py ├── page.py ├── record.py ├── rtws.py ├── securl.py ├── settings.py ├── slash.py ├── types.py ├── views.py ├── webhooks.py ├── websocket.py └── whats_new.md /.github/workflows/check_syntax.yml: -------------------------------------------------------------------------------- 1 | name: Syntax Checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.10" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install flake8 24 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 25 | - name: Lint with flake8 26 | run: flake8 . --count --ignore=W291,W503,W504 --max-line-length=150 --statistics 27 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v1 21 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 3.10 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.10" 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.crt 3 | *.key 4 | *.save 5 | *.sublime-project 6 | *.*-workspace 7 | *.*~ 8 | tmp 9 | log 10 | .vscode 11 | .replit 12 | .DS_Store 13 | .mypy_cache 14 | 15 | cogs/tts/lib 16 | cogs/tts/dic 17 | cogs/tts/outputs 18 | !cogs/tts/outputs/readme.md 19 | cogs/tts/routine 20 | youtube-cookies.txt 21 | data/cookies.txt 22 | data/*.json 23 | data/vcnterrors 24 | data/julius_dict 25 | auth.json 26 | 27 | \#*\# 28 | !cogs/tts/lib/AquesTalk/aquestalk.c 29 | rt_module 30 | __* 31 | eng2kana.json 32 | data/rtlife.json 33 | 34 | .python-version 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 4-Clause License 2 | 3 | Copyright (c) 2022 RT-Team, Free-RT. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 3. All advertising materials mentioning features or use of this software must display the following acknowledgement: 10 | This product includes software developed by the organization. 11 | 4. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /auth.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": { 3 | "test": "テスト用BotのTOKEN、実行時に引数にtestと渡された際に使用されます。", 4 | "production": "本番環境用のTOKEN", 5 | "sub": "りつたんのTOKEN" 6 | }, 7 | "secret": "シークレットキー, テスト時ならなんだっていい。本番の場合は長い方が良い。", 8 | "topgg": "TopGGのTOKENです。テスト用Botのみ入力省略可です。", 9 | "mysql": { 10 | "user": "データベースのユーザー名", "password": "⇦のパスワード", "db": "データベース名", 11 | "port": ポート, "host": "データベースのアドレス、テストなら普通`localhost`" 12 | }, 13 | "twitter": { 14 | "consumer_key": "TwitterのAPIのコンシューマーキー、以下もTwitterのもの。入力しない場合は`twitter`キーごと削除しましょう。", 15 | "consumer_secret": "...", 16 | "access_token": "...", 17 | "access_token_secret": "..." 18 | } 19 | } -------------------------------------------------------------------------------- /bin/libopus-0.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/bin/libopus-0.x64.dll -------------------------------------------------------------------------------- /bin/libopus-0.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/bin/libopus-0.x86.dll -------------------------------------------------------------------------------- /cogs/RT/news.py: -------------------------------------------------------------------------------- 1 | # Free RT - News 2 | 3 | from datetime import datetime 4 | from time import time 5 | 6 | from discord.ext import commands 7 | import discord 8 | 9 | from util.mysql_manager import DatabaseManager 10 | from util.page import EmbedPage 11 | 12 | 13 | class DataManager(DatabaseManager): 14 | def __init__(self, db): 15 | self.db = db 16 | 17 | async def init_table(self, cursor) -> None: 18 | await cursor.create_table( 19 | "news", { 20 | "id": "BIGINT", "time": "TEXT", 21 | "content": "TEXT", "image": "TEXT" 22 | } 23 | ) 24 | 25 | async def add_news(self, cursor, content: str, image: str) -> float: 26 | # Newsを新しく追加します。 27 | now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 28 | t = int(time()) 29 | await cursor.insert_data( 30 | "news", {"id": t, "time": now, "content": content, "image": image} 31 | ) 32 | return t 33 | 34 | async def remove_news(self, cursor, id_: int) -> None: 35 | # Newsを削除します。 36 | await cursor.delete("news", {"id": id_}) 37 | 38 | async def get_news(self, cursor, id_: int) -> tuple: 39 | # Newsを取得する。 40 | return (await cursor.get_data("news", {"id": id_})) 41 | 42 | async def get_news_all(self, cursor) -> list: 43 | # Newsを全て取得する。 44 | return [row async for row in cursor.get_datas( 45 | "news", {}, custom="ORDER BY id DESC") 46 | if row] 47 | 48 | 49 | class News(commands.Cog, DataManager): 50 | def __init__(self, bot): 51 | self.bot, self.rt = bot, bot.data 52 | 53 | async def cog_load(self): 54 | super(commands.Cog, self).__init__(self.bot.mysql) 55 | await self.init_table() 56 | 57 | async def get_rows(self) -> list: 58 | return reversed(await self.get_news_all()) 59 | 60 | def convert_embed(self, doc: str) -> discord.Embed: 61 | # マークダウンをEmbedにする。 62 | i = doc.find("\n") 63 | title = doc if i == -1 else doc[:i] 64 | description = doc[i:(i := doc.find("## "))] 65 | embed = discord.Embed( 66 | title=title[2:] if title.startswith("# ") else title, 67 | description=description 68 | ) 69 | values = doc[i:].replace("### ", "**#** ").split("## ") 70 | for value in values: 71 | if value: 72 | name = value[:(i := value.find("\n"))] 73 | name = "?\n" + name 74 | embed.add_field(name=name, value=value[i:], inline=False) 75 | return embed 76 | 77 | @commands.group( 78 | extras={ 79 | "headding": { 80 | "en": "Show RT's news.", 81 | "ja": "RTのニュースを表示します。" 82 | }, 83 | "parent": "RT" 84 | } 85 | ) 86 | async def news(self, ctx): 87 | """!lang ja 88 | -------- 89 | 最新のニュースを表示します。 90 | `rt!news`だけでいいです。 91 | 下にあるadd/removeはRT管理者のみ実行可能です。 92 | 93 | !lang en 94 | -------- 95 | Show RT's news.""" 96 | if not ctx.invoked_subcommand: 97 | embeds, i = [], 0 98 | for row in await self.get_news_all(): 99 | i += 1 100 | embed = self.convert_embed(row[2]) 101 | embed.title = f"{embed.title}" 102 | embed.set_footer(text=f"{row[1]} | ID:{row[0]}") 103 | if row[3] != "None": 104 | embed.set_image(url=row[3]) 105 | embeds.append(embed) 106 | if i == 10: 107 | break 108 | if embeds: 109 | await ctx.reply( 110 | "**最新のRTニュース**", embed=embeds[0], view=EmbedPage(data=embeds) 111 | ) 112 | else: 113 | await ctx.reply("Newsは現在空です。") 114 | 115 | @commands.is_owner() 116 | @news.command() 117 | async def add(self, ctx, *, content): 118 | """!lang ja 119 | -------- 120 | ニュースに新しくなにかを追加します。 121 | 122 | Parameters 123 | ---------- 124 | image : str 125 | 写真のURLです。 126 | 写真がないのなら`None`を入れてください。 127 | content : str 128 | ニュースに追加する文字列です。""" 129 | now = await self.add_news(content, "None") 130 | await ctx.reply(f"Ok number:{now}") 131 | 132 | @commands.is_owner() 133 | @news.command() 134 | async def remove(self, ctx, id_: int): 135 | """!lang ja 136 | -------- 137 | ニュースを削除します。 138 | 139 | Parameters 140 | ---------- 141 | id : int 142 | 削除するニュースのidです。""" 143 | await self.remove_news(id_) 144 | await ctx.reply("Ok") 145 | 146 | 147 | async def setup(bot): 148 | await bot.add_cog(News(bot)) 149 | -------------------------------------------------------------------------------- /cogs/RT/prefix.py: -------------------------------------------------------------------------------- 1 | # free RT - custom prefix 2 | 3 | from typing import Literal 4 | 5 | from discord.ext import commands 6 | from discord import app_commands 7 | import discord 8 | 9 | from util import db, RT 10 | 11 | 12 | class PrefixDB(db.DBManager): 13 | def __init__(self, bot: RT): 14 | self.bot = bot 15 | 16 | @db.command() 17 | async def set_guild(self, cursor, id_: int, prefix: str) -> None: 18 | "サーバープレフィックスを設定します。" 19 | if id_ in self.bot.guild_prefixes: 20 | await cursor.execute( 21 | "UPDATE GuildPrefix SET Prefix=%s WHERE GuildID=%s", 22 | (prefix, id_,)) 23 | else: 24 | await cursor.execute("INSERT INTO GuildPrefix VALUES (%s, %s)", (id_, prefix,)) 25 | 26 | self.bot.guild_prefixes[id_] = prefix 27 | 28 | @db.command() 29 | async def set_user(self, cursor, id_: int, prefix: str) -> None: 30 | "ユーザープレフィックスを設定します。" 31 | if id_ in self.bot.user_prefixes: 32 | await cursor.execute("UPDATE GuildPrefix SET Prefix=%s WHERE GuildID=%s", 33 | (prefix, id_,)) 34 | else: 35 | await cursor.execute("INSERT INTO GuildPrefix VALUES (%s, %s)", (id_, prefix,)) 36 | 37 | self.bot.user_prefixes[id_] = prefix 38 | 39 | async def manager_load(self, cursor) -> None: 40 | # テーブルの準備をし、データをメモリに保存しておく。 41 | await cursor.execute( 42 | """CREATE TABLE IF NOT EXISTS UserPrefix ( 43 | UserID BIGINT, Prefix TEXT)""" 44 | ) 45 | await cursor.execute("SELECT * FROM UserPrefix") 46 | self.bot.user_prefixes = dict(await cursor.fetchall()) 47 | 48 | # サーバーprefix 49 | await cursor.execute( 50 | """CREATE TABLE IF NOT EXISTS GuildPrefix ( 51 | GuildID BIGINT, Prefix TEXT)""" 52 | ) 53 | await cursor.execute("SELECT * FROM GuildPrefix") 54 | self.bot.guild_prefixes = dict(await cursor.fetchall()) 55 | 56 | 57 | class CustomPrefix(commands.Cog): 58 | def __init__(self, bot): 59 | self.bot = bot 60 | 61 | async def cog_load(self): 62 | self.manager = await self.bot.add_db_manager(PrefixDB(self.bot)) 63 | 64 | @commands.hybrid_command( 65 | aliases=["p", "プレフィックス", "プリフィックス", "接頭辞"], 66 | extras={ 67 | "headding": {"ja": "カスタムプレフィックスを設定します。", "en": "Set custom prefix."}, 68 | "parent": "RT" 69 | } 70 | ) 71 | @app_commands.describe(mode="サーバーかユーザーか", new_prefix="設定する新しいプレフィックス") 72 | async def prefix(self, ctx, mode: Literal["server", "user"] = None, new_prefix: str = ""): 73 | """!lang ja 74 | -------- 75 | カスタムプレフィックスの登録・変更・削除をします。 76 | 登録をしても元々の`rf!`でも動作します。 77 | また、サーバー用・ユーザー用カスタムプレフィックスが両方指定されている場合はどちらでも動作します。 78 | 79 | Parameters 80 | ---------- 81 | mode: `server`か`user`, optional 82 | `server`だとサーバー全体で、`user`だと個人で設定できます。 83 | 指定しない場合は現在の設定を見ることができます。 84 | new_prefix: str, optional 85 | 新しく設定するプレフィックスです。 86 | modeを指定してここを指定しなかった場合はカスタムプレフィックスを削除します。 87 | 88 | !lang en 89 | -------- 90 | Set/Change/Delete custom prefix. 91 | `rf!` will be still working ever if it is set. 92 | 93 | Parameters 94 | ---------- 95 | mode: `server` or `user`, optional 96 | If `server`, it sets to all the server members. If `user`, it sets to you only. 97 | If nothing here, you can view the custom prefix settings. 98 | new_prefix: str, optional 99 | Prefix that you want to set. 100 | If mode is set and here is nothing, custom prefix will be deleted. 101 | """ 102 | modes_ja = {"server": "サーバー", "user": "ユーザー"} 103 | if not mode: 104 | # 現在の情報を表示する。 105 | await ctx.send( 106 | embed=discord.Embed( 107 | title={"ja": "現在のprefix設定を見る", "en": "View prefix settings"}, 108 | description={ 109 | k: f"{j[0]} : `{self.bot.guild_prefixes.get(ctx.guild.id, '')}`\n" if ctx.guild else "" 110 | + f"{j[1]} : `{self.bot.user_prefixes.get(ctx.author.id, '')}`" 111 | for k, j in { 112 | "ja": modes_ja.values(), "en": modes_ja.keys() 113 | }.items() 114 | })) 115 | 116 | if mode == "server" and not ctx.guild: 117 | raise commands.NoPrivateMessage() 118 | if mode == "server" and not ctx.author.guild_permissions.administrator: 119 | raise commands.MissingPermissions(["administrator"]) 120 | 121 | await getattr(self.manager, f"set_{mode}").run( 122 | ctx.author.id if mode == "user" else ctx.guild.id, 123 | new_prefix 124 | ) 125 | if new_prefix == "": 126 | await ctx.send({ 127 | "ja": f"{modes_ja.get(mode)}のカスタムプレフィックスを削除しました。", 128 | "en": f"Deleted {mode.upper()} custom prefix." 129 | }) 130 | await ctx.send({ 131 | "ja": f"{modes_ja.get(mode)}のカスタムプレフィックスを{new_prefix}に変更しました。", 132 | "en": f"Changed {mode.upper()} custom prefix to {new_prefix}." 133 | }) 134 | 135 | 136 | async def setup(bot: RT): 137 | await bot.add_cog(CustomPrefix(bot)) 138 | -------------------------------------------------------------------------------- /cogs/RT/rtrole.py: -------------------------------------------------------------------------------- 1 | # Free RT - Rt Role 2 | 3 | from discord.ext import commands 4 | from discord import app_commands 5 | import discord 6 | 7 | from aiofiles import open as async_open 8 | from collections import defaultdict 9 | from ujson import loads, dumps 10 | from os.path import exists 11 | 12 | 13 | class RTRole(commands.Cog): 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.data = defaultdict(dict) 17 | if exists("data/rtrole.json"): 18 | try: 19 | with open("data/rtrole.json", "r") as f: 20 | self.data.update(loads(f.read())) 21 | except Exception as e: 22 | print("Error on RTRole:", e) 23 | else: 24 | with open("data/rtrole.json", "w") as f: 25 | f.write(r"{}") 26 | 27 | if not getattr(self, "did", False): 28 | self.events = [] 29 | 30 | @bot.check 31 | async def has_role(ctx): 32 | if ctx.guild: 33 | if (roles := [ 34 | r for r in ctx.guild.roles 35 | if "RT-" in r.name or ( 36 | str(ctx.guild.id) in self.data 37 | and str(r.id) in self.data[str(ctx.guild.id)] 38 | and ctx.command.qualified_name in 39 | self.data[str(ctx.guild.id)][str(r.id)].get( 40 | "commands", ""))]): 41 | channels = { 42 | role.id: [ 43 | ch.id for ch in ctx.guild.text_channels 44 | if any(r.id == role.id for r in ch.changed_roles) 45 | ] for role in roles 46 | } 47 | return any( 48 | bool(ctx.author.get_role(role.id)) and ( 49 | not channels or not channels[role.id] or 50 | ctx.channel.id in channels[role.id] 51 | ) for role in roles 52 | ) 53 | return True 54 | self.did = True 55 | self.bot.dispatch("load_rtrole") 56 | 57 | async def save(self): 58 | async with async_open("data/rtrole.json", "w") as f: 59 | await f.write(dumps(self.data, ensure_ascii=True, indent=2)) 60 | 61 | @commands.hybrid_group( 62 | aliases=["rtロール", "りつロール", "rr"], extras={ 63 | "headding": {"ja": "RTを操作できる役職を設定します。", "en": "..."}, 64 | "parent": "RT" 65 | } 66 | ) 67 | async def rtrole(self, ctx): 68 | """!lang ja 69 | -------- 70 | 指定したコマンドを特定の役職を持っている人しか実行できないようにします。 71 | `rf!rtrole`で現在設定されているRTロールのリストを表示します。 72 | 73 | Aliases 74 | ------- 75 | rtロール, りつロール, rr 76 | 77 | Notes 78 | ----- 79 | もし全てのコマンドを特定の役職を持っている人しか実行できないようにしたい場合は、`RT-`が名前の最初にある役職を作れば良いです。 80 | 例:`RT-操作権限` 81 | またこれで設定した役職をチャンネルに設定するとそのチャンネル内でその役職を持っていないとコマンドが使えないようにできます。 82 | 83 | !lang en 84 | -------- 85 | ...""" 86 | if not ctx.invoked_subcommand: 87 | await ctx.reply( 88 | embed=discord.Embed( 89 | title="RT Role List", 90 | description="\n".join( 91 | f"{data['role_name']}:{data['commands']}" 92 | for data in self.data[str(ctx.guild.id)].values() 93 | if data 94 | ), color=self.bot.colors["normal"] 95 | ) 96 | ) 97 | 98 | @rtrole.command("set", aliases=["設定", "s"]) 99 | @commands.has_permissions(administrator=True) 100 | @app_commands.describe(role="設定するロール", commands="持ってないと使えないようにするコマンド") 101 | async def set_(self, ctx, role: discord.Role, *, commands): 102 | """!lang ja 103 | -------- 104 | RTロールを設定します。 105 | 106 | Parameters 107 | ---------- 108 | role : 役職の名前またはメンション 109 | 設定するロールの名前かメンションです。 110 | commands : str 111 | その役職を持っていないと実行できないコマンドの名前です。(空白で複数指定できます。) 112 | 113 | Examples 114 | -------- 115 | `rf!rtrole set ping-info専門の人 ping info` 116 | pingとinfoコマンドを`ping-info専門の人`の役職を持っていないと実行できないようにします。 117 | 118 | !lang en 119 | -------- 120 | ...""" 121 | if len(self.data[str(ctx.guild.id)]) <= 50: 122 | self.data[str(ctx.guild.id)][str(role.id)] = { 123 | "commands": commands, "role_name": role.name 124 | } 125 | await self.save() 126 | await ctx.reply("Ok") 127 | else: 128 | await ctx.reply("50個まで設定可能です。") 129 | 130 | @rtrole.command(aliases=["del", "rm", "remove", "削除"]) 131 | @commands.has_permissions(administrator=True) 132 | @app_commands.describe(role="解除するロール") 133 | async def delete(self, ctx, *, role: discord.Role): 134 | """!lang ja 135 | -------- 136 | `rf!rtrole set`の逆です。 137 | 138 | Parameters 139 | ---------- 140 | role : 役職のメンションか名前 141 | RTロールの設定を解除したいロールの役職の名前です。 142 | 143 | Aliases 144 | ------- 145 | del, rm, remove, 削除 146 | 147 | !lang en 148 | -------- 149 | ...""" 150 | if self.data[(gid := str(ctx.guild.id))]: 151 | try: 152 | del self.data[gid][str(role.id)] 153 | except KeyError: 154 | await ctx.reply("その役職は設定されていません。") 155 | else: 156 | await self.save() 157 | await ctx.reply("Ok") 158 | else: 159 | await ctx.reply("このサーバーにはRTロールが設定されていません。") 160 | 161 | 162 | async def setup(bot: commands.AutoShardedBot): 163 | await bot.add_cog(RTRole(bot)) 164 | -------------------------------------------------------------------------------- /cogs/RT/websockets.py: -------------------------------------------------------------------------------- 1 | # Free RT - WebSockets 2 | 3 | from typing import Union 4 | 5 | from discord.ext import commands 6 | import discord 7 | 8 | from util import RT, websocket 9 | 10 | 11 | DISCORD = "/img/discord.jpg" 12 | 13 | 14 | class WebSockets(commands.Cog): 15 | def __init__(self, bot: RT): 16 | self.bot = bot 17 | 18 | def convert_channel( 19 | self, channel: Union[ 20 | discord.TextChannel, discord.VoiceChannel, 21 | discord.Thread, discord.StageChannel 22 | ] 23 | ) -> dict: 24 | return { 25 | "id": str(channel.id), "name": channel.name, 26 | "voice": isinstance( 27 | channel, (discord.VoiceChannel, discord.StageChannel) 28 | ) 29 | } 30 | 31 | def convert_channels(self, channels: list) -> dict: 32 | return [ 33 | self.convert_channel(channel) 34 | for channel in channels 35 | ] 36 | 37 | def convert_role(self, role: discord.Role) -> dict: 38 | return { 39 | "name": role.name, "id": str(role.id) 40 | } 41 | 42 | def convert_user(self, user: Union[discord.User, discord.Member]) -> dict: 43 | return { 44 | "name": user.name, "id": str(user.id), "icon_url": getattr( 45 | user.display_avatar, "url", DISCORD 46 | ) 47 | } 48 | 49 | def convert_guild(self, guild: discord.Guild) -> dict: 50 | return { 51 | "name": guild.name, "id": str(guild.id), "icon_url": getattr( 52 | guild.icon, "url", DISCORD 53 | ), "text_channels": self.convert_channels(guild.text_channels), 54 | "voice_channels": self.convert_channels(guild.voice_channels), 55 | "channels": self.convert_channels(guild.channels), 56 | "roles": [self.convert_role(role) for role in guild.roles], 57 | "members": [self.convert_user(member) for member in guild.members] 58 | } 59 | 60 | @websocket.websocket("/api/guild", auto_connect=True, reconnect=True) 61 | async def guild(self, ws: websocket.WebSocket, _): 62 | await ws.send("on_ready") 63 | 64 | @guild.event("on_ready") 65 | async def on_ready(self, ws: websocket.WebSocket, _): 66 | await self.guild(ws, None) 67 | 68 | @guild.event("fetch_guilds") 69 | async def fetch_guilds(self, ws: websocket.WebSocket, user_id: str): 70 | user_id = int(user_id) 71 | return [ 72 | self.convert_guild(guild) for guild in self.bot.guilds 73 | if any(member.id == user_id for member in guild.members) 74 | ] 75 | 76 | 77 | async def setup(bot): 78 | await bot.add_cog(WebSockets(bot)) 79 | -------------------------------------------------------------------------------- /cogs/_first.py: -------------------------------------------------------------------------------- 1 | # Free RT - First 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from discord.ext import commands 6 | 7 | if TYPE_CHECKING: 8 | from util import RT 9 | 10 | 11 | class First(commands.Cog): 12 | def __init__(self, bot: "RT"): 13 | self.bot = bot 14 | 15 | @commands.Cog.listener() 16 | async def on_command_error(self, ctx: commands.Context, error: Exception): 17 | if isinstance(error, commands.CommandNotFound): 18 | await ctx.reply("現在起動中のため実行できません。\nすみませんが、もうしばらくお待ちください。") 19 | else: 20 | await ctx.reply(str(error)) 21 | 22 | 23 | async def setup(bot): 24 | await bot.add_cog(First(bot)) 25 | -------------------------------------------------------------------------------- /cogs/_sub.py: -------------------------------------------------------------------------------- 1 | # Free RT Chan - Info 2 | 3 | from discord.ext import commands 4 | import discord 5 | 6 | 7 | class Info(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @commands.hybrid_command( 12 | "help", slash_command=True, aliases=[ 13 | "h", "へるぷ", "ヘルプ", "invite", "info", "about" 14 | ], description="ふりーりつたんの操作方法を表示します。" 15 | ) 16 | async def help(self, ctx): 17 | await ctx.reply( 18 | "どうも、ふりーりつたんだよ。\n詳細や招待や使い方はこちら:https://rt-team.github.io/rt-chan" 19 | ) 20 | 21 | @commands.Cog.listener() 22 | async def on_full_ready(self): 23 | await self.bot.change_presence( 24 | activity=discord.Activity( 25 | name="rf#help | 少女絶賛稼働中!", 26 | type=discord.ActivityType.watching, 27 | state="ふりーりつたん" 28 | ) 29 | ) 30 | 31 | 32 | async def setup(bot): 33 | await bot.add_cog(Info(bot)) 34 | -------------------------------------------------------------------------------- /cogs/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | for name in listdir("cogs/admin"): 7 | if not name.startswith(("_", ".")): 8 | try: 9 | await bot.load_extension( 10 | f"cogs.admin.{name[:-3] if name.endswith('.py') else name}") 11 | except Exception: 12 | traceback.print_exc() 13 | else: 14 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 15 | -------------------------------------------------------------------------------- /cogs/admin/database.py: -------------------------------------------------------------------------------- 1 | # Free RT - Database Manager 2 | 3 | from discord.ext import commands 4 | import discord 5 | 6 | from aiofiles import open as aioopen 7 | from aiofiles.os import remove 8 | 9 | from util import RT 10 | 11 | 12 | class DatabaseManager(commands.Cog): 13 | def __init__(self, bot: RT): 14 | self.bot = bot 15 | 16 | @commands.command( 17 | description="渡された命令文でデータベースを操作します。", 18 | extras={"parent": "Admin"}, aliases=["db", "mysql", "execute", "実行"] 19 | ) 20 | @commands.is_owner() 21 | async def sql( 22 | self, ctx, show: bool, 23 | *, cmd 24 | ): 25 | """!lang ja 26 | -------- 27 | データベースで任意のコマンドを実行します。 28 | 当たり前ですが管理者しか実行できません。 29 | 30 | Notes 31 | ----- 32 | Free RTのデータベースへの接続に使用しているラッパーは`aiomysql`です。 33 | その`aiomysql`のオプションで自動コミットするようにしています。 34 | 35 | Parameters 36 | ---------- 37 | show : bool 38 | 実行結果を表示するかどうかです。 39 | cmd : str 40 | 実行するコードです。 41 | 42 | !lang en 43 | -------- 44 | No description.""" 45 | result = None 46 | async with self.bot.mysql.pool.acquire() as conn: 47 | async with conn.cursor() as cursor: 48 | await cursor.execute(cmd) 49 | if show: 50 | result = "\n".join( 51 | map(lambda x: "\t".join(map(str, x)), 52 | await cursor.fetchall()) 53 | ) 54 | if result is None: 55 | await ctx.reply("Ok") 56 | else: 57 | if len(result) > 2000: 58 | async with aioopen( 59 | name := f"sql_{ctx.author.id}_result.txt", "w" 60 | ) as f: 61 | await f.write(result) 62 | await ctx.reply("Ok", file=discord.File(name)) 63 | await remove(name) 64 | else: 65 | await ctx.reply(f"Ok\n```\n{result}\n```") 66 | 67 | 68 | async def setup(bot): 69 | await bot.add_cog(DatabaseManager(bot)) 70 | -------------------------------------------------------------------------------- /cogs/admin/develop.py: -------------------------------------------------------------------------------- 1 | # free RT - Feature for Developers 2 | 3 | from discord.ext import commands 4 | 5 | from util import RT 6 | 7 | 8 | class Develop(commands.Cog): 9 | 10 | def __init__(self, bot: RT): 11 | self.bot = bot 12 | 13 | @commands.group( 14 | extras={ 15 | "headding": {"ja": "管理者用のコマンドです。", "en": "Only for developers command."}, 16 | "parent": "Admin" 17 | } 18 | ) 19 | @commands.is_owner() 20 | async def develop(self, ctx): 21 | """!lang ja 22 | -------- 23 | 管理者専用のコマンドです。sub_commands: reload_help, command_log 24 | 25 | !lang en 26 | -------- 27 | Command for developers only. sub_commands: reload_help, command_log""" 28 | if ctx.invoked_subcommand is None: 29 | return await ctx.send("使用方法が違います。") 30 | 31 | @develop.command() 32 | async def reload_help(self, ctx, command_name=None): 33 | if command_name is None: 34 | for c in self.bot.commands: 35 | await self.bot.cogs["DocHelp"].on_command_add(c) 36 | await ctx.send("全コマンドのhelp読み込み完了") 37 | else: 38 | for c in [self.bot.get_command(co) for co in command_name.split()]: 39 | await self.bot.cogs["DocHelp"].on_command_add(c) 40 | await ctx.send(f"{', '.join(command_name.split())}のhelp読み込み完了") 41 | 42 | @develop.command( 43 | extras={ 44 | "headding": {"ja": "直近1分間のコマンド実行ログを見ます。", "en": "View commands logs."} 45 | } 46 | ) 47 | @commands.is_owner() 48 | async def command_logs(self, ctx, mode=None): 49 | """!lang ja 50 | -------- 51 | 直近1分間のコマンド実行ログを見ることができます。また、実行ログのループ操作もできます。 52 | 53 | Parameters 54 | ---------- 55 | mode: startやstop、restartなど 56 | logging_loop.○○の○○に入れられる文字列を入れて下さい。 57 | 58 | Warnings 59 | -------- 60 | もちろん実行は管理者専用です。 61 | 62 | !lang en 63 | -------- 64 | View command logs. Also it can control loop of logs. 65 | 66 | Parameters 67 | ---------- 68 | mode: start/stop, or restart 69 | Put the string which can be put in logging_loop.●●. 70 | 71 | Warnings 72 | -------- 73 | Of cource it can only be used by admin. 74 | """ 75 | if mode: 76 | getattr(self.bot.cogs["SystemLog"].logging_loop, mode)() 77 | await ctx.message.add_reaction("✅") 78 | elif len(self.bot.cogs["SystemLog"].names) != 0: 79 | await ctx.reply(embed=self.bot.cogs["SystemLog"]._make_embed()) 80 | else: 81 | await ctx.reply("ログ無し。") 82 | 83 | @develop.command( 84 | extras={"headding": {"ja": "言語データを再読込します。", 85 | "en": "Reload language data."}, 86 | "parent": "Admin"}) 87 | async def reload_language(self, ctx): 88 | """言語データを再読込します。""" 89 | await ctx.typing() 90 | await self.bot.cogs["Language"].update_language() 91 | await ctx.reply("Ok") 92 | 93 | 94 | async def setup(bot): 95 | await bot.add_cog(Develop(bot)) 96 | -------------------------------------------------------------------------------- /cogs/admin/logger.py: -------------------------------------------------------------------------------- 1 | # Free RT - Logger 2 | 3 | from discord.ext import commands, tasks 4 | import discord 5 | 6 | from traceback import TracebackException 7 | 8 | from collections import Counter 9 | 10 | from util import RT 11 | 12 | 13 | ERROR_CHANNEL = 962977145716625439 14 | 15 | 16 | class SystemLog(commands.Cog): 17 | def __init__(self, bot: RT): 18 | self.bot = bot 19 | self.names = [] 20 | self.zero_parents = [] 21 | self.authors = [] 22 | self.guilds = [] 23 | self.errors = set() 24 | self.logging_loop.start() 25 | 26 | def cog_unload(self): 27 | self.logging_loop.cancel() 28 | 29 | def _make_embed(self): 30 | # コマンド実行記録のembedを作成する。 31 | # 使用回数が最大のものを取り出す。 32 | name = Counter(self.names).most_common()[0] 33 | zero_parent = Counter(self.zero_parents).most_common()[0] 34 | author = Counter(self.authors).most_common()[0] 35 | guild = Counter(self.guilds).most_common()[0] 36 | 37 | e = discord.Embed( 38 | title="Free RT command log", 39 | description=f"1分間で{len(self.names)}回のコマンド実行(以下、実行最多記録)", 40 | color=self.bot.Colors.unknown 41 | ) 42 | e.add_field(name="コマンド", value=f"{name[0]}:{name[1]}回") 43 | e.add_field(name="コマンド(Group)", value=f"{zero_parent[0]}:{zero_parent[1]}回") 44 | e.add_field( 45 | name="ユーザー", 46 | value=f"{self.bot.get_user(author[0])}({author[0]}):{author[1]}回" 47 | ) 48 | e.add_field( 49 | name="サーバー", 50 | value=f"{self.bot.get_guild(guild[0])}({guild[0]}):{guild[1]}回" 51 | ) 52 | return e 53 | 54 | @tasks.loop(seconds=60) 55 | async def logging_loop(self): 56 | if len(self.names) != 0: 57 | await self.bot.get_channel(961870556548984862) \ 58 | .send(embed=self._make_embed()) 59 | self.names = [] 60 | self.zero_parents = [] 61 | self.authors = [] 62 | self.guilds = [] 63 | 64 | @commands.Cog.listener() 65 | async def on_command(self, ctx): 66 | self.names.append(ctx.command.name) 67 | self.zero_parents.append( 68 | ctx.command.name if len(ctx.command.parents) == 0 69 | else ctx.command.parents[-1].name 70 | ) 71 | self.authors.append(ctx.author.id) 72 | self.guilds.append(getattr(ctx.guild, "id", 0)) 73 | 74 | @commands.Cog.listener() 75 | async def on_command_error(self, ctx: commands.Context, error: Exception): 76 | # エラー時のログ処理。大量のisinstance->returnがあるのはcogs/RT/__init__.pyで処理するから。 77 | if isinstance( 78 | error, 79 | (commands.CommandNotFound, discord.Forbidden, commands.CommandOnCooldown, 80 | commands.MemberNotFound, commands.UserNotFound, commands.ChannelNotFound, 81 | commands.RoleNotFound, commands.MissingRequiredArgument, commands.BadArgument, 82 | commands.ArgumentParsingError, commands.TooManyArguments, commands.MissingPermissions, 83 | commands.MissingRole, commands.CheckFailure, AssertionError)): 84 | return 85 | elif isinstance(error, commands.CommandInvokeError): 86 | return await self.on_command_error(ctx, error.original) 87 | elif isinstance(error, AttributeError) and "VoiceChannel" in str(error): 88 | return 89 | else: 90 | error_message = "".join(TracebackException.from_exception(error).format()) 91 | print("\033[31m" + error_message + "\033[0m") 92 | ch = self.bot.get_channel(ERROR_CHANNEL) 93 | embed = discord.Embed( 94 | title="Free RT Error log", 95 | description=f"```{error_message}```", 96 | color=self.bot.Colors.unknown 97 | ) 98 | embed.add_field(name="コマンド", value=f"{ctx.command.name} (実行メッセージ: {ctx.message.content})") 99 | embed.add_field(name="ユーザー", value=f"{ctx.author.mention} ({ctx.author.id})") 100 | embed.add_field(name="サーバー", value=f"{ctx.guild.name} ({ctx.guild.id})") 101 | await ch.send(embed=embed) 102 | 103 | 104 | async def setup(bot): 105 | await bot.add_cog(SystemLog(bot)) 106 | -------------------------------------------------------------------------------- /cogs/admin/rtlife.py: -------------------------------------------------------------------------------- 1 | # Free RT - RT Life 2 | 3 | from __future__ import annotations 4 | 5 | from os.path import exists 6 | from asyncio import all_tasks 7 | from time import time 8 | 9 | from discord.ext import commands, tasks 10 | 11 | from jishaku.functools import executor_function 12 | from psutil import virtual_memory, cpu_percent 13 | from aiofiles import open as aioopen 14 | from ujson import load, dumps 15 | 16 | from util import RT 17 | 18 | 19 | class RTLife(commands.Cog): 20 | def __init__(self, bot: RT): 21 | self.bot = bot 22 | if exists("data/rtlife.json"): 23 | with open("data/rtlife.json", "r") as f: 24 | self.data = load(f) 25 | else: 26 | self.data = { 27 | "botCpu": [], "botMemory": [], "backendCpu": [], "backendMemory": [], 28 | "users": [], "guilds": [], "voicePlaying": [], "backendLatency": [], "discordLatency": [], 29 | "botPoolSize": [], "botTaskCount": [], "backendPoolSize": [], "backendTaskCount": [] 30 | } 31 | self.bot.rtws.set_event(self.get_status) 32 | self.update_status.start() 33 | 34 | @executor_function 35 | def process_psutil(self) -> tuple[float, float]: 36 | return virtual_memory().percent, cpu_percent(interval=1) 37 | 38 | @executor_function 39 | def count( 40 | self, data: tuple, server_status: tuple[float, float], 41 | latency: float, task_count: int 42 | ): 43 | for key in self.data.keys(): 44 | count = None 45 | if key == "botMemory": 46 | count = server_status[0] 47 | elif key == "botCpu": 48 | count = server_status[1] 49 | elif key == "users": 50 | count = len(self.bot.users) 51 | elif key == "guilds": 52 | count = len(self.bot.guilds) 53 | elif key == "voicePlaying": 54 | count = len(self.bot.voice_clients) 55 | elif key == "backendLatency": 56 | count = latency 57 | elif key == "discordLatency": 58 | count = round(self.bot.latency * 1000, 1) 59 | elif key == "botPoolSize": 60 | count = self.bot.mysql.pool.size 61 | elif key == "botTaskCount": 62 | count = task_count 63 | elif key == "backendPoolSize": 64 | count = data[0][0] 65 | elif key == "backendTaskCount": 66 | count = data[0][1] 67 | elif key == "backendMemory": 68 | count = data[1][0] 69 | elif key == "backendCpu": 70 | count = data[1][1] 71 | if count is not None: 72 | self.data[key].append(count) 73 | if len(self.data[key]) >= 1008: 74 | self.data[key] = self.data[key][-1008:] 75 | 76 | # @tasks.loop(seconds=10) 77 | @tasks.loop(minutes=10) 78 | async def update_status(self): 79 | try: 80 | data = await self.bot.rtws.request("get_backend_status", None) 81 | except Exception: 82 | data = ((0, 0), (4, 30)) 83 | # バックエンドとのレイテンシを調べる。 84 | if self.bot.backend: 85 | count = time() 86 | async with self.bot.session.get( 87 | f"{self.bot.get_url()}/api/ping" 88 | ) as r: 89 | if await r.text() == "pong": 90 | count = round((time() - count) * 1000, 1) 91 | elif self.data["backendLatency"]: 92 | count = self.data["backendLatency"][-1] 93 | else: 94 | count = 0.0 95 | else: 96 | count = self.data["backendLatency"][-1] if self.data["backendLatency"] else 0.0 97 | await self.count(data, await self.process_psutil(), count, len(all_tasks())) 98 | async with aioopen("data/rtlife.json", "w") as f: 99 | await f.write(dumps(self.data)) 100 | 101 | def get_status(self, _): 102 | return self.data 103 | 104 | def cog_unload(self): 105 | self.update_status.cancel() 106 | 107 | 108 | async def setup(bot): 109 | await bot.add_cog(RTLife(bot)) 110 | -------------------------------------------------------------------------------- /cogs/channelplugin/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | for name in listdir("cogs/channelplugin"): 7 | if not name.startswith(("_", ".")): 8 | try: 9 | await bot.load_extension( 10 | f"cogs.channelplugin.{name[:-3] if name.endswith('.py') else name}") 11 | except Exception: 12 | traceback.print_exc() 13 | else: 14 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 15 | -------------------------------------------------------------------------------- /cogs/channelplugin/autopublic.py: -------------------------------------------------------------------------------- 1 | # Free RT - Auto Public 2 | 3 | from discord.ext import commands 4 | from discord import ChannelType 5 | 6 | 7 | CHP_HELP = { 8 | "ja": ("メッセージ自動公開機能。", 9 | """# メッセージ自動公開プラグイン - autopublic 10 | これは`rf>autopublic `をニュースチャンネルのトピックに入れることで自動的にメッセージを公開してくれる機能です。 11 | 例:`rf>autopublic` (これをトピックに入れたチャンネルに送信したメッセージは全てメッセージを公開してくれます) 12 | 例: `rf>autopublic check` (これをトピックに入れたチャンネルに送信したメッセージは全てメッセージを公開してくれますが、公開するとメッセージにチェックが入ります) 13 | """), 14 | "en": ("message auto public.", 15 | """# # Message autopublishing plugin - autopublic 16 | This is a feature that allows you to automatically publish messages by \ 17 | putting `rf>autopublic ` in the topic of a news channel. 18 | Example: `rf>autopublic` (any message sent to a channel with this in the topic will make the message public) 19 | Example: `rf>autopublic check` (any message sent to a channel with this in the topic \ 20 | will make the message public, but the message will be checked when it is made public) 21 | """) 22 | } 23 | 24 | 25 | class AutoPublic(commands.Cog): 26 | def __init__(self, bot): 27 | self.bot = bot 28 | 29 | @commands.Cog.listener() 30 | async def on_help_reload(self): 31 | for lang in CHP_HELP: 32 | self.bot.cogs["DocHelp"].add_help( 33 | "ChannelPlugin", "AutoPublic", 34 | lang, *CHP_HELP[lang] 35 | ) 36 | 37 | @commands.Cog.listener() 38 | async def on_message(self, message): 39 | if not hasattr(message.channel, "topic"): 40 | return 41 | if not type(message.channel) == ChannelType.news or not message.channel.topic: 42 | return 43 | 44 | for line in message.channel.topic.splitlines(): 45 | if line.startswith("rf>autopublic"): 46 | await message.publish() 47 | if len(line.split()) >= 1: 48 | option = line.split()[0] 49 | if option == "check": 50 | await message.add_reaction("✅") 51 | 52 | 53 | async def setup(bot): 54 | await bot.add_cog(AutoPublic(bot)) 55 | -------------------------------------------------------------------------------- /cogs/entertainment/6ch.py: -------------------------------------------------------------------------------- 1 | # Free RT Ext - 6ch 2 | 3 | from aiofiles import open as async_open 4 | from ujson import load, dumps 5 | from random import randint 6 | from datetime import date 7 | import reprypt 8 | 9 | from discord.ext import commands 10 | from discord import app_commands 11 | 12 | 13 | def rname() -> str: 14 | chars = "" 15 | for i in range(10): 16 | chars += randint(0, 9) 17 | return chars 18 | 19 | 20 | class SixChannel(commands.Cog): 21 | def __init__(self, bot): 22 | self.bot, self.rt = bot, bot.data 23 | self.data = {"thread": {}, "nickname": {}, "id": {}} 24 | self.path = "data/6ch.json" 25 | try: 26 | with open(self.path, "r") as f: 27 | self.data = load(f) 28 | except Exception: 29 | with open(self.path, "w") as f: 30 | f.write(dumps(self.data)) 31 | 32 | async def save(self, path, data, indent): 33 | async with async_open(path, "w") as f: 34 | await f.write(dumps(data, indent=indent)) 35 | 36 | @commands.hybrid_group( 37 | name="6ch", aliases=["ch"], extras={ 38 | "headding": { 39 | "ja": "掲示板", 40 | "en": "6ch, BBS" 41 | }, "parent": "Entertainment" 42 | } 43 | ) 44 | async def sixch(self, ctx): 45 | """!lang ja 46 | --------- 47 | 2ちゃんねるのような掲示板を作れる機能です。 48 | 49 | !lang en 50 | -------- 51 | Make BBS like 4chan.""" 52 | if not ctx.invoked_subcommand: 53 | if self.data["thread"]: 54 | n = "".join( 55 | (f"{key.replace('@', '@')}, 作者:" 56 | + getattr(self.bot.get_user(data["author"]), "name", "???") 57 | .replace("@", "@")) 58 | for key, data in list(self.data["thread"].items()) 59 | ) 60 | await ctx.reply(n) 61 | else: 62 | await ctx.reply("まだありません。") 63 | 64 | @sixch.command() 65 | @app_commands.describe(name="作成するスレッドの名前") 66 | async def new(self, ctx, *, name): 67 | """!lang ja 68 | ------- 69 | 新しくスレッドを作ります。 70 | 71 | Parameters 72 | ---------- 73 | name : str 74 | 作成するスレッドの名前です。 75 | 76 | !lang en 77 | -------- 78 | Create a new thread. 79 | 80 | Parameters 81 | ---------- 82 | name : str 83 | The name of the thread to be created.""" 84 | if name in self.data["thread"]: 85 | await ctx.reply("その名前のスレッドは既にあります。") 86 | else: 87 | self.data["thread"][name] = { 88 | "log": [], 89 | "author": ctx.author.id, 90 | "channels": [], 91 | "count": 0 92 | } 93 | await self.save(self.path, self.data, 4) 94 | await ctx.reply("設定しました。") 95 | 96 | @sixch.command(aliases=["cng"]) 97 | @commands.has_permissions(manage_channels=True) 98 | @app_commands.describe(name="接続するスレッドの名前") 99 | async def connect(self, ctx, *, name): 100 | """!lang ja 101 | -------- 102 | 6chの既にあるスレッドに接続します。 103 | 104 | Parameters 105 | ---------- 106 | name : str 107 | 接続するスレッドの名前です。""" 108 | if name in self.data["thread"]: 109 | self.data["thread"][name]["channels"].append(ctx.channel.id) 110 | await ctx.reply("設定しました。") 111 | else: 112 | await ctx.reply("その名前のスレッドが見つかりませんでした。") 113 | 114 | @sixch.command(name="del", aliases=["delete", "remove", "rm"]) 115 | async def _del(self, ctx): 116 | await ctx.reply("`rf!info`からサポートサーバーにて管理者に問い合わせてください。") 117 | 118 | @sixch.command(aliases=["nick"]) 119 | @app_commands.describe(name="スレッドで使うニックネーム") 120 | async def nickname(self, ctx, *, name): 121 | """!lang ja 122 | -------- 123 | スレッドで使うニックネームを設定します。 124 | 125 | Parameters 126 | ---------- 127 | name : str 128 | ニックネームです。""" 129 | self.data["nickname"][str(ctx.author.id)] = name 130 | await self.save(self.path, self.data, 4) 131 | await ctx.reply("設定しました。") 132 | 133 | @commands.Cog.listener() 134 | async def on_message(self, message): 135 | if message.author.bot or message.content.startswith("rf!"): 136 | return 137 | 138 | for name in self.data["thread"]: 139 | chs = self.data["thread"][name]["channels"] 140 | if message.channel.id in chs: 141 | data = self.data["thread"][name] 142 | u = self.data["nickname"].get( 143 | str(message.author.id), message.author.name) 144 | # もし通報時用のユーザーIDがなかったら作る。 145 | if str(message.author.id) not in self.data: 146 | # Repryptで送信者のIDを暗号化してできたIDを使う。 147 | uid = reprypt.encrypt( 148 | str(message.author.id), "6chの語源はRT") 149 | self.data["id"][str(message.author.id)] = uid 150 | # メッセージ作成。 151 | uid = self.data["id"][str(message.author.id)] 152 | c = f"{data['count']}:**{u}**:{date.today()}:{uid}" 153 | c += "\n" + message.clean_content 154 | # カウントアップする。 155 | self.data["thread"][name]["count"] += 1 156 | # 送信する。 157 | await message.delete() 158 | await message.channel.send(c) 159 | self.data["thread"][name]["log"].append(c) 160 | await self.save(self.path, self.data, 4) 161 | break 162 | 163 | 164 | async def setup(bot): 165 | await bot.add_cog(SixChannel(bot)) 166 | -------------------------------------------------------------------------------- /cogs/entertainment/gamesearch.py: -------------------------------------------------------------------------------- 1 | # Free RT - gamesearch 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from urllib.parse import quote_plus 7 | from ujson import loads 8 | 9 | from util import RT 10 | 11 | 12 | class GameSearch(commands.Cog): 13 | def __init__(self, bot: RT): 14 | self.bot = bot 15 | 16 | @commands.command( 17 | aliases=["searchgame", "ゲームを探す"], 18 | extras={ 19 | "headding": {"ja": "ゲームを探します。", "en": "..."}, 20 | "parent": "Entertainment" 21 | } 22 | ) 23 | async def gamesearch(self, ctx, *, name: str): 24 | """!lang ja 25 | -------- 26 | ゲームを検索して詳細を表示します。 27 | 28 | Parameters 29 | ---------- 30 | name : str 31 | 探したいゲーム名です。 32 | 33 | Aliases 34 | ------- 35 | searchgame, ゲームを探す 36 | 37 | !lang en 38 | -------- 39 | Sorry, this command only supports Japanese. 40 | """ 41 | async with self.bot.session.get( 42 | "https://ysmsrv.wjg.jp/disbot/gamesearch.php?q=" + quote_plus(name, encoding='utf-8') 43 | ) as resp: 44 | gj = loads(await resp.text()) 45 | hdw = "" 46 | try: 47 | game = gj["Items"][0] 48 | gametitle = game["Item"]["titleKana"] 49 | for item in gj["Items"]: 50 | if gametitle in item["Item"]["titleKana"]: 51 | hdw = hdw + " " + item["Item"]["hardware"] 52 | except IndexError: 53 | await ctx.send("すみません。見つかりませんでした。別の単語をお試しください") 54 | else: 55 | embed = discord.Embed( 56 | title=gametitle + "の詳細", 57 | description=game["Item"]["itemCaption"].replace('\\n', '\n'), 58 | color=self.bot.Colors.normal 59 | ) 60 | embed.add_field(name="機種", value=hdw) 61 | embed.set_image(url=game["Item"]["largeImageUrl"]) 62 | embed.set_footer(text="ゲーム情報検索") 63 | await ctx.send(embed=embed) 64 | 65 | 66 | async def setup(bot): 67 | await bot.add_cog(GameSearch(bot)) 68 | -------------------------------------------------------------------------------- /cogs/entertainment/minesweeper.py: -------------------------------------------------------------------------------- 1 | # Free RT - MineSweeper Game Extension 2 | 3 | from discord.ext import commands 4 | from discord import app_commands 5 | import discord 6 | 7 | from asyncio import TimeoutError 8 | import re 9 | 10 | from util.checks import alpha2num 11 | from util import MineSweeper 12 | 13 | 14 | class MSGame(commands.Cog): 15 | def __init__(self, bot): 16 | self.bot = bot 17 | self.games = {} 18 | 19 | @commands.hybrid_command( 20 | aliases=["ms", "MS", "マインスイーパー"], 21 | extras={ 22 | "headding": {"ja": "マインスイーパー", 23 | "en": "Minesweeper"}, 24 | "parent": "Entertainment" 25 | } 26 | ) 27 | @commands.cooldown(1, 15, commands.BucketType.user) 28 | @app_commands.describe(x="ゲームの横の長さ", y="ゲームの縦の長さ", bomb="ボムの数") 29 | async def minesweeper(self, ctx, x: int = 9, y: int = 9, bomb: int = 12): 30 | """!lang ja 31 | -------- 32 | マインスイーパーというゲームで遊びます。 33 | 34 | Aliases 35 | ------- 36 | ms, MS 37 | 38 | !lang en 39 | -------- 40 | Play Minesweeper. 41 | 42 | Aliases 43 | ------- 44 | ms, MS 45 | """ 46 | game = MineSweeper(x, y, bomb) 47 | e = discord.Embed( 48 | title="縦の列を`ABC`, 横の行を`123`として送信してください", 49 | description=game.to_string(), color=self.bot.Colors.normal 50 | ) 51 | msg = await ctx.send("マインスイーパー 1ターン目", embed=e) 52 | while True: 53 | try: 54 | msg = await self.bot.wait_for( 55 | "message", 56 | check=lambda m: ( 57 | m.author == ctx.author and 58 | re.fullmatch(r"(\l+|\u+)\d+", m.content) 59 | ), 60 | timeout=60.0 61 | ) 62 | except TimeoutError: 63 | return await msg.edit(content="タイムアウトしました。") 64 | await ctx.typing() 65 | result = game.open( 66 | alpha2num(re.match(r"(\l+|\u+)", msg.content).group()), 67 | int(re.search(r"\d+", msg.content).group()) 68 | )[0] 69 | 70 | if result == 0: 71 | # 継続。 72 | await msg.edit( 73 | content="マインスイーパー " 74 | f"{int(msg.content.split()[1][:-4]) + 1}ターン目", 75 | embed=discord.Embed( 76 | title=msg.embeds[0].title, 77 | description=game.to_string(), 78 | color=self.bot.Colors.normal)) 79 | elif result == 1: 80 | # クリア。 81 | return await msg.edit( 82 | content=msg.content + "でクリア!", 83 | embed=discord.Embed( 84 | title="クリアしました、おめでとう!", 85 | description=game.to_string("all"), 86 | color=self.bot.Colors.normal)) 87 | elif result == 2: 88 | # ゲームオーバー。 89 | return await msg.edit( 90 | content=msg.content + "でゲームオーバー", 91 | embed=discord.Embed( 92 | title="ゲームオーバー...", 93 | description=game.to_string("all"), 94 | color=self.bot.Colors.unknown)) 95 | 96 | 97 | async def setup(bot): 98 | await bot.add_cog(MSGame(bot)) 99 | -------------------------------------------------------------------------------- /cogs/entertainment/qr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import discord 4 | import pyqrcode 5 | from discord.ext import commands 6 | 7 | 8 | class qr(commands.Cog): 9 | 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @commands.group( 14 | extras={ 15 | "headding": {"ja": "qr関連のコマンドです", "en": "qr command"}, 16 | "parent": "Entertainment" 17 | } 18 | ) 19 | async def qr(self, ctx): 20 | """!lang ja 21 | -------- 22 | qrコード関連のコマンドです。sub_commands: make, read 23 | 24 | !lang en 25 | -------- 26 | qr category command. sub_commands: make, read""" 27 | if ctx.invoked_subcommand is None: 28 | return await ctx.send("使用方法が違います。") 29 | 30 | @qr.command( 31 | extras={ 32 | "headding": {"ja": "文字からqrコードを作成します", "en": "making qr code on string"}, 33 | "parent": "Entertainment" 34 | } 35 | ) 36 | async def make(self, ctx, text): 37 | a = pyqrcode.create(content=text, error='H') 38 | a.png(file=str(ctx.author.id) + '.png', scale=6) 39 | await ctx.send(file=discord.File(str(ctx.author.id) + '.png')) 40 | os.remove(str(ctx.author.id) + '.png') 41 | 42 | @qr.command( 43 | extras={ 44 | "headding": {"ja": "qrコードを読み取ります", "en": "read qr code"}, 45 | "parent": "Entertainment" 46 | } 47 | ) 48 | async def read(self, ctx, url=None): 49 | if url is None: 50 | url = ctx.message.attachments[0].url 51 | async with self.bot.session.get(url) as resp: 52 | with open(str(ctx.author.id) + 'r.png', 'wb') as fp: 53 | while True: 54 | r = await resp.content.read(10) # 4 55 | if not r: 56 | break 57 | fp.write(r) 58 | image = cv2.imread(str(ctx.author.id) + 'r.png') 59 | qrDetector = cv2.QRCodeDetector() 60 | data, bbox, rectifiedImage = qrDetector.detectAndDecode(image) 61 | await ctx.send(data) 62 | os.remove(str(ctx.author.id) + 'r.png') 63 | 64 | 65 | async def setup(bot): 66 | await bot.add_cog(qr(bot)) 67 | -------------------------------------------------------------------------------- /cogs/entertainment/today.py: -------------------------------------------------------------------------------- 1 | # Free RT - What day is today 2 | 3 | from discord.ext import commands, tasks 4 | from discord import app_commands 5 | import discord 6 | 7 | from util.mysql_manager import DatabaseManager 8 | from bs4 import BeautifulSoup 9 | from datetime import datetime, timedelta 10 | 11 | 12 | class DataManager(DatabaseManager): 13 | 14 | DB = "Today" 15 | 16 | def __init__(self, db): 17 | self.db = db 18 | 19 | async def init_table(self, cursor) -> None: 20 | await cursor.create_table( 21 | self.DB, { 22 | "GuildID": "BIGINT", "ChannelID": "BIGINT" 23 | } 24 | ) 25 | 26 | async def write(self, cursor, guild_id: int, channel_id: int) -> None: 27 | target = { 28 | "GuildID": guild_id, "ChannelID": channel_id 29 | } 30 | if await cursor.exists(self.DB, target): 31 | raise KeyError("既に設定されています。") 32 | else: 33 | await cursor.insert_data(self.DB, target) 34 | 35 | async def delete(self, cursor, guild_id: int, channel_id: int) -> None: 36 | await cursor.delete( 37 | self.DB, {"GuildID": guild_id, "ChannelID": channel_id} 38 | ) 39 | 40 | async def reads(self, cursor, guild_id: int = None) -> list: 41 | target = {} 42 | if guild_id is not None: 43 | target["GuildID"] = guild_id 44 | return [ 45 | row async for row in cursor.get_datas( 46 | self.DB, target) 47 | if row 48 | ] 49 | 50 | 51 | class Today(commands.Cog, DataManager): 52 | 53 | YAHOO_ICON = "http://www.google.com/s2/favicons?domain=www.yahoo.co.jp" 54 | 55 | def __init__(self, bot): 56 | self.bot = bot 57 | self.yet = True 58 | 59 | async def cog_load(self): 60 | await self.bot.wait_until_ready() 61 | super(commands.Cog, self).__init__(self.bot.mysql) 62 | await self.init_table() 63 | self.today_notification.start() 64 | 65 | async def get_today(self) -> discord.Embed: 66 | # 今日はなんの日をyahooから持ってくる。 67 | async with self.bot.session.get( 68 | "https://kids.yahoo.co.jp/today" 69 | ) as r: 70 | day = BeautifulSoup( 71 | await r.read(), "html.parser" 72 | ).find("dl") 73 | 74 | embed = discord.Embed( 75 | title=day.find("span").text, 76 | description=day.find("dd").text, 77 | color=0xee373e 78 | ) 79 | embed.set_footer( 80 | text="Yahoo きっず", 81 | icon_url=self.YAHOO_ICON 82 | ) 83 | return embed 84 | 85 | @commands.hybrid_command( 86 | extras={ 87 | "headding": { 88 | "ja": "「今日は何の日」を表示、通知します。", 89 | "en": "Sorry, This command is not supported." 90 | }, "parent": "Entertainment" 91 | } 92 | ) 93 | @commands.cooldown(1, 10, commands.BucketType.guild) 94 | @app_commands.describe(setting="通知をするかどうか") 95 | async def today(self, ctx, setting: bool = None): 96 | """!lang ja 97 | -------- 98 | 「今日は何の日」を表示または通知します。 99 | 100 | Parameters 101 | ---------- 102 | setting : bool, default False 103 | 通知を設定するかどうかです。 104 | onにすると実行したチャンネルに毎日午前9時に通知を送るように設定をします。 105 | offにすると通知をオフにしてその日の分の「今日は何の日」を表示します。 106 | デフォルトはoffです。 107 | 108 | Examples 109 | -------- 110 | `rf!today` 「今日は何の日」を表示します。 111 | `rf!today on` 実行したチャンネルに毎日午前9時に「今日は何の日」を送信します。""" 112 | if setting is None: 113 | await ctx.reply(embed=await self.get_today()) 114 | elif ctx.author.guild_permissions.manage_channels: 115 | try: 116 | await self.write(ctx.guild.id, ctx.channel.id) 117 | if len(await self.reads(ctx.guild.id)) == 4: 118 | raise OverflowError( 119 | "一つのサーバーにつき三つまでしか設定できないようにする。" 120 | ) 121 | except (KeyError, OverflowError) as e: 122 | await self.delete(ctx.guild.id, ctx.channel.id) 123 | if isinstance(e, OverflowError): 124 | return await ctx.reply( 125 | "一つのサーバーにつき四つまで設定が可能です。" 126 | ) 127 | await ctx.reply("Ok") 128 | else: 129 | await ctx.reply("チャンネル管理権限がないと通知の設定はできません。") 130 | 131 | def cog_unload(self): 132 | self.today_notification.cancel() 133 | 134 | @tasks.loop(seconds=30) 135 | async def today_notification(self): 136 | # 今日はなんの日通知をする。 137 | if self.yet and ( 138 | datetime.now() + timedelta(hours=9) 139 | ).strftime("%H:%M") == "09:00": 140 | for row in await self.reads(): 141 | channel = self.bot.get_channel(row[1]) 142 | if channel: 143 | try: 144 | await channel.send(embed=await self.get_today()) 145 | except (discord.HTTPException, discord.Forbidden): 146 | pass 147 | else: 148 | # もしチャンネルが見つからないなら設定を削除する。 149 | await self.delete(row[0], row[1]) 150 | self.yet = False 151 | else: 152 | self.yet = True 153 | 154 | 155 | async def setup(bot): 156 | await bot.add_cog(Today(bot)) 157 | -------------------------------------------------------------------------------- /cogs/individual/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | for name in listdir("cogs/individual"): 7 | if not name.startswith(("_", ".")): 8 | try: 9 | await bot.load_extension( 10 | f"cogs.individual.{name[:-3] if name.endswith('.py') else name}") 11 | except Exception: 12 | traceback.print_exc() 13 | else: 14 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 15 | -------------------------------------------------------------------------------- /cogs/individual/onlinenotice.py: -------------------------------------------------------------------------------- 1 | # Free RT - online Notice 2 | 3 | from discord.ext import commands 4 | from discord import app_commands 5 | import discord 6 | 7 | import asyncio 8 | 9 | from ujson import loads, dumps 10 | 11 | from util import db 12 | 13 | 14 | class DataBaseManager(db.DBManager): 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | async def manager_load(self, cursor): 19 | await cursor.execute( 20 | """CREATE TABLE IF NOT EXISTS 21 | OnlineNotice (notice_user BIGINT, authors TEXT)""" 22 | ) 23 | 24 | @db.command() 25 | async def get_user(self, cursor, notice_user_id: int) -> tuple: 26 | "データを取得します。" 27 | await cursor.execute( 28 | f"SELECT * FROM OnlineNotice WHERE notice_user={notice_user_id}" 29 | ) 30 | return await cursor.fetchall() 31 | 32 | @db.command() 33 | async def set_user(self, cursor, author_id: int, notice_user_id: int) -> None: 34 | "データを入れます。author_id: 通知する人 notice_user_id: 監視される人" 35 | if now := await self.get_user(cursor, author_id): 36 | data = dumps(loads(now[0][1]) + [str(author_id)]) 37 | await cursor.execute( 38 | f"UPDATE OnlineNotice SET authors='{data}' WHERE notice_user={notice_user_id}", 39 | ) 40 | else: 41 | await cursor.execute( 42 | f"INSERT INTO OnlineNotice values ({notice_user_id}, '{dumps([str(author_id)])}')" 43 | ) 44 | 45 | 46 | class OnlineNotice(commands.Cog): 47 | def __init__(self, bot): 48 | self.bot = bot 49 | self.cache = [] 50 | 51 | async def cog_load(self): 52 | self.db = await self.bot.add_db_manager(DataBaseManager(self.bot)) 53 | 54 | @commands.hybrid_group( 55 | extras={ 56 | "headding": {"ja": "オンライン通知", "en": "Online Notice"}, 57 | "parent": "Individual" 58 | } 59 | ) 60 | async def online_notice(self, ctx): 61 | """!lang ja 62 | -------- 63 | ユーザーがオンラインになったときに通知します。 64 | 65 | !lang en 66 | -------- 67 | Notices if a user was online.""" 68 | if ctx.invoked_subcommand is None: 69 | await ctx.send("使用方法が違います。") 70 | 71 | @online_notice.command( 72 | name="add", aliases=["set", "追加", "設定"], 73 | extras={"ja": "通知するユーザーを追加", "en": "Add notice user"} 74 | ) 75 | @app_commands.describe(notice_user="通知するユーザー") 76 | async def _add(self, ctx, notice_user: discord.User): 77 | """!lang ja 78 | -------- 79 | 通知するユーザーを追加します。 80 | 81 | Parameters 82 | ---------- 83 | notice_user: ユーザーIDか名前かメンション 84 | このユーザーがオンラインになった時にあなたのDMに通知が来ます。 85 | 86 | Aliases 87 | ------- 88 | set, 追加, 設定 89 | 90 | !lang en 91 | -------- 92 | Adds the user to notice list. 93 | 94 | Parameters 95 | ---------- 96 | notice_user: User ID, name, or mention 97 | Notice message will come to your DM when the user becomes online. 98 | 99 | Aliases 100 | ------- 101 | set 102 | """ 103 | await self.db.set_user.run(ctx.author.id, notice_user.id) 104 | await ctx.send("Ok") 105 | 106 | # require: presence_intent 107 | 108 | @commands.Cog.listener() 109 | async def on_presence_update(self, before, after): 110 | if before.status == after.status: 111 | return 112 | if after.status != discord.Status.online: 113 | return 114 | if after.id in self.cache: 115 | return 116 | 117 | userdata = await self.db.get_user.run(after.id) 118 | if userdata: 119 | self.cache.append(after.id) 120 | for m in loads(userdata[0][1]): 121 | try: 122 | e = discord.Embed(title="オンライン通知", description=f"{after.mention}さんがオンラインになりました。") 123 | await self.bot.get_user(int(m)).send(embed=e) 124 | except Exception: 125 | pass 126 | await asyncio.sleep(0.5) 127 | self.cache.remove(after.id) 128 | 129 | 130 | async def setup(bot): 131 | await bot.add_cog(OnlineNotice(bot)) 132 | -------------------------------------------------------------------------------- /cogs/individual/tools.py: -------------------------------------------------------------------------------- 1 | # Free RT - Tools For Dashboard 2 | 3 | 4 | from discord.ext import commands 5 | from discord import app_commands 6 | 7 | from asyncio import wait_for, TimeoutError 8 | from time import sleep 9 | import operator 10 | import ast 11 | 12 | from util.settings import Context 13 | from util import RT 14 | 15 | 16 | _OP_MAP = { 17 | ast.Add: operator.add, 18 | ast.Sub: operator.sub, 19 | ast.Div: operator.truediv, 20 | ast.FloorDiv: operator.floordiv, 21 | ast.Mod: operator.mod, 22 | ast.Mult: operator.mul, 23 | ast.Invert: operator.neg, 24 | ast.Pow: operator.pow, 25 | } 26 | 27 | 28 | def custom_eval(node_or_string): 29 | node_or_string = ast.parse(node_or_string.lstrip(" \t"), mode='eval') 30 | if isinstance(node_or_string, ast.Expression): 31 | node_or_string = node_or_string.body 32 | 33 | def _convert(node): 34 | if isinstance(node, ast.Constant): 35 | return node.value 36 | elif isinstance(node, ast.BinOp) and isinstance(node.op, tuple(_OP_MAP.keys())): 37 | sleep(0.1) # タイムアウト実装のため。 38 | left = _convert(node.left) 39 | right = _convert(node.right) 40 | if (right > 5 or left > 10000) and isinstance(node.op, ast.Pow): 41 | raise TimeoutError("Powor too big.") 42 | return _OP_MAP[type(node.op)](left, right) 43 | else: 44 | raise ValueError("can't calculate node of type '%s'" % node.__class__.__name__) 45 | 46 | return _convert(node_or_string) 47 | 48 | 49 | class Tools(commands.Cog): 50 | def __init__(self, bot: RT): 51 | self.bot = bot 52 | 53 | @commands.hybrid_command( 54 | extras={ 55 | "headding": { 56 | "ja": "式を入力して計算を行うことができます。", "en": "Calculation by expression" 57 | }, "parent": "Individual" 58 | } 59 | ) 60 | @app_commands.describe(expression="計算する式") 61 | async def calc(self, ctx: Context, *, expression: str): 62 | """!lang ja 63 | -------- 64 | 渡された式から計算をします。 65 | 66 | Parameters 67 | ---------- 68 | expression : str 69 | 式です。 70 | 71 | !lang en 72 | -------- 73 | Calculate from the expression given. 74 | 75 | Parameters 76 | ---------- 77 | expression : str 78 | Expression""" 79 | try: 80 | x = await wait_for( 81 | self.bot.loop.run_in_executor(None, custom_eval, expression), 3 82 | ) 83 | except (SyntaxError, ValueError): 84 | await ctx.send("計算式がおかしいです!") 85 | except ZeroDivisionError: 86 | await ctx.send("0で割り算することはできません!") 87 | except TimeoutError: 88 | await ctx.send("計算範囲が大きすぎます!頭壊れます。") 89 | else: 90 | await ctx.reply(f"計算結果:`{x}`") 91 | 92 | @commands.command( 93 | extras={ 94 | "headding": { 95 | "ja": "文字列を逆順にします。", "en": "Reverse text" 96 | } 97 | } 98 | ) 99 | async def reverse(self, ctx: Context, *, bigbox): 100 | await ctx.reply(f"結果:\n```\n{bigbox[::-1]}\n```") 101 | 102 | @commands.command( 103 | extras={ 104 | "headding": { 105 | "ja": "文字列の交換を行います。", "en": "Replace text" 106 | } 107 | } 108 | ) 109 | async def replace(self, ctx: Context, before, after, *, text): 110 | await ctx.reply(f"結果:{text.replace(before, after)}") 111 | 112 | @commands.command( 113 | "RTを追い出します。", extras={ 114 | "headding": { 115 | "ja": "Free RTをサーバーから追い出します。", "en": "Kick RT" 116 | } 117 | } 118 | ) 119 | @commands.has_guild_permissions(administrator=True) 120 | async def leave(self, ctx: Context, password="ここに「うらみのワルツ」と入力してください。"): 121 | if password == "うらみのワルツ": 122 | await ctx.guild.leave() 123 | await ctx.reply("* 返事がない。ただの屍のようだ。") 124 | else: 125 | await ctx.reply("「うらみのワルツ」を入力しなければ抜けません。") 126 | 127 | 128 | async def setup(bot): 129 | await bot.add_cog(Tools(bot)) 130 | -------------------------------------------------------------------------------- /cogs/individual/transit.py: -------------------------------------------------------------------------------- 1 | # Free RT - transit 2 | 3 | from discord.ext import commands 4 | import aiohttp 5 | from urllib.parse import quote_plus 6 | import discord 7 | 8 | 9 | class transit(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | self.BASE_URL = "https://ysmsrv.wjg.jp/transit/index_raw.php?from=" 13 | 14 | @commands.command( 15 | aliases=["乗り換え案内"], 16 | extras={ 17 | "headding": {"ja": "乗り換え案内を表示します。", "en": "..."}, 18 | "parent": "Individual" 19 | } 20 | ) 21 | async def transit(self, ctx, depature, to): 22 | async with aiohttp.ClientSession() as session: 23 | async with session.get(self.BASE_URL + quote_plus(depature, encoding='utf-8') + "&to=" + quote_plus(to, encoding='utf-8')) as resp: 24 | sid = await resp.text() 25 | ssplit = sid.splitlines() 26 | ssplit.sort(key=len) 27 | embed = discord.Embed(title=f"{depature}駅から{to}駅までの行き方", description=ssplit[0], color=0x0066ff) 28 | embed.set_footer(text="乗り換え案内") 29 | await ctx.send(embed=embed) 30 | 31 | 32 | async def setup(bot): 33 | await bot.add_cog(transit(bot)) 34 | -------------------------------------------------------------------------------- /cogs/individual/translator.py: -------------------------------------------------------------------------------- 1 | # Free RT - Google Translator 2 | 3 | from discord.ext import commands 4 | import discord 5 | 6 | from jishaku.functools import executor_function 7 | from asyncio import sleep 8 | import deep_translator 9 | 10 | from util import RT 11 | 12 | 13 | CHP_HELP = { 14 | "ja": ("翻訳専用チャンネル機能。", 15 | """# 翻訳チャンネルプラグイン - translate 16 | これは`rf>translate <翻訳先言語コード>`をチャンネルのトピックに入れることで翻訳専用チャンネルにすることのできる機能です。 17 | 例:`rf>translate ja` (これをトピックに入れたチャンネルに送信したメッセージは全て日本語に翻訳されます。) 18 | 19 | ### 言語コード例 20 | ``` 21 | 日本語 ja 22 | 英語  en 23 | 自動  auto 24 | ``` 25 | 他は調べれば出るので`言語名 言語コード`とかで調べてください。 26 | 27 | ### エイリアス 28 | trans, ほんやく, 翻訳 29 | 30 | ### これもあるよ 31 | 翻訳コマンドである`translate`で個人カテゴリーにあります。"""), 32 | "en": ("Dedicated translation channel function", """# translation channel plugin - translate 33 | This is a feature that allows you to make a channel dedicated to translation by putting \ 34 | `rf>translate ` in the channel topic. 35 | Example: `rf>translate ja` (all messages sent to a channel with this in the topic will be translated into Japanese). 36 | 37 | ### Language code example 38 | ``` 39 | Japanese `ja` 40 | English `en` 41 | ``` 42 | Other codes can be found by looking up ` code` or something like that. 43 | 44 | ### Alias 45 | trans 46 | 47 | ### Also see 48 | It's in the personal category with the `translate` command.""") 49 | } 50 | 51 | 52 | class Translator(commands.Cog): 53 | def __init__(self, bot: RT): 54 | self.bot = bot 55 | 56 | @executor_function 57 | def translate(self, text: str, target: str) -> str: 58 | return deep_translator.GoogleTranslator(target=target).translate(text) 59 | 60 | async def cog_load(self): 61 | # ヘルプにチャンネルプラグイン版翻訳を追加するだけ。 62 | await sleep(1.5) 63 | for lang in CHP_HELP: 64 | self.bot.cogs["DocHelp"].add_help( 65 | "ChannelPlugin", "TranslateChannel", 66 | lang, *CHP_HELP[lang] 67 | ) 68 | 69 | @commands.command( 70 | name="translate", aliases=["trans", "ほんやく", "翻訳"], extras={ 71 | "headding": {"ja": "翻訳をします。", "en": "This can do translate."}, 72 | "parent": "Individual" 73 | } 74 | ) 75 | @commands.cooldown(1, 5, commands.BucketType.user) 76 | async def translate_(self, ctx, lang, *, content): 77 | """!lang ja 78 | -------- 79 | 翻訳をします。 80 | 81 | Parameters 82 | ---------- 83 | lang : 言語コード 84 | どの言語に翻訳するかの言語コードです。 85 | 例えば日本語にしたい場合は`ja`で、英語にしたい場合は`en`です。 86 | `auto`とすると自動で翻訳先を英語または日本語に設定します。 87 | content : str 88 | 翻訳する内容です。 89 | 90 | Examples 91 | -------- 92 | `rt!translate ja I wanna be the guy!` 93 | RT:男になりたい! 94 | 95 | Aliases 96 | ------- 97 | trans, ほんやく, 翻訳 98 | 99 | See Also 100 | -------- 101 | translate(チャンネルプラグイン) : 翻訳専用チャンネル機能。 102 | 103 | !lang en 104 | -------- 105 | This can do translate. 106 | 107 | Parameters 108 | ---------- 109 | lang : language code 110 | The language code for which language to translate. 111 | If you want use japanese you do `ja` and If you want to use English you do `en`. 112 | content : str 113 | Translate content 114 | 115 | Examples 116 | -------- 117 | `rt!translate ja I wanna be the guy!` 118 | RT:男になりたい! 119 | 120 | Aliases 121 | ------- 122 | trans 123 | 124 | See Also 125 | -------- 126 | translate(channel plugin) : Only for translate channel.""" 127 | await ctx.typing() 128 | 129 | if lang == "auto": 130 | # もし自動で翻訳先を判別するなら英文字が多いなら日本語にしてそれ以外は英語にする。 131 | lang = "ja" if ( 132 | sum(64 < ord(char) < 123 for char in content) 133 | >= len(content) / 2 134 | ) else "en" 135 | 136 | try: 137 | await ctx.reply( 138 | embed=discord.Embed( 139 | title={"ja": "翻訳結果", 140 | "en": "translate result"}, 141 | description=await self.translate(content, lang), 142 | color=self.bot.colors["normal"] 143 | ).set_footer( 144 | text="Powered by Google Translate", 145 | icon_url="http://tasuren.syanari.com/RT/GoogleTranslate.png" 146 | ) 147 | ) 148 | except deep_translator.exceptions.LanguageNotSupportedException: 149 | await ctx.reply("その言語は対応していません。") 150 | 151 | @commands.Cog.listener() 152 | async def on_message(self, message: discord.Message): 153 | if isinstance(message.channel, discord.Thread): 154 | return 155 | if ((message.author.bot and not ( 156 | message.author.discriminator == "0000" and " #" in message.author.name 157 | )) or not message.guild or not message.channel.topic): 158 | return 159 | 160 | for line in message.channel.topic.splitlines(): 161 | if line.startswith(("rf>translate", "rf>tran", "rf>翻訳", "rf>ほんやく")): 162 | if 1 < len((splited := line.split())): 163 | try: 164 | message.content = f"{splited[1]} {message.content}" 165 | await self.translate_.invoke( 166 | ctx := await self.bot.get_context(message) 167 | ) 168 | except Exception as e: 169 | self.bot.dispatch("command_error", ctx, e) 170 | break 171 | 172 | 173 | async def setup(bot): 174 | await bot.add_cog(Translator(bot)) 175 | -------------------------------------------------------------------------------- /cogs/music/playlist.py: -------------------------------------------------------------------------------- 1 | # Free RT Music - Playlist 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Union, Optional 6 | 7 | import discord 8 | 9 | from .music import Music, MusicDict 10 | 11 | if TYPE_CHECKING: 12 | from .__init__ import MusicCog 13 | 14 | 15 | def to_musics(data: list[MusicDict], cog: MusicCog, author: discord.Member) -> list[Music]: 16 | return [Music.from_dict(cog, author, data) for data in data] 17 | 18 | 19 | class Playlist: 20 | "プレイリストのクラスです。" 21 | 22 | def __init__(self, data: list[MusicDict], max_: int): 23 | self.data, self.max_ = data, max_ 24 | 25 | def _convert(self, music: Union[Music, MusicDict]) -> MusicDict: 26 | if isinstance(music, Music): 27 | music = music.to_dict() 28 | return music 29 | 30 | @property 31 | def length(self): 32 | "プレイリストにある曲の数を返します。ただのエイリアス" 33 | return len(self.data) 34 | 35 | def to_musics(self, cog: MusicCog, author: discord.Member) -> list[Music]: 36 | "全てをMusicにしたリストを返します。" 37 | return to_musics(self.data, cog, author) 38 | 39 | def add(self, music: Union[Music, MusicDict], length: Optional[int] = None) -> None: 40 | "プレイリストに音楽を追加します。" 41 | assert (length or self.length) < self.max_, { 42 | "ja": "これ以上追加できません。", "en": f"You can't add more than {self.max_}." 43 | } 44 | self.data.append(self._convert(music)) 45 | 46 | def extend(self, musics: list[Union[Music, MusicDict]]) -> None: 47 | "プレイリストにMusicのリストを追加します。" 48 | length = self.length 49 | for music in musics: 50 | self.add(music, length) 51 | 52 | def remove(self, music: Union[Music, MusicDict]) -> bool: 53 | "プレイリストからMusicを削除します。" 54 | music = self._convert(music) 55 | for index in range(self.data): 56 | if (self.data[index]["url"] == music["url"] 57 | or self.data[index]["title"] == music["title"]): 58 | del self.data[index] 59 | return True 60 | return False 61 | 62 | def removes(self, musics: list[Union[Music, MusicDict]]) -> None: 63 | "プレイリストからMusicのリストを消します。" 64 | for music in musics: 65 | self.remove(music) 66 | -------------------------------------------------------------------------------- /cogs/other/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | for name in listdir("cogs/other"): 7 | if not name.startswith(("_", ".")): 8 | try: 9 | await bot.load_extension( 10 | f"cogs.other.{name[:-3] if name.endswith('.py') else name}") 11 | except Exception: 12 | traceback.print_exc() 13 | else: 14 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 15 | -------------------------------------------------------------------------------- /cogs/other/github.py: -------------------------------------------------------------------------------- 1 | # Free RT - Github 2 | from discord.ext import commands 3 | 4 | 5 | class Github(commands.Cog): 6 | def __init__(self, bot): 7 | self.bot = bot 8 | self.github_url = "https://api.github.com/repos/free-RT/rt-bot/issues" 9 | self.github_token = bot.secret.get("github", "") 10 | 11 | @commands.group(name="github") 12 | async def github(self, ctx): 13 | if ctx.invoked_subcommand: 14 | return await ctx.send("使い方が間違っています") 15 | 16 | @github.command(name="issue") 17 | async def issue(self, ctx, title, *, description): 18 | title = title + f"{ctx.author.name} ({ctx.author.id})" 19 | data = { 20 | "title": title, 21 | "body": description, 22 | } 23 | headers = { 24 | "Authorization": "Bearer {}".format(self.github_token) 25 | } 26 | async with self.bot.session.post(self.github_url, data=data, headers=headers): 27 | await ctx.send("issueを登録しました") 28 | 29 | 30 | async def setup(bot): 31 | await bot.add_cog(Github(bot)) 32 | -------------------------------------------------------------------------------- /cogs/other/oldcaptcha/image_captcha.py: -------------------------------------------------------------------------------- 1 | # Free RT - Captcha Image Manager 2 | 3 | from typing import TYPE_CHECKING, Optional, Dict, Tuple 4 | 5 | import discord 6 | 7 | from jishaku.functools import executor_function 8 | from captcha.image import ImageCaptcha 9 | from aiofiles.os import remove 10 | from random import randint 11 | from os import listdir 12 | from time import time 13 | 14 | if TYPE_CHECKING: 15 | from .__init__ import Captcha 16 | 17 | 18 | class ImageCaptcha(ImageCaptcha): 19 | 20 | PASSWORD_LENGTH = 5 21 | 22 | def __init__( 23 | self, captcha_cog: "Captcha", 24 | font_path: str = "data/captcha/SourceHanSans-Normal.otf" 25 | ): 26 | self.cog = captcha_cog 27 | super().__init__(fonts=[font_path]) 28 | self.queue: Dict[str, Tuple[str, float]] = {} 29 | self.cog.bot.add_listener(self.on_message, "on_message") 30 | self.cog.bot.add_listener(self.on_close, "on_close") 31 | 32 | @executor_function 33 | def create_image( 34 | self, path: str, characters: Optional[str] = None 35 | ) -> str: 36 | characters = "".join( 37 | str(randint(0, 9)) 38 | for _ in range(self.PASSWORD_LENGTH) 39 | ) if characters is None else characters 40 | self.write(characters, path) 41 | return characters 42 | 43 | async def captcha( 44 | self, channel: discord.TextChannel, member: discord.Member 45 | ) -> None: 46 | name = f"{channel.id}-{member.id}" 47 | path = f"data/captcha/{name}.png" 48 | self.queue[name] = (await self.create_image(path), time()) 49 | await channel.send( 50 | {"ja": f"{member.mention}, 画像にある数字を入力してください。" 51 | "\n放置すると無効になります。", 52 | "en": f"{member.mention}, Please, type number on the picture." 53 | "\nIf you leave it, it will become invalid."}, 54 | target=member.id, file=discord.File(path) 55 | ) 56 | await remove(path) 57 | 58 | async def on_message(self, message: discord.Message) -> None: 59 | name = f"{message.channel.id}-{message.author.id}" 60 | if name in self.queue and len(message.content) == self.PASSWORD_LENGTH: 61 | if message.content == self.queue[name][0]: 62 | row = await self.cog.load(message.guild.id) 63 | role = message.guild.get_role(row[3]) 64 | 65 | if role: 66 | try: 67 | await message.author.add_roles(role) 68 | except Exception as e: 69 | await message.channel.send( 70 | {"ja": (f"{message.author.mention}, 役職を付与することができませんでした。\n" 71 | "付与する役職の位置がFree RTより下にあるか確認してください。\n" 72 | f"エラーコード:`{e}`"), 73 | "en": f"{message.author.mention}, Failed, make sure that the role position below the Free RT role position."} 74 | ) 75 | else: 76 | await message.channel.send( 77 | {"ja": f"{message.author.mention}, 認証に成功しました。", 78 | "en": f"{message.author.mention}, Success!"} 79 | ) 80 | del self.queue[name] 81 | self.cog.remove_cache(message.author) 82 | else: 83 | await message.channel.send( 84 | {"ja": f"{message.author.mention}, 設定されている役職が見つからないため認証に失敗しました。", 85 | "en": f"{message.author.mention}, Failed, I couldn't find the role to add you."} 86 | ) 87 | else: 88 | await message.channel.send( 89 | message.author.mention, 90 | embed=discord.Embed( 91 | description={ 92 | "ja": "認証に失敗しました。\nもしできているはずなのにできない際はこちらを確認してください。\nhttp://tasuren.syanari.com/RT/careful.png", 93 | "en": "Failed, Please confirm your number is true.\nNote that 1 and 7 are similar, so please pay attention to that." 94 | }, color=self.cog.bot.colors["normal"] 95 | ) 96 | ) 97 | 98 | async def on_close(self, _): 99 | # Bot終了時にもし画像認証の画像が残っているのなら削除しておく。 100 | for name in listdir("data/captcha"): 101 | if name.endswith(".png"): 102 | await remove(f"data/captcha/{name}") 103 | -------------------------------------------------------------------------------- /cogs/other/oldcaptcha/web_captcha.py: -------------------------------------------------------------------------------- 1 | # Free RT - Captcha Web Manager 2 | 3 | from typing import TYPE_CHECKING, TypedDict, Dict, Tuple 4 | 5 | import discord 6 | 7 | from inspect import cleandoc 8 | from time import time 9 | 10 | if TYPE_CHECKING: 11 | from .__init__ import Captcha 12 | 13 | 14 | class SuccessedUserData(TypedDict): 15 | guild_id: int 16 | user_id: int 17 | channel: discord.TextChannel 18 | 19 | 20 | class WebCaptcha: 21 | def __init__(self, captcha_cog: "Captcha", secret: str): 22 | self.cog = captcha_cog 23 | self.secret: str = secret 24 | self.queue: Dict[str, Tuple[int, float, discord.TextChannel]] = {} 25 | self.base_url = ( 26 | "http://localhost/" 27 | if self.cog.bot.test 28 | else "https://rt-bot.com/" 29 | ) 30 | 31 | async def success_user(self, userdata: SuccessedUserData): 32 | "ユーザーの認証成功時の処理を実行する。" 33 | if ((guild := self.cog.bot.get_guild(userdata["guild_id"])) 34 | and (member := guild.get_member(userdata["user_id"]))): 35 | # 役職などを取得して役職を付与する。 36 | row = await self.cog.load(userdata["guild_id"]) 37 | role = guild.get_role(row[3]) 38 | 39 | if role: 40 | try: 41 | await member.add_roles(role) 42 | except discord.Forbidden: 43 | result = ( 44 | "認証に失敗しました。" 45 | "付与する役職がFree RTの役職より下にあるか確認してください。\n" 46 | "Failed, make sure that the role position below the Free RT role position.\n" 47 | ) 48 | else: 49 | result = ( 50 | "認証に成功しました。" 51 | "役職が付与されました。\n" 52 | "Success!" 53 | ) 54 | self.cog.remove_cache(member) 55 | n = f"{member.guild.id}-{member.id}" 56 | if n in self.queue: 57 | del self.queue[n] 58 | else: 59 | result = ( 60 | "役職が見つからないので役職を付与できませんでした。" 61 | "すみません!!\n" 62 | "Ah, I couldn't find the role to add to you." 63 | ) 64 | else: 65 | result = ( 66 | "あなたの所在がわからないため認証に失敗しました。" 67 | ) 68 | await userdata["channel"].send( 69 | f"<@{userdata['user_id']}>, {result}" 70 | ) 71 | 72 | async def captcha( 73 | self, channel: discord.TextChannel, member: discord.Member 74 | ) -> None: 75 | self.queue[f"{member.guild.id}-{member.id}"] = (member.id, time(), channel) 76 | embed = discord.Embed( 77 | title={"ja": "ウェブ認証", "en": "Web Captcha"}, 78 | description={ 79 | "ja": cleandoc("""喋るには認証をしなければいけません。 80 | 認証を開始するには下にあるボタンから認証ページにアクセスしてください。 81 | ※放置されると無効になります。"""), 82 | "en": cleandoc("""You must do authentication to speak. 83 | Please access to that url to do authentication. 84 | * If you leave it, it will become invalid.""") 85 | }, color=self.cog.bot.colors["normal"] 86 | ) 87 | embed.set_footer( 88 | text="Powered by hCaptcha", icon_url="https://www.google.com/s2/favicons?domain=hcaptcha.com" 89 | ) 90 | view = discord.ui.View() 91 | view.add_item(discord.ui.Button(label="認証を行う", url=f"{self.base_url}captcha")) 92 | await channel.send( 93 | member.mention, embed=embed, view=view, target=member.id 94 | ) 95 | -------------------------------------------------------------------------------- /cogs/other/oldcaptcha/word_captcha.py: -------------------------------------------------------------------------------- 1 | # Free RT - Captcha Word Manager 2 | 3 | from typing import TYPE_CHECKING, Dict, Tuple 4 | 5 | from discord.ext import commands 6 | import discord 7 | 8 | from time import time 9 | 10 | if TYPE_CHECKING: 11 | from .__init__ import Captcha 12 | 13 | 14 | class WordCaptcha(commands.Cog): 15 | def __init__(self, captcha_cog: "Captcha"): 16 | self.cog = captcha_cog 17 | self.queue: Dict[str, Tuple[Tuple[int, str], float]] = {} 18 | self.cog.bot.add_listener(self.on_message, "on_message") 19 | 20 | async def captcha( 21 | self, channel: discord.TextChannel, member: discord.Member 22 | ) -> None: 23 | await channel.send( 24 | {"ja": f"{member.mention}, 合言葉を入力してください。" 25 | "\n放置すると無効になります。", 26 | "en": f"{member.mention}, Please type password." 27 | "\nIf you leave it, it will become invalid."}, 28 | target=member.id 29 | ) 30 | row = await self.cog.load(channel.guild.id) 31 | self.queue[f"{channel.id}-{member.id}"] = ((row[3], row[4]), time()) 32 | 33 | async def on_message(self, message: discord.Message) -> None: 34 | if (row := self.queue.get( 35 | f"{message.channel.id}-{message.author.id}", (None,)))[0]: 36 | row = row[0] 37 | if message.content == row[1]: 38 | role = message.guild.get_role(row[0]) 39 | if role: 40 | try: 41 | await message.author.add_roles(role) 42 | except Exception as e: 43 | await message.channel.send( 44 | {"ja": (f"{message.author.mention}, 認証に失敗しました。\n" 45 | "付与する役職がRTの役職より下にあるか確認してください。\n" 46 | f"エラーコード:{e}"), 47 | "en": f"{message.author.mention}, Failed, make sure that the role position below the RT role position."}, 48 | ) 49 | else: 50 | await message.channel.send( 51 | {"ja": f"{message.author.mention}, 認証に成功しました。", 52 | "en": f"{message.author.mention}, Success!"} 53 | ) 54 | try: 55 | await message.delete() 56 | except (discord.HTTPException, discord.Forbidden): 57 | pass 58 | finally: 59 | self.cog.remove_cache(message.author) 60 | del self.queue[f"{message.channel.id}-{message.author.id}"] 61 | else: 62 | await message.channel.send( 63 | {"ja": f"{message.author.mention}, 役職が見つからないため認証に失敗しました。", 64 | "en": f"{message.author.mention}, Failed, I couldn't find the role to add you."} 65 | ) 66 | else: 67 | await message.channel.send( 68 | {"ja": f"{message.author.mention}, 合言葉が違います。", 69 | "en": f"{message.author.mention}, That password is wrong."} 70 | ) 71 | -------------------------------------------------------------------------------- /cogs/other/token_remover.py: -------------------------------------------------------------------------------- 1 | # Free RT - Token Remover 2 | 3 | from typing import DefaultDict, List 4 | 5 | from discord.ext import commands, tasks 6 | import discord 7 | 8 | from collections import defaultdict 9 | from re import findall 10 | from time import time 11 | 12 | from util import RT 13 | 14 | 15 | class TokenRemover(commands.Cog): 16 | 17 | DEFAULT_TIMEOUT = 900 18 | 19 | def __init__(self, bot: RT): 20 | self.bot = bot 21 | self.cache: DefaultDict[int, DefaultDict[int, List[int, float]]] = \ 22 | defaultdict(lambda: defaultdict(lambda: [0, time() + self.DEFAULT_TIMEOUT])) 23 | self.cache_remover.start() 24 | 25 | def check_token(self, content: str) -> bool: 26 | "TOKENが含まれているか確認します。" 27 | return bool(findall( 28 | r"[N]([a-zA-Z0-9]{23})\.([a-zA-Z0-9]{6})\.([a-zA-Z0-9]{27})", content 29 | )) 30 | 31 | @commands.Cog.listener() 32 | async def on_message(self, message: discord.Message): 33 | if not message.guild or message.author.id == self.bot.user.id: 34 | return 35 | 36 | if self.check_token(message.content): 37 | self.cache[message.guild.id][message.author.id][0] += 1 38 | self.cache[message.guild.id][message.author.id][1] = \ 39 | time() + self.DEFAULT_TIMEOUT 40 | if self.cache[message.guild.id][message.author.id][0] == 5: 41 | await message.reply("TOKENを送るのをやめてください。") 42 | elif self.cache[message.guild.id][message.author.id][0] == 8: 43 | try: 44 | await message.author.ban(reason="TOKENと思われるものを送っていたため。") 45 | except Exception: 46 | pass 47 | finally: 48 | del self.cache[message.guild.id][message.author.id] 49 | 50 | @tasks.loop(seconds=30) 51 | async def cache_remover(self): 52 | now = time() 53 | for guild_id in list(self.cache.keys()): 54 | for user_id in list(self.cache[guild_id].keys()): 55 | if self.cache[guild_id][user_id][1] < now: 56 | del self.cache[guild_id][user_id] 57 | if not self.cache[guild_id]: 58 | del self.cache[guild_id] 59 | 60 | def cog_unload(self): 61 | self.cache_remover.cancel() 62 | 63 | 64 | async def setup(bot): 65 | await bot.add_cog(TokenRemover(bot)) 66 | -------------------------------------------------------------------------------- /cogs/other/topgg.py: -------------------------------------------------------------------------------- 1 | # Free RT - TopGG 2 | 3 | # from topgg import DBLClient 4 | 5 | 6 | async def setup(bot): 7 | # if not hasattr(bot, "topgg") and not bot.test: 8 | # bot.topgg = DBLClient( 9 | # bot, bot.secret["topgg"], 10 | # autopost=True, post_shard_count=True 11 | # ) 12 | pass 13 | -------------------------------------------------------------------------------- /cogs/serverpanel/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | await bot.load_extension("cogs.serverpanel._oldrole") 7 | for name in listdir("cogs/serverpanel"): 8 | if not name.startswith(("_", ".")): 9 | try: 10 | await bot.load_extension( 11 | f"cogs.serverpanel.{name[:-3] if name.endswith('.py') else name}") 12 | except Exception: 13 | traceback.print_exc() 14 | else: 15 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 16 | -------------------------------------------------------------------------------- /cogs/serverpanel/nickname.py: -------------------------------------------------------------------------------- 1 | # Free RT - Nickname Panel 2 | 3 | from discord.ext import commands 4 | import discord 5 | 6 | from emoji import EMOJI_DATA as UNICODE_EMOJI_ENGLISH 7 | from typing import Dict 8 | 9 | 10 | class NicknamePanel(commands.Cog): 11 | def __init__(self, bot): 12 | self.bot = bot 13 | self.emojis = [chr(0x1f1e6 + i) for i in range(26)] 14 | for e in ("add", "remove"): 15 | self.bot.add_listener( 16 | self.on_full_reaction_add_remove, 17 | f"on_full_reaction_{e}" 18 | ) 19 | 20 | def parse_description(self, description: str) -> Dict[str, str]: 21 | # 文字列から絵文字と文字列を分けて取り出す。 22 | data, i, emoji = {}, -1, "" 23 | for line in description.splitlines(): 24 | i += 1 25 | if line and line != "\n": 26 | if line[0] == "<" and all(char in line for char in (">", ":")): 27 | if line.count(">") != 1: 28 | # もし外部絵文字なら。 29 | emoji = line[:line.find(">") + 1] 30 | elif line[0] in UNICODE_EMOJI_ENGLISH or line[0] in self.emojis: 31 | # もし普通の絵文字なら。 32 | emoji = line[0] 33 | else: 34 | # もし絵文字がないのなら作る。 35 | emoji = self.emojis[i] 36 | line = self.emojis[i] + line 37 | 38 | data[emoji] = line.replace(emoji, "") 39 | return data 40 | 41 | @commands.command( 42 | aliases=["nkp", "ニックネームパネル", "ニックパネル", "にっくぱねる", "nicknamepanel"], 43 | extras={ 44 | "headding": { 45 | "ja": "ニックネームパネル", "en": "Nickname Panel" 46 | }, "parent": "ServerPanel" 47 | } 48 | ) 49 | async def nickpanel(self, ctx, *, description): 50 | """!lang ja 51 | -------- 52 | ニックネームパネルを作ります。 53 | 54 | Parameters 55 | ---------- 56 | title : str 57 | ニックネームパネルのタイトルです。 58 | description : str 59 | ニックネームパネルに入れるニックネームです。 60 | ニックネームの最初に`+`を置くと名前の後ろに後付けされます。 61 | 62 | Examples 63 | -------- 64 | ``` 65 | rf!nickpanel 通話中切り替え 66 | +通話中 67 | +聞き専通話中 68 | 私は誰 69 | ``` 70 | 71 | Notes 72 | ----- 73 | リアクションをつけるとニックネームが変わりリアクションを外すと普通の名前になります。 74 | サーバーオーナーなどの権限を持っている人のニックネームをRTが変えることができません。 75 | パネルでニックネームを変更した後手動でニックネームを変更した場合、RTは自動で上書きしません。 76 | 77 | !lang en 78 | -------- 79 | Create a nickname panel. 80 | 81 | Parameters 82 | ---------- 83 | title : str 84 | The title of the nickname panel. 85 | description : str 86 | The nickname to put in the nickname panel. 87 | If you put `+` at the beginning of the nickname, it will be appended after the name. 88 | 89 | Examples 90 | -------- 91 | ``` 92 | rf!nickpanel Toggle in-call 93 | +Calling 94 | +listening-only call 95 | Who am I? 96 | ``` 97 | 98 | Notes 99 | ----- 100 | If you add a reaction, your nickname will change, and if you remove the reaction, it will become your normal name. 101 | RT cannot change the nickname of the server owner or other authorized person.""" 102 | title = description[:(index := description.find("\n"))] 103 | description = description[index:] 104 | emojis = self.parse_description(description) 105 | embed = discord.Embed( 106 | title=title, 107 | description="\n".join( 108 | f"{emoji} {value if value[0] == '+' else '-' + value}" 109 | for emoji, value in emojis.items() if value 110 | ), 111 | color=ctx.author.color 112 | ) 113 | embed.set_footer( 114 | text="※連打防止のため反映が遅れることがあります。" 115 | ) 116 | message = await ctx.channel.webhook_send( 117 | username=ctx.author.display_name, 118 | avatar_url=ctx.author.display_avatar.url if ctx.author.display_avatar else "", 119 | content="RT Nickname Panel", embed=embed, wait=True 120 | ) 121 | for emoji in emojis: 122 | await message.add_reaction(emoji) 123 | 124 | async def on_full_reaction_add_remove( 125 | self, payload: discord.RawReactionActionEvent, 126 | ): 127 | if (not hasattr(payload, "message") or not payload.message.guild 128 | or not payload.message.author.bot 129 | or "RT Nickname Panel" != payload.message.content 130 | or not payload.message.embeds or payload.member.bot 131 | or not payload.message.embeds[0].description): 132 | return 133 | 134 | emojis = self.parse_description( 135 | payload.message.embeds[0].description 136 | ) 137 | if (value := emojis.get(str(payload.emoji))): 138 | nick = ( 139 | ( 140 | (payload.member.display_name + value.replace("+", "", 1)) 141 | if "+" in value else value.replace("-", "", 1) 142 | ) if payload.event_type == "REACTION_ADD" 143 | else payload.member.name 144 | ) 145 | if payload.member.nick != nick: 146 | try: 147 | await payload.member.edit(nick=nick) 148 | except (discord.Forbidden, discord.HTTPException): 149 | pass 150 | 151 | 152 | async def setup(bot): 153 | await bot.add_cog(NicknamePanel(bot)) 154 | -------------------------------------------------------------------------------- /cogs/serversafety/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | for name in listdir("cogs/serversafety"): 7 | if not name.startswith(("_", ".")): 8 | try: 9 | await bot.load_extension( 10 | f"cogs.serversafety.{name[:-3] if name.endswith('.py') else name}") 11 | except Exception: 12 | traceback.print_exc() 13 | else: 14 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 15 | -------------------------------------------------------------------------------- /cogs/serversafety/automod/cache.py: -------------------------------------------------------------------------------- 1 | # Free RT AutoMod - Cache 2 | 3 | from typing import TYPE_CHECKING, Optional, Any, Dict, List 4 | 5 | import discord 6 | 7 | from time import time 8 | 9 | from .modutils import join 10 | 11 | if TYPE_CHECKING: 12 | from .__init__ import AutoMod 13 | 14 | 15 | class Cache: 16 | "スパム検知度等のキャッシュ兼ユーザーデータクラスです。" 17 | 18 | # アノテーションをクラスにつけているものはUserDataです。 19 | warn: float = 0.0 20 | last_update: float 21 | 22 | # キャッシュのタイムアウト 23 | TIMEOUT = 180 24 | # あやしいレベルのマックスで`suspicious`がこれになると1警告数が上がる。 25 | MAX_SUSPICIOUS = 150 26 | 27 | def __init__( 28 | self, cog: "AutoMod", member: discord.Member, 29 | guild: discord.Guild, data: dict 30 | ): 31 | self.guild, self.member, self.cog = guild, member, cog 32 | self.require_save = False 33 | self.update_timeout() 34 | # 初期状態のデータを書き込む。アノテーションがついている変数に書き込まれるべきです。 35 | for key in data: 36 | setattr(self, key, data[key]) 37 | self.last_update = time() 38 | # 以下以降スパムチェックに使うキャッシュの部分です。 39 | self.before: Optional[discord.Message] = None 40 | self.before_content: Optional[discord.Message] = None 41 | self.before_join: Optional[float] = None 42 | self.suspicious = 0 43 | 44 | def process_suspicious(self) -> bool: 45 | "怪しさがMAXかどうかをチェックします。もしMAXならリセットします。" 46 | if self.suspicious >= self.MAX_SUSPICIOUS: 47 | self.suspicious = 0 48 | self.warn += 1 49 | self.cog.print("[warn.up]", self) 50 | return True 51 | return False 52 | 53 | def update_cache(self, message: discord.Message): 54 | "キャッシュをアップデートします。" 55 | self.update_timeout() 56 | before = self.before 57 | self.before_content = join(self.before or message) 58 | self.before = message 59 | return before 60 | 61 | def update_timeout(self): 62 | "タイムアウトを更新します。" 63 | self.checked = time() 64 | self.timeout = self.checked + self.TIMEOUT 65 | 66 | def keys(self) -> List[str]: 67 | "このデータクラスにあるキーのリストを返します。" 68 | return list(self.__annotations__.keys()) 69 | 70 | def values(self) -> List[Any]: 71 | "このデータクラスにある値のリストを返します。" 72 | return [getattr(self, name) for name in self.keys()] 73 | 74 | def items(self) -> Dict[str, Any]: 75 | "このデータクラスにあるデータを辞書で返します。" 76 | return {key: value for key, value in zip(self.keys(), self.values())} 77 | 78 | def update(self, data: "Cache"): 79 | "このデータクラスにあるデータを更新します。" 80 | for key, value in data.items(): 81 | setattr(self, key, value) 82 | 83 | def __setattr__(self, key, value): 84 | if key in self.__annotations__: 85 | # もしセーブデータが書き換えられたのなら更新が必要とする。最終更新日も更新をする。 86 | if key != "last_update": 87 | self.require_save = True 88 | self.last_update = time() 89 | return super().__setattr__(key, value) 90 | 91 | def __str__(self): 92 | return f"" 94 | -------------------------------------------------------------------------------- /cogs/serversafety/captcha/click.py: -------------------------------------------------------------------------------- 1 | # Free RT Captcha - Click 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from discord import Interaction 6 | 7 | from .image import add_roles 8 | 9 | if TYPE_CHECKING: 10 | from .__init__ import Captcha 11 | 12 | 13 | class ClickCaptcha: 14 | def __init__(self, cog: "Captcha"): 15 | self.cog = cog 16 | self.captcha = self 17 | self.on_success = self.on_failed 18 | 19 | async def on_captcha(self, interaction: Interaction): 20 | # クリックするだけの認証方式のため付与するだけ。 21 | return await add_roles(self, interaction, True) 22 | 23 | async def on_failed(self, guild_id: int, user_id: int): 24 | ... 25 | -------------------------------------------------------------------------------- /cogs/serversafety/captcha/web.py: -------------------------------------------------------------------------------- 1 | # Free RT Captcha - Web Captcha 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | import discord 6 | 7 | from urllib.parse import quote 8 | from reprypt import encrypt 9 | 10 | from .image import Select, make_random_string 11 | 12 | if TYPE_CHECKING: 13 | from .__init__ import Captcha 14 | 15 | 16 | class WebCaptchaView(discord.ui.View): 17 | def __init__(self, captcha: "WebCaptcha", *args, **kwargs): 18 | self.captcha, self.characters = captcha, make_random_string( 19 | captcha.password_length 20 | ) 21 | self.on_success = captcha.cog.remove_queue 22 | super().__init__(*args, **kwargs) 23 | self.add_item(discord.ui.Button( 24 | label="Go to captcha page | 認証ページに行く", 25 | emoji="<:hCaptcha:923086020570927114>", url="".join(( 26 | self.captcha.cog.bot.get_website_url(), 27 | "captcha?session=", quote(encrypt( 28 | self.characters, self.captcha.cog.bot.secret["secret"] 29 | )) 30 | )) 31 | )) 32 | self.add_item(Select( 33 | self, placeholder="The code in the webpage | ウェブページにあったコード" 34 | )) 35 | 36 | async def on_failed(self, guild_id: int, user_id: int): 37 | ... 38 | 39 | 40 | class WebCaptcha: 41 | def __init__(self, cog: "Captcha", password_length: int = 5): 42 | self.cog, self.password_length = cog, password_length 43 | self.view = discord.ui.View() 44 | 45 | async def on_captcha(self, interaction: discord.Interaction): 46 | await interaction.response.send_message( 47 | {"ja": "以下のボタンを押してウェブにある認証ページにてコードを取得してください。\nそしたら取得したコードと同じコードを選んでください。", 48 | "en": "Click on the button below to get the code on the captcha page on the web.\nThen select the same code as the one you got."}, 49 | view=WebCaptchaView(self, timeout=360), ephemeral=True 50 | ) 51 | -------------------------------------------------------------------------------- /cogs/serversafety/captcha/word.py: -------------------------------------------------------------------------------- 1 | # Free RT Captcha - Word 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | import discord 6 | 7 | if TYPE_CHECKING: 8 | from .__init__ import Captcha 9 | 10 | 11 | class WordCaptcha: 12 | def __init__(self, cog: "Captcha"): 13 | self.cog = cog 14 | 15 | async def on_message(self, message: discord.Message): 16 | if (message.channel.id == self.cog.queue[message.guild.id] 17 | [message.author.id][2].extras["data"]["channel_id"]): 18 | if (message.content == self.cog.queue[message.guild.id] 19 | [message.author.id][2].extras["data"]["word"]): 20 | await message.delete() 21 | role = message.guild.get_role( 22 | self.cog.queue[message.guild.id][message.author.id][2].role_id 23 | ) 24 | if role: 25 | try: 26 | await message.author.add_roles(role) 27 | except discord.Forbidden: 28 | await message.channel.send( 29 | f"{message.author.mention}, 役職を権限がないため付与できませんでした。" 30 | ) 31 | else: 32 | await message.channel.send(f"{message.author.mention}, 認証に成功しました。") 33 | await self.cog.remove_queue(message.guild.id, message.author.id) 34 | else: 35 | await message.channel.send( 36 | f"{message.author.mention}, 付与する役職が見つかりませんでした。" 37 | ) 38 | else: 39 | await message.reply("合言葉が違います。") 40 | 41 | async def on_member_join(self, member: discord.Member): 42 | if (channel := member.guild.get_channel( 43 | self.cog.queue[member.guild.id][member.id][2] 44 | .extras["data"]["channel_id"] 45 | )): 46 | await channel.send(f"{member.mention}, 合言葉を送信してください。") 47 | -------------------------------------------------------------------------------- /cogs/serversafety/moderation.py: -------------------------------------------------------------------------------- 1 | # Free RT - Moderation 2 | 3 | from discord.ext import commands 4 | from discord import app_commands 5 | 6 | from util import RT 7 | 8 | 9 | class Moderation(commands.Cog): 10 | def __init__(self, bot: RT): 11 | self.bot = bot 12 | 13 | @commands.hybrid_command( 14 | extras={ 15 | "headding": { 16 | "ja": "メンバーのBAN", 17 | "en": "BAN members" 18 | }, "parent": "ServerSafety" 19 | }, aliases=["バン", "ばん", "BAN"] 20 | ) 21 | @commands.has_guild_permissions(ban_members=True) 22 | @app_commands.describe(members="対象のメンバー") 23 | async def ban( 24 | self, ctx, *, members: str 25 | ): 26 | """!lang ja 27 | -------- 28 | メンバーをBANできます。 29 | 30 | Parameters 31 | ---------- 32 | members : メンバーのメンションか名前 33 | 誰をBANするかです。 34 | カンマで区切って複数人指定もできます。 35 | 36 | !lang en 37 | -------- 38 | Ban members 39 | 40 | Parameters 41 | ---------- 42 | members : Mention or Name of members 43 | Target members. 44 | 45 | Examples 46 | -------- 47 | `rf!ban @tasuren @tasuren-sub`""" 48 | mode = "ban" 49 | members = [ 50 | await commands.UserConverter().convert(ctx, member) 51 | for member in members.split(",") 52 | ] 53 | excepts = [] 54 | for m in members: 55 | try: 56 | await getattr(ctx.guild, mode)(m, reason=f"free RT コマンド / 実行者:{ctx.author}") 57 | except Exception: 58 | excepts.append(m) 59 | if excepts: 60 | mode = mode.upper() 61 | await ctx.reply( 62 | f"{mode}を実行しました。\n(しかし、{', '.join(map(str, excepts))}の{mode}に失敗しました。)", 63 | delete_after=5 64 | ) 65 | else: 66 | await ctx.reply("ok", delete_after=5) 67 | 68 | @commands.has_permissions(kick_members=True) 69 | @commands.hybrid_command( 70 | extras={ 71 | "headding": { 72 | "ja": "メンバーのキック", 73 | "en": "Kick members" 74 | }, "parent": "ServerSafety" 75 | }, aliases=["キック", "きっく", "KICK"] 76 | ) 77 | @app_commands.describe(members="対象のメンバー") 78 | async def kick(self, ctx, *, members): 79 | """!lang ja 80 | -------- 81 | メンバーをキックできます。 82 | 83 | Parameters 84 | ---------- 85 | members : メンバーのメンションか名前 86 | 誰をBANするかです。 87 | カンマで区切って複数人指定もできます。 88 | 89 | !lang en 90 | -------- 91 | Kick members. 92 | 93 | Parameters 94 | ---------- 95 | members : Mention or Name of members 96 | Target members. 97 | 98 | Examples 99 | -------- 100 | `rf!ban @tasuren @tasuren-sub`""" 101 | await self.ban(ctx, members=members, mode="kick") 102 | 103 | kick._callback.__doc__ = ban._callback.__doc__.replace("ban", "kick").replace("BAN", "Kick") \ 104 | .replace("Ban", "Kick") 105 | 106 | 107 | async def setup(bot): 108 | await bot.add_cog(Moderation(bot)) 109 | -------------------------------------------------------------------------------- /cogs/serversafety/noicon_notice.py: -------------------------------------------------------------------------------- 1 | # Free RT - No Icon Notice 2 | 3 | from typing import Optional 4 | 5 | from discord.ext import commands 6 | from discord import app_commands 7 | import discord 8 | 9 | from aiomysql import Pool, Cursor 10 | 11 | from util import DatabaseManager 12 | from util import RT 13 | 14 | 15 | class DataManager(DatabaseManager): 16 | 17 | TABLE = "NoIconNotice" 18 | 19 | def __init__(self, pool: Pool): 20 | self.pool = pool 21 | pool._loop.create_task(self._prepare_table()) 22 | 23 | async def _prepare_table(self, cursor: Cursor = None): 24 | await cursor.execute( 25 | f"""CREATE TABLE IF NOT EXISTS {self.TABLE} ( 26 | GuildID BIGINT PRIMARY KEY NOT NULL, Text TEXT 27 | );""" 28 | ) 29 | 30 | async def write(self, guild_id: int, text: str, cursor: Cursor = None) -> None: 31 | "書き込みをします。もし空の文字列が渡された場合はデータを消します。" 32 | if text: 33 | await cursor.execute( 34 | f"""INSERT INTO {self.TABLE} VALUES (%s, %s) 35 | ON DUPLICATE KEY UPDATE Text = %s;""", 36 | (guild_id, text, text) 37 | ) 38 | else: 39 | assert (await self._read(cursor, guild_id)) is not None, { 40 | "ja": "既に設定がありません。", "en": "Already deleted" 41 | } 42 | await cursor.execute( 43 | f"DELETE FROM {self.TABLE} WHERE GuildID = %s;", 44 | (guild_id,) 45 | ) 46 | 47 | async def _read(self, cursor, guild_id: int) -> Optional[str]: 48 | await cursor.execute( 49 | f"SELECT Text FROM {self.TABLE} WHERE GuildID = %s;", (guild_id,) 50 | ) 51 | if row := await cursor.fetchone(): 52 | return row[0] 53 | 54 | async def read(self, guild_id: int, cursor: Cursor = None) -> Optional[str]: 55 | "読み込みをします。" 56 | return await self._read(cursor, guild_id) 57 | 58 | 59 | class NoIconNotice(commands.Cog, DataManager): 60 | def __init__(self, bot: RT): 61 | self.bot = bot 62 | super(commands.Cog, self).__init__(self.bot.mysql.pool) 63 | 64 | @commands.hybrid_command(aliases=("nin", "アイコン無し通知"), extras={ 65 | "headding": {"ja": "アイコン未設定ユーザーに警告", "en": "Notice to user unset icon"}, 66 | "parent": "ServerSafety" 67 | }) 68 | @commands.has_guild_permissions(administrator=True) 69 | @commands.cooldown(1, 10, commands.BucketType.guild) 70 | @app_commands.describe(text="送信する文字列(指定しなければオフ)") 71 | async def noinotice(self, ctx, *, text=""): 72 | """!lang ja 73 | ------- 74 | アイコンを設定していない人がサーバーに参加した際に、指定したメッセージをその人のDMに送信します。 75 | 76 | Parameters 77 | ---------- 78 | text : str, default "" 79 | 送信する文字列です。 80 | もし何も入力しなかった場合は機能をオフにします。 81 | 82 | Aliases 83 | ------- 84 | nin, アイコン無し通知 85 | 86 | !lang en 87 | -------- 88 | When a person who has not set an icon joins the server, the specified message is sent to that person's DM. 89 | 90 | Parameters 91 | ---------- 92 | text : str, default "" 93 | The string to be sent. 94 | If nothing is entered it will be interpreted as off. 95 | 96 | Aliases 97 | ------- 98 | nin""" 99 | await ctx.typing() 100 | assert len(text) < 1500, { 101 | "ja": "文章は1500文字以内である必要があります。", 102 | "en": "Text must be no more than 1500 characters." 103 | } 104 | await self.write(ctx.guild.id, text) 105 | await ctx.reply("Ok") 106 | 107 | @commands.Cog.listener() 108 | async def on_member_join(self, member: discord.Member): 109 | if member.avatar is None: 110 | if text := await self.read(member.guild.id): 111 | await member.send(f"**Notice from {member.guild.name}:**\n{text}") 112 | 113 | 114 | async def setup(bot): 115 | await bot.add_cog(NoIconNotice(bot)) 116 | -------------------------------------------------------------------------------- /cogs/servertool/auto_role.py: -------------------------------------------------------------------------------- 1 | # Free RT - Auto Role 2 | 3 | from discord.ext import commands 4 | from discord import app_commands 5 | import discord 6 | 7 | from util.mysql_manager import DatabaseManager 8 | 9 | 10 | class DataManager(DatabaseManager): 11 | 12 | DB = "AutoRole" 13 | 14 | def __init__(self, db): 15 | self.db = db 16 | 17 | async def init_table(self, cursor) -> None: 18 | await cursor.create_table( 19 | self.DB, { 20 | "GuildID": "BIGINT", "Role": "BIGINT" 21 | } 22 | ) 23 | 24 | async def write(self, cursor, guild_id: int, role_id: int) -> None: 25 | target, change = {"GuildID": guild_id}, {"Role": role_id} 26 | if await cursor.exists(self.DB, target): 27 | await cursor.update_data(self.DB, change, target) 28 | else: 29 | target.update(change) 30 | await cursor.insert_data(self.DB, target) 31 | 32 | async def read(self, cursor, guild_id: int) -> tuple: 33 | target = {"GuildID": guild_id} 34 | if await cursor.exists(self.DB, target): 35 | return await cursor.get_data(self.DB, target) 36 | return () 37 | 38 | async def delete(self, cursor, guild_id: int) -> None: 39 | target = {"GuildID": guild_id} 40 | await cursor.delete(self.DB, target) 41 | 42 | 43 | class AutoRole(commands.Cog, DataManager): 44 | def __init__(self, bot): 45 | self.bot = bot 46 | 47 | async def cog_load(self): 48 | super(commands.Cog, self).__init__(self.bot.mysql) 49 | await self.init_table() 50 | 51 | @commands.hybrid_command( 52 | aliases=["are", "自動役職", "オートロール", "おーとろーる"], extras={ 53 | "headding": { 54 | "ja": "サーバーに誰かが参加した際に指定された役職を自動で付与します。", 55 | "en": "Automatically assigns a role to a user when he or she joins the server." 56 | }, "parent": "ServerTool" 57 | } 58 | ) 59 | @commands.cooldown(1, 10, commands.BucketType.guild) 60 | @commands.has_guild_permissions(administrator=True) 61 | @app_commands.describe(onoff="有効にするか無効にするか", role="付けるロール") 62 | async def autorole(self, ctx, onoff: bool, *, role: discord.Role = None): 63 | """!lang ja 64 | -------- 65 | ユーザーがサーバーに参加した際に自動で役職を付与します。 66 | 67 | Parameters 68 | ---------- 69 | onoff : bool 70 | この設定を有効にする場合はonで無効にするならoffを入力します。 71 | role : 役職の名前かメンション 72 | サーバーに誰かが参加した際に何の役職を付与するかです。 73 | もしonoffをoffにした場合はこれは入力しなくて良いです。 74 | 75 | Examples 76 | -------- 77 | `rf!autorole on 初心者` 78 | 79 | Aliases 80 | ------- 81 | are, 自動役職, オートロール, おーとろーる 82 | 83 | !lang en 84 | -------- 85 | Automatically assigns a role to a user when he or she joins the server. 86 | 87 | Parameters 88 | ---------- 89 | onoff : bool 90 | Enter "on" to enable this setting, or "off" to disable it. 91 | role : The name of the role or a mention. 92 | When someone joins the server, what role will be assigned to them. 93 | If you set onoff to off, you don't need to enter this. 94 | 95 | Examples 96 | -------- 97 | `rf!autorole on beginner`. 98 | 99 | Aliases 100 | ------- 101 | are""" 102 | if onoff: 103 | if role: 104 | await self.write(ctx.guild.id, role.id) 105 | await ctx.reply("Ok") 106 | else: 107 | await ctx.reply( 108 | {"ja": "設定をONにする場合は役職を指定してください。", 109 | "en": "If you want to set auto role, you must specify role to command."} 110 | ) 111 | else: 112 | await self.delete(ctx.guild.id) 113 | await ctx.reply("Ok") 114 | 115 | @commands.Cog.listener() 116 | async def on_member_join(self, member): 117 | row = await self.read(member.guild.id) 118 | if row: 119 | if (role := member.guild.get_role(row[1])): 120 | try: 121 | await member.add_roles(role) 122 | except discord.Forbidden: 123 | pass 124 | else: 125 | await self.delete(member.guild.id) 126 | 127 | 128 | async def setup(bot): 129 | await bot.add_cog(AutoRole(bot)) 130 | -------------------------------------------------------------------------------- /cogs/servertool/thread_manager/constants.py: -------------------------------------------------------------------------------- 1 | # Free-RT.thread_manager - Constants 2 | 3 | 4 | DB = "ThreadManager" # データベースのテーブル名です。 5 | MAX_CHANNELS = 10 # 設定できるチャンネル数の最大です。 6 | 7 | 8 | # スレッド作成専用チャンネルのチャンネルプラグインのヘルプです。 9 | HELP = { 10 | "ja": ("スレッド作成専用チャンネル", 11 | """# スレッド作成専用チャンネルプラグイン 12 | `rt>thread`をチャンネルのトピックに入れることでそのチャンネルにメッセージを送信するとスレッドが自動でつくられるようになります。 13 | Botが送信したメッセージでもスレッドを作るようにするには`rt>thread bot`のようにしてください。 14 | 15 | ### 注意 16 | この機能を使ったチャンネルで低速モードが十秒より下な場合は自動で低速モードが十秒で設定されます。 17 | これはスレッドの作りすぎでAPI制限になりRTがDiscordと通信できなくなるというのを防ぐためです。 18 | ご了承ください。"""), 19 | "en": ("Dedicated thread creation channel function", 20 | """# Dedicated thread creation channel plugin 21 | By adding `rt>thread` to the channel topic, a thread will be created automatically when a message is sent to the channel. 22 | If you want to create thread by bot, you can enable this by adding `rt>thread bot`. 23 | 24 | ### Warning. 25 | If your channel uses this feature and the slow mode is lower than 10 seconds, the slow mode will be automatically set to 10 seconds. 26 | The reason for this is to prevent excessive thread creation, which may limit the API and prevent RT from communicating with Discord. 27 | Thank you for your understanding.""") 28 | } 29 | -------------------------------------------------------------------------------- /cogs/servertool/thread_manager/dataclass.py: -------------------------------------------------------------------------------- 1 | # Free-RT.thread_manager - Data Class 2 | 3 | from typing import TYPE_CHECKING, Union, Dict 4 | 5 | import discord 6 | 7 | from asyncio import Event, wait_for, TimeoutError 8 | from functools import wraps 9 | 10 | from util import Table 11 | 12 | from .constants import DB, MAX_CHANNELS 13 | 14 | if TYPE_CHECKING: 15 | from .__init__ import ThreadManager 16 | 17 | 18 | class ThreadNotification(Table): 19 | __allocation__ = "GuildID" 20 | channels: list[tuple[int, int]] 21 | 22 | 23 | class DataManager: 24 | def __init__(self, cog: "ThreadManager"): 25 | self.cog, self.pool = cog, cog.pool 26 | self.cog.bot.loop.create_task(self.prepare_table()) 27 | self.notification = ThreadNotification(self.cog.bot) 28 | 29 | async def prepare_table(self) -> None: 30 | """テーブルを準備する。""" 31 | async with self.pool.acquire() as conn: 32 | async with conn.cursor() as cursor: 33 | await cursor.execute( 34 | f"""CREATE TABLE IF NOT EXISTS {DB} ( 35 | GuildID BIGINT, ChannelID BIGINT PRIMARY KEY NOT NULL 36 | );""" 37 | ) 38 | 39 | def check_notification_onoff(self, guild_id: int) -> bool: 40 | return "channels" in self.notification[guild_id] 41 | 42 | async def process_notification(self, thread: discord.Thread, mode: str) -> None: 43 | if self.check_notification_onoff(thread.guild.id): 44 | for tentative in self.notification[thread.guild.id].channels: 45 | if tentative[0] == thread.parent_id: 46 | await thread.send(f"<@&{tentative[1]}>, {mode}") 47 | break 48 | 49 | def get_data(self, guild_id: int) -> "GuildData": 50 | """サーバーのデータクラスを取得します。""" 51 | return GuildData(self.cog, guild_id) 52 | 53 | 54 | def wait_ready(coro): 55 | # データベースの準備ができるまで待つ。 56 | @wraps(coro) 57 | async def new_coro(self: "GuildData", *args, **kwargs): 58 | try: 59 | await wait_for(self.lock.wait(), 6) 60 | except TimeoutError: 61 | raise TimeoutError("データベースに嫌われてしまいました。悲しいです。") 62 | else: 63 | return await coro(self, *args, **kwargs) 64 | return new_coro 65 | 66 | 67 | class GuildData: 68 | def __init__(self, cog: "ThreadManager", guild: Union[int, discord.Guild]): 69 | self.cog, self.pool = cog, cog.pool 70 | self.lock = Event() 71 | self.cog.bot.loop.create_task(self.update_data()) 72 | self.guild: discord.Guild = self.cog.bot.get_guild(guild) \ 73 | if isinstance(guild, int) else guild 74 | 75 | self.channels: Dict[int, discord.TextChannel] = {} 76 | 77 | async def update_data(self) -> None: 78 | """このクラスにあるセーブデータを新しいものに更新します。 79 | このクラスをインスタンス化した際に自動でこの関数は実行されます。""" 80 | if self.lock.is_set(): 81 | self.lock.clear() 82 | async with self.pool.acquire() as conn: 83 | async with conn.cursor() as cursor: 84 | await cursor.execute( 85 | f"SELECT * FROM {DB} WHERE GuildID = %s;", 86 | (self.guild.id,) 87 | ) 88 | for row in await cursor.fetchall(): 89 | if row: 90 | self.channels[row[1]] = self.guild.get_channel(row[1]) 91 | self.lock.set() 92 | 93 | @wait_ready 94 | async def get_channels(self) -> Dict[int, discord.TextChannel]: 95 | """監視対象チャンネルの一覧を取得します。 96 | データ読み込みが完了するまで待ってから`channels`を返します。""" 97 | return self.channels 98 | 99 | @wait_ready 100 | async def add_channel(self, channel_id: int) -> None: 101 | """監視チャンネルリストに追加する。""" 102 | assert len(self.channels) < MAX_CHANNELS, "これ以上追加できません。" 103 | self.channels[channel_id] = self.guild.get_channel(channel_id) 104 | async with self.pool.acquire() as conn: 105 | async with conn.cursor() as cursor: 106 | await cursor.execute( 107 | f"""INSERT IGNORE INTO 108 | {DB} (GuildID, ChannelID) 109 | VALUES (%s, %s);""", 110 | (self.guild.id, channel_id) 111 | ) 112 | 113 | @wait_ready 114 | async def remove_channel(self, channel_id: int) -> None: 115 | """監視チャンネルリストからチャンネルを削除する。""" 116 | del self.channels[channel_id] 117 | async with self.pool.acquire() as conn: 118 | async with conn.cursor() as cursor: 119 | await cursor.execute( 120 | f"""DELETE FROM {DB} 121 | WHERE GuildID = %s AND ChannelID = %s;""", 122 | (self.guild.id, channel_id) 123 | ) 124 | -------------------------------------------------------------------------------- /cogs/serveruseful/__init__.py: -------------------------------------------------------------------------------- 1 | from os import listdir 2 | import traceback 3 | 4 | 5 | async def setup(bot): 6 | for name in listdir("cogs/serveruseful"): 7 | if not name.startswith(("_", ".")): 8 | try: 9 | await bot.load_extension( 10 | f"cogs.serveruseful.{name[:-3] if name.endswith('.py') else name}") 11 | except Exception: 12 | traceback.print_exc() 13 | else: 14 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 15 | -------------------------------------------------------------------------------- /cogs/serveruseful/embed.py: -------------------------------------------------------------------------------- 1 | # Free RT - Embed 2 | 3 | from typing import Union 4 | 5 | from urllib.parse import quote 6 | 7 | from discord.ext import commands 8 | import discord 9 | 10 | from util.markdowns import create_embed 11 | from util import RT 12 | 13 | 14 | class Embed(commands.Cog): 15 | def __init__(self, bot: RT): 16 | self.bot = bot 17 | 18 | @commands.group( 19 | aliases=["埋め込み"], extras={ 20 | "headding": { 21 | "ja": "埋め込みメッセージを作成します。", 22 | "en": "Make embed message." 23 | }, "parent": "ServerUseful" 24 | } 25 | ) 26 | async def embed(self, ctx): 27 | """!lang ja 28 | ------- 29 | [こちらをご覧ください。](https://rt-team.github.io/ja/notes/embed) 30 | 31 | !lang en 32 | -------- 33 | Let's see [here](https://rt-team.github.io/en/notes/embed).""" 34 | if not ctx.invoked_subcommand: 35 | await ctx.reply("使用方法が違います。") 36 | 37 | @embed.command(aliases=["u", "リンク", "link"]) 38 | async def url(self, ctx: commands.Context, color: Union[discord.Color, str], *, content): 39 | try: 40 | e = create_embed( 41 | f"# {content}", color=ctx.author.color if color == "null" else color 42 | ) 43 | except TypeError: 44 | return await ctx.reply( 45 | {"ja": "色の指定がおかしいです。", 46 | "en": "Bad color argument."} 47 | ) 48 | data = {"color": str(hex(e.color.value))[2:]} 49 | if e.title: 50 | data["title"] = e.title 51 | if e.description: 52 | data["description"] = e.description 53 | if ctx.message.attachments: 54 | data["image"] = ctx.message.attachments[0].proxy_url 55 | 56 | await ctx.reply( 57 | f"https://rt-bot.com/embed?{'&'.join(f'{name}={quote(value)}' for name, value in data.items())}" 58 | ) 59 | 60 | @embed.command(aliases=["wh", "ウェブフック"]) 61 | @commands.cooldown(1, 10, commands.BucketType.channel) 62 | async def webhook( 63 | self, ctx: commands.Context, color: Union[discord.Color, str], *, content 64 | ): 65 | rt = False 66 | if "--rticon" in content: 67 | content = content.replace( 68 | " --rticon ", "" 69 | ).replace( 70 | " --rticon", "" 71 | ).replace("--rticon", "") 72 | rt = True 73 | 74 | try: 75 | kwargs = { 76 | "username": ctx.author.display_name, 77 | "avatar_url": getattr(ctx.author.display_avatar, "url", ""), 78 | "embed": create_embed( 79 | "# " + content, 80 | color=ctx.author.color if color == "null" else color 81 | ) 82 | } 83 | except TypeError: 84 | await ctx.reply( 85 | {"ja": "色の指定がおかしいです。", 86 | "en": "Bad color argument."} 87 | ) 88 | else: 89 | if ctx.message.reference: 90 | message = await ctx.channel.fetch_message( 91 | ctx.message.reference.message_id 92 | ) 93 | kwargs = {"embed": kwargs["embed"]} 94 | if message.author.id == self.bot.user.id: 95 | send = message.edit 96 | else: 97 | wb = discord.utils.get( 98 | await message.channel.webhooks(), 99 | name="R2-Tool" if self.bot.test else "RT-Tool" 100 | ) 101 | return await wb.edit_message( 102 | message.id, **kwargs 103 | ) 104 | else: 105 | send = ctx.channel.webhook_send 106 | if rt: 107 | kwargs = {"embed": kwargs["embed"]} 108 | send = ctx.send 109 | 110 | await send(**kwargs) 111 | 112 | 113 | async def setup(bot): 114 | await bot.add_cog(Embed(bot)) 115 | -------------------------------------------------------------------------------- /cogs/tts/data/allowed_characters.csv: -------------------------------------------------------------------------------- 1 | あ い う え お 2 | か き く け こ 3 | さ し す せ そ 4 | た ち つ て と 5 | な に ぬ ね の 6 | は ひ ふ へ ほ 7 | ま み む め も 8 | や   ゆ   よ 9 | ら り る れ ろ 10 | わ ゐ を ゑ ん 11 | 12 | が ぎ ぐ げ ご 13 | ざ じ ず ぜ ぞ 14 | だ ぢ づ で ど 15 | ば び ぶ べ ぼ 16 | ぱ ぴ ぷ ぺ ぽ 17 | 18 | っ 19 | ゃ   ゅ   ょ 20 | 21 | ア イ ウ エ オ 22 | カ キ ク ケ コ 23 | サ シ ス セ ソ 24 | タ チ ツ テ ト 25 | ナ ニ ヌ ネ ノ 26 | ハ ヒ フ ヘ ホ 27 | マ ミ ム メ モ 28 | ヤ   ユ   ヨ 29 | ラ リ ル レ ロ 30 | ワ   ヲ   ン 31 | 32 | ガ ギ グ ゲ ゴ 33 | ザ ジ ズ ゼ ゾ 34 | ダ ヂ ズ デ ド 35 | バ ビ ブ ベ ボ 36 | パ ピ プ ペ ポ 37 | 38 | ッ 39 | ャ   ュ   ョ 40 | 41 | ヴ 42 | 43 | 、 。 ー -------------------------------------------------------------------------------- /cogs/tts/data/avaliable_voices.json: -------------------------------------------------------------------------------- 1 | { 2 | "openjtalk": { 3 | "mei": { 4 | "name": "メイ", 5 | "details": "Copyright (c) 2009-2018 Nagoya Insitute of Technology Department of Computer Science" 6 | }, 7 | "nitech_jp_atr503_m001": { 8 | "name": "男の人", 9 | "details": "Copyright (c) 2003-2012 Nagoya Insitute of Technology Department of Computer Science" 10 | }, 11 | "Miku-Type-a": { 12 | "name": "初音ミク", 13 | "details": "著作権:CUBE370" 14 | }, 15 | "wacky": { 16 | "name": "Wacky~▲", 17 | "details": "音響モデル:zeta, 音声データ:Wacky~▲" 18 | }, 19 | "京歌カオル": { 20 | "name": "京歌カオル", 21 | "details": "音響モデル:アキヒロ, 音声データ:ななかお(kei)" 22 | }, 23 | "蒼歌ネロ": { 24 | "name": "蒼歌ネロ", 25 | "details": "音響モデル:アキヒロ, 音声データ作成者:楓夜" 26 | }, 27 | "沙音ほむ": { 28 | "name": "沙音ほむ", 29 | "details": "音響モデル:アキヒロ, 音声データ:ほむ" 30 | } 31 | }, 32 | "gtts": { 33 | "en": { 34 | "name": "gTTS (For english, 英語用)", 35 | "details": "TTS made by gTTS python lib, Copyright © 2014-2021 Pierre Nicolas Durette & Contributors" 36 | }, 37 | "ja": { 38 | "name": "gTTS (For japanese, 日本語用)", 39 | "details": "TTS made by gTTS python lib, Copyright © 2014-2021 Pierre Nicolas Durette & Contributors" 40 | } 41 | }, 42 | "aquestalk": { 43 | "f1": { 44 | "name": "ゆっくり霊夢", "emoji": "<:yukkuri_reimu:942252914825633802>", 45 | "details": "AquesTalk 評価版, (株)アクエストの音声合成ライブラリAquesTalkによるものです。" 46 | }, 47 | "f2": { 48 | "name": "ゆっくり魔理沙", "emoji": "<:yukkuri_marisa:942253791749746728>", 49 | "details": "AquesTalk 評価版, (株)アクエストの音声合成ライブラリAquesTalkによるものです。" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /cogs/tts/lib/AquesTalk/aquestalk.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include "AquesTalk.h" 5 | 6 | 7 | int main(int argc, char **argv) { 8 | int size; 9 | char chars[8000]; 10 | 11 | if (fgets(chars, 8000 - 1, stdin) == 0) 12 | return 0; 13 | 14 | unsigned char *wav = AquesTalk_Synthe_Utf8( 15 | chars, atoi(argv[1]), &size 16 | ); 17 | 18 | if (wav == 0) { 19 | fprintf(stderr, "ERR:%d\n", size); 20 | return -1; 21 | } else { 22 | fwrite(wav, 1, size, stdout); 23 | } 24 | 25 | AquesTalk_FreeWave(wav); 26 | 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /cogs/tts/manager.py: -------------------------------------------------------------------------------- 1 | # Free RT TTS - Manager 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Optional, Any 6 | 7 | from asyncio import Task 8 | 9 | import discord 10 | 11 | from .voice import Voice 12 | 13 | if TYPE_CHECKING: 14 | from .__init__ import TTSCog 15 | 16 | 17 | EMOJI_ERROR = "<:error:878914351338246165>" 18 | 19 | 20 | async def try_add_reaction(message: discord.Message, emoji: str): 21 | "ただリアクション付与をtryするだけ。" 22 | try: 23 | await message.add_reaction(emoji) 24 | except Exception: 25 | ... 26 | 27 | 28 | class ExtendedVoice(Voice): 29 | "サーバー辞書にある文字列を交換するように拡張したVoiceです。" 30 | 31 | def adjust_text(self, text: str) -> str: 32 | if "dictionary" in self.cog.guild[self.message.guild.id]: 33 | for key, value in list(self.cog.guild[self.message.guild.id].dictionary.items()): 34 | text = text.replace(key, value) 35 | return text 36 | 37 | 38 | class Manager: 39 | "読み上げを管理するためのクラスです。" 40 | 41 | def __init__(self, cog: TTSCog, guild: discord.Guild): 42 | self.cog, self.guild = cog, guild 43 | self.vc: discord.VoiceClient = guild.voice_client 44 | self.queues: list[Voice] = [] 45 | self.channels: list[int] = [] 46 | self._closing = False 47 | 48 | def add_channel(self, channel_id: int) -> None: 49 | "読み上げチャンネルを追加します。" 50 | assert len(self.channels) < 10, { 51 | "ja": "これ以上チャンネルを追加できません。", "en": "You can't add a channel more than 10." 52 | } 53 | self.channels.append(channel_id) 54 | 55 | def remove_channel(self, channel_id: int) -> bool: 56 | "読み上げ対称チャンネルを削除します。" 57 | if channel_id in self.channels: 58 | self.channels.remove(channel_id) 59 | return True 60 | return False 61 | 62 | def check_channel(self, channel_id: int) -> None: 63 | "読み上げ対称チャンネル可動かをチェックします。" 64 | return channel_id in self.channels 65 | 66 | def print(self, *args, **kwargs) -> None: 67 | "デバッグ用のprint" 68 | self.cog.bot.print(f"[{self}]", *args, **kwargs) 69 | 70 | async def disconnect(self, reason: Optional[Any] = None, force: bool = False) -> None: 71 | "切断をします。これをやったあとキューのお片付けをしたい場合はdelしてください。" 72 | self._closing = True 73 | try: 74 | await self.vc.disconnect(force=force) 75 | if reason is not None: 76 | await self.guild.get_channel(self.channels[0]).send(reason) 77 | except Exception: 78 | ... 79 | 80 | async def add(self, message: discord.Message): 81 | "渡されたメッセージを読み上げキューに追加します。" 82 | queue = ExtendedVoice(self, message) 83 | try: 84 | await queue.synthe() 85 | except Exception as e: 86 | if self.cog.bot.test: 87 | await try_add_reaction(message, EMOJI_ERROR) 88 | self.print("Failed to do voice synthesis:", f"{e.__class__.__name__} - {e}") 89 | else: 90 | self.queues.append(queue) 91 | try: 92 | self.queues[1] 93 | except IndexError: 94 | self.play() 95 | 96 | async def _after(self, e: Optional[Exception]): 97 | if self.queues: 98 | if e: 99 | self.print("Failed to play voice:", f"{e.__class__.__name__} - {e}") 100 | self.cog.bot.loop.create_task( 101 | try_add_reaction(self.queues[0].message, EMOJI_ERROR), 102 | name=f"{self}: Try add error reaction" 103 | ) 104 | 105 | await self.queues[0].close() 106 | del self.queues[0] 107 | self.play() 108 | 109 | def play(self): 110 | if self.queues: 111 | self.print("Play voice:", self.queues[0]) 112 | if self.queues[0].source is None: 113 | # 普通ないがもしsourceが用意されていないのなら再生をしない。 114 | self.cog.bot.loop.create_task( 115 | self._after(None), name=f"{self}: After playing voice" 116 | ) 117 | elif self.vc.is_connected(): 118 | self.vc.play( 119 | self.queues[0].source, after=lambda e: self.cog.bot.loop.create_task( 120 | self._after(e), name=f"{self}: After playing voice" 121 | ) 122 | ) 123 | 124 | def clean(self, voice: Voice) -> Task: 125 | "渡されたVoiceのお片付けをします。第二引数にもし数字を渡した場合はキューを消した後にplayを実行します。" 126 | return self.cog.bot.loop.create_task( 127 | voice.close(), name=f"{self}: Remove voice cache file" 128 | ) 129 | 130 | def __del__(self): 131 | for queue in self.queues: 132 | self.clean(queue) 133 | 134 | def __str__(self): 135 | return f"" 136 | -------------------------------------------------------------------------------- /cogs/tts/readme.md: -------------------------------------------------------------------------------- 1 | # Free RTの読み上げ 2 | RTの読み上げではOpenJTalkとAquesTalkとgTTSを使用します。 3 | 4 | ## Installation 5 | ### OpenJTalk 6 | 1. `open_jtalk`からOpenJTalkを実行できるようにする。 7 | 2. `cogs/tts/lib`に`OpenJTalk`というフォルダを作る。 8 | 3. `cogs/tts/lib/OpenJTalk`に`dic`というフォルダを作りそこにOpenJTalkの辞書を入れる。 9 | 4. `cogs/tts/lib/OpenJTalk`に`cogs/tts/data/avaliable_voices.json`にあるOpenJTalkのボイスに対応する`htsvoice`を`.htsvoice`のように配置する。 10 | (もちろん使用するものだけでOKです。) 11 | 12 | ※ 1の実行コマンドは`cogs/tts/agents.py`の`OPENJTALK`の定数から変更が可能で、絶対パスを指定することが可能です。 13 | ※ 2の辞書のパスは`cogs/tts/agents.py`の`OPENJTALK_DICTIONARY`の定数を変更することによって別の場所に配置することが可能です。 14 | ### AquesTalk 15 | まずAquesTalkのライブラリをダウンロードして、そこにあるゆっくり霊夢である`f1`とゆっくり魔理沙である`f2`を使用して`cogs/tts/lib/AquesTalk/aquestalk.c`をコンパイルしてください。 16 | そして完成した`f1`と`f2`の実行ファイルを`AquesTalk`に配置してください。 17 | #### Notes 18 | Pythonには`ctypes`というもので`dll`,`dylib`,`so`を実行することができます。 19 | RTでそれを使用していない理由は`Segmentation fault`のトラウマがあるからです。 20 | ですので`ctypes`使えばなど何も言わないでください。 21 | ### gTTS 22 | `requirements.txt`にあるものをインストールしていれば特に何もする必要はありません。 -------------------------------------------------------------------------------- /cogs/tts/voice.py: -------------------------------------------------------------------------------- 1 | # Free RT TTS - Voice 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Optional 6 | 7 | import discord 8 | 9 | from aiofiles.os import remove 10 | 11 | from .agents import Source, Agent, prepare_source 12 | 13 | if TYPE_CHECKING: 14 | from .manager import Manager 15 | 16 | 17 | OUTPUT_DIRECTORY = "cogs/tts/outputs" 18 | 19 | 20 | class Voice: 21 | "音声クラスです。再生キューに使うクラスです。" 22 | 23 | path: Optional[str] = None 24 | 25 | def __init__(self, manager: Manager, message: discord.Message): 26 | self.cog, self.message, self.manager = manager.cog, message, manager 27 | self.source: Optional[Source] = None 28 | 29 | def print(self, *args, **kwargs): 30 | self.manager.print(f"[{self}]", *args, **kwargs) 31 | 32 | def adjust_text(self, text: str) -> str: 33 | """文字列を調整する関数です。 34 | デフォルトでは何もしません。 35 | オーバーライドして拡張するためのものです。""" 36 | return text 37 | 38 | async def synthe(self) -> None: 39 | """音声合成を行います。Routineの場合はRoutineのSourceを作ります。 40 | インスタンス変数の`source`にSource入れられます。""" 41 | # Routineがあるかチェックをする。 42 | for routine in self.cog.user[self.message.author.id].get("routines", ()): 43 | if any(key in self.message.content for key in routine["keys"]): 44 | if self.cog.RTCHAN: 45 | ... # TODO: りつたんでもバックエンドを経由してRoutineを手に入れて再生を行う。 46 | else: 47 | # Routineがあればそれの再生を行う。 48 | self.source = prepare_source(routine["path"], 1.5) 49 | return self.print("Found routine:", routine) 50 | 51 | # Agentコードを取得する。 52 | code = self.cog.user[self.message.author.id].get("voice") 53 | if code is None: 54 | if self.cog.bot.cogs["Language"].get(self.message.author.id) == "ja": 55 | code = "openjtalk-mei" 56 | else: 57 | code = "gtts-en" 58 | 59 | # 音声合成を行う。 60 | self.path = f"{OUTPUT_DIRECTORY}/{self.message.author.id}-{self.message.id}.wav" 61 | self.print("Doing voice synthesis...: ", code) 62 | self.source = await Agent.from_agent_code(code) \ 63 | .synthe(self.adjust_text(self.message.content), self.path) 64 | 65 | async def close(self) -> None: 66 | "音声合成で作成したファイルを削除します。" 67 | if self.path is not None: 68 | self.print("Cleaning...") 69 | try: 70 | await remove(self.path) 71 | except Exception: 72 | ... 73 | self.path = None 74 | 75 | def is_closed(self) -> bool: 76 | "お片付けが済んでいるかどうかです。" 77 | return self.path is None 78 | 79 | def __del__(self): 80 | if not self.is_closed(): 81 | self.cog.bot.loop.create_task( 82 | self.close(), name=f"{self}: Closing..." 83 | ) 84 | 85 | def __str__(self) -> str: 86 | return f"" 87 | -------------------------------------------------------------------------------- /contributing/about_cogs.md: -------------------------------------------------------------------------------- 1 | # Cogについて 2 | free RTには大量のCogがあります。 3 | 最終的にはそれらすべてのCogに関する表(DBやbackendとの通信を使うかなど)を作成する予定です。 4 | -------------------------------------------------------------------------------- /contributing/about_util.md: -------------------------------------------------------------------------------- 1 | # utilについて 2 | 旧rtutilと旧rtlibは共にRTに関するお役立ちツールが集まったものになっていました。 3 | この2つは統合し、utilというフォルダで作業されています。 4 | これらの機能は 5 | 6 | ## utilにあるもの 7 | * webhook_send (`channel.webhook_send`を使える機能) 8 | * componesy (`discord.ui.View`を簡単に作るためのもの) 9 | * mysql_manager (DBマネージャー) 10 | * lib_data_manager (DBマネージャー②) 11 | * discord.ext.tasksのタスクがデータベースの操作失敗によって止まることがないようにする機能 (標準搭載) 12 | * sendKwargs (ctx.sendにはあるけどinteractionresponce.send_messageにはないKwargsを削除して返す関数っぽい) 13 | * RT (AutoShardedBotを継承したサブクラス、起動時に使っている) 14 | * @websocketデコレータ (ws通信を簡単に行う機能らしい) 15 | * WebSocketManager (ws関連のCog) 16 | * rtws (さっきのwebsocketとは分離されている、RT専用のws機能) 17 | * EmbedPage (buttonを使ってページのようなことを簡単にできるようにしたもの) 18 | * Slash関連 (スラッシュコマンドを通常のcommands.commandと互換性を持たせた状態にするための機能) 19 | * Cacher (キャッシュ管理) 20 | * debug (デバッグ関連のCog) 21 | * dochelp (自動ドキュメンテーションヘルプ管理) 22 | * on_cog_addイベント (cogの追加イベント) 23 | * on_full_reaction_add/removeイベント (rawイベントでは通常取得できないメンバーやメッセージ情報を補ったイベント) 24 | * on_send、on_editイベント (自分がしゃべった・編集したときに発火するイベント) 25 | * MultipleConverters (`, `によって区切っていくつかの対象を同時に取得できるコンバーター) 26 | * data_manager (DBマネージャー③) 27 | * markord (マークダウン変換機) 28 | * minesweeper (マインスイーパー) 29 | * securlAPIを叩く機能 30 | * その他 31 | -------------------------------------------------------------------------------- /contributing/guide.md: -------------------------------------------------------------------------------- 1 | # ようこそ 2 | freeRTに貢献しようとしてくれてありがとうございます! 3 | どのようにコードを書けばいいのかなど、開発する際に気になることなどをまとめました。 4 | ぜひ一度読んでみてください。 5 | 6 | ## 前提条件 7 | freeRTで新しいコマンドを作るために必要な知識・技能は少ししかありません。 8 | * pythonの書き方を知っていること 9 | * discord.py v2.0をある程度扱えること (Cogの書き方は知っていてほしい) 10 | * githubのアカウントと少しでもいいので使えること 11 | 12 | 「この知識もあったら嬉しいな」というものもあります。 13 | * PEP8に関する知識 14 | * pythonの綺麗な書き方 15 | これらのことは知らなくても大丈夫です。「PEP8って何?」って人でも気軽にPull Requestください。 16 | 17 | ## いざ、コマンドを書いてみよう! でもその前に... 18 | 少し知って置いてほしいことがあります。それは「**Cogに書く**」ということです。 19 | freeRTはソースコードを全てmain.pyに書いていると10万行程度になる、と言われるほど膨大な量になっています。そのため、main.pyには最低限の機能しか置いていません。 20 | そして、各機能をdiscord.pyの`Cog`と呼ばれる機能でそれぞれファイル別に分けることで、コードを見やすく・管理しやすくするように努めています。 21 | そのため、cogの中の自分が開発したい機能のカテゴリの中にファイルを作成して始めてください。 22 | Cogの書き方はいつも通りです。頑張って作ってください。 23 | 24 | ## helpへの登録はどうしてるの? 25 | コマンドを書き始めました。おめでとう。freeRTの開発協力の第一歩を踏み出しましたね。 26 | でもそこでちょっとストップ。コマンドを書くときに思ったことがあるはずです。 27 | 「helpへどうやって登録するの?」はい。その疑問に答えましょう。 28 | ```python 29 | @commands.command( 30 | extras={ 31 | "headding": { 32 | "ja": "日本語での短い説明", 33 | "en": "Short description in English" 34 | }, 35 | "parent": "カテゴリ名(英語)" 36 | } 37 | ) 38 | async def command_name(self, ctx): 39 | """!lang ja 40 | -------- 41 | (↑日本語の説明であることの証明。) 42 | helpに実際に乗る説明です。詳しく知りたい場合はutil/dochelp.pyの先頭にある説明を見てください。 43 | 44 | Parameters 45 | ---------- 46 | 引数名: 引数の種類(int, str, もしくは自分のお好みでどうぞ) 47 | 引数の説明... 48 | 複数行にわたって説明する場合は改行の前に2つの空白を入れてください。 49 | 50 | !lang en 51 | -------- 52 | English Description... 53 | """ 54 | pass # ここでは何もしてない 55 | ``` 56 | こんなかんじです。説明読んで自分で理解してください((めんどくさい(( 57 | 58 | ## データベースを使ってみたい! 59 | 60 | データベース使って見たくないですか? あ、使ってみたくないですか。 61 | ならここは読み飛ばしてください。 62 | 読む人へ: -------------------------------------------------------------------------------- /contributing/readme.md: -------------------------------------------------------------------------------- 1 | (このフォルダは日本語での説明となります。現状英語は存在しません。 2 | There aren't any English versions of contributing guide. Sorry.) 3 | # Contributing 4 | このフォルダでは開発に役立つような情報や開発をするときに注意すべき点などをまとめたものです。 5 | 6 | ## ファイルマップ 7 | いくつかの項目は長いため、ファイル別に分けています。 8 | * [RTの全体的な仕組み](./sikumi.md) 9 | * [Cogsについて](./about_cogs.md) 10 | * [utilについて](./about_util.md) 11 | * [free RTのpythonでの構文・記法](./syntax.md) 12 | 13 | ## コミットメッセージ 14 | なるべく以下のような感じにすると嬉しいです。 15 | ```md 16 | # 新しく何かを作った場合 17 | new: 内容 18 | 19 | # 更新をした場合 20 | update: 内容 21 | 22 | # バグ修正をした場合 23 | fix: 内容 24 | 25 | # 何か変更をした場合 26 | change: 内容 27 | ``` 28 | ### 注釈 29 | コミットメッセージの最初のタグ(`fix`や`new`など)の後に`[]`を使って以下のように注釈を付け加えるとより嬉しいです。 30 | ```md 31 | # 文字列を変えただけの場合 32 | change[text]: 内容 33 | 34 | # ドキュメンテーションを変えただけの場合 35 | change[doc]: 内容 36 | ``` 37 | 38 | 39 | ## RT開発の環境について 40 | free-RTは現在いくつかの機能を拡張して使いやすくしています。 41 | そのためいくつか通常のdiscord.pyとは違う点があるので注意してください。 42 | 43 | ### commands.commandの引数 44 | commands.commandの引数にextrasをつけることができます。 45 | ```python 46 | { 47 | "headding":{"ja":"...", "en":"..."}, 48 | "parent":"ServerPanel" 49 | } 50 | ``` 51 | extrasの引数はこのようにheaddingとparentで構成されています。 52 | headdingには日本語と英語でコマンドの簡潔な説明を、parentにはRTのヘルプで出すカテゴリを英語で入れてください。 53 | 54 | ### Context.sendの多言語拡張(対応済み) 55 | Context.sendのcontent引数に{"ja":"...", "en":"..."}の形式で辞書を入れると、 56 | 自動で言語を判別してその言語にあった内容が送信されるようになります。 57 | 現時点で全て対応済みですが、新しくsendする場合は作ると良いでしょう。 58 | 59 | ### Cog内でのon_readyについて 60 | Cogはon_readyが呼ばれてからロードされるので、Cog内ではon_readyの代わりとしてcog_load関数を使うかon_full_readyが使えます。 61 | on_full_readyイベントはfree-rtが全てのCogを読み込んだ後に呼ばれます。 62 | -------------------------------------------------------------------------------- /contributing/sikumi.md: -------------------------------------------------------------------------------- 1 | # RT全体の仕組み 2 | RT全体の仕組みは以下の図を見るとわかりやすいと思います。 3 | 4 | (図は作成中...) 5 | 6 | RT本体はrt-botで動かすことができます。しかし、backend(サーバーサイド)とは通信をしなければならないため単体で動かすことは不可能です。 7 | また、frontendはクライアントサイドなので特にサーバーは必要ありません。 8 | -------------------------------------------------------------------------------- /contributing/syntax.md: -------------------------------------------------------------------------------- 1 | # free RT python記法ルール 2 | free RTではRTの構文・記法を引き継いでいます。異論などがあれば公式サーバーまで。 3 | 4 | ## 基本的な書き方 5 | PEP8で文字数は90文字より少ない文字数にして欲しいです。 6 | ファイルの最初には以下のように`# Free-RT - ファイルにあるものの名前`のようにつけて改行をして欲しいです。 7 | ```python 8 | # Free-RT - Help Command 9 | 10 | ... 11 | ``` 12 | 13 | ## インポート 14 | `impprt`は以下のようにtyping関連、discord、通常モジュール、util、同階層の順にブロック分けして欲しいです。 15 | それぞれのブロックの中では文字数が多い順に並べ替えをしてくれると嬉しいです。 16 | `if TYPE_CHECKING: ...`は一番下でお願いします。(この中でブロック分けは不要です。) 17 | ```python 18 | from typing import TYPE_CHECKING 19 | 20 | from discord.ext import commands 21 | import discord 22 | 23 | from reprypt import decrypt 24 | from ujson import loads 25 | 26 | from util import isintable 27 | 28 | from .constants import DEFAULT 29 | 30 | if TYPE_CHECKING: 31 | from util import RT 32 | ``` 33 | 34 | ## インデントがない場所のものは二行ずつ 35 | クラスや関数などでインデントがない場所にあるものは以下のように二行ずつ開けて欲しいです。 36 | ```python 37 | def test(...) -> ...: 38 | ... 39 | 40 | 41 | class Wow: 42 | def huga(self, ...) -> ...: 43 | ... 44 | 45 | def test(self, ...) -> ...: 46 | ... 47 | 48 | 49 | def setup(bot): 50 | ... 51 | ``` 52 | 53 | ## ドキュメンテーション/アノテーション 54 | 色々な場所で使われるような関数には軽いドキュメンテーションとアノテーションをつけて欲しいです。 55 | utilにあるものには特につけておくべきです。 56 | 内部的に使われるものには`_`を最初につけて関数のちょっとした説明を`#`のコメントで示して欲しいです。 57 | `_`を先頭に付けた関数のアノテーションは不要です。 58 | また、discordの基本的な機能(コマンドのctx引数やsetup関数など)のアノテーションも不要です。 59 | ```python 60 | from typing import Optional 61 | 62 | 63 | def encode(text: str) -> Optional[str]: 64 | "これは渡された文字列をエンコードする関数です。" 65 | ... 66 | 67 | 68 | def _plus(text): 69 | # これは渡されたエンコードされた文字列を拡張するものです。 70 | ... 71 | ``` 72 | 73 | 外部的に使われるような変数には型アノテーションをなるべくつけるようにしてください。 74 | もし汚くなるようでしたらクラス直下にアノテーションをつけた変数を列挙しましょう。 75 | -------------------------------------------------------------------------------- /data/captcha/SourceHanSans-Normal.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/data/captcha/SourceHanSans-Normal.otf -------------------------------------------------------------------------------- /data/captcha/readme.md: -------------------------------------------------------------------------------- 1 | # 認証用のファイル置き場です。 2 | 画像認証のファイルが一時的に置かれます。 -------------------------------------------------------------------------------- /data/headers.py: -------------------------------------------------------------------------------- 1 | # Free RT Data - URL Requesting Headers 2 | 3 | USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " \ 4 | "Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.40" 5 | 6 | 7 | TWITTERID_HEADERS = { 8 | "authority": "tweeterid.com", 9 | "sec-ch-ua": "^\\^Microsoft", 10 | "accept": "*/*", 11 | "content-type": "application/x-www-form-urlencoded; charset=UTF-8", 12 | "x-requested-with": "XMLHttpRequest", 13 | "sec-ch-ua-mobile": "?0", 14 | "user-agent": USER_AGENT, 15 | "sec-ch-ua-platform": "^\\^Windows^\\^", 16 | "origin": "https://tweeterid.com", 17 | "sec-fetch-site": "same-origin", 18 | "sec-fetch-mode": "cors", 19 | "sec-fetch-dest": "empty", 20 | "referer": "https://tweeterid.com/", 21 | "accept-language": "ja,en;q=0.9,en-GB;q=0.8,en-US;q=0.7", 22 | } 23 | 24 | 25 | YAHOO_SEARCH_HEADERS = { 26 | 'User-Agent': USER_AGENT 27 | } 28 | 29 | 30 | SECURL_HEADERS = { 31 | "Connection": "keep-alive", 32 | "sec-ch-ua": '"Microsoft Edge";v="95", "Chromium";v="95", ";Not A Brand";v="99"', 33 | "Accept": "application/json, text/javascript, */*; q=0.01", 34 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 35 | "X-Requested-With": "XMLHttpRequest", 36 | "sec-ch-ua-mobile": "?0", 37 | "User-Agent": USER_AGENT, 38 | "sec-ch-ua-platform": '"macOS"', 39 | "Origin": "https://securl.nu", 40 | "Sec-Fetch-Site": "same-origin", 41 | "Sec-Fetch-Mode": "cors", 42 | "Sec-Fetch-Dest": "empty", 43 | "Referer": "https://securl.nu/", 44 | "Accept-Language": "ja,en;q=0.9,en-GB;q=0.8,en-US;q=0.7", 45 | } 46 | -------------------------------------------------------------------------------- /data/images/game_maker/ps4_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/data/images/game_maker/ps4_base.png -------------------------------------------------------------------------------- /data/images/game_maker/ps4_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/data/images/game_maker/ps4_mask.png -------------------------------------------------------------------------------- /data/images/game_maker/switch_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/data/images/game_maker/switch_base.png -------------------------------------------------------------------------------- /data/images/game_maker/switch_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SakuraProject/rt-bot/80728376c5ab1d104d37ced82cca80c25cea2b7f/data/images/game_maker/switch_mask.png -------------------------------------------------------------------------------- /data/julius_dict/README.md: -------------------------------------------------------------------------------- 1 | # Julius用辞書フォルダ 2 | このフォルダにjuliusの辞書ファイルmain.jconfなどをコピーしてください。 3 | vcnt機能で必要です。 4 | ※詳しくはcogs/serveruseful/vcnt.pyを参照 -------------------------------------------------------------------------------- /data/replies.json: -------------------------------------------------------------------------------- 1 | { 2 | "テスト": { 3 | "en": "test" 4 | }, 5 | "# ここからcogs/bot_general.py #": {}, 6 | "現在のRTのレイテンシ:$$ms": { 7 | "en": "Now RT latency:$$ms" 8 | }, 9 | "# ここからcogs/help.py #": {}, 10 | "Helpが必要ですか?": { 11 | "en": "Do you need help?" 12 | }, 13 | "Help - カテゴリー一覧": { 14 | "en": "categories list" 15 | }, 16 | "`rt!dhelp <名前>`を実行することで詳細を見ることができます。": { 17 | "en": "You can see detail, If you use rt!dhelp " 18 | }, 19 | "検索結果": { 20 | "en": "Results" 21 | }, 22 | "名前部分一致": { 23 | "en": "Name partial match" 24 | }, 25 | "説明部分一致": { 26 | "en": "detail partial match" 27 | }, 28 | "# ここからcogs/news.py": {}, 29 | "Newsは現在空です。": { 30 | "en": "News is now none." 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /log/readme.md: -------------------------------------------------------------------------------- 1 | # Log 2 | pycord's logging folder 3 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """Free RT Backend (C) 2022 Free RT 2 | LICENSE : ./LICENSE 3 | README : ./readme.md 4 | """ 5 | 6 | from os import listdir 7 | from sys import argv 8 | import traceback 9 | 10 | import discord 11 | 12 | from ujson import load 13 | 14 | from util import RT, websocket 15 | from data import data, Colors 16 | 17 | 18 | print("Free RT Discord Bot (C) 2022 Free RT\nNow loading...") 19 | 20 | with open("auth.json", "r") as f: 21 | secret = load(f) 22 | 23 | # Botの準備を行う。 24 | intents = discord.Intents.default() # intents指定 25 | intents.typing = False 26 | intents.members = True 27 | intents.presences = True 28 | intents.message_content = True 29 | 30 | bot = RT( 31 | help_command=None, 32 | intents=intents, 33 | allowed_mentions=discord.AllowedMentions( 34 | everyone=False, 35 | users=False, 36 | replied_user=False 37 | ), 38 | activity=discord.Game("起動準備"), 39 | status=discord.Status.dnd 40 | ) # RTオブジェクトはcommands.Botを継承している 41 | 42 | bot.test = argv[-1] != "production" # argvの最後がproductionかどうか 43 | if not bot.test: 44 | websocket.WEBSOCKET_URI_BASE = "ws://60.158.90.139" 45 | bot.data = data # 全データアクセス用、非推奨 46 | bot.owner_ids = data["admins"] 47 | bot.secret = secret # auth.jsonの内容を入れている 48 | 49 | bot.colors = data["colors"] # 下のColorsを辞書に変換したもの 50 | bot.Colors = Colors # botで使う基本色が入っているclass 51 | 52 | 53 | @bot.listen() 54 | async def on_ready(): 55 | bot.print("Connected to discord") 56 | 57 | # 拡張を読み込む 58 | await bot.setup() 59 | for name in listdir("cogs"): 60 | if not name.startswith(("_", ".")): 61 | try: 62 | await bot.load_extension( 63 | f"cogs.{name[:-3] if name.endswith('.py') else name}") 64 | except Exception: 65 | traceback.print_exc() 66 | else: 67 | bot.print("[Extension]", "Loaded", name) # ロードログの出力 68 | await bot.unload_extension("cogs._first") 69 | bot.print("Completed to boot Free RT") 70 | 71 | bot.dispatch("full_ready") # full_readyイベントを発火する 72 | await bot.tree.sync() # スラコマのツリーを同期する 73 | 74 | 75 | # 実行 76 | bot.run(secret["token"][argv[-1]]) 77 | -------------------------------------------------------------------------------- /readme.ja.md: -------------------------------------------------------------------------------- 1 | 2 | ![Discord](https://img.shields.io/discord/961521739748212776?label=supportFree-rt&logo=discord) 3 | 4 | # Free RT Bot 5 | discordのBotであるRTのフリー版です。 6 | RTはもともと無料で利用できましたが、有料になったためこのリポジトリが作成されました。 7 | RTはBotが1台だけで済むように作成された多機能で便利なBotです。 8 | ウェブ認証などのために`rt-backend`とWebSocketで通信も行います。 9 | RTについて知らない人は[ここ](https://rt-bot.com/)を見てみましょう。 10 | Free RTについて知らない人は[ここ](https://free-rt.com/)を見てみましょう。 11 | 12 | ## LICENSE 13 | `BSD 4-Clause License` (`LICENSE`ファイルに詳細があります。) 14 | 15 | ## Contributing 16 | [contributing](https://github.com/Free-RT/rt-bot/blob/main/contributing)をご覧ください。 17 | 18 | ## Installation 19 | ### Depedencies 20 | 必要なものです。 21 | 22 | * Python 3.9以上 23 | * MySQL または MariaDB 24 | * `requirements.txt`にあるもの全て。 25 | * `rt-backend`の実行 (認証等のバックエンドを必要とする機能を使う場合) 26 | 27 | ### 起動手順 28 | 1. 必要なものを`pip install -r requirements.txt`でインストールをします。 29 | 2. 必要なTOKENなどを`auth.template.json`を参考に`auth.json`に書き込む。 30 | 3. `util`に`rt-module`リポジトリをそのまま(srcだけ置くとかはしないでください)置いてフォルダの名前を`rt_module`にする。 31 | 4. `rt-backend`リポジトリにあるプログラムを動かす。 32 | (任意です。認証等のバックエンドを必要とするものを動かしたい場合は動かす必要があります。) 33 | 5. `python3 main.py test`でテストを実行する。 34 | (この際TOKENは`auth.json`の`test`のキーにあるものが使用されます。) 35 | 36 | ※ もし読み上げを動かしたいのなら`cogs/tts`にある`readme.md`を読んでください。 37 | 38 | ### 本番環境での実行 39 | 起動コマンドは`sudo -E python3 main.py production`で`auth.json`のTOKENで`production`のTOKENが必要となります。 40 | NOTE: `run.sh`の起動でも動きます。 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | (日本語版のreadmeは[こちら](https://github.com/Free-RT/rt-bot/blob/main/readme.ja.md)) 4 | ## Important notice 5 | These features are disabled. You can't use them in this bot. 6 | * cogs.music 7 | 8 | # Free RT Bot 9 | This is an Discord's bot, Free RT. 10 | There is 'RT bot' in discord and we can use it for paying to tasuren, so please use RT if you have enough money. 11 | Free RT is a feature-rich bot with features that most bots have. 12 | It also has features that other bots don't have. 13 | Connect to Free RT, Discord's Bot account, to start Free RT's service. 14 | It also communicates with `rt-backend` via WebSocket for web authentication and so on.(But now do not and we are making. It will be coming soon.) 15 | If you don't know about RT, have a look at [here](https://rt-bot.com/). 16 | If you don't know about Free RT, have a look at [here](https://free-rt.com/). 17 | 18 | ## LICENSE 19 | `BSD 4-Clause License` (The `LICENSE` file has more details.) 20 | 21 | ## CONTRIBUTION 22 | See [contributing](https://github.com/Free-RT/rt-bot/blob/main/contributing/). 23 | 24 | ## Installation 25 | ### Dependencies 26 | These are required. 27 | 28 | * Python 3.9 or higher 29 | * MySQL or MariaDB 30 | * pip requirements all in `requirements.txt` 31 | * Run `rt-backend` if you want to use functions that require a backend such as authentication. 32 | 33 | ### Startup procedure 34 | 1. install required items with `pip install -r requirements.txt` 35 | 2. write necessary TOKEN etc. to `auth.json` referring to `auth.template.json`. 36 | 3. Put the `rt-module` repository in `util` and name the folder `rt_module`. 37 | 4. run the program in the `rt-backend` repository. 38 | (This is optional and you need to do it if you want to run something that requires a backend such as authentication.) 39 | 5. Run the tests with `python3 main.py test`. 40 | (In this case, login-TOKEN will be the one in the `test` key of `auth.json`.) 41 | 42 | * Read `readme.md` in `cogs/tts` if you want to run the readout. 43 | 44 | ### Run production 45 | The startup command is `sudo -E python3 main.py production` and you need `auth.json` TOKEN for `production`. 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/Rapptz/discord.py 2 | jishaku 3 | aiomysql 4 | aiofiles 5 | ujson 6 | websockets 7 | pillow 8 | gTTS 9 | opencv-contrib-python 10 | bs4 11 | youtube_dl 12 | git+https://github.com/yaakiyu/pyopenjtalk 13 | psutil 14 | pyqrcode 15 | pypng 16 | pytz 17 | emoji 18 | deep-translator 19 | captcha 20 | speedtest-cli 21 | tweepy 22 | topggpy 23 | reprypt 24 | niconico.py 25 | pynacl -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | python3 main.py production 2 | -------------------------------------------------------------------------------- /sub.py: -------------------------------------------------------------------------------- 1 | """ふりーりつたん! (C) 2022 Free RT 2 | LICENSE : ./LICENSE 3 | README : ./readme.md 4 | """ 5 | 6 | from discord.ext import commands 7 | import discord 8 | 9 | from aiohttp import ClientSession 10 | from sys import argv 11 | import ujson 12 | import util 13 | 14 | from data import data, RTCHAN_COLORS 15 | 16 | 17 | desc = """ふりーりつたん - (C) 2022 Free RT 18 | 少女起動中...""" 19 | print(desc) 20 | 21 | # routeは無効にする。 22 | commands.Cog.route = lambda *args, **kwargs: lambda *args, **kwargs: (args, kwargs) 23 | 24 | 25 | with open("token.secret", "r", encoding="utf-8_sig") as f: 26 | secret = ujson.load(f) 27 | TOKEN = secret["token"]["sub"] 28 | 29 | 30 | prefixes = data["prefixes"]["sub"] 31 | 32 | 33 | def setup(bot): 34 | bot.owner_ids = data["admins"] 35 | 36 | @bot.listen() 37 | async def on_close(loop): 38 | await bot.session.close() 39 | del bot.mysql 40 | 41 | bot.mysql = bot.data["mysql"] = util.mysql.MySQLManager( 42 | loop=bot.loop, user=secret["mysql"]["user"], 43 | host=(secret["mysql"]["host"] if argv[1] == "production" else "localhost"), 44 | password=secret["mysql"]["password"], db="mysql", 45 | pool=True, minsize=1, maxsize=30, autocommit=True) 46 | 47 | util.setup(bot) 48 | bot.load_extension("jishaku") 49 | 50 | bot._loaded = False 51 | 52 | @bot.event 53 | async def on_ready(): 54 | if not bot._loaded: 55 | bot.session = ClientSession(loop=bot.loop) 56 | for name in ("cogs.tts", "cogs.music", "cogs._sub", "cogs.language"): 57 | bot.load_extension(name) 58 | bot.dispatch("full_ready") 59 | bot._loaded = True 60 | print("少女絶賛稼働中!") 61 | 62 | 63 | intents = discord.Intents.default() 64 | intents.members = True 65 | intents.typing = False 66 | intents.guild_typing = False 67 | intents.dm_typing = False 68 | args = (prefixes,) 69 | kwargs = { 70 | "help_command": None, 71 | "intents": intents 72 | } 73 | bot = commands.Bot( 74 | command_prefix=data["prefixes"]["sub"], **kwargs 75 | ) 76 | bot.test = argv[1] != "production" 77 | 78 | 79 | bot.data = data 80 | bot.colors = RTCHAN_COLORS 81 | 82 | 83 | setup(bot) 84 | 85 | 86 | bot.run(TOKEN) 87 | -------------------------------------------------------------------------------- /test_all.py: -------------------------------------------------------------------------------- 1 | # RT test_all 2 | 3 | def test_util(): 4 | "util関連のテストをします。" 5 | import util 6 | assert "mysql" in dir(util) 7 | 8 | 9 | def test_cog(): 10 | "Cog関連のテストをします。" 11 | import cogs 12 | assert all("setup" in dir(getattr(cogs, m)) 13 | for m in dir(cogs) 14 | if not m.startswith(("_", "."))) 15 | -------------------------------------------------------------------------------- /util/__init__.py: -------------------------------------------------------------------------------- 1 | # Free RT Utilities 2 | 3 | from .bot import RT 4 | from .cacher import Cache, Cacher, CacherPool 5 | from .checks import isintable, has_any_roles, has_all_roles 6 | from .converters import ( 7 | MembersConverter, 8 | UsersConverter, 9 | TextChannelsConverter, 10 | VoiceChannelsConverter, 11 | RolesConverter 12 | ) 13 | from .data_manager import DatabaseManager 14 | from .dpy_monkey import setup 15 | from .lib_data_manager import Table 16 | from .minesweeper import MineSweeper 17 | from . import mysql_manager as mysql 18 | from .olds import tasks_extend, sendKwargs 19 | from .page import EmbedPage 20 | from .record import RTCPacket, PacketQueue, BufferDecoder, Decoder 21 | from .types import sendableString 22 | from .views import TimeoutView 23 | from .webhooks import get_webhook, webhook_send 24 | 25 | from .ext import view as componesy 26 | 27 | 28 | __all__ = [ 29 | "RT", 30 | "Cache", 31 | "Cacher", 32 | "CacherPool", 33 | "isintable", 34 | "has_any_roles", 35 | "has_all_roles", 36 | "MembersConverter", 37 | "UsersConverter", 38 | "TextChannelsConverter", 39 | "VoiceChannelsConverter", 40 | "RolesConverter", 41 | "DatabaseManager", 42 | "debug", 43 | "dochelp", 44 | "docperser", 45 | "setup", 46 | "Table", 47 | "markdowns", 48 | "MineSweeper", 49 | "mysql", 50 | "olds", 51 | "tasks_extend", 52 | "sendKwargs", 53 | "EmbedPage", 54 | "rtws", 55 | "securl", 56 | "settings", 57 | "slash", 58 | "RTCPacket", 59 | "PacketQueue", 60 | "BufferDecoder", 61 | "Decoder", 62 | "sendableString", 63 | "TimeoutView", 64 | "get_webhook", 65 | "webhook_send", 66 | "websocket", 67 | "ext", 68 | "componesy" 69 | ] 70 | -------------------------------------------------------------------------------- /util/bot.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Bot 2 | 3 | from discord.ext import commands 4 | 5 | from aiohttp import ClientSession 6 | from ujson import dumps 7 | from sys import argv 8 | 9 | from .dpy_monkey import _setup 10 | from . import mysql_manager as mysql 11 | from .db import add_db_manager, DBManager 12 | 13 | from data import data 14 | 15 | 16 | class RT(commands.AutoShardedBot): 17 | def __init__(self, *args, **kwargs): 18 | self.user_prefixes: dict[int, str] = {} 19 | self.guild_prefixes: dict[int, str] = {} 20 | # プレフィックスの設定。 21 | kwargs["command_prefix"] = self.get_prefix 22 | return super().__init__(*args, **kwargs) 23 | 24 | def get_prefix(self, m): 25 | pr = data["prefixes"][argv[-1]] 26 | if m.author.id in self.user_prefixes and self.user_prefixes[m.author.id]: 27 | pr.append(self.user_prefixes[m.author.id]) 28 | if m.guild.id in self.guild_prefixes and self.guild_prefixes[m.guild.id]: 29 | pr.append(self.guild_prefixes[m.guild.id]) 30 | return pr 31 | 32 | @property 33 | def session(self) -> ClientSession: 34 | if self._session.closed: 35 | # 閉じていたらもう一度定義。 36 | self._session = ClientSession(loop=self.loop, json_serialize=dumps) 37 | return self._session 38 | 39 | async def setup_hook(self): 40 | # 起動中いつでも使えるaiohttp.ClientSessionを作成 41 | self._session = ClientSession(loop=self.loop, json_serialize=dumps) 42 | # 起動中だと教えられるようにするためのコグを読み込む 43 | await self.load_extension("cogs._first") 44 | # jishakuを読み込む 45 | await self.load_extension("jishaku") 46 | self.mysql = self.data["mysql"] = mysql.MySQLManager( 47 | loop=self.loop, 48 | **self.secret["mysql"], 49 | pool=True, 50 | minsize=1, 51 | maxsize=500 if self.test else 1000000, 52 | autocommit=True 53 | ) # maxsizeはテスト用では500、本番環境では100万になっている 54 | self.pool = self.mysql.pool # bot.mysql.pool のエイリアス 55 | 56 | def print(self, *args, **kwargs) -> None: 57 | "[RT log]と色の装飾を加えてprintをします。" 58 | temp = [*args] 59 | if len(args) >= 1 and args[0].startswith("[") and args[0].endswith("]"): 60 | temp[0] = f"\033[93m{args[0]}\033[0m" 61 | if len(args) >= 2 and args[1].startswith("[") and args[1].endswith("]"): 62 | temp[1] = f"\033[95m{args[1]}\033[0m" 63 | return print("\033[32m[RT log]\033[0m", *temp, **kwargs) 64 | 65 | def get_ip(self) -> str: 66 | return "localhost" if self.test else "60.158.90.139" 67 | 68 | def get_url(self) -> str: 69 | return f"http://{self.get_ip()}" 70 | 71 | async def close(self) -> None: 72 | "botが終了するときの動作。on_closeが呼ばれる。" 73 | self.print("Closing...") 74 | self.dispatch("close", self.loop) 75 | await super().close() 76 | self.print("Bye") 77 | 78 | def get_website_url(self) -> str: 79 | return "http://localhost/" if self.test else "https://free-rt.com/" 80 | 81 | async def add_cog(self, cog: commands.Cog, override: bool = True, **kwargs): 82 | "add_cogの拡張。overrideがデフォルトでTrueなのと、OnCogAddに関する動作をする。" 83 | if "OnCogAdd" in self.cogs: 84 | self.cogs["OnCogAdd"]._add_cog(cog, **kwargs) 85 | return await super().add_cog(cog, override=override, **kwargs) 86 | 87 | async def remove_cog(self, cog_name: str): 88 | "remove_cogの拡張。OnCogAddに関する動作をする。" 89 | if "OnCogAdd" in self.cogs: 90 | self.cogs["OnCogAdd"]._remove_cog(cog_name) 91 | return await super().remove_cog(cog_name) 92 | 93 | async def setup(self, mode: tuple = ()) -> None: 94 | "utilにある拡張cogをすべてもしくは指定されたものだけ読み込みます。" 95 | return await _setup(self, mode) 96 | 97 | async def add_db_manager(self, manager: DBManager) -> DBManager: 98 | return await add_db_manager(self, manager) 99 | -------------------------------------------------------------------------------- /util/cacher.py: -------------------------------------------------------------------------------- 1 | # Free RT - Cacher, キャッシュ管理 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Generic, TypeVar, Any, Optional 6 | from collections.abc import Iterator, Callable 7 | 8 | from time import time 9 | 10 | from discord.ext import tasks 11 | 12 | 13 | DataT = TypeVar("DataT") 14 | 15 | 16 | class Cache(Generic[DataT]): 17 | "キャッシュのデータを格納するためのクラスです。" 18 | 19 | def __init__(self, data: DataT, deadline: float): 20 | self.data, self.deadline = data, deadline 21 | 22 | def is_dead(self, time_: Optional[float] = None) -> bool: 23 | "死んだキャッシュかどうかをチェックします。" 24 | return (time_ or time()) > self.deadline 25 | 26 | def __str__(self) -> str: 27 | return f"" 28 | 29 | def __repr__(self) -> str: 30 | return str(self) 31 | 32 | 33 | KeyT, ValueT = TypeVar("KeyT"), TypeVar("ValueT") 34 | 35 | 36 | class Cacher(Generic[KeyT, ValueT]): 37 | "キャッシュを管理するためのクラスです。\n注意: CacherPoolと兼用しないとデータは自然消滅しません。" 38 | 39 | def __init__(self, lifetime: float, default: Optional[Callable[[], Any]] = None): 40 | self.data: dict[KeyT, Cache[ValueT]] = {} 41 | self.lifetime, self.default = lifetime, default 42 | 43 | self.get, self.pop = self.data.get, self.data.pop 44 | self.keys = self.data.keys 45 | 46 | def set(self, key: KeyT, data: ValueT, lifetime: Optional[float] = None) -> None: 47 | "値を設定します。\n別のライフタイムを指定することができます。" 48 | self.data[key] = Cache(data, time() + (lifetime or self.lifetime)) 49 | 50 | def __contains__(self, key: KeyT) -> bool: 51 | return key in self.data 52 | 53 | def _default(self, key: KeyT): 54 | if self.default is not None and key not in self.data: 55 | self.set(key, self.default()) 56 | 57 | def __getitem__(self, key: KeyT) -> ValueT: 58 | self._default(key) 59 | return self.data[key].data 60 | 61 | def __getattr__(self, key: KeyT) -> ValueT: 62 | return self[key] 63 | 64 | def __delitem__(self, key: KeyT) -> None: 65 | del self.data[key] 66 | 67 | def __delattr__(self, key: KeyT) -> None: 68 | del self[key] 69 | 70 | def __setitem__(self, key: KeyT, value: ValueT) -> None: 71 | self.set(key, value) 72 | 73 | def values(self, mode_list: bool = False) -> Iterator[ValueT]: 74 | for value in list(self.data.values()) if mode_list else self.data.values(): 75 | yield value.data 76 | 77 | def items(self, mode_list: bool = False) -> Iterator[tuple[KeyT, ValueT]]: 78 | for key, value in list(self.data.items()) if mode_list else self.data.items(): 79 | yield (key, value.data) 80 | 81 | def get_raw(self, key: KeyT) -> Cache[ValueT]: 82 | "データが格納されたCacheを取得します。" 83 | self._default(key) 84 | return self.data[key] 85 | 86 | def __str__(self) -> str: 87 | return f"" 88 | 89 | def __repr__(self) -> str: 90 | return str(self) 91 | 92 | 93 | class CacherPool: 94 | "Cacherのプールです。" 95 | 96 | def __init__(self): 97 | self.cachers: list[Cacher] = [] 98 | self._cache_remover.start() 99 | 100 | def acquire(self, lifetime: float, default: Optional[Callable[[], Any]] = None) -> Cacher: 101 | "Cacherを生み出します。" 102 | self.cachers.append(Cacher(lifetime, default)) 103 | return self.cachers[-1] 104 | 105 | def release(self, cacher: Cacher) -> None: 106 | "指定されたCacherを削除します。" 107 | self.cachers.remove(cacher) 108 | 109 | @tasks.loop(seconds=5) 110 | async def _cache_remover(self): 111 | now = time() 112 | for cacher in self.cachers: 113 | for key, value in list(cacher.data.items()): 114 | if value.is_dead(now): 115 | del cacher[key] 116 | 117 | def __del__(self): 118 | if self._cache_remover.is_running(): 119 | self._cache_remover.cancel() 120 | -------------------------------------------------------------------------------- /util/checks.py: -------------------------------------------------------------------------------- 1 | # Free RT Utils - Checks 2 | 3 | import discord 4 | 5 | 6 | def isintable(x: str) -> bool: 7 | "渡された文字列が整数に変換可能かを調べます。" 8 | try: 9 | int(x) 10 | except ValueError: 11 | return False 12 | else: 13 | return True 14 | 15 | 16 | def has_any_roles(member: discord.Member, roles: list[discord.Role]) -> bool: 17 | "ユーザーが指定されたロールのうちどれか1つでも持っているかを調べます。" 18 | return any(role in member.roles for role in roles) 19 | 20 | 21 | def has_all_roles(member: discord.Member, roles: list[discord.Role]) -> bool: 22 | "ユーザーが指定されたロールをすべて持っているかを調べます。" 23 | return all(role in member.roles for role in roles) 24 | 25 | 26 | def similer(before: str, after: str, check_length: int) -> bool: 27 | "beforeがafterとcheck_lengthの文字数分似ているかどうかを調べます。" 28 | return any(after[i:i + check_length] in before 29 | for i in range(len(after) - check_length)) 30 | 31 | 32 | def alpha2num(alpha: str): 33 | "アルファベットを数字に変換するやーつ(A->1, B->2といった具合に)" 34 | num = 0 35 | for index, item in enumerate(list(alpha)): 36 | num += pow(26, len(alpha) - index - 1) * (ord(item) - ord('A') + 1) 37 | return num 38 | 39 | 40 | def num2alpha(num: int): 41 | "数字をアルファベットに変換するやーつ(1->A, 2->Bといった具合に)" 42 | if num <= 26: 43 | return chr(64 + num) 44 | elif num % 26 == 0: 45 | return num2alpha(num // 26 - 1) + chr(90) 46 | else: 47 | return num2alpha(num // 26) + chr(64 + num % 26) 48 | -------------------------------------------------------------------------------- /util/converters.py: -------------------------------------------------------------------------------- 1 | # Free RT Utils - Converters 2 | 3 | from discord.ext import commands 4 | 5 | 6 | class Multiple(commands.Converter): 7 | "`, `で区切って_originalでconvertしたlistを返すConverterの抽象クラスです。" 8 | 9 | _original: commands.Converter = None # type: ignore 10 | 11 | async def convert(self, ctx: commands.Context, arg: str): 12 | return [await self._original().convert(ctx, word) for word in arg.split(",")] 13 | 14 | 15 | class MembersConverter(Multiple): 16 | _original = commands.MemberConverter 17 | 18 | 19 | class UsersConverter(Multiple): 20 | _original = commands.UserConverter 21 | 22 | 23 | class TextChannelsConverter(Multiple): 24 | _original = commands.TextChannelConverter 25 | 26 | 27 | class VoiceChannelsConverter(Multiple): 28 | _original = commands.VoiceChannelConverter 29 | 30 | 31 | class RolesConverter(Multiple): 32 | _original = commands.RoleConverter 33 | -------------------------------------------------------------------------------- /util/data_manager.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Data Manager 2 | 3 | from collections.abc import Callable, Coroutine 4 | 5 | from inspect import iscoroutinefunction, signature, Parameter 6 | from functools import wraps 7 | 8 | from aiomysql import Cursor 9 | 10 | 11 | class _Dummy: 12 | default = Parameter.empty 13 | 14 | 15 | class DatabaseManager: 16 | # データベースマネージャー。現在は昔のrtutilのものを流用。 17 | def __init_subclass__(cls): 18 | # クラスが継承されたときに呼び出される。 19 | for key in dir(cls): 20 | coro: Callable[..., Coroutine] = getattr(cls, key) 21 | if iscoroutinefunction(coro): # コルーチン関数(async def)であれば 22 | if ("cursor" in coro.__annotations__ and 23 | not signature(coro).parameters.get("cursor", _Dummy).default == _Dummy.default): 24 | # cursor引数があれば、自動でデコレータを付ける 25 | setattr(cls, coro.__name__, cls.wrap(coro)) 26 | 27 | @staticmethod 28 | def wrap(coro: Callable[..., Coroutine]) -> Callable[..., Coroutine]: 29 | "自動的にCursorがキーワード引数に渡されるようにするデコレータです。" 30 | @wraps(coro) 31 | async def new_coro(self, *args, **kwargs): 32 | selfmade = "cursor" not in kwargs and not any(isinstance(arg, Cursor) for arg in args) 33 | if selfmade: 34 | # connectionとcursorを作成してkwargsに渡す。 35 | conn = await self.pool.acquire() 36 | kwargs["cursor"] = await conn.cursor() 37 | try: 38 | data = await coro(self, *args, **kwargs) 39 | except Exception as e: 40 | if selfmade: 41 | # 自動でcursorを閉じ、releaseする。 42 | await kwargs["cursor"].close() 43 | self.pool.release(conn) 44 | raise e 45 | finally: 46 | if selfmade: 47 | # 自動でcursorを閉じ、releaseする。 48 | await kwargs["cursor"].close() 49 | self.pool.release(conn) 50 | return data 51 | return new_coro 52 | -------------------------------------------------------------------------------- /util/db.py: -------------------------------------------------------------------------------- 1 | """ 2 | freeRT式新データベースマネージャーです。 3 | このマネージャーはdiscord.pyのCogのように使うことができます。 4 | ※他のbotには応用しにくい仕組みとなっています。申し訳ありません。 5 | コード例: 6 | ```python 7 | from util import db 8 | 9 | class Managerrr(db.DBManager): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | async def manager_load(self, cursor): 14 | # マネージャーが読み込まれた時の特殊関数。 15 | await cursor.execute("CREATE TABLE user(ID BIGINT, description TEXT, fuga TEXT)") 16 | 17 | async def check_user_id(self, obj: Any): 18 | return util.isintable(obj) and self.bot.get_user(obj) 19 | 20 | @db.command() 21 | async def add_user(self, cursor, user: str): 22 | if not self.check_user_id(user): 23 | return False 24 | await cursor.execute("INSERT INTO USERS VALUES(%s, %s, %s)", (int(user), "", "")) 25 | 26 | @db.command(auto=False) 27 | async def get_user(self, conn, user_id: str): 28 | if not self.check_user: 29 | return False 30 | async with conn.cursor() as cursor: 31 | await cursor.execute("SELECT * FROM USERS WHERE ID=%s", (user_id,)) 32 | return await cursor.fetchone() 33 | 34 | class Coooog(commands.Cog): 35 | def __init__(self, bot): 36 | self.bot = bot 37 | 38 | async def cog_load(self): 39 | self.db = await self.bot.add_db_manager(Managerrr(bot)) 40 | 41 | @commands.command() 42 | async def datacheck(self, ctx, id: str): 43 | result = self.db.get_user.run(id) 44 | await ctx.send(result if result else "見つかりませんでした。") 45 | ``` 46 | """ 47 | 48 | from collections.abc import Coroutine 49 | 50 | from discord.ext import commands 51 | 52 | import aiomysql 53 | 54 | from inspect import iscoroutinefunction 55 | from functools import update_wrapper 56 | 57 | 58 | async def mysql_connect(*args, **kwargs): 59 | "全て移行が終了した後に使う予定のmysql接続用関数。" 60 | return await aiomysql.create_pool(*args, **kwargs) 61 | 62 | 63 | class DBManager: 64 | "データベースマネージャーです。db.command()デコレータが着いたものをコマンドとして扱います。" 65 | 66 | def __init_subclass__(cls): 67 | # 各コマンドの_manager属性をインスタンス化前のクラス情報にしておく。 68 | cls.commands = [] 69 | for m in dir(cls): 70 | obj = getattr(cls, m) 71 | if isinstance(obj, Command): 72 | obj._manager = cls 73 | setattr(cls, m, obj) 74 | cls.commands.append(obj) 75 | 76 | return cls 77 | 78 | async def manager_load(self, _): 79 | pass 80 | 81 | 82 | class Command: 83 | 84 | def __init__(self, coro: Coroutine, **kwargs): 85 | self._manager: DBManager = None 86 | self._bot: commands.Bot = None 87 | self._callback = coro 88 | self.__kwargs = kwargs 89 | 90 | # functools.wrapsと同等のことをしてdocstringなどをcoroに揃える。 91 | self = update_wrapper(self, coro) 92 | 93 | async def __call__(self, *args, **kwargs): 94 | # 単純に呼び出すだけ。自動cursor付与などは一切しない。 95 | return await self._callback(self._manager, *args, **kwargs) 96 | 97 | async def run(self, *args, **kwargs): 98 | "関数を`cursor`(もしくは`connection`)をつけて実行します。" 99 | if (self._manager is None) or (self._bot is None): 100 | raise RuntimeError("Managerもしくはbotが見つかりません。") 101 | 102 | async with self._bot.mysql.pool.acquire() as conn: 103 | if not self.__kwargs.get("auto"): 104 | async with conn.cursor() as cursor: 105 | result = await self._callback(self._manager, cursor, *args, **kwargs) 106 | else: 107 | result = await self._callback(self._manager, conn, *args, **kwargs) 108 | 109 | return result 110 | 111 | 112 | def command(**kwargs): 113 | "これがついた関数をコマンドとして扱うデコレータです。外部から`.run(...)`で呼び出せます。" 114 | def deco(func: Coroutine): 115 | if not iscoroutinefunction(func): 116 | raise ValueError("コマンドはコルーチンである必要があります。") 117 | return Command(func, **kwargs) 118 | return deco 119 | 120 | 121 | async def add_db_manager(bot: commands.Bot, manager: DBManager) -> DBManager: 122 | "botにDBManagerを追加します。" 123 | 124 | if not isinstance(manager, DBManager): 125 | raise ValueError("引数managerはDBManagerのサブクラスである必要があります。") 126 | 127 | for m in [x.__name__ for x in manager.commands]: 128 | setattr(getattr(manager, m), "_bot", bot) 129 | setattr(getattr(manager, m), "_manager", manager) 130 | 131 | if not hasattr(bot, "managers"): 132 | bot.managers = [manager] 133 | else: 134 | bot.managers.append(manager) 135 | 136 | # manager_load関数を実行する。(デフォルトでは何もしない) 137 | async with bot.mysql.pool.acquire() as conn: 138 | async with conn.cursor() as cursor: 139 | await manager.manager_load(cursor) 140 | return manager 141 | -------------------------------------------------------------------------------- /util/debug.py: -------------------------------------------------------------------------------- 1 | # Free RT Ext - Debug 2 | 3 | from discord.ext import commands 4 | import discord 5 | 6 | from jishaku.functools import executor_function 7 | from aiofiles import open as async_open, os 8 | from functools import wraps 9 | import psutil 10 | 11 | 12 | def require_admin(coro): 13 | @wraps(coro) 14 | async def new_coro(self, ctx: commands.Context, *args, **kwargs): 15 | if ctx.author.id in self.bot.owner_ids: 16 | return await coro(self, ctx, *args, **kwargs) 17 | else: 18 | return await ctx.reply(self.ON_NO_ADMIN) 19 | return new_coro 20 | 21 | 22 | class Printer: 23 | def __init__(self, max_characters: int = 2000): 24 | self.output: str = "" 25 | self.length: int = 0 26 | self.stdout = type( 27 | "PrinterStdout", (), { 28 | "__init__": lambda *args, **kwargs: None, 29 | "write": self.write 30 | } 31 | ) 32 | self._max = max_characters 33 | 34 | def write(self, text: str): 35 | self.length += len(text) 36 | if self.length <= self._max: 37 | self.output += text 38 | 39 | def print(self, *args, **kwargs) -> None: 40 | if "file" not in kwargs: 41 | kwargs["file"] = self.stdout 42 | return print(*args, **kwargs) 43 | 44 | 45 | class Debug(commands.Cog): 46 | 47 | ON_NO_ADMIN = "あなたはこのコマンドを実行することができません。" 48 | OUTPUT_PATH = "_util_debug_output.txt" 49 | 50 | def __init__(self, bot): 51 | self.bot = bot 52 | if not hasattr(self.bot, "owner_ids"): 53 | self.bot.owner_ids = [] 54 | 55 | @commands.group() 56 | async def debug(self, ctx): 57 | if not ctx.invoked_subcommand: 58 | await ctx.reply("使用方法が違います。") 59 | 60 | @debug.command(aliases=["exec", "run"]) 61 | @require_admin 62 | async def execute(self, ctx, *, code): 63 | printer = Printer() 64 | exec( 65 | "async def _program():\n {}\n{}" 66 | .format( 67 | '\n '.join(code.splitlines()), 68 | "self._program = _program\ndel _program" 69 | ), 70 | { 71 | "bot": self.bot, "ctx": ctx, "discord": discord, 72 | "self": self, "print": printer.print 73 | } 74 | ) 75 | result = await self._program() 76 | async with async_open(self.OUTPUT_PATH, "w") as f: 77 | await f.write( 78 | "<<>>\n{}\n<<>>\n{}" 79 | .format(printer.output, str(result)) 80 | ) 81 | await ctx.reply(file=discord.File(self.OUTPUT_PATH)) 82 | await os.remove(self.OUTPUT_PATH) 83 | 84 | @debug.command(aliases=["su"]) 85 | @require_admin 86 | async def insted(self, ctx, member: discord.Member, *, command): 87 | ctx.message.author = member 88 | ctx.message.content = f"{ctx.prefix}{command}" 89 | await self.bot.process_commands(ctx.message) 90 | 91 | async def _reload(self): 92 | self.bot.cogs["SettingManager"].reset() 93 | for coro in ( 94 | self.bot.cogs["DocHelp"].on_full_ready(), 95 | self.bot.cogs["Translator"].on_command_added(), 96 | self.bot.cogs["ChannelPluginGeneral"].on_command_added() 97 | ): 98 | self.bot.loop.create_task(coro) 99 | self.bot.reload_extension("cogs.server_tool") 100 | self.bot.reload_extension("cogs.log") 101 | self.bot.dispatch("help_reload") 102 | 103 | @debug.command(aliases=["rh", "r"]) 104 | @require_admin 105 | async def reload(self, ctx): 106 | async with ctx.typing(): 107 | await self._reload() 108 | await ctx.reply("Ok") 109 | 110 | @executor_function 111 | def make_monitor_embed(self): 112 | embed = discord.Embed( 113 | title="Free-RT-Run info", 114 | description="Running on Ubuntu Linux", 115 | color=0x0066ff 116 | ) 117 | embed.add_field( 118 | name="Memory", 119 | value=f"{psutil.virtual_memory().percent}%" 120 | ) 121 | embed.add_field( 122 | name="CPU", 123 | value=f"{psutil.cpu_percent(interval=1)}%" 124 | ) 125 | embed.add_field( 126 | name="Disk", 127 | value=f"{psutil.disk_usage('/').percent}%" 128 | ) 129 | return embed 130 | 131 | @debug.command() 132 | @require_admin 133 | async def monitor(self, ctx): 134 | await ctx.typing() 135 | await ctx.reply( 136 | embed=await self.make_monitor_embed() 137 | ) 138 | 139 | 140 | async def setup(bot): 141 | await bot.add_cog(Debug(bot)) 142 | 143 | 144 | if __name__ == "__main__": 145 | printer = Printer() 146 | exec(input(), {"print": printer.print}) 147 | print(printer.output) 148 | -------------------------------------------------------------------------------- /util/dpy_monkey.py: -------------------------------------------------------------------------------- 1 | # Free RT - Monkey_Patch, Author: yaakiyu 2 | # これらのコードはd.pyでのみ動作します。 3 | 4 | from __future__ import annotations 5 | 6 | from discord.ext import commands 7 | import discord 8 | 9 | from .webhooks import webhook_send 10 | from .cacher import CacherPool 11 | from .ext import componesy 12 | 13 | 14 | async def _setup(self, mode: tuple[str, ...] = ()) -> None: 15 | for name in ("on_send", "on_full_reaction", "on_cog_add"): 16 | if name in mode or mode == (): 17 | try: 18 | await self.load_extension("util.ext." + name) 19 | except commands.ExtensionAlreadyLoaded: 20 | pass 21 | for name in ("dochelp", "rtws", "websocket", "debug", "settings", "lib_data_manager"): 22 | if name in mode or mode == (): 23 | try: 24 | await self.load_extension("util." + name) 25 | except commands.ExtensionAlreadyLoaded: 26 | pass 27 | self.cachers = CacherPool() 28 | 29 | 30 | # webhook_sendとcomponesyを新しく定義する。 31 | discord.abc.Messageable.webhook_send = webhook_send # type: ignore 32 | discord.ext.commands.Context.webhook_send = webhook_send # type: ignore 33 | # componesyに関してはモンキーパッチ脱却予定なのでext.easyからのアクセスは非推奨。 34 | discord.ext.easy = componesy # type: ignore 35 | 36 | 37 | default_hybrid_command = commands.hybrid_command 38 | 39 | 40 | def descriptor_hybrid(default): 41 | 42 | def new_function(*args, **kwargs): 43 | if not kwargs.get("description", False): 44 | if kwargs.get("extras", False) and "headding" in kwargs["extras"]: 45 | kwargs["description"] = kwargs["extras"]["headding"]["ja"] 46 | else: 47 | kwargs["description"] = "No description provided." 48 | return default(*args, **kwargs) 49 | 50 | return new_function 51 | 52 | 53 | commands.hybrid_command = descriptor_hybrid(commands.hybrid_command) 54 | commands.hybrid_group = descriptor_hybrid(commands.hybrid_group) 55 | 56 | 57 | def descriptor_sub(default): 58 | 59 | def new_function(self, *args, **kwargs): 60 | if not kwargs.get("description", False): 61 | if kwargs.get("extras", False) and "headding" in kwargs["extras"]: 62 | kwargs["description"] = kwargs["extras"]["headding"]["ja"] 63 | elif self.description is not None: 64 | kwargs["description"] = self.description 65 | else: 66 | kwargs["description"] = "No description provided." 67 | return default(self, *args, **kwargs) 68 | 69 | return new_function 70 | 71 | 72 | commands.HybridGroup.command = descriptor_sub(commands.HybridGroup.command) 73 | commands.HybridGroup.group = descriptor_sub(commands.HybridGroup.group) 74 | 75 | 76 | async def setup(bot): 77 | pass 78 | -------------------------------------------------------------------------------- /util/ext/__init__.py: -------------------------------------------------------------------------------- 1 | from . import view as componesy 2 | 3 | __all__ = [ 4 | "componesy", 5 | "on_cog_add", 6 | "on_full_reaction", 7 | "on_send" 8 | ] 9 | -------------------------------------------------------------------------------- /util/ext/on_cog_add.py: -------------------------------------------------------------------------------- 1 | """Cogの追加/削除時に呼び出される`on_cog_add/remove`というイベントを作るためのエクステンションです。 2 | `bot.load_extension("util.ext.on_cog_add")`で有効化することができます。 3 | また`util.setup(bot)`でも有効化することができます。 4 | 5 | # Examples 6 | ```python 7 | @bot.event 8 | async def on_cog_add(cog): 9 | print("Added cog", cog.__name__) 10 | 11 | @bot.event 12 | async def on_cog_remove(cog): 13 | print("Removed cog", cog.__name__) 14 | ```""" 15 | 16 | from discord.ext import commands 17 | 18 | 19 | class OnCogAdd(commands.Cog): 20 | def __init__(self, bot): 21 | self.bot = bot 22 | 23 | def _add_cog(self, cog, **kwargs): 24 | self.bot.dispatch("cog_add", cog) 25 | 26 | def _remove_cog(self, name): 27 | cog = self.bot.cogs[name] 28 | self.bot.dispatch("cog_remove", cog) 29 | 30 | 31 | async def setup(bot): 32 | await bot.add_cog(OnCogAdd(bot)) 33 | -------------------------------------------------------------------------------- /util/ext/on_full_reaction.py: -------------------------------------------------------------------------------- 1 | 2 | from discord.ext import commands 3 | 4 | 5 | class OnFullReactionAddRemove(commands.Cog): 6 | def __init__(self, bot, timeout: float = 0.025): 7 | self.bot, self.timeout = bot, timeout 8 | 9 | @commands.Cog.listener() 10 | async def on_raw_reaction_add(self, payload): 11 | await self.on_raw_reaction_addremove(payload, "add") 12 | 13 | @commands.Cog.listener() 14 | async def on_raw_reaction_remove(self, payload): 15 | await self.on_raw_reaction_addremove(payload, "remove") 16 | 17 | async def on_raw_reaction_addremove(self, payload, event: str): 18 | # もし`self.on_reaction_addremove`が呼ばれなかった場合は自分でmessageを取得する。 19 | try: 20 | channel = ( 21 | self.bot.get_channel(payload.channel_id) 22 | if payload.guild_id 23 | else self.bot.get_user(payload.user_id) 24 | ) 25 | payload.message = await channel.fetch_message(payload.message_id) 26 | payload.member = ( 27 | payload.message.guild.get_member(payload.user_id) 28 | if payload.guild_id else None 29 | ) 30 | except Exception: 31 | return 32 | finally: 33 | # `on_full_reaction_add/remove`を呼び出す。 34 | self.bot.dispatch("full_reaction_" + event, payload) 35 | 36 | 37 | async def setup(bot): 38 | timeout = getattr(bot, "_util_ofr_timeout", 0.025) 39 | await bot.add_cog(OnFullReactionAddRemove(bot, timeout=timeout)) 40 | -------------------------------------------------------------------------------- /util/markdowns.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Markdowns 2 | 3 | from discord import Embed 4 | from typing import Tuple 5 | 6 | newlinestr = '\n' 7 | 8 | 9 | def decoration(markdown: str, separate: int = 0) -> str: 10 | """見出しが使われているマークダウンをDiscordで有効なものに変換します。 11 | ただたんに`# ...`を`**#** ...`に変換して渡された数だけ後ろに改行を付け足すだけです。 12 | Parameters 13 | ---------- 14 | markdown : str 15 | 変換するマークダウンです。 16 | separate : int, default 1 17 | 見出しを`**`で囲んだ際に後ろに何個改行を含めるかです。""" 18 | new = "" 19 | for line in markdown.splitlines(): 20 | if line.startswith(("# ", "## ", "### ", "#### ", "##### ")): 21 | line = f"**#** {line[line.find(' ')+1:]}" 22 | if line.startswith(("\n", "**#**")): 23 | line = f"{newlinestr * separate}{line}" 24 | new += f"{line}\n" 25 | return new 26 | 27 | 28 | def separate(text: str, character: str = "\n") -> Tuple[str, str]: 29 | "指定された文字列を指定された文字の左右で分けます。" 30 | return text[: (i := text.find(character))], text[i + 1:] 31 | 32 | 33 | def create_embed(markdown: str, **kwargs) -> Embed: 34 | """渡されたマークダウンの文字列をタイトルと説明とフィールドが設定されている`discord.Embed`に変換します。 35 | 見出しは三段階設定することができ、一段でタイトルで二段でフィールドそして三段で`**#** ...`のようになります。 36 | もしフィールドの`inline`を`False`にしたい場合は`## !`のようにしてください。 37 | Parameters 38 | ---------- 39 | markdown : str 40 | 変換するマークダウンです。 41 | kwargs : dict 42 | `discord.Embed`のインスタンス作成時に渡すキーワード引数です。 43 | Examples 44 | -------- 45 | ``` 46 | # Title 47 | Description 48 | ## Field1 49 | Field1 value 50 | ### Field1 Child1 51 | Field1 Child1 Value 52 | #### Field1 Child2 53 | Field1 Child2 Value 54 | ## Field2 55 | Field2 value 56 | ### Field2 Child1 57 | Field2 Child1 Value 58 | #### Field2 Child2 59 | Field2 Child2 Value 60 | ```""" 61 | kwargs["title"], fields = separate(markdown) 62 | fields, kwargs["title"] = fields.split("\n## "), kwargs["title"][2:] 63 | kwargs["description"] = decoration(fields[0]) 64 | del fields[0] 65 | embed = Embed(**kwargs) 66 | for field in fields: 67 | (name, value), inline = separate(field), True 68 | if name.startswith("!"): 69 | inline, name = False, name[1:] 70 | embed.add_field( 71 | name=name, value=decoration(value), inline=inline 72 | ) 73 | return embed 74 | -------------------------------------------------------------------------------- /util/minesweeper.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Mine Sweeper Game Engine 2 | 3 | from typing import Optional 4 | 5 | import random 6 | 7 | newlinestr = '\n' 8 | 9 | 10 | class MineSweeper: 11 | 12 | def __init__( 13 | self, xlen: int, ylen: int, bombs: int, 14 | seed: Optional[int] = None, log: bool = False 15 | ) -> None: 16 | "マインスイーパーです。インスタンス化でデータの作成までを行います。" 17 | if xlen > 100 or ylen > 100: 18 | raise ValueError("xlen and ylen must be 100 or less.") 19 | self.xlen: int = xlen 20 | self.ylen: int = ylen 21 | 22 | if bombs > (xlen * ylen): 23 | raise ValueError("bombs must be less than numbers of all squares.") 24 | self.bombs: int = bombs 25 | 26 | self.logging: bool = log 27 | if seed: 28 | random.seed(seed) 29 | self._make_data() 30 | self.now_opened = [] 31 | 32 | def _make_data(self) -> None: 33 | # 初期データをself.dataに2次元配列で作成します。0~8の数字は回りにある爆弾の数、9は爆弾を表します。 34 | # 通常はこの関数はインスタンス時に自動で実行されます。 35 | raw_data = [0] * (self.xlen * self.ylen) 36 | for i in random.sample(range(self.xlen * self.ylen), k=self.bombs): 37 | raw_data[i] = 9 38 | 39 | # 2次元配列に直す。 40 | t_data = [ 41 | [raw_data[x * self.ylen + y] for y in range(self.ylen)] 42 | for x in range(self.xlen) 43 | ] 44 | 45 | for x_checking in len(t_data): 46 | for y_checking in len(t_data[x_checking]): 47 | t_data[x_checking][y_checking] = \ 48 | self.get_around_data(t_data, x_checking, y_checking).count(9) 49 | 50 | self.data: tuple[tuple[int]] = tuple([tuple(i) for i in t_data]) 51 | if self.logging: 52 | print(f"[util][MineSweeper]maked data: {newlinestr.join(self.data)}") 53 | 54 | def get_around_data(self, t_data, x, y) -> tuple[int]: 55 | "t_dataのx番目のy番目の周りの数(壁を越えていたら0)を取得したリストを返します。" 56 | if t_data[x][y] == 9: 57 | return (9, 9, 9, 9, 9, 9, 9, 9, 9) # 9の数が9個なので問題ない。 58 | d = [] 59 | 60 | for m in ( 61 | (x - 1, y - 1), (x - 1, y), (x - 1, y + 1), 62 | (x, y - 1), (x, y), (x, y + 1), 63 | (x + 1, y - 1), (x + 1, y), (x + 1, y + 1) 64 | ): 65 | if -1 in m or len(t_data) + 1 == m[0] or len(t_data[x]) + 1 == m[1]: 66 | # 限界突破(壁を越えて判定している。) 67 | d.append(0) 68 | d.append(t_data[m[0]][m[1]]) 69 | 70 | return tuple(d) 71 | 72 | def open(self, x: int, y: int) -> tuple[int]: 73 | """self.dataのx行目, y列目を取り出します。 74 | タプル型が返され、1番目が結果(0=操作完了, 1=クリア, 2=ゲームオーバー、3=すでに引いている)で、 75 | 2番目が引いた数字になります。 76 | """ 77 | assert x < self.xlen, "存在しない番地です。" 78 | assert y < self.ylen, "存在しない番地です。" 79 | # 実際のコマンドでは、この2つはコマンド処理側ではじかれるのでエラーは出ない。 80 | 81 | number = self.data[x][y] 82 | if self.logging: 83 | print(f"[util][Minesweeper] opened x : {x}, y : {y} -> {number}") 84 | 85 | if (x, y) in self.now_opened: 86 | # もう引いている。 87 | return (3, number) 88 | 89 | self.now_opened.append((x, y)) 90 | 91 | if number == 9: 92 | # 爆弾を引いてゲームオーバー。 93 | return (2, number) 94 | elif len(self.now_opened) == (self.xlen * self.ylen - self.bombs): 95 | # 爆弾以外すべて引いたのでゲームクリア。 96 | return (1, number) 97 | else: 98 | # ゲームは続行。 99 | return (0, number) 100 | 101 | def to_string(self, mode: str = "s") -> str: 102 | "現在の状況をEmbedのdescriptionに表示する形式の文字列に変換します。" 103 | return "\n".join( 104 | ("`" + "` `".join( 105 | ["💣" if x == 9 else x if x in self.now_opened else "■" for x in line] 106 | if mode == "s" else ["💣" if x == 9 else x for x in line] 107 | ) + "`") for line in [list(i) for i in self.data] 108 | ) 109 | -------------------------------------------------------------------------------- /util/olds.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Old features 2 | 3 | from typing import Optional, Union, Tuple, List 4 | 5 | from discord.ext import commands, tasks # type: ignore 6 | import discord 7 | 8 | from pymysql.err import OperationalError 9 | from . import isintable 10 | 11 | # from .slash import Context as SlashContext 12 | from .cacher import CacherPool 13 | 14 | 15 | def role2obj(guild: discord.Guild, arg: str) -> list[Optional[discord.Role]]: 16 | "`役職1, 役職2, ...`のようになってるものをロールオブジェクトに変換します。" 17 | roles_raw, roles = arg.split(','), [] 18 | for role in roles_raw: 19 | if '@' in role: 20 | roles.append(guild.get_role(int(role[3:-1]))) 21 | elif isintable(role): 22 | roles.append(guild.get_role(int(role))) 23 | else: 24 | roles.append(discord.utils.get(guild.roles, name=role)) 25 | return roles 26 | 27 | 28 | class Roler(commands.Converter): 29 | "`role2obj`のコンバーターです。現在は非推奨です。util.RolesConverterを使ってください。" 30 | async def convert(self, ctx, arg): 31 | return role2obj(ctx.guild, arg) 32 | 33 | 34 | class FakeMessageForCleanContent: 35 | def __init__( 36 | self, guild: Optional[discord.Guild], content: str 37 | ): 38 | self.guild, self.content = guild, content 39 | self.mentions = self._get("member", "") 40 | self.role_mentions, self.channel_mentions = \ 41 | self._get("role"), self._get("channel") 42 | 43 | def _get(self, get_mode, mentions_mode=None): 44 | return [ 45 | getattr(self.guild, f"get_{get_mode}")(mention) 46 | for mention in getattr( 47 | self, f"raw_{f'{get_mode}_' if mentions_mode is None else mentions_mode}mentions" 48 | ) 49 | ] 50 | 51 | 52 | for name in ("clean_content", "raw_mentions", "raw_role_mentions", "raw_channel_mentions"): 53 | setattr(FakeMessageForCleanContent, name, getattr(discord.Message, name)) 54 | 55 | 56 | def clean_content(content: str, guild: discord.Guild) -> str: 57 | "渡された文字列を綺麗にします。" 58 | return FakeMessageForCleanContent(guild, content).clean_content 59 | 60 | 61 | Context = Union[commands.Context] 62 | 63 | 64 | def lib_setup(bot, only: Union[Tuple[str, ...], List[str]] = []): 65 | "元rtlibにあるエクステンションを全てまたは指定されたものだけ読み込みます。" 66 | for name in ("on_send", "on_full_reaction", "on_cog_add"): 67 | if name in only or only == []: 68 | try: 69 | bot.load_extension("util.ext." + name) 70 | except commands.ExtensionAlreadyLoaded: 71 | pass 72 | for name in ("dochelp", "rtws", "websocket", "debug", "settings", "lib_data_manager"): 73 | if name in only or only == []: 74 | try: 75 | bot.load_extension("util." + name) 76 | except commands.ExtensionAlreadyLoaded: 77 | pass 78 | bot.cachers = CacherPool() 79 | 80 | 81 | # discord.ext.tasksのタスクがデータベースの操作失敗によって止まることがないようにする。 82 | def tasks_extend(): 83 | if not getattr(tasks.Loop, "_util_extended", False): 84 | default = tasks.Loop.__init__ 85 | 86 | def _init(self, *args, **kwargs): 87 | default(self, *args, **kwargs) 88 | self.add_exception_type(OperationalError) 89 | self.add_exception_type(discord.DiscordServerError) 90 | tasks.Loop.__init__ = _init 91 | tasks.Loop._util_extended = True 92 | 93 | 94 | def sendKwargs(ctx, **kwargs): 95 | if isinstance(ctx, commands.Context): 96 | for key in list(kwargs.keys()): 97 | if (key not in discord.abc.Messageable.send.__annotations__ 98 | and key in discord.InteractionResponse.send_message.__annotations__): 99 | del kwargs[key] 100 | return kwargs 101 | -------------------------------------------------------------------------------- /util/page.py: -------------------------------------------------------------------------------- 1 | # Free RT - Page, Notes: これはパブリックドメインとします。 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Literal, Optional, Any 6 | 7 | import discord 8 | 9 | from .views import TimeoutView 10 | 11 | 12 | class BasePage(TimeoutView): 13 | def __init__(self, *args, data: Optional[Any] = None, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | self.data, self.page = data, 0 16 | 17 | async def on_turn( 18 | self, mode: Literal["dl", "l", "r", "dr"], interaction: discord.Interaction 19 | ): 20 | if mode[0] == "d": 21 | self.page = 0 if mode[1] == "l" else len(self.data) - 1 22 | else: 23 | self.page = ( 24 | self.page + (len(self.data) - 1 if mode == "l" else 1) 25 | ) % len(self.data) 26 | 27 | @discord.ui.button(emoji="⏪") 28 | async def dash_left(self, interaction: discord.Interaction, _): 29 | await self.on_turn("dl", interaction) 30 | 31 | @discord.ui.button(emoji="◀️") 32 | async def left(self, interaction: discord.Interaction, _): 33 | await self.on_turn("l", interaction) 34 | 35 | @discord.ui.button(emoji="▶️") 36 | async def right(self, interaction: discord.Interaction, _): 37 | await self.on_turn("r", interaction) 38 | 39 | @discord.ui.button(emoji="⏩") 40 | async def dash_right(self, interaction: discord.Interaction, _): 41 | await self.on_turn("dr", interaction) 42 | 43 | 44 | class EmbedPage(BasePage): 45 | def __init__(self, *args, data: Optional[list[discord.Embed]] = None, **kwargs): 46 | assert data is not None, "埋め込みのリストを渡してください。" 47 | super().__init__(*args, data=data, **kwargs) 48 | 49 | async def on_turn(self, mode: str, interaction: discord.Interaction): 50 | before = self.page 51 | await super().on_turn(mode, interaction) 52 | if self.page == before: 53 | return 54 | try: 55 | assert 0 <= self.page 56 | embed = self.data[self.page] 57 | except (AssertionError, IndexError): 58 | self.page = before 59 | if mode == "dl": 60 | self.page = 0 61 | embed = self.data[self.page] 62 | elif mode == "dr": 63 | self.page = len(self.data) - 1 64 | embed = self.data[self.page] 65 | else: 66 | return await interaction.response.send_message( 67 | "これ以上ページを捲ることができません。", ephemeral=True 68 | ) 69 | await interaction.response.edit_message(embed=embed, **self.on_page()) 70 | 71 | def on_page(self): 72 | return {} 73 | -------------------------------------------------------------------------------- /util/rtws.py: -------------------------------------------------------------------------------- 1 | # Free RT - Free RT WebSocket, Description: バックエンドと通信をするためのものです。 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Literal, Union, Optional 6 | 7 | from discord.ext import commands 8 | import discord 9 | 10 | from .rt_module.src import rtws, rtws_feature_types as rft 11 | 12 | if TYPE_CHECKING: 13 | from .types import RT 14 | 15 | 16 | class RTWSGeneralFeatures(commands.Cog): 17 | def __init__(self, bot: RT): 18 | self.bot = bot 19 | for name, value in map(lambda name: (name, getattr(self, name)), dir(self)): 20 | if name.startswith("get"): 21 | self.bot.rtws.set_event(value) 22 | 23 | async def get_user(self, user_id: int) -> Optional[rft.User]: 24 | if user := self.bot.get_user(user_id): 25 | return rft.User( 26 | id=user.id, name=user.name, discriminator=user.discriminator, 27 | avatar_url=getattr(user.display_avatar, "url", ""), full_name=str(user) 28 | ) 29 | 30 | async def get_guilds(self, user_id: int) -> list[rft.Guild]: 31 | return [ 32 | self._prepare_guild(guild, full=False) 33 | for guild in self.bot.guilds 34 | if guild.get_member(user_id) is not None 35 | ] 36 | 37 | def _get_guild_child( 38 | self, guild: rft.Guild, key: str, id_: int 39 | ) -> Optional[dict]: 40 | data = discord.utils.get(guild[key], id=id_) 41 | data["guild"] = guild 42 | return data 43 | 44 | async def get_member(self, data: tuple[rft.Guild, int]) -> Optional[rft.Member]: 45 | return self._get_guild_child(data[0], "members", data[1]) 46 | 47 | def _get_channel( 48 | self, guild: discord.Guild, mode: Optional[Literal["voice", "text"]] = None 49 | ) -> list[rft.Channel]: 50 | channels = [] 51 | for channel in guild.channels: 52 | type_ = "text" \ 53 | if isinstance(channel, (discord.TextChannel, discord.Thread)) \ 54 | else "voice" 55 | if mode is None or type_ == mode: 56 | channels.append(rft.Channel( 57 | id=channel.id, name=channel.name, guild=None, type=type_ 58 | )) 59 | return channels 60 | 61 | async def get_channel(self, data: tuple[rft.Guild, int]) -> Optional[rft.Channel]: 62 | return self._get_guild_child(data[0], "channels", data[1]) 63 | 64 | async def get_role(self, data: tuple[rft.Guild, int]) -> Optional[rft.Role]: 65 | for id_, name in data["roles"].items(): 66 | if id_ == data[1]: 67 | return rft.Role(id=data[1], name=name) 68 | 69 | def _prepare_guild(self, guild: discord.Guild, full: bool) -> rft.Guild: 70 | text_channels = self._get_channel(guild, "text") 71 | voice_channels = self._get_channel(guild, "voice") 72 | if full: 73 | return rft.Guild( 74 | id=guild.id, name=guild.name, avatar_url=getattr(guild.icon, "url", ""), 75 | members=[ 76 | rft.Member( 77 | id=member.id, name=member.name, avatar_url=getattr( 78 | member.display_avatar, "url", "" 79 | ), 80 | full_name=str(member), guild=None 81 | ) for member in guild.members 82 | ], text_channels=text_channels, voice_channels=voice_channels, 83 | channels=text_channels + voice_channels, roles=[ 84 | rft.Role(id=role.id, name=role.name) 85 | for role in guild.roles 86 | ] 87 | ) 88 | else: 89 | return rft.Guild(id=guild.id, name=guild.name) 90 | 91 | async def get_guild(self, guild_id: int, full=True) -> Optional[rft.Guild]: 92 | if guild := self.bot.get_guild(guild_id): 93 | return self._prepare_guild(guild, full) 94 | 95 | async def get_lang(self, user_id: int) -> Union[Literal["ja", "en"], str]: 96 | return self.bot.cogs["Language"].get(user_id) 97 | 98 | def cog_unload(self): 99 | if self.bot.rtws.is_connected(): 100 | self.bot.loop.create_task( 101 | self.bot.rtws.close(1000, "再接続または停止のため切断しました。"), 102 | name="Disconnect RTWebSocket" 103 | ) 104 | self.bot.rtws.task.cancel() 105 | del self.bot.rtws 106 | 107 | 108 | class ExtendedRTWebSocket(rtws.RTWebSocket): 109 | 110 | bot: RT 111 | 112 | def log(self, mode: str, *args, **kwargs): 113 | return self.bot.print("[RTWebSocket]", f"[{mode}]", *args, **kwargs) 114 | 115 | 116 | async def setup(bot: RT): 117 | if not hasattr(bot, "rtws"): 118 | bot.rtws = self = ExtendedRTWebSocket("Bot", loop=bot.loop) 119 | self.bot = bot 120 | 121 | bot.rtws.task = bot.loop.create_task( 122 | self.start( 123 | f"ws://{bot.get_ip()}/api/rtws", 124 | reconnect=not bot.test, okstatus=() 125 | ), name="RTWebSocket" 126 | ) 127 | await bot.add_cog(RTWSGeneralFeatures(bot)) 128 | -------------------------------------------------------------------------------- /util/securl.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Sec URL 2 | 3 | from typing import TypedDict 4 | 5 | from aiohttp import ClientSession 6 | from ujson import loads 7 | 8 | from data.headers import SECURL_HEADERS as HEADERS 9 | 10 | 11 | class SecURLData(TypedDict, total=False): 12 | "SecURLのURLチェックの結果のデータの型です。" 13 | status: int 14 | imgWidth: int 15 | imgHeight: int 16 | reqURL: str 17 | resURL: str 18 | title: str 19 | anchors: list 20 | viruses: list 21 | blackList: list 22 | annoyUrl: str 23 | img: str 24 | capturedDate: str 25 | 26 | 27 | async def check( 28 | session: ClientSession, url: str, wait_time: int = 1, 29 | browser_width: int = 965, browser_height: int = 683, 30 | headers: dict = HEADERS 31 | ) -> SecURLData: 32 | """渡されたURLをSecURLでチェックします。 33 | Parameters 34 | ---------- 35 | session : aiohttp.ClientSession 36 | 通信に使うsessionです。 37 | url : str 38 | チェックするURLです。 39 | wait_time : int, default 1 40 | どれだけ待つかです。 41 | browser_width : int, default 965 42 | ブラウザのサイズです。 43 | browser_height : int, default 683 44 | ブラウザのサイズです。 45 | headers : dict, default HEADERS 46 | 通信に使うヘッダーです。通常は変更しなくても大丈夫です。 47 | Raises 48 | ------ 49 | ValueError : URLにアクセスできなかった際などの失敗時に発生します。""" 50 | async with session.post( 51 | "https://securl.nu/jx/get_page_jx.php", data={ 52 | "url": url, 'waitTime': str(wait_time), 53 | 'browserWidth': str(browser_width), 54 | 'browserHeight': str(browser_height), 'from': '' 55 | }, headers=headers 56 | ) as r: 57 | return loads(await r.text()) 58 | 59 | 60 | def get_capture( 61 | data: SecURLData, full: bool = False 62 | ) -> str: 63 | """渡されたデータにある`img`のデータからURLを作ります。 64 | Parameters 65 | ---------- 66 | data : SecURLData 67 | SecURLから返された辞書データです。 68 | full : bool 69 | 上から一番下までの写真のURLを返すかどうかです。""" 70 | return ( 71 | f"https://securl.nu/save_local_captured.php?key={data['img'][10:-4]}" 72 | if full else f"{HEADERS['Origin']}{data['img']}" 73 | ) 74 | -------------------------------------------------------------------------------- /util/types.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Types 2 | 3 | from typing import Union, Dict 4 | 5 | 6 | sendableString = Union[str, Dict[str, str]] 7 | -------------------------------------------------------------------------------- /util/views.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - Views 2 | 3 | from typing import Optional 4 | 5 | import discord 6 | 7 | 8 | class TimeoutView(discord.ui.View): 9 | "タイムアウト時にコンポーネントを使用不可に編集するようにするViewです。" 10 | 11 | message: Optional[discord.Message] = None 12 | 13 | async def on_timeout(self): 14 | for child in self.children: 15 | if hasattr(child, "disabled"): 16 | child.disabled = True 17 | if self.message is not None: 18 | await self.message.edit(view=self) 19 | -------------------------------------------------------------------------------- /util/webhooks.py: -------------------------------------------------------------------------------- 1 | # Free RT Util - webhooks 2 | 3 | import discord 4 | from discord.ext import commands 5 | from typing import Optional 6 | 7 | 8 | async def get_webhook( 9 | channel: discord.TextChannel, name: str = "RT-Tool" 10 | ) -> Optional[discord.Webhook]: 11 | "ウェブフックを取得します。" 12 | return discord.utils.get(await channel.webhooks(), name=name) 13 | 14 | 15 | async def webhook_send( 16 | channel, *args, webhook_name: str = "free-RT-Tool", **kwargs 17 | ): 18 | """`channel.send`感覚でウェブフック送信をするための関数です。 19 | `channel.webhook_send`のように使えます。 20 | 21 | Parameters 22 | ---------- 23 | *args : tuple 24 | discord.pyのWebhook.sendに入れる引数です。 25 | webhook_name : str, defualt "RT-Tool" 26 | 使用するウェブフックの名前です。 27 | 存在しない場合は作成されます。 28 | **kwargs : dict 29 | discord.pyのWebhook.sendに入れるキーワード引数です。""" 30 | if isinstance(channel, commands.Context): 31 | channel = channel.channel 32 | wb = ( 33 | wb if ( 34 | wb := discord.utils.get(await channel.webhooks(), name=webhook_name) 35 | ) else await channel.create_webhook(name=webhook_name)) 36 | try: 37 | return await wb.send(*args, **kwargs) 38 | except ValueError as e: 39 | if webhook_name == "free-RT-Tool": 40 | return await webhook_send(channel, *args, webhook_name="free-R2-Tool", **kwargs) 41 | elif webhook_name == "free-R2-Tool": 42 | return await webhook_send(channel, *args, webhook_name="free-R3-Tool", **kwargs) 43 | else: 44 | raise e 45 | -------------------------------------------------------------------------------- /util/whats_new.md: -------------------------------------------------------------------------------- 1 | # What's new 2 | 更新ログみたいなものです。 3 | 4 | ## util統合での機能移動 5 | utilへの統合により以下の関数の名前が変更になりました。 6 | 7 | ### rtutil 8 | * rtutil.check_int -> util.isintable 9 | * rtutil.similer -> util.olds.similer 10 | * rtutil.has_roles -> util.has_any_roles 11 | * rtutil.role2obj -> util.olds.role2obj 12 | * rtutil.get_webhook -> util.get_webhook 13 | * rtutil.clean_content -> util.olds.clean_content 14 | * rtutil.converters.xxx 15 | -> それぞれのコンバータが独立。 16 | Members -> util.MembersConverter 17 | TextChannels -> util.TextChannelsConverter 18 | VoiceChannels -> util.VoiceChannelsConverter 19 | Roles -> util.RolesConverter 20 | * rtutil.markord -> util.markdowns 21 | * rtutil.Minesweeper -> util.MineSweeper (内容が変更、詳細はminesweeper.py) 22 | * rtutil.securl -> 変更なし 23 | * rtutil.views.TimeoutView -> util.TimeoutView 24 | 25 | ### rtlib 26 | ほとんどそのまま移行してます。 27 | * rtlib.cacher -> util.cacher 28 | * rtlib.page -> util.page 29 | * rtlib.rtws -> util.rtws 30 | * rtlib.slash -> util.slash 31 | * rtlib.websocket -> util.websocket 32 | * rtlib.typed -> util.bot, util.typesの2つに分割 33 | * rtlib.ext -> いくつかの項目はutil.ext、残りはutilに直接移動 34 | * rtlib.mysql_manager -> util.mysql_manager 35 | * rtlib.Table (rtlib.data_manager.Table) -> util.Table (util.lib_data_manager.Table) 36 | 37 | ## util統合での機能追加 38 | utilへの統合でいくつかの機能が追加されました。 39 | * util.has_all_roles 40 | 41 | # d.py移行 42 | d.pyの移行によりいくつかの機能は仕様変更しました。 43 | * _dpy_monky.setupの作成、olds.lib_setupは動かないように 44 | * bot.add_cogがデフォルトでオーバーライドするように 45 | * スラッシュ関連のモンキーパッチはさようなら 46 | * music関連機能はさようなら(なんでここに書いたかは知らん) --------------------------------------------------------------------------------