···11[package]
22name = "fluxer-rs"
33-version = "0.1.2"
33+version = "0.1.3"
44edition = "2024"
55license = "MIT"
66-description = "A rust implementation of the fluxer api to be used on fluxer.app and other self hosted instances"
66+description = "A rust framework for interacting with fluxer instances"
77homepage = "https://git.killuaa.dev/roufpup/fluxer-rs"
88repository = "https://git.killuaa.dev/roufpup/fluxer-rs"
99readme = "README.md"
10101111[workspace]
1212-members = [
1313- "examples/examplebot",
1414-]
1212+members = ["examples/examplebot", "macros"]
15131614[dependencies]
1515+anyhow = "1.0.102"
1716async-trait = "0.1.89"
1817derive_builder = "0.20.2"
1919-image = "0.25.9"
2018log = "0.4.29"
2121-minreq = { version = "2.14.1", features = ["https"] }
2219serde_json = "1.0.149"
2320serde_with = "3.16.1"
2121+macros = { path = "./macros" }
2222+thiserror = "2.0.18"
2323+reqwest = "0.13.2"
2424+emojis = "0.8.0"
24252526[dependencies.ezsockets]
2627version = "0.7.1"
+86-70
README.md
···11# About
22-A rust implementation of the fluxer api to be used on fluxer.app and other self hosted instances
22+A rust framework for interacting with fluxer instances
33+44+!!! Expect breaking changes !!!
3546For opening issues and feature requests please head over to the MIRROR repo over on https://github.com/roufpup/fluxer-rs
57···1012# Example usage
11131214```rs
1515+// For more verbose messages like from the https client choose debug instead of info
1616+env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
13171414- let bot: FluxerBot = FluxerBot::init(
1515- "<Your bot token here>".to_string(),
1616- "wss://gateway.fluxer.app?v=1&encoding=json&compress=none".to_string(),
1717- "https://api.fluxer.app/v1".to_string(),
1818- )
1919- .await;
1818+let bot = FluxerBot::init(
1919+ "<your bot token here>",
2020+ "wss://gateway.fluxer.app?v=1&encoding=json&compress=none",
2121+ "https://api.fluxer.app/v1",
2222+)?;
20232121- let bot_arc = Arc::new(bot);
2222- bot_arc
2323- .start(ColorbotDispatchHandler {
2424- bot: bot_arc.clone(),
2525- })
2626- .await;
2424+bot.start(ColorbotDispatchHandler {}).await;
2725```
2828-We pass an Arc of the bot to the dispatch handler implementation so we can use the api of our above created bot inside different dispatch callbacks as shown below
2626+Each client using this crate must create their own dispatch handler by implementing the trait `DispatchHandlerTrait`. Once you do you can override the default functions of the dispatch handler that will be called for each dispatch event. The default functions are generated with a macro, and the name of each function follows this pattern: "handle_{}_dispatch" where {} is the name of the dispatch event in snake case. So the `MESSAGE_CREATE` dispatch event would have a function by the name of `handle_message_create_dispatch`
29273028```rs
3131-3232-pub struct ColorbotDispatchHandler {
3333- pub bot: Arc<FluxerBot>,
3434-}
2929+pub struct ColorbotDispatchHandler {}
35303631impl DispatchHandlerTrait for ColorbotDispatchHandler {
3737- async fn handle_message_create_dispatch(&self, data: MessageEventData) {
3232+ async fn handle_message_create_dispatch(
3333+ &self,
3434+ data: MessageData,
3535+ api: &FluxerApiHandler,
3636+ ) -> Result<(), FluxerRsError> {
3837 let mut cmd_handler = CommandHandler::init("!".to_string());
39384040- cmd_handler.register_command(
4141- "ping".to_string(),
4242- PingCommand {
4343- bot: self.bot.clone(),
4444- channel_id: data.channel_id.clone(),
4545- id: data.id.clone(),
4646- },
4747- );
3939+ register_commands!(cmd_handler,[
4040+ {"addrole", AddRoleCommand},
4141+ {"ping", PingCommand},
4242+ {"edit", EditCommand},
4343+ {"react", ReactCommand},
4444+ {"removereact", RemoveReactCommand},
4545+ {"removerole", RemoveRoleCommand},
4646+ {"createrole", CreateRoleCommand},
4747+ {"deleterole", DeleteRoleCommand},
4848+ ]);
48494949- cmd_handler.handle(&data).await;
5050+ cmd_handler.handle(&data, api).await
5051 }
5252+}
5153```
5252-This is also another thing to touch on how to reply to different dispatch events. All you need to do is implement the DispatchHandlerTrait for your struct and implement the functions that you want to override instead of letting the crate use the predefined default ones.
5353-5454-This crate also will provide some high level implementations bots use in general, currently there only is a CommandHandler which will let you easily register custom commands for your bots.
5454+This crate also will provide some high level implementations bots use in general, currently there only is a CommandHandler which will let you easily register custom commands for your bots as shown partially in the above example.
55555656Here is an example of how to make a command for the CommandHandler
57575858```rs
5959-pub struct PingCommand {
6060- pub bot: Arc<FluxerBot>,
6161- pub channel_id: String,
6262- pub id: String,
6363-}
5959+use fluxer_rs::{
6060+ api::{common::send_reply},
6161+ command,
6262+};
64636565-impl CommandTrait for PingCommand {
6666- async fn execute(&self) {
6767- let _ = self.bot.api.execute_call(
6868- SendMessageBuilder::default()
6969- .channel_id(self.channel_id.clone())
7070- .content("pong".to_string())
7171- .message_reference(
7272- MessageReferenceBuilder::default()
7373- .message_id(self.id.clone())
7474- .build()
7575- .unwrap(),
7676- )
7777- .build()
7878- .unwrap(),
7979- );
8080- }
6464+#[command(PingCommand)]
6565+async fn execute(api: &FluxerApiHandler, feedback: &CommandFeedback) {
6666+ let data = feedback.data;
6767+6868+ send_reply(api, &data.channel_id, &data.id, "pong").await?;
6969+ Ok(())
8170}
8271```
8383-When creating a command we pass all the required data into the command struct and just implement the CommandTrait for it. In this example the execute function will simply send a message back to the user by replying to their original message with `pong`
7272+When creating a command, all you have to do is use the command macro and pass the command name which you later register to your dispatch handler. The signiture of the function must be the same as you see it in the example, an async function that takes two references, an api handler and a command feedback.
7373+7474+This crate also implements some basic functions that would be used a lot of the time which can be found under `fluxer_rs::api::common`, so you won't have to build an api call every time.
84758576Of course you don't have to use this and are free to make your own better custom handler for commands :p
86778778# Dispatch events
8888-Not all dispatch events are implemented currently as i am limited on time for how much i can do in such a short period of time, but i will be implementing them constantly as much as possible as fast as possible.
7979+Not all op codes are implemented currently, and not all dispatch events have been registered yet. The reason for the latter is that i choose for the most part to implement data structure myself instead of generating it from a spec. So whenever an unknown dispatch event hits it would print the json that was attempted to be sent, then i implement the structure from it. Which leads to the next paragraph.
89809090-When developing a bot in debug mode, be wary as there is a certain panic function that will occur when an unimplemented dispatch event occurs. It's there by design for my own ease of use ( So i have no choice but to implement the event if i don't want constant crashing ;3). While it's not optimal i do advise to use release mode for now while still a bunch of dispatch events are missing.
8181+When developing a bot in debug mode, be wary as there is a certain panic that will occur when an unimplemented dispatch event occurs. It's there by design for my own ease of use ( So i have no choice but to implement the event if i don't want constant crashing ;3). While it's not optimal i do advise to use release mode for now while still a bunch of dispatch events are missing as it will only send an error log in the terminal for said dispatch event.
91829283# API
9384As far as API calls go there are very few implemented at the moment but are quite useful ones like:
···10495Whenever an api call is missing but you really want to use it be not afraid you can also implement an API call on your own in the meanwhile until it gets added. Here is an example:
1059610697```rs
107107-#[derive(Clone, Builder)]
9898+#[derive(Clone, Debug, Builder)]
10899#[builder(try_setter, setter(into))]
109100pub struct EditMessage {
110110- // Path params
111101 pub channel_id: String,
112102 pub message_id: String,
113103114104 pub content: String,
115105 #[builder(default)]
116106 pub embeds: Option<Vec<Embed>>,
117117- #[builder(default)]
118118- pub message_reference: Option<MessageReference>,
119107}
120108109109+impl ApiCall for EditMessage {
110110+ type ReturnType = MessageData;
121111122122-impl ApiCall for EditMessage {
123123- fn get_req(&self, req: minreq::Request, token: String) -> minreq::Request {
124124- let body = serde_json::to_string(self).unwrap();
125125- info!("BODY CHECK {body}");
126126- req.with_body(body)
127127- .with_header("Authorization", format!("Bot {token}"))
112112+ fn get_req(
113113+ &self,
114114+ req: reqwest::RequestBuilder,
115115+ token: &str,
116116+ ) -> Result<reqwest::RequestBuilder, FluxerRsError> {
117117+ let value = serde_json::to_string(self)?;
118118+119119+ Ok(req
120120+ .body(value)
121121+ .header("Authorization", format!("Bot {token}")))
128122 }
129123130124 fn get_info(&self) -> (String, FluxerApiCallType) {
···133127 FluxerApiCallType::Patch,
134128 )
135129 }
130130+131131+ fn get_data(&self, body: &str) -> Result<Self::ReturnType, FluxerRsError> {
132132+ let value = serde_json::from_str::<MessageData>(body)?;
133133+ Ok(value)
134134+ }
136135}
137136138137impl Serialize for EditMessage {
···145144 if let Some(embed) = self.embeds.clone() {
146145 state.serialize_entry("embeds", &embed)?;
147146 };
148148- if let Some(message_reference) = self.message_reference.clone() {
149149- state.serialize_entry("message_reference", &message_reference)?;
150150- }
151147 state.end()
152148 }
153149}
154150```
155151What you basically need to do is implement the ApiCall trait for the struct that will serve as your storage for parameters and potentially body data if required.
156152157157-In this example taken right out of the crate you implement two functions for the struct, `get_req` and `get_info`, in the first one you construct the request with all the headers and body you need and return it. While in the second you set some information for the api call like the path that will get called which will include also the path parameters taken out of the struct as well as the http request method type.
153153+In this example taken right out of the crate you implement three functions for the struct, `get_req`, `get_info` and `get_data`, in the first one you construct the request with all the headers and body you need and return it. In the second you set some information for the api call like the path that will get called which will include also the path parameters taken out of the struct, and the http request method type. In the last function you return the data type that the api call would usually return. In a lot of cases that would be nothing then you just need to set `type ReturnType = ();` in your implementation. But in this case the api will return `MessageData`, so we deserialize the body and return it so it can be given to the client using the crate.
158154159155I've added a bit of custom serialization to make everything a bit easier when using a struct that shares both the serializable data and the path parameter data
160156161157If you'd like to implement an api call exactly like this in your own app you will have to add the `derive_builder` crate as a dependency as that is what provides the builder macros and generation.
162158163163-After you have implemented the api call you simply call it like in the example above for the PingCommand, the name of the builder is the same as the struct in this case it will be `EditMessageBuilder`159159+After you have implemented the api call you simply call it like the common api functions do (see example below), the name of the builder is the same as the struct in this case it will be `EditMessageBuilder`
160160+161161+```rs
162162+pub async fn edit_message(
163163+ api: &FluxerApiHandler,
164164+ channel_id: &str,
165165+ message_id: &str,
166166+ content: &str,
167167+) -> Result<MessageData, FluxerRsError> {
168168+ let call = EditMessageBuilder::default()
169169+ .channel_id(channel_id)
170170+ .message_id(message_id)
171171+ .content(content)
172172+ .build()
173173+ .map_err(ApiHandlerError::from)?;
174174+175175+ let result = api.execute_call(call).await?;
176176+177177+ Ok(result)
178178+}
179179+```
···11-pub mod gateway;
22-pub mod fluxerbot;
31pub mod api;
22+pub mod error;
33+pub mod fluxerbot;
44+pub mod gateway;
45pub mod high_level;
55-pub use crate::api::data_structure::embed::*;66+pub mod serde;
77+pub mod util;
88+99+pub use macros::command;
1010+pub use macros::register_commands;
+58
src/serde/gateway.rs
···11+#[cfg(not(debug_assertions))]
22+use log::error;
33+44+use anyhow::Result;
55+use serde::ser::SerializeMap;
66+use serde::{Deserialize, Serialize, de};
77+use serde_json::Value;
88+99+use crate::gateway::dispatch::dispatch_deserialize;
1010+use crate::serde::types::gateway::{OP10D, ReceiveData, ReceiveDataType, SendData, SendDataType};
1111+1212+// TODO: make impl Serialize implementation consistent across all places
1313+impl Serialize for SendData {
1414+ fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1515+ let mut serializer_map = serializer.serialize_map(None)?;
1616+1717+ serializer_map.serialize_entry("op", &self.op)?;
1818+1919+ match &self.d {
2020+ SendDataType::OP1(op1_d) => serializer_map.serialize_entry("d", &op1_d)?,
2121+ SendDataType::OP2(op2_d) => serializer_map.serialize_entry("d", &op2_d)?,
2222+ }
2323+2424+ serializer_map.end()
2525+ }
2626+}
2727+2828+impl<'de> Deserialize<'de> for ReceiveData {
2929+ fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3030+ let value = Value::deserialize(deserializer)?;
3131+ let op = value["op"]
3232+ .as_u64()
3333+ .ok_or_else(|| de::Error::missing_field("op"))? as u8;
3434+3535+ let d = match op {
3636+ 0 => ReceiveDataType::OP0(Box::new(
3737+ dispatch_deserialize(&value).map_err(de::Error::custom)?,
3838+ )),
3939+ 1 => {
4040+ let inner: Option<u32> =
4141+ Option::deserialize(&value["d"]).map_err(de::Error::custom)?;
4242+ ReceiveDataType::OP1(inner)
4343+ }
4444+ 9 => {
4545+ let inner = bool::deserialize(&value["d"]).map_err(de::Error::custom)?;
4646+ ReceiveDataType::OP9(inner)
4747+ }
4848+ 10 => {
4949+ let inner = OP10D::deserialize(&value["d"]).map_err(de::Error::custom)?;
5050+ ReceiveDataType::OP10(inner)
5151+ }
5252+ 11 => ReceiveDataType::OP11,
5353+ _ => return Err(de::Error::custom(format!("unknown op: {}", op))),
5454+ };
5555+5656+ Ok(ReceiveData { d, op })
5757+ }
5858+}