├── Procfile ├── requirements.txt ├── .gitattributes ├── docker-compose.yml ├── Dockerfile ├── pm2.json ├── utils ├── http.py ├── dataIO.py ├── argparser.py ├── cache.py ├── data.py ├── default.py └── permissions.py ├── config.json.example ├── README.md ├── index.py ├── .gitignore └── cogs ├── info.py ├── events.py ├── 基本功能.py ├── encryption.py ├── admin.py ├── fun.py └── mod.py /Procfile: -------------------------------------------------------------------------------- 1 | worker: python index.py 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | asyncio 3 | discord.py==1.6.0 4 | psutil 5 | pytz 6 | timeago==1.0.13 7 | yarl<1.2 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | Dockerfile text eol=lf 3 | *.py eol=lf 4 | *.sh text eol=lf 5 | .bat text eol=crlf 6 | .ps1 text eol=crlf -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | bot: 5 | container_name: "discord_bot.py" 6 | build: . 7 | volumes: 8 | - ./config.json:/discord_bot/config.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | MAINTAINER Kantai 3 | 4 | WORKDIR /discord_bot 5 | COPY . /discord_bot 6 | 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | CMD python index.py -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "init.engineer-Discord-Python-Bot", 5 | "script": "index.py", 6 | "interpreter": "python3.7", 7 | "interpreter_args": "-u" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /utils/http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | 4 | from utils import cache 5 | 6 | 7 | @cache.async_cache() 8 | async def query(url, method="get", res_method="text", *args, **kwargs): 9 | async with aiohttp.ClientSession(loop=asyncio.get_event_loop()) as session: 10 | async with getattr(session, method.lower())(url, *args, **kwargs) as res: 11 | return await getattr(res, res_method)() 12 | 13 | 14 | async def get(url, *args, **kwargs): 15 | return await query(url, "get", *args, **kwargs) 16 | 17 | 18 | async def post(url, *args, **kwargs): 19 | return await query(url, "post", *args, **kwargs) 20 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "token": "YOUR_DISCORD_BOT_TOKEN", 3 | "join_message": "如果有新人加入時的資訊 🌟", 4 | "owners": [ 5 | 11111111111111111 6 | ], 7 | "prefix": [ 8 | "!" 9 | ], 10 | "playing": "機器人正在玩的遊戲名稱", 11 | "playing_type": "正在玩|觀看中|收聽中", 12 | "status_type": "線上|閒置|請勿打擾", 13 | "version": "1.0.0", 14 | "reaction_roles": [ 15 | { 16 | "message": 11111, 17 | "roles": [ 18 | { 19 | "sticker": "\uD83C\uDDEE", 20 | "role": 111111 21 | } 22 | ] 23 | } 24 | ], 25 | "auto_publish_channels": [ 26 | 11111111111111111 27 | ] 28 | } -------------------------------------------------------------------------------- /utils/dataIO.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def change_value(file: str, value: str, changeto: str): 5 | try: 6 | with open(file, "r") as jsonFile: 7 | data = json.load(jsonFile) 8 | except FileNotFoundError: 9 | raise FileNotFoundError("您嘗試讀取的文件不存在 ...") 10 | 11 | data[value] = changeto 12 | with open(file, "w") as jsonFile: 13 | json.dump(data, jsonFile, indent=2) 14 | 15 | 16 | def append_value(file: str, value: str, addition: str): 17 | try: 18 | with open(file, "r") as jsonFile: 19 | data = json.load(jsonFile) 20 | except FileNotFoundError: 21 | raise FileNotFoundError("您嘗試讀取的文件不存在 ...") 22 | 23 | data[value].append(addition) 24 | with open(file, "w") as jsonFile: 25 | json.dump(data, jsonFile, indent=2) 26 | -------------------------------------------------------------------------------- /utils/argparser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import shlex 3 | 4 | 5 | class DefaultArguments(argparse.ArgumentParser): 6 | def error(self, message): 7 | raise RuntimeError(message) 8 | 9 | 10 | class Arguments: 11 | def __init__(self, posix: bool = False, allow_abbrev: bool = False, **kwargs): 12 | self.parser = DefaultArguments(allow_abbrev=allow_abbrev, add_help=False, **kwargs) 13 | self.posix = posix 14 | 15 | def add_argument(self, *inputs, **kwargs): 16 | """ Shortcut to argparse.add_argument """ 17 | self.parser.add_argument(*inputs, **kwargs) 18 | 19 | def parse_args(self, text): 20 | """ Shortcut to argparse.parse_args with shlex implemented """ 21 | try: 22 | args = self.parser.parse_args( 23 | shlex.split(text if text else "", posix=self.posix) 24 | ) 25 | except Exception as e: 26 | return (f"ArgumentError: {e}", False) 27 | 28 | return (args, True) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # INIT.ENGINEER DISCORD BOT 2 | 3 | 本機器人是以 [discord_bot.py](https://github.com/AlexFlipnote/discord_bot.py) 作為模板上的延伸。 4 | 5 | # 如何使用它? 6 | 7 | 1. 在[此處](https://discordapp.com/developers/applications/me)建立機器人,並取得機器人的 Token。 8 | 2. 新增一個environment variable,key為"TOKEN",value為上方取得的token 9 | 3. 新增一個environment variable,key為"CONFIG",value你自己的config,格式是json,可以參考config.json.example填寫 10 | 4. 要安裝應用程式所需要的套件,請執行 `pip install -r requirements.txt`,如果上面沒辦法正常執行的話,請執行 `python -m pip install -r requirements.txt`,要注意的是這可能需要使用 `Administrator/sudo` 來執行。 11 | 5. 透過 `cmd/terminal` 終端機來啟動應用程式,你必須移動到該應用程式的資料夾,並且輸入 `python index.py` 來啟動。 12 | 13 | ## CONFIG內容說明 14 | 15 | * ``prefix`` : 指令前綴 16 | * ``owners`` : 陣列,內容為開發者的ID 17 | * ``botserver`` : discord伺服器邀請連結,使用指令botserver會傳給使用者 18 | * ``reaction_roles`` : 陣列,"加入反應以獲得相對應身分組功能"的詳細資料 19 | * ``message`` : 希望使用者對哪則訊息加入反應的ID 20 | * ``roles`` : 陣列,內容為加入反應的貼圖名稱以及相對應的身分組 21 | * ``sticker`` : 加入反應的貼圖**名稱**,unicode emoji的名字即為emoji本身,伺服器的emoji則為當時設定的名稱 22 | * ``role`` : 相對應的身分組的ID 23 | * ``auto_publish_channels`` : 陣列,需要自動發布的頻道ID -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from utils import default 4 | from utils.data import ( 5 | Bot, 6 | HelpFormat, 7 | ) 8 | from multiprocessing import Process 9 | import discord 10 | 11 | 12 | def main(): 13 | config = default.get_from_env("CONFIG") 14 | if config is None: 15 | exit(3) 16 | 17 | print("機器人登入中 ...") 18 | intents = discord.Intents.default() 19 | intents.members = True 20 | bot = Bot( 21 | command_prefix=config.prefix, 22 | prefix=config.prefix, 23 | command_attrs=dict(hidden=True), 24 | help_command=HelpFormat(), 25 | intents=intents 26 | ) 27 | 28 | for file in os.listdir("cogs"): 29 | if file.endswith(".py"): 30 | name = file[:-3] 31 | bot.load_extension(f"cogs.{name}") 32 | 33 | token = os.environ.get("TOKEN") 34 | if token is None: 35 | exit(2) 36 | bot.run(token) 37 | 38 | 39 | if __name__ == '__main__': 40 | while True: 41 | p = Process(target=main) 42 | p.start() 43 | p.join() 44 | if p.exitcode == 100: 45 | print('準備重啟 ...') 46 | continue 47 | else: 48 | exit(p.exitcode) 49 | -------------------------------------------------------------------------------- /utils/cache.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | def cache(maxsize=128): 5 | cache = {} 6 | 7 | def decorator(func): 8 | @wraps(func) 9 | def inner(*args, no_cache=False, **kwargs): 10 | if no_cache: 11 | return func(*args, **kwargs) 12 | 13 | key_base = "_".join(str(x) for x in args) 14 | key_end = "_".join(f"{k}:{v}" for k, v in kwargs.items()) 15 | key = f"{key_base}-{key_end}" 16 | 17 | if key in cache: 18 | return cache[key] 19 | 20 | res = func(*args, **kwargs) 21 | 22 | if len(cache) > maxsize: 23 | del cache[list(cache.keys())[0]] 24 | cache[key] = res 25 | 26 | return res 27 | return inner 28 | return decorator 29 | 30 | 31 | def async_cache(maxsize=128): 32 | cache = {} 33 | 34 | def decorator(func): 35 | @wraps(func) 36 | async def inner(*args, no_cache=False, **kwargs): 37 | if no_cache: 38 | return await func(*args, **kwargs) 39 | 40 | key_base = "_".join(str(x) for x in args) 41 | key_end = "_".join(f"{k}:{v}" for k, v in kwargs.items()) 42 | key = f"{key_base}-{key_end}" 43 | 44 | if key in cache: 45 | return cache[key] 46 | 47 | res = await func(*args, **kwargs) 48 | 49 | if len(cache) > maxsize: 50 | del cache[list(cache.keys())[0]] 51 | cache[key] = res 52 | 53 | return res 54 | return inner 55 | return decorator 56 | -------------------------------------------------------------------------------- /utils/data.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from utils import permissions 4 | from discord.ext.commands import AutoShardedBot, DefaultHelpCommand 5 | 6 | 7 | class Bot(AutoShardedBot): 8 | def __init__(self, *args, prefix=None, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | 11 | async def on_message(self, msg): 12 | if not self.is_ready() or msg.author.bot or not permissions.can_send(msg): 13 | return 14 | 15 | await self.process_commands(msg) 16 | 17 | 18 | class HelpFormat(DefaultHelpCommand): 19 | def get_destination(self, no_pm: bool = False): 20 | if no_pm: 21 | return self.context.channel 22 | else: 23 | return self.context.author 24 | 25 | async def send_error_message(self, error): 26 | destination = self.get_destination(no_pm=True) 27 | await destination.send(error) 28 | 29 | async def send_command_help(self, command): 30 | self.add_command_formatting(command) 31 | self.paginator.close_page() 32 | await self.send_pages(no_pm=True) 33 | 34 | async def send_pages(self, no_pm: bool = False): 35 | try: 36 | if permissions.can_react(self.context): 37 | await self.context.message.add_reaction(chr(0x2709)) 38 | except discord.Forbidden: 39 | pass 40 | 41 | try: 42 | destination = self.get_destination(no_pm=no_pm) 43 | for page in self.paginator.pages: 44 | await destination.send(page) 45 | except discord.Forbidden: 46 | destination = self.get_destination(no_pm=True) 47 | await destination.send("Couldn't send help to you due to blocked DMs...") 48 | -------------------------------------------------------------------------------- /utils/default.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import discord 4 | import traceback 5 | import timeago as timesince 6 | 7 | from collections import namedtuple 8 | from io import BytesIO 9 | import os 10 | 11 | 12 | def get(file): 13 | try: 14 | with open(file, encoding='utf8') as data: 15 | return json.load(data, object_hook=lambda d: namedtuple('X', d.keys())(*d.values())) 16 | except AttributeError: 17 | raise AttributeError("命令執行失敗。") 18 | except FileNotFoundError: 19 | raise FileNotFoundError("找不到 JSON 檔案。") 20 | 21 | def get_from_env(key: str): 22 | val = os.environ.get(key) 23 | if val is None: 24 | return None 25 | try: 26 | return json.loads(val, object_hook=lambda d: namedtuple('X', d.keys())(*d.values())) 27 | except AttributeError: 28 | raise AttributeError("命令執行失敗。") 29 | 30 | def traceback_maker(err, advance: bool = True): 31 | _traceback = ''.join(traceback.format_tb(err.__traceback__)) 32 | error = ('```py\n{1}{0}: {2}\n```').format(type(err).__name__, _traceback, err) 33 | return error if advance else f"{type(err).__name__}: {err}" 34 | 35 | 36 | def timetext(name): 37 | return f"{name}_{int(time.time())}.txt" 38 | 39 | 40 | def timeago(target): 41 | return timesince.format(target) 42 | 43 | 44 | def date(target, clock=True): 45 | if clock is False: 46 | return target.strftime("%d %B %Y") 47 | return target.strftime("%d %B %Y, %H:%M") 48 | 49 | 50 | def responsible(target, reason): 51 | responsible = f"[ {target} ]" 52 | if reason is None: 53 | return f"{responsible} 沒有給出原因 ..." 54 | return f"{responsible} {reason}" 55 | 56 | 57 | def actionmessage(case, mass=False): 58 | output = f"**{case}** the user" 59 | 60 | if mass is True: 61 | output = f"**{case}** the IDs/Users" 62 | 63 | return f"✅ 成功地 {output}" 64 | 65 | 66 | async def prettyResults(ctx, filename: str = "Results", resultmsg: str = "這是結果: ", loop=None): 67 | if not loop: 68 | return await ctx.send("結果為空 ...") 69 | 70 | pretty = "\r\n".join([f"[{str(num).zfill(2)}] {data}" for num, data in enumerate(loop, start=1)]) 71 | 72 | if len(loop) < 15: 73 | return await ctx.send(f"{resultmsg}```ini\n{pretty}```") 74 | 75 | data = BytesIO(pretty.encode('utf-8')) 76 | await ctx.send( 77 | content=resultmsg, 78 | file=discord.File(data, filename=timetext(filename.title())) 79 | ) 80 | -------------------------------------------------------------------------------- /utils/permissions.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from utils import default 4 | from discord.ext import commands 5 | 6 | owners = default.get_from_env("CONFIG").owners 7 | 8 | 9 | def is_owner(ctx): 10 | return ctx.author.id in owners 11 | 12 | 13 | async def check_permissions(ctx, perms, *, check=all): 14 | if ctx.author.id in owners: 15 | return True 16 | 17 | resolved = ctx.channel.permissions_for(ctx.author) 18 | return check(getattr(resolved, name, None) == value for name, value in perms.items()) 19 | 20 | 21 | def has_permissions(*, check=all, **perms): 22 | async def pred(ctx): 23 | return await check_permissions(ctx, perms, check=check) 24 | return commands.check(pred) 25 | 26 | 27 | async def check_priv(ctx, member): 28 | try: 29 | # Self checks 30 | if member == ctx.author: 31 | return await ctx.send(f"You can't {ctx.command.name} yourself") 32 | if member.id == ctx.bot.user.id: 33 | return await ctx.send("So that's what you think of me huh..? sad ;-;") 34 | 35 | # Check if user bypasses 36 | if ctx.author.id == ctx.guild.owner.id: 37 | return False 38 | 39 | # Now permission check 40 | if member.id in owners: 41 | if ctx.author.id not in owners: 42 | return await ctx.send(f"I can't {ctx.command.name} my creator ;-;") 43 | else: 44 | pass 45 | if member.id == ctx.guild.owner.id: 46 | return await ctx.send(f"You can't {ctx.command.name} the owner, lol") 47 | if ctx.author.top_role == member.top_role: 48 | return await ctx.send(f"You can't {ctx.command.name} someone who has the same permissions as you...") 49 | if ctx.author.top_role < member.top_role: 50 | return await ctx.send(f"Nope, you can't {ctx.command.name} someone higher than yourself.") 51 | except Exception: 52 | pass 53 | 54 | 55 | def can_send(ctx): 56 | return isinstance(ctx.channel, discord.DMChannel) or ctx.channel.permissions_for(ctx.guild.me).send_messages 57 | 58 | 59 | def can_embed(ctx): 60 | return isinstance(ctx.channel, discord.DMChannel) or ctx.channel.permissions_for(ctx.guild.me).embed_links 61 | 62 | 63 | def can_upload(ctx): 64 | return isinstance(ctx.channel, discord.DMChannel) or ctx.channel.permissions_for(ctx.guild.me).attach_files 65 | 66 | 67 | def can_react(ctx): 68 | return isinstance(ctx.channel, discord.DMChannel) or ctx.channel.permissions_for(ctx.guild.me).add_reactions 69 | 70 | 71 | def is_nsfw(ctx): 72 | return isinstance(ctx.channel, discord.DMChannel) or ctx.channel.is_nsfw() 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | config.json 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 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 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | -------------------------------------------------------------------------------- /cogs/info.py: -------------------------------------------------------------------------------- 1 | import time 2 | import discord 3 | import psutil 4 | import os 5 | 6 | from datetime import datetime 7 | from discord.ext import commands 8 | from utils import default 9 | 10 | 11 | class Information(commands.Cog, name="資訊"): 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self.config = default.get_from_env("CONFIG") 15 | self.process = psutil.Process(os.getpid()) 16 | 17 | @commands.command() 18 | async def ping(self, ctx): 19 | """ Pong! """ 20 | before = time.monotonic() 21 | before_ws = int(round(self.bot.latency * 1000, 1)) 22 | message = await ctx.send("🏓 Pong") 23 | ping = (time.monotonic() - before) * 1000 24 | await message.edit(content=f"🏓 WS: {before_ws}ms | REST: {int(ping)}ms") 25 | 26 | @commands.command(name="邀請", aliases=['invite', 'joinme', 'join', 'botinvite']) 27 | async def invite(self, ctx): 28 | """ 邀請我到你的伺服器 """ 29 | await ctx.send(f"**{ctx.author.name}**, use this URL to invite me\n<{discord.utils.oauth_url(self.bot.user.id)}>") 30 | 31 | @commands.command(name="程式碼", aliases=['source']) 32 | async def source(self, ctx): 33 | """ Check out my source code <3 """ 34 | # Do not remove this command, this has to stay due to the GitHub LICENSE. 35 | # TL:DR, you have to disclose source according to MIT. 36 | # Reference: https://github.com/AlexFlipnote/discord_bot.py/blob/master/LICENSE 37 | await ctx.send(f"**{ctx.bot.user}** is powered by this source code:\nhttps://github.com/AlexFlipnote/discord_bot.py") 38 | 39 | @commands.command(name="伺服器", aliases=['supportserver', 'feedbackserver']) 40 | async def botserver(self, ctx): 41 | """ 獲取伺服器的邀請連結! """ 42 | if isinstance(ctx.channel, discord.DMChannel) or ctx.guild.id != 86484642730885120: 43 | return await ctx.send(f"**Here you go {ctx.author.name} 🍻\n<{self.config.botserver}>**") 44 | 45 | await ctx.send(f"**{ctx.author.name}** this is my home you know :3") 46 | 47 | @commands.command(name="關於", aliases=['about', 'info', 'stats', 'status']) 48 | async def about(self, ctx): 49 | """ 關於機器人 """ 50 | ram_usage = self.process.memory_full_info().rss / 1024 ** 2 51 | avg_members = round(len(self.bot.users) / len(self.bot.guilds)) 52 | 53 | embed_colour = discord.Embed.Empty 54 | if hasattr(ctx, 'guild') and ctx.guild is not None: 55 | embed_colour = ctx.me.top_role.colour 56 | 57 | embed = discord.Embed(colour=embed_colour) 58 | embed.set_thumbnail(url=ctx.bot.user.avatar_url) 59 | embed.add_field(name="Last boot", value=default.timeago(datetime.now() - self.bot.uptime), inline=True) 60 | embed.add_field( 61 | name=f"Developer{'' if len(self.config.owners) == 1 else 's'}", 62 | value=', '.join([str(self.bot.get_user(x)) for x in self.config.owners]), 63 | inline=True) 64 | embed.add_field(name="Library", value="discord.py", inline=True) 65 | embed.add_field(name="Servers", value=f"{len(ctx.bot.guilds)} ( avg: {avg_members} users/server )", inline=True) 66 | embed.add_field(name="Commands loaded", value=str(len([x.name for x in self.bot.commands])), inline=True) 67 | embed.add_field(name="RAM", value=f"{ram_usage:.2f} MB", inline=True) 68 | 69 | await ctx.send(content=f"ℹ About **{ctx.bot.user}** | **{self.config.version}**", embed=embed) 70 | 71 | 72 | def setup(bot): 73 | bot.add_cog(Information(bot)) 74 | -------------------------------------------------------------------------------- /cogs/events.py: -------------------------------------------------------------------------------- 1 | from discord import ( 2 | Forbidden, 3 | HTTPException, 4 | Message, 5 | Permissions, 6 | RawReactionActionEvent, 7 | ) 8 | import discord 9 | import psutil 10 | import os 11 | 12 | import discord.guild 13 | 14 | from datetime import datetime 15 | from discord.ext import commands 16 | from discord.ext.commands import ( 17 | errors, 18 | has_permissions, 19 | ) 20 | from utils import default 21 | from utils.data import Bot 22 | 23 | 24 | class Events(commands.Cog): 25 | def __init__(self, bot: Bot): 26 | self.bot = bot 27 | self.config = default.get_from_env("CONFIG") 28 | self.process = psutil.Process(os.getpid()) 29 | 30 | @commands.Cog.listener() 31 | async def on_command_error(self, ctx, err): 32 | if isinstance(err, errors.MissingRequiredArgument) or isinstance(err, errors.BadArgument): 33 | helper = str(ctx.invoked_subcommand) if ctx.invoked_subcommand else str(ctx.command) 34 | await ctx.send_help(helper) 35 | 36 | elif isinstance(err, errors.CommandInvokeError): 37 | error = default.traceback_maker(err.original) 38 | 39 | if "2000 or fewer" in str(err) and len(ctx.message.clean_content) > 1900: 40 | return await ctx.send( 41 | f"You attempted to make the command display more than 2,000 characters...\n" 42 | f"Both error and command will be ignored." 43 | ) 44 | 45 | await ctx.send(f"There was an error processing the command ;-;\n{error}") 46 | 47 | elif isinstance(err, errors.CheckFailure): 48 | pass 49 | 50 | elif isinstance(err, errors.MaxConcurrencyReached): 51 | await ctx.send(f"You've reached max capacity of command usage at once, please finish the previous one...") 52 | 53 | elif isinstance(err, errors.CommandOnCooldown): 54 | await ctx.send(f"This command is on cooldown... try again in {err.retry_after:.2f} seconds.") 55 | 56 | elif isinstance(err, errors.CommandNotFound): 57 | pass 58 | 59 | @commands.Cog.listener() 60 | async def on_guild_join(self, guild): 61 | if not self.config.join_message: 62 | return 63 | 64 | try: 65 | to_send = \ 66 | sorted([chan for chan in guild.channels if chan.permissions_for(guild.me).send_messages and isinstance(chan, discord.TextChannel)], 67 | key=lambda x: x.position)[0] 68 | except IndexError: 69 | pass 70 | else: 71 | await to_send.send(self.config.join_message) 72 | 73 | @commands.Cog.listener() 74 | @has_permissions(manage_messages=True) 75 | async def on_message(self, msg: Message): 76 | if msg.channel.id in self.config.auto_publish_channels: 77 | try: 78 | await msg.publish() 79 | except Forbidden as e: 80 | print(e.text) 81 | print("請檢察頻道設定") 82 | except HTTPException as e: 83 | print(e.text) 84 | 85 | @commands.Cog.listener() 86 | async def on_command(self, ctx): 87 | try: 88 | print(f"{ctx.guild.name} > {ctx.author} > {ctx.message.clean_content}") 89 | except AttributeError: 90 | print(f"私人訊息 > {ctx.author} > {ctx.message.clean_content}") 91 | 92 | @commands.Cog.listener() 93 | async def on_ready(self): 94 | """ 機器人啟動完成後會執行的功能。 """ 95 | if not hasattr(self.bot, 'uptime'): 96 | self.bot.uptime = datetime.utcnow() 97 | 98 | # Indicate that the bot has successfully booted up 99 | print(f'準備就緒: {self.bot.user} | 服務數量: {len(self.bot.guilds)}') 100 | 101 | # Check if user desires to have something other than online 102 | if self.config.status_type == "閒置": 103 | status_type = discord.Status.idle 104 | elif self.config.status_type == "請勿打擾": 105 | status_type = discord.Status.dnd 106 | else: 107 | status_type = discord.Status.online 108 | 109 | # Check if user desires to have a different type of playing status 110 | if self.config.playing_type == "收聽中": 111 | playing_type = 2 112 | elif self.config.playing_type == "觀看中": 113 | playing_type = 3 114 | else: 115 | playing_type = 0 116 | 117 | await self.bot.change_presence( 118 | activity=discord.Activity(type=playing_type, name=self.config.playing), 119 | status=status_type 120 | ) 121 | 122 | @commands.Cog.listener() 123 | async def on_raw_reaction_add(self, payload: RawReactionActionEvent): 124 | reaction_roles = self.config.reaction_roles 125 | reaction_role = discord.utils.find(lambda r: r.message == payload.message_id, reaction_roles) 126 | if reaction_role is not None: 127 | role_data = discord.utils.find(lambda r: r.sticker == payload.emoji.name, reaction_role.roles) 128 | guild = self.bot.get_guild(payload.guild_id) 129 | if role_data is None: 130 | print(f"{payload.emoji}沒有對應的身分組\nemoji name: {payload.emoji.name}\n") 131 | 132 | channel = guild.get_channel(payload.channel_id) 133 | message = await channel.fetch_message(payload.message_id) 134 | await message.clear_reaction(payload.emoji) 135 | return 136 | else: 137 | role = guild.get_role(role_data.role) 138 | await payload.member.add_roles(role) 139 | 140 | @commands.Cog.listener() 141 | async def on_raw_reaction_remove(self, payload: RawReactionActionEvent): 142 | reaction_roles = self.config.reaction_roles 143 | reaction_role = discord.utils.find(lambda r: r.message == payload.message_id, reaction_roles) 144 | if reaction_role is not None: 145 | role_data = discord.utils.find(lambda r: r.sticker == payload.emoji.name, reaction_role.roles) 146 | if role_data is None: 147 | print(f"{payload.emoji}沒有對應的身分組\nemoji name: {payload.emoji.name}\n") 148 | return 149 | else: 150 | guild = self.bot.get_guild(payload.guild_id) 151 | role = guild.get_role(role_data.role) 152 | member = guild.get_member(payload.user_id) 153 | await member.remove_roles(role) 154 | 155 | 156 | def setup(bot): 157 | bot.add_cog(Events(bot)) 158 | -------------------------------------------------------------------------------- /cogs/基本功能.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from io import BytesIO 4 | from utils import default 5 | from discord.ext import commands 6 | 7 | 8 | class 基本功能(commands.Cog, name="基本功能"): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | self.config = default.get_from_env("CONFIG") 12 | 13 | @commands.command(name="我的大頭貼", aliases=['avatar']) 14 | @commands.guild_only() 15 | async def avatar(self, ctx, *, user: discord.Member = None): 16 | """ 讓機器人告訴你,你的大頭貼是什麼? """ 17 | user = user or ctx.author 18 | await ctx.send(f"嗨嗨汪、**{user.name}**\r\n你的大頭貼是這張哦 ٩(。・ω・。)و!\r\n{user.avatar_url_as(size=1024)}") 19 | 20 | @commands.command(name="角色列表報告") 21 | @commands.guild_only() 22 | async def 角色列表報告(self, ctx): 23 | """ 獲得頻道當中所有角色詳細資訊的報告。 """ 24 | allroles = "```\r\nNUM | USERS | ID | NAME\r\n----+-------+--------------------+------------------------\r\n" 25 | for num, role in enumerate(sorted(ctx.guild.roles, reverse=True), start=1): 26 | if role.name != '@everyone': 27 | allroles += f"{str(num).rjust(3)} | {str(len(role.members)).rjust(5)} | {role.id} | {role.name}\r\n" 28 | else: 29 | allroles += f"----+-------+--------------------+------------------------\r\n{str(num).rjust(3)} | {str(len(role.members)).rjust(5)} | {role.id} | 全部```" 30 | await ctx.send(allroles) 31 | # data = BytesIO(allroles.encode('utf-8')) 32 | # await ctx.send(content=f"**{ctx.guild.name}**頻道內所有角色的詳細資訊:", file=discord.File(data, filename=f"{default.timetext('Roles')}")) 33 | 34 | @commands.command(name="角色列表資料", aliases=["roles"]) 35 | @commands.guild_only() 36 | async def roles(self, ctx): 37 | """ 獲得頻道當中所有角色詳細資訊的資料。 """ 38 | all_roles = "" 39 | 40 | for num, role in enumerate(sorted(ctx.guild.roles, reverse=True), start=1): 41 | all_roles += f"[{str(num).zfill(2)}] {role.id}\t{role.name}\t[ Users: {len(role.members)} ]\r\n" 42 | 43 | data = BytesIO(all_roles.encode('utf-8')) 44 | await ctx.send(content=f"Roles in **{ctx.guild.name}**", file=discord.File(data, filename=f"{default.timetext('Roles')}")) 45 | 46 | @commands.command(name="我什麼時候加入的", aliases=["joindat", "joined"]) 47 | @commands.guild_only() 48 | async def join_date(self, ctx, *, user: discord.Member = None): 49 | """ 讓機器人告訴你,你什麼時候加入頻道的? """ 50 | user = user or ctx.author 51 | 52 | embed = discord.Embed(colour=user.top_role.colour.value) 53 | embed.set_thumbnail(url=user.avatar_url) 54 | embed.description = f'嗨嗨汪、**{user}**\n你是從 {default.date(user.joined_at)} 開始加入 **{ctx.guild.name}** 這頻道的唷 ٩(。・ω・。)و!' 55 | await ctx.send(embed=embed) 56 | 57 | @commands.command(name="確認使用者上線狀態", aliases=["mods"]) 58 | @commands.guild_only() 59 | async def mods(self, ctx): 60 | """ 檢查當前伺服器當中,有哪些管理員還在線上。 """ 61 | message = "" 62 | online, idle, dnd, offline = [], [], [], [] 63 | 64 | for user in ctx.guild.members: 65 | if ctx.channel.permissions_for(user).kick_members or \ 66 | ctx.channel.permissions_for(user).ban_members: 67 | if not user.bot and user.status is discord.Status.online: 68 | online.append(f"**{user}**") 69 | if not user.bot and user.status is discord.Status.idle: 70 | idle.append(f"**{user}**") 71 | if not user.bot and user.status is discord.Status.dnd: 72 | dnd.append(f"**{user}**") 73 | if not user.bot and user.status is discord.Status.offline: 74 | offline.append(f"**{user}**") 75 | 76 | if online: 77 | message += f"🟢 {', '.join(online)}\n" 78 | if idle: 79 | message += f"🟡 {', '.join(idle)}\n" 80 | if dnd: 81 | message += f"🔴 {', '.join(dnd)}\n" 82 | if offline: 83 | message += f"⚫ {', '.join(offline)}\n" 84 | 85 | await ctx.send(f"**{ctx.guild.name}**\n{message}") 86 | 87 | @commands.group(name="查看頻道詳細資訊") 88 | @commands.guild_only() 89 | async def server(self, ctx): 90 | """ 查看頻道的資訊。 """ 91 | if ctx.invoked_subcommand is None: 92 | findbots = sum(1 for member in ctx.guild.members if member.bot) 93 | 94 | embed = discord.Embed() 95 | 96 | if ctx.guild.icon: 97 | embed.set_thumbnail(url=ctx.guild.icon_url) 98 | if ctx.guild.banner: 99 | embed.set_image(url=ctx.guild.banner_url_as(format="png")) 100 | 101 | embed.add_field(name="頻道名稱", value=ctx.guild.name, inline=True) 102 | embed.add_field(name="頻道 ID", value=ctx.guild.id, inline=True) 103 | embed.add_field(name="會員人數", value=ctx.guild.member_count, inline=True) 104 | embed.add_field(name="機器人數量", value=str(findbots), inline=True) 105 | embed.add_field(name="管理員", value=ctx.guild.owner, inline=True) 106 | embed.add_field(name="位址", value=ctx.guild.region, inline=True) 107 | embed.add_field(name="建立於", value=default.date(ctx.guild.created_at), inline=True) 108 | await ctx.send(content=f"ℹ 這是 **{ctx.guild.name}** 頻道的基本資訊哦汪嗚 ٩(。・ω・。)و", embed=embed) 109 | 110 | @server.command(name="大頭貼", aliases=["icon"]) 111 | async def server_avatar(self, ctx): 112 | """ 取得當前伺服器的大頭貼。 """ 113 | if not ctx.guild.icon: 114 | return await ctx.send(f"嗚 ... **{ctx.guild.name}**好像還沒放大頭貼呢汪 _(:3 」∠ )_") 115 | await ctx.send("{ctx.guild.icon_url_as(size=1024)}") 116 | 117 | @server.command(name="橫幅", aliases=["banner"]) 118 | async def server_banner(self, ctx): 119 | """ 取得當前伺服器的橫幅圖片。 """ 120 | if not ctx.guild.banner: 121 | return await ctx.send(f"嗚 ... **{ctx.guild.name}**好像還沒放橫幅圖片呢汪 _(:3 」∠ )_") 122 | await ctx.send("{ctx.guild.banner_url_as(format='png')}") 123 | 124 | @commands.command(name="我想看看我自己", aliases=["user"]) 125 | @commands.guild_only() 126 | async def user_info(self, ctx, *, user: discord.Member = None): 127 | """ 取得使用者自己的資訊 """ 128 | user = user or ctx.author 129 | 130 | show_roles = ', '.join( 131 | [f"<@&{x.id}>" for x in sorted(user.roles, key=lambda x: x.position, reverse=True) if x.id != ctx.guild.default_role.id] 132 | ) if len(user.roles) > 1 else '空空如也' 133 | 134 | embed = discord.Embed(colour=user.top_role.colour.value) 135 | embed.set_thumbnail(url=user.avatar_url) 136 | 137 | embed.add_field(name="名字", value=user, inline=True) 138 | embed.add_field(name="暱稱", value=user.nick if hasattr(user, "nick") else "無", inline=True) 139 | embed.add_field(name="建立帳號於", value=default.date(user.created_at), inline=True) 140 | embed.add_field(name="加入伺服器於", value=default.date(user.joined_at), inline=True) 141 | 142 | embed.add_field( 143 | name="身份組", 144 | value=show_roles, 145 | inline=False 146 | ) 147 | 148 | await ctx.send(content=f"ℹ 汪嗚!窩找到了<@{user.id}>的名片哦 ٩(。・ω・。)و", embed=embed) 149 | 150 | 151 | def setup(bot): 152 | bot.add_cog(基本功能(bot)) 153 | -------------------------------------------------------------------------------- /cogs/encryption.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import binascii 3 | import codecs 4 | import discord 5 | 6 | from io import BytesIO 7 | from discord.ext import commands 8 | from discord.ext.commands import clean_content 9 | from discord.ext.commands.errors import BadArgument 10 | from utils import default, http 11 | 12 | 13 | async def detect_file(ctx): 14 | print(ctx.message.attachments) 15 | if ctx.message.attachments: 16 | file = ctx.message.attachments[0].url 17 | 18 | if not file.endswith(".txt"): 19 | raise BadArgument("只接受txt檔") 20 | else: 21 | raise 22 | try: 23 | content = await http.get(file, no_cache=True) 24 | except Exception: 25 | raise BadArgument("無效的txt檔") 26 | 27 | if not content: 28 | raise BadArgument("你提供的檔案是空的") 29 | return content 30 | 31 | 32 | async def encrypt_out(ctx, convert, _input): 33 | if not _input: 34 | return await ctx.send(f"Aren't you going to give me anything to encode/decode **{ctx.author.name}**") 35 | 36 | async with ctx.channel.typing(): 37 | if len(_input) > 1900: 38 | try: 39 | data = BytesIO(_input.encode('utf-8')) 40 | except AttributeError: 41 | data = BytesIO(_input) 42 | 43 | try: 44 | return await ctx.send( 45 | content=f"📑 **{convert}**", 46 | file=discord.File(data, filename=default.timetext("Encryption")) 47 | ) 48 | except discord.HTTPException: 49 | return await ctx.send(f"The file I returned was over 8 MB, sorry {ctx.author.name}...") 50 | 51 | try: 52 | await ctx.send(f"📑 **{convert}**```fix\n{_input.decode('UTF-8')}```") 53 | except AttributeError: 54 | await ctx.send(f"📑 **{convert}**```fix\n{_input}```") 55 | 56 | 57 | class Encryption(commands.Cog, name="加密"): 58 | def __init__(self, bot): 59 | self.bot = bot 60 | 61 | @commands.group(name="編碼", aliases=["encode"]) 62 | async def encode(self, ctx): 63 | """ 所有編碼的方法。 """ 64 | if ctx.invoked_subcommand is None: 65 | await ctx.send_help(str(ctx.command)) 66 | 67 | @commands.group(name="解碼", aliases=["decode"]) 68 | async def decode(self, ctx): 69 | """ 所有解碼的方法。 """ 70 | if ctx.invoked_subcommand is None: 71 | await ctx.send_help(str(ctx.command)) 72 | 73 | @encode.command(name="base32", aliases=["b32"]) 74 | async def encode_base32(self, ctx, *, _input: clean_content = None): 75 | """ Encode in base32 """ 76 | if not _input: 77 | _input = await detect_file(ctx) 78 | 79 | await encrypt_out( 80 | ctx, "Text -> base32", base64.b32encode(_input.encode('UTF-8')) 81 | ) 82 | 83 | @decode.command(name="base32", aliases=["b32"]) 84 | async def decode_base32(self, ctx, *, _input: clean_content = None): 85 | """ Decode in base32 """ 86 | if not _input: 87 | _input = await detect_file(ctx) 88 | 89 | try: 90 | await encrypt_out(ctx, "base32 -> Text", base64.b32decode(_input.encode('UTF-8'))) 91 | except Exception: 92 | await ctx.send("Invalid base32...") 93 | 94 | @encode.command(name="base64", aliases=["b64"]) 95 | async def encode_base64(self, ctx, *, _input: clean_content = None): 96 | """ Encode in base64 """ 97 | if not _input: 98 | _input = await detect_file(ctx) 99 | 100 | await encrypt_out( 101 | ctx, "Text -> base64", base64.urlsafe_b64encode(_input.encode('UTF-8')) 102 | ) 103 | 104 | @decode.command(name="base64", aliases=["b64"]) 105 | async def decode_base64(self, ctx, *, _input: clean_content = None): 106 | """ Decode in base64 """ 107 | if not _input: 108 | _input = await detect_file(ctx) 109 | 110 | try: 111 | await encrypt_out(ctx, "base64 -> Text", base64.urlsafe_b64decode(_input.encode('UTF-8'))) 112 | except Exception: 113 | await ctx.send("Invalid base64...") 114 | 115 | @encode.command(name="rot13", aliases=["r13"]) 116 | async def encode_rot13(self, ctx, *, _input: clean_content = None): 117 | """ Encode in rot13 """ 118 | if not _input: 119 | _input = await detect_file(ctx) 120 | 121 | await encrypt_out( 122 | ctx, "Text -> rot13", codecs.decode(_input, 'rot_13') 123 | ) 124 | 125 | @decode.command(name="rot13", aliases=["r13"]) 126 | async def decode_rot13(self, ctx, *, _input: clean_content = None): 127 | """ Decode in rot13 """ 128 | if not _input: 129 | _input = await detect_file(ctx) 130 | 131 | try: 132 | await encrypt_out(ctx, "rot13 -> Text", codecs.decode(_input, 'rot_13')) 133 | except Exception: 134 | await ctx.send("Invalid rot13...") 135 | 136 | @encode.command(name="hex") 137 | async def encode_hex(self, ctx, *, _input: clean_content = None): 138 | """ Encode in hex """ 139 | if not _input: 140 | _input = await detect_file(ctx) 141 | 142 | await encrypt_out( 143 | ctx, "Text -> hex", 144 | binascii.hexlify(_input.encode('UTF-8')) 145 | ) 146 | 147 | @decode.command(name="hex") 148 | async def decode_hex(self, ctx, *, _input: clean_content = None): 149 | """ Decode in hex """ 150 | if not _input: 151 | _input = await detect_file(ctx) 152 | 153 | try: 154 | await encrypt_out(ctx, "hex -> Text", binascii.unhexlify(_input.encode('UTF-8'))) 155 | except Exception: 156 | await ctx.send("Invalid hex...") 157 | 158 | @encode.command(name="base85", aliases=["b85"]) 159 | async def encode_base85(self, ctx, *, _input: clean_content = None): 160 | """ Encode in base85 """ 161 | if not _input: 162 | _input = await detect_file(ctx) 163 | 164 | await encrypt_out( 165 | ctx, "Text -> base85", 166 | base64.b85encode(_input.encode('UTF-8')) 167 | ) 168 | 169 | @decode.command(name="base85", aliases=["b85"]) 170 | async def decode_base85(self, ctx, *, _input: clean_content = None): 171 | """ Decode in base85 """ 172 | if not _input: 173 | _input = await detect_file(ctx) 174 | 175 | try: 176 | await encrypt_out(ctx, "base85 -> Text", base64.b85decode(_input.encode('UTF-8'))) 177 | except Exception: 178 | await ctx.send("Invalid base85...") 179 | 180 | @encode.command(name="ascii85", aliases=["a85"]) 181 | async def encode_ascii85(self, ctx, *, _input: clean_content = None): 182 | """ Encode in ASCII85 """ 183 | if not _input: 184 | _input = await detect_file(ctx) 185 | 186 | await encrypt_out( 187 | ctx, "Text -> ASCII85", 188 | base64.a85encode(_input.encode('UTF-8')) 189 | ) 190 | 191 | @decode.command(name="ascii85", aliases=["a85"]) 192 | async def decode_ascii85(self, ctx, *, _input: clean_content = None): 193 | """ Decode in ASCII85 """ 194 | if not _input: 195 | _input = await detect_file(ctx) 196 | 197 | try: 198 | await encrypt_out(ctx, "ASCII85 -> Text", base64.a85decode(_input.encode('UTF-8'))) 199 | except Exception: 200 | await ctx.send("Invalid ASCII85...") 201 | 202 | 203 | def setup(bot): 204 | bot.add_cog(Encryption(bot)) 205 | -------------------------------------------------------------------------------- /cogs/admin.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import sys 4 | 5 | import aiohttp 6 | import discord 7 | from discord.ext import commands 8 | 9 | from utils import ( 10 | dataIO, 11 | default, 12 | http, 13 | permissions, 14 | ) 15 | 16 | 17 | class Admin(commands.Cog, name="管理員"): 18 | def __init__(self, bot): 19 | self.bot = bot 20 | self.config = default.get_from_env("CONFIG") 21 | self._last_result = None 22 | 23 | @commands.command(name="我是管理者嗎", aliases=["amiadmin"]) 24 | async def am_i_admin(self, ctx): 25 | """ 我是管理者嗎? """ 26 | if ctx.author.id in self.config.owners: 27 | return await ctx.send(f"不,**{ctx.author.name}** 你是個非常好的管理者!✅") 28 | 29 | # Please do not remove this part. 30 | # I would love to be credited as the original creator of the source code. 31 | # -- AlexFlipnote 32 | if ctx.author.id == 86477779717066752: 33 | return await ctx.send(f"Well kinda **{ctx.author.name}**.. you still own the source code") 34 | 35 | await ctx.send(f"不,真的不,**{ctx.author.name}** 你什麼都不是。") 36 | 37 | @commands.command(name="加載cogs", aliases=["load"]) 38 | @commands.check(permissions.is_owner) 39 | async def load_cogs(self, ctx, name: str): 40 | """ 加載 cogs 擴展功能。 """ 41 | try: 42 | self.bot.load_extension(f"cogs.{name}") 43 | except Exception as e: 44 | return await ctx.send(f"汪嗚 ... 窩好像失敗惹 。・゚・(つд`゚)・゚・。 他吐ㄌ一堆窩看不懂ㄉ咚咚{default.traceback_maker(e)}") 45 | await ctx.send(f"窩成功ㄉ加載了 **{name}.py** 哦汪 (`・ω・´)。") 46 | 47 | @commands.command(name="卸載cogs", aliases=["unload"]) 48 | @commands.check(permissions.is_owner) 49 | async def unload_cogs(self, ctx, name: str): 50 | """ 卸載 cogs 擴展功能。 """ 51 | try: 52 | self.bot.unload_extension(f"cogs.{name}") 53 | except Exception as e: 54 | return await ctx.send(default.traceback_maker(e)) 55 | await ctx.send(f"窩成功ㄉ卸載了 **{name}.py** 哦汪 (`・ω・´)。") 56 | 57 | @commands.command(name="重新載入cogs", aliases=["reload"]) 58 | @commands.check(permissions.is_owner) 59 | async def reload_cogs(self, ctx, name: str): 60 | """ 重新載入 cogs 擴展功能。 """ 61 | try: 62 | self.bot.reload_extension(f"cogs.{name}") 63 | except Exception as e: 64 | return await ctx.send(default.traceback_maker(e)) 65 | await ctx.send(f"窩成功ㄉ重新載入了 **{name}.py** 哦汪 (`・ω・´)。") 66 | 67 | @commands.command(name="重新載入所有cogs", aliases=["reloadall", "r"]) 68 | @commands.check(permissions.is_owner) 69 | async def reload_all_cogs(self, ctx): 70 | """ 重新載入所有的 cogs 擴展功能。 """ 71 | error_collection = [] 72 | for file in os.listdir("cogs"): 73 | if file.endswith(".py"): 74 | name = file[:-3] 75 | try: 76 | self.bot.reload_extension(f"cogs.{name}") 77 | except Exception as e: 78 | error_collection.append( 79 | [file, default.traceback_maker(e, advance=False)] 80 | ) 81 | 82 | if error_collection: 83 | output = "\n".join([f"**{g[0]}** ```diff\n- {g[1]}```" for g in error_collection]) 84 | return await ctx.send( 85 | f"汪嗚 ... 窩嘗試重新載入所有擴展功能,能夠重新載入這些咚咚 " 86 | f"但是有些咚咚失敗惹嗚 ... 。・゚・(つд`゚)・゚・。\n\n{output}" 87 | ) 88 | 89 | await ctx.send(f"窩已經成功重新載入所~有的 cogs 哦汪 (`・ω・´)。") 90 | 91 | @commands.command(name="重新載入utils", aliases=["reloadutils"]) 92 | @commands.check(permissions.is_owner) 93 | async def reload_utils(self, ctx, name: str): 94 | """ 重新載入 utils 模組。 """ 95 | name_maker = f"utils/{name}.py" 96 | try: 97 | module_name = importlib.import_module(f"utils.{name}") 98 | importlib.reload(module_name) 99 | except ModuleNotFoundError: 100 | return await ctx.send(f"汪嗚 ... 窩找不到 **{name_maker}** 這個東西呐汪 。・゚・(つд`゚)・゚・。") 101 | except Exception as e: 102 | error = default.traceback_maker(e) 103 | return await ctx.send(f"汪嗚 ... 載入模組 **{name_maker}** 的時候,好像怪怪的吶 。・゚・(つд`゚)・゚・ 有怪怪的東西窩看不懂\n{error}") 104 | await ctx.send(f"窩已經成功重新載入 **{name_maker}** 哦汪 (`・ω・´)。") 105 | 106 | @commands.command(name="重新啟動", aliases=["reboot"]) 107 | @commands.check(permissions.is_owner) 108 | async def reboot(self, ctx): 109 | """ 重新啟動機器人。 """ 110 | await ctx.send('我現在要睡覺覺惹,主人晚安汪 ... (\*´з`\*)') 111 | sys.exit(100) 112 | 113 | @commands.command(name="關閉", aliases=["shutdown"]) 114 | @commands.check(permissions.is_owner) 115 | async def shutdown(self, ctx): 116 | """ 關閉機器人。 """ 117 | await ctx.send("我現在要睡覺覺惹,主人晚安汪 ... (\*´з`\*)") 118 | sys.exit(0) 119 | 120 | @commands.command(name="傳訊息給", aliases=["dm"]) 121 | @commands.check(permissions.is_owner) 122 | async def deliver_message(self, ctx, user_id: int, *, message: str): 123 | """ 傳訊息給使用者。 """ 124 | user = self.bot.get_user(user_id) 125 | if not user: 126 | return await ctx.send(f"汪嗚、我好像找不到 **{user_id}** 是誰呢 ... ( ˘・з・)") 127 | 128 | try: 129 | await user.send(message) 130 | await ctx.send(f"✉️ 窩已經成功把訊息傳給了 **<@{user_id}>** 哦汪 d(`・∀・)b") 131 | except discord.Forbidden: 132 | await ctx.send("汪嗚、我好像被這個人給封鎖惹嗚 ... (´;ω;`)") 133 | 134 | @commands.group(name="切換", aliases=["change"]) 135 | @commands.check(permissions.is_owner) 136 | async def change(self, ctx): 137 | if ctx.invoked_subcommand is None: 138 | await ctx.send_help(str(ctx.command)) 139 | 140 | @change.command(name="狀態", aliases=["status"]) 141 | @commands.check(permissions.is_owner) 142 | async def change_status(self, ctx, *, playing: str): 143 | """ 幫機器人換其他顯示狀態。 """ 144 | if self.config.status_type == "閒置": 145 | status_type = discord.Status.idle 146 | elif self.config.status_type == "請勿打擾": 147 | status_type = discord.Status.dnd 148 | else: 149 | status_type = discord.Status.online 150 | 151 | if self.config.playing_type == "收聽中": 152 | playing_type = 2 153 | elif self.config.playing_type == "觀看中": 154 | playing_type = 3 155 | else: 156 | playing_type = 0 157 | 158 | try: 159 | await self.bot.change_presence( 160 | activity=discord.Activity(type=playing_type, name=playing), 161 | status=status_type 162 | ) 163 | dataIO.change_value("config.json", "正在玩", playing) 164 | await ctx.send(f"我把自己的狀態換成了「**{playing}**」哦汪 (`・ω・´)") 165 | except discord.InvalidArgument as err: 166 | await ctx.send(err) 167 | except Exception as e: 168 | await ctx.send(e) 169 | 170 | @change.command(name="名字", aliases=["username"]) 171 | @commands.check(permissions.is_owner) 172 | async def change_username(self, ctx, *, name: str): 173 | """ 幫機器人換個新名字。 """ 174 | try: 175 | await self.bot.user.edit(username=name) 176 | await ctx.send(f"嗚 ... 從今以後我的名字就叫做「**{name}**」哦汪 (`・ω・´)") 177 | except discord.HTTPException as err: 178 | await ctx.send(f"汪嗚 ... 換名字的時候,好像怪怪的吶 。・゚・(つд`゚)・゚・ 有怪怪的東西窩看不懂\n{err}") 179 | 180 | @change.command(name="暱稱", aliases=["nickname"]) 181 | @commands.check(permissions.is_owner) 182 | async def change_nickname(self, ctx, *, name: str = None): 183 | """ 幫機器人換個新暱稱。 """ 184 | try: 185 | await ctx.guild.me.edit(nick=name) 186 | if name: 187 | await ctx.send(f"嗚 ... 從今以後我的暱稱就叫做「**{name}**」哦汪 (`・ω・´)") 188 | else: 189 | await ctx.send("嗚 ... 從今以後還是叫窩ㄉ名字就好哦汪 (`・ω・´)") 190 | except Exception as err: 191 | await ctx.send(f"汪嗚 ... 換暱稱的時候,好像怪怪的吶 。・゚・(つд`゚)・゚・ 有怪怪的東西窩看不懂\n{err}") 192 | 193 | @change.command(name="大頭貼", aliases=["avatar"]) 194 | @commands.check(permissions.is_owner) 195 | async def change_avatar(self, ctx, url: str = None): 196 | """ 幫機器人換個新大頭貼。 """ 197 | if url is None and len(ctx.message.attachments) == 1: 198 | url = ctx.message.attachments[0].url 199 | else: 200 | url = url.strip('<>') if url else None 201 | 202 | try: 203 | bio = await http.get(url, res_method="read") 204 | await self.bot.user.edit(avatar=bio) 205 | await ctx.send(f"嗚 ... 窩換了新的大頭貼哦汪 (`・ω・´) 窩換成這張:\n{url}") 206 | except aiohttp.InvalidURL: 207 | await ctx.send("汪嗚 ... 這個網址打不開啦 -`д´-") 208 | except discord.InvalidArgument: 209 | await ctx.send("汪嗚 ... 這個網址好像不是圖片啦 -`д´-") 210 | except discord.HTTPException as err: 211 | await ctx.send(f"汪嗚 ... 換大頭貼的時候,好像怪怪的吶 。・゚・(つд`゚)・゚・ 有怪怪的東西窩看不懂\n{err}") 212 | except TypeError: 213 | await ctx.send("汪嗚 ... 尼該不會不知道怎麼幫窩換大頭貼ㄅ,科科笑 σ`∀´)σ") 214 | 215 | 216 | def setup(bot): 217 | bot.add_cog(Admin(bot)) 218 | -------------------------------------------------------------------------------- /cogs/fun.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import re 4 | import secrets 5 | from io import BytesIO 6 | from urllib.parse import quote 7 | 8 | import aiohttp 9 | import discord 10 | from discord.ext import commands 11 | from discord.ext.commands import clean_content 12 | 13 | from utils import ( 14 | argparser, 15 | default, 16 | http, 17 | permissions, 18 | ) 19 | 20 | 21 | async def random_image_api(ctx, url, endpoint): 22 | try: 23 | r = await http.get(url, res_method="json", no_cache=True) 24 | except aiohttp.ClientConnectorError: 25 | return await ctx.send("The API seems to be down...") 26 | except aiohttp.ContentTypeError: 27 | return await ctx.send("The API returned an error or didn't return JSON...") 28 | 29 | await ctx.send(r[endpoint]) 30 | 31 | 32 | async def api_img_creator(ctx, url, filename, content=None): 33 | async with ctx.channel.typing(): 34 | req = await http.get(url, res_method="read") 35 | 36 | if req is None: 37 | return await ctx.send("I couldn't create the image ;-;") 38 | 39 | bio = BytesIO(req) 40 | bio.seek(0) 41 | await ctx.send(content=content, file=discord.File(bio, filename=filename)) 42 | 43 | 44 | class FunCommands(commands.Cog, name="有趣指令"): 45 | def __init__(self, bot): 46 | self.bot = bot 47 | self.config = default.get_from_env("CONFIG") 48 | 49 | @commands.command(name="貓", aliases=["cat"]) 50 | @commands.cooldown(rate=1, per=1.5, type=commands.BucketType.user) 51 | async def cat(self, ctx): 52 | """ 發送一張隨機的貓照片 """ 53 | await random_image_api(ctx, 'https://api.alexflipnote.dev/cats', 'file') 54 | 55 | @commands.command(name="狗", aliases=["dog"]) 56 | @commands.cooldown(rate=1, per=1.5, type=commands.BucketType.user) 57 | async def dog(self, ctx): 58 | """ 發送一張隨機的狗照片 """ 59 | await random_image_api(ctx, 'https://api.alexflipnote.dev/dogs', 'file') 60 | 61 | @commands.command(name="鳥", aliases=["bird", "birb"]) 62 | @commands.cooldown(rate=1, per=1.5, type=commands.BucketType.user) 63 | async def bird(self, ctx): 64 | """ 發送一張隨機的鳥照片 """ 65 | await random_image_api(ctx, 'https://api.alexflipnote.dev/birb', 'file') 66 | 67 | @commands.command(name="鴨子", aliases=["duck"]) 68 | @commands.cooldown(rate=1, per=1.5, type=commands.BucketType.user) 69 | async def duck(self, ctx): 70 | """ 發送一張隨機的鴨子照 """ 71 | await random_image_api(ctx, 'https://random-d.uk/api/v1/random', 'url') 72 | 73 | @commands.command(name="翻硬幣", aliases=['flip', 'coin']) 74 | async def coin_flip(self, ctx): 75 | """ 翻硬幣! """ 76 | coin_sides = ['Heads', 'Tails'] 77 | await ctx.send(f"**{ctx.author.name}** flipped a coin and got **{random.choice(coin_sides)}**!") 78 | 79 | @commands.command() 80 | async def f(self, ctx, *, text: clean_content = None): 81 | """ Press F to pay respect """ 82 | hearts = ['❤', '💛', '💚', '💙', '💜'] 83 | reason = f"for **{text}** " if text else "" 84 | await ctx.send(f"**{ctx.author.name}** has paid their respect {reason}{random.choice(hearts)}") 85 | 86 | @commands.command() 87 | async def supreme(self, ctx, *, text: clean_content(fix_channel_mentions=True)): 88 | """ 製作一個假的 Supreme logo 89 | 90 | Arguments: 91 | --dark | Make the background to dark colour 92 | --light | Make background to light and text to dark colour 93 | """ 94 | parser = argparser.Arguments() 95 | parser.add_argument('input', nargs="+", default=None) 96 | parser.add_argument('-d', '--dark', action='store_true') 97 | parser.add_argument('-l', '--light', action='store_true') 98 | 99 | args, valid_check = parser.parse_args(text) 100 | if not valid_check: 101 | return await ctx.send(args) 102 | 103 | input_text = quote(' '.join(args.input)) 104 | if len(input_text) > 500: 105 | return await ctx.send(f"**{ctx.author.name}**, the Supreme API is limited to 500 characters, sorry.") 106 | 107 | dark_or_light = "" 108 | if args.dark: 109 | dark_or_light = "dark=true" 110 | if args.light: 111 | dark_or_light = "light=true" 112 | if args.dark and args.light: 113 | return await ctx.send(f"**{ctx.author.name}**, you can't define both --dark and --light, sorry..") 114 | 115 | await api_img_creator(ctx, f"https://api.alexflipnote.dev/supreme?text={input_text}&{dark_or_light}", "supreme.png") 116 | 117 | @commands.command(name="顏色", aliases=['colour', 'color']) 118 | @commands.cooldown(rate=1, per=3.0, type=commands.BucketType.user) 119 | async def colour(self, ctx, colour: str): 120 | """ 查看顏色的 HEX 詳細資訊 """ 121 | async with ctx.channel.typing(): 122 | if not permissions.can_embed(ctx): 123 | return await ctx.send("I can't embed in this channel ;-;") 124 | 125 | if colour == "random": 126 | colour = "%06x" % random.randint(0, 0xFFFFFF) 127 | 128 | if colour[:1] == "#": 129 | colour = colour[1:] 130 | 131 | if not re.search(r'^(?:[0-9a-fA-F]{3}){1,2}$', colour): 132 | return await ctx.send("You're only allowed to enter HEX (0-9 & A-F)") 133 | 134 | try: 135 | r = await http.get(f"https://api.alexflipnote.dev/colour/{colour}", res_method="json", no_cache=True) 136 | except aiohttp.ClientConnectorError: 137 | return await ctx.send("The API seems to be down...") 138 | except aiohttp.ContentTypeError: 139 | return await ctx.send("The API returned an error or didn't return JSON...") 140 | 141 | embed = discord.Embed(colour=r["int"]) 142 | embed.set_thumbnail(url=r["image"]) 143 | embed.set_image(url=r["image_gradient"]) 144 | 145 | embed.add_field(name="HEX", value=r['hex'], inline=True) 146 | embed.add_field(name="RGB", value=r['rgb'], inline=True) 147 | embed.add_field(name="Int", value=r['int'], inline=True) 148 | embed.add_field(name="Brightness", value=r['brightness'], inline=True) 149 | 150 | await ctx.send(embed=embed, content=f"{ctx.invoked_with.title()} name: **{r['name']}**") 151 | 152 | @commands.command() 153 | @commands.cooldown(rate=1, per=2.0, type=commands.BucketType.user) 154 | async def urban(self, ctx, *, search: clean_content): 155 | """ 尋找你字的『最佳』定義 """ 156 | async with ctx.channel.typing(): 157 | try: 158 | url = await http.get(f'https://api.urbandictionary.com/v0/define?term={search}', res_method="json") 159 | except Exception: 160 | return await ctx.send("Urban API returned invalid data... might be down atm.") 161 | 162 | if not url: 163 | return await ctx.send("I think the API broke...") 164 | 165 | if not len(url['list']): 166 | return await ctx.send("Couldn't find your search in the dictionary...") 167 | 168 | result = sorted(url['list'], reverse=True, key=lambda g: int(g["thumbs_up"]))[0] 169 | 170 | definition = result['definition'] 171 | if len(definition) >= 1000: 172 | definition = definition[:1000] 173 | definition = definition.rsplit(' ', 1)[0] 174 | definition += '...' 175 | 176 | await ctx.send(f"📚 Definitions for **{result['word']}**```fix\n{definition}```") 177 | 178 | @commands.command(name="反轉", aliases=["reverse"]) 179 | async def reverse(self, ctx, *, text: str): 180 | """ !轉反會都入輸有所 181 | Everything you type after reverse will of course, be reversed 182 | """ 183 | t_rev = text[::-1].replace("@", "@\u200B").replace("&", "&\u200B") 184 | await ctx.send(f"🔁 {t_rev}") 185 | 186 | @commands.command(name="密碼", aliases=["password"]) 187 | async def password(self, ctx, n_bytes: int = 18): 188 | """ 為你生成一串隨機的密碼字串 189 | 190 | This returns a random URL-safe text string, containing nbytes random bytes. 191 | The text is Base64 encoded, so on average each byte results in approximately 1.3 characters. 192 | """ 193 | if n_bytes not in range(3, 1401): 194 | return await ctx.send("I only accept any numbers between 3-1400") 195 | if hasattr(ctx, 'guild') and ctx.guild is not None: 196 | await ctx.send(f"Sending you a private message with your random generated password **{ctx.author.name}**") 197 | await ctx.author.send(f"🎁 **Here is your password:**\n{secrets.token_urlsafe(n_bytes)}") 198 | 199 | @commands.command() 200 | async def rate(self, ctx, *, thing: clean_content): 201 | """ Rates what you desire """ 202 | rate_amount = random.uniform(0.0, 100.0) 203 | await ctx.send(f"I'd rate `{thing}` a **{round(rate_amount, 4)} / 100**") 204 | 205 | @commands.command(name="啤酒", aliases=["beer"]) 206 | async def beer(self, ctx, user: discord.Member = None, *, reason: clean_content = ""): 207 | """ 給某人一杯啤酒! 🍻 """ 208 | if not user or user.id == ctx.author.id: 209 | return await ctx.send(f"**{ctx.author.name}**: paaaarty!🎉🍺") 210 | if user.id == self.bot.user.id: 211 | return await ctx.send("*drinks beer with you* 🍻") 212 | if user.bot: 213 | return await ctx.send(f"I would love to give beer to the bot **{ctx.author.name}**, but I don't think it will respond to you :/") 214 | 215 | beer_offer = f"**{user.name}**, you got a 🍺 offer from **{ctx.author.name}**" 216 | beer_offer = beer_offer + f"\n\n**Reason:** {reason}" if reason else beer_offer 217 | msg = await ctx.send(beer_offer) 218 | 219 | def reaction_check(m): 220 | if m.message_id == msg.id and m.user_id == user.id and str(m.emoji) == "🍻": 221 | return True 222 | return False 223 | 224 | try: 225 | await msg.add_reaction("🍻") 226 | await self.bot.wait_for('raw_reaction_add', timeout=30.0, check=reaction_check) 227 | await msg.edit(content=f"**{user.name}** and **{ctx.author.name}** are enjoying a lovely beer together 🍻") 228 | except asyncio.TimeoutError: 229 | await msg.delete() 230 | await ctx.send(f"well, doesn't seem like **{user.name}** wanted a beer with you **{ctx.author.name}** ;-;") 231 | except discord.Forbidden: 232 | # Yeah so, bot doesn't have reaction permission, drop the "offer" word 233 | beer_offer = f"**{user.name}**, you got a 🍺 from **{ctx.author.name}**" 234 | beer_offer = beer_offer + f"\n\n**Reason:** {reason}" if reason else beer_offer 235 | await msg.edit(content=beer_offer) 236 | 237 | @commands.command(aliases=['howhot', 'hot']) 238 | async def hot_calc(self, ctx, *, user: discord.Member = None): 239 | """ 隨機回傳一個百分比來代表一個人有多 hot """ 240 | user = user or ctx.author 241 | 242 | random.seed(user.id) 243 | r = random.randint(1, 100) 244 | hot = r / 1.17 245 | 246 | emoji = "💔" 247 | if hot > 25: 248 | emoji = "❤" 249 | if hot > 50: 250 | emoji = "💖" 251 | if hot > 75: 252 | emoji = "💞" 253 | 254 | await ctx.send(f"**{user.name}** is **{hot:.2f}%** hot {emoji}") 255 | 256 | @commands.command(aliases=['noticemesenpai']) 257 | async def notice_me(self, ctx): 258 | """ Notice me senpai! owo """ 259 | if not permissions.can_upload(ctx): 260 | return await ctx.send("I cannot send images here ;-;") 261 | 262 | bio = BytesIO(await http.get("https://i.alexflipnote.dev/500ce4.gif", res_method="read")) 263 | await ctx.send(file=discord.File(bio, filename="noticeme.gif")) 264 | 265 | @commands.command(name="老虎機", aliases=['slots', 'bet']) 266 | @commands.cooldown(rate=1, per=3.0, type=commands.BucketType.user) 267 | async def slot(self, ctx): 268 | """ Roll the slot machine """ 269 | emojis = "🍎🍊🍐🍋🍉🍇🍓🍒" 270 | a = random.choice(emojis) 271 | b = random.choice(emojis) 272 | c = random.choice(emojis) 273 | 274 | slotmachine = f"**[ {a} {b} {c} ]\n{ctx.author.name}**," 275 | 276 | if a == b == c: 277 | await ctx.send(f"{slotmachine} All matching, you won! 🎉") 278 | elif (a == b) or (a == c) or (b == c): 279 | await ctx.send(f"{slotmachine} 2 in a row, you won! 🎉") 280 | else: 281 | await ctx.send(f"{slotmachine} No match, you lost 😢") 282 | 283 | 284 | def setup(bot): 285 | bot.add_cog(FunCommands(bot)) 286 | -------------------------------------------------------------------------------- /cogs/mod.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import re 3 | import asyncio 4 | 5 | from discord.ext import commands 6 | from utils import permissions, default 7 | 8 | 9 | # Source: https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/mod.py 10 | class MemberID(commands.Converter): 11 | async def convert(self, ctx, argument): 12 | try: 13 | m = await commands.MemberConverter().convert(ctx, argument) 14 | except commands.BadArgument: 15 | try: 16 | return int(argument, base=10) 17 | except ValueError: 18 | raise commands.BadArgument(f"{argument} is not a valid member or member ID.") from None 19 | else: 20 | return m.id 21 | 22 | 23 | class ActionReason(commands.Converter): 24 | async def convert(self, ctx, argument): 25 | ret = argument 26 | 27 | if len(ret) > 512: 28 | reason_max = 512 - len(ret) - len(argument) 29 | raise commands.BadArgument(f'reason is too long ({len(argument)}/{reason_max})') 30 | return ret 31 | 32 | 33 | class Moderator(commands.Cog): 34 | def __init__(self, bot): 35 | self.bot = bot 36 | self.config = default.get_from_env("CONFIG") 37 | 38 | @commands.command() 39 | @commands.guild_only() 40 | @permissions.has_permissions(kick_members=True) 41 | async def kick(self, ctx, member: discord.Member, *, reason: str = None): 42 | """ Kicks a user from the current server. """ 43 | if await permissions.check_priv(ctx, member): 44 | return 45 | 46 | try: 47 | await member.kick(reason=default.responsible(ctx.author, reason)) 48 | await ctx.send(default.actionmessage("kicked")) 49 | except Exception as e: 50 | await ctx.send(e) 51 | 52 | @commands.command(aliases=["nick"]) 53 | @commands.guild_only() 54 | @permissions.has_permissions(manage_nicknames=True) 55 | async def nickname(self, ctx, member: discord.Member, *, name: str = None): 56 | """ Nicknames a user from the current server. """ 57 | if await permissions.check_priv(ctx, member): 58 | return 59 | 60 | try: 61 | await member.edit(nick=name, reason=default.responsible(ctx.author, "Changed by command")) 62 | message = f"Changed **{member.name}'s** nickname to **{name}**" 63 | if name is None: 64 | message = f"Reset **{member.name}'s** nickname" 65 | await ctx.send(message) 66 | except Exception as e: 67 | await ctx.send(e) 68 | 69 | @commands.command() 70 | @commands.guild_only() 71 | @permissions.has_permissions(ban_members=True) 72 | async def ban(self, ctx, member: MemberID, *, reason: str = None): 73 | """ Bans a user from the current server. """ 74 | m = ctx.guild.get_member(member) 75 | if m is not None and await permissions.check_priv(ctx, m): 76 | return 77 | 78 | try: 79 | await ctx.guild.ban(discord.Object(id=member), reason=default.responsible(ctx.author, reason)) 80 | await ctx.send(default.actionmessage("banned")) 81 | except Exception as e: 82 | await ctx.send(e) 83 | 84 | @commands.command() 85 | @commands.guild_only() 86 | @commands.max_concurrency(1, per=commands.BucketType.user) 87 | @permissions.has_permissions(ban_members=True) 88 | async def massban(self, ctx, reason: ActionReason, *members: MemberID): 89 | """ Mass bans multiple members from the server. """ 90 | try: 91 | for member_id in members: 92 | await ctx.guild.ban(discord.Object(id=member_id), reason=default.responsible(ctx.author, reason)) 93 | await ctx.send(default.actionmessage("massbanned", mass=True)) 94 | except Exception as e: 95 | await ctx.send(e) 96 | 97 | @commands.command() 98 | @commands.guild_only() 99 | @permissions.has_permissions(ban_members=True) 100 | async def unban(self, ctx, member: MemberID, *, reason: str = None): 101 | """ Unbans a user from the current server. """ 102 | try: 103 | await ctx.guild.unban(discord.Object(id=member), reason=default.responsible(ctx.author, reason)) 104 | await ctx.send(default.actionmessage("unbanned")) 105 | except Exception as e: 106 | await ctx.send(e) 107 | 108 | @commands.command() 109 | @commands.guild_only() 110 | @permissions.has_permissions(manage_roles=True) 111 | async def mute(self, ctx, member: discord.Member, *, reason: str = None): 112 | """ Mutes a user from the current server. """ 113 | if await permissions.check_priv(ctx, member): 114 | return 115 | 116 | muted_role = next((g for g in ctx.guild.roles if g.name == "Muted"), None) 117 | 118 | if not muted_role: 119 | return await ctx.send("Are you sure you've made a role called **Muted**? Remember that it's case sensetive too...") 120 | 121 | try: 122 | await member.add_roles(muted_role, reason=default.responsible(ctx.author, reason)) 123 | await ctx.send(default.actionmessage("muted")) 124 | except Exception as e: 125 | await ctx.send(e) 126 | 127 | @commands.command() 128 | @commands.guild_only() 129 | @permissions.has_permissions(manage_roles=True) 130 | async def unmute(self, ctx, member: discord.Member, *, reason: str = None): 131 | """ Unmutes a user from the current server. """ 132 | if await permissions.check_priv(ctx, member): 133 | return 134 | 135 | muted_role = next((g for g in ctx.guild.roles if g.name == "Muted"), None) 136 | 137 | if not muted_role: 138 | return await ctx.send("Are you sure you've made a role called **Muted**? Remember that it's case sensetive too...") 139 | 140 | try: 141 | await member.remove_roles(muted_role, reason=default.responsible(ctx.author, reason)) 142 | await ctx.send(default.actionmessage("unmuted")) 143 | except Exception as e: 144 | await ctx.send(e) 145 | 146 | @commands.command(aliases=["ar"]) 147 | @commands.guild_only() 148 | @permissions.has_permissions(manage_roles=True) 149 | async def announcerole(self, ctx, *, role: discord.Role): 150 | """ Makes a role mentionable and removes it whenever you mention the role """ 151 | if role == ctx.guild.default_role: 152 | return await ctx.send("To prevent abuse, I won't allow mentionable role for everyone/here role.") 153 | 154 | if ctx.author.top_role.position <= role.position: 155 | return await ctx.send("It seems like the role you attempt to mention is over your permissions, therefor I won't allow you.") 156 | 157 | if ctx.me.top_role.position <= role.position: 158 | return await ctx.send("This role is above my permissions, I can't make it mentionable ;-;") 159 | 160 | await role.edit(mentionable=True, reason=f"[ {ctx.author} ] announcerole command") 161 | msg = await ctx.send(f"**{role.name}** is now mentionable, if you don't mention it within 30 seconds, I will revert the changes.") 162 | 163 | while True: 164 | def role_checker(m): 165 | if (role.mention in m.content): 166 | return True 167 | return False 168 | 169 | try: 170 | checker = await self.bot.wait_for('message', timeout=30.0, check=role_checker) 171 | if checker.author.id == ctx.author.id: 172 | await role.edit(mentionable=False, reason=f"[ {ctx.author} ] announcerole command") 173 | return await msg.edit(content=f"**{role.name}** mentioned by **{ctx.author}** in {checker.channel.mention}") 174 | break 175 | else: 176 | await checker.delete() 177 | except asyncio.TimeoutError: 178 | await role.edit(mentionable=False, reason=f"[ {ctx.author} ] announcerole command") 179 | return await msg.edit(content=f"**{role.name}** was never mentioned by **{ctx.author}**...") 180 | break 181 | 182 | @commands.group() 183 | @commands.guild_only() 184 | @permissions.has_permissions(ban_members=True) 185 | async def find(self, ctx): 186 | """ Finds a user within your search term """ 187 | if ctx.invoked_subcommand is None: 188 | await ctx.send_help(str(ctx.command)) 189 | 190 | @find.command(name="正在玩") 191 | async def find_playing(self, ctx, *, search: str): 192 | loop = [] 193 | for i in ctx.guild.members: 194 | if i.activities and (not i.bot): 195 | for g in i.activities: 196 | if g.name and (search.lower() in g.name.lower()): 197 | loop.append(f"{i} | {type(g).__name__}: {g.name} ({i.id})") 198 | 199 | await default.prettyResults( 200 | ctx, "正在玩", f"Found **{len(loop)}** on your search for **{search}**", loop 201 | ) 202 | 203 | @find.command(name="username", aliases=["name"]) 204 | async def find_name(self, ctx, *, search: str): 205 | loop = [f"{i} ({i.id})" for i in ctx.guild.members if search.lower() in i.name.lower() and not i.bot] 206 | await default.prettyResults( 207 | ctx, "name", f"Found **{len(loop)}** on your search for **{search}**", loop 208 | ) 209 | 210 | @find.command(name="nickname", aliases=["nick"]) 211 | async def find_nickname(self, ctx, *, search: str): 212 | loop = [f"{i.nick} | {i} ({i.id})" for i in ctx.guild.members if i.nick if (search.lower() in i.nick.lower()) and not i.bot] 213 | await default.prettyResults( 214 | ctx, "name", f"Found **{len(loop)}** on your search for **{search}**", loop 215 | ) 216 | 217 | @find.command(name="id") 218 | async def find_id(self, ctx, *, search: int): 219 | loop = [f"{i} | {i} ({i.id})" for i in ctx.guild.members if (str(search) in str(i.id)) and not i.bot] 220 | await default.prettyResults( 221 | ctx, "name", f"Found **{len(loop)}** on your search for **{search}**", loop 222 | ) 223 | 224 | @find.command(name="discriminator", aliases=["discrim"]) 225 | async def find_discriminator(self, ctx, *, search: str): 226 | if not len(search) == 4 or not re.compile("^[0-9]*$").search(search): 227 | return await ctx.send("You must provide exactly 4 digits") 228 | 229 | loop = [f"{i} ({i.id})" for i in ctx.guild.members if search == i.discriminator] 230 | await default.prettyResults( 231 | ctx, "discriminator", f"Found **{len(loop)}** on your search for **{search}**", loop 232 | ) 233 | 234 | @commands.group() 235 | @commands.guild_only() 236 | @commands.max_concurrency(1, per=commands.BucketType.guild) 237 | @permissions.has_permissions(manage_messages=True) 238 | async def prune(self, ctx): 239 | """ Removes messages from the current server. """ 240 | if ctx.invoked_subcommand is None: 241 | await ctx.send_help(str(ctx.command)) 242 | 243 | async def do_removal(self, ctx, limit, predicate, *, before=None, after=None, message=True): 244 | if limit > 2000: 245 | return await ctx.send(f'Too many messages to search given ({limit}/2000)') 246 | 247 | if before is None: 248 | before = ctx.message 249 | else: 250 | before = discord.Object(id=before) 251 | 252 | if after is not None: 253 | after = discord.Object(id=after) 254 | 255 | try: 256 | deleted = await ctx.channel.purge(limit=limit, before=before, after=after, check=predicate) 257 | except discord.Forbidden: 258 | return await ctx.send('I do not have permissions to delete messages.') 259 | except discord.HTTPException as e: 260 | return await ctx.send(f'Error: {e} (try a smaller search?)') 261 | 262 | deleted = len(deleted) 263 | if message is True: 264 | await ctx.send(f'🚮 Successfully removed {deleted} message{"" if deleted == 1 else "s"}.') 265 | 266 | @prune.command() 267 | async def embeds(self, ctx, search=100): 268 | """Removes messages that have embeds in them.""" 269 | await self.do_removal(ctx, search, lambda e: len(e.embeds)) 270 | 271 | @prune.command() 272 | async def files(self, ctx, search=100): 273 | """Removes messages that have attachments in them.""" 274 | await self.do_removal(ctx, search, lambda e: len(e.attachments)) 275 | 276 | @prune.command() 277 | async def mentions(self, ctx, search=100): 278 | """Removes messages that have mentions in them.""" 279 | await self.do_removal(ctx, search, lambda e: len(e.mentions) or len(e.role_mentions)) 280 | 281 | @prune.command() 282 | async def images(self, ctx, search=100): 283 | """Removes messages that have embeds or attachments.""" 284 | await self.do_removal(ctx, search, lambda e: len(e.embeds) or len(e.attachments)) 285 | 286 | @prune.command(name='all') 287 | async def _remove_all(self, ctx, search=100): 288 | """Removes all messages.""" 289 | await self.do_removal(ctx, search, lambda e: True) 290 | 291 | @prune.command() 292 | async def user(self, ctx, member: discord.Member, search=100): 293 | """Removes all messages by the member.""" 294 | await self.do_removal(ctx, search, lambda e: e.author == member) 295 | 296 | @prune.command() 297 | async def contains(self, ctx, *, substr: str): 298 | """Removes all messages containing a substring. 299 | The substring must be at least 3 characters long. 300 | """ 301 | if len(substr) < 3: 302 | await ctx.send('The substring length must be at least 3 characters.') 303 | else: 304 | await self.do_removal(ctx, 100, lambda e: substr in e.content) 305 | 306 | @prune.command(name='bots') 307 | async def _bots(self, ctx, search=100, prefix=None): 308 | """Removes a bot user's messages and messages with their optional prefix.""" 309 | 310 | getprefix = prefix if prefix else self.config.prefix 311 | 312 | def predicate(m): 313 | return (m.webhook_id is None and m.author.bot) or m.content.startswith(tuple(getprefix)) 314 | 315 | await self.do_removal(ctx, search, predicate) 316 | 317 | @prune.command(name='users') 318 | async def _users(self, ctx, prefix=None, search=100): 319 | """Removes only user messages. """ 320 | 321 | def predicate(m): 322 | return m.author.bot is False 323 | 324 | await self.do_removal(ctx, search, predicate) 325 | 326 | @prune.command(name='emojis') 327 | async def _emojis(self, ctx, search=100): 328 | """Removes all messages containing custom emoji.""" 329 | custom_emoji = re.compile(r'|[\u263a-\U0001f645]') 330 | 331 | def predicate(m): 332 | return custom_emoji.search(m.content) 333 | 334 | await self.do_removal(ctx, search, predicate) 335 | 336 | @prune.command(name='reactions') 337 | async def _reactions(self, ctx, search=100): 338 | """Removes all reactions from messages that have them.""" 339 | 340 | if search > 2000: 341 | return await ctx.send(f'Too many messages to search for ({search}/2000)') 342 | 343 | total_reactions = 0 344 | async for message in ctx.history(limit=search, before=ctx.message): 345 | if len(message.reactions): 346 | total_reactions += sum(r.count for r in message.reactions) 347 | await message.clear_reactions() 348 | 349 | await ctx.send(f'Successfully removed {total_reactions} reactions.') 350 | 351 | 352 | def setup(bot): 353 | bot.add_cog(Moderator(bot)) 354 | --------------------------------------------------------------------------------