fix(mattermost): preserve threaded default and docs
This commit is contained in:
parent
58120b1c69
commit
1bfd50bce9
5 changed files with 26 additions and 23 deletions
|
|
@ -26,6 +26,7 @@ url = "https://mm.your-domain.com"
|
||||||
bot_token = "your-bot-access-token"
|
bot_token = "your-bot-access-token"
|
||||||
channel_id = "your-channel-id"
|
channel_id = "your-channel-id"
|
||||||
allowed_users = ["user-id-1", "user-id-2"]
|
allowed_users = ["user-id-1", "user-id-2"]
|
||||||
|
thread_replies = true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Fields
|
### Configuration Fields
|
||||||
|
|
@ -36,12 +37,14 @@ allowed_users = ["user-id-1", "user-id-2"]
|
||||||
| `bot_token` | The Personal Access Token for the bot account. |
|
| `bot_token` | The Personal Access Token for the bot account. |
|
||||||
| `channel_id` | (Optional) The ID of the channel to listen to. Required for `listen` mode. |
|
| `channel_id` | (Optional) The ID of the channel to listen to. Required for `listen` mode. |
|
||||||
| `allowed_users` | (Optional) A list of Mattermost User IDs permitted to interact with the bot. Use `["*"]` to allow everyone. |
|
| `allowed_users` | (Optional) A list of Mattermost User IDs permitted to interact with the bot. Use `["*"]` to allow everyone. |
|
||||||
|
| `thread_replies` | (Optional) Whether top-level user messages should be answered in a thread. Default: `true`. Existing thread replies always remain in-thread. |
|
||||||
|
|
||||||
## Threaded Conversations
|
## Threaded Conversations
|
||||||
|
|
||||||
ZeroClaw automatically supports Mattermost threads.
|
ZeroClaw supports Mattermost threads in both modes:
|
||||||
- If a user sends a message in a thread, ZeroClaw will reply within that same thread.
|
- If a user sends a message in an existing thread, ZeroClaw always replies within that same thread.
|
||||||
- If a user sends a top-level message, ZeroClaw will start a thread by replying to that post.
|
- If `thread_replies = true` (default), top-level messages are answered by threading on that post.
|
||||||
|
- If `thread_replies = false`, top-level messages are answered at channel root level.
|
||||||
|
|
||||||
## Security Note
|
## Security Note
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use super::traits::{Channel, ChannelMessage, SendMessage};
|
use super::traits::{Channel, ChannelMessage, SendMessage};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
/// Mattermost channel — polls channel posts via REST API v4.
|
/// Mattermost channel — polls channel posts via REST API v4.
|
||||||
/// Mattermost is API-compatible with many Slack patterns but uses a dedicated v4 structure.
|
/// Mattermost is API-compatible with many Slack patterns but uses a dedicated v4 structure.
|
||||||
|
|
@ -9,12 +10,12 @@ pub struct MattermostChannel {
|
||||||
bot_token: String,
|
bot_token: String,
|
||||||
channel_id: Option<String>,
|
channel_id: Option<String>,
|
||||||
allowed_users: Vec<String>,
|
allowed_users: Vec<String>,
|
||||||
/// When true, replies thread on the original post's root_id.
|
/// When true (default), replies thread on the original post's root_id.
|
||||||
/// When false (default), replies go to the channel root.
|
/// When false, replies go to the channel root.
|
||||||
thread_replies: bool,
|
thread_replies: bool,
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
/// Handle for the background typing-indicator loop (aborted on stop_typing).
|
/// Handle for the background typing-indicator loop (aborted on stop_typing).
|
||||||
typing_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
|
typing_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MattermostChannel {
|
impl MattermostChannel {
|
||||||
|
|
@ -34,7 +35,7 @@ impl MattermostChannel {
|
||||||
allowed_users,
|
allowed_users,
|
||||||
thread_replies,
|
thread_replies,
|
||||||
client: reqwest::Client::new(),
|
client: reqwest::Client::new(),
|
||||||
typing_handle: std::sync::Mutex::new(None),
|
typing_handle: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,8 +196,10 @@ impl Channel for MattermostChannel {
|
||||||
let base_url = self.base_url.clone();
|
let base_url = self.base_url.clone();
|
||||||
|
|
||||||
// recipient is "channel_id" or "channel_id:root_id"
|
// recipient is "channel_id" or "channel_id:root_id"
|
||||||
let channel_id = recipient.split(':').next().unwrap_or(recipient).to_string();
|
let (channel_id, parent_id) = match recipient.split_once(':') {
|
||||||
let parent_id = recipient.split(':').nth(1).map(String::from);
|
Some((channel, parent)) => (channel.to_string(), Some(parent.to_string())),
|
||||||
|
None => (recipient.to_string(), None),
|
||||||
|
};
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let url = format!("{base_url}/api/v4/users/me/typing");
|
let url = format!("{base_url}/api/v4/users/me/typing");
|
||||||
|
|
@ -225,18 +228,16 @@ impl Channel for MattermostChannel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(mut guard) = self.typing_handle.lock() {
|
let mut guard = self.typing_handle.lock();
|
||||||
*guard = Some(handle);
|
*guard = Some(handle);
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_typing(&self, _recipient: &str) -> Result<()> {
|
async fn stop_typing(&self, _recipient: &str) -> Result<()> {
|
||||||
if let Ok(mut guard) = self.typing_handle.lock() {
|
let mut guard = self.typing_handle.lock();
|
||||||
if let Some(handle) = guard.take() {
|
if let Some(handle) = guard.take() {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -315,8 +316,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mattermost_parse_post_basic() {
|
fn mattermost_parse_post_basic() {
|
||||||
let ch =
|
let ch = MattermostChannel::new("url".into(), "token".into(), None, vec!["*".into()], true);
|
||||||
MattermostChannel::new("url".into(), "token".into(), None, vec!["*".into()], false);
|
|
||||||
let post = json!({
|
let post = json!({
|
||||||
"id": "post123",
|
"id": "post123",
|
||||||
"user_id": "user456",
|
"user_id": "user456",
|
||||||
|
|
@ -330,7 +330,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(msg.sender, "user456");
|
assert_eq!(msg.sender, "user456");
|
||||||
assert_eq!(msg.content, "hello world");
|
assert_eq!(msg.content, "hello world");
|
||||||
assert_eq!(msg.reply_target, "chan789"); // Channel-level reply (thread_replies=false)
|
assert_eq!(msg.reply_target, "chan789:post123"); // Default threaded reply
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1261,7 +1261,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||||
mm.bot_token.clone(),
|
mm.bot_token.clone(),
|
||||||
mm.channel_id.clone(),
|
mm.channel_id.clone(),
|
||||||
mm.allowed_users.clone(),
|
mm.allowed_users.clone(),
|
||||||
mm.thread_replies.unwrap_or(false),
|
mm.thread_replies.unwrap_or(true),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1499,8 +1499,8 @@ pub struct MattermostConfig {
|
||||||
pub channel_id: Option<String>,
|
pub channel_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allowed_users: Vec<String>,
|
pub allowed_users: Vec<String>,
|
||||||
/// When true, replies thread on the original post. When false (default),
|
/// When true (default), replies thread on the original post.
|
||||||
/// replies go to the channel root.
|
/// When false, replies go to the channel root.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub thread_replies: Option<bool>,
|
pub thread_replies: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,7 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) ->
|
||||||
mm.bot_token.clone(),
|
mm.bot_token.clone(),
|
||||||
mm.channel_id.clone(),
|
mm.channel_id.clone(),
|
||||||
mm.allowed_users.clone(),
|
mm.allowed_users.clone(),
|
||||||
mm.thread_replies.unwrap_or(false),
|
mm.thread_replies.unwrap_or(true),
|
||||||
);
|
);
|
||||||
channel.send(&SendMessage::new(output, target)).await?;
|
channel.send(&SendMessage::new(output, target)).await?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue