1use chrono::DateTime;
2use maud::{Markup, PreEscaped, html};
3use poem::{
4 IntoResponse as _, Response, handler,
5 web::{Data, Form, Json, Redirect, cookie::CookieJar},
6};
7use serde::Deserialize;
8
9use super::{
10 ISO8601_DATE, Post, clean_empty_string,
11 fetch::{one, one_draft},
12 parse_date, read_secret,
13};
14use crate::{
15 D, W,
16 error::{AppError, Result},
17 post,
18 template::header_extra,
19};
20
21struct PostData {
22 bsky_uri: Option<String>,
23 category: Option<String>,
24 contents: String,
25 creation_datetime: DateTime<chrono::Local>,
26 slug: String,
27 subtitle: Option<String>,
28 title: String,
29}
30
31#[derive(Copy, Clone)]
32enum PostOperation {
33 Insert,
34 Update,
35 UpdateDraft,
36}
37
38#[derive(Deserialize)]
39pub struct SubmitNewPostForm {
40 pub bsky_uri: Option<String>,
41 pub category: Option<String>,
42 pub contents: String,
43 pub creation_datetime: String,
44 pub slug: String,
45 pub subtitle: Option<String>,
46 pub title: String,
47}
48
49#[derive(Deserialize)]
50pub struct RenderDraftForm {
51 pub contents: String,
52}
53
54#[derive(Deserialize)]
55pub struct SubmitEditedPostForm {
56 pub bsky_uri: Option<String>,
57 pub category: Option<String>,
58 pub contents: String,
59 pub creation_datetime: String,
60 pub slug: String,
61 pub subtitle: Option<String>,
62 pub title: String,
63}
64
65fn check_auth(cookie_jar: &CookieJar) -> Result<()> {
66 let secret = read_secret();
67 match cookie_jar.get("secret_pass") {
68 Some(cookie) if cookie.value_str() == secret => Ok(()),
69 _ => Err(AppError::Unauthorized),
70 }
71}
72
73#[handler]
74pub fn login(body: String, cookie_jar: &CookieJar) -> Result<Response> {
75 cookie_jar.add(poem::web::cookie::Cookie::new_with_str("secret_pass", body));
76 let secret_value = cookie_jar
77 .get("secret_pass")
78 .ok_or(AppError::internal_server_error(
79 "Cookie should exist after being set".to_owned(),
80 ))?;
81 Ok(html! {(secret_value)}.into_response())
82}
83
84#[handler]
85pub fn new_post(cookie_jar: &CookieJar, Data(conn): D<&W>) -> Result<Response> {
86 check_auth(cookie_jar)?;
87
88 let post = one_draft(conn)?;
89
90 let response = render_post_form(&post, "POST", "/blog/new", "new");
91 Ok(response.into_response())
92}
93
94#[handler]
95pub fn edit_post(
96 cookie_jar: &CookieJar,
97 poem::web::Path(slug): poem::web::Path<String>,
98 Data(conn): D<&W>,
99) -> Result<Response> {
100 check_auth(cookie_jar)?;
101
102 let post = one(slug.to_string(), conn)?;
103 let response = render_post_form(&post, "POST", &format!("/blog/edit/{slug}"), "edit");
104 Ok(response.into_response())
105}
106
107fn render_post_form(post: &Post, method: &str, action: &str, js_mode: &str) -> Markup {
108 html! {
109 ( header_extra(&html! {
110 script defer src="/static/js/footnotes.js" {}
111 }) )
112 body {
113 form method=(method) action=(action) {
114 #editorWrapper {
115 .editorInnerWrapper {
116 .editorInputs {
117 details {
118 summary { "Options" }
119 .contents {
120 label {
121 "Title: "
122 input name="title" value=(post.title.clone()) {}
123 }
124 label {
125 "Slug: "
126 input name="slug" value=(post.slug.clone()) {}
127 }
128 label {
129 "dtl: "
130 input name="creation_datetime" type="datetime-local" value=(post.creation_datetime.format(ISO8601_DATE)) {}
131 }
132 label {
133 "Subtitle: "
134 input name="subtitle" value=[post.subtitle.clone()] {}
135 }
136 label {
137 "Category: "
138 input name="category" value=[post.category.clone()] {}
139 }
140 label {
141 span {"bsky_uri: " a href="https://pdsls.dev" {"pdsls"}}
142 input name="bsky_uri" value=[post.bsky_uri.clone()] {}
143 }
144 label {
145 span { "Publish!" }
146 button type="submit" disabled style="display: none" aria-hidden="true";
147 button type="submit" { "Publish" }
148 }
149 }
150
151 }
152 }
153 textarea #editor name="contents" { (post.contents.clone()) }
154 }
155 #editorPreview { "hii :3" }
156 }
157 }
158
159 script type="module" {
160 (PreEscaped(format!(r#"
161 import {{ initPostEditor }} from "/static/js/post-editor.js";
162 initPostEditor("{js_mode}");
163 "#)))
164 }
165 }
166 }
167}
168
169#[handler]
170pub fn submit_new_post(
171 cookie_jar: &CookieJar,
172 Form(form): Form<SubmitNewPostForm>,
173 Data(conn): D<&W>,
174) -> Result<Response> {
175 check_auth(cookie_jar)?;
176
177 let creation_datetime = parse_date(&form.creation_datetime).with_timezone(&chrono::Local);
178
179 let post_data = PostData {
180 title: form.title,
181 contents: form.contents,
182 slug: form.slug.clone(),
183 subtitle: form.subtitle,
184 category: form.category,
185 bsky_uri: form.bsky_uri,
186 creation_datetime,
187 };
188
189 save_post(conn, &post_data, PostOperation::Insert)?;
190
191 Ok(Redirect::see_other(format!("/blog/{}", form.slug)).into_response())
192}
193
194#[handler]
195pub fn submit_edited_post(
196 cookie_jar: &CookieJar,
197 Form(form): Form<SubmitEditedPostForm>,
198 Data(conn): D<&W>,
199) -> Result<Response> {
200 check_auth(cookie_jar)?;
201
202 let creation_datetime = parse_date(&form.creation_datetime).with_timezone(&chrono::Local);
203
204 let post_data = PostData {
205 title: form.title,
206 contents: form.contents,
207 slug: form.slug.clone(),
208 subtitle: form.subtitle,
209 category: form.category,
210 bsky_uri: form.bsky_uri,
211 creation_datetime,
212 };
213
214 save_post(conn, &post_data, PostOperation::Update)?;
215
216 Ok(Redirect::see_other(format!("/blog/{}", form.slug)).into_response())
217}
218
219#[handler]
220pub fn update_draft(
221 cookie_jar: &CookieJar,
222 Json(form): Json<SubmitNewPostForm>,
223 Data(conn): D<&W>,
224) -> Result<Response> {
225 check_auth(cookie_jar)?;
226
227 let creation_datetime = parse_date(&form.creation_datetime).with_timezone(&chrono::Local);
228
229 let post_data = PostData {
230 title: form.title,
231 contents: form.contents.clone(),
232 slug: form.slug,
233 subtitle: form.subtitle,
234 category: form.category,
235 bsky_uri: form.bsky_uri,
236 creation_datetime,
237 };
238
239 save_post(conn, &post_data, PostOperation::UpdateDraft)?;
240
241 let rendered = post::render::render(&form.contents)?;
242 Ok(rendered.into_response())
243}
244
245#[handler]
246pub fn render_draft(cookie_jar: &CookieJar, Json(form): Json<RenderDraftForm>) -> Result<Response> {
247 check_auth(cookie_jar)?;
248
249 let rendered = post::render::render(&form.contents)?;
250 Ok(rendered.into_response())
251}
252
253fn save_post(conn: &W, post_data: &PostData, operation: PostOperation) -> Result<()> {
254 let conn = conn.lock()?;
255
256 let query = match operation {
257 PostOperation::Insert => {
258 "INSERT INTO post (title, contents, slug, subtitle, category, bsky_uri, creation_datetime) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
259 }
260 PostOperation::Update => {
261 "UPDATE post SET title = ?1, contents = ?2, slug = ?3, subtitle = ?4, category = ?5, bsky_uri = ?6, creation_datetime = ?7 WHERE slug = ?3"
262 }
263 PostOperation::UpdateDraft => {
264 "UPDATE draft SET title = ?1, contents = ?2, slug = ?3, subtitle = ?4, category = ?5, bsky_uri = ?6, creation_datetime = ?7"
265 }
266 };
267
268 let mut stmt = conn.prepare(query).map_err(AppError::from)?;
269
270 stmt.execute((
271 &post_data.title,
272 &post_data.contents,
273 &post_data.slug,
274 clean_empty_string(post_data.subtitle.clone()),
275 clean_empty_string(post_data.category.clone()),
276 clean_empty_string(post_data.bsky_uri.clone()),
277 post_data.creation_datetime,
278 ))
279 .map_err(AppError::from)?;
280
281 Ok(())
282}