use crate::api::BotApi;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InteractionType {
Ping = 1,
ApplicationCommand = 2,
HttpProxy = 10,
InlineKeyboard = 11,
}
#[allow(non_upper_case_globals)]
pub const InteractionTypePing: InteractionType = InteractionType::Ping;
#[allow(non_upper_case_globals)]
pub const InteractionTypeCommand: InteractionType = InteractionType::ApplicationCommand;
impl From<u8> for InteractionType {
fn from(value: u8) -> Self {
match value {
1 => Self::Ping,
2 => Self::ApplicationCommand,
10 => Self::HttpProxy,
11 => Self::InlineKeyboard,
_ => Self::Ping, }
}
}
impl Serialize for InteractionType {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8(*self as u8)
}
}
impl<'de> Deserialize<'de> for InteractionType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Self::from(u8::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InteractionDataType {
ChatInputSearch = 9,
HttpProxy = 10,
InlineKeyboardButtonClick = 11,
CallbackCommandClick = 12,
MessageFeedbackClick = 13,
ClearSessionClick = 14,
}
#[allow(non_upper_case_globals)]
pub const InteractionDataTypeChatSearch: InteractionDataType = InteractionDataType::ChatInputSearch;
#[allow(non_upper_case_globals)]
pub const InteractionDataTypeInlineKeyboardClick: InteractionDataType =
InteractionDataType::InlineKeyboardButtonClick;
#[allow(non_upper_case_globals)]
pub const InteractionDataTypeCallbackCommandClick: InteractionDataType =
InteractionDataType::CallbackCommandClick;
#[allow(non_upper_case_globals)]
pub const InteractionDataTypeMessageFeedbackClick: InteractionDataType =
InteractionDataType::MessageFeedbackClick;
#[allow(non_upper_case_globals)]
pub const InteractionDataTypeClearSessionClick: InteractionDataType =
InteractionDataType::ClearSessionClick;
pub type LayoutType = u32;
pub const LAYOUT_TYPE_IMAGE_TEXT: LayoutType = 0;
#[allow(non_upper_case_globals)]
pub const LayoutTypeImageText: LayoutType = LAYOUT_TYPE_IMAGE_TEXT;
pub const ACTION_TYPE_SEND_ARK: crate::models::message::ActionType = 0;
#[allow(non_upper_case_globals)]
pub const ActionTypeSendARK: crate::models::message::ActionType = ACTION_TYPE_SEND_ARK;
impl From<u8> for InteractionDataType {
fn from(value: u8) -> Self {
match value {
9 => Self::ChatInputSearch,
10 => Self::HttpProxy,
11 => Self::InlineKeyboardButtonClick,
12 => Self::CallbackCommandClick,
13 => Self::MessageFeedbackClick,
14 => Self::ClearSessionClick,
_ => Self::ChatInputSearch, }
}
}
impl Serialize for InteractionDataType {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8(*self as u8)
}
}
impl<'de> Deserialize<'de> for InteractionDataType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Self::from(u8::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resolved {
pub keyword: Option<String>,
pub button_id: Option<String>,
pub button_data: Option<String>,
pub message_id: Option<String>,
pub user_id: Option<String>,
pub request: Option<String>,
pub member_nick: Option<String>,
pub feature_id: Option<String>,
pub feedback_opt: Option<String>,
pub checked: Option<i32>,
}
impl Resolved {
pub fn new(data: &Value) -> Self {
Self {
keyword: data
.get("keyword")
.and_then(|v| v.as_str())
.map(String::from),
button_id: data
.get("button_id")
.and_then(|v| v.as_str())
.map(String::from),
button_data: data
.get("button_data")
.and_then(|v| v.as_str())
.map(String::from),
message_id: data
.get("message_id")
.and_then(|v| v.as_str())
.map(String::from),
user_id: data
.get("user_id")
.and_then(|v| v.as_str())
.map(String::from),
request: data
.get("request")
.and_then(|v| v.as_str())
.map(String::from),
member_nick: data
.get("member_nick")
.and_then(|v| v.as_str())
.map(String::from),
feature_id: data
.get("feature_id")
.and_then(|v| v.as_str())
.map(String::from),
feedback_opt: data
.get("feedback_opt")
.and_then(|v| v.as_str())
.map(String::from),
checked: data
.get("checked")
.and_then(|v| v.as_i64())
.map(|v| v as i32),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteractionData {
pub name: Option<String>,
#[serde(rename = "type")]
pub data_type: Option<InteractionDataType>,
pub resolved: Resolved,
}
impl InteractionData {
pub fn new(data: &Value) -> Self {
Self {
name: data.get("name").and_then(|v| v.as_str()).map(String::from),
data_type: data
.get("type")
.and_then(|v| v.as_u64())
.map(|v| InteractionDataType::from(v as u8)),
resolved: Resolved::new(
data.get("resolved")
.unwrap_or(&Value::Object(serde_json::Map::new())),
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SearchInputResolved {
pub keyword: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SearchRsp {
#[serde(default)]
pub layouts: Vec<SearchLayout>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SearchLayout {
pub layout_type: Option<u32>,
pub action_type: Option<u32>,
pub title: Option<String>,
#[serde(default)]
pub records: Vec<SearchRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SearchRecord {
pub cover: Option<String>,
pub title: Option<String>,
pub tips: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Interaction {
#[serde(skip)]
api: BotApi,
pub id: Option<String>,
pub application_id: Option<String>,
#[serde(rename = "type")]
pub interaction_type: Option<InteractionType>,
pub scene: Option<String>,
pub chat_type: Option<u64>,
pub event_id: Option<String>,
pub data: InteractionData,
pub guild_id: Option<String>,
pub channel_id: Option<String>,
pub user_openid: Option<String>,
pub group_openid: Option<String>,
pub group_member_openid: Option<String>,
pub timestamp: Option<String>,
pub version: Option<u64>,
}
impl Interaction {
pub fn new(api: BotApi, event_id: Option<String>, data: &Value) -> Self {
Self {
api,
event_id,
id: data.get("id").and_then(|v| v.as_str()).map(String::from),
application_id: data.get("application_id").and_then(|v| {
v.as_str()
.map(String::from)
.or_else(|| v.as_u64().map(|value| value.to_string()))
}),
interaction_type: data
.get("type")
.and_then(|v| v.as_u64())
.map(|v| InteractionType::from(v as u8)),
scene: data.get("scene").and_then(|v| v.as_str()).map(String::from),
chat_type: data.get("chat_type").and_then(|v| v.as_u64()),
data: InteractionData::new(
data.get("data")
.unwrap_or(&Value::Object(serde_json::Map::new())),
),
guild_id: data
.get("guild_id")
.and_then(|v| v.as_str())
.map(String::from),
channel_id: data
.get("channel_id")
.and_then(|v| v.as_str())
.map(String::from),
user_openid: data
.get("user_openid")
.and_then(|v| v.as_str())
.map(String::from),
group_openid: data
.get("group_openid")
.and_then(|v| v.as_str())
.map(String::from),
group_member_openid: data
.get("group_member_openid")
.and_then(|v| v.as_str())
.map(String::from),
timestamp: data.get("timestamp").and_then(|v| {
v.as_str()
.map(String::from)
.or_else(|| v.as_u64().map(|value| value.to_string()))
}),
version: data.get("version").and_then(|v| v.as_u64()),
}
}
pub fn api(&self) -> &BotApi {
&self.api
}
pub fn is_button_interaction(&self) -> bool {
matches!(
self.data.data_type,
Some(InteractionDataType::InlineKeyboardButtonClick)
)
}
pub fn is_command_interaction(&self) -> bool {
matches!(
self.interaction_type,
Some(InteractionType::ApplicationCommand)
)
}
pub fn button_id(&self) -> Option<&str> {
self.data.resolved.button_id.as_deref()
}
pub fn button_data(&self) -> Option<&str> {
self.data.resolved.button_data.as_deref()
}
}
impl std::fmt::Display for Interaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Interaction {{ id: {:?}, type: {:?}, scene: {:?}, chat_type: {:?}, event_id: {:?} }}",
self.id, self.interaction_type, self.scene, self.chat_type, self.event_id
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interaction_type() {
assert_eq!(InteractionType::Ping as u8, 1);
assert_eq!(InteractionType::ApplicationCommand as u8, 2);
assert_eq!(InteractionType::HttpProxy as u8, 10);
assert_eq!(InteractionType::InlineKeyboard as u8, 11);
}
#[test]
fn test_interaction_data_type() {
assert_eq!(InteractionDataType::ChatInputSearch as u8, 9);
assert_eq!(InteractionDataType::HttpProxy as u8, 10);
assert_eq!(InteractionDataType::InlineKeyboardButtonClick as u8, 11);
assert_eq!(InteractionDataType::CallbackCommandClick as u8, 12);
assert_eq!(InteractionDataType::MessageFeedbackClick as u8, 13);
assert_eq!(InteractionDataType::ClearSessionClick as u8, 14);
}
#[test]
fn test_interaction_type_from() {
assert_eq!(InteractionType::from(1), InteractionType::Ping);
assert_eq!(
InteractionType::from(2),
InteractionType::ApplicationCommand
);
assert_eq!(InteractionType::from(10), InteractionType::HttpProxy);
assert_eq!(InteractionType::from(11), InteractionType::InlineKeyboard);
}
#[test]
fn test_interaction_data_type_from() {
assert_eq!(
InteractionDataType::from(9),
InteractionDataType::ChatInputSearch
);
assert_eq!(
InteractionDataType::from(10),
InteractionDataType::HttpProxy
);
assert_eq!(
InteractionDataType::from(11),
InteractionDataType::InlineKeyboardButtonClick
);
assert_eq!(
InteractionDataType::from(12),
InteractionDataType::CallbackCommandClick
);
assert_eq!(
InteractionDataType::from(13),
InteractionDataType::MessageFeedbackClick
);
assert_eq!(
InteractionDataType::from(14),
InteractionDataType::ClearSessionClick
);
}
#[test]
fn interaction_types_serialize_as_botgo_numeric_wire_values() {
assert_eq!(
serde_json::to_value(InteractionType::ApplicationCommand).unwrap(),
serde_json::json!(2)
);
assert_eq!(
serde_json::from_value::<InteractionType>(serde_json::json!(11)).unwrap(),
InteractionType::InlineKeyboard
);
assert_eq!(
serde_json::to_value(InteractionDataType::ChatInputSearch).unwrap(),
serde_json::json!(9)
);
assert_eq!(
serde_json::from_value::<InteractionDataType>(serde_json::json!(14)).unwrap(),
InteractionDataType::ClearSessionClick
);
}
#[test]
fn interaction_payload_uses_botgo_type_fields() {
let interaction = Interaction::new(
BotApi::new(crate::http::HttpClient::new(30, false).unwrap()),
Some("event-1".to_string()),
&serde_json::json!({
"id": "interaction-1",
"application_id": "app-1",
"type": 2,
"data": {
"name": "search",
"type": 9,
"resolved": {
"keyword": "botrs"
}
},
"version": 1
}),
);
let value = serde_json::to_value(&interaction).unwrap();
assert_eq!(value["type"], serde_json::json!(2));
assert_eq!(value["data"]["type"], serde_json::json!(9));
assert!(value.get("interaction_type").is_none());
assert!(value["data"].get("data_type").is_none());
}
}