use crate::models::{HasId, Pager, Snowflake, Timestamp};
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
fn is_zero_u32(value: &u32) -> bool {
*value == 0
}
fn is_botgo_space(c: char) -> bool {
matches!(c, ' ' | '\u{00a0}')
}
fn remove_botgo_at_mentions(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let mut rest = input;
while let Some(start) = rest.find("<@!") {
output.push_str(&rest[..start]);
let after_marker = &rest[start + 3..];
let digit_len = after_marker
.chars()
.take_while(|c| c.is_ascii_digit())
.map(char::len_utf8)
.sum::<usize>();
if digit_len > 0 && after_marker[digit_len..].starts_with('>') {
rest = &after_marker[digit_len + 1..];
} else {
output.push_str("<@!");
rest = after_marker;
}
}
output.push_str(rest);
output
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CMD {
pub cmd: String,
pub content: String,
}
pub fn mention_user(user_id: impl std::fmt::Display) -> String {
format!("<@{user_id}>")
}
#[allow(non_snake_case)]
pub fn MentionUser(user_id: impl std::fmt::Display) -> String {
mention_user(user_id)
}
pub fn mention_all_user() -> &'static str {
"@everyone"
}
#[allow(non_snake_case)]
pub fn MentionAllUser() -> &'static str {
mention_all_user()
}
pub fn mention_channel(channel_id: impl std::fmt::Display) -> String {
format!("<#{channel_id}>")
}
#[allow(non_snake_case)]
pub fn MentionChannel(channel_id: impl std::fmt::Display) -> String {
mention_channel(channel_id)
}
pub fn emoji(id: impl std::fmt::Display) -> String {
format!("<emoji:{id}>")
}
#[allow(non_snake_case)]
pub fn Emoji(id: impl std::fmt::Display) -> String {
emoji(id)
}
#[allow(non_snake_case)]
pub fn ETLInput(input: &str) -> String {
remove_botgo_at_mentions(input)
.trim_matches(is_botgo_space)
.to_string()
}
pub fn parse_command(input: &str) -> CMD {
let cleaned = ETLInput(input);
match cleaned.split_once(' ') {
Some((cmd, content)) => CMD {
cmd: cmd.trim_matches(is_botgo_space).to_string(),
content: content.to_string(),
},
None => CMD {
cmd: cleaned.trim_matches(is_botgo_space).to_string(),
content: String::new(),
},
}
}
#[allow(non_snake_case)]
pub fn ParseCommand(input: &str) -> CMD {
parse_command(input)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MessageScene {
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_data: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageDelete {
pub message: Message,
pub op_user: crate::models::User,
#[serde(skip)]
pub event_id: Option<String>,
}
impl MessageDelete {
pub fn from_data(api: crate::api::BotApi, event_id: String, data: serde_json::Value) -> Self {
let message_data = data.get("message").cloned().unwrap_or_else(|| data.clone());
let op_user = data
.get("op_user")
.cloned()
.map(crate::models::User::from_data)
.unwrap_or_default();
Self {
message: Message::from_data(api, event_id.clone(), message_data),
op_user,
event_id: Some(event_id),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub id: Option<Snowflake>,
pub content: Option<String>,
pub channel_id: Option<Snowflake>,
pub guild_id: Option<Snowflake>,
pub group_id: Option<Snowflake>,
pub author: Option<MessageUser>,
pub member: Option<MessageMember>,
pub message_reference: Option<MessageReference>,
#[serde(default)]
pub mentions: Vec<MessageUser>,
#[serde(default)]
pub attachments: Vec<MessageAttachment>,
#[serde(default)]
pub embeds: Vec<Embed>,
pub ark: Option<Ark>,
pub direct_message: Option<bool>,
pub seq: Option<u64>,
pub seq_in_channel: Option<String>,
pub timestamp: Option<Timestamp>,
pub edited_timestamp: Option<Timestamp>,
pub mention_everyone: Option<bool>,
pub src_guild_id: Option<Snowflake>,
pub file_info: Option<String>,
pub ttl: Option<u32>,
pub message_scene: Option<MessageScene>,
pub event_id: Option<String>,
}
impl Message {
pub fn new() -> Self {
Self {
id: None,
content: None,
channel_id: None,
guild_id: None,
group_id: None,
author: None,
member: None,
message_reference: None,
mentions: Vec::new(),
attachments: Vec::new(),
embeds: Vec::new(),
ark: None,
direct_message: None,
seq: None,
seq_in_channel: None,
timestamp: None,
edited_timestamp: None,
mention_everyone: None,
src_guild_id: None,
file_info: None,
ttl: None,
message_scene: None,
event_id: None,
}
}
pub fn from_data(_api: crate::api::BotApi, event_id: String, data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
content: data
.get("content")
.and_then(|v| v.as_str())
.map(String::from),
channel_id: data
.get("channel_id")
.and_then(|v| v.as_str())
.map(String::from),
guild_id: data
.get("guild_id")
.and_then(|v| v.as_str())
.map(String::from),
group_id: data
.get("group_id")
.and_then(|v| v.as_str())
.map(String::from),
author: data
.get("author")
.map(|v| MessageUser::from_data(v.clone())),
member: data.get("member").map(|v| MessageMember {
nick: v.get("nick").and_then(|n| n.as_str()).map(String::from),
roles: v.get("roles").and_then(|r| r.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect()
}),
joined_at: v
.get("joined_at")
.and_then(|j| j.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
}),
message_reference: data
.get("message_reference")
.map(|v| MessageReference::from_data(v.clone())),
mentions: data
.get("mentions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| MessageUser::from_data(v.clone()))
.collect()
})
.unwrap_or_default(),
attachments: data
.get("attachments")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| MessageAttachment::from_data(v.clone()))
.collect()
})
.unwrap_or_default(),
embeds: data
.get("embeds")
.cloned()
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default(),
ark: data
.get("ark")
.cloned()
.and_then(|v| serde_json::from_value(v).ok()),
direct_message: data.get("direct_message").and_then(|v| v.as_bool()),
seq: data.get("seq").and_then(|v| v.as_u64()),
seq_in_channel: data
.get("seq_in_channel")
.and_then(|v| v.as_str())
.map(String::from),
timestamp: data
.get("timestamp")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
edited_timestamp: data
.get("edited_timestamp")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
mention_everyone: data.get("mention_everyone").and_then(|v| v.as_bool()),
src_guild_id: data
.get("src_guild_id")
.and_then(|v| v.as_str())
.map(String::from),
file_info: data
.get("file_info")
.and_then(|v| v.as_str())
.map(String::from),
ttl: data.get("ttl").and_then(|v| v.as_u64()).map(|v| v as u32),
message_scene: data
.get("message_scene")
.cloned()
.and_then(|v| serde_json::from_value(v).ok()),
event_id: Some(event_id),
}
}
pub async fn reply(
&self,
api: &crate::api::BotApi,
token: &crate::token::Token,
content: &str,
) -> Result<crate::models::api::MessageResponse, crate::error::BotError> {
if let (Some(channel_id), Some(msg_id)) = (&self.channel_id, &self.id) {
let params = MessageParams {
content: Some(content.to_string()),
msg_id: Some(msg_id.clone()),
event_id: self.event_id.clone(),
..Default::default()
};
api.post_message_with_params(token, channel_id, params)
.await
} else {
Err(crate::error::BotError::InvalidData(
"Missing channel_id or message_id for reply".to_string(),
))
}
}
pub fn has_content(&self) -> bool {
self.content.as_ref().is_some_and(|c| !c.is_empty())
}
pub fn has_attachments(&self) -> bool {
!self.attachments.is_empty()
}
pub fn has_mentions(&self) -> bool {
!self.mentions.is_empty()
}
pub fn is_from_bot(&self) -> bool {
self.author.as_ref().is_some_and(|a| a.bot.unwrap_or(false))
}
}
impl Default for Message {
fn default() -> Self {
Self::new()
}
}
impl HasId for Message {
fn id(&self) -> Option<&Snowflake> {
self.id.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DirectMessage {
pub guild_id: Option<Snowflake>,
pub channel_id: Option<Snowflake>,
pub create_time: Option<String>,
}
pub type DirectMessageSession = DirectMessage;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DirectMessageToCreate {
pub source_guild_id: String,
pub recipient_id: String,
}
impl DirectMessageToCreate {
pub fn new(source_guild_id: impl Into<String>, recipient_id: impl Into<String>) -> Self {
Self {
source_guild_id: source_guild_id.into(),
recipient_id: recipient_id.into(),
}
}
}
impl DirectMessage {
pub fn new() -> Self {
Self::default()
}
pub fn from_data(data: serde_json::Value) -> Self {
serde_json::from_value(data).unwrap_or_default()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GroupMessage {
pub id: Option<Snowflake>,
pub content: Option<String>,
pub message_reference: Option<MessageReference>,
#[serde(default)]
pub mentions: Vec<GroupMessageUser>,
#[serde(default)]
pub attachments: Vec<MessageAttachment>,
pub msg_seq: Option<u64>,
pub timestamp: Option<Timestamp>,
pub author: Option<GroupMessageUser>,
pub group_openid: Option<String>,
#[serde(skip)]
pub event_id: Option<String>,
}
impl GroupMessage {
pub fn new() -> Self {
Self {
id: None,
content: None,
message_reference: None,
mentions: Vec::new(),
attachments: Vec::new(),
msg_seq: None,
timestamp: None,
author: None,
group_openid: None,
event_id: None,
}
}
pub fn from_data(_api: crate::api::BotApi, event_id: String, data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
content: data
.get("content")
.and_then(|v| v.as_str())
.map(String::from),
message_reference: data
.get("message_reference")
.map(|v| MessageReference::from_data(v.clone())),
mentions: data
.get("mentions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| GroupMessageUser::from_data(v.clone()))
.collect()
})
.unwrap_or_default(),
attachments: data
.get("attachments")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| MessageAttachment::from_data(v.clone()))
.collect()
})
.unwrap_or_default(),
msg_seq: data.get("msg_seq").and_then(|v| v.as_u64()),
timestamp: data
.get("timestamp")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
author: data
.get("author")
.map(|v| GroupMessageUser::from_data(v.clone())),
group_openid: data
.get("group_openid")
.and_then(|v| v.as_str())
.map(String::from),
event_id: Some(event_id),
}
}
pub async fn reply(
&self,
api: &crate::api::BotApi,
token: &crate::token::Token,
content: &str,
) -> Result<crate::models::api::MessageResponse, crate::error::BotError> {
if let (Some(group_openid), Some(msg_id)) = (&self.group_openid, &self.id) {
let params = GroupMessageParams {
msg_type: 0,
content: Some(content.to_string()),
msg_id: Some(msg_id.clone()),
event_id: self.event_id.clone(),
..Default::default()
};
api.post_group_message_with_params(token, group_openid, params)
.await
} else {
Err(crate::error::BotError::InvalidData(
"Missing group_openid or message_id for reply".to_string(),
))
}
}
}
impl Default for GroupMessage {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct C2CMessage {
pub id: Option<String>,
pub content: Option<String>,
pub message_reference: Option<MessageReference>,
pub mentions: Vec<C2CMessageUser>,
pub attachments: Vec<MessageAttachment>,
pub msg_seq: Option<u64>,
pub timestamp: Option<Timestamp>,
pub author: Option<C2CMessageUser>,
pub message_scene: Option<Value>,
pub event_id: Option<String>,
}
impl C2CMessage {
pub fn new() -> Self {
Self {
id: None,
content: None,
message_reference: None,
mentions: Vec::new(),
attachments: Vec::new(),
msg_seq: None,
timestamp: None,
author: None,
message_scene: None,
event_id: None,
}
}
pub fn from_data(_api: crate::api::BotApi, event_id: String, data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
content: data
.get("content")
.and_then(|v| v.as_str())
.map(String::from),
message_reference: data
.get("message_reference")
.map(|v| MessageReference::from_data(v.clone())),
mentions: data
.get("mentions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| C2CMessageUser::from_data(v.clone()))
.collect()
})
.unwrap_or_default(),
attachments: data
.get("attachments")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| MessageAttachment::from_data(v.clone()))
.collect()
})
.unwrap_or_default(),
msg_seq: data.get("msg_seq").and_then(|v| v.as_u64()),
timestamp: data
.get("timestamp")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
author: data
.get("author")
.map(|v| C2CMessageUser::from_data(v.clone())),
message_scene: data.get("message_scene").cloned(),
event_id: Some(event_id),
}
}
pub async fn reply(
&self,
api: &crate::api::BotApi,
token: &crate::token::Token,
content: &str,
) -> Result<crate::models::api::MessageResponse, crate::error::BotError> {
if let (Some(user_openid), Some(msg_id)) = (
self.author.as_ref().and_then(|a| a.user_openid.as_ref()),
&self.id,
) {
let params = C2CMessageParams {
msg_type: 0,
content: Some(content.to_string()),
msg_id: Some(msg_id.clone()),
msg_seq: Some(1),
event_id: self.event_id.clone(),
..Default::default()
};
api.post_c2c_message_with_params(token, user_openid, params)
.await
} else {
Err(crate::error::BotError::InvalidData(
"Missing user_openid or message_id for C2C reply".to_string(),
))
}
}
}
impl Default for C2CMessage {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageAudit {
pub audit_id: Option<Snowflake>,
pub message_id: Option<Snowflake>,
pub channel_id: Option<Snowflake>,
pub guild_id: Option<Snowflake>,
pub audit_time: Option<Timestamp>,
pub create_time: Option<Timestamp>,
pub seq_in_channel: Option<String>,
pub event_id: Option<String>,
}
impl MessageAudit {
pub fn new() -> Self {
Self {
audit_id: None,
message_id: None,
channel_id: None,
guild_id: None,
audit_time: None,
create_time: None,
seq_in_channel: None,
event_id: None,
}
}
pub fn from_data(_api: crate::api::BotApi, event_id: String, data: serde_json::Value) -> Self {
Self {
audit_id: data
.get("audit_id")
.and_then(|v| v.as_str())
.map(String::from),
message_id: data
.get("message_id")
.and_then(|v| v.as_str())
.map(String::from),
audit_time: data
.get("audit_time")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
channel_id: data
.get("channel_id")
.and_then(|v| v.as_str())
.map(String::from),
guild_id: data
.get("guild_id")
.and_then(|v| v.as_str())
.map(String::from),
create_time: data
.get("create_time")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
seq_in_channel: data
.get("seq_in_channel")
.and_then(|v| v.as_str())
.map(String::from),
event_id: Some(event_id),
}
}
}
impl Default for MessageAudit {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageUser {
pub id: Option<Snowflake>,
pub username: Option<String>,
pub bot: Option<bool>,
pub avatar: Option<String>,
}
impl MessageUser {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
username: data
.get("username")
.and_then(|v| v.as_str())
.map(String::from),
bot: data.get("bot").and_then(|v| v.as_bool()),
avatar: data
.get("avatar")
.and_then(|v| v.as_str())
.map(String::from),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectMessageUser {
pub id: Option<Snowflake>,
pub username: Option<String>,
pub avatar: Option<String>,
}
impl DirectMessageUser {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
username: data
.get("username")
.and_then(|v| v.as_str())
.map(String::from),
avatar: data
.get("avatar")
.and_then(|v| v.as_str())
.map(String::from),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GroupMessageUser {
pub id: Option<String>,
pub member_openid: Option<String>,
pub union_openid: Option<String>,
}
impl GroupMessageUser {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
member_openid: data
.get("member_openid")
.and_then(|v| v.as_str())
.map(String::from),
union_openid: data
.get("union_openid")
.and_then(|v| v.as_str())
.map(String::from),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct C2CMessageUser {
pub id: Option<String>,
pub union_openid: Option<String>,
pub user_openid: Option<String>,
}
impl C2CMessageUser {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
union_openid: data
.get("union_openid")
.and_then(|v| v.as_str())
.map(String::from),
user_openid: data
.get("user_openid")
.and_then(|v| v.as_str())
.map(String::from),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageMember {
pub nick: Option<String>,
pub roles: Option<Vec<Snowflake>>,
pub joined_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DirectMessageMember {
pub joined_at: Option<Timestamp>,
}
impl DirectMessageMember {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
joined_at: data
.get("joined_at")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageReference {
pub message_id: Option<Snowflake>,
pub ignore_get_message_error: Option<bool>,
}
impl MessageReference {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
message_id: data
.get("message_id")
.and_then(|v| v.as_str())
.map(String::from),
ignore_get_message_error: data
.get("ignore_get_message_error")
.and_then(|v| v.as_bool()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageAttachment {
pub id: Option<Snowflake>,
pub filename: Option<String>,
pub content_type: Option<String>,
pub size: Option<u64>,
pub url: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
}
impl MessageAttachment {
pub fn from_data(data: serde_json::Value) -> Self {
Self {
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
filename: data
.get("filename")
.and_then(|v| v.as_str())
.map(String::from),
content_type: data
.get("content_type")
.and_then(|v| v.as_str())
.map(String::from),
size: data.get("size").and_then(|v| v.as_u64()),
url: data.get("url").and_then(|v| v.as_str()).map(String::from),
width: data.get("width").and_then(|v| v.as_u64()).map(|w| w as u32),
height: data
.get("height")
.and_then(|v| v.as_u64())
.map(|h| h as u32),
}
}
pub fn is_image(&self) -> bool {
self.content_type
.as_ref()
.is_some_and(|ct| ct.starts_with("image/"))
}
pub fn is_video(&self) -> bool {
self.content_type
.as_ref()
.is_some_and(|ct| ct.starts_with("video/"))
}
pub fn is_audio(&self) -> bool {
self.content_type
.as_ref()
.is_some_and(|ct| ct.starts_with("audio/"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_botgo_message_helpers() {
assert_eq!(MentionUser("123"), "<@123>");
assert_eq!(MentionAllUser(), "@everyone");
assert_eq!(MentionChannel("456"), "<#456>");
assert_eq!(Emoji(1), "<emoji:1>");
assert_eq!(ETLInput("<@!123> ping value"), "ping value");
assert_eq!(
ETLInput("\u{00a0}<@!123> ping value\u{00a0}"),
"ping value"
);
assert_eq!(ETLInput("<@123> ping"), "<@123> ping");
assert_eq!(ETLInput("<@!abc> ping"), "<@!abc> ping");
let command = ParseCommand("<@!123> /ping value");
assert_eq!(command.cmd, "/ping");
assert_eq!(command.content, "value");
let command = ParseCommand("<@!123> /ping value");
assert_eq!(command.cmd, "/ping");
assert_eq!(command.content, "value");
let command = ParseCommand("/ping\tvalue");
assert_eq!(command.cmd, "/ping\tvalue");
assert_eq!(command.content, "");
}
#[test]
fn botgo_api_message_helpers_match_send_type_contract() {
let message = MessageToCreate {
event_id: Some("event-1".to_string()),
..Default::default()
};
let rich_media = RichMediaMessage {
event_id: Some("ignored".to_string()),
..Default::default()
};
let reference = Reference {
message_id: Some("message-1".to_string()),
ignore_get_message_error: Some(true),
};
assert_eq!(message.GetEventID(), "event-1");
assert_eq!(message.GetSendType(), SendType::Text);
assert_eq!(rich_media.GetEventID(), "");
assert_eq!(rich_media.GetSendType(), SendType::RichMedia);
assert_eq!(reference.GetEventID(), "message-1");
assert_eq!(reference.GetSendType(), SendType::Text);
let api_message = ApiMessage::from(message);
assert_eq!(api_message.GetEventID(), "event-1");
assert_eq!(api_message.GetSendType(), SendType::Text);
}
#[test]
fn botgo_messages_pager_query_params() {
let pager = MessagesPager::new(Some(MPTBefore), Some("msg-1"), Some(20));
let query = pager.QueryParams();
assert_eq!(query.get("before").map(String::as_str), Some("msg-1"));
assert_eq!(query.get("limit").map(String::as_str), Some("20"));
}
#[test]
fn test_message_creation() {
let message = Message::new();
assert!(message.id.is_none());
assert!(message.content.is_none());
assert!(!message.has_content());
assert!(!message.has_attachments());
assert!(!message.has_mentions());
}
#[test]
fn botgo_direct_message_is_session_dto() {
let session = DirectMessage::from_data(serde_json::json!({
"guild_id": "guild-1",
"channel_id": "channel-1",
"create_time": "2024-01-02T03:04:05+08:00",
"content": "ignored"
}));
assert_eq!(session.guild_id.as_deref(), Some("guild-1"));
assert_eq!(session.channel_id.as_deref(), Some("channel-1"));
assert_eq!(
session.create_time.as_deref(),
Some("2024-01-02T03:04:05+08:00")
);
let value = serde_json::to_value(&session).unwrap();
assert_eq!(value["guild_id"], serde_json::json!("guild-1"));
assert_eq!(value["channel_id"], serde_json::json!("channel-1"));
assert_eq!(
value["create_time"],
serde_json::json!("2024-01-02T03:04:05+08:00")
);
assert!(value.get("content").is_none());
}
#[test]
fn test_message_with_content() {
let mut message = Message::new();
message.content = Some("Hello, world!".to_string());
assert!(message.has_content());
}
#[test]
fn botgo_message_reference_keeps_ignore_error_flag() {
let reference = MessageReference::from_data(serde_json::json!({
"message_id": "message-1",
"ignore_get_message_error": true
}));
assert_eq!(reference.message_id.as_deref(), Some("message-1"));
assert_eq!(reference.ignore_get_message_error, Some(true));
let value = serde_json::to_value(&reference).unwrap();
assert_eq!(value["message_id"], serde_json::json!("message-1"));
assert_eq!(value["ignore_get_message_error"], serde_json::json!(true));
}
#[test]
fn botgo_message_audit_keeps_channel_sequence() {
let audit = MessageAudit::from_data(
crate::api::BotApi::new(crate::http::HttpClient::new(30, false).unwrap()),
"event-1".to_string(),
serde_json::json!({
"audit_id": "audit-1",
"message_id": "message-1",
"guild_id": "guild-1",
"channel_id": "channel-1",
"seq_in_channel": "42"
}),
);
assert_eq!(audit.seq_in_channel.as_deref(), Some("42"));
let value = serde_json::to_value(&audit).unwrap();
assert_eq!(value["seq_in_channel"], serde_json::json!("42"));
}
#[test]
fn botgo_embed_keeps_prompt_field() {
let embed = Embed {
title: Some("title".to_string()),
prompt: Some("summary".to_string()),
..Default::default()
};
let value = serde_json::to_value(&embed).unwrap();
assert_eq!(value["prompt"], serde_json::json!("summary"));
let parsed: Embed = serde_json::from_value(value).unwrap();
assert_eq!(parsed.prompt.as_deref(), Some("summary"));
}
#[test]
fn test_message_attachment_types() {
let mut attachment = MessageAttachment {
id: Some("123".to_string()),
filename: Some("image.png".to_string()),
content_type: Some("image/png".to_string()),
size: Some(1024),
url: Some("https://example.com/image.png".to_string()),
width: Some(800),
height: Some(600),
};
assert!(attachment.is_image());
assert!(!attachment.is_video());
assert!(!attachment.is_audio());
attachment.content_type = Some("video/mp4".to_string());
assert!(!attachment.is_image());
assert!(attachment.is_video());
assert!(!attachment.is_audio());
}
#[test]
fn test_bot_detection() {
let mut message = Message::new();
message.author = Some(MessageUser {
id: Some("123".to_string()),
username: Some("Bot".to_string()),
bot: Some(true),
avatar: None,
});
assert!(message.is_from_bot());
message.author.as_mut().unwrap().bot = Some(false);
assert!(!message.is_from_bot());
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Ark {
pub template_id: Option<u32>,
pub kv: Option<Vec<ArkKv>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MessageArk {
#[serde(skip_serializing_if = "Option::is_none")]
pub ark: Option<Ark>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArkKv {
pub key: Option<String>,
pub value: Option<String>,
pub obj: Option<Vec<ArkObj>>,
}
pub type ArkKV = ArkKv;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArkObj {
pub obj_kv: Option<Vec<ArkObjKv>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArkObjKv {
pub key: Option<String>,
pub value: Option<String>,
}
pub type ArkObjKV = ArkObjKv;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Embed {
pub title: Option<String>,
pub description: Option<String>,
pub prompt: Option<String>,
pub url: Option<String>,
pub timestamp: Option<String>,
pub color: Option<u32>,
pub footer: Option<EmbedFooter>,
pub image: Option<EmbedImage>,
pub thumbnail: Option<EmbedThumbnail>,
pub video: Option<EmbedVideo>,
pub provider: Option<EmbedProvider>,
pub author: Option<EmbedAuthor>,
pub fields: Option<Vec<EmbedField>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedFooter {
pub text: Option<String>,
pub icon_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedImage {
pub url: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedThumbnail {
pub url: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
}
pub type MessageEmbedThumbnail = EmbedThumbnail;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedVideo {
pub url: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedProvider {
pub name: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedAuthor {
pub name: Option<String>,
pub url: Option<String>,
pub icon_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbedField {
pub name: Option<String>,
pub value: Option<String>,
pub inline: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Keyboard {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<KeyboardContent>,
}
pub type ActionType = u32;
pub type PermissionType = u32;
pub const ACTION_TYPE_URL: ActionType = 0;
pub const ACTION_TYPE_CALLBACK: ActionType = 1;
pub const ACTION_TYPE_AT_BOT: ActionType = 2;
pub const ACTION_TYPE_MQQ_API: ActionType = 3;
pub const ACTION_TYPE_SUBSCRIBE: ActionType = 4;
#[allow(non_upper_case_globals)]
pub const ActionTypeURL: ActionType = ACTION_TYPE_URL;
#[allow(non_upper_case_globals)]
pub const ActionTypeCallback: ActionType = ACTION_TYPE_CALLBACK;
#[allow(non_upper_case_globals)]
pub const ActionTypeAtBot: ActionType = ACTION_TYPE_AT_BOT;
#[allow(non_upper_case_globals)]
pub const ActionTypeMQQAPI: ActionType = ACTION_TYPE_MQQ_API;
#[allow(non_upper_case_globals)]
pub const ActionTypeSubscribe: ActionType = ACTION_TYPE_SUBSCRIBE;
pub const PERMISSION_TYPE_SPECIFY_USER_IDS: PermissionType = 0;
pub const PERMISSION_TYPE_MANAGER: PermissionType = 1;
pub const PERMISSION_TYPE_ALL: PermissionType = 2;
pub const PERMISSION_TYPE_SPECIFY_ROLE_IDS: PermissionType = 3;
#[allow(non_upper_case_globals)]
pub const PermissionTypeSpecifyUserIDs: PermissionType = PERMISSION_TYPE_SPECIFY_USER_IDS;
#[allow(non_upper_case_globals)]
pub const PermissionTypManager: PermissionType = PERMISSION_TYPE_MANAGER;
#[allow(non_upper_case_globals)]
pub const PermissionTypAll: PermissionType = PERMISSION_TYPE_ALL;
#[allow(non_upper_case_globals)]
pub const PermissionTypSpecifyRoleIDs: PermissionType = PERMISSION_TYPE_SPECIFY_ROLE_IDS;
pub type MessageKeyboard = Keyboard;
pub type CustomKeyboard = KeyboardContent;
pub type Row = KeyboardRow;
pub type Button = KeyboardButton;
pub type RenderData = KeyboardButtonRenderData;
pub type Action = KeyboardButtonAction;
pub type Permission = KeyboardButtonPermission;
pub type SubscribeData = KeyboardSubscribeData;
pub type TemplateID = KeyboardTemplateId;
pub type Modal = KeyboardModal;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardContent {
#[serde(skip_serializing_if = "Option::is_none")]
pub rows: Option<Vec<KeyboardRow>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<KeyboardStyle>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub font_size: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardRow {
#[serde(skip_serializing_if = "Option::is_none")]
pub buttons: Option<Vec<KeyboardButton>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardButton {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub render_data: Option<KeyboardButtonRenderData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<KeyboardButtonAction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardButtonRenderData {
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visited_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardButtonAction {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub action_type: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission: Option<KeyboardButtonPermission>,
#[serde(skip_serializing_if = "Option::is_none")]
pub click_limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reply: Option<bool>,
pub enter: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub at_bot_show_channel_list: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe_data: Option<KeyboardSubscribeData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modal: Option<KeyboardModal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardButtonPermission {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub permission_type: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub specify_role_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub specify_user_ids: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeyboardSubscribeData {
#[serde(skip_serializing_if = "Option::is_none")]
pub template_ids: Option<Vec<KeyboardTemplateId>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct KeyboardTemplateId {
#[serde(skip_serializing_if = "Option::is_none")]
pub template_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_template_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct KeyboardModal {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confirm_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cancel_text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KeyboardPayload {
pub content: serde_json::Value,
}
pub type Markdown = MarkdownPayload;
pub type MarkdownParams = MarkdownParam;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MarkdownPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub template_id: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_template_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Vec<MarkdownParam>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<MarkdownStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub process_msg: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MarkdownStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub main_font_size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MarkdownParam {
pub key: Option<String>,
pub values: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Media {
pub file_info: Option<String>,
pub ttl: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reference {
pub message_id: Option<String>,
pub ignore_get_message_error: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "u8", into = "u8")]
#[repr(u8)]
pub enum SendType {
Text = 1,
RichMedia = 2,
Unknown(u8),
}
impl From<u8> for SendType {
fn from(value: u8) -> Self {
match value {
1 => Self::Text,
2 => Self::RichMedia,
other => Self::Unknown(other),
}
}
}
impl From<SendType> for u8 {
fn from(send_type: SendType) -> Self {
match send_type {
SendType::Text => 1,
SendType::RichMedia => 2,
SendType::Unknown(value) => value,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "u32", into = "u32")]
#[repr(u32)]
pub enum MessageCreateType {
Text = 0,
Markdown = 2,
Ark = 3,
Embed = 4,
At = 5,
InputNotify = 6,
RichMedia = 7,
Unknown(u32),
}
#[allow(non_upper_case_globals)]
pub const TextMsg: MessageCreateType = MessageCreateType::Text;
#[allow(non_upper_case_globals)]
pub const MarkdownMsg: MessageCreateType = MessageCreateType::Markdown;
#[allow(non_upper_case_globals)]
pub const ArkMsg: MessageCreateType = MessageCreateType::Ark;
#[allow(non_upper_case_globals)]
pub const EmbedMsg: MessageCreateType = MessageCreateType::Embed;
#[allow(non_upper_case_globals)]
pub const ATMsg: MessageCreateType = MessageCreateType::At;
#[allow(non_upper_case_globals)]
pub const InputNotifyMsg: MessageCreateType = MessageCreateType::InputNotify;
#[allow(non_upper_case_globals)]
pub const RichMediaMsg: MessageCreateType = MessageCreateType::RichMedia;
#[allow(non_upper_case_globals)]
pub const RichMedia: SendType = SendType::RichMedia;
impl From<u32> for MessageCreateType {
fn from(value: u32) -> Self {
match value {
0 => Self::Text,
2 => Self::Markdown,
3 => Self::Ark,
4 => Self::Embed,
5 => Self::At,
6 => Self::InputNotify,
7 => Self::RichMedia,
other => Self::Unknown(other),
}
}
}
impl From<MessageCreateType> for u32 {
fn from(message_type: MessageCreateType) -> Self {
match message_type {
MessageCreateType::Text => 0,
MessageCreateType::Markdown => 2,
MessageCreateType::Ark => 3,
MessageCreateType::Embed => 4,
MessageCreateType::At => 5,
MessageCreateType::InputNotify => 6,
MessageCreateType::RichMedia => 7,
MessageCreateType::Unknown(value) => value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct InputNotify {
#[serde(skip_serializing_if = "Option::is_none")]
pub input_type: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_second: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MediaInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub file_info: Option<String>,
}
impl From<Media> for MediaInfo {
fn from(media: Media) -> Self {
Self {
file_info: media.file_info,
}
}
}
impl From<MediaInfo> for Media {
fn from(media: MediaInfo) -> Self {
Self {
file_info: media.file_info,
ttl: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Stream {
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reset: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct PromptKeyboard {
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ActionButton {
#[serde(skip_serializing_if = "Option::is_none")]
pub template_id: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub re_generate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_generate: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MessageToCreate {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_type: Option<MessageCreateType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embed: Option<Embed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ark: Option<Ark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<MarkdownPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_seq: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_notify: Option<InputNotify>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<MediaInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_keyboard: Option<PromptKeyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_button: Option<ActionButton>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<Stream>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_image: Option<String>,
}
impl MessageToCreate {
pub fn new_text(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
..Default::default()
}
}
pub fn with_file_image(mut self, data: &[u8]) -> Self {
self.file_image = Some(base64::engine::general_purpose::STANDARD.encode(data));
self
}
pub fn with_reply(mut self, message_id: impl Into<String>) -> Self {
self.msg_id = Some(message_id.into());
self
}
pub const fn send_type(&self) -> SendType {
SendType::Text
}
#[allow(non_snake_case)]
pub fn GetEventID(&self) -> &str {
self.event_id.as_deref().unwrap_or("")
}
#[allow(non_snake_case)]
pub const fn GetSendType(&self) -> SendType {
self.send_type()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct RichMediaMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_type: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub srv_send_msg: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_seq: Option<i64>,
}
impl RichMediaMessage {
pub fn new(file_type: u64, url: impl Into<String>) -> Self {
Self {
file_type: Some(file_type),
url: Some(url.into()),
..Default::default()
}
}
pub const fn send_type(&self) -> SendType {
SendType::RichMedia
}
#[allow(non_snake_case)]
pub const fn GetEventID(&self) -> &str {
""
}
#[allow(non_snake_case)]
pub const fn GetSendType(&self) -> SendType {
self.send_type()
}
}
pub trait APIMessage: Serialize {
#[allow(non_snake_case)]
fn GetEventID(&self) -> &str;
#[allow(non_snake_case)]
fn GetSendType(&self) -> SendType;
}
#[derive(Debug, Clone, PartialEq)]
pub enum ApiMessage {
Message(Box<MessageToCreate>),
RichMedia(RichMediaMessage),
}
impl ApiMessage {
pub const fn send_type(&self) -> SendType {
match self {
Self::Message(message) => message.send_type(),
Self::RichMedia(message) => message.send_type(),
}
}
#[allow(non_snake_case)]
pub fn GetEventID(&self) -> &str {
match self {
Self::Message(message) => message.GetEventID(),
Self::RichMedia(message) => message.GetEventID(),
}
}
#[allow(non_snake_case)]
pub const fn GetSendType(&self) -> SendType {
self.send_type()
}
}
impl APIMessage for MessageToCreate {
fn GetEventID(&self) -> &str {
self.GetEventID()
}
fn GetSendType(&self) -> SendType {
self.GetSendType()
}
}
impl APIMessage for RichMediaMessage {
fn GetEventID(&self) -> &str {
self.GetEventID()
}
fn GetSendType(&self) -> SendType {
self.GetSendType()
}
}
impl APIMessage for ApiMessage {
fn GetEventID(&self) -> &str {
self.GetEventID()
}
fn GetSendType(&self) -> SendType {
self.GetSendType()
}
}
impl Serialize for ApiMessage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Message(message) => message.serialize(serializer),
Self::RichMedia(message) => message.serialize(serializer),
}
}
}
impl From<MessageToCreate> for ApiMessage {
fn from(message: MessageToCreate) -> Self {
Self::Message(Box::new(message))
}
}
impl From<RichMediaMessage> for ApiMessage {
fn from(message: RichMediaMessage) -> Self {
Self::RichMedia(message)
}
}
impl APIMessage for Reference {
fn GetEventID(&self) -> &str {
self.message_id.as_deref().unwrap_or("")
}
fn GetSendType(&self) -> SendType {
SendType::Text
}
}
impl From<KeyboardPayload> for Keyboard {
fn from(payload: KeyboardPayload) -> Self {
serde_json::from_value(payload.content).unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessagePagerType {
Around,
Before,
After,
}
#[allow(non_upper_case_globals)]
pub const MPTAround: MessagePagerType = MessagePagerType::Around;
#[allow(non_upper_case_globals)]
pub const MPTBefore: MessagePagerType = MessagePagerType::Before;
#[allow(non_upper_case_globals)]
pub const MPTAfter: MessagePagerType = MessagePagerType::After;
impl MessagePagerType {
pub const fn as_str(self) -> &'static str {
match self {
Self::Around => "around",
Self::Before => "before",
Self::After => "after",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MessagesPager {
#[serde(skip)]
pub pager_type: Option<MessagePagerType>,
#[serde(skip)]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
}
impl MessagesPager {
pub fn new(
pager_type: Option<MessagePagerType>,
id: Option<impl Into<String>>,
limit: Option<impl ToString>,
) -> Self {
Self {
pager_type,
id: id.map(Into::into),
limit: limit.map(|value| value.to_string()),
}
}
pub fn query_params(&self) -> std::collections::HashMap<String, String> {
let mut query = std::collections::HashMap::new();
if let Some(limit) = &self.limit {
query.insert("limit".to_string(), limit.clone());
}
if let (Some(pager_type), Some(id)) = (self.pager_type, &self.id) {
query.insert(pager_type.as_str().to_string(), id.clone());
}
query
}
#[allow(non_snake_case)]
pub fn QueryParams(&self) -> std::collections::HashMap<String, String> {
self.query_params()
}
}
impl Pager for MessagesPager {
fn query_params(&self) -> std::collections::HashMap<String, String> {
MessagesPager::query_params(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SettingGuide {
pub guild_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SettingGuideToCreate {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub setting_guide: Option<SettingGuide>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MessageParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_type: Option<MessageCreateType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embed: Option<Embed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ark: Option<Ark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<MarkdownPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_seq: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_notify: Option<InputNotify>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<MediaInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_keyboard: Option<PromptKeyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_button: Option<ActionButton>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<Stream>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GroupMessageParams {
#[serde(skip_serializing_if = "is_zero_u32")]
pub msg_type: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embed: Option<Embed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ark: Option<Ark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<Media>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_seq: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<MarkdownPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<KeyboardPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_notify: Option<InputNotify>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_keyboard: Option<PromptKeyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_button: Option<ActionButton>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<Stream>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct C2CMessageParams {
#[serde(skip_serializing_if = "is_zero_u32")]
pub msg_type: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embed: Option<Embed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ark: Option<Ark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<Media>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_seq: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<MarkdownPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<KeyboardPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_notify: Option<InputNotify>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_keyboard: Option<PromptKeyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_button: Option<ActionButton>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<Stream>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DirectMessageParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_type: Option<MessageCreateType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embed: Option<Embed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ark: Option<Ark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<MarkdownPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg_seq: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_notify: Option<InputNotify>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<MediaInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_keyboard: Option<PromptKeyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_button: Option<ActionButton>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<Stream>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<u32>,
}
impl MessageParams {
pub fn new_text(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
..Default::default()
}
}
pub fn with_file_image(mut self, data: &[u8]) -> Self {
self.file_image = Some(base64::engine::general_purpose::STANDARD.encode(data));
self
}
pub fn with_reply(mut self, message_id: impl Into<String>) -> Self {
self.msg_id = Some(message_id.into());
self
}
pub fn into_message_to_create(self) -> MessageToCreate {
self.into()
}
}
impl GroupMessageParams {
pub fn new_text(content: impl Into<String>) -> Self {
Self {
msg_type: 0,
content: Some(content.into()),
..Default::default()
}
}
pub fn with_reply(mut self, message_id: impl Into<String>) -> Self {
self.msg_id = Some(message_id.into());
self
}
pub fn into_message_to_create(self) -> MessageToCreate {
self.into()
}
}
impl C2CMessageParams {
pub fn new_text(content: impl Into<String>) -> Self {
Self {
msg_type: 0,
content: Some(content.into()),
..Default::default()
}
}
pub fn with_reply(mut self, message_id: impl Into<String>) -> Self {
self.msg_id = Some(message_id.into());
self
}
pub fn into_message_to_create(self) -> MessageToCreate {
self.into()
}
}
impl DirectMessageParams {
pub fn new_text(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
..Default::default()
}
}
pub fn with_file_image(mut self, data: &[u8]) -> Self {
self.file_image = Some(base64::engine::general_purpose::STANDARD.encode(data));
self
}
pub fn with_reply(mut self, message_id: impl Into<String>) -> Self {
self.msg_id = Some(message_id.into());
self
}
pub fn into_message_to_create(self) -> MessageToCreate {
self.into()
}
}
impl From<MessageParams> for MessageToCreate {
fn from(params: MessageParams) -> Self {
Self {
content: params.content,
msg_type: params.msg_type,
embed: params.embed,
ark: params.ark,
image: params.image,
msg_id: params.msg_id,
message_reference: params.message_reference,
markdown: params.markdown,
keyboard: params.keyboard,
event_id: params.event_id,
timestamp: params.timestamp,
msg_seq: params.msg_seq,
subscribe_id: params.subscribe_id,
input_notify: params.input_notify,
media: params.media,
prompt_keyboard: params.prompt_keyboard,
action_button: params.action_button,
stream: params.stream,
feature_id: params.feature_id,
file_image: params.file_image,
}
}
}
impl From<DirectMessageParams> for MessageToCreate {
fn from(params: DirectMessageParams) -> Self {
Self {
content: params.content,
msg_type: params.msg_type,
embed: params.embed,
ark: params.ark,
image: params.image,
msg_id: params.msg_id,
message_reference: params.message_reference,
markdown: params.markdown,
keyboard: params.keyboard,
event_id: params.event_id,
timestamp: params.timestamp,
msg_seq: params.msg_seq,
subscribe_id: params.subscribe_id,
input_notify: params.input_notify,
media: params.media,
prompt_keyboard: params.prompt_keyboard,
action_button: params.action_button,
stream: params.stream,
feature_id: params.feature_id,
file_image: params.file_image,
}
}
}
impl From<GroupMessageParams> for MessageToCreate {
fn from(params: GroupMessageParams) -> Self {
Self {
content: params.content,
msg_type: Some(MessageCreateType::from(params.msg_type)),
embed: params.embed,
ark: params.ark,
msg_id: params.msg_id,
message_reference: params.message_reference,
markdown: params.markdown,
keyboard: params.keyboard.map(Into::into),
event_id: params.event_id,
timestamp: params.timestamp,
msg_seq: params.msg_seq,
subscribe_id: params.subscribe_id,
input_notify: params.input_notify,
media: params.media.map(Into::into),
prompt_keyboard: params.prompt_keyboard,
action_button: params.action_button,
stream: params.stream,
feature_id: params.feature_id,
..Default::default()
}
}
}
impl From<C2CMessageParams> for MessageToCreate {
fn from(params: C2CMessageParams) -> Self {
Self {
content: params.content,
msg_type: Some(MessageCreateType::from(params.msg_type)),
embed: params.embed,
ark: params.ark,
msg_id: params.msg_id,
message_reference: params.message_reference,
markdown: params.markdown,
keyboard: params.keyboard.map(Into::into),
event_id: params.event_id,
timestamp: params.timestamp,
msg_seq: params.msg_seq,
subscribe_id: params.subscribe_id,
input_notify: params.input_notify,
media: params.media.map(Into::into),
prompt_keyboard: params.prompt_keyboard,
action_button: params.action_button,
stream: params.stream,
feature_id: params.feature_id,
..Default::default()
}
}
}