# How to integrate a new feed service ## Where should my code go? The NewsFlash application is composed of multiple crates (libraries in the Rust world). So let's first clarify where new service integrations are supposed to go. ### `news_flash_gtk` Is the graphical interface of the application. Its "only" purpose is to display the collected data to the user and let them interact with it. ### `news_flash` Is the heart of the application. This crate handles getting the feed data from the web. Either by syncing with a web-based feed service or the traditional way of downloading all feeds from the respective websites. The crate also handles writing and reading all data to the disc and provides all the necessary utilities like favicon discovery and OPML import & export. Spoiler alert: This is the crate you'll work on to implement a new service. ### API crates The news-flash group contains multiple crates that end with `_api`. These are 1:1 rust implementations of feed service API's. ## Suggested order of tackling the tasks ahead ### 1:1 API implementation Ideally, you'll start by creating or looking for a straight rust implementation of the desired service API. This will separate the API details from the more high-level integration into `news_flash`. Besides, you might be lucky and somebody in the rust community has already created exactly what you're looking for. So check [crates.io](https://crates.io/) and do a quick web search. If the service API somewhat resembles the classic google reader API there is a good chance the [greader_api](https://gitlab.com/news-flash/greader_api) crated by [\@Stunkymonkey](https://gitlab.com/Stunkymonkey) is what you should use. The other way around other projects might benefit from you creating a pure rust implementation of an API later on. ### Meta Data Next on the agenda is a state-less collection of metadata. The `ApiMetadata` trait is a way to get the internal ID of the implementation, metadata like the name and website, but also a description of what the login flow should look like and creating human-readable messages from errors. Lastly, it is the responsibility of the `ApiMetadata` to create an instance of the actual `FeedApi` covered further down. ### Configuration Each service implementation is expected to handle all the necessary configurations themselves. Generally, that means a username and a password and an URL in the case of a self-hosted service. But anything is possible. Passwords should of course only be written to disc encrypted. `news_flash` provides a utility for that. It is planned to use the respective OS secret stores at some point. But currently, there is no rust cross-platform solution that ticks all the boxes. ### FeedApi Last on the list: the above mentioned `FeedApi` trait. Simply put this trait is used to log into the account if needed, sync all the data and, mirror local modifications like adding/removing feeds and changing the state articles on the account. ## Getting your hands dirty This section will contain additional tips and information for each task. ### Template There is a [template implementation](https://gitlab.com/news-flash/news_flash/-/tree/master/src/feed_api_implementations/template) in the source tree. Copy and rename it to get the annoying boilerplate for free. ### Service API The obvious thing to do is to mimic the way other API crates are written. Here is a list of all the crates: - [feedly_api](https://gitlab.com/news-flash/feedly_api) - [miniflux_api](https://gitlab.com/news-flash/miniflux_api) - [feedbin_api](https://gitlab.com/news-flash/feedbin_api) - [fever_api](https://gitlab.com/news-flash/fever_api) - [greader_api](https://gitlab.com/news-flash/greader_api) A few basic rules: - `async` for everything that makes sense - `reqwest` as the http-client - every public method that makes use of an http-client takes a `&reqwest::Client` as one of the arguments ### Meta Data The [`ApiMetadata`](https://gitlab.com/news-flash/news_flash/-/blob/master/src/feed_api/mod.rs#L18) trait: ``` pub trait ApiMetadata { fn id(&self) -> PluginID; fn info(&self) -> FeedApiResult; fn parse_error(&self, error: &dyn Fail) -> Option; fn get_instance(&self, config: &PathBuf, portal: Box) -> FeedApiResult>; } ``` The `PluginID` is supposed to be a short string with a unique name similar to the actual service name. ``` pub struct PluginInfo { pub id: PluginID, pub name: String, pub icon: Option, pub icon_symbolic: Option, pub website: Option, pub service_type: ServiceType, pub license_type: ServiceLicense, pub service_price: ServicePrice, pub login_gui: LoginGUI, } ``` Most of the details should be pretty self-explanatory. One thing to note though is: Vector icons are very much preferred over pixel based icons. Finally, add the meta struct into this list [here](https://gitlab.com/news-flash/news_flash/-/blob/master/src/feed_api_implementations/mod.rs#L21). ### Configuration The sky is the limit here. You'll get the path to the configuration folder of NewsFlash into which you can write one or multiple files. Doing best practice, you should: - only create one configuration file - use the `PluginID` as the file-name - (de)serialize a struct to JSON ### FeedApi The [`FeedApi`](https://gitlab.com/news-flash/news_flash/-/blob/master/src/feed_api/mod.rs#L26) trait: ``` #[async_trait] pub trait FeedApi: Send + Sync { fn features(&self) -> FeedApiResult; fn has_user_configured(&self) -> FeedApiResult; fn user_name(&self) -> Option; fn get_login_data(&self) -> Option; async fn is_logged_in(&self, client: &Client) -> FeedApiResult; async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()>; async fn logout(&mut self, client: &Client) -> FeedApiResult<()>; async fn initial_sync(&self, client: &Client) -> FeedApiResult; async fn sync(&self, max_count: u32, last_sync: DateTime, client: &Client) -> FeedApiResult; async fn set_article_read(&self, articles: &[ArticleID], read: Read, client: &Client) -> FeedApiResult<()>; async fn set_article_marked(&self, articles: &[ArticleID], marked: Marked, client: &Client) -> FeedApiResult<()>; async fn set_feed_read(&self, feeds: &[FeedID], last_sync: DateTime, client: &Client) -> FeedApiResult<()>; async fn set_category_read(&self, categories: &[CategoryID], last_sync: DateTime, client: &Client) -> FeedApiResult<()>; async fn set_tag_read(&self, tags: &[TagID], last_sync: DateTime, client: &Client) -> FeedApiResult<()>; async fn set_all_read(&self, last_sync: DateTime, client: &Client) -> FeedApiResult<()>; async fn add_feed(&self, url: &Url, title: Option, category: Option, client: &Client) -> FeedApiResult<(Feed, Option)>; async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()>; async fn move_feed(&self, feed_id: &FeedID, from: &CategoryID, to: &CategoryID, client: &Client) -> FeedApiResult<()>; async fn rename_feed(&self, feed_id: &FeedID, new_title: &str, client: &Client) -> FeedApiResult; async fn add_category(&self, title: &str, parent: Option<&CategoryID>, client: &Client) -> FeedApiResult; async fn remove_category(&self, id: &CategoryID, remove_children: bool, client: &Client) -> FeedApiResult<()>; async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult; async fn move_category(&self, id: &CategoryID, parent: &CategoryID, client: &Client) -> FeedApiResult<()>; async fn import_opml(&self, opml: &str, client: &Client) -> FeedApiResult<()>; async fn add_tag(&self, title: &str, client: &Client) -> FeedApiResult; async fn remove_tag(&self, id: &TagID, client: &Client) -> FeedApiResult<()>; async fn rename_tag(&self, id: &TagID, new_title: &str, client: &Client) -> FeedApiResult; async fn tag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()>; async fn untag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()>; async fn get_favicon(&self, feed_id: &FeedID, client: &Client) -> FeedApiResult; } ``` The `PluginCapabilities` is a set of bitflags that describe what the implementation can and can't do. If something important is not covered here, feel free to extend it. ``` pub struct PluginCapabilities: u32 { const NONE = 0b0000_0000; const ADD_REMOVE_FEEDS = 0b0000_0001; const SUPPORT_CATEGORIES = 0b0000_0010; const MODIFY_CATEGORIES = 0b0000_0100; const SUPPORT_TAGS = 0b0000_1000; const SUPPORT_SUBCATEGORIES = 0b0001_0000; } ``` There are two distinct methods to sync. A *normal* `sync` and the `initial_sync`. The initial sync is happening right after the user logged into an account. It is supposed to get all the starred and tagged articles together with the unread ones. Old already read articles can be ignored by the initial sync. A sync returns a `SyncResult`: ``` pub struct SyncResult { pub feeds: Option>, pub categories: Option>, pub mappings: Option>, pub tags: Option>, pub headlines: Option>, pub articles: Option>, pub enclosures: Option>, pub taggings: Option>, } ``` Every sync needs to include the **full** list of feeds, categories, and tags. `FeedMapping`s assign a `Feed` to a `Category` since feeds can be in multiple categories on some services. `Tagging`s describe a similar relation between articles and tags. `Headline`s are a concept stolen from the tiny tiny RSS API. They can be used to update the state of an article without needing to get the whole article from the web. If that is not possible they can be ignored. Basically: If there is a way for you to know the state of already existing articles without downloading the full article use a `Headline`. Otherwise you may as well return the full `FatArticle`. ``` pub struct Headline { pub article_id: ArticleID, pub unread: Read, pub marked: Marked, } ```