Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/sync user #267

Merged
merged 4 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Added:
- Configuration option to enable a topic banner in channels. This can be enabled under `buffer.channel.topic`
- Messages typed into the input will persist until sent. Typed messages are saved when switching a pane to another buffer, then
are restored when that buffer is returned to.
- Added display of highest access level in front of nicks in chat, mirroring the format in the nick list
- Added ability to toggle Op and Voice from user context menu in channels

Fix:

Expand Down
22 changes: 20 additions & 2 deletions data/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,9 @@ impl Client {
if victim == self.nickname().as_ref() {
self.chanmap.remove(channel);
} else if let Some(channel) = self.chanmap.get_mut(channel) {
channel.users.remove(&User::from(Nick::from(victim.as_str())));
channel
.users
.remove(&User::from(Nick::from(victim.as_str())));
}
}
Command::Numeric(RPL_WHOREPLY, args) => {
Expand Down Expand Up @@ -708,7 +710,13 @@ impl Client {
self.chanmap.get(channel).map(|channel| &channel.topic)
}

fn users<'a>(&'a self, channel: &str) -> &'a [User] {
fn resolve_user_attributes<'a>(&'a self, channel: &str, user: &User) -> Option<&'a User> {
self.chanmap
.get(channel)
.and_then(|channel| channel.users.get(user))
}

pub fn users<'a>(&'a self, channel: &str) -> &'a [User] {
self.users
.get(channel)
.map(Vec::as_slice)
Expand Down Expand Up @@ -854,6 +862,16 @@ impl Map {
}
}

pub fn resolve_user_attributes<'a>(
&'a self,
server: &Server,
channel: &str,
user: &User,
) -> Option<&'a User> {
self.client(server)
.and_then(|client| client.resolve_user_attributes(channel, user))
}

pub fn get_channel_users<'a>(&'a self, server: &Server, channel: &str) -> &'a [User] {
self.client(server)
.map(|client| client.users(channel))
Expand Down
14 changes: 14 additions & 0 deletions data/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum Kind {
Part,
Topic,
Kick,
Mode,
Raw,
}

Expand All @@ -35,6 +36,7 @@ impl FromStr for Kind {
"part" => Ok(Kind::Part),
"topic" => Ok(Kind::Topic),
"kick" => Ok(Kind::Kick),
"mode" => Ok(Kind::Mode),
"raw" => Ok(Kind::Raw),
_ => Err(()),
}
Expand All @@ -53,6 +55,7 @@ pub enum Command {
Part(String, Option<String>),
Topic(String, Option<String>),
Kick(String, String, Option<String>),
Mode(String, Option<String>, Vec<String>),
Raw(String, Vec<String>),
Unknown(String, Vec<String>),
}
Expand Down Expand Up @@ -107,6 +110,16 @@ pub fn parse(s: &str, buffer: Option<&Buffer>) -> Result<Command, Error> {
Kind::Kick => validated::<2, 1, true>(args, |[channel, user], [comment]| {
Command::Kick(channel, user, comment)
}),
Kind::Mode => {
let (channel, rest) = args.split_first().ok_or(Error::MissingCommand)?;
let (mode, users) = rest.split_first().ok_or(Error::MissingCommand)?;

Ok(Command::Mode(
channel.to_string(),
Some(mode.to_string()),
users.iter().map(|s| s.to_string()).collect(),
))
}
Kind::Raw => {
let (cmd, args) = args.split_first().ok_or(Error::MissingCommand)?;

Expand Down Expand Up @@ -176,6 +189,7 @@ impl TryFrom<Command> for proto::Command {
Command::Part(chanlist, reason) => proto::Command::PART(chanlist, reason),
Command::Topic(channel, topic) => proto::Command::TOPIC(channel, topic),
Command::Kick(channel, user, comment) => proto::Command::KICK(channel, user, comment),
Command::Mode(channel, mode, users) => proto::Command::MODE(channel, mode, users),
Command::Raw(command, args) => proto::Command::Unknown(command, args),
Command::Unknown(command, args) => proto::Command::new(&command, args),
})
Expand Down
6 changes: 3 additions & 3 deletions data/src/history/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::config::buffer::Exclude;
use crate::history::{self, History};
use crate::message::{self, Limit};
use crate::time::Posix;
use crate::user::{Nick, NickRef};
use crate::user::Nick;
use crate::{config, input};
use crate::{server, Buffer, Config, Input, Server, User};

Expand Down Expand Up @@ -167,8 +167,8 @@ impl Manager {
}
}

pub fn record_input(&mut self, input: Input, our_nick: NickRef) {
if let Some(message) = input.message(our_nick) {
pub fn record_input(&mut self, input: Input, user: User) {
if let Some(message) = input.message(user) {
self.record_message(input.server(), message);
}

Expand Down
10 changes: 3 additions & 7 deletions data/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use irc::proto;
use irc::proto::format;

use crate::time::Posix;
use crate::user::NickRef;
use crate::{command, message, Buffer, Command, Message, Server, User};

const INPUT_HISTORY_LENGTH: usize = 100;
Expand Down Expand Up @@ -56,7 +55,7 @@ impl Input {
self.buffer.server()
}

pub fn message(&self, our_nick: NickRef) -> Option<Message> {
pub fn message(&self, user: User) -> Option<Message> {
let to_target = |target: String, source| {
if proto::is_channel(&target) {
Some(message::Target::Channel {
Expand All @@ -80,18 +79,15 @@ impl Input {
received_at: Posix::now(),
server_time: Utc::now(),
direction: message::Direction::Sent,
target: to_target(
target,
message::Source::User(User::from(our_nick.to_owned())),
)?,
target: to_target(target, message::Source::User(user))?,
text,
}),
Command::Me(target, action) => Some(Message {
received_at: Posix::now(),
server_time: Utc::now(),
direction: message::Direction::Sent,
target: to_target(target, message::Source::Action)?,
text: message::action_text(our_nick, &action),
text: message::action_text(user.nickname(), &action),
}),
_ => None,
}
Expand Down
78 changes: 54 additions & 24 deletions data/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,15 @@ impl Message {
&& matches!(self.target.source(), Source::User(_) | Source::Action)
}

pub fn received(encoded: Encoded, our_nick: Nick, config: &Config) -> Option<Message> {
pub fn received(
encoded: Encoded,
our_nick: Nick,
config: &Config,
resolve_attributes: impl Fn(&User, &str) -> Option<User>,
) -> Option<Message> {
let server_time = server_time(&encoded);
let text = text(&encoded, &our_nick, config)?;
let target = target(encoded, &our_nick)?;
let text = text(&encoded, &our_nick, config, &resolve_attributes)?;
let target = target(encoded, &our_nick, &resolve_attributes)?;

Some(Message {
received_at: Posix::now(),
Expand All @@ -110,7 +115,11 @@ impl Message {
}
}

fn target(message: Encoded, our_nick: &Nick) -> Option<Target> {
fn target(
message: Encoded,
our_nick: &Nick,
resolve_attributes: &dyn Fn(&User, &str) -> Option<User>,
) -> Option<Target> {
use proto::command::Numeric::*;

let user = message.user();
Expand Down Expand Up @@ -176,10 +185,13 @@ fn target(message: Encoded, our_nick: &Nick) -> Option<Target> {
};

match (proto::is_channel(&target), user) {
(true, Some(user)) => Some(Target::Channel {
channel: target,
source: source(user),
}),
(true, Some(user)) => {
let source = source(resolve_attributes(&user, &target).unwrap_or(user));
Some(Target::Channel {
channel: target,
source,
})
}
(false, Some(user)) => {
let target = User::try_from(target.as_str()).ok()?;

Expand All @@ -202,10 +214,13 @@ fn target(message: Encoded, our_nick: &Nick) -> Option<Target> {
};

match (proto::is_channel(&target), user) {
(true, Some(user)) => Some(Target::Channel {
channel: target,
source: source(user),
}),
(true, Some(user)) => {
let source = source(resolve_attributes(&user, &target).unwrap_or(user));
Some(Target::Channel {
channel: target,
source,
})
}
(false, Some(user)) => {
let target = User::try_from(target.as_str()).ok()?;

Expand Down Expand Up @@ -274,28 +289,39 @@ pub fn server_time(message: &Encoded) -> DateTime<Utc> {
.unwrap_or_else(Utc::now)
}

fn text(message: &Encoded, our_nick: &Nick, config: &Config) -> Option<String> {
fn text(
message: &Encoded,
our_nick: &Nick,
config: &Config,
resolve_attributes: &dyn Fn(&User, &str) -> Option<User>,
) -> Option<String> {
use irc::proto::command::Numeric::*;

let user = message.user();
match &message.command {
Command::TOPIC(_, topic) => {
let user = user?;
Command::TOPIC(target, topic) => {
let raw_user = message.user()?;
let user = resolve_attributes(&raw_user, target).unwrap_or(raw_user);

let topic = topic.as_ref()?;

Some(format!(" ∙ {user} changed topic to {topic}"))
}
Command::PART(_, text) => {
let user = user?.formatted(config.buffer.server_messages.part.username_format);
Command::PART(target, text) => {
let raw_user = message.user()?;
let user = resolve_attributes(&raw_user, target)
.unwrap_or(raw_user)
.formatted(config.buffer.server_messages.part.username_format);

let text = text
.as_ref()
.map(|text| format!(" ({text})"))
.unwrap_or_default();

Some(format!("⟵ {user} has left the channel{text}"))
}
Command::JOIN(_, _) => {
let user = user?;
Command::JOIN(target, _) => {
let raw_user = message.user()?;
let user = resolve_attributes(&raw_user, target).unwrap_or(raw_user);

(user.nickname() != *our_nick).then(|| {
format!(
Expand All @@ -304,8 +330,10 @@ fn text(message: &Encoded, our_nick: &Nick, config: &Config) -> Option<String> {
)
})
}
Command::KICK(_, victim, comment) => {
let user = user?;
Command::KICK(channel, victim, comment) => {
let raw_user = message.user()?;
let user = resolve_attributes(&raw_user, channel).unwrap_or(raw_user);

let comment = comment
.as_ref()
.map(|comment| format!(" ({comment})"))
Expand All @@ -319,7 +347,9 @@ fn text(message: &Encoded, our_nick: &Nick, config: &Config) -> Option<String> {
Some(format!("⟵ {target} been kicked by {user}{comment}"))
}
Command::MODE(target, modes, args) if proto::is_channel(target) => {
let user = user?;
let raw_user = message.user()?;
let user = resolve_attributes(&raw_user, target).unwrap_or(raw_user);

let modes = modes
.iter()
.map(|mode| mode.to_string())
Expand All @@ -336,7 +366,7 @@ fn text(message: &Encoded, our_nick: &Nick, config: &Config) -> Option<String> {
}
Command::PRIVMSG(_, text) => {
// Check if a synthetic action message
if let Some(nick) = user.as_ref().map(User::nickname) {
if let Some(nick) = message.user().as_ref().map(User::nickname) {
if let Some(action) = parse_action(nick, text) {
return Some(action);
}
Expand Down
8 changes: 6 additions & 2 deletions data/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ impl User {
.unwrap_or(AccessLevel::Member)
}

pub fn has_access_level(&self, access_level: AccessLevel) -> bool {
self.access_levels.get(&access_level).is_some()
}

pub fn update_access_level(&mut self, operation: mode::Operation, mode: mode::Channel) {
if let Ok(level) = AccessLevel::try_from(mode) {
match operation {
Expand Down Expand Up @@ -203,7 +207,7 @@ impl From<proto::User> for User {

impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.nickname())
write!(f, "{}{}", self.highest_access_level(), self.nickname())
}
}

Expand Down Expand Up @@ -279,7 +283,7 @@ impl<'a> PartialEq<Nick> for NickRef<'a> {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum AccessLevel {
Member,
Voice,
Expand Down
9 changes: 5 additions & 4 deletions src/buffer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub use data::buffer::Settings;
use data::{buffer, history, Config, User};
use data::user::Nick;
use data::{buffer, history, Config};
use iced::Command;

use self::channel::Channel;
Expand Down Expand Up @@ -154,19 +155,19 @@ impl Buffer {

pub fn insert_user_to_input(
&mut self,
user: User,
nick: Nick,
history: &mut history::Manager,
) -> Command<Message> {
if let Some(buffer) = self.data() {
match self {
Buffer::Empty | Buffer::Server(_) => Command::none(),
Buffer::Channel(channel) => channel
.input_view
.insert_user(user, buffer, history)
.insert_user(nick, buffer, history)
.map(|message| Message::Channel(channel::Message::InputView(message))),
Buffer::Query(query) => query
.input_view
.insert_user(user, buffer, history)
.insert_user(nick, buffer, history)
.map(|message| Message::Query(query::Message::InputView(message))),
}
} else {
Expand Down
Loading
Loading