Skip to content

Commit

Permalink
[RSS] added support for targeted channels (#158)
Browse files Browse the repository at this point in the history
* rss support for targeted channels

* process review

* remove style and revert black

* Check for permissions

Co-authored-by: aikaterna <20862007+aikaterna@users.noreply.github.com>
  • Loading branch information
Jyu Viole Grace and aikaterna authored Sep 27, 2020
1 parent c4492ab commit 2bfd002
Showing 1 changed file with 124 additions and 52 deletions.
176 changes: 124 additions & 52 deletions rss/rss.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import logging
import re
import time
from typing import Optional
from types import MappingProxyType, SimpleNamespace
from urllib.parse import urlparse

Expand All @@ -24,7 +25,7 @@
log = logging.getLogger("red.aikaterna.rss")


__version__ = "1.1.9"
__version__ = "1.1.10"


class RSS(commands.Cog):
Expand Down Expand Up @@ -61,9 +62,9 @@ def _add_content_images(self, bs4_soup: BeautifulSoup, rss_object: feedparser.ut
rss_object["is_special"].append(tag_name)
return rss_object

async def _add_feed(self, ctx, feed_name: str, url: str):
async def _add_feed(self, ctx, feed_name: str, channel: discord.TextChannel, url: str):
"""Helper for rss add."""
rss_exists = await self._check_feed_existing(ctx, feed_name)
rss_exists = await self._check_feed_existing(ctx, feed_name, channel)
if not rss_exists:
feedparser_obj = await self._fetch_feedparser_object(url)
if not feedparser_obj:
Expand All @@ -72,15 +73,16 @@ async def _add_feed(self, ctx, feed_name: str, url: str):
feedparser_plus_obj = await self._add_to_feedparser_object(feedparser_obj[0], url)
rss_object = await self._convert_feedparser_to_rssfeed(feed_name, feedparser_plus_obj, url)

async with self.config.channel(ctx.channel).feeds() as feed_data:
async with self.config.channel(channel).feeds() as feed_data:
feed_data[feed_name] = rss_object.to_json()
msg = (
f'Feed "{feed_name}" added. List the template tags with `{ctx.prefix}rss listtags` '
f"Feed `{feed_name}` added in channel: {channel.mention}\n"
f"List the template tags with `{ctx.prefix}rss listtags` "
f"and modify the template using `{ctx.prefix}rss template`."
)
await ctx.send(msg)
else:
await ctx.send(f"There is already an existing feed named {bold(feed_name)}.")
await ctx.send(f"There is already an existing feed named {bold(feed_name)} in {channel.mention}.")
return

def _add_generic_html_plaintext(self, bs4_soup: BeautifulSoup):
Expand Down Expand Up @@ -161,29 +163,48 @@ async def _append_bs4_tags(self, rss_object: feedparser.util.FeedParserDict, url

return rss_object

async def _check_feed_existing(self, ctx, feed_name: str):
async def _check_channel_permissions(self, ctx, channel: discord.TextChannel, addl_send_messages_check=True):
"""Helper for rss functions."""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
if not channel.permissions_for(ctx.me).read_messages:
await ctx.send("I don't have permissions to read that channel.")
return False
elif not channel.permissions_for(ctx.author).read_messages:
await ctx.send("You don't have permissions to read that channel.")
return False
elif addl_send_messages_check:
# check for send messages perm if needed, like on an rss add
# not needed on something like rss delete
if not channel.permissions_for(ctx.me).send_messages:
await ctx.send("I don't have permissions to send messages in that channel.")
return False
else:
return True
else:
return True

async def _check_feed_existing(self, ctx, feed_name: str, channel: discord.TextChannel):
"""Helper for rss functions."""
rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)
if not rss_feed:
return False
return True

async def _delete_feed(self, ctx, feed_name: str):
async def _delete_feed(self, ctx, feed_name: str, channel: discord.TextChannel):
"""Helper for rss delete."""
rss_exists = await self._check_feed_existing(ctx, feed_name)
rss_exists = await self._check_feed_existing(ctx, feed_name, channel)

if rss_exists:
async with self.config.channel(ctx.channel).feeds() as rss_data:
async with self.config.channel(channel).feeds() as rss_data:
rss_data.pop(feed_name, None)
return True
return False

async def _edit_template(self, ctx, feed_name: str, template: str):
async def _edit_template(self, ctx, feed_name: str, channel: discord.TextChannel, template: str):
"""Helper for rss template."""
rss_exists = await self._check_feed_existing(ctx, feed_name)
rss_exists = await self._check_feed_existing(ctx, feed_name, channel)

if rss_exists:
async with self.config.channel(ctx.channel).feeds.all() as feed_data:
async with self.config.channel(channel).feeds.all() as feed_data:
if feed_name not in feed_data:
feed_data[feed_name] = {}
feed_data[feed_name]["template"] = template
Expand Down Expand Up @@ -316,8 +337,8 @@ async def _valid_url(self, url: str):
"""Helper for rss add."""
try:
result = urlparse(url)
except:
log.debug(f"failed to resolve {url}")
except Exception as e:
log.exception(e, exc_info=e)
return False

if all([result.scheme, result.netloc, result.path]):
Expand Down Expand Up @@ -359,29 +380,44 @@ async def rss(self, ctx):
pass

@rss.command(name="add")
async def _rss_add(self, ctx, feed_name: str, url: str):
"""Add an RSS feed to the current channel."""
async def _rss_add(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None, url: str = None):
"""
Add an RSS feed to a channel.
Defaults to the current channel if no channel is specified.
"""
no_url = "Invalid or unavailable URL."
if not url:
await ctx.send(no_url)
return

channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel)
if not channel_permission_check:
return

valid_url = await self._valid_url(url)
if valid_url:
await self._add_feed(ctx, feed_name.lower(), url)
await self._add_feed(ctx, feed_name.lower(), channel, url)
else:
await ctx.send("Invalid or unavailable URL.")
await ctx.send(no_url)

@rss.group(name="embed")
async def _rss_embed(self, ctx):
"""Embed feed settings."""
pass

@_rss_embed.command(name="color", aliases=["colour"])
async def _rss_embed_color(self, ctx, feed_name: str, *, color: str):
async def _rss_embed_color(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None, *, color: str = None):
"""
Set an embed color for a feed.
Use this command with no color to reset to the default.
`color` must be a hex code like #990000, a [Discord color name](https://discordpy.readthedocs.io/en/latest/api.html#colour),
or a [CSS3 color name](https://www.w3.org/TR/2018/REC-css-color-3-20180619/#svg-color).
"""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
channel = channel or ctx.channel
rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)
if not rss_feed:
await ctx.send("That feed name doesn't exist in this channel.")
return
Expand All @@ -395,7 +431,7 @@ async def _rss_embed_color(self, ctx, feed_name: str, *, color: str):
)

if not color:
async with self.config.channel(ctx.channel).feeds() as feed_data:
async with self.config.channel(channel).feeds() as feed_data:
feed_data[feed_name]["embed_color"] = None
await ctx.send(
f"{embed_state_message}The color for {bold(feed_name)} has been reset. "
Expand All @@ -421,21 +457,22 @@ async def _rss_embed_color(self, ctx, feed_name: str, *, color: str):
if hex_code == "0xFFFFFF":
hex_code = "0xFFFFFE"

async with self.config.channel(ctx.channel).feeds() as feed_data:
async with self.config.channel(channel).feeds() as feed_data:
# data is always a 0xFFFFFF style value
feed_data[feed_name]["embed_color"] = hex_code

await ctx.send(f"Embed color for {bold(feed_name)} set to {user_facing_hex} ({color_name}).")

@_rss_embed.command(name="image")
async def _rss_embed_image(self, ctx, feed_name: str, image_tag_name: str = None):
async def _rss_embed_image(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None, image_tag_name: str = None):
"""
Set a tag to be a large embed image.
This image will be applied to the last embed in the paginated list.
Use this command with no image_tag_name to clear the embed image.
"""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
channel = channel or ctx.channel
rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)
if not rss_feed:
await ctx.send("That feed name doesn't exist in this channel.")
return
Expand All @@ -452,7 +489,7 @@ async def _rss_embed_image(self, ctx, feed_name: str, image_tag_name: str = None
if image_tag_name.startswith("$"):
image_tag_name = image_tag_name.strip("$")

async with self.config.channel(ctx.channel).feeds() as feed_data:
async with self.config.channel(channel).feeds() as feed_data:
feed_data[feed_name]["embed_image"] = image_tag_name

if image_tag_name:
Expand All @@ -463,14 +500,15 @@ async def _rss_embed_image(self, ctx, feed_name: str, image_tag_name: str = None
)

@_rss_embed.command(name="thumbnail")
async def _rss_embed_thumbnail(self, ctx, feed_name: str, thumbnail_tag_name: str = None):
async def _rss_embed_thumbnail(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None, thumbnail_tag_name: str = None):
"""
Set a tag to be a thumbnail image.
This thumbnail will be applied to the first embed in the paginated list.
Use this command with no thumbnail_tag_name to clear the embed thumbnail.
"""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
channel = channel or ctx.channel
rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)
if not rss_feed:
await ctx.send("That feed name doesn't exist in this channel.")
return
Expand All @@ -487,7 +525,7 @@ async def _rss_embed_thumbnail(self, ctx, feed_name: str, thumbnail_tag_name: st
if thumbnail_tag_name.startswith("$"):
thumbnail_tag_name = thumbnail_tag_name.strip("$")

async with self.config.channel(ctx.channel).feeds() as feed_data:
async with self.config.channel(channel).feeds() as feed_data:
feed_data[feed_name]["embed_thumbnail"] = thumbnail_tag_name

if thumbnail_tag_name:
Expand All @@ -499,49 +537,58 @@ async def _rss_embed_thumbnail(self, ctx, feed_name: str, thumbnail_tag_name: st
)

@_rss_embed.command(name="toggle")
async def _rss_embed_toggle(self, ctx, feed_name: str):
async def _rss_embed_toggle(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None):
"""
Toggle whether a feed is sent in an embed or not.
If the bot doesn't have permissions to post embeds,
the feed will always be plain text, even if the embed
toggle is set.
"""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
channel = channel or ctx.channel
rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)
if not rss_feed:
await ctx.send("That feed name doesn't exist in this channel.")
return

embed_toggle = rss_feed["embed"]
toggle_text = "disabled" if embed_toggle else "enabled"

async with self.config.channel(ctx.channel).feeds() as feed_data:
async with self.config.channel(channel).feeds() as feed_data:
feed_data[feed_name]["embed"] = not embed_toggle

await ctx.send(f"Embeds for {bold(feed_name)} are {toggle_text}.")

@rss.command(name="force")
async def _rss_force(self, ctx, feed_name: str):
async def _rss_force(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None):
"""Forces a feed alert."""
channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel)
if not channel_permission_check:
return

feeds = await self.config.all_channels()
try:
feeds[ctx.channel.id]
feeds[channel.id]
except KeyError:
await ctx.send("There are no feeds in this channel.")
return

if feed_name not in feeds[ctx.channel.id]["feeds"]:
if feed_name not in feeds[channel.id]["feeds"]:
await ctx.send("That feed name doesn't exist in this channel.")
return

rss_feed = feeds[ctx.channel.id]["feeds"][feed_name]
await self.get_current_feed(ctx.channel, feed_name, rss_feed, force=True)
rss_feed = feeds[channel.id]["feeds"][feed_name]
await self.get_current_feed(channel, feed_name, rss_feed, force=True)

@rss.command(name="list")
async def _rss_list(self, ctx, channel: discord.TextChannel = None):
"""List currently available feeds for this channel, or a specific channel."""
if not channel:
channel = ctx.channel
"""List saved feeds for this channel or a specific channel."""
channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel)
if not channel_permission_check:
return

feeds = await self._get_feed_names(channel)
msg = f"[ Available Feeds for #{channel.name} ]\n\n\t"
if feeds:
Expand All @@ -552,9 +599,14 @@ async def _rss_list(self, ctx, channel: discord.TextChannel = None):
await ctx.send(box(page, lang="ini"))

@rss.command(name="listtags")
async def _rss_list_tags(self, ctx, feed_name: str):
async def _rss_list_tags(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None):
"""List the tags available from a specific feed."""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel)
if not channel_permission_check:
return

rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)

if not rss_feed:
await ctx.send("No feed with that name in this channel.")
Expand Down Expand Up @@ -594,19 +646,32 @@ async def _rss_list_tags_helper(self, ctx, rss_feed: dict, feed_name: str):
await ctx.send(box(msg, lang="ini"))

@rss.command(name="remove", aliases=["delete", "del"])
async def _rss_remove(self, ctx, name: str):
"""Removes a feed from this channel."""
success = await self._delete_feed(ctx, name)
async def _rss_remove(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None):
"""
Removes a feed from a channel.
Defaults to the current channel if no channel is specified.
"""
channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel, addl_send_messages_check=False)
if not channel_permission_check:
return

success = await self._delete_feed(ctx, feed_name, channel)
if success:
await ctx.send("Feed deleted.")
else:
await ctx.send("Feed not found!")

@rss.command(name="showtemplate")
async def _rss_show_template(self, ctx, feed_name: str):
async def _rss_show_template(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None):
"""Show the template in use for a specific feed."""
rss_feed = await self.config.channel(ctx.channel).feeds.get_raw(feed_name, default=None)
channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel)
if not channel_permission_check:
return

rss_feed = await self.config.channel(channel).feeds.get_raw(feed_name, default=None)
if not rss_feed:
await ctx.send("No feed with that name in this channel.")
return
Expand Down Expand Up @@ -639,15 +704,22 @@ async def _rss_show_template(self, ctx, feed_name: str):
await ctx.send(f"Template for {bold(feed_name)}:\n\n`{rss_template}`\n{box(embed_settings, lang='ini')}")

@rss.command(name="template")
async def _rss_template(self, ctx, feed_name: str, *, template: str):
async def _rss_template(self, ctx, feed_name: str, channel: Optional[discord.TextChannel] = None, *, template: str = None):
"""
Set a template for the feed alert.
Each variable must start with $, valid variables can be found with `[p]rss listtags`.
"""
channel = channel or ctx.channel
channel_permission_check = await self._check_channel_permissions(ctx, channel)
if not channel_permission_check:
return
if not template:
await ctx.send_help()
return
template = template.replace("\\t", "\t")
template = template.replace("\\n", "\n")
success = await self._edit_template(ctx, feed_name, template)
success = await self._edit_template(ctx, feed_name, channel, template)
if success:
await ctx.send("Template added successfully.")
else:
Expand Down Expand Up @@ -703,7 +775,7 @@ async def get_current_feed(self, channel: discord.TextChannel, name: str, rss_fe
if (last_title != entry.title) and (last_link != entry.link) and (last_time < entry_time):
log.debug(f"New entry found via time validation for feed {name} on cid {channel.id}")
feedparser_plus_obj = await self._add_to_feedparser_object(entry, url)
feedparser_plus_objects.append(feedparser_plus_obj)
feedparser_plus_objects.append(feedparser_plus_obj)

# this is a post that has no time information attached to it and we can only
# verify that the title and link did not match the previously posted entry
Expand Down

0 comments on commit 2bfd002

Please sign in to comment.