diff --git a/src/api.py b/src/api.py index d648f8d..0d69c22 100644 --- a/src/api.py +++ b/src/api.py @@ -108,7 +108,7 @@ class API: url=url ) - async def get_subscriptions(self, **filters): + async def get_subscriptions(self, **filters) -> tuple[list[dict], int]: """ Get multiple subscriptions. """ @@ -117,6 +117,15 @@ class API: return await self._get_many(self.API_ENDPOINT + "subscription/", filters) + async def get_subscription_channels(self, **filters) -> tuple[list[dict], int]: + """ + Get many subscription channels. + """ + + log.debug("getting multiple channels") + + return await self._get_many(self.API_ENDPOINT + "subchannel/", filters) + # async def create_subscription(self, name: str, rss_url: str, image_url: str, server_id: str, targets: list) -> dict: # """ # Create a new Subscription. diff --git a/src/extensions/rss.py b/src/extensions/rss.py index f7d333f..55d83a9 100644 --- a/src/extensions/rss.py +++ b/src/extensions/rss.py @@ -11,7 +11,7 @@ import validators from feedparser import FeedParserDict, parse from discord.ext import commands from discord import Interaction, TextChannel -from discord.app_commands import Choice, Group, autocomplete, rename +from discord.app_commands import Choice, Group, autocomplete, rename, command from api import API from feed import Subscription, SubscriptionChannel, TrackedContent @@ -92,240 +92,240 @@ class FeedCog(commands.Cog): log.info("%s cog is ready", self.__class__.__name__) - async def autocomplete_subscriptions(self, inter: Interaction, name: str) -> list[Choice]: - """""" + # async def autocomplete_subscriptions(self, inter: Interaction, name: str) -> list[Choice]: + # """""" - log.debug("autocompleting subscriptions '%s'", name) + # log.debug("autocompleting subscriptions '%s'", name) - try: - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - results, _ = await api.get_subscriptions(server=inter.guild_id, search=name) + # try: + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # results, _ = await api.get_subscriptions(server=inter.guild_id, search=name) - except Exception as exc: - log.error(exc) - return [] + # except Exception as exc: + # log.error(exc) + # return [] - subscriptions = Subscription.from_list(results) + # subscriptions = Subscription.from_list(results) - return [ - Choice(name=sub.name, value=sub.uuid) - for sub in subscriptions - ] + # return [ + # Choice(name=sub.name, value=sub.uuid) + # for sub in subscriptions + # ] - async def autocomplete_subscription_channels(self, inter: Interaction, uuid: str): - """""" + # async def autocomplete_subscription_channels(self, inter: Interaction, uuid: str): + # """""" - log.debug("autocompleting subscription channels") + # log.debug("autocompleting subscription channels") - try: - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - results, _ = await api.get_subscription_channels() + # try: + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # results, _ = await api.get_subscription_channels() - except Exception as exc: - log.error(exc) - return [] + # except Exception as exc: + # log.error(exc) + # return [] - subscription_channels = SubscriptionChannel.from_list(results) + # subscription_channels = SubscriptionChannel.from_list(results) - async def name(link): - result = self.bot.get_channel(link.id) or await self.bot.fetch_channel(link.id) - return f"{link.subscription.name} -> #{result.name}" + # async def name(link): + # result = self.bot.get_channel(link.id) or await self.bot.fetch_channel(link.id) + # return f"{link.subscription.name} -> #{result.name}" - return [ - Choice(name=await name(link), value=link.uuid) - for link in subscription_channels - ] + # return [ + # Choice(name=await name(link), value=link.uuid) + # for link in subscription_channels + # ] - subscription_group = Group( - name="subscriptions", - description="subscription commands", - guild_only=True - ) + # subscription_group = Group( + # name="subscriptions", + # description="subscription commands", + # guild_only=True + # ) - @subscription_group.command(name="link") - @autocomplete(sub_uuid=autocomplete_subscriptions) - @rename(sub_uuid="subscription") - async def link_subscription_channel(self, inter: Interaction, sub_uuid: str, channel: TextChannel): - """ - Link Subscription to discord.TextChannel. - """ + # @subscription_group.command(name="link") + # @autocomplete(sub_uuid=autocomplete_subscriptions) + # @rename(sub_uuid="subscription") + # async def link_subscription_channel(self, inter: Interaction, sub_uuid: str, channel: TextChannel): + # """ + # Link Subscription to discord.TextChannel. + # """ - await inter.response.defer() + # await inter.response.defer() - try: - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - data = await api.create_subscription_channel(str(channel.id), sub_uuid) + # try: + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # data = await api.create_subscription_channel(str(channel.id), sub_uuid) - except aiohttp.ClientResponseError as exc: - return await ( - Followup( - f"Error · {exc.message}", - "Ensure you haven't: \n" - "- Already linked this subscription to this channel\n" - "- Already linked this subscription to the maximum of 4 channels" - ) - .footer(f"HTTP {exc.code}") - .error() - .send(inter) - ) + # except aiohttp.ClientResponseError as exc: + # return await ( + # Followup( + # f"Error · {exc.message}", + # "Ensure you haven't: \n" + # "- Already linked this subscription to this channel\n" + # "- Already linked this subscription to the maximum of 4 channels" + # ) + # .footer(f"HTTP {exc.code}") + # .error() + # .send(inter) + # ) - subscription = Subscription.from_dict(data.pop("subscription")) - data["subscription"] = ( - f"{subscription.name}\n" - f"[RSS]({subscription.rss_url}) · " - f"[API Subscription]({API.SUBSCRIPTION_ENDPOINT}{subscription.uuid}) · " - f"[API Link]({API.CHANNEL_ENDPOINT}{data['uuid']})" - ) + # subscription = Subscription.from_dict(data.pop("subscription")) + # data["subscription"] = ( + # f"{subscription.name}\n" + # f"[RSS]({subscription.rss_url}) · " + # f"[API Subscription]({API.SUBSCRIPTION_ENDPOINT}{subscription.uuid}) · " + # f"[API Link]({API.CHANNEL_ENDPOINT}{data['uuid']})" + # ) - channel_id = int(data.pop("id")) - channel = self.bot.get_channel(channel_id) or await self.bot.fetch_channel(channel_id) - data["channel"] = channel.mention + # channel_id = int(data.pop("id")) + # channel = self.bot.get_channel(channel_id) or await self.bot.fetch_channel(channel_id) + # data["channel"] = channel.mention - data.pop("creation_datetime") - data.pop("uuid") + # data.pop("creation_datetime") + # data.pop("uuid") - await ( - Followup("Linked!") - .fields(**data) - .added() - .send(inter) - ) + # await ( + # Followup("Linked!") + # .fields(**data) + # .added() + # .send(inter) + # ) - @subscription_group.command(name="unlink") - @autocomplete(uuid=autocomplete_subscription_channels) - @rename(uuid="link") - async def unlink_subscription_channel(self, inter: Interaction, uuid: str): - """ - Unlink subscription from discord.TextChannel. - """ + # @subscription_group.command(name="unlink") + # @autocomplete(uuid=autocomplete_subscription_channels) + # @rename(uuid="link") + # async def unlink_subscription_channel(self, inter: Interaction, uuid: str): + # """ + # Unlink subscription from discord.TextChannel. + # """ - await inter.response.defer() + # await inter.response.defer() - try: - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - # data = await api.get_subscription(uuid=uuid) - await api.delete_subscription_channel(uuid=uuid) - # sub_channel = await SubscriptionChannel.from_dict(data) + # try: + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # # data = await api.get_subscription(uuid=uuid) + # await api.delete_subscription_channel(uuid=uuid) + # # sub_channel = await SubscriptionChannel.from_dict(data) - except Exception as exc: - return await ( - Followup(exc.__class__.__name__, str(exc)) - .error() - .send(inter) - ) + # except Exception as exc: + # return await ( + # Followup(exc.__class__.__name__, str(exc)) + # .error() + # .send(inter) + # ) - await ( - Followup("Subscription unlinked!", uuid) - .added() - .send(inter) - ) + # await ( + # Followup("Subscription unlinked!", uuid) + # .added() + # .send(inter) + # ) - @subscription_group.command(name="list-links") - async def list_subscription(self, inter: Interaction): - """List Subscriptions Channels in this server.""" + # @subscription_group.command(name="list-links") + # async def list_subscription(self, inter: Interaction): + # """List Subscriptions Channels in this server.""" - await inter.response.defer() + # await inter.response.defer() - async def formatdata(index: int, item: dict) -> tuple[str, str]: - item = SubscriptionChannel.from_dict(item) - next_emoji = self.bot.get_emoji(1204542366602502265) - key = f"{index}. {item.subscription.name} {next_emoji} {item.mention}" - return key, item.hyperlinks_string + # async def formatdata(index: int, item: dict) -> tuple[str, str]: + # item = SubscriptionChannel.from_dict(item) + # next_emoji = self.bot.get_emoji(1204542366602502265) + # key = f"{index}. {item.subscription.name} {next_emoji} {item.mention}" + # return key, item.hyperlinks_string - async def getdata(page: int, pagesize: int) -> dict: - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - return await api.get_subscription_channels( - subscription__server=inter.guild.id, page=page, page_size=pagesize - ) + # async def getdata(page: int, pagesize: int) -> dict: + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # return await api.get_subscription_channels( + # subscription__server=inter.guild.id, page=page, page_size=pagesize + # ) - embed = Followup(f"Links in {inter.guild.name}").info()._embed - pagination = PaginationView( - self.bot, - inter=inter, - embed=embed, - getdata=getdata, - formatdata=formatdata, - pagesize=10, - initpage=1 - ) - await pagination.send() + # embed = Followup(f"Links in {inter.guild.name}").info()._embed + # pagination = PaginationView( + # self.bot, + # inter=inter, + # embed=embed, + # getdata=getdata, + # formatdata=formatdata, + # pagesize=10, + # initpage=1 + # ) + # await pagination.send() - @subscription_group.command(name="add") - async def new_subscription(self, inter: Interaction, name: str, rss_url: str): - """Subscribe this server to a new RSS Feed.""" + # @subscription_group.command(name="add") + # async def new_subscription(self, inter: Interaction, name: str, rss_url: str): + # """Subscribe this server to a new RSS Feed.""" - await inter.response.defer() + # await inter.response.defer() - try: - parsed_rssfeed = await self.bot.functions.validate_feed(name, rss_url) - image_url = parsed_rssfeed.get("feed", {}).get("image", {}).get("href") + # try: + # parsed_rssfeed = await self.bot.functions.validate_feed(name, rss_url) + # image_url = parsed_rssfeed.get("feed", {}).get("image", {}).get("href") - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - data = await api.create_subscription(name, rss_url, image_url, str(inter.guild_id), [-1]) + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # data = await api.create_subscription(name, rss_url, image_url, str(inter.guild_id), [-1]) - except aiohttp.ClientResponseError as exc: - return await ( - Followup( - f"Error · {exc.message}", - "Ensure you haven't: \n" - "- Reused an identical name of an existing Subscription\n" - "- Already created the maximum of 25 Subscriptions" - ) - .footer(f"HTTP {exc.code}") - .error() - .send(inter) - ) + # except aiohttp.ClientResponseError as exc: + # return await ( + # Followup( + # f"Error · {exc.message}", + # "Ensure you haven't: \n" + # "- Reused an identical name of an existing Subscription\n" + # "- Already created the maximum of 25 Subscriptions" + # ) + # .footer(f"HTTP {exc.code}") + # .error() + # .send(inter) + # ) - # Omit data we dont want the user to see - data.pop("uuid") - data.pop("image") - data.pop("server") - data.pop("creation_datetime") + # # Omit data we dont want the user to see + # data.pop("uuid") + # data.pop("image") + # data.pop("server") + # data.pop("creation_datetime") - # Update keys to be more human readable - data["url"] = data.pop("rss_url") + # # Update keys to be more human readable + # data["url"] = data.pop("rss_url") - await ( - Followup("Subscription Added!") - .fields(**data) - .image(image_url) - .added() - .send(inter) - ) + # await ( + # Followup("Subscription Added!") + # .fields(**data) + # .image(image_url) + # .added() + # .send(inter) + # ) - @subscription_group.command(name="remove") - @autocomplete(uuid=autocomplete_subscriptions) - @rename(uuid="choice") - async def remove_subscriptions(self, inter: Interaction, uuid: str): - """Unsubscribe this server from an existing RSS Feed.""" + # @subscription_group.command(name="remove") + # @autocomplete(uuid=autocomplete_subscriptions) + # @rename(uuid="choice") + # async def remove_subscriptions(self, inter: Interaction, uuid: str): + # """Unsubscribe this server from an existing RSS Feed.""" - await inter.response.defer() + # await inter.response.defer() - try: - async with aiohttp.ClientSession() as session: - api = API(self.bot.api_token, session) - await api.delete_subscription(uuid) + # try: + # async with aiohttp.ClientSession() as session: + # api = API(self.bot.api_token, session) + # await api.delete_subscription(uuid) - except Exception as exc: - return await ( - Followup(exc.__class__.__name__, str(exc)) - .error() - .send(inter) - ) + # except Exception as exc: + # return await ( + # Followup(exc.__class__.__name__, str(exc)) + # .error() + # .send(inter) + # ) - await ( - Followup("Subscription Removed!", uuid) - .trash() - .send(inter) - ) + # await ( + # Followup("Subscription Removed!", uuid) + # .trash() + # .send(inter) + # ) - @subscription_group.command(name="list") + @command(name="subscriptions") async def list_subscription(self, inter: Interaction): """List Subscriptions from this server.""" diff --git a/src/extensions/tasks.py b/src/extensions/tasks.py index a5c5326..03b7a2b 100644 --- a/src/extensions/tasks.py +++ b/src/extensions/tasks.py @@ -3,6 +3,7 @@ Extension for the `TaskCog`. Loading this file via `commands.Bot.load_extension` will add `TaskCog` to the bot. """ +import json import logging import datetime from os import getenv @@ -16,7 +17,7 @@ from discord.errors import Forbidden from sqlalchemy import insert, select, and_ from feedparser import parse -from feed import Source, Article, RSSFeed, Subscription, SubscriptionChannel +from feed import Source, Article, RSSFeed, Subscription, SubscriptionChannel, SubChannel from db import ( DatabaseManager, FeedChannelModel, @@ -75,133 +76,166 @@ class TaskCog(commands.Cog): async def rss_task(self): """Automated task responsible for processing rss feeds.""" - log.debug("sub task disabled") - return - log.info("Running subscription task") time = process_time() - # async with DatabaseManager() as database: - # query = select(FeedChannelModel, RssSourceModel).join(RssSourceModel) - # result = await database.session.execute(query) - # feeds = result.scalars().all() - - # for feed in feeds: - # await self.process_feed(feed, database) - guild_ids = [guild.id for guild in self.bot.guilds] - page = 1 - page_size = 10 - - async def get_subs(api, page=1): - return await api.get_subscriptions(server__in=guild_ids, page_size=page_size) + data = [] async with aiohttp.ClientSession() as session: api = API(self.bot.api_token, session) - data, total_subs = await get_subs(api) - await self.batch_process_subs(data) - processed_subs = len(data) - - while processed_subs < total_subs: - log.debug("we are missing '%s' items, fetching next page '%s'", total_subs - processed_subs, page + 1) + page = 0 + while True: page += 1 - data, _ = await get_subs(api, page) - await self.batch_process_subs(next_page_data) - processed_subs += len(data) + page_data = await self.get_subscriptions(api, guild_ids, page) - else: - log.debug("we have all '%s' items, ending while loop", total_subs) + if not page_data: + break - log.info("Finished subscription task, time elapsed: %s", process_time() - time) + data.extend(page_data) + log.debug("extending data by '%s' items", len(page_data)) - async def batch_process_subs(self, data: list): + log.debug("finished api data collection, browsed %s pages for %s subscriptions", page, len(data)) - log.debug("batch process subs, count '%s'", len(data)) - subscriptions = Subscription.from_list(data) + subscriptions = Subscription.from_list(data) + for sub in subscriptions: + await self.process_subscription(api, session, sub) - for sub in subscriptions: - log.info(sub.name) + log.info("Finished subscription task, time elapsed: %s", process_time() - time) + async def get_subscriptions(self, api, guild_ids: list[int], page: int): - async def process_feed(self, feed: FeedChannelModel, database: DatabaseManager): - """Process the passed feed. Will also call process for each article found in the feed. + log.debug("attempting to get subscriptions for page: %s", page) - Parameters - ---------- - feed : FeedChannelModel - Database model for the feed. - database : DatabaseManager - Database connection handler, must be open. - """ - - log.debug("Processing feed: %s", feed.id) - - channel = self.bot.get_channel(feed.discord_channel_id) - - # TODO: integrate the `validate_feed` code into here, also do on list command and show errors. - - async with aiohttp.ClientSession() as session: - - unparsed_content = await get_unparsed_feed(feed.rss_source.rss_url) - parsed_feed = parse(unparsed_content) - source = Source.from_parsed(parsed_feed) - articles = source.get_latest_articles(5) - - if not articles: - log.info("No articles to process for %s in ", feed.rss_source.nick, feed.discord_server_id) - return - - for article in articles: - await self.process_article(feed.id, article, channel, database, session) - - async def process_article( - self, feed_id: int, article: Article, channel: TextChannel, database: DatabaseManager, - session: aiohttp.ClientSession - ): - """Process the passed article. Will send the embed to a channel if all is valid. - - Parameters - ---------- - feed_id : int - The feed model ID, used to log the sent article. - article : Article - Database model for the article. - channel : TextChannel - Where the article will be sent to. - database : DatabaseManager - Database connection handler, must be open. - """ - - log.debug("Processing article: %s", article.url) - - query = select(SentArticleModel).where(and_( - SentArticleModel.article_url == article.url, - SentArticleModel.discord_channel_id == channel.id, - )) - result = await database.session.execute(query) - - if result.scalars().all(): - log.debug("Article already processed: %s", article.url) - return - - embed = await article.to_embed(session) try: - await channel.send(embed=embed) - except Forbidden as error: # TODO: find some way of informing the user about this error. - log.error("Can't send article to channel: %s · %s · %s", channel.name, channel.id, error) + return (await api.get_subscriptions(server__in=guild_ids, page=page))[0] + except aiohttp.ClientResponseError as error: + if error.status == 404: + log.debug(error) + return [] + + log.error(error) + + async def process_subscription(self, api, session, sub: Subscription): + log.debug("processing subscription '%s' '%s' for '%s'", sub.id, sub.name, sub.guild_id) + + if not sub.active: + log.debug("skipping sub because it's active flag is 'False'") return - query = insert(SentArticleModel).values( - article_url = article.url, - discord_channel_id = channel.id, - discord_server_id = channel.guild.id, - discord_message_id = -1, - feed_channel_id = feed_id - ) - await database.session.execute(query) + channels = [self.bot.get_channel(subchannel.channel_id) for subchannel in await sub.get_channels(api)] + if not channels: + log.warning("No channels to send this to") + return - log.debug("new Article processed: %s", article.url) + unparsed_content = await get_unparsed_feed(sub.url, session) + parsed_content = parse(unparsed_content) + source = Source.from_parsed(parsed_content) + articles = source.get_latest_articles(3) + + if not articles: + log.debug("No articles found") + + for article in articles: + await self.process_article(session, channels, article) + + async def process_article(self, session, channels: list[SubChannel], article: Article): + embed = await article.to_embed(session) + + log.debug("attempting to send embed to %s channel(s)", len(channels)) + + for channel in channels: + await channel.send(embed=embed) + + + # async def batch_process_subs(self, data: list): + + # log.debug("batch process subs, count '%s'", len(data)) + # subscriptions = Subscription.from_list(data) + + # for sub in subscriptions: + # log.info(sub.name) + + + # async def process_feed(self, feed: FeedChannelModel, database: DatabaseManager): + # """Process the passed feed. Will also call process for each article found in the feed. + + # Parameters + # ---------- + # feed : FeedChannelModel + # Database model for the feed. + # database : DatabaseManager + # Database connection handler, must be open. + # """ + + # log.debug("Processing feed: %s", feed.id) + + # channel = self.bot.get_channel(feed.discord_channel_id) + + # # TODO: integrate the `validate_feed` code into here, also do on list command and show errors. + + # async with aiohttp.ClientSession() as session: + + # unparsed_content = await get_unparsed_feed(feed.rss_source.rss_url) + # parsed_feed = parse(unparsed_content) + # source = Source.from_parsed(parsed_feed) + # articles = source.get_latest_articles(5) + + # if not articles: + # log.info("No articles to process for %s in ", feed.rss_source.nick, feed.discord_server_id) + # return + + # for article in articles: + # await self.process_article(feed.id, article, channel, database, session) + + # async def process_article( + # self, feed_id: int, article: Article, channel: TextChannel, database: DatabaseManager, + # session: aiohttp.ClientSession + # ): + # """Process the passed article. Will send the embed to a channel if all is valid. + + # Parameters + # ---------- + # feed_id : int + # The feed model ID, used to log the sent article. + # article : Article + # Database model for the article. + # channel : TextChannel + # Where the article will be sent to. + # database : DatabaseManager + # Database connection handler, must be open. + # """ + + # log.debug("Processing article: %s", article.url) + + # query = select(SentArticleModel).where(and_( + # SentArticleModel.article_url == article.url, + # SentArticleModel.discord_channel_id == channel.id, + # )) + # result = await database.session.execute(query) + + # if result.scalars().all(): + # log.debug("Article already processed: %s", article.url) + # return + + # embed = await article.to_embed(session) + # try: + # await channel.send(embed=embed) + # except Forbidden as error: # TODO: find some way of informing the user about this error. + # log.error("Can't send article to channel: %s · %s · %s", channel.name, channel.id, error) + # return + + # query = insert(SentArticleModel).values( + # article_url = article.url, + # discord_channel_id = channel.id, + # discord_server_id = channel.guild.id, + # discord_message_id = -1, + # feed_channel_id = feed_id + # ) + # await database.session.execute(query) + + # log.debug("new Article processed: %s", article.url) async def setup(bot): diff --git a/src/feed.py b/src/feed.py index e1456cf..e2027cb 100644 --- a/src/feed.py +++ b/src/feed.py @@ -52,7 +52,7 @@ class Article: The Article created from the feed entry. """ - log.debug("Creating Article from entry: %s", dumps(entry)) + # log.debug("Creating Article from entry: %s", dumps(entry)) published_parsed = entry.get("published_parsed") published = datetime(*entry.published_parsed[0:-2]) if published_parsed else None @@ -80,7 +80,7 @@ class Article: The thumbnail URL, or None if not found. """ - log.debug("Fetching thumbnail for article: %s", self) + # log.debug("Fetching thumbnail for article: %s", self) try: async with session.get(self.url, timeout=15) as response: @@ -111,7 +111,7 @@ class Article: A Discord Embed object representing the article. """ - log.debug(f"Creating embed from article: {self}") + # log.debug(f"Creating embed from article: {self}") # Replace HTML with Markdown, and shorten text. title = shorten(markdownify(self.title, strip=["img", "a"]), 256) @@ -164,7 +164,7 @@ class Source: The Source object """ - log.debug("Creating Source from feed: %s", dumps(feed)) + # log.debug("Creating Source from feed: %s", dumps(feed)) return cls( name=feed.get("channel", {}).get("title"), @@ -194,7 +194,7 @@ class Source: A list of Article objects. """ - log.debug("Fetching latest articles from %s, max=%s", self, max) + # log.debug("Fetching latest articles from %s, max=%s", self, max) return [ Article.from_entry(self, entry) @@ -270,24 +270,28 @@ class Subscription(DjangoDataModel): return item - # uuid: str - # name: str - # rss_url: str - # image_url: str - # creation_datetime: datetime - # server: int - # targets: list[int] - # extra_notes: str - # active: bool + async def get_channels(self, api): + channel_data, _ = await api.get_subscription_channels(subscription=self.id) + return SubChannel.from_list(channel_data) - # @staticmethod - # def parser(item: dict) -> dict: - # item["image_url"] = item.pop("image") - # item["creation_datetime"] = datetime.strptime(item["creation_datetime"], DATETIME_FORMAT) - # item["server"] = int(item["server"]) - # item["targets"] = item["targets"].split(";") - # return item +@dataclass(slots=True) +class SubChannel(DjangoDataModel): + + id: int + channel_id: int + subscription: int + + @staticmethod + def parser(item: dict) -> dict: + item["channel_id"] = int(item["channel_id"]) + item["subscription"] = int(item["subscription"]) + + return item + + @property + def mention(self) -> str: + return f"<#{self.channel_id}>" @dataclass(slots=True)