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}