add(backend): move to dorsal for database
add(boards ui): change open button to expand add(posts ui): action button icons fix(pagination): make next page button clickable
This commit is contained in:
parent
e364a7f0a3
commit
53443da9d8
2
.cargo/config
Normal file
2
.cargo/config
Normal file
|
@ -0,0 +1,2 @@
|
|||
[registries]
|
||||
stellular = { index = "sparse+https://code.stellular.org/api/packages/stellular/cargo/" }
|
20
Cargo.toml
20
Cargo.toml
|
@ -3,16 +3,16 @@ name = "puffer"
|
|||
authors = ["hkau"]
|
||||
license = "MIT"
|
||||
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
postgres = []
|
||||
mysql = []
|
||||
sqlite = []
|
||||
default = ["sqlite"]
|
||||
postgres = ["dorsal/postgres"]
|
||||
mysql = ["dorsal/mysql"]
|
||||
sqlite = ["dorsal/sqlite"]
|
||||
default = ["dorsal/sqlite"]
|
||||
|
||||
[dependencies]
|
||||
actix-cors = "0.7.0"
|
||||
|
@ -20,21 +20,13 @@ actix-files = "0.6.5"
|
|||
actix-web = "4.5.1"
|
||||
askama = "0.12.1"
|
||||
awc = { version = "3.4.0", features = ["rustls"] }
|
||||
dorsal = { version = "0.1.0", registry = "stellular", default-features = false } # crates: disable-check
|
||||
comrak = "0.22.0"
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.11.3"
|
||||
hex_fmt = "0.3.0"
|
||||
redis = "0.25.2"
|
||||
regex = "1.10.4"
|
||||
serde = "1.0.197"
|
||||
serde_json = "1.0.115"
|
||||
sha2 = "0.10.8"
|
||||
sqlx = { version = "0.7.3", features = [
|
||||
"sqlite",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"any",
|
||||
"runtime-tokio",
|
||||
"tls-native-tls",
|
||||
] }
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::db::AppData;
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::db::pufferdb::AppData;
|
||||
|
||||
#[derive(Default, PartialEq, serde::Deserialize)]
|
||||
pub struct CallbackQueryProps {
|
||||
pub uid: Option<String>, // this uid will need to be sent to the client as a token
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
use crate::db::{AppData, Board, BoardMetadata, BoardPostLog, DefaultReturn};
|
||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::{
|
||||
db::pufferdb::{
|
||||
AppData, Board, BoardMetadata, BoardPostLog, DefaultReturn, UserMailStreamIdentifier,
|
||||
},
|
||||
// pages::boards,
|
||||
};
|
||||
|
||||
#[derive(Default, PartialEq, serde::Deserialize)]
|
||||
pub struct OffsetQueryProps {
|
||||
pub offset: Option<i32>,
|
||||
|
@ -89,54 +83,6 @@ pub async fn create_request(
|
|||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*}/mail")]
|
||||
pub async fn create_mail_stream_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "text/plain"))
|
||||
.body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable()
|
||||
.append_header(("Content-Type", "text/plain"))
|
||||
.body("An account is required to do this");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.create_mail_stream(&mut UserMailStreamIdentifier {
|
||||
_is_user_mail_stream: true,
|
||||
user1: token_user.unwrap().payload.unwrap().user.username,
|
||||
user2: name,
|
||||
})
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[get("/api/board/{name:.*}/posts")]
|
||||
pub async fn get_posts_request(
|
||||
req: HttpRequest,
|
||||
|
@ -418,11 +364,13 @@ pub async fn pin_post_request(req: HttpRequest, data: web::Data<AppData>) -> imp
|
|||
// this doesn't change the number of posts so we only need to refresh ALL OFFSETS
|
||||
// TODO: maybe do something to figure out the post offset if possible so we don't clear all offsets every time
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove_starting_with(format!("post-replies:{}:*", post.reply.as_ref().unwrap()))
|
||||
.await;
|
||||
} else {
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove_starting_with(format!("board-posts:{}:*", post.board))
|
||||
.await;
|
||||
|
@ -548,11 +496,13 @@ pub async fn update_post_request(
|
|||
// this doesn't change the number of posts so we only need to refresh ALL OFFSETS
|
||||
// TODO: maybe do something to figure out the post offset if possible so we don't clear all offsets every time
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove_starting_with(format!("post-replies:{}:*", post.reply.as_ref().unwrap()))
|
||||
.await;
|
||||
} else {
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove_starting_with(format!("board-posts:{}:*", post.board))
|
||||
.await;
|
||||
|
@ -736,11 +686,13 @@ pub async fn delete_post_request(req: HttpRequest, data: web::Data<AppData>) ->
|
|||
// update cache
|
||||
if post.reply.is_some() {
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove(format!("post-replies:{}", post.reply.as_ref().unwrap()))
|
||||
.await;
|
||||
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove(format!(
|
||||
"post-replies:{}:offset0",
|
||||
|
@ -752,11 +704,13 @@ pub async fn delete_post_request(req: HttpRequest, data: web::Data<AppData>) ->
|
|||
// to update the number when viewing this post in a feed, but that's a waste of time and memory
|
||||
} else {
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove(format!("board-posts:{}", post.board))
|
||||
.await;
|
||||
|
||||
data.db
|
||||
.base
|
||||
.cachedb
|
||||
.remove(format!("board-posts:{}:offset0", post.board))
|
||||
.await;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,112 +0,0 @@
|
|||
//! # CacheDB
|
||||
//!
|
||||
//! Redis connection.
|
||||
//!
|
||||
//! Identifiers should be a string following this format: `TYPE_OF_OBJECT:OBJECT_ID`. For pastes this would look like: `paste:{custom_url}`
|
||||
use redis::Commands;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheDB {
|
||||
pub client: redis::Client,
|
||||
}
|
||||
|
||||
impl CacheDB {
|
||||
pub async fn new() -> CacheDB {
|
||||
return CacheDB {
|
||||
client: redis::Client::open("redis://127.0.0.1:6379").unwrap(),
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn get_con(&self) -> redis::Connection {
|
||||
self.client.get_connection().unwrap()
|
||||
}
|
||||
|
||||
// GET
|
||||
/// Get a cache object by its identifier
|
||||
///
|
||||
/// # Arguments:
|
||||
/// * `id` - `String` of the object's id
|
||||
pub async fn get(&self, id: String) -> Option<String> {
|
||||
// fetch from database
|
||||
let mut c = self.get_con().await;
|
||||
let res = c.get(id);
|
||||
|
||||
if res.is_err() {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
// return
|
||||
Option::Some(res.unwrap())
|
||||
}
|
||||
|
||||
// SET
|
||||
/// Set a cache object by its identifier and content
|
||||
///
|
||||
/// # Arguments:
|
||||
/// * `id` - `String` of the object's id
|
||||
/// * `content` - `String` of the object's content
|
||||
pub async fn set(&self, id: String, content: String) -> bool {
|
||||
// set
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.set(id, content);
|
||||
|
||||
if res.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// return
|
||||
true
|
||||
}
|
||||
|
||||
/// Update a cache object by its identifier and content
|
||||
///
|
||||
/// # Arguments:
|
||||
/// * `id` - `String` of the object's id
|
||||
/// * `content` - `String` of the object's content
|
||||
pub async fn update(&self, id: String, content: String) -> bool {
|
||||
self.set(id, content).await
|
||||
}
|
||||
|
||||
/// Remove a cache object by its identifier
|
||||
///
|
||||
/// # Arguments:
|
||||
/// * `id` - `String` of the object's id
|
||||
pub async fn remove(&self, id: String) -> bool {
|
||||
// remove
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.del(id);
|
||||
|
||||
if res.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// return
|
||||
true
|
||||
}
|
||||
|
||||
/// Remove a cache object by its identifier('s start)
|
||||
///
|
||||
/// # Arguments:
|
||||
/// * `id` - `String` of the object's id('s start)
|
||||
pub async fn remove_starting_with(&self, id: String) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
|
||||
// get keys
|
||||
let mut cmd = redis::cmd("DEL");
|
||||
let keys: Result<Vec<String>, redis::RedisError> = c.keys(id);
|
||||
|
||||
for key in keys.unwrap() {
|
||||
cmd.arg(key);
|
||||
}
|
||||
|
||||
// remove
|
||||
let res: Result<String, redis::RedisError> = cmd.query(&mut c);
|
||||
|
||||
if res.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// return
|
||||
true
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
pub mod pufferdb;
|
||||
pub mod cachedb;
|
||||
pub mod sql;
|
100
src/db/sql.rs
100
src/db/sql.rs
|
@ -1,100 +0,0 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct DatabaseOpts {
|
||||
pub _type: Option<String>,
|
||||
pub host: Option<String>,
|
||||
pub user: String,
|
||||
pub pass: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// ...
|
||||
#[derive(Clone)]
|
||||
pub struct Database<T> {
|
||||
pub client: T,
|
||||
pub _type: String,
|
||||
}
|
||||
|
||||
// ...
|
||||
#[cfg(feature = "mysql")]
|
||||
/// Create a new "mysql" database
|
||||
pub async fn create_db(options: DatabaseOpts) -> Database<sqlx::MySqlPool> {
|
||||
// mysql
|
||||
let opts = sqlx::mysql::MySqlPoolOptions::new()
|
||||
.max_connections(25)
|
||||
.acquire_timeout(std::time::Duration::from_millis(2000))
|
||||
.idle_timeout(Some(std::time::Duration::from_secs(60 * 5)));
|
||||
// .max_lifetime(Some(std::time::Duration::from_secs(120)));
|
||||
|
||||
let client = opts
|
||||
.connect(&format!(
|
||||
"mysql://{}:{}@{}/{}",
|
||||
options.user,
|
||||
options.pass,
|
||||
if options.host.is_some() {
|
||||
options.host.unwrap()
|
||||
} else {
|
||||
"localhost".to_string()
|
||||
},
|
||||
options.name
|
||||
))
|
||||
.await;
|
||||
|
||||
if client.is_err() {
|
||||
panic!("failed to connect to database: {}", client.err().unwrap());
|
||||
}
|
||||
|
||||
return Database {
|
||||
client: client.unwrap(),
|
||||
_type: String::from("mysql"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
/// Create a new "postgres" database
|
||||
pub async fn create_db(options: DatabaseOpts) -> Database<sqlx::PgPool> {
|
||||
// postgres
|
||||
let opts = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(25)
|
||||
.acquire_timeout(std::time::Duration::from_millis(2000))
|
||||
.idle_timeout(Some(std::time::Duration::from_secs(60 * 5)));
|
||||
// .max_lifetime(Some(std::time::Duration::from_secs(120)));
|
||||
|
||||
let client = opts
|
||||
.connect(&format!(
|
||||
"postgres://{}:{}@{}/{}",
|
||||
options.user,
|
||||
options.pass,
|
||||
if options.host.is_some() {
|
||||
options.host.unwrap()
|
||||
} else {
|
||||
"localhost".to_string()
|
||||
},
|
||||
options.name
|
||||
))
|
||||
.await;
|
||||
|
||||
if client.is_err() {
|
||||
panic!("failed to connect to database: {}", client.err().unwrap());
|
||||
}
|
||||
|
||||
return Database {
|
||||
client: client.unwrap(),
|
||||
_type: String::from("postgres"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
/// Create a new "sqlite" database
|
||||
pub async fn create_db(_options: DatabaseOpts) -> Database<sqlx::SqlitePool> {
|
||||
// sqlite
|
||||
let client = sqlx::SqlitePool::connect("sqlite://pufferbb.db").await;
|
||||
|
||||
if client.is_err() {
|
||||
panic!("Failed to connect to database!");
|
||||
}
|
||||
|
||||
return Database {
|
||||
client: client.unwrap(),
|
||||
_type: String::from("sqlite"),
|
||||
};
|
||||
}
|
10
src/main.rs
10
src/main.rs
|
@ -10,8 +10,6 @@ use actix_files as fs;
|
|||
use actix_web::{web, App, HttpServer};
|
||||
use dotenv;
|
||||
|
||||
use sqlx;
|
||||
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod utility;
|
||||
|
@ -21,8 +19,8 @@ pub mod pages;
|
|||
|
||||
pub mod markup;
|
||||
|
||||
use crate::db::pufferdb::{AppData, PufferDB};
|
||||
use crate::db::sql::DatabaseOpts;
|
||||
use crate::db::{AppData, Database};
|
||||
use dorsal::DatabaseOpts;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
@ -58,8 +56,7 @@ async fn main() -> std::io::Result<()> {
|
|||
panic!("Missing required database config settings!");
|
||||
}
|
||||
|
||||
sqlx::any::install_default_drivers(); // install database drivers
|
||||
let db: PufferDB = PufferDB::new(DatabaseOpts {
|
||||
let db: Database = Database::new(DatabaseOpts {
|
||||
_type: db_type,
|
||||
host: db_host,
|
||||
user: if db_is_other {
|
||||
|
@ -127,7 +124,6 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(crate::api::boards::update_post_tags_request)
|
||||
.service(crate::api::boards::metadata_request)
|
||||
.service(crate::api::boards::pin_post_request)
|
||||
.service(crate::api::boards::create_mail_stream_request)
|
||||
// DELETE boards api
|
||||
.service(crate::api::boards::delete_post_request)
|
||||
.service(crate::api::boards::delete_board_request)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use crate::db::AppData;
|
||||
use actix_web::{web::Data, HttpRequest};
|
||||
|
||||
pub struct BaseTemplate {
|
||||
pub info: String,
|
||||
pub auth_state: bool,
|
||||
|
@ -32,3 +35,40 @@ pub fn get_base_values(token_cookie: bool) -> BaseTemplate {
|
|||
body_embed,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_auth_status(
|
||||
req: HttpRequest,
|
||||
data: Data<AppData>,
|
||||
) -> (
|
||||
String,
|
||||
Option<actix_web::cookie::Cookie<'static>>,
|
||||
Option<dorsal::DefaultReturn<Option<dorsal::db::special::auth_db::FullUser<String>>>>,
|
||||
) {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user: Option<
|
||||
dorsal::DefaultReturn<Option<dorsal::db::special::auth_db::FullUser<String>>>,
|
||||
> = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.auth
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
|
||||
// return
|
||||
(set_cookie.to_string(), token_cookie, token_user)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use actix_web::HttpRequest;
|
||||
use actix_web::{get, web, HttpResponse, Responder};
|
||||
use dorsal::DefaultReturn;
|
||||
|
||||
use super::base;
|
||||
use askama::Template;
|
||||
|
||||
use crate::db;
|
||||
use crate::db::pufferdb::{self, Board, BoardMetadata, BoardPostLog, Log};
|
||||
use crate::db::{AppData, Board, BoardIdentifier, BoardMetadata, BoardPostLog, Log};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "boards/new.html")]
|
||||
|
@ -130,7 +130,7 @@ struct SettingsTemplate {
|
|||
#[derive(Template)]
|
||||
#[template(path = "dashboard.html")]
|
||||
struct DashboardTemplate {
|
||||
pub boards: Vec<pufferdb::BoardIdentifier>,
|
||||
pub boards: Vec<BoardIdentifier>,
|
||||
// required fields (super::base)
|
||||
info: String,
|
||||
auth_state: bool,
|
||||
|
@ -141,7 +141,7 @@ struct DashboardTemplate {
|
|||
|
||||
/* #[derive(Default, PartialEq, serde::Deserialize)]
|
||||
struct SearchProps {
|
||||
pub boards: Vec<pufferdb::BoardIdentifier>,
|
||||
pub boards: Vec<BoardIdentifier>,
|
||||
pub tags: String,
|
||||
pub offset: i32,
|
||||
pub auth_state: Option<bool>,
|
||||
|
@ -155,44 +155,14 @@ pub struct SearchQueryProps {
|
|||
|
||||
#[get("/new")]
|
||||
/// Available at "/new"
|
||||
pub async fn new_request(req: HttpRequest, data: web::Data<pufferdb::AppData>) -> impl Responder {
|
||||
pub async fn new_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
// token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
if token_user.is_none() {
|
||||
// you must have an account to create boards
|
||||
let base = base::get_base_values(token_user.is_some());
|
||||
let props = AuthPromptTemplate {
|
||||
// required fields
|
||||
info: base.info,
|
||||
auth_state: base.auth_state,
|
||||
bundlrs: base.bundlrs,
|
||||
guppy: base.guppy,
|
||||
body_embed: base.body_embed,
|
||||
};
|
||||
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(props.render().unwrap());
|
||||
return HttpResponse::NotAcceptable()
|
||||
.append_header(("Content-Type", "text/plain"))
|
||||
.body("An account is required to do this.");
|
||||
}
|
||||
|
||||
// ...
|
||||
|
@ -218,36 +188,17 @@ pub async fn new_request(req: HttpRequest, data: web::Data<pufferdb::AppData>) -
|
|||
/// Available at "/{name}"
|
||||
pub async fn view_board_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<pufferdb::AppData>,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<ViewBoardQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, token_cookie, token_user) =
|
||||
base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
// get board
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.clone()).await;
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
|
||||
if board.success == false {
|
||||
return HttpResponse::NotFound()
|
||||
|
@ -258,8 +209,7 @@ pub async fn view_board_request(
|
|||
// check if board is private
|
||||
// if it is, only the owner and users with the "staff" role can view it
|
||||
let metadata =
|
||||
serde_json::from_str::<pufferdb::BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
|
||||
.unwrap();
|
||||
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
|
||||
|
||||
if metadata.is_private == "yes" {
|
||||
// anonymous
|
||||
|
@ -326,7 +276,7 @@ pub async fn view_board_request(
|
|||
}
|
||||
|
||||
// ...
|
||||
let posts: pufferdb::DefaultReturn<Option<Vec<Log>>> =
|
||||
let posts: DefaultReturn<Option<Vec<Log>>> =
|
||||
if info.tags.is_some() && (info.tags.as_ref().unwrap().len() > 0) {
|
||||
data.db
|
||||
.get_board_posts_by_tag(
|
||||
|
@ -340,7 +290,7 @@ pub async fn view_board_request(
|
|||
data.db.get_board_posts(name.clone(), info.offset).await
|
||||
};
|
||||
|
||||
let pinned: pufferdb::DefaultReturn<Option<Vec<Log>>> =
|
||||
let pinned: DefaultReturn<Option<Vec<Log>>> =
|
||||
data.db.get_pinned_board_posts(name.clone()).await;
|
||||
|
||||
// ...
|
||||
|
@ -389,36 +339,17 @@ pub async fn view_board_request(
|
|||
/// Available at "/{name}/new"
|
||||
pub async fn create_board_post_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::pufferdb::AppData>,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<ViewBoardQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, token_cookie, token_user) =
|
||||
base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
// get board
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.clone()).await;
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
|
||||
if board.success == false {
|
||||
return HttpResponse::NotFound()
|
||||
|
@ -429,8 +360,7 @@ pub async fn create_board_post_request(
|
|||
// check if board is private
|
||||
// if it is, only the owner and users with the "staff" role can view it
|
||||
let metadata =
|
||||
serde_json::from_str::<pufferdb::BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
|
||||
.unwrap();
|
||||
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
|
||||
|
||||
if metadata.is_private == "yes" {
|
||||
// anonymous
|
||||
|
@ -522,38 +452,19 @@ pub async fn create_board_post_request(
|
|||
/// Available at "/{name}/posts/{id:.*}"
|
||||
pub async fn view_board_post_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::pufferdb::AppData>,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<ViewBoardPostQueryProps>,
|
||||
) -> impl Responder {
|
||||
// you're able to do this even if the board is private ON PURPOSE
|
||||
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, token_cookie, token_user) =
|
||||
base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
// get board
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.clone()).await;
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
|
||||
if board.success == false {
|
||||
return HttpResponse::NotFound()
|
||||
|
@ -566,7 +477,7 @@ pub async fn view_board_post_request(
|
|||
|
||||
// get post
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
let p: pufferdb::DefaultReturn<Option<Log>> = data.db.get_log_by_id(id.clone()).await;
|
||||
let p: DefaultReturn<Option<Log>> = data.db.get_log_by_id(id.clone()).await;
|
||||
|
||||
if p.success == false {
|
||||
return HttpResponse::NotFound()
|
||||
|
@ -578,7 +489,7 @@ pub async fn view_board_post_request(
|
|||
let post = serde_json::from_str::<BoardPostLog>(&p.content).unwrap();
|
||||
|
||||
// get replies
|
||||
let replies: pufferdb::DefaultReturn<Option<Vec<Log>>> = data
|
||||
let replies: DefaultReturn<Option<Vec<Log>>> = data
|
||||
.db
|
||||
.get_post_replies_limited(id.clone(), false, info.offset)
|
||||
.await;
|
||||
|
@ -665,15 +576,11 @@ pub async fn view_board_post_request(
|
|||
|
||||
#[get("/{name:.*}/manage")]
|
||||
/// Available at "/{name}/manage"
|
||||
pub async fn board_settings_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<pufferdb::AppData>,
|
||||
) -> impl Responder {
|
||||
pub async fn board_settings_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
// get board
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name).await;
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name).await;
|
||||
|
||||
if board.success == false {
|
||||
return HttpResponse::NotFound()
|
||||
|
@ -682,37 +589,18 @@ pub async fn board_settings_request(
|
|||
}
|
||||
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, token_cookie, token_user) =
|
||||
base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
if token_user.is_none() {
|
||||
return HttpResponse::NotAcceptable()
|
||||
.append_header(("Content-Type", "text/plain"))
|
||||
.body("An account is required to do this");
|
||||
.body("An account is required to do this.");
|
||||
}
|
||||
|
||||
// ...
|
||||
let metadata =
|
||||
serde_json::from_str::<pufferdb::BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
|
||||
.unwrap();
|
||||
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
|
||||
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
let can_view: bool = (user.user.username == metadata.owner)
|
||||
|
@ -835,7 +723,7 @@ fn build_search_renderer_with_props(props: SearchProps) -> ServerRenderer<Search
|
|||
/// Available at "/d/browse"
|
||||
pub async fn search_by_tags_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::pufferdb::AppData>,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<SearchQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
|
@ -912,31 +800,10 @@ You can create an account at: /d/auth/register",
|
|||
|
||||
#[get("/d")]
|
||||
/// Available at "/d"
|
||||
pub async fn dashboard_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::pufferdb::AppData>,
|
||||
) -> impl Responder {
|
||||
pub async fn dashboard_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, token_cookie, token_user) =
|
||||
base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
if token_user.is_none() {
|
||||
// you must have an account to use boards
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::db::pufferdb;
|
||||
use crate::db::AppData;
|
||||
|
||||
use super::base;
|
||||
use askama::Template;
|
||||
|
@ -13,32 +13,13 @@ struct HomeTemplate {
|
|||
auth_state: bool,
|
||||
bundlrs: String,
|
||||
guppy: String,
|
||||
body_embed: String,
|
||||
body_embed: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn home_request(req: HttpRequest, data: web::Data<pufferdb::AppData>) -> impl Responder {
|
||||
pub async fn home_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let mut set_cookie: &str = "";
|
||||
|
||||
let mut token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists, refresh token if not
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
|
||||
token_user = Option::None;
|
||||
}
|
||||
}
|
||||
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
|
||||
|
||||
// ...
|
||||
let base = base::get_base_values(token_user.is_some());
|
||||
|
@ -50,9 +31,9 @@ pub async fn home_request(req: HttpRequest, data: web::Data<pufferdb::AppData>)
|
|||
// required fields
|
||||
info: base.info,
|
||||
auth_state: base.auth_state,
|
||||
bundlrs: base.bundlrs,
|
||||
guppy: base.guppy,
|
||||
body_embed: base.body_embed,
|
||||
bundlrs: base.bundlrs,
|
||||
guppy: base.guppy,
|
||||
body_embed: base.body_embed,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
|
|
@ -103,6 +103,12 @@ button.puffer-primary:hover,
|
|||
background: var(--primary-low);
|
||||
}
|
||||
|
||||
button[disabled="false"],
|
||||
.button[disabled="false"] {
|
||||
opacity: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* input modifications */
|
||||
button + input,
|
||||
.button + input {
|
||||
|
|
|
@ -86,6 +86,13 @@ setTimeout(() => {
|
|||
).toLocaleString();
|
||||
}, 50);
|
||||
|
||||
// disabled="false"
|
||||
for (const element of Array.from(
|
||||
document.querySelectorAll('[disabled="false"]')
|
||||
) as HTMLButtonElement[]) {
|
||||
element.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
// disable "a"
|
||||
setTimeout(() => {
|
||||
for (const element of Array.from(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="flex mobile:flex-column g-4 full message-box">
|
||||
{% if show_open == true && post.topic.is_some() %}
|
||||
<div class="card message topic round full flex justify-space-between align-center flex-wrap g-4"
|
||||
style="background: var(--background-surface0-5)">
|
||||
style="background: var(--background-surface0-5);">
|
||||
<a class="flex align-center g-4" href="/{{ post.board }}/posts/{{ p.id }}" title="Expand Topic">
|
||||
{% if post.pinned.is_some() && post.pinned.as_ref().unwrap().to_owned() == true %}
|
||||
<div class="flex align-center" style="color: var(--primary);" title="Pinned Post">
|
||||
|
@ -58,7 +58,7 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card message round full flex g-4" , style="background: var(--background-surface0-5)">
|
||||
<div class="card message round full flex g-4" style="background: var(--background-surface0-5);">
|
||||
<div class="flex g-4 full justify-space-between">
|
||||
<div class="full">
|
||||
{{ post.content_html|safe }}
|
||||
|
@ -92,13 +92,14 @@
|
|||
{% if show_open == true %}
|
||||
<div class="flex g-4 flex-wrap">
|
||||
<a class="button invisible round" href="/{{ post.board }}/posts/{{ p.id }}"
|
||||
style="color: var(--text-color);" target="_blank" title="open/manage">
|
||||
style="color: var(--text-color);" title="Expand Post">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-arrow-up-right-from-square">
|
||||
<path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6" />
|
||||
<path d="m21 3-9 9" />
|
||||
<path d="M15 3h6v6" />
|
||||
class="lucide lucide-expand">
|
||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
||||
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<div class="full flex justify-space-between align-center g-4">
|
||||
<h5 class="no-margin">
|
||||
{% if board.name.starts_with("inbox-") %}
|
||||
{% let mailstream = crate::db::pufferdb::deserialize_mailstream(board_m.about.as_ref().unwrap()) %}
|
||||
{% let mailstream = crate::db::deserialize_mailstream(board_m.about.as_ref().unwrap()) %}
|
||||
<!-- show OTHER USER's username if we're in an inbox -->
|
||||
{% if me == mailstream.user1 %}
|
||||
{{ mailstream.user2 }}
|
||||
|
@ -94,7 +94,7 @@
|
|||
<!-- don't look here, my formatter doesn't support whitespace for this -->
|
||||
{% if pinned.len() > 0 %}
|
||||
{% for p in pinned %}
|
||||
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
|
||||
{% let post = crate::db::deserialize_post(p.to_owned()) %}
|
||||
{% let show_open = true %}
|
||||
{% include "message_component.html" %}
|
||||
{% endfor %}
|
||||
|
@ -104,7 +104,7 @@
|
|||
|
||||
{% if !topic_required %}
|
||||
{% for p in posts %}
|
||||
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
|
||||
{% let post = crate::db::deserialize_post(p.to_owned()) %}
|
||||
{% let show_open = true %}
|
||||
{% include "message_component.html" %}
|
||||
{% endfor %}
|
||||
|
@ -118,7 +118,7 @@
|
|||
|
||||
<tbody>
|
||||
{% for p in posts %}
|
||||
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
|
||||
{% let post = crate::db::deserialize_post(p.to_owned()) %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="flex align-center g-4" href="/{{ post.board }}/posts/{{ p.id }}" title="Expand Topic">
|
||||
|
@ -144,7 +144,8 @@
|
|||
|
||||
<td class="flex align-center g-4">
|
||||
{% if post.author != "Anonymous" %}
|
||||
<img class="avatar" style="--size: 25px;" src="{{ guppy }}/api/auth/users/{{ post.author }}/avatar" />
|
||||
<img class="avatar" style="--size: 25px;"
|
||||
src="{{ guppy }}/api/auth/users/{{ post.author }}/avatar" />
|
||||
{% endif %}
|
||||
|
||||
<span class="chip mention round" style="width: max-content;">
|
||||
|
@ -177,7 +178,7 @@
|
|||
</a>
|
||||
|
||||
<a class="button round" href="?tags={{tags}}&offset={{offset + 50}}&view={{view}}"
|
||||
disabled="{{ posts.len()==0 }}">
|
||||
disabled="{{ posts.len() == 0 }}">
|
||||
Next
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
|
|
|
@ -89,19 +89,55 @@
|
|||
|
||||
<div class="flex flex-wrap g-4">
|
||||
{% if can_manage %}
|
||||
<button class="border round" id="delete-post"
|
||||
data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}">Delete</button>
|
||||
<a class="button border round" href="?edit_tags=true">Edit Tags</a>
|
||||
<button class="round red" id="delete-post" data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-trash-2">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
<line x1="10" x2="10" y1="11" y2="17" />
|
||||
<line x1="14" x2="14" y1="11" y2="17" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
<a class="button round" href="?edit_tags=true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-tag">
|
||||
<path
|
||||
d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />
|
||||
<circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
|
||||
</svg>
|
||||
Edit Tags
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_board %}
|
||||
<button class="border round" id="pin-post"
|
||||
data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}/pin">Pin</button>
|
||||
<button class="round" id="pin-post" data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}/pin">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-pin">
|
||||
<line x1="12" x2="12" y1="17" y2="22" />
|
||||
<path
|
||||
d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />
|
||||
</svg>
|
||||
Pin
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if can_edit %}
|
||||
<a class="button border round" href="?edit_text=true">Edit</a>
|
||||
<a class="button round" href="?edit_text=true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-pencil">
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||
<path d="m15 5 4 4" />
|
||||
</svg>
|
||||
Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@ -146,7 +182,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% for p in replies %}
|
||||
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
|
||||
{% let post = crate::db::deserialize_post(p.to_owned()) %}
|
||||
{% let show_open = true %}
|
||||
{% include "message_component.html" %}
|
||||
{% endfor %}
|
||||
|
|
Loading…
Reference in a new issue