Skip to main content

cortexfs_channels/
platform.rs

1use std::collections::BTreeMap;
2
3use serde_json::Value;
4
5use crate::{ChannelError, ChannelId, InboundMessage, OutboundMessage, Participant};
6
7pub mod discord;
8pub mod feishu;
9pub mod slack;
10pub mod telegram;
11
12/// Platform-neutral HTTP operation emitted by a webhook codec.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct OutboundRequest {
15    pub method: String,
16    pub path: String,
17    pub content_type: String,
18    pub body: String,
19    pub headers: BTreeMap<String, String>,
20}
21
22/// Stateless codec for one platform's webhook payload and send shape.
23pub trait ChannelCodec: Send + Sync {
24    fn channel(&self) -> ChannelId;
25    fn decode(&self, payload: &str) -> Result<Option<InboundMessage>, ChannelError>;
26    fn encode(&self, message: &OutboundMessage) -> Result<OutboundRequest, ChannelError>;
27    fn challenge(&self, _payload: &str) -> Option<String> {
28        None
29    }
30}
31
32pub(crate) fn object(payload: &str) -> Result<Value, ChannelError> {
33    let value: Value = serde_json::from_str(payload)
34        .map_err(|error| ChannelError::Protocol(format!("invalid webhook JSON: {error}")))?;
35    if value.is_object() {
36        Ok(value)
37    } else {
38        Err(ChannelError::Protocol(
39            "webhook payload is not an object".to_owned(),
40        ))
41    }
42}
43
44pub(crate) fn string(value: Option<&Value>, field: &str) -> Result<String, ChannelError> {
45    value
46        .and_then(Value::as_str)
47        .filter(|text| !text.is_empty())
48        .map(str::to_owned)
49        .ok_or_else(|| ChannelError::Protocol(format!("webhook field `{field}` is missing")))
50}
51
52pub(crate) fn scalar(value: Option<&Value>, field: &str) -> Result<String, ChannelError> {
53    if let Some(value) = value
54        .and_then(Value::as_str)
55        .filter(|value| !value.is_empty())
56    {
57        return Ok(value.to_owned());
58    }
59    value
60        .and_then(Value::as_number)
61        .map(ToString::to_string)
62        .ok_or_else(|| ChannelError::Protocol(format!("webhook field `{field}` is missing")))
63}
64
65pub(crate) fn participant(value: Option<&Value>, id: String) -> Participant {
66    Participant {
67        id,
68        display_name: value
69            .and_then(|item| item.get("display_name").or_else(|| item.get("username")))
70            .and_then(Value::as_str)
71            .map(str::to_owned),
72        handle: value
73            .and_then(|item| item.get("username").or_else(|| item.get("handle")))
74            .and_then(Value::as_str)
75            .map(str::to_owned),
76    }
77}
78
79pub(crate) fn timestamp_ms(value: Option<&Value>) -> Option<u64> {
80    value
81        .and_then(|item| item.as_u64().or_else(|| item.as_str()?.parse().ok()))
82        .map(|seconds| seconds.saturating_mul(1_000))
83}
84
85pub(crate) fn text(value: Option<&Value>) -> Result<crate::MessageBody, ChannelError> {
86    crate::MessageBody::text(string(value, "text")?)
87}