From 2c0a8ab61631e0b79f8517de4c6c20f98db3850b Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Tue, 5 Mar 2024 00:24:15 +0100
Subject: [PATCH 1/2] Add event return type

---
 Cargo.lock            |  2 +-
 src/commands.rs       |  3 +-
 src/main.rs           |  2 +
 src/models/channel.rs |  1 -
 src/models/event.rs   | 83 +++++++++++++++++++++++++++++++++++++++++
 src/models/mod.rs     |  4 +-
 src/response.rs       | 86 +++++++++++++++++++++++++++++++++++++------
 7 files changed, 166 insertions(+), 15 deletions(-)
 create mode 100644 src/models/event.rs

diff --git a/Cargo.lock b/Cargo.lock
index edd51d3..01db8ec 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -139,7 +139,7 @@ dependencies = [
 
 [[package]]
 name = "teamspeak-query-lib"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "clap",
  "telnet",
diff --git a/src/commands.rs b/src/commands.rs
index a448c55..4e2a323 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -106,4 +106,5 @@ pub fn clientupdate(connection: &mut Telnet, parameters: ParameterList) -> Resul
 
 pub fn sendtextmessage(connection: &mut Telnet, target: SendTextMessageTarget, msg: String) -> Result<Response, String> {
     let msg = String::from(Parameter::new("msg", &msg));
-    send_command(connection, &format!("sendtextmessage {} {}", msg, String::from(target)), false) }
+    send_command(connection, &format!("sendtextmessage {} {}", msg, String::from(target)), false)
+}
diff --git a/src/main.rs b/src/main.rs
index f4d735b..8899b28 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -49,6 +49,8 @@ fn main() {
     wrappers::skip_welcome(&mut connection);
     wrappers::login(&mut connection);
 
+
+
     // You can check for the existence of subcommands, and if found use their
     // matches just as you would the top level cmd
     match cli {
diff --git a/src/models/channel.rs b/src/models/channel.rs
index bc0d928..731dcf5 100644
--- a/src/models/channel.rs
+++ b/src/models/channel.rs
@@ -81,4 +81,3 @@ impl Channel {
         list_sorted
     }
 }
-
diff --git a/src/models/event.rs b/src/models/event.rs
new file mode 100644
index 0000000..d18f943
--- /dev/null
+++ b/src/models/event.rs
@@ -0,0 +1,83 @@
+use std::fmt::{Display, Formatter};
+
+#[derive(Debug)]
+pub enum Event {
+    ChannelList,
+    ChannelListFinished,
+    NotifyTalkStatusChange,
+    NotifyMessage,
+    NotifyMessageList,
+    NotifyComplainList,
+    NotifyBanList,
+    NotifyClientMoved,
+    NotifyClientLeftView,
+    NotifyClientEnterView,
+    NotifyClientPoke,
+    NotifyClientChatClosed,
+    NotifyClientChatComposing,
+    NotifyClientUpdated,
+    NotifyClientIds,
+    NotifyClientDBIDFromUid,
+    NotifyClientNameFromUid,
+    NotifyClientNameFromDBID,
+    NotifyClientUidFromClid,
+    NotifyConnectionInfo,
+    NotifyChannelCreated,
+    NotifyChannelEdited,
+    NotifyChannelDeleted,
+    NotifyChannelMoved,
+    NotifyServerEdited,
+    NotifyServerUpdated,
+    NotifyTextMessage,
+    NotifyCurrentServerConnectionChanged,
+    NotifyConnectStatusChange,
+}
+
+impl TryFrom<&str> for Event {
+    type Error = String;
+
+    //noinspection SpellCheckingInspection
+    fn try_from(event_type: &str) -> Result<Self, Self::Error> {
+        match event_type {
+            "channellist" => Ok(Event::ChannelList),
+            "channellistfinished" => Ok(Event::ChannelListFinished),
+            "notifytalkstatuschange" => Ok(Event::NotifyTalkStatusChange),
+            "notifymessage" => Ok(Event::NotifyMessage),
+            "notifymessagelist" => Ok(Event::NotifyMessageList),
+            "notifycomplainlist" => Ok(Event::NotifyComplainList),
+            "notifybanlist" => Ok(Event::NotifyBanList),
+            "notifyclientmoved" => Ok(Event::NotifyClientMoved),
+            "notifyclientleftview" => Ok(Event::NotifyClientLeftView),
+            "notifycliententerview" => Ok(Event::NotifyClientEnterView),
+            "notifyclientpoke" => Ok(Event::NotifyClientPoke),
+            "notifyclientchatclosed" => Ok(Event::NotifyClientChatClosed),
+            "notifyclientchatcomposing" => Ok(Event::NotifyClientChatComposing),
+            "notifyclientupdated" => Ok(Event::NotifyClientUpdated),
+            "notifyclientids" => Ok(Event::NotifyClientIds),
+            "notifyclientdbidfromuid" => Ok(Event::NotifyClientDBIDFromUid),
+            "notifyclientnamefromuid" => Ok(Event::NotifyClientNameFromUid),
+            "notifyclientnamefromdbid" => Ok(Event::NotifyClientNameFromDBID),
+            "notifyclientuidfromclid" => Ok(Event::NotifyClientUidFromClid),
+            "notifyconnectioninfo" => Ok(Event::NotifyConnectionInfo),
+            "notifychannelcreated" => Ok(Event::NotifyChannelCreated),
+            "notifychanneledited" => Ok(Event::NotifyChannelEdited),
+            "notifychanneldeleted" => Ok(Event::NotifyChannelDeleted),
+            "notifychannelmoved" => Ok(Event::NotifyChannelMoved),
+            "notifyserveredited" => Ok(Event::NotifyServerEdited),
+            "notifyserverupdated" => Ok(Event::NotifyServerUpdated),
+            "notifytextmessage" => Ok(Event::NotifyTextMessage),
+            "notifycurrentserverconnectionchanged" => {
+                Ok(Event::NotifyCurrentServerConnectionChanged)
+            }
+            "notifyconnectstatuschange" => Ok(Event::NotifyConnectStatusChange),
+            _ => Err(String::from("Unknown event type.")),
+        }
+    }
+}
+
+
+impl Display for Event {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:?}", self)
+    }
+}
\ No newline at end of file
diff --git a/src/models/mod.rs b/src/models/mod.rs
index df71696..a37ebaf 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -1,5 +1,7 @@
 mod channel;
 mod client;
+mod event;
 
 pub use channel::Channel;
-pub use client::Client;
\ No newline at end of file
+pub use client::Client;
+pub use event::Event;
\ No newline at end of file
diff --git a/src/response.rs b/src/response.rs
index d4c5a00..261ff24 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -1,4 +1,5 @@
 use std::fmt::{Debug, Display, Formatter};
+use crate::models::Event;
 
 use crate::parameter::*;
 
@@ -6,6 +7,14 @@ pub enum Response {
     Ok,
     Data(ParameterList),
     DataList(Vec<ParameterList>),
+    Event(Event, ParameterList),
+}
+
+pub enum ResponseType {
+    Error,
+    Event(Event),
+    Data,
+    Unknown,
 }
 
 pub struct ResponseError {
@@ -13,23 +22,66 @@ pub struct ResponseError {
     pub msg: String,
 }
 
+fn split_type_and_params(response_str: &str) -> Result<(ResponseType, &str), String> {
+    if let Some(first_space) = response_str.find(' ') {
+        if let Some(first_equal) = response_str.find('=') {
+            if first_equal > first_space {
+                // The first word is not a parameter. Error or Event?
+                let (type_str, params) = response_str.split_once(' ').unwrap(); // We found a space, so this is safe.
+                let response_type = get_response_type(type_str)?;
+                return Ok((response_type, params));
+            }
+        }
+    }
+    Ok((ResponseType::Data, response_str))
+}
+
+fn get_response_type(type_str: &str) -> Result<ResponseType, String> {
+    if type_str == "error" {
+        return Ok(ResponseType::Error);
+    }
+    if type_str.starts_with("notify") || type_str == "channellist" || type_str == "channellistfinished" {
+        let event_type = Event::try_from(type_str)?;
+        return Ok(ResponseType::Event(event_type));
+    }
+
+    return Ok(ResponseType::Unknown);
+}
+
 impl TryFrom<String> for Response {
     type Error = ResponseError;
 
     fn try_from(response_str: String) -> Result<Self, ResponseError> {
-        let mut response_str = response_str.trim_end_matches("\n\r");
+        let response_str = response_str.trim_end_matches("\n\r");
 
-        if response_str.starts_with("error ") {
-            response_str = response_str.trim_start_matches("error ");
-            let response_params = parameter_parse(response_str);
-            Err(ResponseError::create_error(&response_params))
-        } else {
-            let mut parameter_lists: Vec<ParameterList> = Vec::new();
-            for response_entry in response_str.split('|') {
-                let response_params = parameter_parse(response_entry);
-                parameter_lists.push(response_params);
+        let (rsp_type, rsp_params) = split_type_and_params(&response_str)
+            .map_err(|err| ResponseError {
+                id: -1,
+                msg: err,
+            })?;
+
+        let mut parameter_lists: Vec<ParameterList> = Vec::new();
+        for response_entry in rsp_params.split('|') {
+            let response_params = parameter_parse(response_entry);
+            parameter_lists.push(response_params);
+        }
+
+        return match rsp_type {
+            ResponseType::Error => {
+                Err(ResponseError::create_error(&parameter_lists[0]))
+            }
+            ResponseType::Event(event_type) => {
+                Ok(Response::Event(event_type, parameter_lists[0].clone()))
+            }
+            ResponseType::Data => {
+                Ok(Response::DataList(parameter_lists))
+            }
+            ResponseType::Unknown => {
+                Err(ResponseError {
+                    id: -1,
+                    msg: "Unknown response type.".to_string(),
+                })
             }
-            Ok(Response::DataList(parameter_lists))
         }
     }
 }
@@ -52,6 +104,11 @@ impl Debug for Response {
                 }
                 Ok(())
             }
+            Response::Event(event, params) => {
+                write!(f, "Event: {:?}", event)?;
+                write!(f, "{:?};", params)?;
+                Ok(())
+            }
         }
     }
 }
@@ -78,6 +135,13 @@ impl Display for Response {
                 }
                 Ok(())
             }
+            Response::Event(event, params) => {
+                write!(f, "Event: {}", event)?;
+                for param in params {
+                    write!(f, "{};", param)?;
+                }
+                Ok(())
+            }
         }
     }
 }

From d8cdc2bb1194814bba0c970ddee0f18a2d541716 Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Tue, 5 Mar 2024 03:52:30 +0100
Subject: [PATCH 2/2] Add events and refactor

Add event listener with JSON output (WIP)
Add notifier on movement events
Refactor Parameter and ParameterList (still shit)
---
 Cargo.lock            |  45 +++++++++++
 Cargo.toml            |   3 +
 src/cli.rs            |  34 ++++----
 src/commands.rs       |  17 ++--
 src/main.rs           |  35 +++++++-
 src/models/channel.rs |  17 ++--
 src/models/client.rs  |  14 ++--
 src/models/event.rs   | 182 +++++++++++++++++++++++++++++++++---------
 src/models/mod.rs     |   2 +-
 src/parameter.rs      | 119 ++++++++-------------------
 src/response.rs       |  29 ++++---
 src/utils.rs          |  10 +--
 src/wrappers.rs       |  10 +--
 ts-control            |   7 +-
 14 files changed, 338 insertions(+), 186 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 01db8ec..d658b69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -102,6 +102,12 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.69"
@@ -120,6 +126,43 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[package]]
+name = "serde"
+version = "1.0.193"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.193"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "strsim"
 version = "0.10.0"
@@ -142,6 +185,8 @@ name = "teamspeak-query-lib"
 version = "0.1.1"
 dependencies = [
  "clap",
+ "serde",
+ "serde_json",
  "telnet",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index d43dbcd..a39f88b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,3 +6,6 @@ edition = "2021"
 [dependencies]
 telnet = "0.2"
 clap = { version = "4.4", features = ["derive"] }
+
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
diff --git a/src/cli.rs b/src/cli.rs
index f518b41..9cfdb0d 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,8 +1,8 @@
 use clap::{Args, Parser, Subcommand};
 use telnet::Telnet;
 
-use crate::parameter::{Parameter, ParameterList};
-use crate::models::Channel;
+use crate::parameter::ParameterList;
+use crate::models::{Channel, EventType};
 use crate::models::Client;
 use crate::utils::SendTextMessageTarget;
 use crate::wrappers;
@@ -23,6 +23,7 @@ pub enum Commands {
     Message(MessageArgs),
     Move(MoveArgs),
     Update(UpdateArgs),
+    Events(EventArgs),
 }
 
 #[derive(Args)]
@@ -78,6 +79,11 @@ pub struct UpdateArgs {
     speakers: Option<bool>,
 }
 
+#[derive(Args)]
+pub struct EventArgs {
+    pub event: Vec<EventType>,
+}
+
 impl FetchArgs {
     pub fn want_channel(&self) -> bool {
         self.channel.is_some()
@@ -89,14 +95,14 @@ impl FetchArgs {
 
     pub fn channel(&self, connection: &mut Telnet) -> Result<Option<Channel>, String> {
         if let Some(channel) = &self.channel {
-            wrappers::find_channel(connection, channel, self.strict_channel)
+            wrappers::find_channel(connection, "channel_name", channel, self.strict_channel)
         } else {
             Err("No channel specified.".to_string())
         }
     }
     pub fn client(&self, connection: &mut Telnet) -> Result<Option<Client>, String> {
         if let Some(client) = &self.client {
-            wrappers::find_client(connection, client, self.strict_client)
+            wrappers::find_client(connection, "client_nickname", client, self.strict_client)
         } else {
             Err("No client specified.".to_string())
         }
@@ -108,7 +114,7 @@ impl MessageArgs {
         if self.server {
             Ok(SendTextMessageTarget::Server)
         } else if let Some(client) = &self.client {
-            if let Some(client) = wrappers::find_client(connection, client, self.strict_client)? {
+            if let Some(client) = wrappers::find_client(connection, "client_nickname", client, self.strict_client)? {
                 return Ok(SendTextMessageTarget::Client(client));
             }
             return Err("Could not find client.".to_string());
@@ -120,12 +126,12 @@ impl MessageArgs {
 
 impl MoveArgs {
     pub fn channel(&self, connection: &mut Telnet) -> Result<Option<Channel>, String> {
-        wrappers::find_channel(connection, &self.channel, self.strict_channel)
+        wrappers::find_channel(connection, "channel_name", &self.channel, self.strict_channel)
     }
     pub fn client(&self, connection: &mut Telnet) -> Result<Option<Client>, String> {
         match &self.client {
             Some(client) => {
-                wrappers::find_client(connection, client, self.strict_client)
+                wrappers::find_client(connection, "client_nickname", client, self.strict_client)
             }
             None => {
                 match wrappers::find_self(connection) {
@@ -139,25 +145,25 @@ impl MoveArgs {
 
 impl UpdateArgs {
     pub fn to_parameter_list(&self) -> ParameterList {
-        let mut params: ParameterList = Vec::new();
+        let mut params: ParameterList = ParameterList::new();
 
         if let Some(name) = &self.name {
-            params.push(Parameter::new("client_nickname", name));
+            params.insert(String::from("client_nickname"), name.clone());
         }
 
         if let Some(away) = &self.away {
-            params.push(Parameter::new("client_away_message", away));
-            params.push(Parameter::new("client_away", "1"));
+            params.insert(String::from("client_away_message"), away.clone());
+            params.insert(String::from("client_away"), String::from("1"));
         }
         if self.back {
-            params.push(Parameter::new("client_away", "0"));
+            params.insert(String::from("client_away"), String::from("0"));
         }
 
         if let Some(microphone) = self.microphone {
-            params.push(Parameter::new("client_input_muted", &u8::from(!microphone).to_string()));
+            params.insert(String::from("client_input_muted"), u8::from(!microphone).to_string());
         }
         if let Some(speakers) = self.speakers {
-            params.push(Parameter::new("client_output_muted", &u8::from(!speakers).to_string()));
+            params.insert(String::from("client_output_muted"), u8::from(!speakers).to_string());
         }
 
         params
diff --git a/src/commands.rs b/src/commands.rs
index 4e2a323..204e455 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -1,8 +1,9 @@
 use crate::utils::SendTextMessageTarget;
 use telnet::Event::Data;
 use telnet::Telnet;
+use crate::models::EventType;
 
-use crate::parameter::{Parameter, ParameterList};
+use crate::parameter::{Parameter, parameters_to_string, ParameterList, parameter_to_string, parameter_list_to_string};
 use crate::response::Response;
 
 fn to_single_response(resp: Response) -> Response {
@@ -48,7 +49,7 @@ fn send_command(connection: &mut Telnet, command: &str, skip_ok: bool) -> Result
     read_response(connection, skip_ok, String::new())
 }
 
-fn read_response(connection: &mut Telnet, skip_ok: bool, mut buffer: String) -> Result<Response, String> {
+pub fn read_response(connection: &mut Telnet, skip_ok: bool, mut buffer: String) -> Result<Response, String> {
     let (response_str, buffer) = read_response_buffer(connection, &mut buffer)?;
 
     match Response::try_from(response_str) {
@@ -93,18 +94,22 @@ pub fn whoami(connection: &mut Telnet) -> Result<Response, String> {
 pub fn clientmove(connection: &mut Telnet, cid: &i32, clid_list: Vec<&i32>) -> Result<Response, String> {
     let clid_param_list = clid_list
         .into_iter()
-        .map(|clid| Parameter::new("clid", &clid.to_string()))
+        .map(|clid| (String::from("clid"), clid.to_string()))
         .collect::<Vec<Parameter>>();
-    let clid_str = Parameter::list_to_string_sep(clid_param_list, "|");
+    let clid_str = parameters_to_string(clid_param_list, "|");
     send_command(connection, &format!("clientmove cid={} {}", cid, clid_str), false)
 }
 
 pub fn clientupdate(connection: &mut Telnet, parameters: ParameterList) -> Result<Response, String> {
-    let parameters_str = Parameter::list_to_string(parameters);
+    let parameters_str = parameter_list_to_string(parameters, " ");
     send_command(connection, &format!("clientupdate {}", parameters_str), false)
 }
 
 pub fn sendtextmessage(connection: &mut Telnet, target: SendTextMessageTarget, msg: String) -> Result<Response, String> {
-    let msg = String::from(Parameter::new("msg", &msg));
+    let msg = parameter_to_string((String::from("msg"), msg));
     send_command(connection, &format!("sendtextmessage {} {}", msg, String::from(target)), false)
 }
+
+pub fn clientnotifyregister(connection: &mut Telnet, schandlerid: u32, event: EventType) -> Result<Response, String> {
+    send_command(connection, &format!("clientnotifyregister schandlerid={} event={}", schandlerid, String::from(&event)), false)
+}
diff --git a/src/main.rs b/src/main.rs
index 8899b28..6af36ac 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,8 +3,9 @@ use std::process::exit;
 use telnet::Telnet;
 
 use crate::cli::Commands;
-use crate::models::Channel;
+use crate::models::{Channel, Event};
 use crate::models::Client;
+use crate::response::Response;
 
 mod wrappers;
 mod commands;
@@ -49,8 +50,6 @@ fn main() {
     wrappers::skip_welcome(&mut connection);
     wrappers::login(&mut connection);
 
-
-
     // You can check for the existence of subcommands, and if found use their
     // matches just as you would the top level cmd
     match cli {
@@ -154,5 +153,35 @@ fn main() {
                 }
             }
         }
+
+        Commands::Events(args) => {
+            for event in args.event {
+                if commands::clientnotifyregister(&mut connection, 1, event).is_err() {
+                    println!("Failed to register event listener.");
+                    exit(1);
+                }
+            }
+
+            loop {
+                match commands::read_response(&mut connection, true, String::new()) {
+                    Ok(response) => {
+                        if let Response::Event(event_type, params) = response {
+
+                            let event = Event::new(&mut connection, event_type, params);
+                            match serde_json::to_string(&event) {
+                                Ok(json) => println!("{}", json),
+                                Err(_) => {
+                                    // TODO: Handle error
+                                }
+                            }
+
+                        }
+                    }
+                    Err(_) => {
+                        // TODO: Handle error
+                    }
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/src/models/channel.rs b/src/models/channel.rs
index 731dcf5..ed7f5f7 100644
--- a/src/models/channel.rs
+++ b/src/models/channel.rs
@@ -1,8 +1,9 @@
 use std::fmt::{Display, Formatter};
+use serde::{Deserialize, Serialize};
 
 use crate::parameter::{parameter_find, ParameterList};
 
-#[derive(Debug)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct Channel {
     pub cid: i32,
     pub pid: i32,
@@ -23,22 +24,20 @@ impl From<ParameterList> for Channel {
         Channel {
             cid: parameter_find(&value, "cid")
                 .unwrap_or_default()
-                .to_i32(-1),
+                .parse::<i32>().unwrap_or(-1),
             pid: parameter_find(&value, "pid")
                 .unwrap_or_default()
-                .to_i32(-1),
+                .parse::<i32>().unwrap_or(-1),
             channel_order: parameter_find(&value, "channel_order")
                 .unwrap_or_default()
-                .to_i32(-1),
+                .parse::<i32>().unwrap_or(-1),
             channel_name: parameter_find(&value, "channel_name")
-                .unwrap_or_default()
-                .value,
+                .unwrap_or_default(),
             channel_topic: parameter_find(&value, "channel_topic")
-                .unwrap_or_default()
-                .value,
+                .unwrap_or_default(),
             channel_flag_are_subscribed: parameter_find(&value, "channel_flag_are_subscribed")
                 .unwrap_or_default()
-                .to_i32(0) == 1,
+                .parse::<i32>().unwrap_or(0) == 1,
         }
     }
 }
diff --git a/src/models/client.rs b/src/models/client.rs
index 25405e0..fe38e06 100644
--- a/src/models/client.rs
+++ b/src/models/client.rs
@@ -1,8 +1,9 @@
 use std::fmt::{Display, Formatter};
+use serde::{Deserialize, Serialize};
 
 use crate::parameter::{parameter_find, ParameterList};
 
-#[derive(Debug)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct Client {
     pub cid: i32,
     pub clid: i32,
@@ -22,19 +23,18 @@ impl From<ParameterList> for Client {
         Client {
             cid: parameter_find(&value, "cid")
                 .unwrap_or_default()
-                .to_i32(-1),
+                .parse::<i32>().unwrap_or(-1),
             clid: parameter_find(&value, "clid")
                 .unwrap_or_default()
-                .to_i32(-1),
+                .parse::<i32>().unwrap_or(-1),
             client_database_id: parameter_find(&value, "client_database_id")
                 .unwrap_or_default()
-                .to_i32(-1),
+                .parse::<i32>().unwrap_or(-1),
             client_nickname: parameter_find(&value, "client_nickname")
-                .unwrap_or_default()
-                .value,
+                .unwrap_or_default(),
             client_type: parameter_find(&value, "channel_topic")
                 .unwrap_or_default()
-                .to_i32(0),
+                .parse::<i32>().unwrap_or(0),
         }
     }
 }
\ No newline at end of file
diff --git a/src/models/event.rs b/src/models/event.rs
index d18f943..1d383c8 100644
--- a/src/models/event.rs
+++ b/src/models/event.rs
@@ -1,7 +1,16 @@
 use std::fmt::{Display, Formatter};
+use std::str::FromStr;
 
-#[derive(Debug)]
-pub enum Event {
+use serde::{Deserialize, Serialize};
+use telnet::Telnet;
+
+use crate::models::{Channel, Client};
+use crate::parameter::{parameter_find, ParameterList};
+use crate::wrappers;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum EventType {
+    Any,
     ChannelList,
     ChannelListFinished,
     NotifyTalkStatusChange,
@@ -16,6 +25,7 @@ pub enum Event {
     NotifyClientChatClosed,
     NotifyClientChatComposing,
     NotifyClientUpdated,
+    NotifyClientChannelGroupChanged,
     NotifyClientIds,
     NotifyClientDBIDFromUid,
     NotifyClientNameFromUid,
@@ -33,51 +43,151 @@ pub enum Event {
     NotifyConnectStatusChange,
 }
 
-impl TryFrom<&str> for Event {
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Event {
+    pub event_type: EventType,
+    pub params: ParameterList,
+    pub channel: Option<Channel>,
+    pub client: Option<Client>,
+}
+
+impl TryFrom<&str> for EventType {
     type Error = String;
 
     //noinspection SpellCheckingInspection
     fn try_from(event_type: &str) -> Result<Self, Self::Error> {
-        match event_type {
-            "channellist" => Ok(Event::ChannelList),
-            "channellistfinished" => Ok(Event::ChannelListFinished),
-            "notifytalkstatuschange" => Ok(Event::NotifyTalkStatusChange),
-            "notifymessage" => Ok(Event::NotifyMessage),
-            "notifymessagelist" => Ok(Event::NotifyMessageList),
-            "notifycomplainlist" => Ok(Event::NotifyComplainList),
-            "notifybanlist" => Ok(Event::NotifyBanList),
-            "notifyclientmoved" => Ok(Event::NotifyClientMoved),
-            "notifyclientleftview" => Ok(Event::NotifyClientLeftView),
-            "notifycliententerview" => Ok(Event::NotifyClientEnterView),
-            "notifyclientpoke" => Ok(Event::NotifyClientPoke),
-            "notifyclientchatclosed" => Ok(Event::NotifyClientChatClosed),
-            "notifyclientchatcomposing" => Ok(Event::NotifyClientChatComposing),
-            "notifyclientupdated" => Ok(Event::NotifyClientUpdated),
-            "notifyclientids" => Ok(Event::NotifyClientIds),
-            "notifyclientdbidfromuid" => Ok(Event::NotifyClientDBIDFromUid),
-            "notifyclientnamefromuid" => Ok(Event::NotifyClientNameFromUid),
-            "notifyclientnamefromdbid" => Ok(Event::NotifyClientNameFromDBID),
-            "notifyclientuidfromclid" => Ok(Event::NotifyClientUidFromClid),
-            "notifyconnectioninfo" => Ok(Event::NotifyConnectionInfo),
-            "notifychannelcreated" => Ok(Event::NotifyChannelCreated),
-            "notifychanneledited" => Ok(Event::NotifyChannelEdited),
-            "notifychanneldeleted" => Ok(Event::NotifyChannelDeleted),
-            "notifychannelmoved" => Ok(Event::NotifyChannelMoved),
-            "notifyserveredited" => Ok(Event::NotifyServerEdited),
-            "notifyserverupdated" => Ok(Event::NotifyServerUpdated),
-            "notifytextmessage" => Ok(Event::NotifyTextMessage),
+        match event_type.to_lowercase().as_str() {
+            "any" => Ok(EventType::Any),
+            "channellist" => Ok(EventType::ChannelList),
+            "channellistfinished" => Ok(EventType::ChannelListFinished),
+            "notifytalkstatuschange" => Ok(EventType::NotifyTalkStatusChange),
+            "notifymessage" => Ok(EventType::NotifyMessage),
+            "notifymessagelist" => Ok(EventType::NotifyMessageList),
+            "notifycomplainlist" => Ok(EventType::NotifyComplainList),
+            "notifybanlist" => Ok(EventType::NotifyBanList),
+            "notifyclientmoved" => Ok(EventType::NotifyClientMoved),
+            "notifyclientleftview" => Ok(EventType::NotifyClientLeftView),
+            "notifycliententerview" => Ok(EventType::NotifyClientEnterView),
+            "notifyclientpoke" => Ok(EventType::NotifyClientPoke),
+            "notifyclientchatclosed" => Ok(EventType::NotifyClientChatClosed),
+            "notifyclientchatcomposing" => Ok(EventType::NotifyClientChatComposing),
+            "notifyclientupdated" => Ok(EventType::NotifyClientUpdated),
+            "notifyclientchannelgroupchanged" => Ok(EventType::NotifyClientChannelGroupChanged),
+            "notifyclientids" => Ok(EventType::NotifyClientIds),
+            "notifyclientdbidfromuid" => Ok(EventType::NotifyClientDBIDFromUid),
+            "notifyclientnamefromuid" => Ok(EventType::NotifyClientNameFromUid),
+            "notifyclientnamefromdbid" => Ok(EventType::NotifyClientNameFromDBID),
+            "notifyclientuidfromclid" => Ok(EventType::NotifyClientUidFromClid),
+            "notifyconnectioninfo" => Ok(EventType::NotifyConnectionInfo),
+            "notifychannelcreated" => Ok(EventType::NotifyChannelCreated),
+            "notifychanneledited" => Ok(EventType::NotifyChannelEdited),
+            "notifychanneldeleted" => Ok(EventType::NotifyChannelDeleted),
+            "notifychannelmoved" => Ok(EventType::NotifyChannelMoved),
+            "notifyserveredited" => Ok(EventType::NotifyServerEdited),
+            "notifyserverupdated" => Ok(EventType::NotifyServerUpdated),
+            "notifytextmessage" => Ok(EventType::NotifyTextMessage),
             "notifycurrentserverconnectionchanged" => {
-                Ok(Event::NotifyCurrentServerConnectionChanged)
+                Ok(EventType::NotifyCurrentServerConnectionChanged)
             }
-            "notifyconnectstatuschange" => Ok(Event::NotifyConnectStatusChange),
-            _ => Err(String::from("Unknown event type.")),
+            "notifyconnectstatuschange" => Ok(EventType::NotifyConnectStatusChange),
+            _ => Err(format!("Unknown event type: {}", event_type)),
         }
     }
 }
 
+impl FromStr for EventType {
+    type Err = String;
 
-impl Display for Event {
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        EventType::try_from(s)
+    }
+}
+
+impl From<&EventType> for String {
+    //noinspection SpellCheckingInspection
+    fn from(value: &EventType) -> Self {
+        match value {
+            EventType::Any => String::from("any"),
+            EventType::ChannelList => String::from("channellist"),
+            EventType::ChannelListFinished => String::from("channellistfinished"),
+            EventType::NotifyTalkStatusChange => String::from("notifytalkstatuschange"),
+            EventType::NotifyMessage => String::from("notifymessage"),
+            EventType::NotifyMessageList => String::from("notifymessagelist"),
+            EventType::NotifyComplainList => String::from("notifycomplainlist"),
+            EventType::NotifyBanList => String::from("notifybanlist"),
+            EventType::NotifyClientMoved => String::from("notifyclientmoved"),
+            EventType::NotifyClientLeftView => String::from("notifyclientleftview"),
+            EventType::NotifyClientEnterView => String::from("notifycliententerview"),
+            EventType::NotifyClientPoke => String::from("notifyclientpoke"),
+            EventType::NotifyClientChatClosed => String::from("notifyclientchatclosed"),
+            EventType::NotifyClientChatComposing => String::from("notifyclientchatcomposing"),
+            EventType::NotifyClientChannelGroupChanged => String::from("notifyclientchannelgroupchanged"),
+            EventType::NotifyClientUpdated => String::from("notifyclientupdated"),
+            EventType::NotifyClientIds => String::from("notifyclientids"),
+            EventType::NotifyClientDBIDFromUid => String::from("notifyclientdbidfromuid"),
+            EventType::NotifyClientNameFromUid => String::from("notifyclientnamefromuid"),
+            EventType::NotifyClientNameFromDBID => String::from("notifyclientnamefromdbid"),
+            EventType::NotifyClientUidFromClid => String::from("notifyclientuidfromclid"),
+            EventType::NotifyConnectionInfo => String::from("notifyconnectioninfo"),
+            EventType::NotifyChannelCreated => String::from("notifychannelcreated"),
+            EventType::NotifyChannelEdited => String::from("notifychanneledited"),
+            EventType::NotifyChannelDeleted => String::from("notifychanneldeleted"),
+            EventType::NotifyChannelMoved => String::from("notifychannelmoved"),
+            EventType::NotifyServerEdited => String::from("notifyserveredited"),
+            EventType::NotifyServerUpdated => String::from("notifyserverupdated"),
+            EventType::NotifyTextMessage => String::from("notifytextmessage"),
+            EventType::NotifyCurrentServerConnectionChanged => {
+                String::from("notifycurrentserverconnectionchanged")
+            }
+            EventType::NotifyConnectStatusChange => String::from("notifyconnectstatuschange"),
+        }
+    }
+}
+
+impl Display for EventType {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{:?}", self)
+        let event_str: String = self.into();
+        write!(f, "{}", event_str)
+    }
+}
+
+impl EventType {
+    pub fn get_channel(&self, connection: &mut Telnet, params: &ParameterList) -> Option<Channel> {
+        match self {
+            EventType::NotifyClientMoved => {
+                let id = parameter_find(params, "ctid")?
+                    .parse::<i32>().unwrap_or(0);
+                wrappers::find_channel(connection, "cid", &id.to_string(), true).unwrap().or(None)
+            }
+            _ => None,
+        }
+    }
+
+    pub fn get_client(&self, connection: &mut Telnet, params: &ParameterList) -> Option<Client> {
+        match self {
+            EventType::NotifyClientMoved => {
+                let id = parameter_find(params, "clid")?
+                    .parse::<i32>().unwrap_or(0);
+                wrappers::find_client(connection, "clid", &id.to_string(), true).unwrap().or(None)
+            }
+            EventType::NotifyClientEnterView => {
+                Some(Client::from(params.clone()))
+            }
+            _ => None,
+        }
+    }
+}
+
+impl Event {
+    pub fn new(connection: &mut Telnet, event_type: EventType, params: ParameterList) -> Event {
+        let channel = event_type.get_channel(connection, &params);
+        let client = event_type.get_client(connection, &params);
+
+        Event {
+            event_type,
+            params,
+            channel,
+            client,
+        }
     }
 }
\ No newline at end of file
diff --git a/src/models/mod.rs b/src/models/mod.rs
index a37ebaf..6d919ab 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -4,4 +4,4 @@ mod event;
 
 pub use channel::Channel;
 pub use client::Client;
-pub use event::Event;
\ No newline at end of file
+pub use event::{EventType, Event};
\ No newline at end of file
diff --git a/src/parameter.rs b/src/parameter.rs
index 2c7623e..6424ecb 100644
--- a/src/parameter.rs
+++ b/src/parameter.rs
@@ -1,36 +1,38 @@
-use std::fmt::{Debug, Display, Formatter};
+use std::collections::HashMap;
 use crate::utils::{decode_value, encode_value};
 
-#[derive(Clone)]
-pub struct Parameter {
-    pub name: String,
-    pub value: String,
+pub type ParameterList = HashMap<String, String>;
+pub type Parameter = (String, String);
+
+pub fn parameter_find(params: &ParameterList, name: &str) -> Option<String> {
+    params.get(name).map(|value| value.clone())
 }
 
-pub type ParameterList = Vec<Parameter>;
-
-pub fn parameter_find(params: &Vec<Parameter>, name: &str) -> Option<Parameter> {
-    for param in params {
-        if param.name == name {
-            return Some(param.clone());
+pub fn parameter_list_has(params: &ParameterList, key: &str, value: &str, strict: bool) -> bool {
+    if let Some(check_value) = params.get(key) {
+        if check_value == value && strict {
+            return true;
+        }
+        if check_value.contains(value) && !strict {
+            return true;
         }
     }
-    None
+    return false;
 }
 
-pub fn parameter_list_find(param_lists: &Vec<ParameterList>, name: &str, value: &str, strict: bool) -> Option<ParameterList> {
+pub fn parameter_list_find(param_lists: &Vec<ParameterList>, key: &str, value: &str, strict: bool) -> Option<ParameterList> {
     for params in param_lists {
-        if params.iter().any(|param| param.is(name, value, strict)) {
+        if parameter_list_has(params, key, value, strict) {
             return Some(params.clone());
         }
     }
     None
 }
 
-pub fn parameter_list_find_all(param_lists: &Vec<ParameterList>, name: &str, value: &str, strict: bool) -> Vec<ParameterList> {
+pub fn parameter_list_find_all(param_lists: &Vec<ParameterList>, key: &str, value: &str, strict: bool) -> Vec<ParameterList> {
     let mut found = Vec::new();
     for params in param_lists {
-        if params.iter().any(|param| param.is(name, value, strict)) {
+        if parameter_list_has(params, key, value, strict) {
             found.push(params.clone())
         }
     }
@@ -40,82 +42,31 @@ pub fn parameter_list_find_all(param_lists: &Vec<ParameterList>, name: &str, val
 pub fn parameter_parse(params_str: &str) -> ParameterList {
     let parts: Vec<&str> = params_str.split(' ').collect();
 
-    let mut response_params = Vec::new();
+    let mut response_params = ParameterList::new();
     parts.iter().for_each(|part| {
-        response_params.push(Parameter::from(part.split_once('=').unwrap()));
+        let (key, value) = part.split_once('=').unwrap_or((part, "1"));
+        response_params.insert(key.to_string(), decode_value(value));
     });
 
     response_params
 }
 
-impl From<(&str, &str)> for Parameter {
-    fn from(param: (&str, &str)) -> Parameter {
-        Parameter::new(param.0, param.1)
-    }
+pub fn parameter_to_string(param: Parameter) -> String {
+    format!("{}={}", param.0, encode_value(&param.1))
 }
 
-impl Parameter {
-    pub fn new(name: &str, value: &str) -> Parameter {
-        Parameter {
-            name: String::from(name),
-            value: decode_value(value)
-        }
-    }
-
-    pub fn is(&self, name: &str, value: &str, strict: bool) -> bool {
-        if self.name != name {
-            return false;
-        }
-        if self.value != value && strict {
-            return false;
-        }
-        if !self.value.contains(value) && !strict {
-            return false;
-        }
-        true
-    }
-
-    pub fn to_i32(&self, default: i32) -> i32 {
-        self.value.parse::<i32>().unwrap_or(default)
-    }
-
-    pub fn list_to_string(parameter_list: ParameterList) -> String {
-        parameter_list
-            .into_iter()
-            .map(String::from)
-            .collect::<Vec<String>>()
-            .join(" ")
-    }
-
-    pub fn list_to_string_sep(parameter_list: ParameterList, sep: &str) -> String {
-        parameter_list
-            .into_iter()
-            .map(String::from)
-            .collect::<Vec<String>>()
-            .join(sep)
-    }
+pub fn parameter_list_to_string(parameter_list: ParameterList, sep: &str) -> String {
+    parameter_list
+        .into_iter()
+        .map(|param| parameter_to_string(param))
+        .collect::<Vec<String>>()
+        .join(sep)
 }
 
-impl Display for Parameter {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}={}", self.name, self.value)
-    }
-}
-
-impl Debug for Parameter {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}={}", self.name, self.value)
-    }
-}
-
-impl Default for Parameter {
-    fn default() -> Self {
-        Parameter::new("", "")
-    }
-}
-
-impl From<Parameter> for String {
-    fn from(value: Parameter) -> Self {
-        format!("{}={}", value.name, encode_value(&value.value))
-    }
+pub fn parameters_to_string(parameters: Vec<Parameter>, sep: &str) -> String {
+    parameters
+        .into_iter()
+        .map(|param| parameter_to_string(param))
+        .collect::<Vec<String>>()
+        .join(sep)
 }
\ No newline at end of file
diff --git a/src/response.rs b/src/response.rs
index 261ff24..aed60d2 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -1,5 +1,5 @@
 use std::fmt::{Debug, Display, Formatter};
-use crate::models::Event;
+use crate::models::EventType;
 
 use crate::parameter::*;
 
@@ -7,12 +7,12 @@ pub enum Response {
     Ok,
     Data(ParameterList),
     DataList(Vec<ParameterList>),
-    Event(Event, ParameterList),
+    Event(EventType, ParameterList),
 }
 
 pub enum ResponseType {
     Error,
-    Event(Event),
+    Event(EventType),
     Data,
     Unknown,
 }
@@ -41,7 +41,7 @@ fn get_response_type(type_str: &str) -> Result<ResponseType, String> {
         return Ok(ResponseType::Error);
     }
     if type_str.starts_with("notify") || type_str == "channellist" || type_str == "channellistfinished" {
-        let event_type = Event::try_from(type_str)?;
+        let event_type = EventType::try_from(type_str)?;
         return Ok(ResponseType::Event(event_type));
     }
 
@@ -120,16 +120,16 @@ impl Display for Response {
                 write!(f, "Ok")
             }
             Response::Data(params) => {
-                for param in params {
-                    write!(f, "{};", param)?;
+                for param in params.clone() {
+                    write!(f, "{};", parameter_to_string(param))?;
                 }
                 Ok(())
             }
             Response::DataList(params_list) => {
                 for params in params_list {
                     write!(f, "[")?;
-                    for param in params {
-                        write!(f, "{};", param)?;
+                    for param in params.clone() {
+                        write!(f, "{};", parameter_to_string(param))?;
                     }
                     write!(f, "]")?;
                 }
@@ -137,8 +137,8 @@ impl Display for Response {
             }
             Response::Event(event, params) => {
                 write!(f, "Event: {}", event)?;
-                for param in params {
-                    write!(f, "{};", param)?;
+                for param in params.clone() {
+                    write!(f, "{};", parameter_to_string(param))?;
                 }
                 Ok(())
             }
@@ -151,14 +151,13 @@ impl ResponseError {
         self.id == 0
     }
 
-    fn create_error(params: &Vec<Parameter>) -> ResponseError {
+    fn create_error(params: &ParameterList) -> ResponseError {
         ResponseError {
             id: parameter_find(params, "id")
-                .unwrap_or_else(|| Parameter::new("id", "-1"))
-                .to_i32(-1),
+                .unwrap_or_else(|| String::from("-1"))
+                .parse::<i32>().unwrap_or(-1),
             msg: parameter_find(params, "msg")
-                .unwrap_or_else(|| Parameter::new("msg", "Unknown error."))
-                .value,
+                .unwrap_or_else(|| String::from("Unknown error."))
         }
     }
 }
\ No newline at end of file
diff --git a/src/utils.rs b/src/utils.rs
index 766eb23..e21547f 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,5 +1,5 @@
 use crate::models::Client;
-use crate::parameter::Parameter;
+use crate::parameter::parameters_to_string;
 
 pub fn decode_value(value: &str) -> String {
     value
@@ -32,9 +32,9 @@ impl From<SendTextMessageTarget> for String {
             _ => String::from("0"),
         };
 
-        Parameter::list_to_string(vec![
-            Parameter::new("targetmode", target_mode),
-            Parameter::new("target", &target)
-        ])
+        parameters_to_string(vec![
+            (String::from("targetmode"), String::from(target_mode)),
+            (String::from("target"), target)
+        ], " ")
     }
 }
\ No newline at end of file
diff --git a/src/wrappers.rs b/src/wrappers.rs
index 0effadc..9c50680 100644
--- a/src/wrappers.rs
+++ b/src/wrappers.rs
@@ -57,10 +57,10 @@ pub fn get_channels(connection: &mut Telnet, spacers: bool) -> Result<Vec<Channe
     }
 }
 
-pub fn find_channel(connection: &mut Telnet, name: &str, strict: bool) -> Result<Option<Channel>, String> {
+pub fn find_channel(connection: &mut Telnet, key: &str, value: &str, strict: bool) -> Result<Option<Channel>, String> {
     match commands::channellist(connection)? {
         Response::DataList(parameter_lists) => {
-            match parameter::parameter_list_find(&parameter_lists, "channel_name", name, strict) {
+            match parameter::parameter_list_find(&parameter_lists, key, value, strict) {
                 Some(params) => {
                     Ok(Some(Channel::from(params)))
                 }
@@ -88,10 +88,10 @@ pub fn get_clients(connection: &mut Telnet) -> Result<Vec<Client>, String> {
     }
 }
 
-pub fn find_client(connection: &mut Telnet, name: &str, strict: bool) -> Result<Option<Client>, String> {
+pub fn find_client(connection: &mut Telnet, key: &str, value: &str, strict: bool) -> Result<Option<Client>, String> {
     match commands::clientlist(connection)? {
         Response::DataList(parameter_lists) => {
-            match parameter::parameter_list_find(&parameter_lists, "client_nickname", name, strict) {
+            match parameter::parameter_list_find(&parameter_lists, key, value, strict) {
                 Some(params) => {
                     Ok(Some(Client::from(params)))
                 }
@@ -122,7 +122,7 @@ fn get_self_clid(connection: &mut Telnet) -> Result<String, String> {
         Response::Data(params) => {
             match parameter::parameter_find(&params, "clid") {
                 None => Err(String::from("Could not find clid in models from Teamspeak.")),
-                Some(param) => Ok(param.value)
+                Some(param) => Ok(param)
             }
         }
         _ => Err(String::from("Received unexpected models from Teamspeak for whoami."))
diff --git a/ts-control b/ts-control
index 3ce6769..5f26108 100755
--- a/ts-control
+++ b/ts-control
@@ -8,7 +8,8 @@ away
 not away
 back
 message
-message-user"
+message-user
+events-move"
 
 _ts_control_get_entity() {
 	entity=$(_ts_control_single_or_dmenu "$(teamspeak-query-lib "$1s")" "$2")
@@ -97,4 +98,8 @@ case $action in
 		message=$(_ts_control_get_message "$3")
 		teamspeak-query-lib message --strict-client --client "$user" "$message"
 		;;
+	"events-move")
+		teamspeak-query-lib events NotifyClientMoved NotifyClientEnterView \
+			| jq -r --unbuffered '.client.client_nickname + " joined " + .channel.channel_name // "the server"' \
+			| xargs -I{} notify-send "TS3 movement" "{}"
 esac