diff --git a/.env.template b/.env.template index 72a9b32e4b11608b725539e44edc30826f3a68e2..28a6e9f18bd18860af253353208391ddb4967d56 100644 --- a/.env.template +++ b/.env.template @@ -5,6 +5,7 @@ DISCORD_ACTIVITY=<What should be shown, Bot is playing right now> DISCORD_GITHUB_USER=<Github username used to create issues> DISCORD_GITHUB_TOKEN=<Github personal access token, can be created in Settings > Developer settings > Personal access tokens (repo scope neccessary)> DISCORD_GITHUB_ISSUE_URL=<URL of Github API to create Issues in a repo> +DISCORD_PROD=<True, if running in an productive environment, otherwise False> # IDs DISCORD_OWNER=<ID of Server owner> @@ -35,6 +36,8 @@ DISCORD_IDEE_CHANNEL=<ID of Channel, where bot ideas can be submitted> DISCORD_IDEE_EMOJI=<ID of Idee Emoji, used for reactions> DISCORD_CALMDOWN_ROLE=<ID of "Calmdown" role> DISCORD_BOTUEBUNGSPLATZ_CHANNEL=<ID of 'bot-übungsplatz' channel> +DISCORD_ELM_STREET_CHANNEL=<ID of elm street channel> +DISCORD_HALLOWEEN_CATEGORY=<ID of Halloween category> # JSON Files DISCORD_ROLES_FILE=<File name for roles JSON file> diff --git a/cogs/elm_street.py b/cogs/elm_street.py new file mode 100644 index 0000000000000000000000000000000000000000..d3e693d7dedf25e81ab8a6393c0cdef4108099c0 --- /dev/null +++ b/cogs/elm_street.py @@ -0,0 +1,732 @@ +import json +import os +from asyncio import sleep +from copy import deepcopy +from random import SystemRandom +from typing import Union + +import disnake +from disnake import ApplicationCommandInteraction, ButtonStyle +from disnake.ext import commands, tasks +from dotenv import load_dotenv + +from utils import send_dm + +load_dotenv() + + +def get_player_from_embed(embed: disnake.Embed): + return embed.description.split()[0].strip("<@!>") + + +def calculate_sweets(event): + sweets_min = event.get("sweets_min") + sweets_max = event.get("sweets_max") + + if sweets_min and sweets_max: + return SystemRandom().randint(sweets_min, sweets_max) + + return None + + +def calculate_courage(event): + courage_min = event.get("courage_min") + courage_max = event.get("courage_max") + + if courage_min and courage_max: + return SystemRandom().randint(courage_min, courage_max) + + return None + + +ShowOption = commands.option_enum(["10", "all"]) + + +def get_doors_visited(group): + if doors_visited := group.get("doors_visited"): + return doors_visited + else: + doors_visited = [] + group["doors_visited"] = doors_visited + return doors_visited + + +class ElmStreet(commands.Cog): + def __init__(self, bot): + + self.max_courage = 100 + self.min_courage = 20 + self.min_group_courage = 20 + + self.inc_courage_step = 10 + + self.bot = bot + self.groups = {} + self.players = {} + self.story = {} + self.load() + self.elm_street_channel_id = int(os.getenv("DISCORD_ELM_STREET_CHANNEL")) + self.halloween_category_id = int(os.getenv("DISCORD_HALLOWEEN_CATEGORY")) + self.bot.view_manager.register("on_join", self.on_join) + self.bot.view_manager.register("on_joined", self.on_joined) + self.bot.view_manager.register("on_start", self.on_start) + self.bot.view_manager.register("on_stop", self.on_stop) + self.bot.view_manager.register("on_story", self.on_story) + self.bot.view_manager.register("on_leave", self.on_leave) + + self.increase_courage.start() + + def load(self): + with open("data/elm_street_groups.json", "r") as groups_file: + self.groups = json.load(groups_file) + with open("data/elm_street_players.json", "r") as players_file: + self.players = json.load(players_file) + with open("data/elm_street_story.json", "r") as story_file: + self.story = json.load(story_file) + + def save(self): + with open("data/elm_street_groups.json", "w") as groups_file: + json.dump(self.groups, groups_file) + with open("data/elm_street_players.json", "w") as players_file: + json.dump(self.players, players_file) + + @commands.slash_command(name="leaderboard", + description="Zeigt das Leaderboard der Elm Street Sammlerinnen-Gemeinschaft an.", + guild_ids=[int(os.getenv('DISCORD_GUILD'))]) + async def cmd_leaderboard(self, interaction: ApplicationCommandInteraction, show: ShowOption = "10"): + embed = await self.leaderboard(all=show) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @commands.slash_command(name="group-stats", + description="Zeigt die aktuelle Gruppenstatistik an.", + guild_ids=[int(os.getenv('DISCORD_GUILD'))]) + async def cmd_group_stats(self, interaction: ApplicationCommandInteraction): + thread_id = interaction.channel_id + if str(thread_id) in self.groups.keys(): + embed = await self.get_group_stats_embed(interaction.channel_id) + await interaction.response.send_message(embed=embed) + else: + await interaction.response.send_message("Gruppenstatistiken können nur in Gruppenthreads ausgegeben werden." + , ephemeral=True) + + @commands.slash_command(name="leave-group", + description="Hiermit verlässt du deine aktuelle Gruppe", + guild_ids=[int(os.getenv('DISCORD_GUILD'))]) + async def cmd_leave_group(self, interaction: ApplicationCommandInteraction): + thread_id = interaction.channel_id + player_id = interaction.author.id + if group := self.groups.get(str(thread_id)): + if not player_id == group['owner']: + if player_id in group.get('players'): + self.leave_group(thread_id, player_id) + await interaction.response.send_message(f"<@{player_id}> hat die Gruppe verlassen.") + else: + await interaction.response.send_message( + "Du bist garnicht Teil dieser Gruppe.", ephemeral=True) + else: + await interaction.response.send_message( + "Du darfst deine Gruppe nicht im Stich lassen. Als Gruppenleiterin kannst du sie höchstens beenden, " + "aber nicht verlassen.", ephemeral=True) + else: + await interaction.response.send_message("Dieses Kommando kann nur in einem Gruppenthread ausgeführt werden." + , ephemeral=True) + + @commands.slash_command(name="stats", + description="Zeigt deine persönliche Statistik an.", + guild_ids=[int(os.getenv('DISCORD_GUILD'))]) + async def cmd_stats(self, interaction: ApplicationCommandInteraction): + embed = self.get_personal_stats_embed(interaction.author.id) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @commands.slash_command(name="start-group", + description="Erstelle eine Gruppe für einen Streifzug durch die Elm Street", + guild_ids=[int(os.getenv('DISCORD_GUILD'))]) + async def cmd_start_group(self, interaction: ApplicationCommandInteraction, name: str): + author = interaction.author + category = await self.bot.fetch_channel(self.halloween_category_id) + channel = await self.bot.fetch_channel(self.elm_street_channel_id) + channel_type = None if self.bot.is_prod() else disnake.ChannelType.public_thread + + player = self.get_player(author) + + if interaction.channel == channel: + if self.can_play(player): + if not self.is_playing(author.id): + if player["courage"] >= 50: + thread = await channel.create_thread(name=name, auto_archive_duration=1440, type=channel_type) + voice_channel = await category.create_voice_channel(name) + await voice_channel.set_permissions(interaction.author, view_channel=True, connect=True) + + await thread.send( + f"Hallo {author.mention}. Der Streifzug deiner Gruppe durch die Elm-Street findet " + f"in diesem Thread statt. Sobald deine Gruppe sich zusammen gefunden hat, kannst " + f"du über einen Klick auf den Start Button eure Reise starten.\n\n" + f"Für das volle Gruselerlebnis könnt ihr euch während des Abenteuers gegenseitig " + f"Schauermärchen in eurem Voice Channel {voice_channel.mention} erzählen.", + view=self.get_start_view()) + + await interaction.response.send_message(self.get_invite_message(author), + view=self.get_join_view(thread.id)) + + message = await interaction.original_message() + self.groups[str(thread.id)] = {"message": message.id, "players": [author.id], + "owner": author.id, + "requests": [], 'stats': {'sweets': 0, 'courage': 0, 'doors': 0}, + "voice_channel": voice_channel.id} + self.save() + else: + await interaction.response.send_message( + "Du fühlst dich derzeit noch nicht mutig genug, um aus Süßigkeitenjagd zu gehen. Warte, bis deine Mutpunkte wieder mindestens 50 betragen. Den aktuellen Stand deiner Mutpunkte kannst du über /stats prüfen.", + ephemeral=True) + else: + await interaction.response.send_message( + "Es tut mir leid, aber du kannst nicht an mehr als einer Jagd gleichzeitig teilnehmen. " + "Beende erst das bisherige Abenteuer, bevor du dich einer neuen Gruppe anschließen kannst.", + ephemeral=True) + else: + await interaction.response.send_message( + "Du zitterst noch zu sehr von deiner letzten Runde. Ruh dich noch ein wenig aus bevor du weiter spielst.", + ephemeral=True) + else: + await interaction.response.send_message( + f"Gruppen können nur in <#{self.elm_street_channel_id}> gestartet werden.", + ephemeral=True) + + async def on_join(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + player = self.get_player(interaction.author) + + try: + if group := self.groups.get(str(value)): + requests = [r['player'] for r in group.get('requests')] + if interaction.author.id not in requests: + if self.can_play(player): + if not self.is_already_in_this_group(interaction.author.id, interaction.message.id): + if not self.is_playing(interaction.author.id): + if player["courage"] >= 50: + thread = await self.bot.fetch_channel(value) + msg = await self.bot.view_manager.confirm(thread, "Neuer Rekrut", + f"{interaction.author.mention} würde sich gerne der Gruppe anschließen.", + fields=[{'name': 'aktuelle Mutpunkte', + 'value': self.get_courage_message( + player)}], + custom_prefix="rekrut", + callback_key="on_joined") + player.get('messages').append({'id': msg.id, 'channel': thread.id}) + group.get('requests').append({'player': interaction.author.id, 'id': msg.id}) + self.save() + else: + await interaction.response.send_message( + "Du fühlst dich derzeit noch nicht mutig genug, um aus Süßigkeitenjagd zu gehen. Warte, bis deine Mutpunkte wieder mindestens 50 betragen. Den aktuellen Stand deiner Mutpunkte kannst du über /stats prüfen.", + ephemeral=True) + else: + await interaction.response.send_message( + "Es tut mir leid, aber du kannst nicht an mehr als einer Jagd gleichzeitig teilnehmen. " + "Beende erst das bisherige Abenteuer, bevor du dich einer neuen Gruppe anschließen kannst.", + ephemeral=True) + else: + await interaction.response.send_message( + "Du bist schon Teil dieser Gruppe! Schau doch mal in eurem " + "Thread vorbei.", ephemeral=True) + else: + await interaction.response.send_message( + "Du zitterst noch zu sehr von deiner letzten Runde. Ruh dich noch ein wenig aus bevor du weiter spielst.", + ephemeral=True) + else: + await interaction.response.send_message( + "Für diese Gruppe hast du dich schon beworben. Warte auf eine Entscheidung des Gruppenleiters.", + ephemeral=True) + except Exception as e: + await interaction.response.send_message( + "Ein Fehler ist aufgetreten. Ãœberprüfe bitte, ob du der richtigen Gruppe beitreten wolltest. " + "Sollte der Fehler erneut auftreten, sende mir (Boty McBotface) bitte eine Direktnachricht.", + ephemeral=True) + + if not interaction.response.is_done(): + await interaction.response.send_message("Dein Wunsch, der Gruppe beizutreten wurde weitergeleitet.", + ephemeral=True) + + async def on_joined(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + player_id = int(get_player_from_embed(interaction.message.embeds[0])) + thread_id = interaction.channel_id + owner_id = self.groups.get(str(thread_id)).get('owner') + + if interaction.author.id == owner_id: + if group := self.groups.get(str(interaction.channel_id)): + if value: + if not self.is_playing(player_id): + group["players"].append(player_id) + + # Request-Nachrichten aus allen Threads und aus players löschen + for thread_id in self.groups: + requests = self.groups.get(str(thread_id)).get('requests') + for request in requests: + if request['player'] == player_id: + thread = await self.bot.fetch_channel(int(thread_id)) + message = await thread.fetch_message(request['id']) + player = await self.bot.fetch_user(player_id) + voice_channel = await self.bot.fetch_channel(group["voice_channel"]) + await voice_channel.set_permissions(player, view_channel=True, connect=True) + await message.delete() + self.delete_message_from_player(player_id, request['id']) + requests.remove(request) + + await interaction.message.channel.send( + f"<@!{player_id}> ist jetzt Teil der Crew! Herzlich willkommen.", + view=self.get_leave_view()) + self.save() + else: + await self.deny_join_request(group, interaction.message, player_id) + else: + await interaction.response.send_message("Nur die Gruppenerstellerin kann User annehmen oder ablehnen.", + ephemeral=True) + + async def on_start(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + thread_id = interaction.channel_id + owner_id = self.groups.get(str(thread_id)).get('owner') + if interaction.author.id == owner_id: + if group := self.groups.get(str(interaction.channel.id)): + + elm_street_channel = await self.bot.fetch_channel(self.elm_street_channel_id) + group_message = await elm_street_channel.fetch_message(group["message"]) + await group_message.delete() + await interaction.message.edit(view=self.get_start_view(disabled=True)) + + if value: # auf Start geklickt + await self.deny_open_join_requests(thread_id, group) + random_player = await self.bot.fetch_user(SystemRandom().choice(group.get('players'))) + bags = ["einen Putzeimer, der", "eine Plastiktüte von Aldi, die", "einen Einhorn-Rucksack, der", + "eine Reisetasche, die", "eine Wickeltasche mit zweifelhaftem Inhalt, die", + "einen Rucksack, der", "eine alte Holzkiste, die", "einen Leinensack, der", + "einen Müllsack, der", "einen Jutebeutel mit verwaschener gotischer Schrift, die", + "eine blaue Ikea-Tasche, die"] + await interaction.response.send_message( + f"```\nSeid ihr bereit? Taschenlampe am Gürtel, Schminke im Gesicht? Dann kann es losgehen!\n" + f"Doch als ihr gerade in euer Abenteuer starten wollt, fällt {random_player.name} auf, dass ihr euch erst noch Behälter für die erwarteten Süßigkeiten suchen müsst. \nIhr schnappt euch also {SystemRandom().choice(bags)} gerade da ist. \nNun aber los!\n```") + await self.on_story(button, interaction, "doors") + else: # auf Abbrechen geklickt + # voice channel löschen + voice_channel_id = self.groups[str(thread_id)]["voice_channel"] + voice_channel = await self.bot.fetch_channel(voice_channel_id) + if len(voice_channel.members) == 0: + await voice_channel.delete() + + self.groups.pop(str(thread_id)) + self.save() + await interaction.response.send_message(f"Du hast die Runde abgebrochen. Dieser Thread wurde " + f"archiviert und du kannst in <#{self.elm_street_channel_id}>" + f" eine neue Runde starten.", ephemeral=True) + await interaction.channel.send(f"Dieses Abenteuer ist beendet und zum Nachlesen archiviert." + f"\nFür mehr Halloween-Spaß, schau in <#{self.elm_street_channel_id}>" + f"vorbei") + await interaction.channel.edit(archived=True) + else: + await interaction.response.send_message( + "Nur die Gruppenerstellerin kann die Gruppe starten lassen oder die " + "Tour abbrechen.", + ephemeral=True) + + async def on_stop(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + thread_id = interaction.channel_id + + # Button disablen + await interaction.message.edit(view=self.get_stop_view(disabled=True)) + + # Gruppenstatistik in elm-street posten + stats_embed = await self.get_group_stats_embed(thread_id) + elm_street = await self.bot.fetch_channel(self.elm_street_channel_id) + await elm_street.send("", embed=stats_embed) + + # jedem Spieler seine Süßigkeiten geben + sweets = self.groups.get(str(thread_id)).get('stats').get('sweets') + self.share_sweets(sweets, thread_id) + + # aktuelles leaderboard in elm-street posten + leaderboard_embed = await self.leaderboard(all="all") + await elm_street.send("", embed=leaderboard_embed) + + # voice channel löschen + voice_channel_id = self.groups[str(thread_id)]["voice_channel"] + voice_channel = await self.bot.fetch_channel(voice_channel_id) + if len(voice_channel.members) == 0: + await voice_channel.delete() + + # Gruppe aus json löschen + self.groups.pop(str(thread_id)) + self.save() + + # Thread archivieren + await interaction.channel.send(f"Dieses Abenteuer ist beendet und zum Nachlesen archiviert." + f"\nFür mehr Halloween-Spaß, schau in <#{self.elm_street_channel_id}>" + f"vorbei") + await interaction.channel.edit(archived=True) + + async def on_story(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + thread_id = interaction.channel_id + group = self.groups.get(str(thread_id)) + owner_id = group.get('owner') + if interaction.author.id == owner_id: + if value == "stop": + await self.on_stop(button, interaction, value) + + elif not self.can_proceed_story(interaction.channel_id): + value = "fear" + + if events := self.story.get("events"): + if value == "knock_on_door": + group = self.groups.get(str(thread_id)) + group_stats = group.get('stats') + group_stats['doors'] += 1 + self.save() + + if event := events.get(value): + channel = interaction.message.channel + choice = self.get_choice(value, event, group) + if not choice: + view = self.get_story_view("fear") + await channel.send("```\nAls ihr euch auf den Weg zur nächsten Tür macht, seht ihr am Horizont " + "langsam die Sonne aufgehen. Ihr betrachtet eure Beute und beschließt, " + "für dieses Jahr die Jagd zu beenden und tretet den Heimweg an.\n```", + view=view) + await interaction.message.delete() + else: + text = choice["text"] + view = self.get_story_view(choice.get("view")) + sweets = calculate_sweets(choice) + courage = calculate_courage(choice) + text = self.apply_sweets_and_courage(text, sweets, courage, interaction.channel_id) + await channel.send(f"```\n{text}\n```") + if view: + await channel.send("Was wollt ihr als nächstes tun?", view=view) + if next := choice.get("next"): + await self.on_story(button, interaction, next) + else: + await interaction.message.delete() + else: + await interaction.response.send_message("Nur die Gruppenleiterin kann die Gruppe steuern.", + ephemeral=True) + + async def on_leave(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + thread_id = interaction.channel_id + player_id = interaction.author.id + msg_player = interaction.message.mentions[0] + + if msg_player.id == player_id: + self.leave_group(thread_id, player_id) + await interaction.response.send_message(f"<@{player_id}> hat die Gruppe verlassen.") + await interaction.message.edit(view=self.get_leave_view(disabled=True)) + else: + await interaction.response.send_message( + f"Nur <@{player_id}> darf diesen Button bedienen. Wenn du die Gruppe " + f"verlassen willst, versuche es mit `/leave-group`", ephemeral=True) + + def get_choice(self, key, event, group): + if key == "doors": + doors_visited = get_doors_visited(group) + r = list(range(0, len(event) - 1)) + for door_visited in doors_visited: + r.remove(door_visited) + + if len(r) == 0: + return None + + i = SystemRandom().choice(r) + doors_visited.append(i) + self.save() + return event[i] + else: + return SystemRandom().choice(event) + + def get_story_view(self, view_name: str): + if views := self.story.get("views"): + if buttons := views.get(view_name): + return self.bot.view_manager.view(deepcopy(buttons), "on_story") + + return None + + def get_join_view(self, group_id: int): + buttons = [ + {"label": "Join", "style": ButtonStyle.green, "value": group_id, "custom_id": "elm_street:join"} + ] + return self.bot.view_manager.view(buttons, "on_join") + + def get_start_view(self, disabled=False): + buttons = [ + {"label": "Start", "style": ButtonStyle.green, "value": True, "custom_id": "elm_street:start", + "disabled": disabled}, + {"label": "Abbrechen", "style": ButtonStyle.gray, "value": False, "custom_id": "elm_street:cancel", + "disabled": disabled} + ] + return self.bot.view_manager.view(buttons, "on_start") + + def get_stop_view(self, disabled=False): + buttons = [ + {"label": "Beendet", "style": ButtonStyle.red, "custom_id": "elm_street:stop", "disabled": disabled} + ] + return self.bot.view_manager.view(buttons, "on_stop") + + def get_leave_view(self, disabled=False): + buttons = [ + {"label": "Verlassen", "style": ButtonStyle.gray, "custom_id": "elm_street:leave", "disabled": disabled} + ] + return self.bot.view_manager.view(buttons, "on_leave") + + def is_playing(self, user_id: int = None): + for group in self.groups.values(): + if players := group.get("players"): + if user_id in players: + return True + return False + + def is_already_in_this_group(self, user_id, message_id): + for group in self.groups.values(): + if message_id == group.get('message'): + if user_id in group.get('players'): + return True + return False + + def can_proceed_story(self, thread_id): + group = self.groups.get(str(thread_id)) + + player_ids = group.get("players") + num_players = 0 + group_courage = 0 + + for player_id in player_ids: + player = self.players.get(str(player_id)) + num_players += 1 + group_courage += player["courage"] + average_courage = group_courage / num_players + + return self.min_group_courage < average_courage + + def can_play(self, player): + if player.get('courage') < self.min_courage: + return False + return True + + def get_player(self, user: Union[disnake.User, disnake.Member]): + if player := self.players.get(str(user.id)): + return player + else: + player = {"courage": self.max_courage, "sweets": 0, "messages": []} + self.players[str(user.id)] = player + self.save() + return player + + def get_courage_message(self, player): + courage = player.get('courage') + message = f"{courage}" + return message + + def delete_message_from_player(self, player_id, message_id): + if player := self.players.get(str(player_id)): + messages = player.get('messages') + for msg in messages: + if msg['id'] == message_id: + messages.remove(msg) + self.save() + + async def leaderboard(self, all: ShowOption = 10, interaction: ApplicationCommandInteraction = None): + places = scores = "\u200b" + place = 0 + max = 0 if all == "all" else 10 + ready = False + embed = disnake.Embed(title="Elm-Street Leaderboard", + description="Wie süß bist du wirklich??\n" + + (":jack_o_lantern: " * 8)) + last_score = -1 + for player_id, player_data in sorted(self.players.items(), key=lambda item: item[1]["sweets"], reverse=True): + value = player_data["sweets"] + # embed.set_thumbnail( + # url="https://www.planet-wissen.de/kultur/religion/ostern/tempxostereiergjpg100~_v-gseagaleriexl.jpg") + elm_street_channel = await self.bot.fetch_channel(self.elm_street_channel_id) + try: + if last_score != value: + place += 1 + last_score = value + if 0 < max < place: + if ready: + break + # elif str(ctx.author.id) != player_id: + # continue + places += f"{place}: <@!{player_id}>\n" + scores += f"{value:,}\n".replace(",", ".") + + # if str(ctx.author.id) == player_id: + # ready = True + except: + pass + + embed.add_field(name=f"Sammlerin", value=places) + embed.add_field(name=f"Süßigkeiten", value=scores) + return embed + # await elm_street_channel.send("", embed=embed) + + async def get_group_stats_embed(self, thread_id): + thread = await self.bot.fetch_channel(thread_id) + players = self.groups.get(str(thread_id)).get('players') + stats = self.groups.get(str(thread_id)).get('stats') + + players_value = ', '.join([f'<@{int(player)}>' for player in players]) + doors_value = stats.get('doors') + sweets_value = stats.get('sweets') + courage_value = stats.get('courage') + + embed = disnake.Embed(title=f'Erfolge der Gruppe "{thread.name}"') + embed.add_field(name='Mitspieler', value=players_value, inline=False) + embed.add_field(name="Besuchte Türen", value=doors_value) + embed.add_field(name="Gesammelte Süßigkeiten", value=sweets_value) + embed.add_field(name="Verlorene Mutpunkte", value=courage_value) + + return embed + + def get_personal_stats_embed(self, player_id): + player = self.players.get(str(player_id)) + embed = disnake.Embed(title="Deine persönlichen Erfolge") + embed.add_field(name="Süßigkeiten", value=player['sweets']) + embed.add_field(name="Mutpunkte", value=player['courage']) + return embed + + def get_invite_message(self, author): + texts = [f"Du bist mitten in einer Großstadt gelandet.\n" + f"Der leise Wind weht Papier die Straße lang. " + f"Ansonsten hörst du nur in der Ferne das Geräusch vorbeifahrender Autos.\n" + f"Da, was war das?\n" + f"Hat sich da nicht etwas bewegt?\n" + f"Ein Schatten an der Mauer?\n" + f"Ein Geräusch wie von Krallen auf Asphalt.\n" + f"Du drehst dich im Kreis.\n" + f"Ein leises Lachen in deinem Rücken.\n" + f"Und da, gerade außerhalb deines Sichtfeldes eine Tür die sich quietschend öffnet.\n" + f"Eine laute Stimme ruft fragend: \"Ich zieh los um die Häuser, wäre ja gelacht wenn nur Kinder heute " + f"abend Süßkram bekommen. Wer ist mit dabei?\"\n" + f"Du drehst dich zur Tür und siehst {author.mention}s entschlossenen Gesichtsausdruck.", + f"Eine Einladung über die Sozialen Netzwerke hat dich Aufmerksam werden lassen. \n" + f"Darin war von einer großen Halloween Party die Rede, " + f"als Treffpunkt war ein Park in der Innenstadt angegeben.\n" + f"Schon beim eintreffen merkst du, dass es keine angemeldete Party ist: " + f"überall ist Blaulicht und du siehst einige Polizeiwagen.\n" + f"Du entscheidest dich die Pläne für den Abend noch mal zu überdenken. " + f"Aber was tun? \n" + f"Deine Verkleidung ist zu aufwendig um schon wieder nach Hause zu gehen.\n" + f"In deiner Nähe stehen noch andere Menschen in Verkleidung die nicht wissen was sie mit dem angebrochenen Abend anfangen sollen. " + f"Da fragt {author.mention} laut in die Runde: \"Wer hat Lust um die Häuser zu ziehen und gemeinsam Süßigkeiten zu sammeln?\"" + ] + + return SystemRandom().choice(texts) + + def get_group_by_voice_id(self, voice_id): + for group in self.groups.values(): + if vc := group.get("voice_channel"): + if vc == voice_id: + return group + + return None + + def apply_sweets_and_courage(self, text, sweets, courage, thread_id): + group = self.groups.get(str(thread_id)) + player_ids = group.get("players") + group_stats = group.get('stats') + + if sweets: + if sweets > 0: + text += f"\n\nIhr erhaltet jeweils {sweets} Süßigkeiten." + if sweets == 0: + text += f"\n\nIhr habt genau so viele Süßigkeiten wie vorher." + if sweets < 0: + text += f"\n\nIhr verliert jeweils {sweets} Süßigkeiten." + group_stats['sweets'] += sweets + if courage: + if courage > 0: + text += f"\n\nIhr verliert jeweils {courage} Mutpunkte." + for player_id in player_ids: + player = self.players.get(str(player_id)) + player["courage"] -= courage + group_stats['courage'] += courage + + self.save() + # TODO Was passiert wenn die courage eines Players zu weit sinkt? + return text + + def share_sweets(self, sweets, thread_id): + group = self.groups.get(str(thread_id)) + player_ids = group.get("players") + for player_id in player_ids: + player = self.players.get(str(player_id)) + player["sweets"] += sweets + + def leave_group(self, thread_id, player_id): + group = self.groups.get(str(thread_id)) + group_players = group.get('players') + player = self.players.get(str(player_id)) + + # Spieler auszahlen + group_stats = group.get('stats') + player["sweets"] += group_stats['sweets'] + + # Spieler aus Gruppe löschen + group_players.remove(player_id) + self.save() + + async def deny_join_request(self, group, message, player_id): + user = self.bot.get_user(player_id) + outfit = ["Piraten", "Einhörner", "Geister", "Katzen", "Weihnachtswichtel"] + dresscode = ["Werwölfe", "Vampire", "Alice im Wunderland", "Hexen", "Zombies"] + texts = [ + "Wir wollen um die Häuser ziehen und Kinder erschrecken. Du schaust aus, als würdest du den " + "Kindern lieber unsere Süßigkeiten geben. Versuch es woanders.", + f"Ich glaub du hast dich verlaufen, in dieser Gruppe können wir keine " + f"{SystemRandom().choice(outfit)} gebrauchen. Unser Dresscode ist: {SystemRandom().choice(dresscode)}."] + await send_dm(user, SystemRandom().choice(texts)) + group["requests"].remove({'player': player_id, 'id': message.id}) + self.save() + # Request Nachricht aus diesem Thread und aus players löschen + self.delete_message_from_player(player_id, message.id) + await message.delete() + + async def deny_open_join_requests(self, thread_id, group): + thread = await self.bot.fetch_channel(thread_id) + + if requests := group.get("requests"): + for request in requests: + message = await thread.fetch_message(request["id"]) + await self.deny_join_request(group, message, request["player"]) + + @tasks.loop(minutes=5) + async def increase_courage(self): + actual_playing = [] + for p in (self.groups.get(group).get('players') for group in self.groups): + actual_playing += p + # pro Spieler: courage erhöhen + for player in self.players: + # nur wenn Spieler nicht gerade spielt + if int(player) not in actual_playing: + player = self.players.get(player) + courage = player.get('courage') + if courage < self.max_courage: + courage += self.inc_courage_step + player['courage'] = courage if courage < self.max_courage else self.max_courage + self.save() + + # pro Nachricht: Nachricht erneuern + if messages := player.get('messages'): + for message in messages: + channel = await self.bot.fetch_channel(message['channel']) + msg = await channel.fetch_message(message['id']) + embed = msg.embeds[0] + embed.clear_fields() + embed.add_field(name='aktuelle Mutpunkte', value=self.get_courage_message(player)) + await msg.edit(embed=embed) + + @increase_courage.before_loop + async def before_increase(self): + await sleep(10) + + @commands.Cog.listener(name="on_voice_state_update") + async def voice_state_changed(self, member, before, after): + if not after.channel: + voice_channel_left = before.channel + if len(voice_channel_left.members) == 0 and \ + voice_channel_left.category_id == self.halloween_category_id and \ + not self.get_group_by_voice_id(voice_channel_left.id): + await voice_channel_left.delete() diff --git a/fernuni_bot.py b/fernuni_bot.py index a1fe53f7682139813892f9c1d2389866cdcea80f..5de2d9a6e5c1117553da2b23b7f430ff245ea092 100644 --- a/fernuni_bot.py +++ b/fernuni_bot.py @@ -6,7 +6,7 @@ from dotenv import load_dotenv from cogs import appointments, calmdown, christmas, easter, github, help, learninggroups, links, \ news, polls, roles, support, text_commands, voice, welcome, xkcd, module_information -# , timer +from view_manager import ViewManager # .env file is necessary in the same directory, that contains several strings. load_dotenv() @@ -16,17 +16,15 @@ ACTIVITY = os.getenv('DISCORD_ACTIVITY') OWNER = int(os.getenv('DISCORD_OWNER')) ROLES_FILE = os.getenv('DISCORD_ROLES_FILE') HELP_FILE = os.getenv('DISCORD_HELP_FILE') -CATEGORY_LERNGRUPPEN = os.getenv("DISCORD_CATEGORY_LERNGRUPPEN") +CATEGORY_LERNGRUPPEN = int(os.getenv("DISCORD_CATEGORY_LERNGRUPPEN")) PIN_EMOJI = "📌" -intents = disnake.Intents.default() -intents.members = True - class Boty(commands.Bot): def __init__(self): super().__init__(command_prefix='!', help_command=None, activity=disnake.Game(ACTIVITY), owner_id=OWNER, - intents=intents) + intents=disnake.Intents.all()) + self.view_manager = ViewManager(self) self.add_cog(appointments.Appointments(self)) self.add_cog(text_commands.TextCommands(self)) self.add_cog(polls.Polls(self)) @@ -45,7 +43,13 @@ class Boty(commands.Bot): self.add_cog(calmdown.Calmdown(self)) self.add_cog(github.Github(self)) # self.add_cog(timer.Timer(self)) + # self.add_cog(elm_street.ElmStreet(self)) + def is_prod(self): + return os.getenv("DISCORD_PROD") == "True" + async def on_ready(self): + self.view_manager.on_ready() + print("Client started!") bot = Boty() @@ -81,9 +85,9 @@ async def unpin_message(message): await message.unpin() -@bot.event -async def on_ready(): - print("Client started!") +# @bot.event +# async def on_ready(): +# print("Client started!") @bot.event diff --git a/view_manager.py b/view_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..ca22fc12063728812e16876b095d09214221525e --- /dev/null +++ b/view_manager.py @@ -0,0 +1,83 @@ +import json +import uuid + +import disnake + +from views.dialog_view import DialogView + + +class ViewManager: + + def __init__(self, bot): + self.bot = bot + self.views = {} + self.functions = {} + self.load() + + def on_ready(self): + for view_id in self.views: + try: + view = self.build_view(view_id) + self.bot.add_view(view) + except: + pass + + def save(self): + with open("data/views.json", "w") as file: + json.dump(self.views, file) + + def load(self): + with open("data/views.json", "r") as file: + self.views = json.load(file) + + def register(self, key, func): + self.functions[key] = func + + async def confirm(self, channel, title, description, custom_prefix, message="", fields=[], callback_key: str = None): + return await self.dialog( + channel=channel, + title=title, + description=description, + message=message, + fields=fields, + callback_key=callback_key, + buttons=[ + {"emoji": "ðŸ‘", "custom_id": f"{custom_prefix}_confirm_yes", "value": True}, + {"emoji": "👎", "custom_id": f"{custom_prefix}_confirm_no", "value": False} + ] + ) + + async def dialog(self, channel, title, description, message="", fields=[], buttons=None, callback_key: str = None): + embed = disnake.Embed(title=title, + description=description, + color=19607) + for field in fields: + embed.add_field(**field) + return await channel.send(message, embed=embed, view=self.view(buttons, callback_key)) + + def view(self, buttons=None, callback_key: str = None): + if buttons is None: + buttons = [] + view_id = str(uuid.uuid4()) + self.prepare_buttons(buttons, view_id) + view_config = {"buttons": buttons, "callback_key": callback_key} + self.views[view_id] = view_config + self.save() + return self.build_view(view_id) + + def build_view(self, view_id): + view_config = self.views[view_id] + callback_key = view_config["callback_key"] + func = self.functions[callback_key] + return DialogView(self.views[view_id]["buttons"], func) + + def add_button(self, config): + pass + + def prepare_buttons(self, buttons, view_id=None): + for config in buttons: + config["custom_id"] = config.get("custom_id", "") + ("" if not view_id else "_" + str(view_id)) + + + + diff --git a/views/dialog_view.py b/views/dialog_view.py index 2f62a79ddb72c99de29b835c190138f757607baf..b625a29aa919bf977a7d6397c6497c7d94780f83 100644 --- a/views/dialog_view.py +++ b/views/dialog_view.py @@ -1,7 +1,9 @@ import disnake -from disnake import ButtonStyle +from disnake import ButtonStyle + class DialogView(disnake.ui.View): + def __init__(self, buttons=None, callback=None): super().__init__(timeout=None) self.callback = callback @@ -26,5 +28,5 @@ class DialogView(disnake.ui.View): def internal_callback(self, button): async def button_callback(interaction): await self.callback(button, interaction, value=button.value) - return button_callback + return button_callback