use crate::{keyboard::Reply, platform::PlatformKind, Result};
use std::sync::Arc;
#[derive(Clone)]
pub struct Ctx {
pub(crate) inner: Arc<CtxInner>,
}
pub(crate) struct CtxInner {
pub(crate) platform: PlatformKind,
pub(crate) chat_id: String,
pub(crate) user_id: String,
pub(crate) text: String,
pub(crate) reply_fn: ReplyFn,
pub(crate) user_name: Option<String>,
pub(crate) is_reply_to_bot: bool,
pub(crate) is_dm: Option<bool>,
pub(crate) callback_data: Option<String>,
pub(crate) edit_fn: Option<EditFn>,
pub(crate) avatar_fn: Option<UrlFn>,
pub(crate) banner_fn: Option<UrlFn>,
pub(crate) chatinfo_fn: Option<ChatInfoFn>,
pub(crate) typing_fn: Option<TypingFn>,
pub(crate) temp_reply_fn: Option<TempReplyFn>,
pub(crate) user_avatar_fn: Option<UserUrlFn>,
pub(crate) image_fn: Option<ImageFn>,
pub(crate) has_image: bool,
}
pub type ReplyFn =
Box<dyn Fn(Reply) -> futures::future::BoxFuture<'static, Result<()>> + Send + Sync + 'static>;
pub type EditFn =
Arc<dyn Fn(Reply) -> futures::future::BoxFuture<'static, Result<()>> + Send + Sync + 'static>;
pub type UrlFn =
Arc<dyn Fn() -> futures::future::BoxFuture<'static, Result<Option<String>>> + Send + Sync>;
pub type ChatInfoFn =
Arc<dyn Fn() -> futures::future::BoxFuture<'static, Result<ChatInfo>> + Send + Sync>;
pub type TypingFn = Arc<dyn Fn() -> futures::future::BoxFuture<'static, Result<()>> + Send + Sync>;
pub type TempReplyFn =
Arc<dyn Fn(Reply, u64) -> futures::future::BoxFuture<'static, Result<()>> + Send + Sync>;
pub type UserUrlFn = Arc<
dyn Fn(String) -> futures::future::BoxFuture<'static, Result<Option<String>>> + Send + Sync,
>;
pub type ImageFn =
Arc<dyn Fn() -> futures::future::BoxFuture<'static, Result<Option<Vec<u8>>>> + Send + Sync>;
#[derive(Debug, Clone, Default)]
pub struct ChatInfo {
pub id: String,
pub title: Option<String>,
pub member_count: Option<u64>,
pub icon_url: Option<String>,
pub description: Option<String>,
pub is_private: bool,
}
impl Ctx {
pub fn new(
platform: PlatformKind,
chat_id: impl Into<String>,
user_id: impl Into<String>,
text: impl Into<String>,
reply_fn: ReplyFn,
) -> Self {
Self {
inner: Arc::new(CtxInner {
platform,
chat_id: chat_id.into(),
user_id: user_id.into(),
text: text.into(),
reply_fn,
user_name: None,
is_reply_to_bot: false,
is_dm: None,
callback_data: None,
edit_fn: None,
avatar_fn: None,
banner_fn: None,
chatinfo_fn: None,
typing_fn: None,
temp_reply_fn: None,
user_avatar_fn: None,
image_fn: None,
has_image: false,
}),
}
}
pub fn new_full(
platform: PlatformKind,
chat_id: impl Into<String>,
user_id: impl Into<String>,
text: impl Into<String>,
reply_fn: ReplyFn,
is_dm: Option<bool>,
callback_data: Option<String>,
) -> Self {
Self {
inner: Arc::new(CtxInner {
platform,
chat_id: chat_id.into(),
user_id: user_id.into(),
text: text.into(),
reply_fn,
user_name: None,
is_reply_to_bot: false,
is_dm,
callback_data,
edit_fn: None,
avatar_fn: None,
banner_fn: None,
chatinfo_fn: None,
typing_fn: None,
temp_reply_fn: None,
user_avatar_fn: None,
image_fn: None,
has_image: false,
}),
}
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_edit(
platform: PlatformKind,
chat_id: impl Into<String>,
user_id: impl Into<String>,
text: impl Into<String>,
reply_fn: ReplyFn,
is_dm: Option<bool>,
callback_data: Option<String>,
edit_fn: Option<EditFn>,
) -> Self {
Self {
inner: Arc::new(CtxInner {
platform,
chat_id: chat_id.into(),
user_id: user_id.into(),
text: text.into(),
reply_fn,
user_name: None,
is_reply_to_bot: false,
is_dm,
callback_data,
edit_fn,
avatar_fn: None,
banner_fn: None,
chatinfo_fn: None,
typing_fn: None,
temp_reply_fn: None,
user_avatar_fn: None,
image_fn: None,
has_image: false,
}),
}
}
pub fn with_lookups(
mut self,
avatar_fn: Option<UrlFn>,
banner_fn: Option<UrlFn>,
chatinfo_fn: Option<ChatInfoFn>,
) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.avatar_fn = avatar_fn;
inner.banner_fn = banner_fn;
inner.chatinfo_fn = chatinfo_fn;
}
self
}
pub fn with_typing(mut self, typing_fn: Option<TypingFn>) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.typing_fn = typing_fn;
}
self
}
pub fn with_temp_reply(mut self, temp_reply_fn: Option<TempReplyFn>) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.temp_reply_fn = temp_reply_fn;
}
self
}
pub fn with_user_avatar(mut self, user_avatar_fn: Option<UserUrlFn>) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.user_avatar_fn = user_avatar_fn;
}
self
}
pub(crate) fn with_incoming_image(mut self, has: bool, image_fn: Option<ImageFn>) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.has_image = has;
inner.image_fn = image_fn;
}
self
}
pub fn with_user_name(mut self, name: Option<String>) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.user_name = name;
}
self
}
pub fn with_reply_to_bot(mut self, yes: bool) -> Self {
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.is_reply_to_bot = yes;
}
self
}
pub fn is_reply_to_bot(&self) -> bool {
self.inner.is_reply_to_bot
}
pub fn user_name(&self) -> Option<&str> {
self.inner.user_name.as_deref()
}
pub fn platform(&self) -> PlatformKind {
self.inner.platform
}
pub fn chat_id(&self) -> &str {
&self.inner.chat_id
}
pub fn user_id(&self) -> &str {
&self.inner.user_id
}
pub fn text(&self) -> &str {
&self.inner.text
}
pub fn args(&self) -> &str {
self.inner
.text
.split_once(char::is_whitespace)
.map(|(_, rest)| rest.trim())
.unwrap_or("")
}
pub fn is_dm(&self) -> bool {
match self.inner.is_dm {
Some(v) => v,
None => {
matches!(self.inner.platform, PlatformKind::Telegram)
&& self.inner.chat_id == self.inner.user_id
}
}
}
pub fn callback_data(&self) -> Option<&str> {
self.inner.callback_data.as_deref()
}
pub fn is_callback(&self) -> bool {
self.inner.callback_data.is_some()
}
pub async fn reply(&self, text: impl Into<String>) -> Result<()> {
(self.inner.reply_fn)(Reply::text(text)).await
}
pub async fn reply_with(&self, reply: impl Into<Reply>) -> Result<()> {
(self.inner.reply_fn)(reply.into()).await
}
pub async fn edit_reply(&self, reply: impl Into<Reply>) -> Result<()> {
let r = reply.into();
if let Some(edit) = &self.inner.edit_fn {
return (edit)(r).await;
}
(self.inner.reply_fn)(r).await
}
pub async fn avatar_url(&self) -> Result<Option<String>> {
match &self.inner.avatar_fn {
Some(f) => f().await,
None => Ok(None),
}
}
pub async fn avatar_url_of(&self, user_id: &str) -> Result<Option<String>> {
match &self.inner.user_avatar_fn {
Some(f) => f(user_id.to_owned()).await,
None => Ok(None),
}
}
pub async fn banner_url(&self) -> Result<Option<String>> {
match &self.inner.banner_fn {
Some(f) => f().await,
None => Ok(None),
}
}
pub async fn chat_info(&self) -> Result<ChatInfo> {
match &self.inner.chatinfo_fn {
Some(f) => f().await,
None => Ok(ChatInfo {
id: self.inner.chat_id.clone(),
is_private: self.is_dm(),
..Default::default()
}),
}
}
pub async fn typing(&self) {
if let Some(f) = &self.inner.typing_fn {
let _ = f().await;
}
}
pub async fn reply_temporary(&self, reply: impl Into<Reply>, secs: u64) -> Result<()> {
let r = reply.into();
if let Some(f) = &self.inner.temp_reply_fn {
return f(r, secs).await;
}
(self.inner.reply_fn)(r).await
}
pub async fn incoming_image(&self) -> Result<Option<Vec<u8>>> {
match &self.inner.image_fn {
Some(f) => f().await,
None => Ok(None),
}
}
pub fn has_incoming_image(&self) -> bool {
self.inner.has_image
}
}
#[cfg(test)]
mod tests {
use super::*;
fn noop_reply() -> ReplyFn {
Box::new(|_| Box::pin(async { Ok(()) }))
}
#[tokio::test]
async fn no_incoming_image_by_default() {
let ctx = Ctx::new(PlatformKind::Telegram, "1", "2", "hi", noop_reply());
assert!(!ctx.has_incoming_image());
assert!(ctx.incoming_image().await.unwrap().is_none());
}
#[tokio::test]
async fn incoming_image_capability_resolves() {
let lookup: ImageFn = Arc::new(|| Box::pin(async { Ok(Some(vec![1, 2, 3])) }));
let ctx = Ctx::new(PlatformKind::Discord, "1", "2", "", noop_reply())
.with_incoming_image(true, Some(lookup));
assert!(ctx.has_incoming_image());
assert_eq!(ctx.incoming_image().await.unwrap(), Some(vec![1, 2, 3]));
}
}