🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2023-01-12 22:56:08.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-01-12 23:07:44.0 +00:00:00
commit
c471119d217ab39539ffabced0caeed3ff9640ea [patch]
tree
b2a7c49689ddb448f44736d7a8c221fbfbe5d735
parent
6e153814f682219b99543106ec08b8b01e2a2dda
download
c471119d217ab39539ffabced0caeed3ff9640ea.tar.gz

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

diff --git a/src/channel.rs b/src/channel.rs
index d77662f..a914628 100644
--- a/src/channel.rs
+++ a/src/channel.rs
@@ -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 {
    /// Grabs the user's permissions from the permission cache, defaulting to `Normal`.

    #[must_use]
    pub fn get_user_permissions(&self, user_id: UserId) -> Permission {
        self.permissions
            .get(&user_id)
            .copied()
            .unwrap_or(Permission::Normal)
    }
}

/// Broadcast a raw IRC message to all clients connected to this channel.

impl Handler<Broadcast> for Channel {
    type Result = ();
@@ -102,12 +131,12 @@
    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((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() {
            // TODO
            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 @@
            // broadcast the message to `client`
            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;
                };

                // TODO: this should allow setting perms not currently in the channel, this probably
                //  ties into fetching all user permissions on boot of the channel
                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 {
                // TODO
            }
        }
    }
}

/// Called by other users to set a permission on a user.

///

/// Users are currently only able to set permissions of users that are currently at a lower

/// permission level to themselves, and can only set permissions to levels lower than their

/// own.

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

        // TODO: this should allow setting perms not currently in the channel
        let Some((_, affected_user)) = self.clients.iter().find(|(_, connection)| connection.nick == msg.affected_nick) else {
            error!("Unknown user to set perms on");
            return;
        };

        // grab the permissions of the user we're trying to affect
        let affected_user_perms = self.get_user_permissions(affected_user.user_id);

        // calculate the new permissions that should be set on the user
        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;
        };

        // check if the caller can set these permissions on the user
        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;
        }

        // persist the permissions change both locally and to the database
        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,
        });

        // broadcast the change for all nicks that the affected user is connected with
        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 {
        // 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;
        };

@@ -243,74 +307,88 @@
///

/// This will return a `ChannelJoinRejectionReason` if the channel couldn't be joined.

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

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

                // 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()
                span: msg.span.clone(),
            });

        // we need to send out the set user channel permissions after the channel joined persistence
        // event has been sent so the user's row exists
        if self.permissions.is_empty() {
            // the first person to ever join the channel should get founder permissions
            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());

        // 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: 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]),
                    },
                });
            }
        }

        // 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(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()
        {
            // TODO
            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() {
            // TODO
            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,
}
diff --git a/src/connection.rs b/src/connection.rs
index d930e3d..ff37ef8 100644
--- a/src/connection.rs
+++ a/src/connection.rs
@@ -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)]
diff --git a/src/persistence.rs b/src/persistence.rs
index 00c9411..7f806ce 100644
--- a/src/persistence.rs
+++ a/src/persistence.rs
@@ -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()
        })
    }
}
diff --git a/src/server.rs b/src/server.rs
index 548e56a..ea70404 100644
--- a/src/server.rs
+++ a/src/server.rs
@@ -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,
diff --git a/src/channel/permissions.rs b/src/channel/permissions.rs
index aa538af..af63c85 100644
--- a/src/channel/permissions.rs
+++ a/src/channel/permissions.rs
@@ -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 {
    /// A list of (mode)prefix used to inform clients of which modes set which prefixes.

    pub const SUPPORTED_PREFIXES: &'static str = "(qohv)~@%+";

    /// Builds the mode message that's used to set (or unset) this permission.

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

    /// Grabs the prefix that is used to represent a permission.

    #[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),
        }
    }
}
diff --git a/src/channel/response.rs b/src/channel/response.rs
index 8457f0f..b0d2357 100644
--- a/src/channel/response.rs
+++ a/src/channel/response.rs
@@ -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(),
        }
    }
diff --git a/src/persistence/events.rs b/src/persistence/events.rs
index 2fbdd95..1591817 100644
--- a/src/persistence/events.rs
+++ a/src/persistence/events.rs
@@ -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)]