use crate::channel::types::{ChannelRef, ConversationRef, SenderRef};
use crate::traits::ChannelMessage;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SenderDmDecision {
Allow,
Pairing,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GroupAccessPolicy {
Open,
Allowlist,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MentionGate {
ExplicitMention,
ReplyToBot,
BotThreadParticipant,
NativeCommand,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct AccessContext {
pub dm_decision: SenderDmDecision,
pub group_policy: GroupAccessPolicy,
pub mention_gate: MentionGate,
pub command_authorized: bool,
#[serde(skip)]
pub delivered_via_upstream_relay: bool,
}
impl Default for AccessContext {
fn default() -> Self {
Self {
dm_decision: SenderDmDecision::Deny,
group_policy: GroupAccessPolicy::Disabled,
mention_gate: MentionGate::None,
command_authorized: false,
delivered_via_upstream_relay: false,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MediaKind {
Image,
Video,
Audio,
Document,
#[default]
Unknown,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct MediaReference {
pub path: Option<String>,
pub url: Option<String>,
pub content_type: Option<String>,
pub kind: MediaKind,
pub transcribed: bool,
pub platform_message_id: Option<String>,
pub local_cache_path: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(default, rename_all = "PascalCase")]
pub struct InboundMediaPayload {
pub media_path: Option<String>,
pub media_url: Option<String>,
pub media_type: Option<String>,
pub media_paths: Option<Vec<String>>,
pub media_urls: Option<Vec<String>>,
pub media_types: Option<Vec<String>>,
pub media_transcribed_indexes: Option<Vec<usize>>,
}
impl InboundMediaPayload {
pub fn from_media(media: &[MediaReference]) -> Self {
let transcribed_indexes: Vec<usize> = media
.iter()
.enumerate()
.filter_map(|(index, item)| item.transcribed.then_some(index))
.collect();
Self {
media_path: media.first().and_then(|m| m.path.clone()),
media_url: media
.first()
.and_then(|m| m.url.clone().or_else(|| m.path.clone())),
media_type: media.first().and_then(media_type),
media_paths: aligned_strings(media.iter().map(|m| m.path.clone()).collect()),
media_urls: aligned_strings(
media
.iter()
.map(|m| m.url.clone().or_else(|| m.path.clone()))
.collect(),
),
media_types: aligned_strings(media.iter().map(media_type).collect()),
media_transcribed_indexes: (!transcribed_indexes.is_empty())
.then_some(transcribed_indexes),
}
}
}
fn media_type(media: &MediaReference) -> Option<String> {
media
.content_type
.clone()
.or_else(|| Some(format!("{:?}", media.kind).to_ascii_lowercase()))
}
fn aligned_strings(values: Vec<Option<String>>) -> Option<Vec<String>> {
values.iter().any(Option::is_some).then(|| {
values
.into_iter()
.map(|value| value.unwrap_or_default())
.collect()
})
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct ChannelInboundEnvelope {
pub channel: ChannelRef,
pub message_id: String,
pub conversation: ConversationRef,
pub sender: SenderRef,
pub text: String,
pub access: AccessContext,
pub media: Vec<MediaReference>,
pub raw: Option<Value>,
}
pub fn inbound_envelope_from_legacy_message(msg: &ChannelMessage) -> ChannelInboundEnvelope {
let thread_ts = msg
.thread_ts
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_owned);
let (thread_id, topic_id) = if msg.channel == "telegram" {
(None, thread_ts)
} else {
(thread_ts, None)
};
ChannelInboundEnvelope {
channel: ChannelRef {
id: msg.channel.clone(),
account_id: None,
},
message_id: msg.id.clone(),
conversation: ConversationRef {
id: msg.reply_target.clone(),
thread_id,
topic_id,
..Default::default()
},
sender: SenderRef {
id: msg.sender.clone(),
..Default::default()
},
text: msg.content.clone(),
..Default::default()
}
}
pub fn legacy_message_from_inbound_envelope(
envelope: &ChannelInboundEnvelope,
timestamp: u64,
) -> ChannelMessage {
ChannelMessage {
id: envelope.message_id.clone(),
sender: envelope.sender.id.clone(),
reply_target: envelope.conversation.id.clone(),
content: envelope.text.clone(),
channel: envelope.channel.id.clone(),
timestamp,
thread_ts: envelope
.conversation
.topic_id
.clone()
.or_else(|| envelope.conversation.thread_id.clone()),
}
}