diff --git a/.gitignore b/.gitignore index 64ecfe3..2984dac 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,141 @@ discordia.log gateway.json init.sh .env -*.pyc \ No newline at end of file + +__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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__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/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3cb4716 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "discordslashcommands"] + path = discordslashcommands + url = https://github.com/mactul/discordslashcommands diff --git a/discordslashcommands b/discordslashcommands new file mode 160000 index 0000000..0b2454e --- /dev/null +++ b/discordslashcommands @@ -0,0 +1 @@ +Subproject commit 0b2454ee71675d32116c6584625d26262b46ff5e diff --git a/requirements.txt b/requirements.txt index 88d55d1..6c336d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ aiohttp==3.7.4.post0 -aiotfm==1.4.0rc0 +aiotfm>=1.4.2 async-timeout==3.0.1 -discord-py-slash-command==1.1.2 +discord-components==1.1.4 discord.py==1.7.1 multidict==5.1.0 w3lib==1.22.0 diff --git a/script.bash b/script.bash index 1277d7d..477be20 100755 --- a/script.bash +++ b/script.bash @@ -1,3 +1,8 @@ +cd discordslashcommands +pip install -r requirements.txt +python setup.py install +cd .. + function run() { python src/main.py || run } diff --git a/src/bots/Discord.py b/src/bots/Discord.py index 9c16f3b..b8d6129 100644 --- a/src/bots/Discord.py +++ b/src/bots/Discord.py @@ -1,11 +1,17 @@ +import asyncio +import json +import os +import random import re +from datetime import datetime, timedelta + import discord -import os -import utils -import json +import discordslashcommands as slash import requests - +import utils from data import data +from discord_components import Button, DiscordComponents, Select, SelectOption + from bots.cmd_handler import commands WANDBOX_ENDPOINT = "https://wandbox.org/api" @@ -26,6 +32,7 @@ def __init__(self): async def on_ready(self): print("[INFO][DISCORD] Client is ready!") + await asyncio.sleep(3) self.main_guild = self.get_guild(data["guild"]) self.data_channel = self.get_guild(data["data_guild"]).get_channel(data["channels"]["data"]) @@ -38,9 +45,19 @@ async def on_ready(self): self.mod_data = await self.data_channel.fetch_message(data["data"]["mod"]) self.mod_data = json.loads(self.mod_data.content[7:-3]) + self.slash = {}#slash.Manager(self) + DiscordComponents(self) + + await self.start_period_tasks() + async def on_message(self, message): + + # temporary code + if (str(message.channel.id) == os.getenv("GRAVEYARD")) and (self.main_guild.get_role(data["roles"]["mafia_dead"]) in message.author.roles): + await self.get_channel(int(os.getenv("MEDIUM_CHAT"))).send(":speaking_head: **[**<@{}>**]** `{}`".format(message.author.id, message.content)) + if message.content.startswith(">"): - content = re.match(r"^>\s*(.+)", message.content).group(1) + content = re.match(r"^>\s*((.|\n)*)", message.content).group(1) args = re.split(r"\s+", content) if args[0] in commands and commands[args[0]]["discord"]: @@ -90,6 +107,47 @@ async def on_message(self, message): await message.reply(embed = discord.Embed.from_dict(res)) + elif self.user.id in message.raw_mentions: + fact = requests.get("https://uselessfacts.jsph.pl/random.md?language=en", headers = { "User-Agent": "Seniru" }).text + await message.reply(embed = discord.Embed.from_dict({ + "title": "{}! Wanna hear a fact? :bulb:".format(random.choice([ + "Hi", "Hello", "Howdy", "Hola", "Yo", "Wassup", "Hola", "Namasthe", "Hi there", "Greetings", + "What's going on", "How's everything", "Good to see you", "Great to see you", "Nice to see you", + "Saluton", "What's new", "How are you feeling today","Hey there" + ])), + "description": fact, + "color": 0x2987ba + })) + + async def on_interaction(self, member, interaction): + await interaction.end(content = "** **") + cmd_name = interaction.command.name + if cmd_name in commands and commands[cmd_name]["discord"]: + cmd = commands[cmd_name] + interaction.author = self.main_guild.get_member(interaction._member_data["user"]["id"]) + interaction.member = interaction.author + if cmd["allowed_roles"]: + for role in cmd["allowed_roles"]: + if self.main_guild.get_role(role) in interaction.member.roles: + break + else: + return await interaction.channel.send(embed = discord.Embed.from_dict({ + "title": ":x: Missing permissions", + "description": "You need 1 of the following roles to use this command: \n{}".format( + ", ".join(list(map(lambda role: "<@&{}>".format(role), cmd["allowed_roles"]))) + ), + "color": 0xcc0000 + })) + interaction.reply = self.main_guild.get_channel(interaction.channel.id).send + interaction.send = self.main_guild.get_channel(interaction.channel.id).send + interaction.options = list(map(lambda o: o.value, interaction.command.options)) + interaction.mentions = list( + map( + lambda m: self.main_guild.get_member(int(re.match(r".*?(\d+).*", m)[1])), + filter(lambda o: re.match(r"^<@!?(\d+)>$", o), interaction.options) + )) + await cmd["f"](interaction.options, interaction, self) + async def on_member_join(self, member): error = False try: @@ -133,6 +191,11 @@ async def on_member_update(self, before, after): await after.add_roles(rank_role) + async def on_error(self, evt, *args, **kwargs): + import sys + exe_type, val, traceback = sys.exc_info() + await self.get_channel(data["channels"]["tribe_chat"]).send(f"<@!522972601488900097> `[ERR][DISCORD@evt_{evt}]` ```py\n{exe_type.__name__}@{traceback.tb_frame.f_code.co_filename}-{traceback.tb_lineno}: {val}```") + async def send_verification_key(self, member): key = utils.generate_random_key(member.id) await member.send(f"Here's your verification key! `{key}\n`Whisper the following to Wtal#5272 (`/c Wtal#5272`) to get verified\n") @@ -163,6 +226,33 @@ async def update_mod_data(self): ``` """.format(json.dumps(self.mod_data))) + async def start_period_tasks(self): + print("[INFO] Checking for periodic tasks...") + # check qotd + await commands["qotd"]["f"](["ask"], None, self) + # other daily tasks + last_daily_data = await self.data_channel.fetch_message(data["data"]["daily"]) + now = datetime.now() + if now > datetime.fromtimestamp(float(last_daily_data.content)) + timedelta(days=1): + for task in (("bday", []), ("stats", [])): + try: + await commands[task[0]]["f"](task[1], None, self) + except Exception as e: + await self.main_guild.get_channel(data["data"]["channels"]["admin"]).send( + "<@!522972601488900097> [DAILY TASK FAILURE|{}] `{}`\n```\n{}```" + .format(task[0], e, e.with_traceback())) + await last_daily_data.edit(content=str(now.timestamp())) + await asyncio.sleep(1 * 60 * 5) + await self.start_period_tasks() + + async def set_status(self): + tribe_total = await self.tfm.getTribe(True) + tribe_online = await self.tfm.getTribe(False) + await self.change_presence( + status=discord.Status.online, + activity=discord.Activity(type = discord.ActivityType.playing, name = "{} / {} online!".format(len(tribe_online.members), len(tribe_total.members))) + ) + def search_member(self, name, deep_check=False): if member := self.main_guild.get_member_named(utils.get_discord_nick_format(name)): return member diff --git a/src/bots/Transformice.py b/src/bots/Transformice.py index d10e04d..ce01f19 100644 --- a/src/bots/Transformice.py +++ b/src/bots/Transformice.py @@ -1,15 +1,16 @@ -import aiotfm import asyncio import json import os import re +import aiotfm import data import utils from bots.cmd_handler import commands from bots.commands.mod import kick + class Transformice(aiotfm.Client): def __init__(self, name, password, loop, discord, community=0): super().__init__(community, True, True, loop) @@ -61,7 +62,7 @@ def run(self, block=True): async def on_login_ready(self, online_players, community, country): print(f"[INFO][TFM] Login Ready [{community}-{country}]") - await self.login(self.name, self.password, encrypted=False, room="*#castle") + await self.login(self.name, self.password, encrypted=False, room="*#bolodefchoco") async def on_logged(self, player_id, username, played_time, community, pid): self.pid = pid @@ -74,10 +75,10 @@ async def on_ready(self): "Beep boop! [>.<]", "Howdy or smth.", "Imagine being a bot in a rat game lol", - "Hey why, thanks :P", "Saluton mundo!", "Hello world!" ])) + await self.discord.set_status() async def on_tribe_message(self, author, message): author = utils.normalize_name(author) @@ -93,16 +94,18 @@ async def on_tribe_member_get_role(self, setter, target, role): async def on_tribe_new_member(self, name): name = utils.normalize_name(name) - await self.discord.get_channel(data.data["channels"]["tribe_chat"]).send("> {} just joined the tribe!.".format(utils.normalize_name(name))) + await self.discord.get_channel(data.data["channels"]["tribe_chat"]).send("> {} just joined the tribe!".format(utils.normalize_name(name))) if self.discord.mod_data["blacklist"].get(name): await kick([name], None, self.discord) # passing self.discord is just a hacky approach here return await self.sendTribeMessage(f"{name} is in the blacklist, please do not invite them again!") await self.sendTribeMessage(f"Welcome to 'A place to call home' {name}!") await self.update_member(name) + await self.discord.set_status() async def on_tribe_member_left(self, name): await self.discord.get_channel(data.data["channels"]["tribe_chat"]).send("> {} has left the tribe ;c".format(utils.normalize_name(name))) await self.update_member(name) + await self.discord.set_status() async def on_tribe_member_kicked(self, name, kicker): await self.discord.get_channel(data.data["channels"]["tribe_chat"]).send("> {} has kicked {} out of the tribe!".format( @@ -110,19 +113,26 @@ async def on_tribe_member_kicked(self, name, kicker): utils.normalize_name(name) )) await self.update_member(name) + await self.discord.set_status() async def on_member_connected(self, name): await self.discord.get_channel(data.data["channels"]["tribe_chat"]).send(f"> {utils.normalize_name(name)} just connected!") await self.sendTribeMessage(f"Welcome back {utils.normalize_name(name)}!") + await self.discord.set_status() async def on_member_disconnected(self, name): await self.discord.get_channel(data.data["channels"]["tribe_chat"]).send(f"> {utils.normalize_name(name)} has disconnected!") + await self.discord.set_status() async def on_whisper(self, message): args = re.split(r"\s+", message.content) if (not message.sent) and args[0] in commands and commands[args[0]]["tfm"] and commands[args[0]]["whisper_command"]: await commands[args[0]]["f"](args[1:], message, self) + async def on_error(self, evt, e, *args, **kwargs): + await self.discord.get_channel(data["channels"]["tribe_chat"]).send(f"<@!522972601488900097> `[ERR][TFM@evt_{evt}]` ```py\n{e}```") + + async def update_member(self, target): discord_nick = utils.get_discord_nick_format(utils.normalize_name(target)) member = self.discord.main_guild.get_member_named(discord_nick) diff --git a/src/bots/cmd_handler.py b/src/bots/cmd_handler.py index 5720ee0..018dfe4 100644 --- a/src/bots/cmd_handler.py +++ b/src/bots/cmd_handler.py @@ -1,15 +1,16 @@ -import functools -import utils -import re -import requests import asyncio +import functools import json +import re +from datetime import datetime -from bots import translations - +import requests +import utils +from aiotfm import Packet from data import data from discord import Embed +from bots import translations commands = {} @@ -40,6 +41,7 @@ def wrapper(*args, **kwargs): from .commands import qotd as qhandler + @command(discord = True, allowed_roles = [ data["roles"]["admin"] ]) async def qotd(args, msg, client): if len(args) > 0: @@ -53,6 +55,8 @@ async def qotd(args, msg, client): command(discord = True, allowed_roles = [ data["roles"]["admin"], data["roles"]["mod"] ])(mod.blacklist) command(discord = True, allowed_roles = [ data["roles"]["admin"], data["roles"]["mod"] ])(mod.whitelist) command(discord = True, allowed_roles = [ data["roles"]["admin"], data["roles"]["mod"] ])(mod.warnings) +command(discord = True, allowed_roles = [ data["roles"]["admin"], data["roles"]["mod"] ])(mod.warn) +command(discord = True, allowed_roles = [ data["roles"]["admin"], data["roles"]["mod"] ])(mod.rwarn) command(discord = True, allowed_roles = [ data["roles"]["admin"], data["roles"]["mod"] ])(mod.nick) @command(discord=True) @@ -68,6 +72,24 @@ async def restart(args, msg, client): await msg.reply(":hourglass_flowing_sand: | Restarting...") sys.exit("Restart") +@command(discord=True, allowed_roles = [data["roles"]["admin"]] ) +async def room(args, msg, client): + try: + await client.tfm.joinRoom(" ".join(args)) + room = await client.tfm.wait_for("on_joined_room", timeout=4) + await msg.reply(":white_check_mark: | Joined the room (name: `{}` | community: `{}`)".format(room.name, room.community)) + except Exception as e: + await msg.reply(f":x: | {e}") + +@command(discord=True, allowed_roles = [ data["roles"]["admin"], data["roles"]["event"] ]) +async def setmsg(args, msg, client): + try: + await client.tfm.sendCP(98, Packet().writeUTF(" ".join(args))) + await client.tfm.wait_for("on_raw_cp", lambda tc, packet: tc == 125, 2) + await msg.reply(":white_check_mark: | Changed the message!") + except Exception as e: + await msg.reply(f":x: | Failed to change the message (Error: `{e}`)") + @command(tfm=True, whisper_command=True) async def inv(args, msg, client): await client.recruit(msg.author.username) @@ -272,7 +294,65 @@ async def profile(args, msg, client): tfm_profile.stats.divineModeSaves )} ] + roles = json.loads(requests.get(f"https://cheese.formice.com/api/players/{fName}-{disc}").text)["tfm_roles"] + embed["color"] = ({ + "admin": 0xEB1D51, "mod": 0xBABD2F, "sentinel": 0x2ECF73, "mapcrew": 0x2F7FCC, "module": 0x95D9D6, "funcorp": 0xF89F4B + }).get(roles[0] if roles else ("admin" if disc == "0001" else 0), 0x009D9D) + await msg.reply(embed = Embed.from_dict(embed)) + +@command(discord=True) +async def bday(args, msg, client): + channel = client.main_guild.get_channel(data["channels"]["bday"]) + raw_data = "" + for msg_id in data["data"]["bday"]: + message = await channel.fetch_message(msg_id) + raw_data += message.content + raw_data = re.sub("`", "", raw_data)[20:-86] + today = datetime.now().strftime("%-d %B") + bdays = re.findall("{} - (.+)\n".format(today), raw_data) + if msg is None and len(args) == 0: return + method = msg.reply if msg else client.main_guild.get_channel(data["channels"]["admin"]).send + await method(embed = Embed.from_dict({ + "title": "Today's birthdays :tada:", + "color": 0xccdd33, + "description": "No birthdays today ;c" if len(bdays) == 0 else "• {}".format("\n• ".join(bdays)), + "timestamp": datetime.now().isoformat() + })) + +@command(discord=True) +async def stats(args, msg, client): + res = json.loads(requests.get("https://cheese.formice.com/api/tribes/A%20Place%20to%20Call%20Home").text) + method = msg.reply if msg else client.main_guild.get_channel(data["channels"]["stats"]).send + await method(content = + """:calendar_spiral: **Daily tribe stats `[{}]` <:tribehouse:689470787950084154> **\n> ┗ :medal: **Position:** `{}` + > + > :person_running: **Rounds: ** `{}` + > <:cheese:691158951563362314> **Cheese:** `{}` + > <:p7:836550194380275742> **Firsts:** `{}` + > <:bootcamp:836550195683917834> **Bootcamp:** `{}` + > + > <:shaman:836550192387850251> **Gathered cheese/Normal/Hard/Divine: [** `{}`/`{}`/`{}`/`{}` **]** + """.format( + datetime.now().strftime("%d/%m/%y"), + res["position"], + res["stats"]["normal"]["rounds"], + res["stats"]["normal"]["cheese"], + res["stats"]["normal"]["first"], + res["stats"]["normal"]["bootcamp"], + res["stats"]["shaman"]["cheese"], + res["stats"]["shaman"]["saves_normal"], + res["stats"]["shaman"]["saves_hard"], + res["stats"]["shaman"]["saves_divine"] + )) + +@command(discord=True) +async def test(args, msg, client): + from discord_components import DiscordComponents, Button, Select, SelectOption + + await msg.reply(content="Hello", components=[Button(label="hello")]) + interaction = await client.wait_for("button_click") + print(interaction) + #await interaction. + await interaction.respond(content="ur mom") - embed["image"] = { "url": "http://santanl.000webhostapp.com/badges.php?row_max=18&badges={}".format(",".join([str(x) for x in tfm_profile.badges.keys()])) } - await msg.reply(embed = Embed.from_dict(embed)) diff --git a/src/bots/commands/mod.py b/src/bots/commands/mod.py index 7eb311b..2b27558 100644 --- a/src/bots/commands/mod.py +++ b/src/bots/commands/mod.py @@ -24,6 +24,36 @@ async def whitelist(args, msg, client): return await msg.reply(":angel: | Whitelisted {}".format(args[0])) await msg.reply(":x: | `\"{}\"` is not in the blacklist".format(args[0])) +async def warn(args, msg, client): + if not len(args) >= 2: + return await msg.reply(":x: | Invalid syntax (`>warn `)") + warned = args[0] # no checks because I think mods are intelligent + reason = " ".join(args[1:]) + await client.tfm.whisper(warned, f"You have been warned in your tribe \"A Place to Call Home\"!\nReason: {reason}", True) + if not client.mod_data["warnings"].get(warned): + client.mod_data["warnings"][warned] = [] + client.mod_data["warnings"][warned].append(reason) + await client.update_mod_data() + await msg.reply(f":white_check_mark: | Warned {warned}!") + +async def rwarn(args, msg, client): + if not len(args) >= 2 and not int(args[1]): + return await msg.reply(":x: | Invalid syntax (`>rwarn `)") + target = args[0] + index = int(args[1]) + index -= 1 + warnings = client.mod_data["warnings"].get(target) + if not warnings: + return await msg.reply(":x: | No warnings for this member to remove.") + if index > len(warnings) or index < 0: + return await msg.reply(":x: | Invalid index") + warnings.pop(index) + if len(warnings) == 0: + client.mod_data["warnings"].pop(target) + await client.update_mod_data() + await msg.reply(":white_check_mark: | Removed a warning.") + + async def warnings(args, msg, client): if client.client_type == "Discord": if len(args) != 0 and (warns := client.mod_data["warnings"].get(args[0])): diff --git a/src/bots/commands/qotd.py b/src/bots/commands/qotd.py index f21c702..051782a 100644 --- a/src/bots/commands/qotd.py +++ b/src/bots/commands/qotd.py @@ -38,7 +38,7 @@ async def ask(args, msg, client): qotd_channel = client.get_channel(data["channels"]["qotd"]) if force or cooldown_over: if len(client.questions["questions"]) > 0: - await qotd_channel.send(embed = Embed.from_dict({ + await qotd_channel.send(content = "<@&742418187198660719>", embed = Embed.from_dict({ "title": "QOTD #{}".format(client.questions["index"]), "description": client.questions["questions"].pop(0), "color": 0x2987ba diff --git a/src/data.py b/src/data.py index 77cabc8..53920ce 100644 --- a/src/data.py +++ b/src/data.py @@ -5,14 +5,19 @@ "tribe_chat": 671247476782661632, "lobby": 678770711791140875, "data": 718723565167575061, - "qotd": 626192940364202004 + "qotd": 626192940364202004, + "bday": 592742600058994688, + "admin": 592341953547337739, + "stats": 575328568008114176 }, "roles": { "admin": 655909026101723147, "mod": 831316968660795403, + "event": 831875623118045251, "cmder": 702120436279934996, "verified": 677396391189807104, - "member": 678097505975533599 + "member": 678097505975533599, + "mafia_dead": 847742079177719828 }, "ranks": { "Passer-by": 571736272163438650, @@ -44,6 +49,8 @@ "data": { "ccmds": 718746213385502772, "qotd": 718728423475904562, - "mod": 718747955557040158 + "mod": 718747955557040158, + "daily": 719816851781058670, + "bday": [ 592742900509704205, 592743169863581713 ] } } diff --git a/src/main.py b/src/main.py index cbb9975..6744ddd 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,9 @@ import os +#import sys import asyncio from bots.Transformice import Transformice +#sys.path.append("discordslashcommands") from bots.Discord import Discord loop = asyncio.get_event_loop()