[nitter] Allow Usage of Custom Nitter Instances (#19)

It is now possible to use a custom Nitter instances. This means a user
must not rely on our Nitter instance and can instead use his own
instance. To use a custom Nitter instance a user must provide the full
RSS feed url for the instance.
This commit is contained in:
Rico Berger
2023-10-03 21:51:09 +02:00
committed by GitHub
parent 181807e090
commit 86db47c291
2 changed files with 60 additions and 15 deletions

View File

@@ -14,8 +14,11 @@ import 'package:feeddeck/widgets/source/add/add_source_form.dart';
const _helpText = '''
The Nitter source can be used to add an RSS feed for Nitter:
- **RSS Feed**: `https://nitter.net/rico_berger/rss` or `https://nitter.net/search/rss?f=tweets&q=FeedDeck`
- **Username**: `@rico_berger`
- **Search Term**: `FeedDeck`
**Note:** We recommend that you use your own Nitter instance to avoid rate limiting.
''';
/// The [AddSourceNitter] widget is used to display the form to add a new Nitter

View File

@@ -26,15 +26,16 @@ export const getNitterFeed = async (
throw new Error("Invalid source options");
}
const nitterOptions = parseNitterOptions(source.options.nitter);
/**
* Get the RSS for the provided `nitter` username or search term. If a feed doesn't contains an item we return an
* error.
*/
const feedUrl = generateFeedUrl(source.options.nitter);
const response = await fetchWithTimeout(
feedUrl,
nitterOptions.feedUrl,
{
headers: {
headers: nitterOptions.isCustomInstance ? undefined : {
"Authorization": `Basic ${FEEDDECK_SOURCE_NITTER_BASIC_AUTH}`,
},
method: "get",
@@ -44,7 +45,7 @@ export const getNitterFeed = async (
const xml = await response.text();
log("debug", "Add source", {
sourceType: "nitter",
requestUrl: feedUrl,
requestUrl: nitterOptions.feedUrl,
responseStatus: response.status,
});
const feed = await parseFeed(xml);
@@ -66,7 +67,7 @@ export const getNitterFeed = async (
);
}
source.type = "nitter";
source.title = source.options.nitter;
source.title = nitterOptions.sourceTitle;
if (feed.links.length > 0) {
source.link = feed.links[0];
}
@@ -75,7 +76,7 @@ export const getNitterFeed = async (
* When the source doesn't has an icon yet and the user requested the feed of a user (string starts with `@`) we try
* to get an icon for the source.
*/
if (!source.icon && source.options.nitter[0] === "@" && feed.image?.url) {
if (!source.icon && nitterOptions.isUsername && feed.image?.url) {
source.icon = feed.image.url;
source.icon = await uploadSourceIcon(supabaseClient, source);
}
@@ -143,20 +144,61 @@ export const getNitterFeed = async (
};
/**
* `generateFeedUrl` returns the url to the RSS feed for the provided user input. By default we are using nitter.net,
* but we can also use our own instance via the `FEEDDECK_SOURCE_NITTER_INSTANCE` environment variable.
* `parseNitterOptions` parsed the Nitter options and returns an object with all the required data to get the feed and
* to create the database entry for the source.
*
* This is required, because a user can provide the RSS feed of his own Nitter instance or a username or search term,
* where we have to use our own Nitter instance.
*/
const generateFeedUrl = (input: string): string => {
let instance = FEEDDECK_SOURCE_NITTER_INSTANCE;
if (!instance) {
instance = "https://nitter.net";
const parseNitterOptions = (
options: string,
): {
feedUrl: string;
sourceTitle: string;
isUsername: boolean;
isCustomInstance: boolean;
} => {
if (options.startsWith("http://") || options.startsWith("https://")) {
if (options.endsWith("/rss")) {
return {
feedUrl: options,
sourceTitle: `@${
options.slice(
options.replace("/rss", "").lastIndexOf("/") + 1,
options.replace("/rss", "").length,
)
}`,
isUsername: true,
isCustomInstance: true,
};
}
const url = new URL(options);
return {
feedUrl: options,
sourceTitle: url.searchParams.get("q") || options,
isUsername: false,
isCustomInstance: true,
};
}
if (input[0] === "@") {
return `${instance}/${input.slice(1)}/rss`;
if (options[0] === "@") {
return {
feedUrl: `${FEEDDECK_SOURCE_NITTER_INSTANCE}/${options.slice(1)}/rss`,
sourceTitle: options,
isUsername: true,
isCustomInstance: false,
};
}
return `${instance}/search/rss?f=tweets&q=${encodeURIComponent(input)}`;
return {
feedUrl: `${FEEDDECK_SOURCE_NITTER_INSTANCE}/search/rss?f=tweets&q=${
encodeURIComponent(options)
}`,
sourceTitle: options,
isUsername: false,
isCustomInstance: false,
};
};
/**