Author: Kevin Schoon [me@kevinschoon.com]
Committer: Manos Pitsidianakis [manos@pitsidianak.is] Sun, 31 Dec 2023 18:57:33 +0000
Hash: 3a515c27186b4175f1aaa16a06a112c7ac04253c
Timestamp: Sun, 31 Dec 2023 18:57:33 +0000 (8 months ago)

+121 -114 +/-3 browse
move thread listing to core
1diff --git a/core/src/connection.rs b/core/src/connection.rs
2index bc3a1d8..6aabd6f 100644
3--- a/core/src/connection.rs
4+++ b/core/src/connection.rs
5 @@ -32,6 +32,7 @@ use crate::{
6 config::Configuration,
7 errors::{ErrorKind::*, *},
8 models::{changesets::MailingListChangeset, DbVal, ListOwner, MailingList, Post},
9+ StripCarets,
10 };
11
12 /// A connection to a `mailpot` database.
13 @@ -592,6 +593,111 @@ impl Connection {
14 Ok(ret)
15 }
16
17+ /// Fetch the contents of a single thread in the form of `(depth, post)`
18+ /// where `depth` is the reply distance between a message and the thread
19+ /// root message.
20+ pub fn list_thread(&self, list_pk: i64, root: &str) -> Result<Vec<(i64, DbVal<Post>)>> {
21+ let mut stmt = self
22+ .connection
23+ .prepare(
24+ "WITH RECURSIVE cte_replies AS MATERIALIZED
25+ (
26+ SELECT
27+ pk,
28+ message_id,
29+ REPLACE(
30+ TRIM(
31+ SUBSTR(
32+ CAST(message AS TEXT),
33+ INSTR(
34+ CAST(message AS TEXT),
35+ 'In-Reply-To: '
36+ )
37+ +
38+ LENGTH('in-reply-to: '),
39+ INSTR(
40+ SUBSTR(
41+ CAST(message AS TEXT),
42+ INSTR(
43+ CAST(message AS TEXT),
44+ 'In-Reply-To: ')
45+ +
46+ LENGTH('in-reply-to: ')
47+ ),
48+ '>'
49+ )
50+ )
51+ ),
52+ ' ',
53+ ''
54+ ) AS in_reply_to,
55+ INSTR(
56+ CAST(message AS TEXT),
57+ 'In-Reply-To: '
58+ ) AS offset
59+ FROM post
60+ WHERE
61+ offset > 0
62+ UNION
63+ SELECT
64+ pk,
65+ message_id,
66+ NULL AS in_reply_to,
67+ INSTR(
68+ CAST(message AS TEXT),
69+ 'In-Reply-To: '
70+ ) AS offset
71+ FROM post
72+ WHERE
73+ offset = 0
74+ ),
75+ cte_thread(parent, root, depth) AS (
76+ SELECT DISTINCT
77+ message_id AS parent,
78+ message_id AS root,
79+ 0 AS depth
80+ FROM cte_replies
81+ WHERE
82+ in_reply_to IS NULL
83+ UNION ALL
84+ SELECT
85+ t.message_id AS parent,
86+ cte_thread.root AS root,
87+ (cte_thread.depth + 1) AS depth
88+ FROM cte_replies
89+ AS t
90+ JOIN
91+ cte_thread
92+ ON cte_thread.parent = t.in_reply_to
93+ WHERE t.in_reply_to IS NOT NULL
94+ )
95+ SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
96+ )
97+ .unwrap();
98+ let iter = stmt
99+ .query_map(rusqlite::params![root], |row| {
100+ let parent: String = row.get("parent")?;
101+ let root: String = row.get("root")?;
102+ let depth: i64 = row.get("depth")?;
103+ Ok((parent, root, depth))
104+ })?;
105+ let mut ret = vec![];
106+ for post in iter {
107+ ret.push(post?);
108+ }
109+ let posts = self.list_posts(list_pk, None).unwrap();
110+ let ret = ret.into_iter()
111+ .filter_map(|(m, _, depth)| {
112+ posts
113+ .iter()
114+ .find(|p| m.as_str().strip_carets() == p.message_id.as_str().strip_carets())
115+ .map(|p| (depth, p.clone()))
116+ })
117+ .skip(1)
118+ .collect();
119+ Ok(ret)
120+ }
121+
122 /// Fetch the owners of a mailing list.
123 pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
124 let mut stmt = self
125 diff --git a/web/src/lists.rs b/web/src/lists.rs
126index 6499e10..ef99149 100644
127--- a/web/src/lists.rs
128+++ b/web/src/lists.rs
129 @@ -19,6 +19,7 @@
130
131 use chrono::TimeZone;
132 use indexmap::IndexMap;
133+ use mailpot::models::Post;
134
135 use super::*;
136
137 @@ -172,7 +173,20 @@ pub async fn list_post(
138 StatusCode::NOT_FOUND,
139 ));
140 };
141- let thread = super::utils::thread_db(&db, list.pk, &post.message_id);
142+ let thread: Vec<(i64, DbVal<Post>, String, String)> = {
143+ let thread: Vec<(i64, DbVal<Post>)> = db.list_thread(list.pk, &post.message_id)?;
144+
145+ thread
146+ .into_iter()
147+ .map(|(depth, p)| {
148+ let envelope = melib::Envelope::from_bytes(p.message.as_slice(), None).unwrap();
149+ let body = envelope.body_bytes(p.message.as_slice());
150+ let body_text = body.text();
151+ let date = envelope.date_as_str().to_string();
152+ (depth, p, body_text, date)
153+ })
154+ .collect()
155+ };
156 let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
157 .with_status(StatusCode::BAD_REQUEST)?;
158 let body = envelope.body_bytes(post.message.as_slice());
159 diff --git a/web/src/utils.rs b/web/src/utils.rs
160index 96af462..3d2c35e 100644
161--- a/web/src/utils.rs
162+++ b/web/src/utils.rs
163 @@ -301,119 +301,6 @@ pub struct ThreadEntry {
164 pub datetime: String,
165 }
166
167- pub fn thread_db(
168- db: &mailpot::Connection,
169- list: i64,
170- root: &str,
171- ) -> Vec<(i64, DbVal<mailpot::models::Post>, String, String)> {
172- let mut stmt = db
173- .connection
174- .prepare(
175- "WITH RECURSIVE cte_replies AS MATERIALIZED
176- (
177- SELECT
178- pk,
179- message_id,
180- REPLACE(
181- TRIM(
182- SUBSTR(
183- CAST(message AS TEXT),
184- INSTR(
185- CAST(message AS TEXT),
186- 'In-Reply-To: '
187- )
188- +
189- LENGTH('in-reply-to: '),
190- INSTR(
191- SUBSTR(
192- CAST(message AS TEXT),
193- INSTR(
194- CAST(message AS TEXT),
195- 'In-Reply-To: ')
196- +
197- LENGTH('in-reply-to: ')
198- ),
199- '>'
200- )
201- )
202- ),
203- ' ',
204- ''
205- ) AS in_reply_to,
206- INSTR(
207- CAST(message AS TEXT),
208- 'In-Reply-To: '
209- ) AS offset
210- FROM post
211- WHERE
212- offset > 0
213- UNION
214- SELECT
215- pk,
216- message_id,
217- NULL AS in_reply_to,
218- INSTR(
219- CAST(message AS TEXT),
220- 'In-Reply-To: '
221- ) AS offset
222- FROM post
223- WHERE
224- offset = 0
225- ),
226- cte_thread(parent, root, depth) AS (
227- SELECT DISTINCT
228- message_id AS parent,
229- message_id AS root,
230- 0 AS depth
231- FROM cte_replies
232- WHERE
233- in_reply_to IS NULL
234- UNION ALL
235- SELECT
236- t.message_id AS parent,
237- cte_thread.root AS root,
238- (cte_thread.depth + 1) AS depth
239- FROM cte_replies
240- AS t
241- JOIN
242- cte_thread
243- ON cte_thread.parent = t.in_reply_to
244- WHERE t.in_reply_to IS NOT NULL
245- )
246- SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
247- )
248- .unwrap();
249- let iter = stmt
250- .query_map(rusqlite::params![root], |row| {
251- let parent: String = row.get("parent")?;
252- let root: String = row.get("root")?;
253- let depth: i64 = row.get("depth")?;
254- Ok((parent, root, depth))
255- })
256- .unwrap();
257- let mut ret = vec![];
258- for post in iter {
259- let post = post.unwrap();
260- ret.push(post);
261- }
262- let posts = db.list_posts(list, None).unwrap();
263- ret.into_iter()
264- .filter_map(|(m, _, depth)| {
265- posts
266- .iter()
267- .find(|p| m.as_str().strip_carets() == p.message_id.as_str().strip_carets())
268- .map(|p| {
269- let envelope = melib::Envelope::from_bytes(p.message.as_slice(), None).unwrap();
270- let body = envelope.body_bytes(p.message.as_slice());
271- let body_text = body.text();
272- let date = envelope.date_as_str().to_string();
273- (depth, p.clone(), body_text, date)
274- })
275- })
276- .skip(1)
277- .collect()
278- }
279-
280 pub fn thread(
281 envelopes: &Arc<std::sync::RwLock<HashMap<melib::EnvelopeHash, melib::Envelope>>>,
282 threads: &melib::Threads,