Skip to content

Commit

Permalink
feat(rss): add bluesky to RSS feed types
Browse files Browse the repository at this point in the history
  • Loading branch information
ZRunner committed Nov 30, 2024
1 parent 913e9cf commit 8005421
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 8 deletions.
1 change: 1 addition & 0 deletions core/emojis_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,5 +1012,6 @@ def get_emoji(self, name: str) -> discord.Emoji | None:
"minecraft": 958305433439834152,
"github": 1130174138267480244,
"readthedocs": 484841075001786368,
"bluesky": 1312561135794393232,
}
return self.bot.get_emoji(ids[name])
6 changes: 3 additions & 3 deletions docs/rss.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ To manage this plugin (add, edit or remove feeds), you will need at least the Ma
See the last post
-----------------

**Syntax:** :code:`last-post <name|link> [youtube|twitch|deviant|web]`
**Syntax:** :code:`last-post <name|link> [bluesky|deviantart|twitch|youtube|web]`

This command allows you to see the last post of a youtube channel, a user on Twitch or DeviantArt, or from any valid RSS feed. If you provide a full URL, the bot will automatically detect the type of feed. If you only provide the name of the channel, you will have to specify the type of feed.
This command allows you to see the last post of a YouTube channel, a user on Twitch/DeviantArt/Bluesky, or from any valid RSS feed. If you provide a full URL, the bot will automatically detect the type of feed. If you only provide the name of the channel, you will have to specify the type of feed.

.. note:: No specific permission is required for this command. Remember to allow the use of external emojis to get a prettier look.

Expand All @@ -27,7 +27,7 @@ Follow a feed

If you want to automatically track an rss feed, this command should be used. You can only track a maximum feeds, which will be reloaded every 20 minutes. Note that Minecraft server tracing also counts as an rss feed, and therefore will cost you a slot (which are currently limited to 10 per server).

For YouTube channels, simply give the link of the channel, so that the bot automatically detects the type and name of the channel. If no type is recognized, the 'web' type will be selected.
For YouTube, Twitch or Bluesky channels, simply give the link of the channel, so that the bot automatically detects the type and name of the channel. If no type is recognized, the 'web' type will be selected.

.. note:: To post a message, the bot does not need any specific permission. But if it's a Minecraft server feed (see the `corresponding section <minecraft.html>`__), don't forget the "`Read message history <perms.html#read-message-history>`__" permission!

Expand Down
2 changes: 1 addition & 1 deletion events-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@
}
},
"christmas-2024": {
"begin": "2024-11-01",
"begin": "2024-12-01",
"end": "2024-12-31",
"type": "christmas",
"icon": "https://zrunner.me/cdn/christmas_2024.png",
Expand Down
3 changes: 3 additions & 0 deletions lang/rss/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"many": "Type the roles to mention for these feeds (either names, IDs or mentions), separated by spaces."
},
"ask-roles-hint-example": "For example:\n> Members \"Super VIP\" @Ping roles",
"bluesky": "Bluesky",
"bluesky-default-flow": "{logo} | New post of {author}! ({date})\n\n{title}\n\nLink: {link}\n\n{mentions}",
"bluesky-form-last": "{logo} | Here is the last post of {author}:\nWritten on {date}\n\n{title}\n\nLink: {url}",
"change-txt": {
"title": "Edit your feed text",
"label": "Message template",
Expand Down
3 changes: 3 additions & 0 deletions lang/rss/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"many": "Entrez les rôles à mentionner pour ces flux (au choix leurs noms, IDs ou mentions), séparés par des espaces."
},
"ask-roles-hint-example": "Par exemple:\n> Membres \"Super VIP\" @Notifs",
"bluesky": "Bluesky",
"bluesky-default-flow": "{logo} | Nouveau post de {author} ! ({date})\n\n{title}\n\nLien : {link}\n\n{mentions}",
"bluesky-form-last": "{logo} | Voici le dernier post de {author}:\nÉcrit le {date}\n\n{title}\n\nLien : {url}",
"change-txt": {
"title": "Modifiez le texte de votre flux",
"label": "Modèle de message",
Expand Down
37 changes: 36 additions & 1 deletion modules/rss/rss.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from core.paginator import PaginatedSelectView, Paginator
from core.tips import GuildTip
from core.views import ConfirmView, TextInputModal
from modules.rss.src.rss_bluesky import BlueskyRSS

from .src import (FeedEmbedData, FeedObject, FeedType, RssMessage, YoutubeRSS,
feed_parse)
Expand Down Expand Up @@ -72,6 +73,7 @@ def __init__(self, bot: Axobot):
self.web_rss = WebRSS(self.bot)
self.deviant_rss = DeviantartRSS(self.bot)
self.twitch_rss = TwitchRSS(self.bot)
self.bluesky_rss = BlueskyRSS(self.bot)

self.cache: dict[str, list[RssMessage]] = {}
# launch rss loop
Expand All @@ -92,7 +94,7 @@ async def cog_unload(self):
@app_commands.rename(feed_type="type")
@app_commands.checks.cooldown(3, 20)
async def rss_last_post(self, interaction: discord.Interaction, url: str,
feed_type: Literal["youtube", "twitter", "twitch", "deviantart", "web"] | None):
feed_type: Literal["bluesky", "deviantart", "twitch", "youtube", "web"] | None):
"""Search the last post of a feed
..Example rss last-post https://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA
Expand All @@ -116,6 +118,8 @@ async def rss_last_post(self, interaction: discord.Interaction, url: str,
await self.last_post_twitch(interaction, url)
elif feed_type == "deviantart":
await self.last_post_deviant(interaction, url)
elif feed_type == "bluesky":
await self.last_post_bluesky(interaction, url)
elif feed_type == "web":
await self.last_post_web(interaction, url)
else:
Expand All @@ -131,6 +135,8 @@ async def get_feed_type_from_url(self, url: str):
return "twitch"
if self.deviant_rss.is_deviantart_url(url):
return "deviantart"
if self.bluesky_rss.is_bluesky_url(url):
return "bluesky"
if self.web_rss.is_web_url(url):
return "web"
return None
Expand Down Expand Up @@ -193,6 +199,21 @@ async def last_post_deviant(self, interaction: discord.Interaction, user: str):
else:
await interaction.followup.send(obj)

async def last_post_bluesky(self, interaction: discord.Interaction, user: str):
"Search for the last post of a bluesky user"
if extracted_user := await self.bluesky_rss.get_username_by_url(user):
user = extracted_user
text = await self.bluesky_rss.get_last_post(interaction.channel, user, filter_config=None)
if isinstance(text, str):
await interaction.followup.send(text)
else:
form = await self.bot._(interaction, "rss.bluesky-form-last")
obj = await text.create_msg(form)
if isinstance(obj, discord.Embed):
await interaction.followup.send(embed=obj)
else:
await interaction.followup.send(obj)

async def last_post_web(self, interaction: discord.Interaction, link: str):
"Search for the last post of a web feed"
try:
Expand Down Expand Up @@ -259,6 +280,11 @@ async def system_add(self, interaction: discord.Interaction, link: str,
if identifiant is not None:
feed_type = "deviant"
display_type = "deviantart"
if identifiant is None:
identifiant = await self.bluesky_rss.get_username_by_url(link)
if identifiant is not None:
feed_type = "bluesky"
display_type = "bluesky"
if identifiant is not None and not link.startswith("https://"):
link = "https://"+link
if identifiant is None and link.startswith("https"):
Expand Down Expand Up @@ -1132,6 +1158,8 @@ async def check_rss_url(self, url: str):
return True
if self.deviant_rss.is_deviantart_url(url):
return True
if self.bluesky_rss.is_bluesky_url(url):
return True
# check web feed
feed = await feed_parse(url, 8)
if feed is None:
Expand All @@ -1153,6 +1181,8 @@ async def create_id(self, feed_type: FeedType):
numb = int("50"+numb)
elif feed_type == "twitch":
numb = int("60"+numb)
elif feed_type == "bluesky":
numb = int("70"+numb)
else:
numb = int("66"+numb)
return numb
Expand Down Expand Up @@ -1339,6 +1369,11 @@ async def check_feed(self, feed: FeedObject, session: ClientSession = None, shou
objs = await self.twitch_rss.get_last_post(chan, feed.link, feed.filter_config, session)
else:
objs = await self.twitch_rss.get_new_posts(chan, feed.link, feed.date, feed.filter_config, session)
elif feed.type == "bluesky":
if feed.date is None:
objs = await self.bluesky_rss.get_last_post(chan, feed.link, feed.filter_config, session)
else:
objs = await self.bluesky_rss.get_new_posts(chan, feed.link, feed.date, feed.filter_config, session)
else:
self.bot.dispatch("error", RuntimeError(f"Unknown feed type {feed.type}"))
return False
Expand Down
95 changes: 95 additions & 0 deletions modules/rss/src/rss_bluesky.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations

import datetime as dt
import re
from typing import TYPE_CHECKING

import aiohttp
import discord
from feedparser.util import FeedParserDict

from .convert_post_to_text import get_text_from_entry
from .rss_general import (FeedFilterConfig, FeedObject, RssMessage,
check_filter, feed_parse)

if TYPE_CHECKING:
from core.bot_classes import Axobot

class BlueskyRSS:
"Utilities class for any Bluesky RSS action"

def __init__(self, bot: Axobot):
self.bot = bot
self.min_time_between_posts = 60 # seconds
self.url_pattern = r"^https://(?:www\.)?bsky.app/profile/([\w._:-]+)$"

def is_bluesky_url(self, string: str):
"Check if an url is a valid Bluesky URL"
matches = re.match(self.url_pattern, string)
return bool(matches)

async def get_username_by_url(self, url: str) -> str | None:
"Extract the Bluesky username from a URL"
matches = re.match(self.url_pattern, url)
if not matches:
return None
return matches.group(1)

async def _get_feed(self, username: str, filter_config: FeedFilterConfig | None=None,
session: aiohttp.ClientSession | None=None) -> FeedParserDict:
"Get a list of feeds from a Bluesky username"
url = f"https://bsky.app/profile/{username}/rss"
feed = await feed_parse(url, 9, session)
if feed is None or "bozo_exception" in feed or not feed.entries:
return None
if filter_config is not None:
feed.entries = [entry for entry in feed.entries[:50] if await check_filter(entry, filter_config)]
return feed

async def _parse_entry(self, entry: FeedParserDict, feed: FeedParserDict, url: str, channel: discord.TextChannel):
"Parse a feed entry to get the relevant information and return a RssMessage object"
full_author = feed["feed"]["title"]
author = re.search(r"^@[\w.-_]+ - (\S+)$", full_author).group(1)
post_text = await get_text_from_entry(entry)
return RssMessage(
bot=self.bot,
feed=FeedObject.unrecorded("bluesky", channel.guild.id if channel.guild else None, link=url),
url=entry["link"],
date=entry["published_parsed"],
entry_id=entry["id"],
title=post_text,
author=author,
channel=full_author,
post_text=post_text
)

async def get_last_post(self, channel: discord.TextChannel, username: str,
filter_config: FeedFilterConfig | None,
session: aiohttp.ClientSession | None=None) -> RssMessage | str:
"Get the last post from a Bluesky user"
feed = await self._get_feed(username, filter_config, session)
if feed is None or not feed.entries:
return await self.bot._(channel.guild, "rss.nothing")
entry = feed.entries[0]
url = f"https://bsky.app/profile/{username}/rss"
return await self._parse_entry(entry, feed, url, channel)

async def get_new_posts(self, channel: discord.TextChannel, username: str, date: dt.datetime,
filter_config: FeedFilterConfig | None,
session: aiohttp.ClientSession | None=None) -> list[RssMessage]:
"Get all new posts from a Bluesky user"
feed = await self._get_feed(username, filter_config, session)
if feed is None or not feed.entries:
return []
posts_list: list[RssMessage] = []
url = f"https://bsky.app/profile/{username}/rss"
for entry in feed.entries:
# don't return more than 10 posts
if len(posts_list) > 10:
break
# don't return posts older than the date
if (dt.datetime(*entry["published_parsed"][:6], tzinfo=dt.UTC) - date).total_seconds() < self.min_time_between_posts:
break
posts_list.append(await self._parse_entry(entry, feed, url, channel))
posts_list.reverse()
return posts_list
4 changes: 2 additions & 2 deletions modules/rss/src/rss_deviantart.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def _parse_entry(self, entry: FeedParserDict, feed: FeedParserDict, url: s

async def get_last_post(self, channel: discord.TextChannel, username: str,
filter_config: FeedFilterConfig | None,
session: aiohttp.ClientSession | None= None):
session: aiohttp.ClientSession | None = None):
"Get the last post from a DeviantArt user"
feed = await self._get_feed(username, filter_config, session)
if feed is None:
Expand All @@ -80,7 +80,7 @@ async def get_new_posts(self, channel: discord.TextChannel, username: str, date:
posts_list: list[RssMessage] = []
url = "https://www.deviantart.com/" + username
for entry in feed.entries:
if dt.datetime(*entry["published_parsed"][:6], tzinfo=dt.UTC) <= date:
if (dt.datetime(*entry["published_parsed"][:6], tzinfo=dt.UTC) - date).total_seconds() <= self.min_time_between_posts:
break
obj = await self._parse_entry(entry, feed, url, channel)
posts_list.append(obj)
Expand Down
4 changes: 3 additions & 1 deletion modules/rss/src/rss_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from core.formatutils import FormatUtils
from core.safedict import SafeDict

FeedType = Literal["tw", "yt", "twitch", "reddit", "mc", "deviant", "web"]
FeedType = Literal["tw", "yt", "twitch", "reddit", "mc", "deviant", "bluesky", "web"]

if TYPE_CHECKING:
from core.bot_classes import Axobot
Expand Down Expand Up @@ -386,6 +386,8 @@ def get_emoji(self, cog: "EmojisManager") -> discord.Emoji | str:
return cog.get_emoji("minecraft")
if self.type == "deviant":
return cog.get_emoji("deviant")
if self.type == "bluesky":
return cog.get_emoji("bluesky")
if self.link is not None:
if self.link.startswith("https://github.com/"):
return cog.get_emoji("github")
Expand Down

0 comments on commit 8005421

Please sign in to comment.