From df8e7dffd87d4272610b2a15b67fba60c98243a8 Mon Sep 17 00:00:00 2001 From: dnns01 <git@dnns01.de> Date: Sun, 8 Sep 2024 21:19:38 +0200 Subject: [PATCH] Minor enhancements to news and appointments --- extensions/appointments.py | 3 +- extensions/news.py | 88 ++++++++++++++++++++++++-------------- fernuni_bot.py | 4 +- json_import.py | 45 ++++++++++++++++++- models.py | 16 +++++-- requirements.txt | 6 ++- 6 files changed, 120 insertions(+), 42 deletions(-) diff --git a/extensions/appointments.py b/extensions/appointments.py index 96b5643..cbd4d85 100644 --- a/extensions/appointments.py +++ b/extensions/appointments.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from discord import app_commands, errors, Interaction from discord.ext import tasks, commands -from models import Appointment +from models import Appointment, Attendee from views.appointment_view import AppointmentView @@ -84,6 +84,7 @@ class Appointments(commands.GroupCog, name="appointments", description="Handle A appointment = Appointment.create(channel=channel.id, message=0, date_time=date_time, reminder=reminder, title=title, description=description, author=author_id, recurring=recurring, reminder_sent=reminder == 0, uuid=uuid.uuid4()) + Attendee.create(appointment=appointment, member_id=author_id) await interaction.response.send_message(embed=appointment.get_embed(0), view=AppointmentView()) message = await interaction.original_response() diff --git a/extensions/news.py b/extensions/news.py index ad43ea2..5b8fb39 100644 --- a/extensions/news.py +++ b/extensions/news.py @@ -1,8 +1,21 @@ +from typing import Optional +from xml.etree.ElementTree import Element + from aiohttp import ClientSession from bs4 import BeautifulSoup +from discord import errors, Embed from discord.ext import commands, tasks +from defusedxml.ElementTree import fromstring + +from models import NewsFeed, NewsArticle, Settings + + +def find_text(parent: Element, tag_name: str): + element = parent.find(tag_name) + if element is not None: + return element.text -import models + return None class News(commands.Cog): @@ -12,39 +25,50 @@ class News(commands.Cog): @tasks.loop(hours=1) async def news_loop(self): - # ToDo: Add better handling for guild - guild = models.Settings.select()[0].guild_id - url = self.bot.get_settings(guild).news_url - channel_id = self.bot.get_settings(guild).news_channel_id + for feed in NewsFeed.select(): + match feed.type: + case "RSS": + await self.parse_rss(feed) + async def parse_rss(self, feed: NewsFeed): async with ClientSession() as session: - async with session.get(url) as r: - if r.status == 200: - content = await r.read() - soup = BeautifulSoup(content, "html.parser") - channel = await self.bot.fetch_channel(channel_id) - - for news in soup.find("ul", attrs={"class": "fu-link-list"}).find_all("li"): - date = news.span.text - title = str(news.a.text) - link = news.a['href'] - - if link[0] == "/": - link = f"https://www.fernuni-hagen.de" + link - - if news := models.News.get_or_none(link=link): - if news.date != date: - await self.announce_news(channel, date, title, link) - news.update(date=date).where(models.News.link == link).execute() - else: - await self.announce_news(channel, date, title, link) - models.News.create(link=link, date=date) - - async def announce_news(self, channel, date, title, link): - guild = models.Settings.select()[0].guild_id - news_role = self.bot.get_settings(guild).news_role_id - await channel.send( - f":loudspeaker: <@&{news_role}> Neues aus der Fakultät vom {date} :loudspeaker: \n{title} \n{link}") + async with session.get(feed.url) as response: + if response.status == 200: + content = str(await response.read(), encoding='utf-8') + root = fromstring(content) + for article in root.iter('item'): + if news_article := await self.parse_article_rss(article, feed): + await self.announce_news(news_article) + + @staticmethod + async def parse_article_rss(article: Element, feed: NewsFeed) -> Optional[NewsArticle]: + title = find_text(article, "title") + description = find_text(article, "description") + link = find_text(article, "link") + pubDate = find_text(article, "pubDate") + + if news_article := NewsArticle.get_or_none(link=link): + if news_article.pubDate != pubDate: + news_article.update(title=title, description=description, pubDate=pubDate).where( + NewsArticle.link == link).execute() + return NewsArticle.get_or_none(link=link) + else: + return None + else: + return NewsArticle.create(news_feed=feed, title=title, description=description, link=link, pubDate=pubDate) + + async def announce_news(self, news_article: NewsArticle): + settings = news_article.news_feed.settings + news_role = settings.news_role_id + try: + channel = await self.bot.fetch_channel(settings.news_channel_id) + embed = Embed(title=news_article.title, url=news_article.link) + if news_article.description: + embed.description = news_article.description + await channel.send( + f":loudspeaker: <@&{news_role}> Neues aus der Fakultät vom {news_article.pubDate} :loudspeaker:", embed=embed) + except errors.NotFound: + pass async def setup(bot: commands.Bot) -> None: diff --git a/fernuni_bot.py b/fernuni_bot.py index 3875c48..cbcebe0 100644 --- a/fernuni_bot.py +++ b/fernuni_bot.py @@ -1,6 +1,5 @@ import logging import os -from logging import DEBUG from typing import List import discord @@ -21,7 +20,8 @@ OWNER = int(os.getenv('DISCORD_OWNER')) PIN_EMOJI = "📌" intents = Intents.all() -extensions = ["welcome", "xkcd", "mod_mail", "module_information"] +extensions = ["welcome", "xkcd", "mod_mail", "module_information", "links", "news", "appointments"] +# ["learninggroups", "polls", "text_commends", "timer", "voice"] _log = logging.getLogger('discord.boty') diff --git a/json_import.py b/json_import.py index 7f02c5c..332e33c 100644 --- a/json_import.py +++ b/json_import.py @@ -1,6 +1,14 @@ import json +import os +import uuid +import datetime + +import discord +from dotenv import load_dotenv import models +from models import Appointment +from views.appointment_view import AppointmentView def import_links(json_file: str) -> None: @@ -43,11 +51,44 @@ def import_courses(json_file: str) -> None: role_id=int(course["role"])) -if __name__ == "__main__": +async def import_appointments(json_file: str) -> None: + file = open(json_file, mode="r") + appointments = json.load(file) + + for channel_id, messages in appointments.items(): + for message_id, appointment in messages.items(): + date_time = datetime.datetime.strptime(appointment["date_time"], "%d.%m.%Y %H:%M") + reminder_sent = True if appointment["reminder"] == 0 and appointment.get( + "original_reminder") is not None else False + + app = models.Appointment.create(channel=int(channel_id), message=int(message_id), date_time=date_time, appointment=appointment["reminder"], description="", author=appointment["author_id"], + recurring=appointment.get("recurring", 0), reminder_sent=reminder_sent, + uuid=uuid.uuid4()) + channel = await client.fetch_channel(app.channel_id) + message = await channel.fetch_message(app.message) + for reaction in message.reactions: + if reaction.emoji == "👍": + async for user in reaction.users(): + if not user.bot: + models.Attendee.create(appointment=app, member_id=user.id) + new_msg = await channel.send(embed=appointment.get_embed(0), view=AppointmentView()) + Appointment.update(message=new_msg.id).where(Appointment.id == app.id).execute() + + +load_dotenv() +client = discord.Client(intents=discord.Intents.all()) + + +@client.event() +async def on_ready(): """ - Make sure to create a database backup before you import data from json files. + Make sure to create a database backup before you import data from json files. """ # import_links("data/links.json") # import_news("data/news.json") # import_commands("data/text_commands.json") # import_courses("data/courses_of_studies.json") + await import_appointments("data/appointments.json") + + +client.run(os.getenv("DISCORD_TOKEN")) diff --git a/models.py b/models.py index 5decbfd..c1b224a 100644 --- a/models.py +++ b/models.py @@ -53,9 +53,19 @@ class Link(BaseModel): category = ForeignKeyField(LinkCategory, backref='links') -class News(BaseModel): +class NewsFeed(BaseModel): + settings = ForeignKeyField(Settings) + url = CharField() + type = CharField() + + +class NewsArticle(BaseModel): + news_feed = ForeignKeyField(NewsFeed) + title = CharField(null=True) + description = CharField(null=True) link = CharField() - date = CharField() + pubDate = CharField() + class Poll(BaseModel): @@ -254,5 +264,5 @@ class Contact(BaseModel): db.create_tables( - [Settings, LinkCategory, Link, News, Poll, PollChoice, PollParticipant, Command, CommandText, Appointment, + [Settings, LinkCategory, Link, NewsFeed, NewsArticle, Poll, PollChoice, PollParticipant, Command, CommandText, Appointment, Attendee, Course, Module, Event, Support, Exam, Download, Contact], safe=True) diff --git a/requirements.txt b/requirements.txt index 2def8e8..956218b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,21 @@ aiohttp==3.10.5 beautifulsoup4==4.12.3 +defusedxml==0.7.1 discord.py==2.4.0 emoji==2.12.1 peewee==3.17.6 PyNaCl==1.5.0 python-dotenv==1.0.1 + aiohappyeyeballs==2.4.0 aiosignal==1.3.1 attrs==24.2.0 -cffi==1.17.0 +cffi==1.17.1 frozenlist==1.4.1 idna==3.8 multidict==6.0.5 pycparser==2.22 soupsieve==2.6 typing_extensions==4.12.2 -yarl==1.9.4 +yarl==1.10.0 -- GitLab