use crate::{
card::CardElement,
emoji::EmojiValue,
error::ChatError,
event::ChatEvent,
file::FileUpload,
message::{AdapterPostableMessage, EphemeralMessage, SentMessage},
modal::ModalElement,
stream::{StreamOptions, TextStream},
};
#[async_trait::async_trait]
pub trait ChatAdapter: Send + Sync {
fn name(&self) -> &str;
async fn post_message(
&self,
thread_id: &str,
message: &AdapterPostableMessage,
) -> Result<SentMessage, ChatError>;
async fn edit_message(
&self,
thread_id: &str,
message_id: &str,
message: &AdapterPostableMessage,
) -> Result<SentMessage, ChatError>;
async fn delete_message(&self, thread_id: &str, message_id: &str) -> Result<(), ChatError>;
fn render_card(&self, card: &CardElement) -> String;
fn render_message(&self, message: &AdapterPostableMessage) -> String;
async fn recv_event(&mut self) -> Option<ChatEvent>;
async fn stream(
&self,
thread_id: &str,
text_stream: TextStream,
options: &StreamOptions,
) -> Result<SentMessage, ChatError> {
crate::stream::fallback_stream(self, thread_id, text_stream, options).await
}
async fn add_reaction(
&self,
_thread_id: &str,
_message_id: &str,
_emoji: &EmojiValue,
) -> Result<(), ChatError> {
Err(ChatError::NotSupported("reactions".into()))
}
async fn remove_reaction(
&self,
_thread_id: &str,
_message_id: &str,
_emoji: &EmojiValue,
) -> Result<(), ChatError> {
Err(ChatError::NotSupported("reactions".into()))
}
async fn open_dm(&self, _user_id: &str) -> Result<String, ChatError> {
Err(ChatError::NotSupported("direct messages".into()))
}
async fn post_ephemeral(
&self,
_thread_id: &str,
_user_id: &str,
_message: &AdapterPostableMessage,
) -> Result<EphemeralMessage, ChatError> {
Err(ChatError::NotSupported("ephemeral messages".into()))
}
async fn open_modal(
&self,
_trigger_id: &str,
_modal: &ModalElement,
_context_id: Option<&str>,
) -> Result<String, ChatError> {
Err(ChatError::NotSupported("modals".into()))
}
async fn start_typing(&self, _thread_id: &str, _status: Option<&str>) -> Result<(), ChatError> {
Ok(())
}
async fn upload_file(
&self,
_thread_id: &str,
_file: &FileUpload,
) -> Result<SentMessage, ChatError> {
Err(ChatError::NotSupported("file uploads".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::ChatEvent;
struct StubAdapter;
#[async_trait::async_trait]
impl ChatAdapter for StubAdapter {
fn name(&self) -> &str {
"stub"
}
async fn post_message(
&self,
_thread_id: &str,
_message: &AdapterPostableMessage,
) -> Result<SentMessage, ChatError> {
Ok(SentMessage {
id: "m1".into(),
thread_id: "t1".into(),
adapter_name: "stub".into(),
raw: None,
})
}
async fn edit_message(
&self,
_thread_id: &str,
_message_id: &str,
_message: &AdapterPostableMessage,
) -> Result<SentMessage, ChatError> {
Ok(SentMessage {
id: "m1".into(),
thread_id: "t1".into(),
adapter_name: "stub".into(),
raw: None,
})
}
async fn delete_message(
&self,
_thread_id: &str,
_message_id: &str,
) -> Result<(), ChatError> {
Ok(())
}
fn render_card(&self, _card: &CardElement) -> String {
String::new()
}
fn render_message(&self, _message: &AdapterPostableMessage) -> String {
String::new()
}
async fn recv_event(&mut self) -> Option<ChatEvent> {
None
}
}
#[test]
fn object_safe() {
let _adapter: Box<dyn ChatAdapter> = Box::new(StubAdapter);
}
#[tokio::test]
async fn default_optional_methods() {
let adapter = StubAdapter;
let emoji = EmojiValue::from_well_known(crate::emoji::WellKnownEmoji::ThumbsUp);
let err = adapter.add_reaction("t1", "m1", &emoji).await.unwrap_err();
assert!(matches!(err, ChatError::NotSupported(_)));
let err = adapter.remove_reaction("t1", "m1", &emoji).await.unwrap_err();
assert!(matches!(err, ChatError::NotSupported(_)));
let err = adapter.open_dm("u1").await.unwrap_err();
assert!(matches!(err, ChatError::NotSupported(_)));
let err = adapter
.post_ephemeral("t1", "u1", &AdapterPostableMessage::Text("hi".into()))
.await
.unwrap_err();
assert!(matches!(err, ChatError::NotSupported(_)));
let err = adapter
.open_modal(
"trigger",
&ModalElement {
callback_id: "cb".into(),
title: "test".into(),
submit_label: None,
children: vec![],
private_metadata: None,
notify_on_close: false,
},
None,
)
.await
.unwrap_err();
assert!(matches!(err, ChatError::NotSupported(_)));
adapter.start_typing("t1", None).await.expect("start_typing should default to Ok");
let err = adapter
.upload_file(
"t1",
&crate::file::FileUpload {
filename: "f.txt".into(),
mime_type: None,
data: bytes::Bytes::new(),
},
)
.await
.unwrap_err();
assert!(matches!(err, ChatError::NotSupported(_)));
}
#[tokio::test]
async fn stub_required_methods() {
let mut adapter = StubAdapter;
let sent = adapter
.post_message("t1", &AdapterPostableMessage::Text("hello".into()))
.await
.expect("post_message");
assert_eq!(sent.id, "m1");
let sent = adapter
.edit_message("t1", "m1", &AdapterPostableMessage::Text("edited".into()))
.await
.expect("edit_message");
assert_eq!(sent.id, "m1");
adapter.delete_message("t1", "m1").await.expect("delete_message");
assert_eq!(adapter.name(), "stub");
assert!(
adapter
.render_card(&CardElement { title: None, children: vec![], fallback_text: None })
.is_empty()
);
assert!(adapter.render_message(&AdapterPostableMessage::Text("x".into())).is_empty());
let event = adapter.recv_event().await;
assert!(event.is_none());
}
}