| 1 |
|
| 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 |
|
| 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('&', "&") |
| 85 |
.replace('<', "<") |
| 86 |
.replace('>', ">") |
| 87 |
.replace('"', """) |
| 88 |
} |
| 89 |
|