🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2023-01-09 21:15:56.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-01-09 21:15:56.0 +00:00:00
commit
b62d80bd9e9b51b082136943359644e05a4f8275 [patch]
tree
8048fdefd0bd5f7ae4d92e683d6f06ad65a9e9cd
parent
27570a6c6019beed4c9f8465ff01bad9320b79fe
download
b62d80bd9e9b51b082136943359644e05a4f8275.tar.gz

Implement basic channel permissioning for kicks, joins, chatting & topic setting



Diff

 src/channel.rs             | 155 ++++++++++++++++++++++++++++++----------------
 src/channel/permissions.rs |  35 ++++++++++-
 src/channel/response.rs    |   7 +-
 src/client.rs              |   8 ++-
 src/messages.rs            |   4 +-
 src/persistence.rs         |  59 +++++++++++++++++-
 src/persistence/events.rs  |  20 +++++-
 7 files changed, 229 insertions(+), 59 deletions(-)

diff --git a/src/channel.rs b/src/channel.rs
index 9498509..0dfff9f 100644
--- a/src/channel.rs
+++ b/src/channel.rs
@@ -1,3 +1,4 @@
pub mod permissions;
pub mod response;

use std::collections::HashMap;
@@ -12,7 +13,12 @@ use irc_proto::{Command, Message};
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 @@ use crate::{
        ChannelMemberList, ChannelMessage, ChannelPart, ChannelUpdateTopic, FetchClientByNick,
        ServerDisconnect, UserKickedFromChannel, UserNickChange,
    },
    persistence::Persistence,
    persistence::{events::FetchUserChannelPermissions, Persistence},
    server::Server,
};

@@ -32,7 +38,7 @@ pub struct ChannelId(pub i64);
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,11 +99,17 @@ impl Handler<ChannelMessage> for Channel {
    fn handle(&mut self, msg: ChannelMessage, _ctx: &mut Self::Context) -> Self::Result {
        // ensure the user is actually in the channel by their handle, and grab their
        // nick & host if they are
        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() {
            // TODO
            error!("User cannot send message to channel");
            return;
        }

        // build the nick prefix for the message we're about to broadcast
        let nick = sender.to_nick();

@@ -106,7 +118,7 @@ impl Handler<ChannelMessage> for Channel {
                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 @@ impl Handler<UserNickChange> for Channel {

    fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result {
        // grab the user's current info
        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<UserNickChange> for Channel {
///
/// Sends the current topic & user list, and returns a handle to the channel so the user can
/// start sending us messages.
///
/// This will return a `ChannelJoinRejectionReason` if the channel couldn't be joined.
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");

        // persist the user's join to the database
        // TODO: maybe do the lookup at channel `started` so we dont have to do a query every time
        //  a user attempts to join the 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());

        // broadcast the user's join to everyone in the channel, including the joining user
        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),
                },
            });
        }

        // send the channel's topic to the joining user
        for message in ChannelTopic::new(self).into_messages(self.name.to_string(), true) {
            msg.client.do_send(Broadcast {
                message,
                span: Span::current(),
            });
        }

        // send the user list to the user
        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));
                }

                // persist the user's join to the database
                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()));

                // broadcast the user's join to everyone in the channel, including the joining user
                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),
                        },
                    });
                }

                // send the channel's topic to the joining user
                for message in ChannelTopic::new(this).into_messages(this.name.to_string(), true) {
                    msg.client.do_send(Broadcast {
                        message,
                        span: Span::current(),
                    });
                }

                // send the user list to the user
                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,19 +241,25 @@ impl Handler<ChannelUpdateTopic> for Channel {

    #[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() {
            // TODO
            error!("User cannot set channel topic");
            return;
        }

        self.topic = Some(CurrentChannelTopic {
            topic: msg.topic,
            set_by: client_info.nick.to_string(),
            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 @@ impl Handler<ChannelKickUser> for Channel {
    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() {
            // TODO
            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 @@ impl Handler<ChannelPart> for Channel {

    #[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 @@ impl Handler<ChannelInvite> for Channel {

    #[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 @@ impl Handler<ServerDisconnect> for Channel {

    #[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;
        };

diff --git a/src/channel/permissions.rs b/src/channel/permissions.rs
new file mode 100644
index 0000000..8ce47ac
--- /dev/null
+++ b/src/channel/permissions.rs
@@ -0,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 {
    /// Returns true, if the user is allowed to chat in the channel.
    #[must_use]
    pub fn can_chatter(self) -> bool {
        self != Self::Ban
    }

    /// Returns true, if the user is allowed to join the channel.
    #[must_use]
    pub fn can_join(self) -> bool {
        self != Self::Ban
    }

    /// Returns true, if the user is allowed to set the channel topic.
    #[must_use]
    pub const fn can_set_topic(self) -> bool {
        (self as i16) >= (Self::HalfOperator as i16)
    }

    /// Returns true, if the user is allowed to kick people from the channel.
    #[must_use]
    pub fn can_kick(self) -> bool {
        self == Self::Operator
    }
}
diff --git a/src/channel/response.rs b/src/channel/response.rs
index 98c718e..8457f0f 100644
--- a/src/channel/response.rs
+++ b/src/channel/response.rs
@@ -77,7 +77,7 @@ impl ChannelNamesList {
            nick_list: channel
                .clients
                .values()
                .map(|v| v.nick.to_string())
                .map(|(_, v)| v.nick.to_string())
                .collect(),
        }
    }
@@ -162,3 +162,8 @@ impl ChannelInviteResult {
        })
    }
}

#[derive(Copy, Clone, Debug)]
pub enum ChannelJoinRejectionReason {
    Banned,
}
diff --git a/src/client.rs b/src/client.rs
index d8aaf32..ba93af0 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -207,6 +207,14 @@ impl Handler<JoinChannelRequest> for Client {
        )
        .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 {
diff --git a/src/messages.rs b/src/messages.rs
index 4b54dba..d8fc843 100644
--- a/src/messages.rs
+++ b/src/messages.rs
@@ -51,7 +51,9 @@ pub struct ChannelList {

/// Sent when the user attempts to join a channel.
#[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>,
diff --git a/src/persistence.rs b/src/persistence.rs
index ec5ee13..b18f655 100644
--- a/src/persistence.rs
+++ b/src/persistence.rs
@@ -7,9 +7,12 @@ use chrono::Utc;
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,
    },
};

/// Takes events destined for other actors and persists them to the database.
@@ -123,6 +126,56 @@ impl Handler<ChannelParted> for Persistence {
    }
}

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();
        })
    }
}

impl Handler<FetchUserChannels> for Persistence {
    type Result = ResponseFuture<Vec<String>>;

diff --git a/src/persistence/events.rs b/src/persistence/events.rs
index 2117825..266c432 100644
--- a/src/persistence/events.rs
+++ b/src/persistence/events.rs
@@ -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")]
@@ -33,6 +36,21 @@ pub struct FetchUserChannels {
}

#[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)]
#[rtype(result = "()")]
pub struct ChannelMessage {
    pub channel_id: ChannelId,