post_archiver/manager/
author.rs

1use chrono::{DateTime, Utc};
2use rusqlite::{params, OptionalExtension};
3
4use crate::{
5    manager::{PostArchiverConnection, PostArchiverManager},
6    Alias, Author, AuthorId, FileMetaId, PlatformId, Post, PostId,
7};
8
9//=============================================================
10// Querying
11//=============================================================
12impl<T> PostArchiverManager<T>
13where
14    T: PostArchiverConnection,
15{
16    /// Retrieve all authors in the archive.
17    ///
18    /// # Errors
19    ///
20    /// Returns `rusqlite::Error` if there was an error accessing the database.
21    pub fn list_authors(&self) -> Result<Vec<Author>, rusqlite::Error> {
22        let mut stmt = self.conn().prepare_cached("SELECT * FROM authors")?;
23        let authors = stmt.query_map([], Author::from_row)?;
24        authors.collect()
25    }
26    /// Find an author by their aliases.
27    ///
28    /// Checks if any of the provided aliases match an existing author in the archive.
29    ///
30    /// # Errors
31    ///
32    /// Returns `rusqlite::Error` if there was an error querying the database.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// # use post_archiver::manager::PostArchiverManager;
38    /// # use post_archiver::{AuthorId, PlatformId};
39    /// # fn example(manager: &PostArchiverManager) -> Result<(), Box<dyn std::error::Error>> {
40    /// let platform = todo!("Define your platform ID here");
41    ///
42    /// let author = manager.find_author(&[("octocat", platform)])?;
43    ///
44    /// match author {
45    ///     Some(id) => println!("Found author with ID: {}", id),
46    ///     None => println!("No author found for the given aliases"),
47    /// }
48    /// # Ok(())
49    /// # }
50    /// ```
51    pub fn find_author(
52        &self,
53        aliases: &[impl FindAlias],
54    ) -> Result<Option<AuthorId>, rusqlite::Error> {
55        if aliases.is_empty() {
56            return Ok(None);
57        }
58
59        let mut stmt = self.conn().prepare_cached(
60            "SELECT target FROM author_aliases WHERE platform = ? AND source = ?",
61        )?;
62
63        for alias in aliases {
64            if let Some(id) = stmt
65                .query_row(params![alias.platform(), alias.source()], |row| row.get(0))
66                .optional()?
67            {
68                return Ok(id);
69            }
70        }
71
72        Ok(None)
73    }
74    /// Retrieve an author by their ID.
75    ///
76    /// Fetches all information about an author including their name, links, and metadata.
77    ///
78    /// # Errors
79    ///
80    /// Returns `rusqlite::Error` if:
81    /// * The author ID does not exist
82    /// * There was an error accessing the database
83    pub fn get_author(&self, author: AuthorId) -> Result<Author, rusqlite::Error> {
84        let mut stmt = self
85            .conn()
86            .prepare_cached("SELECT * FROM authors WHERE id = ?")?;
87        stmt.query_row([author], |row| {
88            Ok(Author {
89                id: row.get("id")?,
90                name: row.get("name")?,
91                thumb: row.get("thumb")?,
92                updated: row.get("updated")?,
93            })
94        })
95    }
96}
97
98pub trait FindAlias {
99    fn source(&self) -> &str;
100    fn platform(&self) -> PlatformId;
101}
102
103impl FindAlias for (&str, PlatformId) {
104    fn source(&self) -> &str {
105        self.0
106    }
107    fn platform(&self) -> PlatformId {
108        self.1
109    }
110}
111
112#[cfg(feature = "importer")]
113impl FindAlias for crate::importer::UnsyncAlias {
114    fn source(&self) -> &str {
115        &self.source
116    }
117    fn platform(&self) -> PlatformId {
118        self.platform
119    }
120}
121
122//=============================================================
123// Modifying
124//=============================================================
125impl<T> PostArchiverManager<T>
126where
127    T: PostArchiverConnection,
128{
129    /// Add a new author to the archive.
130    ///
131    /// Inserts a new author with the given name and optional updated timestamp.
132    /// It does not check for duplicates, so ensure the author does not already exist.
133    ///
134    /// # Parameters
135    /// - updated: Optional timestamp for when the author was last updated. Defaults to the current time if not provided.
136    ///
137    ///
138    /// # Errors
139    ///
140    /// Returns `rusqlite::Error` if there was an error accessing the database.
141    pub fn add_author(
142        &self,
143        name: String,
144        updated: Option<DateTime<Utc>>,
145    ) -> Result<AuthorId, rusqlite::Error> {
146        let mut stmt = self
147            .conn()
148            .prepare_cached("INSERT INTO authors (name, updated) VALUES (?, ?) RETURNING id")?;
149
150        let id: AuthorId = stmt
151            .query_row(params![name, updated.unwrap_or_else(Utc::now)], |row| {
152                row.get(0)
153            })?;
154        Ok(id)
155    }
156    /// Remove an author from the archive.
157    ///
158    /// This operation will also remove all associated aliases.
159    /// And Author-Post relationships will be removed as well.
160    ///
161    /// # Errors
162    ///
163    /// Returns `rusqlite::Error` if there was an error accessing the database.
164    pub fn remove_author(&self, author: AuthorId) -> Result<(), rusqlite::Error> {
165        self.conn()
166            .execute("DELETE FROM authors WHERE id = ?", [author])?;
167        Ok(())
168    }
169    /// Add or update aliases for an author.
170    ///
171    /// Inserts multiple aliases for the specified author.
172    /// If alias.source and alias.platform already exist for the author, it will be replaced.
173    ///
174    /// # Parameters
175    ///
176    /// - aliases[..]: (Source, Platform, Link)
177    ///
178    /// # Errors
179    ///
180    /// Returns `rusqlite::Error` if there was an error accessing the database.
181    ///
182    /// # Examples
183    ///
184    /// ```rust
185    /// # use post_archiver::manager::PostArchiverManager;
186    /// # use post_archiver::{AuthorId, PlatformId};
187    /// # fn example(manager: &PostArchiverManager, author_id: AuthorId) -> Result<(), rusqlite::Error> {
188    /// let aliases = vec![
189    ///     ("octocat".to_string(), PlatformId(1), Some("https://example.com/octocat".to_string())),
190    ///     ("octocat2".to_string(), PlatformId(2), None),
191    /// ];
192    ///
193    /// manager.add_author_aliases(author_id, aliases)
194    /// # }
195    /// ```
196    ///
197    pub fn add_author_aliases(
198        &self,
199        author: AuthorId,
200        aliases: Vec<(String, PlatformId, Option<String>)>,
201    ) -> Result<(), rusqlite::Error> {
202        let mut stmt = self
203            .conn()
204            .prepare_cached("INSERT OR REPLACE INTO author_aliases (target, source, platform, link) VALUES (?, ?, ?, ?)")?;
205
206        for (source, platform, target) in aliases {
207            stmt.execute(params![author, source, platform, target])?;
208        }
209        Ok(())
210    }
211
212    /// Remove aliases for an author.
213    ///
214    /// Deletes the specified aliases for the given author.
215    /// If an alias does not exist, it will be ignored.
216    ///
217    /// # Parameters
218    ///
219    /// - `aliases[..]`: (Source, Platform)
220    ///
221    /// # Errors
222    ///
223    /// Returns `rusqlite::Error` if there was an error accessing the database.
224    pub fn remove_author_aliases(
225        &self,
226        author: AuthorId,
227        aliases: &[(String, PlatformId)],
228    ) -> Result<(), rusqlite::Error> {
229        let mut stmt = self.conn().prepare_cached(
230            "DELETE FROM author_aliases WHERE target = ? AND source = ? AND platform = ?",
231        )?;
232
233        for (source, platform) in aliases {
234            stmt.execute(params![author, source, platform])?;
235        }
236        Ok(())
237    }
238
239    /// Set an name of author's alias.
240    ///
241    /// # Parameters
242    ///
243    /// - `alias`: (Source, Platform)
244    ///
245    /// # Errors
246    ///
247    /// Returns `rusqlite::Error` if there was an error accessing the database.
248    pub fn set_author_alias_name(
249        &self,
250        author: AuthorId,
251        alias: &(String, PlatformId),
252        name: String,
253    ) -> Result<(), rusqlite::Error> {
254        let mut stmt = self.conn().prepare_cached(
255            "UPDATE author_aliases SET source = ? WHERE target = ? AND source = ? AND platform = ?",
256        )?;
257
258        stmt.execute(params![name, author, alias.0, alias.1])?;
259        Ok(())
260    }
261
262    /// Set an platform of author's alias.
263    ///
264    /// # Parameters
265    ///
266    /// - `alias`: (Source, Platform)
267    ///
268    /// # Errors
269    ///
270    /// Returns `rusqlite::Error` if there was an error accessing the database.
271    ///
272    pub fn set_author_alias_platform(
273        &self,
274        author: AuthorId,
275        alias: &(String, PlatformId),
276        platform: PlatformId,
277    ) -> Result<(), rusqlite::Error> {
278        let mut stmt = self
279            .conn()
280            .prepare_cached("UPDATE author_aliases SET platform = ? WHERE target = ? AND source = ? AND platform = ?")?;
281
282        stmt.execute(params![platform, author, alias.0, alias.1])?;
283        Ok(())
284    }
285
286    /// Set a link of author's alias.
287    ///
288    /// # Parameters
289    ///
290    /// - `alias`: (Source, Platform)
291    ///
292    /// # Errors
293    ///
294    /// Returns `rusqlite::Error` if there was an error accessing the database.
295    pub fn set_author_alias_link(
296        &self,
297        author: AuthorId,
298        alias: &(String, PlatformId),
299        link: Option<String>,
300    ) -> Result<(), rusqlite::Error> {
301        let mut stmt = self.conn().prepare_cached(
302            "UPDATE author_aliases SET link = ? WHERE target = ? AND source = ? AND platform = ?",
303        )?;
304
305        stmt.execute(params![link, author, alias.0, alias.1])?;
306        Ok(())
307    }
308
309    /// Set an name of author.
310    ///
311    /// # Errors
312    ///
313    /// Returns `rusqlite::Error` if there was an error accessing the database.
314    pub fn set_author_name(&self, author: AuthorId, name: String) -> Result<(), rusqlite::Error> {
315        let mut stmt = self
316            .conn()
317            .prepare_cached("UPDATE authors SET name = ? WHERE id = ?")?;
318        stmt.execute(params![name, author])?;
319        Ok(())
320    }
321
322    /// Set a thumb of author.
323    ///
324    /// # Errors
325    ///
326    /// Returns `rusqlite::Error` if there was an error accessing the database.
327    pub fn set_author_thumb(
328        &self,
329        author: AuthorId,
330        thumb: Option<FileMetaId>,
331    ) -> Result<(), rusqlite::Error> {
332        let mut stmt = self
333            .conn()
334            .prepare_cached("UPDATE authors SET thumb = ? WHERE id = ?")?;
335        stmt.execute(params![thumb, author])?;
336        Ok(())
337    }
338
339    /// Set the author's thumb to the latest post's thumb  that has a non-null thumb.
340    ///
341    /// # Errors
342    ///
343    /// Returns `rusqlite::Error` if there was an error accessing the database.
344    pub fn set_author_thumb_by_latest(&self, author: AuthorId) -> Result<(), rusqlite::Error> {
345        let mut stmt = self
346            .conn()
347            .prepare_cached("UPDATE authors SET thumb = (SELECT thumb FROM posts WHERE id IN (SELECT post FROM author_posts WHERE author = ?) AND thumb IS NOT NULL ORDER BY updated DESC LIMIT 1) WHERE id = ?")?;
348        stmt.execute(params![author, author])?;
349        Ok(())
350    }
351
352    /// Set the updated timestamp of an author.
353    ///
354    /// # Errors
355    ///
356    /// Returns `rusqlite::Error` if there was an error accessing the database.
357    pub fn set_author_updated(
358        &self,
359        author: AuthorId,
360        updated: DateTime<Utc>,
361    ) -> Result<(), rusqlite::Error> {
362        let mut stmt = self
363            .conn()
364            .prepare_cached("UPDATE authors SET updated = ? WHERE id = ?")?;
365        stmt.execute(params![updated, author])?;
366        Ok(())
367    }
368
369    pub fn set_author_updated_by_latest(&self, author: AuthorId) -> Result<(), rusqlite::Error> {
370        let mut stmt = self
371            .conn()
372            .prepare_cached("UPDATE authors SET updated = (SELECT updated FROM posts WHERE id IN (SELECT post FROM author_posts WHERE author = ?) ORDER BY updated DESC LIMIT 1) WHERE id = ?")?;
373        stmt.execute(params![author, author])?;
374        Ok(())
375    }
376}
377
378//=============================================================
379// Relationships
380//=============================================================
381impl<T> PostArchiverManager<T>
382where
383    T: PostArchiverConnection,
384{
385    /// Retrieve all aliases associated with an author.
386    ///
387    /// # Errors
388    ///
389    /// Returns `rusqlite::Error` if there was an error accessing the database.
390    pub fn list_author_aliases(&self, author: AuthorId) -> Result<Vec<Alias>, rusqlite::Error> {
391        let mut stmt = self
392            .conn()
393            .prepare_cached("SELECT * FROM author_aliases WHERE target = ?")?;
394        let tags = stmt.query_map([author], Alias::from_row)?;
395        tags.collect()
396    }
397    /// Retrieve all posts associated with an author.
398    ///
399    /// # Errors
400    ///
401    /// Returns `rusqlite::Error` if there was an error accessing the database.
402    pub fn list_author_posts(&self, author: AuthorId) -> Result<Vec<Post>, rusqlite::Error> {
403        let mut stmt = self
404            .conn()
405            .prepare_cached("SELECT posts.* FROM posts INNER JOIN author_posts ON author_posts.post = posts.id WHERE author_posts.author = ?")?;
406        let posts = stmt.query_map([author], Post::from_row)?;
407        posts.collect()
408    }
409    /// Retrieve all authors associated with a post.
410    ///
411    /// # Errors
412    ///
413    /// Returns `rusqlite::Error` if there was an error accessing the database.
414    pub fn list_post_authors(&self, post: &PostId) -> Result<Vec<Author>, rusqlite::Error> {
415        let mut stmt = self
416            .conn()
417            .prepare_cached("SELECT authors.* FROM authors INNER JOIN author_posts ON author_posts.author = authors.id WHERE author_posts.post = ?")?;
418        let authors = stmt.query_map([post], Author::from_row)?;
419        authors.collect()
420    }
421}
422
423impl Author {
424    /// Retrieve all aliases associated with this author.
425    ///
426    /// # Errors
427    ///
428    /// Returns `rusqlite::Error` if there was an error accessing the database.
429    pub fn aliases(&self, manager: &PostArchiverManager) -> Result<Vec<Alias>, rusqlite::Error> {
430        manager.list_author_aliases(self.id)
431    }
432    /// Retrieve all posts associated with this author.
433    ///
434    /// # Errors
435    ///
436    /// Returns `rusqlite::Error` if there was an error accessing the database.
437    pub fn posts(&self, manager: &PostArchiverManager) -> Result<Vec<Post>, rusqlite::Error> {
438        manager.list_author_posts(self.id)
439    }
440}
441
442impl Post {
443    /// Retrieve all authors associated with this post.
444    ///
445    /// # Errors
446    ///
447    /// Returns `rusqlite::Error` if there was an error accessing the database.
448    pub fn authors(&self, manager: &PostArchiverManager) -> Result<Vec<Author>, rusqlite::Error> {
449        manager.list_post_authors(&self.id)
450    }
451}