diff --git a/Cargo.lock b/Cargo.lock index 86eccc9..11d719e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3744,7 +3744,7 @@ dependencies = [ [[package]] name = "tetratto" -version = "2.1.0" +version = "2.2.0" dependencies = [ "ammonia", "async-stripe", @@ -3775,7 +3775,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "2.1.0" +version = "2.2.0" dependencies = [ "async-recursion", "base16ct", @@ -3799,7 +3799,7 @@ dependencies = [ [[package]] name = "tetratto-l10n" -version = "2.1.0" +version = "2.2.0" dependencies = [ "pathbufd", "serde", @@ -3808,7 +3808,7 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "2.1.0" +version = "2.2.0" dependencies = [ "ammonia", "chrono", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index b232819..492741f 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "2.1.0" +version = "2.2.0" edition = "2024" [features] diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index b731547..3b45ceb 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -160,3 +160,6 @@ version = "1.0.0" "chats:label.go_back" = "Go back" "chats:action.leave" = "Leave" "chats:label.viewing_single_message" = "You're viewing a single message!" +"chats:action.add_someone" = "Add someone" +"chats:action.kick_member" = "Kick member" +"chats:action.mention_user" = "Mention user" diff --git a/crates/app/src/public/html/chats/app.html b/crates/app/src/public/html/chats/app.html index 92a1ed9..440bc0e 100644 --- a/crates/app/src/public/html/chats/app.html +++ b/crates/app/src/public/html/chats/app.html @@ -47,7 +47,7 @@ hide_user_menu=true) }} {% endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, -secondary=false) -%} +show_kick=false, secondary=false) -%}
@@ -1125,6 +1130,26 @@ secondary=false) -%} {{ self::user_menu() }}
+ {% elif show_kick %} + {% endif %} {%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%} diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs index 16a3f3a..cb8c3c1 100644 --- a/crates/app/src/routes/api/v1/channels/channels.rs +++ b/crates/app/src/routes/api/v1/channels/channels.rs @@ -80,10 +80,12 @@ pub async fn create_group_request( Err(e) => return Json(e.into()), }; - if other_user.settings.private_chats && data + if other_user.settings.private_chats + && data .get_userfollow_by_initiator_receiver(other_user.id, user.id) .await - .is_err() { + .is_err() + { return Json(Error::NotAllowed.into()); } } @@ -168,6 +170,28 @@ pub async fn update_position_request( } } +pub async fn add_member_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.add_channel_member(id, user, req.member).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Member added".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + pub async fn kick_member_request( jar: CookieJar, Extension(data): Extension, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 6146ccd..e44254b 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -300,6 +300,10 @@ pub fn routes() -> Router { post(channels::channels::update_position_request), ) .route("/channels/{id}", delete(channels::channels::delete_request)) + .route( + "/channels/{id}/add", + post(channels::channels::add_member_request), + ) .route( "/channels/{id}/kick", post(channels::channels::kick_member_request), diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs index 4b6f73a..733e4fb 100644 --- a/crates/app/src/routes/pages/chats.rs +++ b/crates/app/src/routes/pages/chats.rs @@ -15,6 +15,7 @@ use serde::Deserialize; #[derive(Deserialize)] pub struct RenderMessage { pub data: String, + pub grouped: bool, } pub async fn redirect_request() -> impl IntoResponse { @@ -276,6 +277,7 @@ pub async fn message_request( context.insert("channel", &channel); context.insert("community", &community); + context.insert("grouped", &req.grouped); // return Ok(Html(data.1.render("chats/message.html", &context).unwrap())) @@ -285,7 +287,7 @@ pub async fn message_request( pub async fn channels_request( jar: CookieJar, Extension(data): Extension, - Path((community, channel)): Path<(usize, usize)>, + Path((community, channel_id)): Path<(usize, usize)>, Query(props): Query, ) -> impl IntoResponse { let data = data.read().await; @@ -310,14 +312,42 @@ pub async fn channels_request( } }; + let channel = if channel_id != 0 { + Some(match data.0.get_channel_by_id(channel_id).await { + Ok(p) => p, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }) + } else { + None + }; + + let members = if community == 0 && channel.is_some() { + let ignore_users = data.0.get_userblocks_receivers(user.id).await; + + let mut channel = channel.as_ref().unwrap().clone(); + channel.members.insert(0, channel.owner); // include the owner in the members list (at the start) + + Some( + match data.0.fill_members(&channel.members, ignore_users).await { + Ok(p) => p, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }, + ) + } else { + None + }; + let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0, lang, &Some(user)).await; context.insert("channels", &channels); context.insert("page", &props.page); + context.insert("members", &members); + context.insert("channel", &channel); + context.insert("selected_community", &community); - context.insert("selected_channel", &channel); + context.insert("selected_channel", &channel_id); // return Ok(Html( diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 692c5c5..1169c31 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-core" -version = "2.1.0" +version = "2.2.0" edition = "2024" [features] diff --git a/crates/core/src/database/channels.rs b/crates/core/src/database/channels.rs index 6026c48..527008b 100644 --- a/crates/core/src/database/channels.rs +++ b/crates/core/src/database/channels.rs @@ -34,6 +34,25 @@ impl DataManager { auto_method!(get_channel_by_id(usize as i64)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}"); + /// Get all member profiles from a channel members list. + pub async fn fill_members( + &self, + members: &Vec, + ignore_users: Vec, + ) -> Result> { + let mut out = Vec::new(); + + for member in members { + if ignore_users.contains(&member) { + continue; + } + + out.push(self.get_user_by_id(member.to_owned()).await?); + } + + Ok(out) + } + /// Get all channels by community. /// /// # Arguments @@ -214,6 +233,55 @@ impl DataManager { Ok(()) } + pub async fn add_channel_member(&self, id: usize, user: User, member: String) -> Result<()> { + let mut y = self.get_channel_by_id(id).await?; + + if user.id != y.owner && member != user.username { + if !user.permissions.check(FinePermission::MANAGE_CHANNELS) { + return Err(Error::NotAllowed); + } else { + self.create_audit_log_entry(AuditLogEntry::new( + user.id, + format!("invoked `add_channel_member` with x value `{member}`"), + )) + .await? + } + } + + // check permissions + let member = self.get_user_by_username(&member).await?; + + if self + .get_userblock_by_initiator_receiver(member.id, user.id) + .await + .is_ok() + { + return Err(Error::NotAllowed); + } + + // ... + y.members.push(member.id); + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "UPDATE channels SET members = $1 WHERE id = $2", + params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.channel:{}", id)).await; + + Ok(()) + } + pub async fn remove_channel_member(&self, id: usize, user: User, member: usize) -> Result<()> { let mut y = self.get_channel_by_id(id).await?; @@ -230,7 +298,10 @@ impl DataManager { } y.members - .remove(y.members.iter().position(|x| *x == member).unwrap()); + .remove(match y.members.iter().position(|x| *x == member) { + Some(i) => i, + None => return Err(Error::GeneralNotFound("member".to_string())), + }); let conn = match self.connect().await { Ok(c) => c, diff --git a/crates/core/src/model/channels.rs b/crates/core/src/model/channels.rs index 987cd31..5bbefdf 100644 --- a/crates/core/src/model/channels.rs +++ b/crates/core/src/model/channels.rs @@ -4,7 +4,7 @@ use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; use super::communities_permissions::CommunityPermission; /// A channel is a more "chat-like" feed in communities. -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct Channel { pub id: usize, pub community: usize, diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 7f96ff0..8a55128 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-l10n" -version = "2.1.0" +version = "2.2.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index baa7c8b..23e28a6 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-shared" -version = "2.1.0" +version = "2.2.0" edition = "2024" authors.workspace = true repository.workspace = true