Skip to content
This repository has been archived by the owner on Sep 8, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1 from Katze719/refactor
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
Katze719 authored Apr 25, 2024
2 parents 089882a + 61cfeb3 commit 781cbfa
Show file tree
Hide file tree
Showing 24 changed files with 628 additions and 319 deletions.
15 changes: 0 additions & 15 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
21 changes: 21 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Version 4.0.0 - 2024-04-24

## Der Bot ist jetzt als Open-Source-Projekt unter der GPL 2.0 Lizenz verfügbar. Ihr könnt den Quellcode [HIER](https://github.com/Katze719/BSZET_IT_BOT) einsehen.

### Added
- Komplett neu geschriebener Code für den gesamten Bot.
- Implementierung neuer, optimierter Datenstrukturen für schnellere Antwortzeiten.

### Changed
- Überarbeitetes Fehlerbehandlungssystem zur besseren Erkennung und Protokollierung von Ausnahmen.
- Aktualisierung aller Abhängigkeiten auf die neuesten verfügbaren Versionen.

### Fixed
- Mehrere kleinere Bugs, die die Stabilität und Leistung beeinträchtigten.
- Spezifische Probleme mit der Speicherverwaltung behoben.

### Removed
- Entfernung veralteter Befehle, die nicht mehr verwendet werden.
- Entfernung nicht mehr unterstützter Funktionen.

---
360 changes: 339 additions & 21 deletions LICENSE

Large diffs are not rendered by default.

226 changes: 20 additions & 206 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,212 +1,26 @@
import discord
import src.document as document
import src.compare_pdf as compare_pdf
import src.table_parser as tp
import src.helpers as h
import src.settings as settings
import copy
import camelot
import datetime
import os
from src.log import logger
from discord.ext import commands, tasks
from discord import app_commands
from pdf2image import convert_from_path
from PIL import Image
from bsz_bot import BSZ_BOT, setup_tasks, Plan, log

HARD_ERROR=False
SUBSTITUTION_PLAN_PDF_URL = "http://geschuetzt.bszet.de/s-lk-vw/Vertretungsplaene/vertretungsplan-bs-it.pdf"
SUBSTITUTION_PLAN_FILENAME = "vertretungsplan"

file_name = 'vertretungsplan'
# Benutzername und Passwort für die Authentifizierung
USERNAME = "bsz-et-2324"
PASSWORD = "schulleiter#23"

bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())

changelog_embed=discord.Embed(title="Auto Update", url="https://github.com/Katze719/SchulBot/pkgs/container/schulbot", description="Version v3.0.0 -> Version v3.1.0", color=h.EMBED_COLOR)
changelog_embed.add_field(name="better error handling", value="der bot schreibt bei fehlern in den chat")

# Event handling

@bot.event
async def on_ready():
logger.info(f'Bot is ready! Logged in as {bot.user}')
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="Reddit"))
try:
synced = await bot.tree.sync()
logger.info(f"Synced {len(synced)} command(s)")
except Exception as e:
logger.exception(e)
plan__()
check_plan.start()

# Event handling end


# commands
@bot.tree.command(name="ping")
async def ping(ctx):
"""
Pingt den Bot ob er erreichbar ist.
Usage:
!ping
"""
await ctx.response.send_message(embed=h.simple_embed(f"Pong! {round(bot.latency * 1000)}ms"))

def plan__():
error_code = document.save_pdf_doc()

if error_code != 200:
return error_code

images = convert_from_path(f"{file_name}.pdf")

combined_image = Image.new('RGB', (images[0].width, sum(image.height for image in images)))

y_offset = 0
for image in images:
combined_image.paste(image, (0, y_offset))
y_offset += image.height

combined_image.save(f'{file_name}.png', 'PNG')
return error_code


@bot.tree.command(name="plan")
async def plan(ctx):
"""
Sendet den Vertretungsplan als PNG bild zurück.
Usage:
!plan
"""
error_code = plan__()
if error_code != 200:
await ctx.response.send_message(embed=h.simple_embed(f"request: {error_code}", "ERROR: the pdf document could not be downloaded (ask Paul D. for more Information)"))
return
file = discord.File(f"{file_name}.png")
await ctx.response.send_message(file=file, embed=h.simple_embed('Aktueller Vertretungsplan', '', f"attachment://{file_name}.png"))

@bot.tree.command(name="set")
async def set(ctx, variable_name: str, value: str):
s = settings.GuildSettings(ctx.guild.id)
if variable_name in s.get_all_settings():
s.set(variable_name, value)
if variable_name == 'routine' and (value == 'True' or value == 'true'):
s.set('routine', 'True')
s.set('routine_channel_id', ctx.channel.id)
await ctx.response.send_message(embed=h.simple_embed(f'Set {variable_name} and routine_channel_id', f"{variable_name} was set to {value} and routine_channel_id was set to {ctx.channel.id}"))
return
await ctx.response.send_message(embed=h.simple_embed(f'Set {variable_name}', f"{variable_name} was set to {value}"))
return
await ctx.response.send_message(embed=h.simple_embed(f'Set {variable_name}', f"{variable_name} Does not exist!"))

@bot.tree.command(name="get")
async def get(ctx, variable_name: str):
s = settings.GuildSettings(ctx.guild.id)
if variable_name == 'all':
response = "```toml\n"
for key, value in s.get_all_settings().items():
response += f"{key} : {value}\n"
response += "```"
await ctx.response.send_message(embed=h.simple_embed('All vars', response))
return
value = s.get(variable_name)
await ctx.response.send_message(embed=h.simple_embed(f'Get {variable_name}',f"{variable_name} : {value}"))

@bot.tree.command(name="reset")
async def reset(ctx):
s = settings.GuildSettings(ctx.guild.id)
s.settings.clear()
s.save_settings()
await ctx.response.send_message(embed=h.simple_embed("Reset!", 'success'))

@bot.tree.command(name="parse_table")
async def parse_table(ctx, class_name : str = '22/5'):
await ctx.response.send_message(embed=h.simple_embed('Downloading ...'))
plan__()
await ctx.edit_original_response(embed=h.simple_embed('Parsing PDF ...'))
tables = camelot.read_pdf(f"{file_name}.pdf", pages='all') #address of file location
await ctx.edit_original_response(embed=h.simple_embed('Parsing Tables ...'))
# print the first table as Pandas DataFrame
important = ''
for table in tables:
if class_name in table.df[6][0]:
important += f"**{table.df.iat[0,0].replace('...', '')} {table.df.iat[0,1].replace('...', '')}**\n{tp.parse_table(table.df)}\n"
if important == '':
important = f"IT{class_name} steht nicht im aktuellen Vertretungsplan!"
embed=h.simple_embed(f'Table (IT{class_name})', important)
embed.set_footer(text=str(datetime.datetime.now()))
await ctx.edit_original_response(embed=embed)

# @bot.tree.command(name="changelog")
# async def changelog(ctx):
# await ctx.response.send_message(embed=changelog_embed)

async def send_to_all_guilds(msg : str):
for guild in bot.guilds:
if msg == "update":
settings.GuildSettings(guild.id).set("name", guild.name)
channel = bot.get_channel(int(settings.GuildSettings(guild.id).get("routine_channel_id")))
if not channel:
break
if msg == "changelog":
await channel.send(embed=changelog_embed)
if msg == "error":
await channel.send(embed=h.simple_embed(f"request: FAILED", "ERROR: the pdf document could not be downloaded (stacktrace dumped in console [`docker-compose logs schulbot | grep -i 'error' | tee -a ./error.log`])"))
if msg == "recovered":
await channel.send(embed=h.simple_embed(f"recovered!", "bot is now back online and working."))



@bot.tree.command(name="send_info_to_all_guilds")
async def send_info_to_all_guilds(ctx, password : str, msg : str):
if password == "Katze719":
await send_to_all_guilds(msg)
await ctx.response.send_message(embed=h.simple_embed(msg, "Done"))
else:
await ctx.response.send_message(embed=h.simple_embed("wrong password!"))


# commands end

# tasks

@tasks.loop(minutes=5)
async def check_plan():
# read old plan
global HARD_ERROR
old_file = compare_pdf.hash_read_file(f"{file_name}.pdf")

# get new plan
error_code = plan__()
if error_code != 200:
if HARD_ERROR:
return
logger.error("Downloading new Plan failed!")
HARD_ERROR=True
await send_to_all_guilds("error")
return

HARD_ERROR=False

# read new plan
new_file = compare_pdf.hash_read_file(f"{file_name}.pdf")

# compare old with new plan
logger.info("comparing old with new plan")
if old_file == new_file:
return
if __name__ == '__main__':
Plan.save_settings(SUBSTITUTION_PLAN_PDF_URL, USERNAME, PASSWORD, SUBSTITUTION_PLAN_FILENAME)


# sending to channels
for guild in bot.guilds:
s = settings.GuildSettings(guild.id)
if s.get("routine") != "True" or s.get("routine_channel_id") == None:
continue
file = discord.File(f"{file_name}.png")
channel = bot.get_channel(int(s.get("routine_channel_id")))
if channel:
await channel.send(file=file, embed=h.simple_embed('Neuer Vertretungsplan!', '', f"attachment://{file_name}.png"))
print(f"send plan to {s.get('routine_channel_id')} :D")
#tasks end

bot.run(os.environ['DISCORD_BOT_TOKEN'])
@BSZ_BOT.event
async def on_ready():
log.logger.info(f'Bot is ready! Logged in as {BSZ_BOT.user}')
await BSZ_BOT.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="Reddit"))
try:
synced = await BSZ_BOT.tree.sync()
log.logger.info(f"Synced {len(synced)} command(s)")
except Exception as e:
log.logger.exception(e)
await setup_tasks()

BSZ_BOT.run(os.environ['DISCORD_BOT_TOKEN'])
11 changes: 11 additions & 0 deletions bsz_bot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .tasks import setup_tasks
from .core import BSZ_BOT
from .commands import ping, plan, get, set, reset, changelog
from .helpers import Plan, log

BSZ_BOT.tree.add_command(changelog)
BSZ_BOT.tree.add_command(reset)
BSZ_BOT.tree.add_command(ping)
BSZ_BOT.tree.add_command(plan)
BSZ_BOT.tree.add_command(get)
BSZ_BOT.tree.add_command(set)
6 changes: 6 additions & 0 deletions bsz_bot/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .ping import ping
from .plan import plan
from .get import get
from .set import set
from .reset import reset
from .changelog import changelog
27 changes: 27 additions & 0 deletions bsz_bot/commands/changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import discord
from ..helpers import *

def parse_changelog():
try:
with open('CHANGELOG.txt', 'r') as file:
content = file.read()
end_index = content.find('---')
if end_index != -1:
latest_entry = content[:end_index].strip()
else:
latest_entry = content.strip()
return latest_entry
except FileNotFoundError:
return "CHANGELOG.txt file not found."
except Exception as e:
return f"Error reading the changelog: {str(e)}"

@discord.app_commands.command(name="changelog", description="Gives you the latest changelog entry.")
async def changelog(ctx: discord.Interaction):
latest_changelog = parse_changelog()
if "Error" in latest_changelog or "not found" in latest_changelog:
await ctx.response.send_message(embed=simple_embed("Error!", latest_changelog))
else:
if len(latest_changelog) > 2048:
latest_changelog = latest_changelog[:2045] + "..."
await ctx.response.send_message(embed=simple_embed("Latest Changelog", f"[View the latest changelog here](https://github.com/Katze719/BSZET_IT_BOT)\n\n{latest_changelog}"))
15 changes: 15 additions & 0 deletions bsz_bot/commands/get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import discord
from ..helpers import *

@discord.app_commands.command(name="get", description="Get a variable.")
async def get(ctx : discord.Interaction, variable_name: str):
s = GuildSettings(ctx.guild.id)
if variable_name == 'all':
response = "```toml\n"
for key, value in s.get_all_settings().items():
response += f"{key} : {value}\n"
response += "```"
await ctx.response.send_message(embed=simple_embed('All vars', response))
return
value = s.get(variable_name)
await ctx.response.send_message(embed=simple_embed(f'Get {variable_name}',f"{variable_name} : {value}"))
6 changes: 6 additions & 0 deletions bsz_bot/commands/ping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import discord
from ..helpers import *

@discord.app_commands.command(name="ping", description="Ping the bot to see if he reacts.")
async def ping(ctx : discord.Interaction):
await ctx.response.send_message(embed=simple_embed(f"Pong!"))
14 changes: 14 additions & 0 deletions bsz_bot/commands/plan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import discord
from ..helpers import *

@discord.app_commands.command(name="plan", description="Get the plan.")
async def plan(ctx : discord.Interaction):
plan = Plan()
error_code = plan.download()
if error_code != 200:
await ctx.response.send_message(embed=simple_embed(f"request: {error_code}", "ERROR: the pdf document could not be downloaded (ask Paul D. for more Information)"))
return
file = discord.File(f"{plan.get_file_name()}.png")
await ctx.response.send_message(file=file, embed=simple_embed('Aktueller Vertretungsplan', '', f"attachment://{plan.get_file_name()}.png"))


9 changes: 9 additions & 0 deletions bsz_bot/commands/reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import discord
from ..helpers import *

@discord.app_commands.command(name="reset", description="Reset the guild settings.")
async def reset(ctx : discord.Interaction):
s = GuildSettings(ctx.guild.id)
s.settings.clear()
s.save_settings()
await ctx.response.send_message(embed=simple_embed("Reset!", 'success'))
16 changes: 16 additions & 0 deletions bsz_bot/commands/set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import discord
from ..helpers import *

@discord.app_commands.command(name="set", description="Set a variable.")
async def set(ctx : discord.Interaction, variable_name: str, value: str):
s = GuildSettings(ctx.guild.id)
if variable_name in s.get_all_settings():
s.set(variable_name, value)
if variable_name == 'routine' and (value == 'True' or value == 'true'):
s.set('routine', 'True')
s.set('routine_channel_id', ctx.channel.id)
await ctx.response.send_message(embed=simple_embed(f'Set {variable_name} and routine_channel_id', f"{variable_name} was set to {value} and routine_channel_id was set to {ctx.channel.id}"))
return
await ctx.response.send_message(embed=simple_embed(f'Set {variable_name}', f"{variable_name} was set to {value}"))
return
await ctx.response.send_message(embed=simple_embed(f'Set {variable_name}', f"{variable_name} Does not exist!"))
4 changes: 4 additions & 0 deletions bsz_bot/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import discord
from discord.ext import commands

BSZ_BOT = commands.Bot(command_prefix="!$!", intents=discord.Intents.all())
5 changes: 5 additions & 0 deletions bsz_bot/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .simple_embed import simple_embed
from .log import logger
from .settings import GuildSettings
from .document import fetch_document
from .plan import Plan
Loading

0 comments on commit 781cbfa

Please sign in to comment.