├── .dockerignore ├── .gitignore ├── Containerfile ├── LICENSE ├── README.md ├── bot ├── __init__.py ├── cogs │ ├── __init__.py │ ├── commands.py │ └── owner.py └── guild_caches.py ├── config.json ├── requirements.txt └── runner.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .*/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc 2 | .*/ 3 | database/ 4 | logs/ 5 | *.sqlite3 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # IPython Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | RUN python3 -m venv .venv \ 6 | && . .venv/bin/activate 7 | 8 | COPY requirements.txt requirements.txt 9 | 10 | RUN pip install -r requirements.txt 11 | 12 | COPY . . 13 | 14 | CMD ["python", "runner.py"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rapptz 4 | Copyright (c) 2017-2021 Ivan Borgia Dardi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RustbotPython 2 | 3 | ## How to run 4 | 5 | Create a `.env` file like so: 6 | 7 | ```bash 8 | DISCORD_TOKEN= 9 | ``` 10 | 11 | Then run the following commands: 12 | 13 | ```bash 14 | docker build -t rustbotpy -f Containerfile . 15 | docker run --rm --name rustbotpy --env-file .env rustbotpy 16 | ``` 17 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import traceback 4 | 5 | import discord 6 | from discord.ext import commands 7 | 8 | from bot.guild_caches import GuildEmojis, GuildRoles 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class RustBot(commands.Bot): 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | 17 | self.uptime = datetime.datetime.utcnow() 18 | self.config = kwargs["config"] 19 | self.custom_extensions = self.config["extensions"] 20 | self.guild = None 21 | self.emoji = None 22 | self.role = None 23 | 24 | for extension in self.custom_extensions: 25 | try: 26 | self.load_extension(extension) 27 | except Exception as e: # noqa 28 | log.error( 29 | "Failed to load extension %s\n%s: %s", 30 | extension, 31 | type(e).__name__, 32 | e, 33 | ) 34 | 35 | async def on_ready(self): 36 | log.info("Logged in as %s", self.user) 37 | 38 | await self.change_presence(activity=discord.Game(name="?help")) 39 | 40 | self.guild = await self.fetch_guild(self.config["guild_id"]) 41 | 42 | self.emoji = GuildEmojis(self.config["emojis"], self.guild.emojis) 43 | self.role = GuildRoles(self.config["roles"], self.guild.roles) 44 | 45 | async def on_command(self, ctx): 46 | destination = f"#{ctx.channel}" 47 | if isinstance(ctx.channel, discord.abc.PrivateChannel): 48 | destination += f" ({ctx.guild})" 49 | log.info(f"{ctx.author} in {destination}: {ctx.message.content}") 50 | 51 | async def on_command_error(self, ctx: commands.Context, error): 52 | tb = "".join( 53 | traceback.format_exception(type(error), error, error.__traceback__) 54 | ) 55 | log.error(f"Command error in %s:\n%s", ctx.command, tb) 56 | if isinstance(error, (commands.CheckFailure, commands.ConversionError)): 57 | await ctx.message.add_reaction("❌") 58 | await ctx.send(str(error)) 59 | -------------------------------------------------------------------------------- /bot/cogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivandardi/RustbotPython/646415cdb7e28795692dceccbb916c65ea583df3/bot/cogs/__init__.py -------------------------------------------------------------------------------- /bot/cogs/commands.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from bot import RustBot 7 | 8 | 9 | class Commands(commands.Cog): 10 | def __init__(self, bot: RustBot): 11 | self.bot = bot 12 | 13 | @commands.command() 14 | async def uptime(self, ctx: commands.Context): 15 | """Tells you how long the bot has been up for.""" 16 | 17 | now = datetime.datetime.utcnow() 18 | delta = now - self.bot.uptime 19 | hours, remainder = divmod(int(delta.total_seconds()), 3600) 20 | minutes, seconds = divmod(remainder, 60) 21 | days, hours = divmod(hours, 24) 22 | 23 | fmt = "{h}h {m}m {s}s" 24 | if days: 25 | fmt = "{d}d " + fmt 26 | 27 | await ctx.reply( 28 | content="Uptime: {}".format( 29 | fmt.format(d=days, h=hours, m=minutes, s=seconds) 30 | ) 31 | ) 32 | 33 | @commands.command() 34 | async def invite(self, ctx: commands.Context): 35 | """Points the user to the #informational channel, 36 | which contains invite links. 37 | """ 38 | 39 | channel = "<#273547351929520129>" 40 | link = "https://discordapp.com/channels/273534239310479360/273547351929520129/288101969980162049" 41 | await ctx.reply(f"Invite links are provided in {channel}\n{link}") 42 | 43 | @commands.command() 44 | async def cleanup(self, ctx: commands.Context, limit=None): 45 | """Deletes the bot's messages for cleanup. 46 | You can specify how many messages to look for. 47 | """ 48 | 49 | limit = limit or 100 50 | 51 | def is_me(m): 52 | return m.author.id == self.bot.user.id 53 | 54 | deleted = await ctx.channel.purge(limit=limit, check=is_me) 55 | await ctx.reply(f"Deleted {len(deleted)} message(s)", delete_after=5) 56 | await ctx.message.add_reaction(self.bot.emoji.ok) 57 | 58 | @commands.command() 59 | async def source(self, ctx: commands.Context): 60 | """Links to the bot GitHub repo.""" 61 | 62 | await ctx.reply("https://github.com/ivandardi/RustbotPython") 63 | 64 | @commands.command(aliases=["banne"]) 65 | async def ban(self, ctx: commands.Context, member: discord.Member): 66 | """Bans another person.""" 67 | await ctx.reply( 68 | f"Banned user {member.display_name} <:ferrisBanne:419884768256327680>" 69 | ) 70 | await ctx.message.add_reaction(self.bot.emoji.ok) 71 | 72 | async def cog_command_error(self, ctx: commands.Context, error): 73 | await ctx.message.clear_reactions() 74 | await ctx.message.add_reaction("❌") 75 | 76 | 77 | def setup(bot): 78 | bot.add_cog(Commands(bot)) 79 | -------------------------------------------------------------------------------- /bot/cogs/owner.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from bot import RustBot 5 | 6 | 7 | class Owner(commands.Cog): 8 | """Admin-only commands that make the bot dynamic.""" 9 | 10 | def __init__(self, bot: RustBot): 11 | self.bot = bot 12 | 13 | @commands.command() 14 | @commands.is_owner() 15 | async def close(self, ctx: commands.Context): 16 | """Closes the bot safely. Can only be used by the owner.""" 17 | await self.bot.logout() 18 | 19 | @commands.command() 20 | @commands.is_owner() 21 | async def status(self, ctx: commands.Context, *, status: str): 22 | """Changes the bot's status. Can only be used by the owner.""" 23 | await self.bot.change_presence(activity=discord.Game(name=status)) 24 | 25 | @commands.command(name="reload") 26 | @commands.is_owner() 27 | async def _reload(self, ctx, *, ext: str = None): 28 | """Reloads a module. Can only be used by the owner.""" 29 | 30 | if ext: 31 | self.bot.unload_extension(ext) 32 | self.bot.load_extension(ext) 33 | else: 34 | for m in self.bot.custom_extensions: 35 | self.bot.unload_extension(m) 36 | self.bot.load_extension(m) 37 | 38 | await ctx.message.add_reaction(self.bot.emoji.ok) 39 | 40 | 41 | def setup(bot): 42 | bot.add_cog(Owner(bot)) 43 | -------------------------------------------------------------------------------- /bot/guild_caches.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import discord 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | 8 | class GuildEmojis: 9 | def __init__(self, emoji_config, discord_emojis): 10 | for emoji_label, emoji_name in emoji_config.items(): 11 | emoji = discord.utils.get(discord_emojis, name=emoji_name) 12 | 13 | if not emoji: 14 | log.info("Emoji %s not loaded.", emoji_name) 15 | continue 16 | 17 | log.info("Emoji %s loaded.", emoji_name) 18 | setattr(self, emoji_label, emoji) 19 | 20 | 21 | class GuildRoles: 22 | def __init__(self, role_config, discord_roles): 23 | for role_label, role_id in role_config.items(): 24 | role = discord.utils.get(discord_roles, id=role_id) 25 | 26 | if not role: 27 | log.info("Role %s not loaded.", role_label) 28 | continue 29 | 30 | log.info("Role %s loaded.", role_label) 31 | setattr(self, role_label, role) 32 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | "bot.cogs.commands", 4 | "bot.cogs.owner" 5 | ], 6 | "prefixes": [ 7 | "??" 8 | ], 9 | "guild_id": 273534239310479360, 10 | "emojis": { 11 | "ok": "rustOk" 12 | }, 13 | "roles": { 14 | "rustacean": 319953207193501696 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.7.4 2 | discord.py==1.7.3 3 | -------------------------------------------------------------------------------- /runner.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | import logging 4 | import os 5 | 6 | import discord 7 | from discord.ext import commands 8 | 9 | from bot import RustBot 10 | 11 | 12 | @contextlib.contextmanager 13 | def setup_logging(): 14 | logger = logging.getLogger("bot") 15 | 16 | logger.setLevel(logging.INFO) 17 | 18 | try: 19 | dt_fmt = "%Y-%m-%d %H:%M:%S" 20 | formatter = logging.Formatter( 21 | "[{asctime}] [{levelname:<7}] {name}: {message}", dt_fmt, style="{" 22 | ) 23 | 24 | file_handler = logging.FileHandler( 25 | filename="logging.log", encoding="utf-8", mode="w" 26 | ) 27 | file_handler.setFormatter(formatter) 28 | 29 | stream_handler = logging.StreamHandler() 30 | stream_handler.setFormatter(formatter) 31 | 32 | logger.addHandler(file_handler) 33 | logger.addHandler(stream_handler) 34 | 35 | yield 36 | finally: 37 | handlers = logger.handlers[:] 38 | for handler in handlers: 39 | handler.close() 40 | logger.removeHandler(handler) 41 | 42 | 43 | def main(): 44 | with open("config.json") as f: 45 | config = json.load(f) 46 | 47 | intents = discord.Intents.default() 48 | intents.members = True 49 | 50 | with setup_logging(): 51 | bot = RustBot( 52 | command_prefix=commands.when_mentioned_or(*config["prefixes"]), 53 | config=config, 54 | intents=intents, 55 | ) 56 | bot.run(os.environ["DISCORD_TOKEN"]) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | --------------------------------------------------------------------------------