Skip to main content

max / makenotwork

2.8 KB · 89 lines History Blame Raw
1 //! User embed handlers: tip button.
2
3 use axum::{
4 extract::{Path, State},
5 response::{Html, IntoResponse, Response},
6 };
7
8 use crate::{
9 db::{self, Username},
10 error::{AppError, Result},
11 AppState,
12 };
13
14 use super::item::set_embed_headers;
15
16 #[tracing::instrument(skip_all, name = "embed::tip_button")]
17 /// GET /embed/u/{username}/tip
18 pub(super) async fn tip_button(
19 State(state): State<AppState>,
20 Path(username): Path<String>,
21 ) -> Result<Response> {
22 let username = Username::new(&username).map_err(|_| AppError::NotFound)?;
23 let user = db::users::get_user_by_username(&state.db, &username)
24 .await?
25 .ok_or(AppError::NotFound)?;
26
27 if user.is_suspended() || user.is_deactivated() || !user.tips_enabled {
28 return Err(AppError::NotFound);
29 }
30
31 let display_name = user.display_name.as_deref().unwrap_or(&user.username);
32 let tip_url = format!("{}/u/{}/tip", state.config.host_url, user.username);
33 let avatar_html = user.avatar_url.as_ref()
34 .map(|url| format!(r#"<img class="avatar" src="{}" alt="">"#, html_escape(url)))
35 .unwrap_or(r#"<div class="avatar placeholder"></div>"#.to_string());
36
37 let html = format!(
38 r#"<!doctype html>
39 <html lang="en">
40 <head>
41 <meta charset="UTF-8">
42 <meta name="viewport" content="width=device-width, initial-scale=1.0">
43 <title>Support {display_name} — Makenot.work</title>
44 <style>
45 * {{ margin: 0; padding: 0; box-sizing: border-box; }}
46 body {{
47 font-family: Lato, -apple-system, sans-serif;
48 background: #ede8e1; color: #3d3530;
49 display: flex; align-items: center;
50 height: 100vh; padding: 8px 12px;
51 }}
52 .tip {{ display: flex; align-items: center; gap: 10px; width: 100%; }}
53 .avatar {{ width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }}
54 .placeholder {{ background: #f5f0eb; }}
55 .label {{ flex: 1; font-size: 13px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
56 .support-btn {{
57 background: #6c5ce7; color: #fff; border: none; border-radius: 4px;
58 padding: 8px 14px; font-size: 12px; font-weight: 600;
59 cursor: pointer; text-decoration: none; white-space: nowrap;
60 }}
61 .support-btn:hover {{ background: #5a4bd6; }}
62 </style>
63 </head>
64 <body>
65 <div class="tip">
66 {avatar}
67 <span class="label">Support @{username}</span>
68 <a class="support-btn" href="{tip_url}" target="_blank" rel="noopener">Support</a>
69 </div>
70 </body>
71 </html>"#,
72 display_name = html_escape(display_name),
73 avatar = avatar_html,
74 username = html_escape(&user.username),
75 tip_url = tip_url,
76 );
77
78 let mut response = Html(html).into_response();
79 set_embed_headers(&mut response);
80 Ok(response)
81 }
82
83 fn html_escape(s: &str) -> String {
84 s.replace('&', "&amp;")
85 .replace('<', "&lt;")
86 .replace('>', "&gt;")
87 .replace('"', "&quot;")
88 }
89