1#![allow(clippy::must_use_candidate)]
4#![allow(clippy::doc_markdown)]
5#![allow(clippy::option_if_let_else)]
6#![allow(clippy::cast_sign_loss)]
7#![allow(clippy::derivable_impls)]
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone)]
14pub struct AuthToken {
15 pub token: String,
17}
18
19#[derive(Debug, Clone, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct UserInfo {
23 pub user_id: String,
25 pub user_name: String,
27 pub user_email: Option<String>,
29 pub user_profile_id: Option<String>,
31}
32
33#[derive(Debug, Clone, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct Subscription {
37 pub id: String,
39 pub title: String,
41 pub url: String,
43 #[serde(default)]
45 pub html_url: Option<String>,
46 #[serde(default)]
48 pub icon_url: Option<String>,
49 #[serde(default)]
51 pub categories: Vec<Category>,
52}
53
54#[derive(Debug, Clone, Deserialize, Serialize)]
56pub struct Category {
57 pub id: String,
59 pub label: String,
61 #[serde(rename = "type", default)]
63 pub category_type: Option<String>,
64}
65
66#[derive(Debug, Deserialize)]
68pub struct SubscriptionList {
69 pub subscriptions: Vec<Subscription>,
71}
72
73#[derive(Debug, Deserialize)]
75pub struct TagList {
76 pub tags: Vec<Tag>,
78}
79
80#[derive(Debug, Clone, Deserialize)]
82pub struct Tag {
83 pub id: String,
85 #[serde(rename = "sortid", default)]
87 pub sort_id: Option<String>,
88}
89
90#[derive(Debug, Deserialize)]
92pub struct UnreadCount {
93 pub max: i64,
95 #[serde(default)]
97 pub unreadcounts: Vec<UnreadCountItem>,
98}
99
100#[derive(Debug, Deserialize)]
102pub struct UnreadCountItem {
103 pub id: String,
105 pub count: i64,
107 #[serde(rename = "newestItemTimestampUsec", default)]
109 pub newest_item_timestamp_usec: Option<String>,
110}
111
112#[derive(Debug, Deserialize)]
114pub struct StreamContents {
115 pub id: String,
117 #[serde(default)]
119 pub title: Option<String>,
120 #[serde(default)]
122 pub updated: Option<i64>,
123 #[serde(default)]
125 pub continuation: Option<String>,
126 #[serde(default)]
128 pub items: Vec<StreamItem>,
129}
130
131#[derive(Debug, Clone, Deserialize)]
133pub struct StreamItem {
134 pub id: String,
136 #[serde(default)]
138 pub origin: Option<StreamItemOrigin>,
139 #[serde(default)]
141 pub title: Option<String>,
142 #[serde(default)]
144 pub author: Option<String>,
145 #[serde(default)]
147 pub published: Option<i64>,
148 #[serde(default)]
150 pub updated: Option<i64>,
151 #[serde(rename = "crawlTimeMsec", default)]
153 pub crawl_time_msec: Option<String>,
154 #[serde(rename = "timestampUsec", default)]
156 pub timestamp_usec: Option<String>,
157 #[serde(default)]
159 pub categories: Vec<String>,
160 #[serde(default)]
162 pub canonical: Option<Vec<StreamItemLink>>,
163 #[serde(default)]
165 pub alternate: Option<Vec<StreamItemLink>>,
166 #[serde(default)]
168 pub summary: Option<StreamItemContent>,
169 #[serde(default)]
171 pub content: Option<StreamItemContent>,
172}
173
174impl StreamItem {
175 pub fn is_read(&self) -> bool {
177 self.categories
178 .iter()
179 .any(|c| c.ends_with("/state/com.google/read"))
180 }
181
182 pub fn is_starred(&self) -> bool {
184 self.categories
185 .iter()
186 .any(|c| c.ends_with("/state/com.google/starred"))
187 }
188
189 pub fn link(&self) -> Option<&str> {
191 self.canonical
192 .as_ref()
193 .and_then(|links| links.first())
194 .or_else(|| self.alternate.as_ref().and_then(|links| links.first()))
195 .map(|l| l.href.as_str())
196 }
197
198 pub fn get_content(&self) -> Option<&str> {
200 self.content
201 .as_ref()
202 .or(self.summary.as_ref())
203 .map(|c| c.content.as_str())
204 }
205
206 pub fn published_at(&self) -> Option<DateTime<Utc>> {
208 self.published
209 .and_then(|ts| DateTime::from_timestamp(ts, 0))
210 }
211
212 pub fn id_decimal(&self) -> Option<i64> {
214 parse_item_id(&self.id)
215 }
216}
217
218#[derive(Debug, Clone, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct StreamItemOrigin {
222 pub stream_id: String,
224 #[serde(default)]
226 pub title: Option<String>,
227 #[serde(default)]
229 pub html_url: Option<String>,
230}
231
232#[derive(Debug, Clone, Deserialize)]
234pub struct StreamItemLink {
235 pub href: String,
237 #[serde(rename = "type", default)]
239 pub link_type: Option<String>,
240}
241
242#[derive(Debug, Clone, Deserialize)]
244pub struct StreamItemContent {
245 #[serde(default)]
247 pub direction: Option<String>,
248 pub content: String,
250}
251
252#[derive(Debug, Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct StreamItemIds {
256 pub item_refs: Vec<ItemRef>,
258 #[serde(default)]
260 pub continuation: Option<String>,
261}
262
263#[derive(Debug, Clone, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct ItemRef {
267 pub id: String,
269 #[serde(default)]
271 pub timestamp_usec: Option<String>,
272 #[serde(default)]
274 pub direct_stream_ids: Option<Vec<String>>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct SyncConfig {
280 pub provider: SyncProvider,
282 pub server: String,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub username: Option<String>,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub password: Option<String>,
290}
291
292impl SyncConfig {
293 #[must_use]
295 pub fn get_credentials(&self) -> Option<(String, String)> {
296 let credential_key = format!("sync@{}", self.server);
298 if let Some(creds) = crate::credentials::get_credentials(&credential_key) {
299 return Some(creds);
300 }
301 match (&self.username, &self.password) {
303 (Some(u), Some(p)) => Some((u.clone(), p.clone())),
304 _ => None,
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(rename_all = "lowercase")]
312pub enum SyncProvider {
313 FreshRSS,
315 Miniflux,
317 GReader,
319}
320
321impl Default for SyncProvider {
322 fn default() -> Self {
323 Self::GReader
324 }
325}
326
327pub fn parse_item_id(id: &str) -> Option<i64> {
336 const PREFIX: &str = "tag:google.com,2005:reader/item/";
337
338 if let Some(hex) = id.strip_prefix(PREFIX) {
339 i64::from_str_radix(hex, 16).ok()
341 } else if id.len() == 16 && id.chars().all(|c| c.is_ascii_hexdigit()) {
342 i64::from_str_radix(id, 16).ok()
344 } else {
345 id.parse().ok()
347 }
348}
349
350pub fn format_item_id_long(id: i64) -> String {
352 format!("tag:google.com,2005:reader/item/{:016x}", id as u64)
353}
354
355pub fn format_item_id_short(id: i64) -> String {
357 format!("{:016x}", id as u64)
358}
359
360pub mod streams {
364 pub const READING_LIST: &str = "user/-/state/com.google/reading-list";
366 pub const READ: &str = "user/-/state/com.google/read";
368 pub const STARRED: &str = "user/-/state/com.google/starred";
370 pub const KEPT_UNREAD: &str = "user/-/state/com.google/kept-unread";
372 pub const BROADCAST: &str = "user/-/state/com.google/broadcast";
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_parse_item_id_long() {
382 let id = "tag:google.com,2005:reader/item/000000000000001f";
383 assert_eq!(parse_item_id(id), Some(31));
384 }
385
386 #[test]
387 fn test_parse_item_id_short_hex() {
388 let id = "000000000000001f";
389 assert_eq!(parse_item_id(id), Some(31));
390 }
391
392 #[test]
393 fn test_parse_item_id_decimal() {
394 let id = "31";
395 assert_eq!(parse_item_id(id), Some(31));
396 }
397
398 #[test]
399 fn test_format_item_id_long() {
400 assert_eq!(
401 format_item_id_long(31),
402 "tag:google.com,2005:reader/item/000000000000001f"
403 );
404 }
405}