diff --git a/app/fonts/FDIcons.ttf b/app/fonts/FDIcons.ttf index 9c2be9e..cde5a09 100644 Binary files a/app/fonts/FDIcons.ttf and b/app/fonts/FDIcons.ttf differ diff --git a/app/lib/models/source.dart b/app/lib/models/source.dart index f96ea5a..561cf79 100644 --- a/app/lib/models/source.dart +++ b/app/lib/models/source.dart @@ -12,6 +12,7 @@ import 'package:feeddeck/utils/constants.dart'; /// - [mastodon] /// - [medium] /// - [nitter] +/// - [pinterest] /// - [podcast] /// - [reddit] /// - [rss] @@ -30,6 +31,7 @@ enum FDSourceType { mastodon, medium, nitter, + pinterest, podcast, reddit, rss, @@ -62,6 +64,8 @@ extension FDSourceTypeExtension on FDSourceType { return 'Medium'; case FDSourceType.nitter: return 'Nitter'; + case FDSourceType.pinterest: + return 'Pinterest'; case FDSourceType.podcast: return 'Podcast'; case FDSourceType.reddit: @@ -95,6 +99,8 @@ extension FDSourceTypeExtension on FDSourceType { return const Color(0xff000000); case FDSourceType.nitter: return const Color(0xffff6c60); + case FDSourceType.pinterest: + return const Color(0xffe60023); case FDSourceType.podcast: return const Color(0xff872ec4); case FDSourceType.reddit: @@ -188,6 +194,7 @@ class FDSourceOptions { String? mastodon; String? medium; String? nitter; + String? pinterest; String? podcast; String? reddit; String? rss; @@ -202,6 +209,7 @@ class FDSourceOptions { this.mastodon, this.medium, this.nitter, + this.pinterest, this.podcast, this.reddit, this.rss, @@ -233,6 +241,10 @@ class FDSourceOptions { responseData.containsKey('nitter') && responseData['nitter'] != null ? responseData['nitter'] : null, + pinterest: responseData.containsKey('pinterest') && + responseData['pinterest'] != null + ? responseData['pinterest'] + : null, podcast: responseData.containsKey('podcast') && responseData['podcast'] != null ? responseData['podcast'] @@ -269,6 +281,7 @@ class FDSourceOptions { 'mastodon': mastodon, 'medium': medium, 'nitter': nitter, + 'pinterest': pinterest, 'podcast': podcast, 'reddit': reddit, 'rss': rss, diff --git a/app/lib/utils/fd_icons.dart b/app/lib/utils/fd_icons.dart index 3620398..8adb55b 100644 --- a/app/lib/utils/fd_icons.dart +++ b/app/lib/utils/fd_icons.dart @@ -29,6 +29,8 @@ class FDIcons { IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData googlenews = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData pinterest = + IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData medium = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData nitter = diff --git a/app/lib/widgets/item/details/item_details.dart b/app/lib/widgets/item/details/item_details.dart index c82aae0..9b3c214 100644 --- a/app/lib/widgets/item/details/item_details.dart +++ b/app/lib/widgets/item/details/item_details.dart @@ -10,6 +10,7 @@ import 'package:feeddeck/utils/openurl.dart'; import 'package:feeddeck/widgets/item/details/item_details_mastodon.dart'; import 'package:feeddeck/widgets/item/details/item_details_medium.dart'; import 'package:feeddeck/widgets/item/details/item_details_nitter.dart'; +import 'package:feeddeck/widgets/item/details/item_details_pinterest.dart'; import 'package:feeddeck/widgets/item/details/item_details_podcast.dart'; import 'package:feeddeck/widgets/item/details/item_details_reddit.dart'; import 'package:feeddeck/widgets/item/details/item_details_rss.dart'; @@ -85,6 +86,11 @@ class ItemDetails extends StatelessWidget { item: item, source: source, ); + case FDSourceType.pinterest: + return ItemDetailsPinterest( + item: item, + source: source, + ); case FDSourceType.podcast: return ItemDetailsPodcast( item: item, diff --git a/app/lib/widgets/item/details/item_details_pinterest.dart b/app/lib/widgets/item/details/item_details_pinterest.dart new file mode 100644 index 0000000..831a622 --- /dev/null +++ b/app/lib/widgets/item/details/item_details_pinterest.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:feeddeck/models/item.dart'; +import 'package:feeddeck/models/source.dart'; +import 'package:feeddeck/widgets/item/details/utils/item_description.dart'; +import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart'; +import 'package:feeddeck/widgets/item/details/utils/item_title.dart'; + +class ItemDetailsPinterest extends StatelessWidget { + const ItemDetailsPinterest({ + super.key, + required this.item, + required this.source, + }); + + final FDItem item; + final FDSource source; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ItemTitle( + itemTitle: item.title, + ), + ItemSubtitle( + item: item, + source: source, + ), + ItemDescription( + itemDescription: item.description, + sourceFormat: DescriptionFormat.html, + tagetFormat: DescriptionFormat.markdown, + ), + ], + ); + } +} diff --git a/app/lib/widgets/item/details/utils/item_title.dart b/app/lib/widgets/item/details/utils/item_title.dart index 307d7c7..0654524 100644 --- a/app/lib/widgets/item/details/utils/item_title.dart +++ b/app/lib/widgets/item/details/utils/item_title.dart @@ -12,6 +12,10 @@ class ItemTitle extends StatelessWidget { @override Widget build(BuildContext context) { + if (itemTitle.isEmpty) { + return Container(); + } + return SelectableText( itemTitle, textAlign: TextAlign.left, diff --git a/app/lib/widgets/item/preview/item_preview.dart b/app/lib/widgets/item/preview/item_preview.dart index 9d54f9e..9c73410 100644 --- a/app/lib/widgets/item/preview/item_preview.dart +++ b/app/lib/widgets/item/preview/item_preview.dart @@ -10,6 +10,7 @@ import 'package:feeddeck/widgets/item/preview/item_preview_googlenews.dart'; import 'package:feeddeck/widgets/item/preview/item_preview_mastodon.dart'; import 'package:feeddeck/widgets/item/preview/item_preview_medium.dart'; import 'package:feeddeck/widgets/item/preview/item_preview_nitter.dart'; +import 'package:feeddeck/widgets/item/preview/item_preview_pinterest.dart'; import 'package:feeddeck/widgets/item/preview/item_preview_podcast.dart'; import 'package:feeddeck/widgets/item/preview/item_preview_reddit.dart'; import 'package:feeddeck/widgets/item/preview/item_preview_rss.dart'; @@ -67,6 +68,11 @@ class ItemPreview extends StatelessWidget { item: item, source: source, ); + case FDSourceType.pinterest: + return ItemPreviewPinterest( + item: item, + source: source, + ); case FDSourceType.podcast: return ItemPreviewPodcast( item: item, diff --git a/app/lib/widgets/item/preview/item_preview_pinterest.dart b/app/lib/widgets/item/preview/item_preview_pinterest.dart new file mode 100644 index 0000000..91a6236 --- /dev/null +++ b/app/lib/widgets/item/preview/item_preview_pinterest.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:feeddeck/models/item.dart'; +import 'package:feeddeck/models/source.dart'; +import 'package:feeddeck/widgets/item/preview/utils/details.dart'; +import 'package:feeddeck/widgets/item/preview/utils/item_actions.dart'; +import 'package:feeddeck/widgets/item/preview/utils/item_description.dart'; +import 'package:feeddeck/widgets/item/preview/utils/item_media.dart'; +import 'package:feeddeck/widgets/item/preview/utils/item_source.dart'; +import 'package:feeddeck/widgets/item/preview/utils/item_title.dart'; + +class ItemPreviewPinterest extends StatelessWidget { + const ItemPreviewPinterest({ + super.key, + required this.item, + required this.source, + }); + + final FDItem item; + final FDSource source; + + @override + Widget build(BuildContext context) { + return ItemActions( + item: item, + onTap: () => showDetails(context, item, source), + children: [ + ItemSource( + sourceTitle: source.title, + sourceSubtitle: source.type.toLocalizedString(), + sourceType: source.type, + sourceIcon: source.icon, + itemPublishedAt: item.publishedAt, + itemIsRead: item.isRead, + ), + ItemTitle( + itemTitle: item.title, + ), + ItemMedia( + itemMedia: item.media, + ), + ItemDescription( + itemDescription: item.description, + sourceFormat: DescriptionFormat.html, + tagetFormat: DescriptionFormat.plain, + ), + ], + ); + } +} diff --git a/app/lib/widgets/item/preview/utils/item_description.dart b/app/lib/widgets/item/preview/utils/item_description.dart index e338c16..7be76be 100644 --- a/app/lib/widgets/item/preview/utils/item_description.dart +++ b/app/lib/widgets/item/preview/utils/item_description.dart @@ -82,6 +82,10 @@ class ItemDescription extends StatelessWidget { /// [_buildPlain] renders the provided [content] as plain text. Widget _buildPlain(String content) { + if (content == '') { + return Container(); + } + return Container( padding: const EdgeInsets.only( bottom: Constants.spacingExtraSmall, diff --git a/app/lib/widgets/source/add/add_source.dart b/app/lib/widgets/source/add/add_source.dart index 0a8d946..75be020 100644 --- a/app/lib/widgets/source/add/add_source.dart +++ b/app/lib/widgets/source/add/add_source.dart @@ -1,3 +1,4 @@ +import 'package:feeddeck/widgets/source/add/add_source_pinterest.dart'; import 'package:flutter/material.dart'; import 'package:feeddeck/models/column.dart'; @@ -63,6 +64,10 @@ class _AddSourceState extends State { return AddSourceNitter(column: widget.column); } + if (_sourceType == FDSourceType.pinterest) { + return AddSourcePinterst(column: widget.column); + } + if (_sourceType == FDSourceType.podcast) { return AddSourcePodcast(column: widget.column); } @@ -75,6 +80,10 @@ class _AddSourceState extends State { return AddSourceRSS(column: widget.column); } + if (_sourceType == FDSourceType.stackoverflow) { + return AddSourceStackOverflow(column: widget.column); + } + if (_sourceType == FDSourceType.tumblr) { return AddSourceTumblr(column: widget.column); } @@ -83,10 +92,6 @@ class _AddSourceState extends State { // return AddSourceX(column: widget.column); // } - if (_sourceType == FDSourceType.stackoverflow) { - return AddSourceStackOverflow(column: widget.column); - } - if (_sourceType == FDSourceType.youtube) { return AddSourceYouTube(column: widget.column); } diff --git a/app/lib/widgets/source/add/add_source_pinterest.dart b/app/lib/widgets/source/add/add_source_pinterest.dart new file mode 100644 index 0000000..7ebcbaa --- /dev/null +++ b/app/lib/widgets/source/add/add_source_pinterest.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:provider/provider.dart'; + +import 'package:feeddeck/models/column.dart'; +import 'package:feeddeck/models/source.dart'; +import 'package:feeddeck/repositories/app_repository.dart'; +import 'package:feeddeck/utils/api_exception.dart'; +import 'package:feeddeck/utils/constants.dart'; +import 'package:feeddeck/utils/openurl.dart'; +import 'package:feeddeck/widgets/source/add/add_source_form.dart'; + +const _helpText = ''' +The Pinterest source can be used to follow your favorite Pinterest users or +boards. + +- **User**: `@username` or `https://www.pinterest.com/username/` +- **Board**: `@username/board` or `https://www.pinterest.com/username/board/` +'''; + +/// The [AddSourcePinterest] widget is used to display the form to add a new +/// Pinterest source. +class AddSourcePinterst extends StatefulWidget { + const AddSourcePinterst({ + super.key, + required this.column, + }); + + final FDColumn column; + + @override + State createState() => _AddSourcePinterstState(); +} + +class _AddSourcePinterstState extends State { + final _formKey = GlobalKey(); + final _pinterestController = TextEditingController(); + bool _isLoading = false; + String _error = ''; + + /// [_addSource] adds a new Pinterst source. To add a new Pinterest source the + /// user must provide the URL of an user / a board or an url via the + /// [_pinterestController]. + Future _addSource() async { + setState(() { + _isLoading = true; + _error = ''; + }); + + try { + AppRepository app = Provider.of(context, listen: false); + await app.addSource( + widget.column.id, + FDSourceType.pinterest, + FDSourceOptions( + pinterest: _pinterestController.text, + ), + ); + setState(() { + _isLoading = false; + _error = ''; + }); + if (mounted) { + Navigator.of(context).pop(); + } + } on ApiException catch (err) { + setState(() { + _isLoading = false; + _error = 'Failed to add source: ${err.message}'; + }); + } catch (err) { + setState(() { + _isLoading = false; + _error = 'Failed to add source: ${err.toString()}'; + }); + } + } + + @override + void dispose() { + _pinterestController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AddSourceForm( + onTap: _addSource, + isLoading: _isLoading, + error: _error, + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownBody( + selectable: true, + data: _helpText, + onTapLink: (text, href, title) { + try { + if (href != null) { + openUrl(href); + } + } catch (_) {} + }, + ), + const SizedBox( + height: Constants.spacingMiddle, + ), + TextFormField( + controller: _pinterestController, + keyboardType: TextInputType.text, + autocorrect: false, + enableSuggestions: true, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Pinterest Url, Username or Board', + ), + onFieldSubmitted: (value) => _addSource(), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/widgets/source/source_icon.dart b/app/lib/widgets/source/source_icon.dart index 79d091d..475524d 100644 --- a/app/lib/widgets/source/source_icon.dart +++ b/app/lib/widgets/source/source_icon.dart @@ -79,6 +79,13 @@ class SourceIcon extends StatelessWidget { type.color, const Color(0xffffffff), ); + case FDSourceType.pinterest: + return buildIcon( + FDIcons.pinterest, + iconSize, + type.color, + const Color(0xffffffff), + ); case FDSourceType.podcast: return buildIcon( Icons.podcasts, diff --git a/app/templates/iconfont/config.json b/app/templates/iconfont/config.json index fcde60f..172fc51 100644 --- a/app/templates/iconfont/config.json +++ b/app/templates/iconfont/config.json @@ -299,6 +299,20 @@ "search": [ "mastodon" ] + }, + { + "uid": "84fb197462be8b1518105fbfaf61716f", + "css": "pinterest", + "code": 59397, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M524 0C318.5 0 115.3 137 115.3 358.8 115.3 499.9 194.7 580 242.7 580 262.6 580 274 524.7 274 509.1 274 490.5 226.5 450.8 226.5 373.3 226.5 212.2 349.1 98 507.8 98 644.3 98 745.2 175.5 745.2 318 745.2 424.4 702.6 623.9 564.3 623.9 514.4 623.9 471.7 587.8 471.7 536.2 471.7 460.4 524.6 387.1 524.6 309 524.6 176.3 336.5 200.4 336.5 360.6 336.5 394.3 340.7 431.6 355.7 462.2 328.1 581.2 271.6 758.6 271.6 881.2 271.6 919 277 956.3 280.6 994.2 287.4 1001.8 284 1001 294.4 997.2 395.4 858.9 391.8 831.9 437.5 651 462.1 697.8 525.8 723.1 576.3 723.1 789.1 723.1 884.7 515.7 884.7 328.8 884.7 129.8 712.8 0 524 0Z", + "width": 1000 + }, + "search": [ + "pinterest" + ] } ] } \ No newline at end of file diff --git a/app/templates/iconfont/iconfont.afdesign b/app/templates/iconfont/iconfont.afdesign index cd31217..ddfbd06 100644 Binary files a/app/templates/iconfont/iconfont.afdesign and b/app/templates/iconfont/iconfont.afdesign differ diff --git a/app/templates/iconfont/pinterest.svg b/app/templates/iconfont/pinterest.svg new file mode 100644 index 0000000..1fa8833 --- /dev/null +++ b/app/templates/iconfont/pinterest.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/landing/app/page.tsx b/landing/app/page.tsx index a867c08..1f10591 100644 --- a/landing/app/page.tsx +++ b/landing/app/page.tsx @@ -67,6 +67,7 @@ export default function Home() { + @@ -252,6 +253,24 @@ const Nitter = () => ( ); +const Pinterest = () => ( +
+ + + + + +
Pinterest
+
+); + const Reddit = () => (
{ + if (!url.startsWith('https://www.')) { + return false; + } + + const parsedUrl = new URL(url); + + for (const pinterestUrl of pinterestUrls) { + if (parsedUrl.hostname.endsWith(pinterestUrl)) { + return true; + } + } + return false; +}; + +export const getPinterestFeed = async ( + _supabaseClient: SupabaseClient, + _redisClient: Redis | undefined, + _profile: IProfile, + source: ISource, +): Promise<{ source: ISource; items: IItem[] }> => { + /** + * Since the `pinterest` option supports multiple input format we need to + * normalize it to a valid Pinterest feed url. If this is not possible we + * consider the provided option as invalid. + */ + if (source.options?.pinterest) { + const input = source.options.pinterest; + /** + * If the input starts with `@` we assume that a username or board was + * provided in the form of `@username` or `@username/board`. We then use + * the provided username or board to generate a valid Pinterest feed url. + */ + if (input.length > 1 && input[0] === '@') { + if (input.includes('/')) { + source.options.pinterest = `https://www.pinterest.com/${ + input.substring(1) + }.rss`; + } else { + source.options.pinterest = `https://www.pinterest.com/${ + input.substring(1) + }/feed.rss`; + } + } else { + /** + * If the input does not start with `@` we assume that a valid Pinterest + * url was provided and replace the domain with `pinterest.com`. + * + * If the url ends with `.rss` or `/feed.rss` we consider that already a + * Pinterest RSS feed url was provided and use it as is. + * + * If the url does not end with `.rss` or `/feed.rss` we have to generate + * the feed url, by appending `.rss` or `/feed.rss` to the url, depending + * on if the url contains a `/` or not. + */ + if (isPinterestUrl(input)) { + const pinterestDotComUrl = replaceDomain(input); + if ( + pinterestDotComUrl.endsWith('.rss') || + pinterestDotComUrl.endsWith('/feed.rss') + ) { + source.options.pinterest = pinterestDotComUrl; + } else { + const urlParameters = pinterestDotComUrl.replace( + 'https://www.pinterest.com/', + '', + ).replace(/\/$/, ''); + if (urlParameters.includes('/')) { + source.options.pinterest = + `https://www.pinterest.com/${urlParameters}.rss`; + } else { + source.options.pinterest = + `https://www.pinterest.com/${urlParameters}/feed.rss`; + } + } + } else { + throw new Error('Invalid source options'); + } + } + } else { + throw new Error('Invalid source options'); + } + + /** + * Get the RSS for the provided `pinterest` url and parse it. If a feed + * doesn't contains an item we return an error. + */ + const response = await fetchWithTimeout(source.options.pinterest, { + method: 'get', + }, 5000); + const xml = await response.text(); + log('debug', 'Add source', { + sourceType: 'pinterest', + requestUrl: source.options.pinterest, + responseStatus: response.status, + }); + const feed = await parseFeed(xml); + + if (!feed.title.value) { + throw new Error('Invalid feed'); + } + + /** + * Generate a source id based on the user id, column id and the normalized + * `pinterest` url. Besides that we also set the source type to `pinterest`and + * set the title and link for the source. + */ + if (source.id === '') { + source.id = generateSourceId( + source.userId, + source.columnId, + source.options.pinterest, + ); + } + source.type = 'pinterest'; + source.title = feed.title.value; + if (feed.links.length > 0) { + source.link = feed.links[0]; + } + + /** + * Now that the source does contain all the required information we can start + * to generate the items for the source, by looping over all the feed entries. + */ + const items: IItem[] = []; + + for (const [index, entry] of feed.entries.entries()) { + if (skipEntry(index, entry, source.updatedAt || 0)) { + continue; + } + + /** + * Each item need a unique id which is generated using the `generateItemId` + * function. The id is a combination of the source id and the id of the + * entry or if the entry does not have an id we use the link of the first + * link of the entry. + */ + let itemId = ''; + if (entry.id != '') { + itemId = generateItemId(source.id, entry.id); + } else if (entry.links.length > 0 && entry.links[0].href) { + itemId = generateItemId(source.id, entry.links[0].href); + } else { + continue; + } + + /** + * Create the item object and add it to the `items` array. + */ + items.push({ + id: itemId, + userId: source.userId, + columnId: source.columnId, + sourceId: source.id, + title: entry.title?.value ?? '', + link: entry.links[0].href!, + media: getMedia(entry), + description: getItemDescription(entry), + author: `@${ + source.options.pinterest.replace('https://www.pinterest.com/', '') + .replace('.rss', '').replace('/feed.rss', '').split('/')[0] + }`, + publishedAt: Math.floor(entry.published!.getTime() / 1000), + }); + } + + return { source, items }; +}; + +/** + * `replaceDomain` replaces the domain of the Pinterest url with + * `pinterest.com`. + */ +const replaceDomain = (url: string): string => { + let finalUrl = url; + + for (const pinterestUrl of pinterestUrls) { + if (pinterestUrl !== 'pinterest.com') { + finalUrl = finalUrl.replace(pinterestUrl, 'pinterest.com'); + } + } + return finalUrl; +}; + +/** + * `skipEntry` is used to determin if an entry should be skipped or not. When a + * entry in the RSS feed is skipped it will not be added to the database. An + * entry will be skipped when + * - it is not within the first 50 entries of the feed, because we only keep the + * last 50 items of each source in our + * delete logic. + * - the entry does not contain a title, a link or a published date. + * - the published date of the entry is older than the last update date of the + * source minus 10 seconds. + */ +const skipEntry = ( + index: number, + entry: FeedEntry, + sourceUpdatedAt: number, +): boolean => { + if (index === 50) { + return true; + } + + if ((entry.links.length === 0 || !entry.links[0].href) || !entry.published) { + return true; + } + + if (Math.floor(entry.published.getTime() / 1000) <= (sourceUpdatedAt - 10)) { + return true; + } + + return false; +}; + +/** + * `generateSourceId` generates a unique source id based on the user id, column + * id and the link of the RSS feed. We use the MD5 algorithm for the link to + * generate the id. + */ +const generateSourceId = ( + userId: string, + columnId: string, + link: string, +): string => { + return `pinterest-${userId}-${columnId}-${new Md5().update(link).toString()}`; +}; + +/** + * `generateItemId` generates a unique item id based on the source id and the + * identifier of the item. We use the MD5 algorithm for the identifier, which + * can be the link of the item or the id of the item. + */ +const generateItemId = (sourceId: string, identifier: string): string => { + return `${sourceId}-${new Md5().update(identifier).toString()}`; +}; + +/** + * `getItemDescription` returns the description of the item. If the item has a + * `content` property we use that as our description, otherwise we use the + * `description` property. + */ +const getItemDescription = (entry: FeedEntry): string | undefined => { + if (entry.description?.value) { + return unescape(entry.description.value); + } + + return undefined; +}; + +/** + * `getMedia` returns an image for the provided feed entry from it's content or + * description. If we could not get an image from the content or description we + * return `undefined`. + */ +const getMedia = (entry: FeedEntry): string | undefined => { + if (entry.description?.value) { + const matches = /]+\bsrc=["']([^"']+)["']/.exec( + unescape(entry.description.value), + ); + if (matches && matches.length == 2 && matches[1].startsWith('https://')) { + return matches[1]; + } + } + + return undefined; +}; diff --git a/supabase/functions/_shared/feed/rss.ts b/supabase/functions/_shared/feed/rss.ts index 544f383..16d7e40 100644 --- a/supabase/functions/_shared/feed/rss.ts +++ b/supabase/functions/_shared/feed/rss.ts @@ -203,7 +203,6 @@ const getFeedFromWebsite = async ( const $ = cheerio.load(html); const rssLink = $('link[type="application/rss+xml"]').attr('href'); - console.log(rssLink); if (!rssLink) { return undefined; } diff --git a/supabase/functions/_shared/models/source.ts b/supabase/functions/_shared/models/source.ts index 65e3da0..4c451f6 100644 --- a/supabase/functions/_shared/models/source.ts +++ b/supabase/functions/_shared/models/source.ts @@ -8,6 +8,7 @@ export type TSourceType = | 'mastodon' | 'medium' | 'nitter' + | 'pinterest' | 'podcast' | 'reddit' | 'rss' @@ -30,16 +31,17 @@ export interface ISource { } export interface ISourceOptions { - rss?: string; - youtube?: string; + github?: ISourceOptionsGithub; + googlenews?: ISourceOptionsGoogleNews; mastodon?: string; medium?: string; nitter?: string; - reddit?: string; + pinterest?: string; podcast?: string; - github?: ISourceOptionsGithub; - googlenews?: ISourceOptionsGoogleNews; + reddit?: string; + rss?: string; + stackoverflow?: ISourceOptionsStackOverflow; tumblr?: string; x?: string; - stackoverflow?: ISourceOptionsStackOverflow; + youtube?: string; }