generated from t/malachite
add: api routes
This commit is contained in:
parent
d7ee379a9a
commit
ce9ce4f635
16 changed files with 1119 additions and 109 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
target/
|
target/
|
||||||
|
tetratto.toml
|
||||||
|
app.toml
|
||||||
|
|
652
Cargo.lock
generated
652
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -32,3 +32,4 @@ toml = "0.9.4"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] }
|
oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] }
|
||||||
buckets-core = "1.0.4"
|
buckets-core = "1.0.4"
|
||||||
|
axum-image = "0.1.1"
|
||||||
|
|
|
@ -5,6 +5,7 @@ use tetratto_core::{
|
||||||
auto_method,
|
auto_method,
|
||||||
model::{Error, Result, auth::User},
|
model::{Error, Result, auth::User},
|
||||||
};
|
};
|
||||||
|
use tetratto_shared::unix_epoch_timestamp;
|
||||||
|
|
||||||
impl DataManager {
|
impl DataManager {
|
||||||
/// Get a [`Message`] from an SQL row.
|
/// Get a [`Message`] from an SQL row.
|
||||||
|
@ -12,10 +13,11 @@ impl DataManager {
|
||||||
Message {
|
Message {
|
||||||
id: get!(x->0(i64)) as usize,
|
id: get!(x->0(i64)) as usize,
|
||||||
created: get!(x->1(i64)) as usize,
|
created: get!(x->1(i64)) as usize,
|
||||||
owner: get!(x->2(i64)) as usize,
|
edited: get!(x->2(i64)) as usize,
|
||||||
chat: get!(x->3(i64)) as usize,
|
owner: get!(x->3(i64)) as usize,
|
||||||
content: get!(x->4(String)),
|
chat: get!(x->4(i64)) as usize,
|
||||||
uploads: serde_json::from_str(&get!(x->5(String))).unwrap(),
|
content: get!(x->5(String)),
|
||||||
|
uploads: serde_json::from_str(&get!(x->6(String))).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +43,11 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"INSERT INTO t_messages VALUES ($1, $2, $3, $4, $5, $6)",
|
"INSERT INTO t_messages VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
params![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
&(data.edited as i64),
|
||||||
&(data.owner as i64),
|
&(data.owner as i64),
|
||||||
&(data.chat as i64),
|
&(data.chat as i64),
|
||||||
&data.content,
|
&data.content,
|
||||||
|
@ -95,5 +98,37 @@ impl DataManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_method!(update_message_content(&str) -> "UPDATE t_messages SET content = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.message:{}");
|
/// Update the content of the given message.
|
||||||
|
pub async fn update_message_content(
|
||||||
|
&self,
|
||||||
|
id: usize,
|
||||||
|
content: &str,
|
||||||
|
user: &User,
|
||||||
|
) -> Result<()> {
|
||||||
|
let message = self.get_message_by_id(id).await?;
|
||||||
|
|
||||||
|
if message.owner != user.id {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// update message
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"UPDATE t_messages SET content = $1, edited = $2 WHERE id = $3",
|
||||||
|
params![&content, &(unix_epoch_timestamp() as i64), &(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,18 @@ use crate::config::Config;
|
||||||
use buckets_core::{Config as BucketsConfig, DataManager as BucketsManager};
|
use buckets_core::{Config as BucketsConfig, DataManager as BucketsManager};
|
||||||
use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult};
|
use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tetratto_core::model::{Error, Result};
|
use tetratto_core::{
|
||||||
|
DataManager as TetrattoManager,
|
||||||
|
config::Config as TetrattoConfig,
|
||||||
|
model::{Error, Result},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DataManager(pub OiseauManager<Config>, pub BucketsManager);
|
pub struct DataManager(
|
||||||
|
pub OiseauManager<Config>,
|
||||||
|
pub BucketsManager,
|
||||||
|
pub TetrattoManager,
|
||||||
|
);
|
||||||
|
|
||||||
impl DataManager {
|
impl DataManager {
|
||||||
/// Create a new [`DataManager`].
|
/// Create a new [`DataManager`].
|
||||||
|
@ -22,7 +30,15 @@ impl DataManager {
|
||||||
.await
|
.await
|
||||||
.expect("failed to create buckets manager");
|
.expect("failed to create buckets manager");
|
||||||
|
|
||||||
Ok(Self(OiseauManager::new(config).await?, buckets_manager))
|
let tetratto_manager = TetrattoManager::new(TetrattoConfig::get_config())
|
||||||
|
.await
|
||||||
|
.expect("failed to create tetratto manager");
|
||||||
|
|
||||||
|
Ok(Self(
|
||||||
|
OiseauManager::new(config).await?,
|
||||||
|
buckets_manager,
|
||||||
|
tetratto_manager,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize tables.
|
/// Initialize tables.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
CREATE TABLE IF NOT EXISTS t_messages (
|
CREATE TABLE IF NOT EXISTS t_messages (
|
||||||
id BIGINT NOT NULL,
|
id BIGINT NOT NULL,
|
||||||
created BIGINT NOT NULL,
|
created BIGINT NOT NULL,
|
||||||
|
edited BIGINT NOT NULL,
|
||||||
owner BIGINT NOT NULL,
|
owner BIGINT NOT NULL,
|
||||||
chats BIGINT NOT NULL,
|
chats BIGINT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
|
|
49
src/macros.rs
Normal file
49
src/macros.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get_user_from_token {
|
||||||
|
($jar:ident, $db:expr) => {{
|
||||||
|
// pages; regular token only
|
||||||
|
if let Some(token) = $jar.get("__Secure-atto-token") {
|
||||||
|
match $db
|
||||||
|
.get_user_by_token(&tetratto_shared::hash::hash(
|
||||||
|
token.to_string().replace("__Secure-atto-token=", ""),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ua) => {
|
||||||
|
if ua.permissions.check_banned() {
|
||||||
|
// check expiration
|
||||||
|
let now = tetratto_shared::unix_epoch_timestamp();
|
||||||
|
let expired = ua.ban_expire <= now;
|
||||||
|
|
||||||
|
if expired && ua.ban_expire != 0 {
|
||||||
|
$db.update_user_role(
|
||||||
|
ua.id,
|
||||||
|
ua.permissions
|
||||||
|
- tetratto_core::model::permissions::FinePermission::BANNED,
|
||||||
|
&ua,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to auto unban user");
|
||||||
|
|
||||||
|
Some(ua)
|
||||||
|
} else {
|
||||||
|
// banned
|
||||||
|
let mut banned_user = tetratto_core::model::auth::User::banned();
|
||||||
|
|
||||||
|
banned_user.ban_reason = ua.ban_reason;
|
||||||
|
banned_user.ban_expire = ua.ban_expire;
|
||||||
|
|
||||||
|
Some(banned_user)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(ua)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
|
mod macros;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod model;
|
mod model;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
10
src/model.rs
10
src/model.rs
|
@ -1,12 +1,12 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct GroupChatInfo {
|
pub struct GroupChatInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum ChatStyle {
|
pub enum ChatStyle {
|
||||||
/// Direct messages between two users.
|
/// Direct messages between two users.
|
||||||
Direct,
|
Direct,
|
||||||
|
@ -52,6 +52,7 @@ impl Chat {
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub created: usize,
|
pub created: usize,
|
||||||
|
pub edited: usize,
|
||||||
pub owner: usize,
|
pub owner: usize,
|
||||||
pub chat: usize,
|
pub chat: usize,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
@ -61,9 +62,12 @@ pub struct Message {
|
||||||
impl Message {
|
impl Message {
|
||||||
/// Create a new [`Message`].
|
/// Create a new [`Message`].
|
||||||
pub fn new(owner: usize, chat: usize, content: String, uploads: Vec<usize>) -> Self {
|
pub fn new(owner: usize, chat: usize, content: String, uploads: Vec<usize>) -> Self {
|
||||||
|
let created = unix_epoch_timestamp();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||||
created: unix_epoch_timestamp(),
|
created,
|
||||||
|
edited: created,
|
||||||
owner,
|
owner,
|
||||||
chat,
|
chat,
|
||||||
content,
|
content,
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
use crate::{State, config::Config};
|
|
||||||
use axum::{
|
|
||||||
Extension, Router,
|
|
||||||
extract::Path,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
routing::{get, get_service},
|
|
||||||
};
|
|
||||||
use pathbufd::PathBufD;
|
|
||||||
use tera::Context;
|
|
||||||
use tetratto_core::model::Error;
|
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.nest_service(
|
|
||||||
"/public",
|
|
||||||
get_service(tower_http::services::ServeDir::new("./public")),
|
|
||||||
)
|
|
||||||
.fallback(not_found_request)
|
|
||||||
.route("/docs/{name}", get(view_doc_request))
|
|
||||||
// pages
|
|
||||||
.route("/", get(index_request))
|
|
||||||
// api
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_context(config: &Config, build_code: &str) -> Context {
|
|
||||||
let mut ctx = Context::new();
|
|
||||||
ctx.insert("name", &config.name);
|
|
||||||
ctx.insert("theme_color", &config.theme_color);
|
|
||||||
ctx.insert("build_code", &build_code);
|
|
||||||
ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// pages
|
|
||||||
async fn not_found_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
|
||||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
|
||||||
|
|
||||||
let mut ctx = default_context(&data.0.0, &build_code);
|
|
||||||
ctx.insert(
|
|
||||||
"error",
|
|
||||||
&Error::GeneralNotFound("page".to_string()).to_string(),
|
|
||||||
);
|
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
|
||||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
|
||||||
Html(
|
|
||||||
tera.render("index.lisp", &default_context(&data.0.0, &build_code))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn view_doc_request(
|
|
||||||
Extension(data): Extension<State>,
|
|
||||||
Path(name): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
|
||||||
let path = PathBufD::current().extend(&["docs", &format!("{name}.md")]);
|
|
||||||
|
|
||||||
if !std::fs::exists(&path).unwrap_or(false) {
|
|
||||||
let mut ctx = default_context(&data.0.0, &build_code);
|
|
||||||
ctx.insert(
|
|
||||||
"error",
|
|
||||||
&Error::GeneralNotFound("entry".to_string()).to_string(),
|
|
||||||
);
|
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = match std::fs::read_to_string(&path) {
|
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => {
|
|
||||||
let mut ctx = default_context(&data.0.0, &build_code);
|
|
||||||
ctx.insert("error", &Error::MiscError(e.to_string()).to_string());
|
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ctx = default_context(&data.0.0, &build_code);
|
|
||||||
|
|
||||||
ctx.insert("text", &text);
|
|
||||||
ctx.insert("file_name", &name);
|
|
||||||
|
|
||||||
return Html(tera.render("doc.lisp", &ctx).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// api
|
|
||||||
// ...
|
|
137
src/routes/api/chats.rs
Normal file
137
src/routes/api/chats.rs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
use crate::{
|
||||||
|
State, get_user_from_token,
|
||||||
|
model::{Chat, ChatStyle, GroupChatInfo},
|
||||||
|
};
|
||||||
|
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tetratto_core::model::{ApiReturn, Error};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateChat {
|
||||||
|
pub style: ChatStyle,
|
||||||
|
pub members: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Json(req): Json<CreateChat>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.members.len() > 2 && req.style == ChatStyle::Direct {
|
||||||
|
return Json(Error::DataTooLong("members".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match data
|
||||||
|
.create_chat(Chat::new(req.style, {
|
||||||
|
let mut x = req.members;
|
||||||
|
x.push(user.id);
|
||||||
|
x
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: x.id.to_string(),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn leave_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut chat = match data.get_chat_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !chat.members.contains(&user.id) {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
chat.members
|
||||||
|
.remove(chat.members.iter().position(|x| *x == user.id).unwrap());
|
||||||
|
|
||||||
|
if chat.members.len() == 0 {
|
||||||
|
// we were the last member
|
||||||
|
match data.delete_chat(id).await {
|
||||||
|
Ok(_) => {
|
||||||
|
return Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match data.update_chat_members(chat.id, chat.members).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateChatInfo {
|
||||||
|
pub info: GroupChatInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_info_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Json(req): Json<UpdateChatInfo>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let chat = match data.get_chat_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !chat.members.contains(&user.id) {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match chat.style {
|
||||||
|
ChatStyle::Group(_) => {
|
||||||
|
match data
|
||||||
|
.update_chat_style(chat.id, ChatStyle::Group(req.info))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Json(Error::DoesNotSupportField("info".to_string()).into()),
|
||||||
|
}
|
||||||
|
}
|
147
src/routes/api/messages.rs
Normal file
147
src/routes/api/messages.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
use crate::{State, get_user_from_token, model::Message};
|
||||||
|
use axum::{Extension, Json, body::Bytes, extract::Path, response::IntoResponse};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
use axum_image::{encode::save_webp_buffer, extract::JsonMultipart};
|
||||||
|
use buckets_core::model::{MediaType, MediaUpload};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tetratto_core::model::{ApiReturn, Error};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateMessage {
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAXIMUM_UPLOAD_SIZE: usize = 4_194_304; // 4 MiB
|
||||||
|
|
||||||
|
pub async fn create_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
JsonMultipart(byte_parts, req): JsonMultipart<CreateMessage>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// check fields
|
||||||
|
if req.content.trim().len() < 2 {
|
||||||
|
return Json(Error::DataTooShort("content".to_string()).into());
|
||||||
|
} else if req.content.len() > 2048 {
|
||||||
|
return Json(Error::DataTooLong("content".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// check chat permissions
|
||||||
|
let chat = match data.get_chat_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !chat.members.contains(&user.id) {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// create uploads
|
||||||
|
let mut uploads: Vec<(MediaUpload, Bytes)> = Vec::new();
|
||||||
|
|
||||||
|
for part in &byte_parts {
|
||||||
|
if part.len() < MAXIMUM_UPLOAD_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for part in byte_parts {
|
||||||
|
uploads.push((
|
||||||
|
MediaUpload::new(MediaType::Webp, user.id, "message_media".to_string()),
|
||||||
|
part,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// create message
|
||||||
|
match data
|
||||||
|
.create_message(Message::new(
|
||||||
|
user.id,
|
||||||
|
chat.id,
|
||||||
|
req.content,
|
||||||
|
uploads.iter().map(|x| x.0.id).collect(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => {
|
||||||
|
// store uploads
|
||||||
|
for (upload, part) in uploads {
|
||||||
|
let upload = match data.1.create_upload(upload).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if save_webp_buffer(
|
||||||
|
&upload.path(&data.1.0.0.directory).to_string(),
|
||||||
|
part.to_vec(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: x.id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.delete_message(id, &user).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateMessageContent {
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_content_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Json(req): Json<UpdateMessageContent>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.update_message_content(id, &req.content, &user).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
14
src/routes/api/mod.rs
Normal file
14
src/routes/api/mod.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
pub mod chats;
|
||||||
|
pub mod messages;
|
||||||
|
|
||||||
|
use axum::routing::{Router, delete, post, put};
|
||||||
|
|
||||||
|
pub fn routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/chats", post(chats::create_request))
|
||||||
|
.route("/chats/{id}/leave", post(chats::leave_request))
|
||||||
|
.route("/chats/{id}/info", post(chats::update_info_request))
|
||||||
|
.route("/messages", post(messages::create_request))
|
||||||
|
.route("/messages/{id}", delete(messages::delete_request))
|
||||||
|
.route("/messages/{id}", put(messages::update_content_request))
|
||||||
|
}
|
25
src/routes/mod.rs
Normal file
25
src/routes/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use axum::{Router, routing::get_service};
|
||||||
|
use tera::Context;
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
|
pub mod pages;
|
||||||
|
|
||||||
|
pub fn routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.nest_service(
|
||||||
|
"/public",
|
||||||
|
get_service(tower_http::services::ServeDir::new("./public")),
|
||||||
|
)
|
||||||
|
.fallback(pages::misc::not_found_request)
|
||||||
|
.merge(pages::routes())
|
||||||
|
.nest("/api/v1", api::routes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_context(config: &Config, build_code: &str) -> Context {
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
ctx.insert("name", &config.name);
|
||||||
|
ctx.insert("theme_color", &config.theme_color);
|
||||||
|
ctx.insert("build_code", &build_code);
|
||||||
|
ctx
|
||||||
|
}
|
25
src/routes/pages/misc.rs
Normal file
25
src/routes/pages/misc.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::{State, routes::default_context};
|
||||||
|
use axum::{
|
||||||
|
Extension,
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
|
use tetratto_core::model::Error;
|
||||||
|
|
||||||
|
pub async fn not_found_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
|
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||||
|
|
||||||
|
let mut ctx = default_context(&data.0.0, &build_code);
|
||||||
|
ctx.insert(
|
||||||
|
"error",
|
||||||
|
&Error::GeneralNotFound("page".to_string()).to_string(),
|
||||||
|
);
|
||||||
|
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
|
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||||
|
Html(
|
||||||
|
tera.render("index.lisp", &default_context(&data.0.0, &build_code))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
7
src/routes/pages/mod.rs
Normal file
7
src/routes/pages/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
pub mod misc;
|
||||||
|
|
||||||
|
use axum::routing::{Router, get};
|
||||||
|
|
||||||
|
pub fn routes() -> Router {
|
||||||
|
Router::new().route("/", get(misc::index_request))
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue