Load permission list at channel boot & send prefixes in LIST response
Diff
src/channel.rs | 349 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
src/connection.rs | 3 ++-
src/persistence.rs | 27 ++++++++++++++-------------
src/server.rs | 8 ++++++--
src/channel/permissions.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++
src/channel/response.rs | 8 +++++++-
src/persistence/events.rs | 7 ++++---
7 files changed, 296 insertions(+), 150 deletions(-)
@@ -20,14 +20,14 @@
},
},
client::Client,
connection::InitiatedConnection,
connection::{InitiatedConnection, UserId},
messages::{
Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser,
ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, ChannelUpdateTopic,
FetchClientByNick, ServerDisconnect, UserKickedFromChannel, UserNickChange,
},
persistence::{
events::{FetchUserChannelPermissions, SetUserChannelPermissions},
events::{FetchAllUserChannelPermissions, SetUserChannelPermissions},
Persistence,
},
server::Server,
@@ -41,7 +41,8 @@
pub struct Channel {
pub name: String,
pub server: Addr<Server>,
pub clients: HashMap<Addr<Client>, (Permission, InitiatedConnection)>,
pub permissions: HashMap<UserId, Permission>,
pub clients: HashMap<Addr<Client>, InitiatedConnection>,
pub topic: Option<CurrentChannelTopic>,
pub persistence: Addr<Persistence>,
pub channel_id: ChannelId,
@@ -57,12 +58,29 @@
name: self.name.to_string(),
})
.into_actor(self)
.then(|res, this, ctx| {
match res {
Ok(channel_id) => {
this.channel_id.0 = channel_id;
}
Err(error) => {
error!(%error, "Failed to create channel in database");
ctx.terminate();
}
}
this.persistence
.send(FetchAllUserChannelPermissions {
channel_id: this.channel_id,
})
.into_actor(this)
})
.map(|res, this, ctx| match res {
Ok(channel_id) => {
this.channel_id.0 = channel_id;
Ok(permissions) => {
this.permissions = permissions;
}
Err(error) => {
error!(%error, "Failed to create channel in database");
error!(%error, "Failed to fetch channel permissions");
ctx.terminate();
}
}),
@@ -72,6 +90,17 @@
impl Supervised for Channel {}
impl Channel {
#[must_use]
pub fn get_user_permissions(&self, user_id: UserId) -> Permission {
self.permissions
.get(&user_id)
.copied()
.unwrap_or(Permission::Normal)
}
}
impl Handler<Broadcast> for Channel {
type Result = ();
@@ -102,12 +131,12 @@
fn handle(&mut self, msg: ChannelMessage, _ctx: &mut Self::Context) -> Self::Result {
let Some((permissions, sender)) = self.clients.get(&msg.client) else {
let Some(sender) = self.clients.get(&msg.client) else {
error!("Received message from user not in channel");
return;
};
if !permissions.can_chatter() {
if !self.get_user_permissions(sender.user_id).can_chatter() {
error!("User cannot send message to channel");
return;
@@ -122,7 +151,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 +163,7 @@
client.do_send(Broadcast {
span: Span::current(),
message: irc_proto::Message {
message: Message {
tags: None,
prefix: Some(nick.clone()),
command: Command::PRIVMSG(self.name.to_string(), msg.message.clone()),
@@ -149,7 +178,7 @@
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ChannelSetMode, ctx: &mut Self::Context) -> Self::Result {
let Some((permissions, client)) = self.clients.get(&msg.client).cloned() else {
let Some(client) = self.clients.get(&msg.client).cloned() else {
return;
};
@@ -163,58 +192,93 @@
if let Ok(user_mode) = Permission::try_from(channel_mode) {
let Some(affected_nick) = arg else {
error!("No user given");
continue;
};
let Some((_, (affected_user_perms, affected_user))) =
self.clients.iter_mut().find(|(_, (_, connection))| {
connection.nick == affected_nick
}) else {
error!("Unknown user to set perms on");
continue;
};
let new_affected_user_perms = if add {
user_mode
} else if *affected_user_perms == user_mode {
Permission::Normal
} else {
error!("Removing the given permission would do nothing");
continue;
};
if !permissions.can_set_permission(new_affected_user_perms, *affected_user_perms) {
error!(
?permissions,
?new_affected_user_perms,
?affected_user_perms,
"User is not allowed to set permissions for this user"
);
continue;
}
self.persistence.do_send(SetUserChannelPermissions {
channel_id: self.channel_id,
user_id: affected_user.user_id,
permissions: new_affected_user_perms,
});
*affected_user_perms = new_affected_user_perms;
ctx.notify(Broadcast {
message: Message {
tags: None,
prefix: Some(client.to_nick()),
command: Command::ChannelMODE(self.name.to_string(), vec![mode]),
},
ctx.notify(SetUserMode {
requester: client.clone(),
add,
affected_nick,
user_mode,
span: Span::current(),
});
} else {
}
}
}
}
impl Handler<SetUserMode> for Channel {
type Result = ();
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: SetUserMode, ctx: &mut Self::Context) -> Self::Result {
let permissions = self.get_user_permissions(msg.requester.user_id);
let Some((_, affected_user)) = self.clients.iter().find(|(_, connection)| connection.nick == msg.affected_nick) else {
error!("Unknown user to set perms on");
return;
};
let affected_user_perms = self.get_user_permissions(affected_user.user_id);
let new_affected_user_perms = if msg.add {
msg.user_mode
} else if affected_user_perms == msg.user_mode {
Permission::Normal
} else {
error!("Removing the given permission would do nothing");
return;
};
if !permissions.can_set_permission(new_affected_user_perms, affected_user_perms) {
error!(
?permissions,
?new_affected_user_perms,
?affected_user_perms,
"User is not allowed to set permissions for this user"
);
return;
}
self.permissions
.insert(affected_user.user_id, new_affected_user_perms);
self.persistence.do_send(SetUserChannelPermissions {
channel_id: self.channel_id,
user_id: affected_user.user_id,
permissions: new_affected_user_perms,
});
let all_connected_for_user_id = self
.clients
.values()
.filter(|connection| connection.user_id == affected_user.user_id);
for connection in all_connected_for_user_id {
let Some(mode) = msg.user_mode.into_mode(msg.add, connection.nick.to_string()) else {
continue;
};
ctx.notify(Broadcast {
message: Message {
tags: None,
prefix: Some(connection.to_nick()),
command: Command::ChannelMODE(self.name.to_string(), vec![mode.clone()]),
},
span: Span::current(),
});
}
}
}
@@ -225,7 +289,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;
};
@@ -243,74 +307,88 @@
impl Handler<ChannelJoin> for Channel {
type Result = ResponseActFuture<
Self,
Result<Result<Addr<Self>, ChannelJoinRejectionReason>, anyhow::Error>,
>;
type Result = MessageResult<ChannelJoin>;
#[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");
let mut permissions = self
.permissions
.get(&msg.connection.user_id)
.copied()
.unwrap_or(Permission::Normal);
if !permissions.can_join() {
return MessageResult(Ok(Err(ChannelJoinRejectionReason::Banned)));
}
self.persistence
.send(FetchUserChannelPermissions {
.do_send(crate::persistence::events::ChannelJoined {
channel_id: self.channel_id,
user_id: msg.connection.user_id,
})
.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()
span: msg.span.clone(),
});
if self.permissions.is_empty() {
permissions = Permission::Founder;
self.permissions.insert(msg.connection.user_id, permissions);
self.persistence.do_send(SetUserChannelPermissions {
channel_id: self.channel_id,
user_id: msg.connection.user_id,
permissions,
});
}
self.clients
.insert(msg.client.clone(), msg.connection.clone());
for client in self.clients.keys() {
client.do_send(Broadcast {
span: Span::current(),
message: Message {
tags: None,
prefix: Some(msg.connection.to_nick()),
command: Command::JOIN(self.name.to_string(), None, None),
},
});
if let Some(mode) = permissions.into_mode(true, msg.connection.nick.to_string()) {
client.do_send(Broadcast {
span: Span::current(),
message: Message {
tags: None,
prefix: Some(msg.connection.to_nick()),
command: Command::ChannelMODE(self.name.to_string(), vec![mode]),
},
});
}
}
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(Ok(ctx.address())))
}
}
@@ -320,13 +398,16 @@
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: ChannelUpdateTopic, _ctx: &mut Self::Context) -> Self::Result {
let Some((permissions, client_info)) = self.clients.get(&msg.client) else {
let Some(client_info) = self.clients.get(&msg.client) else {
return;
};
debug!(msg.topic, "User is attempting to update channel topic");
if !permissions.can_set_topic() {
if !self
.get_user_permissions(client_info.user_id)
.can_set_topic()
{
error!("User cannot set channel topic");
return;
@@ -338,7 +419,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 {
@@ -355,12 +436,12 @@
type Result = ();
fn handle(&mut self, msg: ChannelKickUser, _ctx: &mut Self::Context) -> Self::Result {
let Some((permissions, kicker)) = self.clients.get(&msg.client) else {
let Some(kicker) = self.clients.get(&msg.client) else {
error!("Kicker is unknown");
return;
};
if !permissions.can_kick() {
if !self.get_user_permissions(kicker.user_id).can_kick() {
error!("Kicker can not kick people from the channel");
return;
@@ -371,9 +452,9 @@
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;
};
@@ -418,7 +499,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;
};
@@ -431,7 +512,7 @@
});
let message = Broadcast {
message: irc_proto::Message {
message: Message {
tags: None,
prefix: Some(client_info.to_nick()),
command: Command::PART(self.name.to_string(), msg.message),
@@ -450,7 +531,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));
};
@@ -510,13 +591,13 @@
#[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;
};
let message = Broadcast {
span: Span::current(),
message: irc_proto::Message {
message: Message {
tags: None,
prefix: Some(client_info.to_nick()),
command: Command::QUIT(msg.message),
@@ -533,4 +614,14 @@
pub topic: String,
pub set_by: String,
pub set_time: DateTime<Utc>,
}
#[derive(actix::Message)]
#[rtype(result = "()")]
pub struct SetUserMode {
requester: InitiatedConnection,
add: bool,
affected_nick: String,
user_mode: Permission,
span: Span,
}
@@ -30,7 +30,8 @@
pub const SUPPORTED_CAPABILITIES: &[&str] = &[concatcp!("sasl=", AuthStrategy::SUPPORTED)];
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, sqlx::Type)]
#[sqlx(transparent)]
pub struct UserId(pub i64);
#[derive(Default)]
@@ -1,6 +1,6 @@
pub mod events;
use std::time::Duration;
use std::{collections::HashMap, time::Duration};
use actix::{AsyncContext, Context, Handler, ResponseFuture, WrapFuture};
use chrono::Utc;
@@ -9,9 +9,11 @@
use crate::{
channel::permissions::Permission,
connection::UserId,
persistence::events::{
ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages,
FetchUserChannelPermissions, FetchUserChannels, ReserveNick, SetUserChannelPermissions,
ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted,
FetchAllUserChannelPermissions, FetchUnseenMessages, FetchUserChannels, ReserveNick,
SetUserChannelPermissions,
},
};
@@ -126,29 +128,28 @@
}
}
impl Handler<FetchUserChannelPermissions> for Persistence {
type Result = ResponseFuture<Option<Permission>>;
impl Handler<FetchAllUserChannelPermissions> for Persistence {
type Result = ResponseFuture<HashMap<UserId, Permission>>;
fn handle(
&mut self,
msg: FetchUserChannelPermissions,
msg: FetchAllUserChannelPermissions,
_ctx: &mut Self::Context,
) -> Self::Result {
let conn = self.database.clone();
Box::pin(async move {
sqlx::query_as(
"SELECT permissions
sqlx::query_as::<_, (UserId, Permission)>(
"SELECT user, permissions
FROM channel_users
WHERE user = ?
AND channel = ?",
WHERE channel = ?",
)
.bind(msg.user_id.0)
.bind(msg.channel_id.0)
.fetch_optional(&conn)
.fetch_all(&conn)
.await
.unwrap()
.map(|(v,)| v)
.into_iter()
.collect()
})
}
}
@@ -15,7 +15,7 @@
use tracing::{debug, instrument, warn, Span};
use crate::{
channel::{Channel, ChannelId},
channel::{permissions::Permission, Channel, ChannelId},
client::Client,
config::Config,
connection::InitiatedConnection,
@@ -91,7 +91,10 @@
),
(
Response::RPL_ISUPPORT,
vec!["D".into(), "are supported by this server".into()],
vec![
format!("PREFIX={}", Permission::SUPPORTED_PREFIXES).into(),
"are supported by this server".into(),
],
),
];
@@ -163,6 +166,7 @@
Supervisor::start_in_arbiter(&arbiter, move |_ctx| Channel {
name: channel_name,
permissions: HashMap::new(),
clients: HashMap::new(),
topic: None,
server,
@@ -1,5 +1,5 @@
use anyhow::anyhow;
use irc_proto::ChannelMode;
use irc_proto::{ChannelMode, Mode};
#[derive(Copy, Clone, Debug, Eq, PartialEq, sqlx::Type)]
#[repr(i16)]
@@ -23,6 +23,48 @@
ChannelMode::Oper => Ok(Self::Operator),
ChannelMode::Founder => Ok(Self::Founder),
_ => Err(anyhow!("unknown user access level: {value:?}")),
}
}
}
impl Permission {
pub const SUPPORTED_PREFIXES: &'static str = "(qohv)~@%+";
#[must_use]
pub fn into_mode(self, add: bool, nick: String) -> Option<Mode<ChannelMode>> {
<Option<ChannelMode>>::from(self).map(|v| {
if add {
Mode::Plus(v, Some(nick))
} else {
Mode::Minus(v, Some(nick))
}
})
}
#[must_use]
pub const fn into_prefix(self) -> &'static str {
match self {
Self::Ban | Self::Normal => "",
Self::Voice => "+",
Self::HalfOperator => "%",
Self::Operator => "@",
Self::Founder => "~",
}
}
}
impl From<Permission> for Option<ChannelMode> {
fn from(value: Permission) -> Self {
match value {
Permission::Ban => Some(ChannelMode::Ban),
Permission::Normal => None,
Permission::Voice => Some(ChannelMode::Voice),
Permission::HalfOperator => Some(ChannelMode::Halfop),
Permission::Operator => Some(ChannelMode::Oper),
Permission::Founder => Some(ChannelMode::Founder),
}
}
}
@@ -77,7 +77,13 @@
nick_list: channel
.clients
.values()
.map(|(_, v)| v.nick.to_string())
.map(|v| {
format!(
"{}{}",
channel.get_user_permissions(v.user_id).into_prefix(),
v.nick
)
})
.collect(),
}
}
@@ -1,3 +1,5 @@
use std::collections::HashMap;
use actix::Message;
use tracing::Span;
@@ -36,10 +38,9 @@
}
#[derive(Message)]
#[rtype(result = "Option<Permission>")]
pub struct FetchUserChannelPermissions {
#[rtype(result = "HashMap<UserId, Permission>")]
pub struct FetchAllUserChannelPermissions {
pub channel_id: ChannelId,
pub user_id: UserId,
}
#[derive(Message)]