├── .gitignore ├── Dockerfile ├── Procfile ├── README.md ├── cogs ├── change_bot_nickname.py ├── help.py ├── match.py └── role.py ├── config.py ├── fly.toml ├── launcher.py ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | my_config.py 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6 2 | WORKDIR /bot 3 | COPY requirements.txt /bot/ 4 | RUN pip install -r requirements.txt 5 | COPY . /bot 6 | CMD python launcher.py 7 | 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | discordbot: python launcher.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grouping-manager-bot 2 | 3 | 'grouping-manager-bot' は、Discord での小グループ活動を促進する Bot です。 4 | 以下の2つの機能があります。 5 | 1. マッチング機能:マッチング希望者を、2人、もしくは3人を基本とした小グループに分ける 6 | 2. ロール付与機能:任意の名前のロールを作成し、リアクションによってロールを on/off する 7 | 8 | ## 開発環境 9 | Python 3.9.6 10 | 11 | 12 | ## 機能概要 13 | ### マッチング 14 | 1. Discord サーバーにて`+match` コマンドを実行 15 | → マッチング希望者を募るメッセージを生成します。 16 | 2. 生成されたメッセージに、2️⃣もしくは 3️⃣にてリアクション 17 | → マッチング結果を表示します。 18 | 19 | ### ロール付与 20 | Discord サーバーにて`+role (引数)` コマンドを実行 21 | → 以下の2つが生成されます。 22 | - 引数に与えた名前を冠したロール 23 | - そのロールの着脱用のメッセージ 24 | 25 | ※ロールの削除は、リアクションだけでなく、`+rm (引数)`コマンドでも行えます。 26 | 27 | ## 利用方法 28 | 以下の2種類からお選びください。 29 | 30 | ### 1. こちらで公開しているものを招待 31 | 32 | https://discord.com/api/oauth2/authorize?client_id=874495948246614068&permissions=335629392&scope=bot 33 | 34 | ### 2. 自前でDiscordのApplicationの作成 35 | 36 | https://discordapp.com/developers/applications/ 37 | 38 | Botの設定 39 | 40 | - Developer Portal から Bot を作成し、Token を環境変数に設定 41 | - PRESENCE INTENT と SERVER MEMBERS INTENT を ON にする 42 | - OAuth2 の Scope から Bot をチェックし、必要権限にチェックする 43 | - 発行された URL から Bot をサーバーに招待する 44 | 45 | #### 必要権限 46 | - Manage Roles 47 | - Manage Channels 48 | - Change Nickname 49 | - View Channels 50 | - Send Messages 51 | - Embed Links 52 | - Read Message History 53 | - Add Reactions 54 | 55 | #### 環境変数の設定 56 | 57 | | 環境変数名 | 説明 | 58 | | --------------------- | ----------------------------------------- | 59 | | TOKEN | BotのToken | 60 | 61 | #### 起動方法 62 | Heroku: Procfile が存在するので、特別な設定は不要です。 63 | ローカル: `python launcher.py`で Bot をオンライン状態に立ち上げることができます。 64 | -------------------------------------------------------------------------------- /cogs/change_bot_nickname.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | 4 | class ChNick(commands.Cog): 5 | def __init__(self, bot): 6 | self.bot = bot 7 | 8 | @commands.command() 9 | async def chnick(self, ctx): 10 | await ctx.guild.me.edit(nick="マッチングアプリ") 11 | 12 | 13 | def setup(bot): 14 | bot.add_cog(ChNick(bot)) 15 | -------------------------------------------------------------------------------- /cogs/help.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | 5 | class Help(commands.Cog): 6 | def __init__(self, bot): 7 | self.bot = bot 8 | 9 | def make_embed(self): 10 | embed = discord.Embed( 11 | color=discord.Color.orange(), 12 | ) 13 | embed.add_field( 14 | name="match", 15 | value="マッチング募集用メッセージの送信", 16 | inline=False 17 | ) 18 | embed.add_field( 19 | name="role (引数)", 20 | value="ロール付与用メッセージの送信\n" 21 | "e.g.) `+role hoge`: `role_hoge`を生成\n" 22 | "引数なしで実行した場合、未使用の数値が割り当てられます。", 23 | inline=False 24 | ) 25 | embed.set_footer( 26 | text="不具合等のご報告は、あんすと(unstoppa61e)までお願いします。" 27 | ) 28 | return embed 29 | 30 | @commands.command() 31 | async def help(self, ctx): 32 | embed = self.make_embed() 33 | await ctx.send(embed=embed) 34 | 35 | 36 | def setup(bot): 37 | bot.add_cog(Help(bot)) 38 | -------------------------------------------------------------------------------- /cogs/match.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | import asyncio 4 | import random 5 | import math 6 | from cogs.role import Role 7 | 8 | 9 | class Match(commands.Cog): 10 | WILLING_EMOJI = '✋' 11 | TWO_EMOJI = '2️⃣' 12 | THREE_EMOJI = '3️⃣' 13 | 14 | def __init__(self, bot): 15 | self.bot = bot 16 | 17 | def make_text(self, author_mention) -> str: 18 | return f"マッチング希望者は、{self.WILLING_EMOJI}によるリアクションをお願いします"\ 19 | "(通知用の新ロールが付与されます)。\n"\ 20 | ":two:で2名、:three:で3名を基本としたマッチングを行います。\n"\ 21 | f"{author_mention} {Role.make_how_to_destruct_role_message()}" 22 | 23 | def make_line(self, channel_name, room_member_ids, guild): 24 | room_members_mentions = [] 25 | for member_id in room_member_ids: 26 | member = guild.get_member(member_id) 27 | room_members_mentions.append(member.mention) 28 | return f"**{channel_name}**: {' / '.join(room_members_mentions)}\n" 29 | 30 | def make_capacity_per_room_two_basis(self, users_num, capacity_basis): 31 | ROOMS_NUM = users_num // 2 32 | capacity_per_room = [2] * ROOMS_NUM 33 | if users_num % 2 == 1: 34 | capacity_per_room[0] += 1 35 | return capacity_per_room 36 | 37 | def make_capacity_per_room_three_basis(self, users_num, capacity_basis): 38 | ROOMS_NUM = math.ceil(users_num / 3.0) 39 | capacity_per_room = [2] * ROOMS_NUM 40 | REMAINDER = users_num - 2 * ROOMS_NUM 41 | for i in range(REMAINDER): 42 | capacity_per_room[i] += 1 43 | return capacity_per_room 44 | 45 | def make_capacity_per_room(self, users_num, capacity_basis): 46 | if capacity_basis == 2: 47 | return self.make_capacity_per_room_two_basis( 48 | users_num, 49 | capacity_basis 50 | ) 51 | return self.make_capacity_per_room_three_basis( 52 | users_num, 53 | capacity_basis 54 | ) 55 | 56 | async def send_invitation(self, channel, channel_name, user_ids): 57 | invitation_line = self.make_line(channel_name, user_ids, channel.guild) 58 | await channel.send(invitation_line) 59 | 60 | async def send_introduction(self, channel, reactioner_mention): 61 | embed = discord.Embed( 62 | description=f"{reactioner_mention}さんがマッチングを実施しました。\n" 63 | "各自、割り当てられたボイスチャンネルへ入室してください。", 64 | color=discord.Color.gold() 65 | ) 66 | await channel.send(embed=embed) 67 | 68 | def channel_already_exists(self, channels, channel_name): 69 | for channel in channels: 70 | if channel.name == channel_name: 71 | return True 72 | return False 73 | 74 | async def send_invitations_creating_channels( 75 | self, 76 | channel, 77 | user_ids, 78 | capacity_basis, 79 | reactioner_mention, 80 | role_index_str 81 | ): 82 | await self.send_introduction(channel, reactioner_mention) 83 | capacity_per_room = self.make_capacity_per_room( 84 | len(user_ids), 85 | capacity_basis 86 | ) 87 | start_i = 0 88 | category = channel.category 89 | for i, capacity in enumerate(capacity_per_room, start=1): 90 | end_i = start_i + capacity 91 | channel_name = f"room{role_index_str}-{i}" 92 | await self.send_invitation( 93 | channel, 94 | channel_name, 95 | user_ids[start_i:end_i] 96 | ) 97 | start_i += capacity 98 | if self.channel_already_exists( 99 | channel.guild.channels, 100 | channel_name 101 | ): 102 | continue 103 | await channel.guild.create_voice_channel( 104 | channel_name, 105 | category=category 106 | ) 107 | 108 | @commands.command() 109 | async def match(self, ctx): 110 | await self.bot.wait_until_ready() 111 | channel = ctx.channel 112 | embed = discord.Embed( 113 | description=self.make_text(ctx.author.mention), 114 | color=discord.Color.blue() 115 | ) 116 | role_name = Role.make_role_name_with_index(ctx.guild.roles) 117 | role = await ctx.guild.create_role(name=role_name) 118 | embed.add_field(name="New role", value=role_name) 119 | embed.add_field(name="Called by", value=ctx.author.name) 120 | msg = await ctx.send(embed=embed) 121 | emojis = [ 122 | self.WILLING_EMOJI, 123 | self.TWO_EMOJI, 124 | self.THREE_EMOJI, 125 | Role.REMOVER_EMOJI 126 | ] 127 | for emoji in emojis: 128 | await msg.add_reaction(emoji) 129 | 130 | async def get_user_ids_for_matching(self, reactions): 131 | users = [] 132 | for reaction in reactions: 133 | if str(reaction.emoji) == self.WILLING_EMOJI: 134 | reaction_users = await reaction.users().flatten() 135 | for user in reaction_users: 136 | if user.bot: 137 | continue 138 | users.append(user.id) 139 | break 140 | return users 141 | 142 | async def handle_matching_result(self, payload, emoji_name): 143 | channel = self.bot.get_channel(payload.channel_id) 144 | if not isinstance(channel, discord.TextChannel): 145 | return 146 | try: 147 | capacity_basis = 2 if emoji_name == self.TWO_EMOJI else 3 148 | reactioned_msg = await channel.fetch_message( 149 | payload.message_id 150 | ) 151 | if reactioned_msg.author.id != self.bot.user.id: 152 | return 153 | user_ids = await self.get_user_ids_for_matching( 154 | reactioned_msg.reactions 155 | ) 156 | reactioner_mention = payload.member.mention 157 | if len(user_ids) < 2: 158 | embed = discord.Embed( 159 | description="マッチングに必要な人数が集まっていません。", 160 | color=discord.Color.red() 161 | ) 162 | await channel.send(embed=embed) 163 | return 164 | random.shuffle(user_ids) 165 | role_index_str = reactioned_msg.embeds[0].fields[0].value[5:] 166 | await self.send_invitations_creating_channels( 167 | channel, 168 | user_ids, 169 | capacity_basis, 170 | reactioner_mention, 171 | role_index_str 172 | ) 173 | except Exception as e: 174 | print(e) 175 | print(type(e)) 176 | 177 | @commands.is_owner() 178 | @commands.command() 179 | async def reload(self, ctx, module_name): 180 | await ctx.send(f"モジュール{module_name}の再読み込みを開始します。") 181 | try: 182 | self.bot.reload_extension(module_name) 183 | await ctx.send(f"モジュール{module_name}の再読み込みを終了しました。") 184 | except ( 185 | commands.errors.ExtensionNotLoaded, 186 | commands.errors.ExtensionNotFound, 187 | commands.errors.NoEntryPointError, 188 | commands.errors.ExtensionFailed 189 | ) as e: 190 | await ctx.send(f"モジュール{module_name}の再読み込みに失敗しました。理由: {e}") 191 | return 192 | 193 | @commands.Cog.listener() 194 | async def on_raw_reaction_add(self, payload): 195 | if payload.user_id == self.bot.user.id: 196 | return 197 | emoji_name = payload.emoji.name 198 | if emoji_name == self.WILLING_EMOJI: 199 | await Role.handle_role_toggling_reaction(self, payload, True) 200 | elif emoji_name == self.TWO_EMOJI or emoji_name == self.THREE_EMOJI: 201 | await self.handle_matching_result(payload, emoji_name) 202 | 203 | @commands.Cog.listener() 204 | async def on_raw_reaction_remove(self, payload): 205 | if payload.emoji.name != self.WILLING_EMOJI: 206 | return 207 | if payload.user_id == self.bot.user.id: 208 | return 209 | await Role.handle_role_toggling_reaction(self, payload, False) 210 | 211 | 212 | def setup(bot): 213 | bot.add_cog(Match(bot)) 214 | -------------------------------------------------------------------------------- /cogs/role.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | import asyncio 4 | 5 | 6 | class Role(commands.Cog): 7 | REGISTER_EMOJI = '✅' 8 | REMOVER_EMOJI = '❌' 9 | ROLE_NAME_HEAD = 'role_' 10 | 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | def make_how_to_destruct_role_message(): 15 | message = f"このロールが不要になりましたら、{Role.REMOVER_EMOJI}にて、サーバーから削除できます。" 16 | return message 17 | 18 | def make_text(self, author_mention) -> str: 19 | return f"各自、{self.REGISTER_EMOJI}にて、新ロールのオン・オフが可能です。\n"\ 20 | f"{author_mention} {Role.make_how_to_destruct_role_message()}" 21 | 22 | def get_role_index_not_used(roles): 23 | num = 0 24 | while True: 25 | found = False 26 | for role in roles: 27 | if role.name == f"{Role.ROLE_NAME_HEAD}{num}": 28 | found = True 29 | break 30 | if found is False: 31 | break 32 | num += 1 33 | return num 34 | 35 | def make_role_name_with_index(roles): 36 | role_index = Role.get_role_index_not_used(roles) 37 | return f"{Role.ROLE_NAME_HEAD}{role_index}" 38 | 39 | @commands.Cog.listener() 40 | async def on_raw_reaction_add(self, payload): 41 | if payload.user_id == self.bot.user.id: 42 | return 43 | if payload.emoji.name == self.REGISTER_EMOJI: 44 | await self.handle_role_toggling_reaction(payload, True) 45 | elif payload.emoji.name == self.REMOVER_EMOJI: 46 | await self.handle_role_destroying_reaction(payload) 47 | 48 | @commands.Cog.listener() 49 | async def on_raw_reaction_remove(self, payload): 50 | if not payload.emoji.name == self.REGISTER_EMOJI: 51 | return 52 | if payload.user_id == self.bot.user.id: 53 | return 54 | await self.handle_role_toggling_reaction(payload, False) 55 | 56 | def get_role_corresponding_to_message(guild, message): 57 | role_name = message.embeds[0].fields[0].value 58 | role = discord.utils.get(guild.roles, name=role_name) 59 | return role 60 | 61 | async def handle_role_toggling_reaction(self, payload, adding): 62 | channel = self.bot.get_channel(payload.channel_id) 63 | if not isinstance(channel, discord.TextChannel): 64 | return 65 | try: 66 | reactioned_msg = await channel.fetch_message(payload.message_id) 67 | if reactioned_msg.author.id != self.bot.user.id: 68 | return 69 | guild = reactioned_msg.guild 70 | member = discord.utils.find( 71 | lambda m: m.id == payload.user_id, 72 | guild.members 73 | ) 74 | if member.bot: 75 | return 76 | role = Role.get_role_corresponding_to_message( 77 | guild, 78 | reactioned_msg 79 | ) 80 | if role is None: 81 | return 82 | if adding: 83 | await member.add_roles(role) 84 | else: 85 | await member.remove_roles(role) 86 | except Exception as e: 87 | print(e) 88 | print(type(e)) 89 | 90 | async def send_role_destroying_msg(self, user_mention, role_name, channel): 91 | embed = discord.Embed( 92 | description=f"{user_mention}さんにより、`{role_name}`ロールが削除されました。", 93 | color=discord.Color.green() 94 | ) 95 | await channel.send(embed=embed) 96 | 97 | async def send_error_msg_via_dm(self, member, msg, text): 98 | msg_url = msg.jump_url 99 | text_to_jump_back = f"[こちらのリンク]({msg_url})から、元のメッセージに戻れます。" 100 | embed = discord.Embed( 101 | color=discord.Color.red(), 102 | description=f"{text}\n{text_to_jump_back}" 103 | ) 104 | await member.send(embed=embed) 105 | return 106 | 107 | async def handle_role_destroying_reaction(self, payload): 108 | channel = self.bot.get_channel(payload.channel_id) 109 | if not isinstance(channel, discord.TextChannel): 110 | return 111 | try: 112 | reactioned_msg = await channel.fetch_message(payload.message_id) 113 | if reactioned_msg.author.id != self.bot.user.id: 114 | return 115 | guild = reactioned_msg.guild 116 | member = discord.utils.find( 117 | lambda m: m.id == payload.user_id, 118 | guild.members 119 | ) 120 | if member.bot: 121 | return 122 | role = Role.get_role_corresponding_to_message( 123 | guild, 124 | reactioned_msg 125 | ) 126 | if role is None: 127 | role_name = reactioned_msg.embeds[0].fields[0].value 128 | text = f"`{role_name}`ロールは既に削除済みです。" 129 | await self.send_error_msg_via_dm(member, reactioned_msg, text) 130 | return 131 | command_user_name = reactioned_msg.embeds[0].fields[1].value 132 | if command_user_name != member.name: 133 | text = f"{self.REMOVER_EMOJI}によるロールの削除は、"\ 134 | f"元のコマンドを実行した`{command_user_name}`さんのみに許可されています。" 135 | await self.send_error_msg_via_dm(member, reactioned_msg, text) 136 | return 137 | await self.send_role_destroying_msg( 138 | payload.member.mention, 139 | role.name, 140 | channel 141 | ) 142 | await role.delete() 143 | except Exception as e: 144 | print(e) 145 | print(type(e)) 146 | 147 | def role_name_exists(self, role_name, roles): 148 | for role in roles: 149 | if role.name == role_name: 150 | return True 151 | return False 152 | 153 | def make_role_name_error_message(self, role_name): 154 | embed = discord.Embed( 155 | description=f"`{role_name}`という名前のロールは既に存在します。", 156 | color=discord.Color.red() 157 | ) 158 | return embed 159 | 160 | def make_role_name(self, role_name_tail, roles): 161 | role_name = None 162 | if role_name_tail is None: 163 | role_name = Role.make_role_name_with_index(roles) 164 | else: 165 | role_name = f"{self.ROLE_NAME_HEAD}{role_name_tail}" 166 | return role_name 167 | 168 | @commands.command() 169 | async def role(self, ctx, role_name_tail=None): 170 | await self.bot.wait_until_ready() 171 | roles = ctx.guild.roles 172 | role_name = self.make_role_name(role_name_tail, roles) 173 | if role_name_tail and self.role_name_exists(role_name, roles): 174 | embed = self.make_role_name_error_message(role_name) 175 | await ctx.send(embed=embed) 176 | return 177 | embed = discord.Embed( 178 | description=self.make_text(ctx.author.mention), 179 | color=discord.Color.blue() 180 | ) 181 | await ctx.guild.create_role(name=role_name) 182 | embed.add_field(name="New Role", value=role_name) 183 | embed.add_field(name="Called by", value=ctx.author.name) 184 | msg = await ctx.send(embed=embed) 185 | await msg.add_reaction(self.REGISTER_EMOJI) 186 | await msg.add_reaction(self.REMOVER_EMOJI) 187 | 188 | @commands.command() 189 | async def rm(self, ctx, role_name_tail): 190 | embed = discord.Embed() 191 | role_name = f"{self.ROLE_NAME_HEAD}{role_name_tail}" 192 | role = discord.utils.get(ctx.guild.roles, name=role_name) 193 | if role is None: 194 | embed.color = discord.Color.red() 195 | embed.description = f"{ctx.author.mention}"\ 196 | f" `{role_name}`というロールは存在しません。" 197 | await ctx.send(embed=embed) 198 | else: 199 | embed.color = discord.Color.green() 200 | embed.description = f"{ctx.author.mention}さんが"\ 201 | f"`{role.name}`ロールを削除しました。" 202 | await ctx.send(embed=embed) 203 | await role.delete() 204 | 205 | 206 | def setup(bot): 207 | bot.add_cog(Role(bot)) 208 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | TOKEN = os.environ['TOKEN'] 4 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for grouping-manager-bot on 2022-11-25T10:36:47+09:00 2 | 3 | app = "grouping-manager-bot" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [env] 9 | 10 | -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | import config 4 | 5 | 6 | def create_intents() -> discord.Intents: 7 | intents = discord.Intents().all() 8 | return intents 9 | 10 | 11 | class GroupingManagerBot(commands.Bot): 12 | async def on_ready(self): 13 | self.load_extension("cogs.change_bot_nickname") 14 | self.load_extension("cogs.match") 15 | self.load_extension("cogs.role") 16 | self.load_extension("cogs.help") 17 | print("on_ready") 18 | 19 | 20 | def main(): 21 | intents = create_intents() 22 | bot = GroupingManagerBot( 23 | command_prefix="+", 24 | intents=intents, 25 | help_command=None 26 | ) 27 | bot.run(config.TOKEN) 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.7.4.post0 2 | async-timeout==3.0.1 3 | attrs==21.2.0 4 | chardet==4.0.0 5 | discord.py==1.7.3 6 | idna==3.2 7 | multidict==5.1.0 8 | yarl==1.6.3 9 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.6 2 | --------------------------------------------------------------------------------