From a8a5fed56f94802b45de089cc03754154069f6ac Mon Sep 17 00:00:00 2001
From: 0ry5 <oscar.bloch@posteo.de>
Date: Sun, 30 Mar 2025 18:21:42 +0200
Subject: [PATCH] init repo

---
 .example.env        |   7 ++
 bot.py              | 173 ++++++++++++++++++++++++++++++++++++++++++++
 github_connector.py |  71 ++++++++++++++++++
 requirements.txt    |  24 ++++++
 4 files changed, 275 insertions(+)
 create mode 100644 .example.env
 create mode 100644 bot.py
 create mode 100644 github_connector.py
 create mode 100644 requirements.txt

diff --git a/.example.env b/.example.env
new file mode 100644
index 0000000..3b2cc6d
--- /dev/null
+++ b/.example.env
@@ -0,0 +1,7 @@
+DISCORD_TOKEN="DISCORT_BOT_TOKEN"
+CHANNEL_ID="DISCORD_CHANNEL_ID"
+GITHUB_TOKEN="GITHUB_TOKEN_TO_PUSH_TO_MAIN"
+REPO_OWNER="netz39"
+REPO_NAME="wwww.netz39.de"
+BASE_BRANCH="main"
+MAINTAINERS='[DISCORD_IDS_OF_BRANCH_MAINTAINERS]'
\ No newline at end of file
diff --git a/bot.py b/bot.py
new file mode 100644
index 0000000..d1457a5
--- /dev/null
+++ b/bot.py
@@ -0,0 +1,173 @@
+import json
+import re
+import discord
+from discord.ext import commands
+from dotenv import load_dotenv
+from os import getenv
+from github_connector import create_or_update_file, delete_file
+
+
+load_dotenv("./.env")
+
+CHANNEL_ID = int(getenv("CHANNEL_ID"))
+MAINTAINERS = json.loads(getenv("MAINTAINERS"))
+DISCORD_TOKEN = getenv("DISCORD_TOKEN")
+
+print(MAINTAINERS)
+
+# Intents are required to listen to events like messages
+intents = discord.Intents.default()
+intents.message_content = True
+intents.members = True
+intents.reactions = True
+intents.dm_messages = True
+
+# Create an instance of the Bot
+bot = commands.Bot(command_prefix="!", intents=intents)
+
+
+def getFileName(event):
+    return f"{event.start_time.strftime("%Y-%m-%d")}_{event.name}"
+
+
+def getFilePath(event):
+    return f"_events/{event.start_time.strftime("%Y")}/discord-event-{event.id}.md"
+
+
+def getEventYML(event):
+    return f'# event imported from discord manual changes may be overwritten\n---\n \nlayout: event\ntitle: "{event.name}"\nauthor: "Netz39 e.V." \nevent:\n  start: {event.start_time.strftime("%Y-%m-%d %H:%M:%S")} \n  end:   {event.end_time.strftime("%Y-%m-%d %H:%M:%S")} \n  organizer: "Netz39 Team <kontakt@netz39.de>" \n  location: "Leibnizstr. 18, 39104 Magdeburg"\n---'
+
+
+def addMaintainer(maintainer):
+    MAINTAINERS.append(maintainer)
+    with open(".env", "r") as fileR:
+        # read a list of lines into data
+        data = fileR.readlines()
+        data[6] = f"MAINTAINERS='{json.dumps(MAINTAINERS)}'"
+    with open(".env", "w") as fileW:
+        fileW.writelines(data)
+
+
+async def handleEvent(event):
+    eventYML = getEventYML(event)
+    if "netz39" in event.location.lower() or (
+        "leibniz" in event.location.lower() and "18" in event.location
+    ):
+        if event.creator_id in MAINTAINERS:
+            create_or_update_file(
+                getFilePath(event),
+                eventYML,
+                f"new event: {getFileName(event)}",
+            )
+            channel = bot.get_channel(CHANNEL_ID)
+            await channel.send(
+                f"📅 New event '{event.name}' on {event.start_time.strftime("%Y-%m-%d")}."
+            )
+        else:
+            eventJSON = json.dumps(
+                {
+                    "yml": eventYML,
+                    "path": getFilePath(event),
+                    "fileName": getFileName(event),
+                    "name": event.name,
+                    "date": event.start_time.strftime("%Y-%m-%d"),
+                },
+                indent=2,
+            )
+            for maintainer in MAINTAINERS:
+                user = await bot.fetch_user(event.creator_id)
+                await sendDm(
+                    maintainer,
+                    f"{user.name} added a new Event on {event.start_time.strftime("%Y")}: {event.name}. Like this message to approve.\n\n\n```json\n{eventJSON}```",
+                )
+
+
+# Event: When the bot is ready
+@bot.event
+async def on_ready():
+    print(f"Logged in as {discord.user}")
+
+
+# Command: !hello
+@bot.command()
+async def hello(ctx):
+    await ctx.send(
+        "Hello, I am a bot to add events from discord to the Netz39 calendar on the website!"
+    )
+
+
+@bot.event
+async def on_ready():
+    print(f"Logged in as {bot.user}")
+    channel = bot.get_channel(CHANNEL_ID)
+    await channel.send("Hello everyone! I'm online and ready to go! 📅")
+
+
+@bot.event
+async def on_scheduled_event_update(event):
+    await handleEvent(event)
+
+
+@bot.event
+async def on_scheduled_event_create(event):
+    await handleEvent(event)
+
+
+async def sendDm(userID, message):
+    user = await bot.fetch_user(userID)
+    await user.send(message)
+
+
+@bot.event
+async def on_reaction_add(reaction, user):
+    if not user.id in MAINTAINERS:
+        return  # Ignore non-maintainers
+    if user.bot:
+        return  # Ignore bot reactions
+
+    if (
+        isinstance(reaction.message.channel, discord.DMChannel)
+        and reaction.message.author.bot
+    ):  # Check if it's a DM and from the bot
+        if reaction.emoji == "👍":
+            messageJSON = json.loads(
+                re.search(
+                    "(?s)(?<=```json).*?(?=```)", reaction.message.content
+                ).group()
+            )
+            create_or_update_file(
+                messageJSON["path"],
+                messageJSON["yml"],
+                f"new event: {messageJSON["fileName"]}",
+            )
+            channel = bot.get_channel(CHANNEL_ID)
+            await channel.send(
+                f"📅 New event '{messageJSON["name"]}' on {messageJSON["date"]}."
+            )
+
+
+@bot.event
+async def on_message(message):
+    if isinstance(message.channel, discord.DMChannel):
+        global MAINTAINERS
+        author = message.author
+        if "add maintainer" in message.content.lower() and author.id in MAINTAINERS:
+            newMaintainer = message.content.lower().replace("add maintainer ", "")
+            if not newMaintainer in MAINTAINERS:
+                addMaintainer(int(newMaintainer))
+                await sendDm(
+                    newMaintainer,
+                    "You are now a Maintainer for posthorn! Create an event located at 'Netz39 e.V.' and it will be added to the calendar.",
+                )
+                await message.channel.send("Maintainer added!")
+    await bot.process_commands(message)
+
+
+@bot.event
+async def on_scheduled_event_delete(event):
+    delete_file(getFilePath(event))
+    channel = bot.get_channel(CHANNEL_ID)
+    await channel.send(f"❌ Event has been canceled: {event.name}")
+
+
+bot.run(DISCORD_TOKEN)
diff --git a/github_connector.py b/github_connector.py
new file mode 100644
index 0000000..9160a4b
--- /dev/null
+++ b/github_connector.py
@@ -0,0 +1,71 @@
+import requests
+import json
+from dotenv import load_dotenv
+from os import getenv
+import base64
+
+load_dotenv()
+
+# load env vars
+REPO_OWNER = getenv("REPO_OWNER")
+REPO_NAME = getenv("REPO_NAME")
+BASE_BRANCH = getenv("BASE_BRANCH")
+GITHUB_TOKEN = getenv("GITHUB_TOKEN")
+
+# GitHub API URL
+API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}"
+
+# Headers for authentication
+HEADERS = {
+    "Authorization": f"token {GITHUB_TOKEN}",
+    "Accept": "application/vnd.github.v3+json",
+}
+
+
+def get_file_sha(file_path):
+    url = f"{API_URL}/contents/{file_path}?ref={BASE_BRANCH}"
+    response = requests.get(url, headers=HEADERS)
+
+    if response.status_code == 200:
+        return response.json()["sha"]
+    else:
+        raise Exception(f"Error getting file SHA: {response.json()}")
+
+
+def delete_file(file_path):
+    url = f"{API_URL}/contents/{file_path}"
+    data = {
+        "message": f"Deleting file: {file_path}",
+        "sha": get_file_sha(file_path),
+        "branch": BASE_BRANCH,
+    }
+    response = requests.delete(url, headers=HEADERS, json=data)
+
+    if not response.status_code == 200 and not response.status_code == 404:
+        raise Exception(f"Error deleting file: {response.json()}")
+
+
+def create_or_update_file(file_path, content, commit_message):
+    url = f"{API_URL}/contents/{file_path}"
+    b = base64.b64encode(bytes(content, "utf-8"))
+    base64_str = b.decode("utf-8")
+
+    # Check if file exists
+    response = requests.get(url, headers=HEADERS)
+    if response.status_code == 200:
+        sha = response.json()["sha"]
+    else:
+        sha = None
+
+    data = {
+        "message": commit_message,
+        "content": base64_str,
+        "branch": BASE_BRANCH,
+    }
+
+    if sha:
+        data["sha"] = sha  # If file exists, update it
+
+    response = requests.put(url, headers=HEADERS, json=data)
+    if not response.status_code in [200, 201]:
+        raise Exception(f"Error committing file: {response.json()}")
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..44381ad
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,24 @@
+aiohappyeyeballs==2.6.1
+aiohttp==3.11.14
+aiosignal==1.3.2
+attrs==25.3.0
+certifi==2025.1.31
+cffi==1.17.1
+charset-normalizer==3.4.1
+cryptography==44.0.2
+Deprecated==1.2.18
+discord.py==2.5.2
+frozenlist==1.5.0
+idna==3.10
+multidict==6.2.0
+propcache==0.3.1
+pycparser==2.22
+PyGithub==2.6.1
+PyJWT==2.10.1
+PyNaCl==1.5.0
+python-dotenv==1.1.0
+requests==2.32.3
+typing_extensions==4.13.0
+urllib3==2.3.0
+wrapt==1.17.2
+yarl==1.18.3