Implement basic channel permissioning for kicks, joins, chatting & topic setting
Diff
src/channel.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
src/client.rs | 8 ++++++++
src/messages.rs | 4 +++-
src/persistence.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/channel/permissions.rs | 35 +++++++++++++++++++++++++++++++++++
src/channel/response.rs | 7 ++++++-
src/persistence/events.rs | 20 ++++++++++++++++++++
7 files changed, 229 insertions(+), 59 deletions(-)
@@ -1,3 +1,4 @@
pub mod permissions;
pub mod response;
use std::collections::HashMap;
@@ -12,7 +13,12 @@
use tracing::{debug, error, info, instrument, Span};
use crate::{
channel::response::{ChannelInviteResult, ChannelNamesList, ChannelTopic},
channel::{
permissions::Permission,
response::{
ChannelInviteResult, ChannelJoinRejectionReason, ChannelNamesList, ChannelTopic,
},
},
client::Client,
connection::InitiatedConnection,
messages::{
@@ -20,7 +26,7 @@
ChannelMemberList, ChannelMessage, ChannelPart, ChannelUpdateTopic, FetchClientByNick,
ServerDisconnect, UserKickedFromChannel, UserNickChange,
},
persistence::Persistence,
persistence::{events::FetchUserChannelPermissions, Persistence},
server::Server,
};
@@ -32,7 +38,7 @@
pub struct Channel {
pub name: String,
pub server: Addr<Server>,
pub clients: HashMap<Addr<Client>, InitiatedConnection>,
pub clients: HashMap<Addr<Client>, (Permission, InitiatedConnection)>,
pub topic: Option<CurrentChannelTopic>,
pub persistence: Addr<Persistence>,
pub channel_id: ChannelId,
@@ -93,10 +99,16 @@
fn handle(&mut self, msg: ChannelMessage, _ctx: &mut Self::Context) -> Self::Result {
let Some(sender) = self.clients.get(&msg.client) else {
let Some((permissions, sender)) = self.clients.get(&msg.client) else {
error!("Received message from user not in channel");
return;
};
if !permissions.can_chatter() {
error!("User cannot send message to channel");
return;
}
let nick = sender.to_nick();
@@ -106,7 +118,7 @@
channel_id: self.channel_id,
sender: nick.to_string(),
message: msg.message.to_string(),
receivers: self.clients.values().map(|v| v.user_id).collect(),
receivers: self.clients.values().map(|(_, v)| v.user_id).collect(),
});
for client in self.clients.keys() {
@@ -134,7 +146,7 @@
fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result {
let Some(sender) = self.clients.get_mut(&msg.client) else {
let Some((_, sender)) = self.clients.get_mut(&msg.client) else {
return;
};
@@ -149,53 +161,77 @@
impl Handler<ChannelJoin> for Channel {
type Result = MessageResult<ChannelJoin>;
type Result = ResponseActFuture<
Self,
Result<Result<Addr<Self>, ChannelJoinRejectionReason>, anyhow::Error>,
>;
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ChannelJoin, ctx: &mut Self::Context) -> Self::Result {
fn handle(&mut self, msg: ChannelJoin, _ctx: &mut Self::Context) -> Self::Result {
info!(self.name, msg.connection.nick, "User is joining channel");
self.persistence
.do_send(crate::persistence::events::ChannelJoined {
.send(FetchUserChannelPermissions {
channel_id: self.channel_id,
user_id: msg.connection.user_id,
span: msg.span.clone(),
});
self.clients
.insert(msg.client.clone(), msg.connection.clone());
for client in self.clients.keys() {
client.do_send(Broadcast {
span: Span::current(),
message: irc_proto::Message {
tags: None,
prefix: Some(msg.connection.to_nick()),
command: Command::JOIN(self.name.to_string(), None, None),
},
});
}
for message in ChannelTopic::new(self).into_messages(self.name.to_string(), true) {
msg.client.do_send(Broadcast {
message,
span: Span::current(),
});
}
for message in ChannelNamesList::new(self).into_messages(msg.connection.nick.to_string()) {
msg.client.do_send(Broadcast {
message,
span: Span::current(),
});
}
MessageResult(Ok(ctx.address()))
})
.into_actor(self)
.map(move |res, this, ctx| {
let permissions: Permission = res.unwrap().unwrap_or(Permission::Normal);
if !permissions.can_join() {
return Ok(Err(ChannelJoinRejectionReason::Banned));
}
this.persistence
.do_send(crate::persistence::events::ChannelJoined {
channel_id: this.channel_id,
user_id: msg.connection.user_id,
span: msg.span.clone(),
});
this.clients
.insert(msg.client.clone(), (permissions, msg.connection.clone()));
for client in this.clients.keys() {
client.do_send(Broadcast {
span: Span::current(),
message: irc_proto::Message {
tags: None,
prefix: Some(msg.connection.to_nick()),
command: Command::JOIN(this.name.to_string(), None, None),
},
});
}
for message in ChannelTopic::new(this).into_messages(this.name.to_string(), true) {
msg.client.do_send(Broadcast {
message,
span: Span::current(),
});
}
for message in
ChannelNamesList::new(this).into_messages(msg.connection.nick.to_string())
{
msg.client.do_send(Broadcast {
message,
span: Span::current(),
});
}
Ok(Ok(ctx.address()))
})
.boxed_local()
}
}
@@ -205,11 +241,17 @@
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ChannelUpdateTopic, _ctx: &mut Self::Context) -> Self::Result {
let Some(client_info) = self.clients.get(&msg.client) else {
let Some((permissions, client_info)) = self.clients.get(&msg.client) else {
return;
};
debug!(msg.topic, "User is attempting to update channel topic");
if !permissions.can_set_topic() {
error!("User cannot set channel topic");
return;
}
self.topic = Some(CurrentChannelTopic {
topic: msg.topic,
@@ -217,7 +259,7 @@
set_time: Utc::now(),
});
for (client, connection) in &self.clients {
for (client, (_, connection)) in &self.clients {
for message in ChannelTopic::new(self).into_messages(connection.nick.to_string(), false)
{
client.do_send(Broadcast {
@@ -234,18 +276,25 @@
type Result = ();
fn handle(&mut self, msg: ChannelKickUser, _ctx: &mut Self::Context) -> Self::Result {
let Some(kicker) = self.clients.get(&msg.client) else {
let Some((permissions, kicker)) = self.clients.get(&msg.client) else {
error!("Kicker is unknown");
return;
};
if !permissions.can_kick() {
error!("Kicker can not kick people from the channel");
return;
}
let kicker = kicker.to_nick();
let kicked_user = self
.clients
.iter()
.find(|(_handle, client)| client.nick == msg.user)
.find(|(_handle, (_, client))| client.nick == msg.user)
.map(|(k, v)| (k.clone(), v));
let Some((kicked_user_handle, kicked_user_info)) = kicked_user else {
let Some((kicked_user_handle, (_, kicked_user_info))) = kicked_user else {
error!(msg.user, "Attempted to kick unknown user");
return;
};
@@ -290,7 +339,7 @@
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ChannelPart, ctx: &mut Self::Context) -> Self::Result {
let Some(client_info) = self.clients.remove(&msg.client) else {
let Some((_, client_info)) = self.clients.remove(&msg.client) else {
return;
};
@@ -322,7 +371,7 @@
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ChannelInvite, _ctx: &mut Self::Context) -> Self::Result {
let Some(source) = self.clients.get(&msg.client) else {
let Some((_, source)) = self.clients.get(&msg.client) else {
return Box::pin(futures::future::ready(ChannelInviteResult::NotOnChannel));
};
@@ -382,7 +431,7 @@
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ServerDisconnect, ctx: &mut Self::Context) -> Self::Result {
let Some(client_info) = self.clients.remove(&msg.client) else {
let Some((_, client_info)) = self.clients.remove(&msg.client) else {
return;
};
@@ -207,6 +207,14 @@
)
.map(|result, this, _ctx| {
for (channel_name, handle, messages) in result {
let handle = match handle {
Ok(v) => v,
Err(error) => {
error!(?error, "Failed to join user to channel");
continue;
}
};
this.channels.insert(channel_name.clone(), handle);
for (source, message) in messages {
@@ -51,7 +51,9 @@
#[derive(Message)]
#[rtype(result = "Result<Addr<Channel>>")]
#[rtype(
result = "Result<std::result::Result<Addr<Channel>, super::channel::response::ChannelJoinRejectionReason>>"
)]
pub struct ChannelJoin {
pub channel_name: String,
pub client: Addr<Client>,
@@ -7,9 +7,12 @@
use itertools::Itertools;
use tracing::instrument;
use crate::persistence::events::{
ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages,
FetchUserChannels,
use crate::{
channel::permissions::Permission,
persistence::events::{
ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages,
FetchUserChannelPermissions, FetchUserChannels, SetUserChannelPermissions,
},
};
@@ -116,6 +119,56 @@
)
.bind(msg.channel_id.0)
.bind(msg.user_id.0)
.execute(&conn)
.await
.unwrap();
})
}
}
impl Handler<FetchUserChannelPermissions> for Persistence {
type Result = ResponseFuture<Option<Permission>>;
fn handle(
&mut self,
msg: FetchUserChannelPermissions,
_ctx: &mut Self::Context,
) -> Self::Result {
let conn = self.database.clone();
Box::pin(async move {
sqlx::query_as(
"SELECT permissions
FROM channel_users
WHERE user = ?
AND channel = ?",
)
.bind(msg.user_id.0)
.bind(msg.channel_id.0)
.fetch_optional(&conn)
.await
.unwrap()
.map(|(v,)| v)
})
}
}
impl Handler<SetUserChannelPermissions> for Persistence {
type Result = ResponseFuture<()>;
fn handle(&mut self, msg: SetUserChannelPermissions, _ctx: &mut Self::Context) -> Self::Result {
let conn = self.database.clone();
Box::pin(async move {
sqlx::query(
"UPDATE channel_users
SET permissions = ?
WHERE user = ?
AND channel = ?",
)
.bind(msg.permissions)
.bind(msg.user_id.0)
.bind(msg.channel_id.0)
.execute(&conn)
.await
.unwrap();
@@ -1,0 +1,35 @@
#[derive(Copy, Clone, Debug, Eq, PartialEq, sqlx::Type)]
#[repr(i16)]
pub enum Permission {
Ban = -1,
Normal = 0,
Voice = 1,
HalfOperator = 2,
Operator = i16::MAX,
}
impl Permission {
#[must_use]
pub fn can_chatter(self) -> bool {
self != Self::Ban
}
#[must_use]
pub fn can_join(self) -> bool {
self != Self::Ban
}
#[must_use]
pub const fn can_set_topic(self) -> bool {
(self as i16) >= (Self::HalfOperator as i16)
}
#[must_use]
pub fn can_kick(self) -> bool {
self == Self::Operator
}
}
@@ -77,7 +77,7 @@
nick_list: channel
.clients
.values()
.map(|v| v.nick.to_string())
.map(|(_, v)| v.nick.to_string())
.collect(),
}
}
@@ -161,4 +161,9 @@
command,
})
}
}
#[derive(Copy, Clone, Debug)]
pub enum ChannelJoinRejectionReason {
Banned,
}
@@ -1,7 +1,10 @@
use actix::Message;
use tracing::Span;
use crate::{channel::ChannelId, connection::UserId};
use crate::{
channel::{permissions::Permission, ChannelId},
connection::UserId,
};
#[derive(Message)]
#[rtype(result = "i64")]
@@ -30,6 +33,21 @@
pub struct FetchUserChannels {
pub user_id: UserId,
pub span: Span,
}
#[derive(Message)]
#[rtype(result = "Option<Permission>")]
pub struct FetchUserChannelPermissions {
pub channel_id: ChannelId,
pub user_id: UserId,
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct SetUserChannelPermissions {
pub channel_id: ChannelId,
pub user_id: UserId,
pub permissions: Permission,
}
#[derive(Message)]