Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/aionui-api-types/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct TestPluginExtraConfig {
pub app_id: Option<String>,
#[serde(default)]
pub app_secret: Option<String>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_json::Value>,
}

// ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion crates/aionui-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ name = "aioncore"
path = "src/main.rs"

[features]
default = ["telegram", "lark", "dingtalk", "weixin"]
default = ["telegram", "lark", "dingtalk", "weixin", "mattermost"]
telegram = ["aionui-channel/telegram"]
lark = ["aionui-channel/lark"]
dingtalk = ["aionui-channel/dingtalk"]
weixin = ["aionui-channel/weixin"]
mattermost = ["aionui-channel/mattermost"]

[dependencies]
aionui-common.workspace = true
Expand Down
17 changes: 13 additions & 4 deletions crates/aionui-app/tests/channel_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,20 @@ async fn get_plugins_empty() {
let json = body_json(resp).await;
assert!(json["success"].as_bool().unwrap());
let data = json["data"].as_array().unwrap();
assert_eq!(data.len(), 7);
assert_eq!(data.len(), 8);
let types: std::collections::HashSet<_> = data.iter().filter_map(|item| item["type"].as_str()).collect();
assert_eq!(
types,
std::collections::HashSet::from(["telegram", "lark", "dingtalk", "slack", "discord", "weixin", "wecom",])
std::collections::HashSet::from([
"telegram",
"lark",
"dingtalk",
"slack",
"discord",
"weixin",
"wecom",
"mattermost",
])
);
assert!(data.iter().all(|item| item["enabled"] == false));
}
Expand Down Expand Up @@ -548,7 +557,7 @@ async fn enable_disable_plugin_lifecycle() {
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
let plugins = json["data"].as_array().unwrap();
assert_eq!(plugins.len(), 7);
assert_eq!(plugins.len(), 8);
let telegram = plugins
.iter()
.find(|plugin| plugin["plugin_id"] == "telegram")
Expand All @@ -575,7 +584,7 @@ async fn enable_disable_plugin_lifecycle() {
let resp = app.oneshot(req).await.unwrap();
let json = body_json(resp).await;
let plugins = json["data"].as_array().unwrap();
assert_eq!(plugins.len(), 7);
assert_eq!(plugins.len(), 8);
let telegram = plugins
.iter()
.find(|plugin| plugin["plugin_id"] == "telegram")
Expand Down
1 change: 1 addition & 0 deletions crates/aionui-channel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ telegram = ["dep:reqwest"]
lark = ["dep:reqwest", "dep:tokio-tungstenite", "dep:futures-util", "dep:prost", "dep:rustls", "dep:rustls-native-certs"]
dingtalk = ["dep:reqwest", "dep:tokio-tungstenite", "dep:futures-util", "dep:rustls", "dep:rustls-native-certs"]
weixin = ["dep:reqwest", "dep:futures-util", "dep:base64", "dep:uuid"]
mattermost = ["dep:reqwest", "dep:tokio-tungstenite", "dep:futures-util", "dep:rustls", "dep:rustls-native-certs"]

[dependencies]
aionui-common.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/aionui-channel/src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub fn format_text_for_platform(text: &str, platform: PluginType) -> String {
match platform {
PluginType::Telegram => markdown_to_telegram_html(text),
PluginType::Lark | PluginType::Dingtalk => html_to_markdown(text),
PluginType::Mattermost => strip_tags_loop(text),
PluginType::Weixin => strip_html(text),
_ => escape_html(text),
}
Expand Down
2 changes: 2 additions & 0 deletions crates/aionui-channel/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ impl ChannelManager {
PluginType::Lark => "Lark Bot".into(),
PluginType::Dingtalk => "DingTalk Bot".into(),
PluginType::Weixin => "WeChat Bot".into(),
PluginType::Mattermost => "Mattermost".into(),
PluginType::Slack => "Slack Bot".into(),
PluginType::Discord => "Discord Bot".into(),
}
Expand Down Expand Up @@ -1440,6 +1441,7 @@ mod tests {
assert_eq!(mgr.default_plugin_name(PluginType::Lark), "Lark Bot");
assert_eq!(mgr.default_plugin_name(PluginType::Dingtalk), "DingTalk Bot");
assert_eq!(mgr.default_plugin_name(PluginType::Weixin), "WeChat Bot");
assert_eq!(mgr.default_plugin_name(PluginType::Mattermost), "Mattermost");
assert_eq!(mgr.default_plugin_name(PluginType::Slack), "Slack Bot");
assert_eq!(mgr.default_plugin_name(PluginType::Discord), "Discord Bot");
}
Expand Down
4 changes: 3 additions & 1 deletion crates/aionui-channel/src/message_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ fn platform_to_source(platform: PluginType) -> ConversationSource {
PluginType::Dingtalk => ConversationSource::Dingtalk,
PluginType::Weixin => ConversationSource::Weixin,
// Reserved variants default to Aionui
PluginType::Slack | PluginType::Discord => ConversationSource::Aionui,
PluginType::Mattermost | PluginType::Slack | PluginType::Discord => ConversationSource::Aionui,
}
}

Expand Down Expand Up @@ -377,6 +377,7 @@ fn channel_conversation_name(
PluginType::Lark => "lark",
PluginType::Dingtalk => "ding",
PluginType::Weixin => "wx",
PluginType::Mattermost => "mm",
PluginType::Slack => "slack",
PluginType::Discord => "discord",
};
Expand Down Expand Up @@ -430,6 +431,7 @@ mod tests {

#[test]
fn platform_to_source_reserved_defaults_to_aionui() {
assert_eq!(platform_to_source(PluginType::Mattermost), ConversationSource::Aionui);
assert_eq!(platform_to_source(PluginType::Slack), ConversationSource::Aionui);
assert_eq!(platform_to_source(PluginType::Discord), ConversationSource::Aionui);
}
Expand Down
111 changes: 111 additions & 0 deletions crates/aionui-channel/src/plugins/mattermost/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use reqwest::{Client, StatusCode};

use crate::error::ChannelError;

use super::types::{CreatePostRequest, MattermostUser, PatchPostRequest};

#[derive(Clone)]
pub(crate) struct MattermostApi {
client: Client,
server_url: String,
access_token: String,
}

impl MattermostApi {
pub fn new(client: Client, server_url: String, access_token: String) -> Self {
Self {
client,
server_url,
access_token,
}
}

pub async fn get_me(&self) -> Result<MattermostUser, ChannelError> {
let url = format!("{}/api/v4/users/me", self.server_url);
let response = self
.client
.get(url)
.bearer_auth(&self.access_token)
.send()
.await
.map_err(|e| ChannelError::ConnectionFailed(format!("Mattermost user request failed: {e}")))?;

let status = response.status();
if !status.is_success() {
return Err(mattermost_status_error("Mattermost user request failed", status));
}

response
.json()
.await
.map_err(|e| ChannelError::PlatformApi(format!("Mattermost user response parse failed: {e}")))
}

pub async fn create_post(&self, req: &CreatePostRequest) -> Result<String, ChannelError> {
let url = format!("{}/api/v4/posts", self.server_url);
let response = self
.client
.post(url)
.bearer_auth(&self.access_token)
.json(req)
.send()
.await
.map_err(|e| ChannelError::MessageSendFailed(format!("Mattermost post request failed: {e}")))?;

let status = response.status();
if !status.is_success() {
return Err(ChannelError::MessageSendFailed(format!(
"Mattermost post request failed with status {status}"
)));
}

let value: serde_json::Value = response
.json()
.await
.map_err(|e| ChannelError::MessageSendFailed(format!("Mattermost post response parse failed: {e}")))?;
value["id"]
.as_str()
.map(ToOwned::to_owned)
.ok_or_else(|| ChannelError::MessageSendFailed("Mattermost post response missing id".into()))
}

pub async fn patch_post(&self, post_id: &str, req: &PatchPostRequest) -> Result<(), ChannelError> {
let url = format!("{}/api/v4/posts/{post_id}/patch", self.server_url);
let response = self
.client
.put(url)
.bearer_auth(&self.access_token)
.json(req)
.send()
.await
.map_err(|e| ChannelError::MessageSendFailed(format!("Mattermost post patch request failed: {e}")))?;

let status = response.status();
if !status.is_success() {
return Err(ChannelError::MessageSendFailed(format!(
"Mattermost post patch request failed with status {status}"
)));
}

Ok(())
}

pub fn websocket_url(&self) -> String {
let ws_base = if let Some(rest) = self.server_url.strip_prefix("https://") {
format!("wss://{rest}")
} else if let Some(rest) = self.server_url.strip_prefix("http://") {
format!("ws://{rest}")
} else {
self.server_url.clone()
};
format!("{ws_base}/api/v4/websocket")
}

pub fn access_token(&self) -> &str {
&self.access_token
}
}

fn mattermost_status_error(context: &str, status: StatusCode) -> ChannelError {
ChannelError::ConnectionFailed(format!("{context} with status {status}"))
}
5 changes: 5 additions & 0 deletions crates/aionui-channel/src/plugins/mattermost/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod api;
mod plugin;
mod types;

pub use plugin::MattermostPlugin;
Loading