Ver Fonte

channels

Rain há 10 meses atrás
pai
commit
010104ffbb
7 ficheiros alterados com 267 adições e 45 exclusões
  1. 1 0
      client/Cargo.toml
  2. 6 2
      client/src/actions.rs
  3. 97 7
      client/src/main.rs
  4. 25 22
      client/src/message.rs
  5. 7 1
      client/src/persistence.rs
  6. 105 12
      client/src/ui.rs
  7. 26 1
      utils/src/serialize.rs

+ 1 - 0
client/Cargo.toml

@@ -2,6 +2,7 @@
 name = "client"
 version = "0.1.0"
 edition = "2021"
+default-run = "client"
 
 [dependencies]
 utils = { path = "../utils" }

+ 6 - 2
client/src/actions.rs

@@ -17,7 +17,10 @@ pub fn on_message_click(siv: &mut Cursive, message_id: &str) {
         ui::alert(
             siv,
             ui::Labels::Message.localize(appdata.language),
-            format!("{}\n{}\n{}", message.time, message.sender, message.content),
+            format!(
+                "Time: {}\nChannel: {}\nSender: {}\nContent: {}",
+                message.time, message.channel, message.sender, message.content
+            ),
         );
     }
 }
@@ -31,8 +34,9 @@ pub fn on_input_submit(siv: &mut Cursive, input_field_id: &str) {
 
     let appdata = get_appdata(siv);
     let language = appdata.language;
+    let channel = appdata.current_channel;
 
-    let message = Message::new(&appdata.username, &text);
+    let message = Message::new(&appdata.username, &text, &channel);
     if message.is_valid() {
         // NOTE:
         // If an error was shown during send, it's likely that loading will fail too

+ 97 - 7
client/src/main.rs

@@ -24,6 +24,7 @@ use ui::{
 const MAX_MESSAGE_LENGTH: usize = 512;
 const MAX_USERNAME_LENGTH: usize = 16;
 const DEFAULT_USERNAME_PREFIX: &str = "Myst";
+const DEFAULT_CHANNEL: &str = "Root";
 
 const SAVE_FILE: &str = "savedata.bin";
 const SAVE_FILE_FUZZY: u64 = 0b0110110001101001011001110110110101100001001000000101100001000100;
@@ -36,9 +37,11 @@ pub struct Appdata {
     pub blocked_phrases_censor_char: char,
     pub api_endpoint: String,
     pub api_refresh_rate: usize,
+    pub current_channel: String,
 
     pub messages: HashMap<String, Message>,
     pub quick_close_window_count: usize,
+    pub local_channels: Vec<String>,
 }
 
 impl Default for Appdata {
@@ -62,6 +65,8 @@ impl Appdata {
             api_refresh_rate: 10,
             blocked_phrases: Vec::new(),
             blocked_phrases_censor_char: '*',
+            current_channel: DEFAULT_CHANNEL.to_string(),
+            local_channels: vec![DEFAULT_CHANNEL.to_string()],
         }
     }
 }
@@ -139,6 +144,71 @@ fn main() {
         });
     }
 
+    // Channels
+    let channels_panel = {
+        let mut view: SelectView<String> = SelectView::new();
+        view.set_on_submit(move |siv, channel_id: &str| {
+            siv.with_user_data(|appdata: &mut Appdata| {
+                appdata.current_channel = channel_id.to_string();
+            });
+
+            ui::visual_update(siv);
+        });
+        Panel::new(ScrollView::new(view)).with_name(ui::CHANNEL_VIEW_ID)
+    };
+
+    let channel_current_panel = {
+        let view = TextView::new("");
+        Panel::new(view).with_name(ui::CHANNEL_CURRENT_PANEL_ID)
+    };
+
+    let channels_new_button = Button::new("", move |siv| {
+        ui::keybind_setup_close_once(siv);
+
+        let language = get_appdata(siv).language;
+
+        siv.add_layer(
+            cursive::views::Dialog::new()
+                .title(ui::Labels::NewChannel.localize(language))
+                .content(
+                    LinearLayout::vertical().child(
+                        EditView::new()
+                            .with_name(ui::CHANNEL_NEW_FIELD_ID)
+                            .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
+                    ),
+                )
+                .button(ui::Labels::Ok.localize(language), move |siv| {
+                    let input = siv
+                        .call_on_name(ui::CHANNEL_NEW_FIELD_ID, |view: &mut EditView| {
+                            view.get_content()
+                        })
+                        .expect("Failed to retrieve channel creation input.");
+
+                    if message::is_valid_username(&input) {
+                        // Valid
+                        siv.with_user_data(|appdata: &mut Appdata| {
+                            appdata.local_channels.push(input.to_string());
+                        });
+
+                        ui::visual_update(siv);
+                        ui::keybind_close_manual_end(siv, false);
+                    } else {
+                        // Invalid
+                        ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
+                        ui::error(
+                            siv,
+                            ui::Labels::InvalidChannelNameExplination.localize(language),
+                        );
+                    }
+                })
+                .button(ui::Labels::Cancel.localize(language), |siv| {
+                    ui::keybind_close_manual_end(siv, false);
+                })
+                .min_size(DIALOGUE_MIN_SIZE),
+        );
+    })
+    .with_name(ui::CHANNEL_NEW_BUTTON_ID);
+
     // Users
     let users_panel = {
         let mut users_view: SelectView<String> = SelectView::new();
@@ -380,7 +450,7 @@ fn main() {
                                 .child(
                                     EditView::new()
                                         .content(appdata.api_endpoint)
-                                        .with_name(ui::SERVER_SETTINGS_FIELD_ID)
+                                        .with_name(ui::SERVER_SETTINGS_ADDRESS_FIELD_ID)
                                         .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
                                         .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
                                 ),
@@ -395,20 +465,33 @@ fn main() {
                                 .child(
                                     EditView::new()
                                         .content(appdata.api_refresh_rate.to_string())
+                                        .with_name(ui::SERVER_SETTINGS_REFRESH_FIELD_ID)
                                         .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
                                         .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
                                 ),
                         ),
                 )
                 .button(ui::Labels::Ok.localize(language), |siv| {
-                    let input = siv
-                        .call_on_name(ui::SERVER_SETTINGS_FIELD_ID, |view: &mut EditView| {
-                            view.get_content()
-                        })
-                        .expect("Failed to retrieve server settings input.");
+                    let input_addr = siv
+                        .call_on_name(
+                            ui::SERVER_SETTINGS_ADDRESS_FIELD_ID,
+                            |view: &mut EditView| view.get_content(),
+                        )
+                        .expect("Failed to retrieve server settings address input.");
+
+                    let input_refresh = siv
+                        .call_on_name(
+                            ui::SERVER_SETTINGS_REFRESH_FIELD_ID,
+                            |view: &mut EditView| view.get_content(),
+                        )
+                        .expect("Failed to retrieve server settings refresh rate input.");
 
                     siv.with_user_data(|appdata: &mut Appdata| {
-                        appdata.api_endpoint = input.to_string();
+                        appdata.api_endpoint = input_addr.to_string();
+
+                        if let Ok(rate) = input_refresh.parse() {
+                            appdata.api_refresh_rate = rate;
+                        }
                     });
 
                     ui::visual_update(siv);
@@ -426,6 +509,11 @@ fn main() {
     let main_layout = LinearLayout::vertical()
         .child(
             LinearLayout::horizontal()
+                .child(
+                    LinearLayout::vertical()
+                        .child(channels_panel.fixed_width(USER_PANEL_SIZE).full_height())
+                        .child(channel_current_panel),
+                )
                 .child(
                     LinearLayout::vertical()
                         .child(users_panel.resized(
@@ -443,6 +531,8 @@ fn main() {
                 .child(DummyView.full_width())
                 .child(username_button)
                 .child(DummyView.full_width())
+                .child(channels_new_button)
+                .child(DummyView.full_width())
                 .child(blocked_words_list_button)
                 .child(DummyView.full_width())
                 .child(server_settings_button)

+ 25 - 22
client/src/message.rs

@@ -10,20 +10,24 @@ use crate::{get_appdata, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH};
 pub struct Message {
     pub sender: String,
     pub content: String,
+    pub channel: String,
     pub time: u128,
 }
 
 impl Message {
-    pub fn new(sender: &str, content: &str) -> Self {
+    pub fn new(sender: &str, content: &str, channel: &str) -> Self {
         Message {
             sender: sender.to_string(),
             content: content.to_string(),
+            channel: channel.to_string(),
             time: time_millis(),
         }
     }
 
     pub fn is_valid(&self) -> bool {
-        is_valid_username(&self.sender) && is_valid_message(&self.content)
+        is_valid_username(&self.sender)
+            && is_valid_username(&self.channel)
+            && is_valid_message(&self.content)
     }
 }
 
@@ -43,9 +47,10 @@ impl Serialize for Message {
 
     fn serialize_unchecked(&self) -> Vec<u8> {
         let separate_bytes: Vec<Vec<u8>> = vec![
-            self.sender.as_bytes().to_vec(),
-            self.content.as_bytes().to_vec(),
-            self.time.to_be_bytes().to_vec(),
+            self.sender.serialize_checked(),
+            self.content.serialize_checked(),
+            self.channel.serialize_checked(),
+            self.time.serialize_checked(),
         ];
 
         separate_bytes.serialize_unchecked()
@@ -57,29 +62,19 @@ impl Serialize for Message {
         Self: Sized,
     {
         let separate_bytes = SetOfSerializedObjects::deserialize_unchecked(data)?;
-        if separate_bytes.len() != 3 {
-            return Err(serialize::DeserializationError::InvalidData(
-                "Invalid number of fields",
-            ));
+        if separate_bytes.len() != 4 {
+            return Err(serialize::DeserializationError::InvalidDataLength);
         }
 
-        let sender = String::from_utf8(separate_bytes[0].clone())
-            .map_err(|_| serialize::DeserializationError::InvalidData("Sender isn't UTF-8"))?;
-        let content = String::from_utf8(separate_bytes[1].clone())
-            .map_err(|_| serialize::DeserializationError::InvalidData("Content isn't UTF-8"))?;
-        let timestamp_bytes = &separate_bytes[2];
-        if timestamp_bytes.len() != 16 {
-            return Err(serialize::DeserializationError::InvalidData(
-                "Invalid timestamp length",
-            ));
-        }
-        let mut timestamp = [0u8; 16];
-        timestamp.copy_from_slice(timestamp_bytes);
-        let timestamp = u128::from_be_bytes(timestamp);
+        let sender = String::deserialize_checked(&separate_bytes[0])?;
+        let content = String::deserialize_checked(&separate_bytes[1])?;
+        let channel = String::deserialize_checked(&separate_bytes[2])?;
+        let timestamp = u128::deserialize_checked(&separate_bytes[3])?;
 
         Ok(Message {
             sender,
             content,
+            channel,
             time: timestamp,
         })
     }
@@ -101,6 +96,7 @@ pub fn is_valid_message(message: &str) -> bool {
 pub struct MessageSanitized {
     pub sender: String,
     pub content: String,
+    pub channel: String,
     pub time: u128,
     pub hash: Hash,
 }
@@ -112,6 +108,7 @@ impl MessageSanitized {
         let censor_char = appdata.blocked_phrases_censor_char;
         self.sender = utils::strings::remove_bad_words(&self.sender, &wordlist, censor_char);
         self.content = utils::strings::remove_bad_words(&self.content, &wordlist, censor_char);
+        self.channel = utils::strings::remove_bad_words(&self.channel, &wordlist, censor_char);
     }
 }
 
@@ -123,6 +120,11 @@ impl From<Message> for MessageSanitized {
             sender = format!("Invl@{}", &value.sender.hash().to_string()[..8]);
         }
 
+        let mut channel = value.channel.clone();
+        if !is_valid_username(&channel) {
+            channel = format!("Invl@{}", &value.channel.hash().to_string()[..8]);
+        }
+
         let mut content = value.content.clone();
         if !is_valid_message(&value.content) {
             content = format!("Invalid@{}", &value.content.hash().to_string());
@@ -131,6 +133,7 @@ impl From<Message> for MessageSanitized {
         MessageSanitized {
             sender: sender.replace("\n", ""),
             content: content.replace("\n", ""),
+            channel: channel.replace("\n", ""),
             time: value.time,
             hash: value.hash(),
         }

+ 7 - 1
client/src/persistence.rs

@@ -9,6 +9,7 @@ pub struct Savedata {
     pub blocked_phrases_censor_char: char,
     pub api_endpoint: String,
     pub api_refresh_rate: u64,
+    pub current_channel: String,
 }
 
 impl From<Appdata> for Savedata {
@@ -20,6 +21,7 @@ impl From<Appdata> for Savedata {
             blocked_phrases_censor_char: value.blocked_phrases_censor_char,
             api_endpoint: value.api_endpoint,
             api_refresh_rate: value.api_refresh_rate as u64,
+            current_channel: value.current_channel,
         }
     }
 }
@@ -34,6 +36,7 @@ impl From<Savedata> for Appdata {
         s.blocked_phrases_censor_char = value.blocked_phrases_censor_char;
         s.api_endpoint = value.api_endpoint;
         s.api_refresh_rate = value.api_refresh_rate as usize;
+        s.current_channel = value.current_channel;
 
         s
     }
@@ -87,6 +90,7 @@ impl Serialize for Savedata {
         data.push(self.blocked_phrases_censor_char.serialize_checked());
         data.push(self.api_endpoint.serialize_checked());
         data.push(self.api_refresh_rate.serialize_checked());
+        data.push(self.current_channel.serialize_checked());
 
         data.serialize_checked()
     }
@@ -99,7 +103,7 @@ impl Serialize for Savedata {
         let data = data.as_ref();
         let serialized_items = SetOfSerializedObjects::deserialize_checked(data)?;
 
-        if serialized_items.len() != 6 {
+        if serialized_items.len() != 7 {
             return Err(DeserializationError::InvalidDataLength);
         }
 
@@ -109,6 +113,7 @@ impl Serialize for Savedata {
         let blocked_phrases_censor_char = char::deserialize_checked(&serialized_items[3])?;
         let api_endpoint = String::deserialize_checked(&serialized_items[4])?;
         let api_refresh_rate = u64::deserialize_checked(&serialized_items[5])?;
+        let current_channel = String::deserialize_checked(&serialized_items[6])?;
 
         Ok(Self {
             username,
@@ -117,6 +122,7 @@ impl Serialize for Savedata {
             blocked_phrases_censor_char,
             api_endpoint,
             api_refresh_rate,
+            current_channel,
         })
     }
 }

+ 105 - 12
client/src/ui.rs

@@ -11,6 +11,10 @@ use crate::{get_appdata, message, Appdata, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENG
 
 pub const USERS_PANEL_ID: &str = "users_view_id";
 pub const MESSAGE_PANEL_ID: &str = "message_view_id";
+pub const CHANNEL_VIEW_ID: &str = "channel_view_id";
+pub const CHANNEL_NEW_FIELD_ID: &str = "channel_new_field_id";
+pub const CHANNEL_NEW_BUTTON_ID: &str = "CHANNEL_NEW_BUTTON_ID";
+pub const CHANNEL_CURRENT_PANEL_ID: &str = "CHANNEL_CURRENT_PANEL_ID";
 pub const INPUT_FIELD_ID: &str = "input_field_id";
 pub const INPUT_PANEL_ID: &str = "input_panel_id";
 pub const INPUT_BUTTON_ID: &str = "input_button_id";
@@ -21,7 +25,8 @@ pub const USERNAME_FIELD_ID: &str = "username_field_id";
 pub const USERNAME_BUTTON_ID: &str = "username_button_id";
 pub const REFRESH_BUTTON_ID: &str = "refresh_button_id";
 pub const BLOCKED_WORDS_BUTTON_ID: &str = "blocked_words_view_id";
-pub const SERVER_SETTINGS_FIELD_ID: &str = "server_settings_field_id";
+pub const SERVER_SETTINGS_ADDRESS_FIELD_ID: &str = "server_settings_address_field_id";
+pub const SERVER_SETTINGS_REFRESH_FIELD_ID: &str = "server_settings_refresh_field_id";
 pub const SERVER_SETTINGS_BUTTON_ID: &str = "server_settings_button_id";
 
 pub const DIALOGUE_MIN_SIZE: (u16, u16) = (20, 5);
@@ -34,12 +39,17 @@ pub enum Labels {
     Close,
     Submit,
     Send,
+    New,
     Clear,
     Error,
     User,
     Users,
     Message,
     Messages,
+    Channel,
+    Channels,
+    NewChannel,
+    InvalidChannelNameExplination,
     TypeAMessage,
     SetLanguage,
     SetUsername,
@@ -134,11 +144,11 @@ impl Labels {
             Labels::InvalidUsernameExplination => {
                 buf = [
                     r"Invalid username. Must match ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
-                        .replace("${MAX_MESSAGE_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
+                        .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
                     r"Ongeldige gebruikersnaam. Moet overeenkomen met ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
-                        .replace("${MAX_MESSAGE_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
+                        .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
                     r"無効なユーザー名。 ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$ に一致する必要があります"
-                        .replace("${MAX_MESSAGE_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
+                        .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
                 ];
 
                 [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
@@ -162,12 +172,32 @@ impl Labels {
             ],
             Labels::Submit => ["Submit", "Indienen", "提出する"],
             Labels::ServerSettings => ["Server settings", "Serverinstellingen", "サーバー設定"],
-            Labels::ServerAddress => ["Server address", "Serveradres", "サーバーアドレス"],
+            Labels::ServerAddress => [
+                "Server address (IP:Port)",
+                "Serveradres (IP:poort)",
+                "サーバーアドレス(IP:ポート)",
+            ],
             Labels::ServerRefreshRate => [
                 "Update rate (seconds)",
                 "Updatefrequentie (seconden)",
                 "更新率 (秒)",
             ],
+            Labels::Channels => ["Channels", "Kanalen", "チャンネル"],
+            Labels::NewChannel => ["New Channel", "Nieuw kanaal", "新しいチャンネル"],
+            Labels::InvalidChannelNameExplination => {
+                buf = [
+                    r"Invalid channel name. Must match ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
+                        .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
+                    r"Ongeldige kanaalnaam. Moet overeenkomen met ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
+                        .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
+                    r"無効なチャンネル名。 ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$ に一致する必要があります"
+                        .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
+                ];
+
+                [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
+            }
+            Labels::New => ["New", "Nieuw", "新"],
+            Labels::Channel => ["Channel", "Kanaal", "チャネル"],
         };
 
         let idx = match language {
@@ -237,6 +267,10 @@ pub fn keybind_close_manual_end(siv: &mut Cursive, close_all: bool) {
 
 pub fn visual_update(siv: &mut Cursive) {
     let appdata = get_appdata(siv);
+    let current_channel = appdata.current_channel;
+    let language = get_appdata(siv).language;
+
+    // --- Messages ---
 
     let mut messages: Vec<message::MessageSanitized> = appdata
         .messages
@@ -252,7 +286,6 @@ pub fn visual_update(siv: &mut Cursive) {
         message.remove_blocked_phrases(siv);
     }
 
-    // Messages
     siv.call_on_name(
         MESSAGE_PANEL_ID,
         |panel: &mut Panel<ScrollView<SelectView>>| {
@@ -261,7 +294,10 @@ pub fn visual_update(siv: &mut Cursive) {
             let selected = view.selected_id();
 
             view.clear();
-            for message in messages.iter() {
+            for message in messages
+                .iter()
+                .filter(|x| channel_matches(&x.channel, &current_channel))
+            {
                 view.add_item(
                     // TODO: Localized timestamps
                     format!(
@@ -280,19 +316,24 @@ pub fn visual_update(siv: &mut Cursive) {
         },
     );
 
-    // Members list
+    // --- Members list ---
+
     siv.call_on_name(
         USERS_PANEL_ID,
         |panel: &mut Panel<ScrollView<SelectView>>| {
             let view = panel.get_inner_mut().get_inner_mut();
 
             let mut senders: Vec<String> = vec![];
-            for message in messages.iter() {
+            for message in messages
+                .iter()
+                .filter(|x| channel_matches(&x.channel, &current_channel))
+            {
                 if senders.contains(&message.sender) {
                     continue;
                 }
                 senders.push(message.sender.clone());
             }
+            senders.sort();
 
             let selected = view.selected_id();
             view.clear();
@@ -306,7 +347,50 @@ pub fn visual_update(siv: &mut Cursive) {
         },
     );
 
-    // Current username
+    // --- Channels ---
+
+    siv.call_on_name(
+        CHANNEL_VIEW_ID,
+        |panel: &mut Panel<ScrollView<SelectView>>| {
+            let view = panel.get_inner_mut().get_inner_mut();
+
+            let mut channels = appdata.local_channels.clone();
+            for message in messages.iter() {
+                if !channels.contains(&message.channel) {
+                    channels.push(message.channel.clone());
+                }
+            }
+            channels.sort();
+
+            let selected = view.selected_id();
+            view.clear();
+
+            for channel in channels {
+                view.add_item(channel.clone(), channel.clone());
+            }
+
+            if let Some(selected) = selected {
+                view.set_selection(selected);
+            }
+        },
+    );
+
+    // --- Current channel ---
+
+    let appdata_c = get_appdata(siv);
+    siv.call_on_name(
+        CHANNEL_CURRENT_PANEL_ID,
+        |view: &mut NamedView<Panel<TextView>>| {
+            view.get_mut()
+                .get_inner_mut()
+                .set_content(appdata_c.current_channel);
+
+            view.get_mut().set_title(Labels::Channel.localize(language));
+        },
+    );
+
+    // --- Current username ---
+
     siv.call_on_name(
         CURRENT_USERNAME_PANEL_ID,
         |view: &mut NamedView<Panel<TextView>>| {
@@ -314,18 +398,23 @@ pub fn visual_update(siv: &mut Cursive) {
         },
     );
 
-    // Localize buttons
-    let language = get_appdata(siv).language;
+    // --- Localize buttons ---
 
     for (name, label) in [
         (USERS_PANEL_ID, Labels::Users.localize(language)),
         (MESSAGE_PANEL_ID, Labels::Messages.localize(language)),
+        (CHANNEL_VIEW_ID, Labels::Channels.localize(language)),
     ] {
         siv.call_on_name(name, |panel: &mut Panel<ScrollView<SelectView>>| {
             panel.set_title(label);
         });
     }
 
+    siv.call_on_name(CHANNEL_NEW_BUTTON_ID, |view: &mut NamedView<Button>| {
+        view.get_mut()
+            .set_label(Labels::NewChannel.localize(language));
+    });
+
     siv.call_on_name(INPUT_PANEL_ID, |panel: &mut Panel<LinearLayout>| {
         panel.set_title(Labels::TypeAMessage.localize(language));
     });
@@ -380,3 +469,7 @@ pub fn change_language(siv: &mut Cursive, language: Language) {
 
     visual_update(siv);
 }
+
+pub fn channel_matches(message_channel: &str, reference_channel: &str) -> bool {
+    message_channel == reference_channel
+}

+ 26 - 1
utils/src/serialize.rs

@@ -317,5 +317,30 @@ impl Serialize for u64 {
 
 #[test]
 fn test_u64_serialization() {
-    basic_test(1234567890);
+    basic_test(1234567890u128);
+}
+
+impl Serialize for u128 {
+    const SIGNATURE: &'static str = "u128";
+    const LENGTH: usize = u128::BITS as usize / 8;
+
+    fn serialize_unchecked(&self) -> Vec<u8> {
+        let b = self.to_be_bytes();
+        b.to_vec()
+    }
+
+    fn deserialize_unchecked<B>(data: B) -> Result<Self>
+    where
+        B: AsRef<[u8]>,
+        Self: Sized,
+    {
+        let data = data.as_ref();
+
+        let mut buf = [0u8; Self::LENGTH];
+        buf.copy_from_slice(data);
+
+        let i = u128::from_be_bytes(buf);
+
+        Ok(i)
+    }
 }