Working
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
|
.env
|
||||||
|
|
||||||
venv/
|
venv/
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
|
|||||||
5
Dockerfile
Normal file
5
Dockerfile
Normal 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
6
README.md
Normal 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
20
config.toml
Normal 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
20
debug.toml
Normal 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
3
docker-compose.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
build: .
|
||||||
@@ -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
99
master-bot/discord_api.py
Normal 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
98
master-bot/tiss.py
Normal 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
BIN
requirements.txt
Normal file
Binary file not shown.
Reference in New Issue
Block a user