1
Fork 0
mirror of https://github.com/RGBCube/minearchy-bot synced 2025-07-27 17:07:45 +00:00

Initial commit

This commit is contained in:
RGBCube 2022-06-06 08:51:00 +00:00
commit d2f19f0e8a
11 changed files with 581 additions and 0 deletions

163
.gitignore vendored Normal file
View file

@ -0,0 +1,163 @@
# Configs
config.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

1
.replit Normal file
View file

@ -0,0 +1 @@
run = "pip install -Ur requirements.txt && python bot.py"

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
opyright (c) 2022-present RGBCube & Minearchy Team
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

23
README.md Normal file
View file

@ -0,0 +1,23 @@
# Minearchy Bot
This is a simple bot made for the Minearchy Discord server.
## Commands
### Minecraft Server Related Commands
`=ip [java|bedrock]`: Sends both of the server IPs. If the version is specified, it sends that versions IP.
`=status`: Sends the Minecraft servers player count and latency(ping).
`=store`: Sends a link to the store.
`=wiki`: Sends a link to the wiki.
### Miscellanios Commands
`=hello`: Hello!
`=help`: Sends bot help.
`=info`: Sends info about the bot. This is the bots Python runtime version and uptime.
`=ping`: Sends the bots latency.

97
bot.py Normal file
View file

@ -0,0 +1,97 @@
import discord
import time
import aiohttp
from discord.ext import commands
import os
import json
import asyncio
from mcstatus import MinecraftServer
import traceback
import pathlib
class MinearchyBot(commands.Bot):
session: aiohttp.ClientSession
log_webhook: discord.Webhook
up_ts: float
embed_color = 0x39FF14
def __init__(self, token: str, webhook_url: str, /) -> None:
ip = "play.landsofminearchy.com"
self.mc_server = MinecraftServer.lookup(ip)
self.mc_server.ip = ip
self.mc_server.bedrock_ip = "bedrock.landsofminearchy.com"
self.token = token
self.webhook_url = webhook_url
intents = discord.Intents(
guilds=True,
members=True,
messages=True,
message_content=True,
)
stuff_to_cache = discord.MemberCacheFlags.from_intents(intents)
super().__init__(
command_prefix="=",
owner_ids=set([512640455834337290]),
intents=intents,
case_insensitive=True,
allowed_mentions=discord.AllowedMentions.none(),
member_cache_flags=stuff_to_cache,
max_messages=1000,
strip_after_prefix=True,
help_attrs=dict(
brief="Sends help.",
help="Sends all the commands of the bot, or help of a specific command and module.",
),
)
async def on_ready(self) -> None:
self.up_ts = time.time()
print(f"\nConnected to Discord!\nUser: {self.user}\nID: {self.user.id}")
await self.log_webhook.send("Bot is now online!")
async def load_extensions(self) -> None:
for fn in map(
lambda file_path: str(file_path).replace("/", ".")[:-3],
pathlib.Path("./cogs").rglob("*.py"),
):
try:
await self.load_extension(fn)
print(f"Loaded {fn}")
except (commands.ExtensionFailed, commands.NoEntryPointError):
print(f"Couldn't load {fn}:\n{traceback.format_exc()}")
def run(self) -> None:
async def runner() -> None:
async with self, aiohttp.ClientSession() as session:
self.session = session
self.log_webhook = discord.Webhook.from_url(
self.webhook_url, session=self.session, bot_token=self.token
)
await self.load_extensions()
await self.start(self.token, reconnect=True)
try:
asyncio.run(runner())
except KeyboardInterrupt:
pass
with open("./config.json") as f:
config = json.load(f)
for key in ["BOT_TOKEN", "WEBHOOK_URL"]:
config.setdefault(key, os.getenv(key))
bot = MinearchyBot(config["BOT_TOKEN"], config["WEBHOOK_URL"])
if os.getenv("USING_REPLIT"):
import webserver
webserver.keep_alive()
bot.run()

54
cogs/error_handler.py Normal file
View file

@ -0,0 +1,54 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from discord.ext import commands
import discord
import sys
import traceback
if TYPE_CHECKING:
from bot import MinearchyBot
class ErrorHandler(commands.Cog):
def __init__(self, bot: MinearchyBot, /) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_command_error(
self, ctx: commands.Context, error: commands.CommandError, /
) -> None:
if hasattr(ctx.command, "on_error"):
return
if cog := ctx.cog:
if cog._get_overridden_method(cog.cog_command_error) is not None:
return
ignored = (commands.CommandNotFound,)
error = getattr(error, "original", error)
if isinstance(error, ignored):
return
elif isinstance(error, commands.NoPrivateMessage):
try:
await ctx.author.send(
f"The commmand `{ctx.command}` cannot be used in DMs."
)
except discord.HTTPException:
pass
elif isinstance(error, commands.MissingPermissions):
await ctx.reply("You can't use this command!")
else:
trace = traceback.format_exception(
type(error), error, error.__traceback__
)
print(f"Ignoring exception in command {ctx.command}:\n{trace}")
self.bot.log_webhook.send(f"<@512640455834337290>```{trace}```")
async def setup(bot: MinearchyBot, /) -> None:
await bot.add_cog(ErrorHandler(bot))

81
cogs/mc_server.py Normal file
View file

@ -0,0 +1,81 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from discord.ext import commands
import discord
if TYPE_CHECKING:
from bot import MinearchyBot
class MinecraftServer(
commands.Cog,
name="Minecraft Server",
description="Utilites for the Minecraft server.",
):
def __init__(self, bot: MinearchyBot, /) -> None:
self.bot = bot
@commands.group(
invoke_without_command=True,
brief="Sends the server IP.",
help="Sends the server IP.",
)
async def ip(self, ctx: commands.Context, /) -> None:
await ctx.reply(
f"Java edition IP: `{self.bot.mc_server.ip}`\nBedrock edition IP: `{self.bot.mc_server.bedrock_ip}`"
)
@ip.command(brief="Sends the Java edition IP.", help="Sends the Java edition IP.")
async def java(self, ctx: commands.Context, /) -> None:
await ctx.reply(
f"The IP to connect on Minecraft Java edition is `{self.bot.mc_server.ip}`"
)
@ip.command(
brief="Sends the Bedrock edition IP.", help="Sends the Bedrock edition IP."
)
async def bedrock(self, ctx: commands.Context, /) -> None:
await ctx.reply(
f"The IP to connect on Minecraft Bava edition is `{self.bot.mc_server.ip}`"
)
@commands.command(
brief="Shows information about the Minecraft server.",
help="Shows the total player count, the Minecraft server IP and the server latency.",
)
async def status(self, ctx: commands.Context, /) -> None:
server = self.bot.mc_server
status = server.status()
await ctx.reply(
f"The server with the IP `{server.ip}` has {status.players.online} "
f"players and responded in `{int(status.latency)}ms`"
)
@commands.command(
brief="Sends the link to the wiki.", help="Sends the link to the wiki."
)
async def wiki(self, ctx: commands.Context, /) -> None:
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Go to the wiki!", url="https://www.landsofminearchy.com/wiki"
)
)
await ctx.reply(view=view)
@commands.command(
brief="Sends the link to the store.", help="Sends the link to the store."
)
async def store(self, ctx: commands.Context, /) -> None:
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Go to the store!", url="https://www.landsofminearchy.com/store"
)
)
await ctx.reply(view=view)
async def setup(bot: MinearchyBot) -> None:
await bot.add_cog(MinecraftServer(bot))

116
cogs/misc.py Normal file
View file

@ -0,0 +1,116 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from discord.ext import commands
import datetime
from collections import defaultdict, deque
import discord
from discord.utils import escape_markdown as es_md
import platform
import time
if TYPE_CHECKING:
from bot import MinearchyBot
class Miscellanious(
commands.Cog,
name="Miscellanious",
description="Various utilities.",
):
def __init__(self, bot: MinearchyBot, /) -> None:
self.bot = bot
self.bot.help_command.cog = self
self.sniped = defaultdict(deque)
def cog_unload(self) -> None:
self.bot.help_command.cog = None
self.bot.help_command.hidden = True
@commands.command(brief="Sends the bots ping.", help="Sends the bots ping.")
async def ping(self, ctx: commands.Context, /) -> None:
ts = time.time()
message = await ctx.reply("Pong!")
ts = time.time() - ts
await message.edit(content=f"Pong! `{int(ts * 1000)}ms`")
@commands.command(
brief="Sends info about the bot.", help="Sends info about the bot."
)
async def info(self, ctx: commands.Context, /) -> None:
embed = discord.Embed(title="Bot Info", color=self.bot.embed_color)
embed.add_field(
name="Python Version", value=f"```v{platform.python_version()}```"
)
embed.add_field(
name="Uptime",
value=f"```{datetime.timedelta(seconds=int(time.time() - self.bot.up_ts))}```",
)
await ctx.reply(embed=embed)
@commands.command(brief="Hello!", help="Hello!")
async def hello(self, ctx: commands.Context, /) -> None:
await ctx.reply(f"Hi {es_md(ctx.author.name)}, yes the bot is running :)")
@commands.Cog.listener()
async def on_message_delete(self, message: discord.Message, /) -> None:
if not message.guild:
return
logs = self.sniped[message.channel.id]
logs.appendleft((message, int(time.time())))
while len(logs) > 5:
logs.pop()
@commands.command(
brief="Sends the latest deleted messages.",
help="Sends the last 5 deleted messages in a specified channel.\nIf the channel isn't specified, it uses the current channel.",
)
@commands.has_permissions(
manage_messages=True
) # needs to be able to delete messages to run the command
async def snipe(
self, ctx: commands.Context, channel: discord.TextChannel = None, /
) -> None:
if channel is None:
channel = (
ctx.channel
) # i am not checking if the channel is in the current server, since this bot is for a private server
logs = self.sniped[channel.id]
if not logs:
await ctx.reply(
f"There are no messages to be sniped in {'this channel.' if channel.id == ctx.channel.id else channel.mention}"
)
return
embed = discord.Embed(
title=f"Showing last 5 deleted messages for {'the current channel' if ctx.channel.id == channel.id else channel}",
description="The lower the number is, the more recent it got deleted.",
color=self.bot.embed_color,
)
zwsp = "\uFEFF"
for i, log in reversed(list(enumerate(logs))):
message, ts = log
embed.add_field(
name=str(i) + (" (latest)" if not i else ""),
value=f"""
Author: {message.author.mention} (ID: {message.author.id}, Plain: {discord.utils.escape_markdown(str(message.author))})
Deleted at: <t:{ts}:F> (Relative: <t:{ts}:R>)
Content:
```
{message.content.replace('`', f'{zwsp}`{zwsp}')}
```
""", # zero-width spaces, or it will break.
inline=False,
)
await ctx.reply(embed=embed)
async def setup(bot: MinearchyBot) -> None:
await bot.add_cog(Miscellanious(bot))

7
config.example.json Normal file
View file

@ -0,0 +1,7 @@
{
"BEDROCK_IP": "bedrock.landsofminearchy.com",
"JAVA_IP": "play.landsofminearchy.com",
"BOT_PREFIX": "=",
"BOT_TOKEN": "xxxxxxxxxxxxxxxxxxxxxxxxx(also env files work with this too)",
"WEBHOOK_URL": "xxxxxxxxxxxxxxxxxxxxxxx(also env files work with this too)"
}

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
discord.py @ git+https://github.com/Rapptz/discord.py
mcstatus

17
webserver.py Normal file
View file

@ -0,0 +1,17 @@
from flask import Flask
from threading import Thread
app = Flask(__name__)
@app.route("/")
def root() -> str:
return "Online!"
def run() -> None:
app.run(host="0.0.0.0", port=8080)
def keep_alive() -> None:
Thread(target=run).start()