This commit is contained in:
2024-09-23 01:28:03 +02:00
parent 672651c08c
commit 82f0f6f511
10 changed files with 284 additions and 0 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.env
venv/ venv/
**/__pycache__/ **/__pycache__/

5
Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM python:3.12-alpine
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "master-bot"]

6
README.md Normal file
View File

@@ -0,0 +1,6 @@
# Hallo Informatik Utility Bot
```
https://discord.com/oauth2/authorize?client_id=1287513150614143017&permissions=8&integration_type=0&scope=bot
```

20
config.toml Normal file
View File

@@ -0,0 +1,20 @@
[channels]
forum = 1283905750263005286
[curricula]
# Masterstudium Logic and Computation
066931 = 1283906091759046729
# Masterstudium Visual Computing
066932 = 1283906123191156836
# Masterstudium Media and Human-Centered Computing
066935 = 1283906160398831621
# Masterstudium Medizinische Informatik
066936 = 1283906227599966220
# Masterstudium Software Engineering & Internet Computing
066937 = 1283906271979896832
# Masterstudium Technische Informatik
066938 = 1283906438909001808
# Masterstudium Wirtschaftsinformatik
066926 = 1283907195125104640
# Masterstudium Data Science
066645 = 1283907243657400402

20
debug.toml Normal file
View File

@@ -0,0 +1,20 @@
[channels]
forum = 1287512195025993890
[curricula]
# Masterstudium Logic and Computation
066931 = 1287512455475630203
# Masterstudium Visual Computing
066932 = 1287512474672828529
# Masterstudium Media and Human-Centered Computing
066935 = 1287512500216008837
# Masterstudium Medizinische Informatik
066936 = 1287512512324964402
# Masterstudium Software Engineering & Internet Computing
066937 = 1287512525138694164
# Masterstudium Technische Informatik
066938 = 1287512538187304980
# Masterstudium Wirtschaftsinformatik
066926 = 1287512548714741891
# Masterstudium Data Science
066645 = 1287512561448783923

3
docker-compose.yml Normal file
View File

@@ -0,0 +1,3 @@
services:
bot:
build: .

View File

@@ -0,0 +1,31 @@
import discord_api
import tiss
from dotenv import load_dotenv
import os
try: import tomllib
except ModuleNotFoundError: import pip._vendor.tomli as tomllib
load_dotenv()
if __name__ == '__main__':
# course_nr = tiss.TISS.get_code_from_any('https://tiss.tuwien.ac.at/course/courseDetails.xhtml?dswid=7270&dsrid=953&courseNr=186140&semester=2024W')
# lva = tiss.TISS.get_lva(course_nr)
# print(lva)
# print(lva.curricula)
config_file = os.getenv("CONFIG_FILE", "config.toml")
discord_token = os.getenv("DISCORD_TOKEN")
if discord_token is None:
print("[ERROR] Please set the DISCORD_TOKEN environment variable")
exit()
with open(config_file, "rb") as f:
config = tomllib.load(f)
discord_api.run(discord_token, config)

99
master-bot/discord_api.py Normal file
View File

@@ -0,0 +1,99 @@
import nextcord
from nextcord.ext import commands, application_checks
import tiss
cache = {}
def run(token: str, config: dict):
bot = commands.Bot()
def get_channel(id):
global cache
if id not in cache:
cache[id] = bot.get_channel(id)
return cache[id]
@bot.slash_command(description="Manager master channels")
@application_checks.has_guild_permissions(administrator=True)
async def master(
interaction: nextcord.Interaction,
):
pass
@master.subcommand(description="Sync Master LVA with Channel")
@application_checks.has_guild_permissions(administrator=True)
async def sync(
interaction: nextcord.Interaction,
link: str = nextcord.SlashOption(description="Enter TISS link")
):
lva_code = tiss.TISS.get_code_from_any(link)
if lva_code == None:
await interaction.send("Invalid TISS link", ephemeral=True)
return
try:
lva = tiss.TISS.get_lva(lva_code)
except:
await interaction.send("Could not fetch TISS data", ephemeral=True)
return
if lva is None:
await interaction.send("Could not fetch TISS data", ephemeral=True)
return
forum = get_channel(config["channels"]["forum"])
# Get forum data
# Get tags
tags_available = forum.available_tags
thread_tags = []
config_curricula = config["curricula"]
# This should work? but doesn't somehow
# for curriculum in lva.curricula:
# if curriculum in config_curricula:
# print(f"Curriculum found: {curriculum}")
# try:
# print(f"Looking for id {config_curricula[curriculum]}")
# tags.append(forum.get_tag(id = config_curricula[curriculum]))
# print("Tag found")
# except:
# print(f"Tag not found for curriculum {curriculum}")
# else:
# print(f"Curriculum not found: {curriculum}")
for curriculum in config_curricula:
if curriculum in lva.curricula:
# Find the tag in the available tags
tag = next(x for x in tags_available if x.id == config_curricula[curriculum])
if tag is not None:
thread_tags.append(tag)
# Get name
thread_name = f"{lva.type} {lva.name} ({lva.id_pretty()})"
# Get content
thread_content = tiss.TISS.get_anonymous_tiss_link(lva.id)
# Determine if the thread already exists
for thread in forum.threads:
# Does the thread name contain the LVA code?
if lva.id_pretty() in thread.name:
await thread.edit(name = thread_name, applied_tags = thread_tags)
message = await thread.fetch_message(thread.id)
await message.edit(content = thread_content)
await interaction.send(f"Existing forum thread has been synced {thread.jump_url}", ephemeral=False)
return
# If it doesn't exist, create a new thread
thread = await forum.create_thread(name = thread_name, content = thread_content, applied_tags = thread_tags)
reply_message = f"Forum channel created! {thread.jump_url}{" No tags were found, are you sure this was a Master LVA?" if len(thread_tags) == 0 else ""}"
await interaction.send(reply_message, ephemeral=False)
bot.run(token)

98
master-bot/tiss.py Normal file
View File

@@ -0,0 +1,98 @@
import requests
from urllib.parse import urlparse, parse_qs
import re
from bs4 import BeautifulSoup, NavigableString
import random
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0'
})
class LVA:
def __init__(self, id: str, name: str, ects: float, curricula: list[str], type: str):
self.id = id
self.name = name
self.ects = ects
self.curricula = curricula
self.type = type
def id_pretty(self):
# format 123123 -> 123.123
return f'{self.id[:3]}.{self.id[3:]}'
def __str__(self):
return f'{self.name} ({self.id}) - {self.ects} ECTS'
def __repr__(self):
return f'{self.name} ({self.id})'
def get_direct_text_clean(element):
text = ''.join(child for child in element.children if isinstance(child, NavigableString))
return re.sub(r'\s+', ' ', text).strip()
def gen_request_tokens():
request_token = str(random.randint(0, 999))
window_id = str(random.randint(0, 999))
session.cookies.set(f"dsrwid-{request_token}", window_id)
return (request_token, window_id)
class TISS:
@staticmethod
def get_anonymous_tiss_link(code: str):
return f"https://tiss.tuwien.ac.at/course/courseDetails.xhtml?courseNr={code}"
@staticmethod
def get_lva(code: str):
tokens = gen_request_tokens()
res = session.get(f"https://tiss.tuwien.ac.at/course/courseDetails.xhtml?courseNr={code}&dsrid={tokens[0]}&dswid={tokens[1]}")
soup = BeautifulSoup(res.text, 'html.parser')
course_name = get_direct_text_clean(soup.select('#contentInner > h1')[0])
subheader = soup.select('#subHeader')[0].text
ects = float(re.search(r'(\d+\.\d+)EC', subheader).group(1))
# extract "VU" from "2024W, VU, 2.0h, 3.0EC"
type = subheader.split(",")[1].strip()
table = soup.select('th[aria-label="Studienkennzahl"]')[0].parent.parent.parent
curricula = [re.sub("\D", "", tr.find("td").text) for tr in table.find("tbody").find_all("tr")]
lva = LVA(code, course_name, ects, curricula, type)
return lva
@staticmethod
def get_code_from_any(any: str):
# Example: https://tiss.tuwien.ac.at/course/courseDetails.xhtml?dswid=5304&dsrid=381&courseNr=180771&semester=2024W
any = any.strip()
url = urlparse(any)
if all([url.scheme, url.netloc]):
# Yes, this was an URL
params = parse_qs(url.query)
course_nr = params.get('courseNr', [None])[0]
return course_nr
else:
if len(any) == 6:
return any
match = re.search(r'\w{3}\.\w{3}', any)
if match:
return match.group(0).replace(".", "")
else:
return None

BIN
requirements.txt Normal file

Binary file not shown.