Skip to content

Commit 5302fa7

Browse files
feat: implement remaining OpenClaw parity — FetchBans, message chunking, allowBots, reactionAllowlist, responsPrefix
- Add ReactionMode::Allowlist variant and reaction_allowlist config (DISCORD_REACTION_ALLOWLIST) - Add allow_bots config field (DISCORD_ALLOW_BOTS) to process bot messages - Add response_prefix config (DISCORD_RESPONSE_PREFIX) prepended to outbound messages - FetchedBan type + FetchBansCommand + fetch_bans NATS subject + handle_fetch_bans handler - Message chunking in handle_send_messages: splits content >2000 chars at newlines/spaces, applies response_prefix to first chunk, sends embeds/components/files on last chunk - Fix outbound_tests.rs to pass pairing_state and response_prefix to OutboundProcessor::new - Fix SendMessageCommand test literals to include as_voice field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f97641d commit 5302fa7

9 files changed

Lines changed: 255 additions & 39 deletions

File tree

rsworkspace/crates/discord-bot/src/bridge.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,12 @@ pub struct DiscordBridge {
181181
pub pairing_state: Arc<PairingState>,
182182
/// Controls which reaction events are forwarded to NATS.
183183
pub reaction_mode: crate::config::ReactionMode,
184+
/// User IDs allowed to trigger reaction events when reaction_mode = allowlist.
185+
pub reaction_allowlist: Vec<u64>,
184186
/// Optional emoji to react with when a message is received and will be processed.
185187
pub ack_reaction: Option<String>,
188+
/// Whether to process messages sent by other bots.
189+
pub allow_bots: bool,
186190
}
187191

188192
impl TypeMapKey for DiscordBridge {
@@ -198,7 +202,9 @@ impl DiscordBridge {
198202
presence_enabled: bool,
199203
guild_commands_guild_id: Option<u64>,
200204
reaction_mode: crate::config::ReactionMode,
205+
reaction_allowlist: Vec<u64>,
201206
ack_reaction: Option<String>,
207+
allow_bots: bool,
202208
) -> Self {
203209
Self {
204210
publisher: MessagePublisher::new(client, prefix),
@@ -209,7 +215,9 @@ impl DiscordBridge {
209215
guild_commands_guild_id,
210216
pairing_state: Arc::new(PairingState::new()),
211217
reaction_mode,
218+
reaction_allowlist,
212219
ack_reaction,
220+
allow_bots,
213221
}
214222
}
215223

@@ -220,12 +228,16 @@ impl DiscordBridge {
220228

221229
/// Returns true if reaction events should be forwarded based on the configured mode.
222230
///
223-
/// `is_bot_message` — whether the message the reaction was added to was sent by this bot.
224-
pub fn should_publish_reaction(&self, is_bot_message: bool) -> bool {
231+
/// `is_bot_message` — whether the message was sent by this bot.
232+
/// `reactor_user_id` — the user who added/removed the reaction.
233+
pub fn should_publish_reaction(&self, is_bot_message: bool, reactor_user_id: Option<u64>) -> bool {
225234
match self.reaction_mode {
226235
crate::config::ReactionMode::Off => false,
227236
crate::config::ReactionMode::Own => is_bot_message,
228237
crate::config::ReactionMode::All => true,
238+
crate::config::ReactionMode::Allowlist => reactor_user_id
239+
.map(|id| self.reaction_allowlist.contains(&id))
240+
.unwrap_or(false),
229241
}
230242
}
231243

rsworkspace/crates/discord-bot/src/config.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub enum ReactionMode {
4646
Own,
4747
/// Publish all reaction events (default)
4848
All,
49+
/// Only publish reactions from users in `reaction_allowlist`
50+
Allowlist,
4951
}
5052

5153
impl Default for ReactionMode {
@@ -73,10 +75,19 @@ pub struct DiscordBotConfig {
7375
/// Controls which reaction events are forwarded to NATS
7476
#[serde(default)]
7577
pub reaction_mode: ReactionMode,
78+
/// User IDs allowed to trigger reaction events when `reaction_mode = allowlist`
79+
#[serde(default)]
80+
pub reaction_allowlist: Vec<u64>,
7681
/// Optional emoji to react with when a message is received and will be processed.
7782
/// Use a unicode emoji (e.g. "👀") or omit to disable.
7883
#[serde(default)]
7984
pub ack_reaction: Option<String>,
85+
/// Whether to process messages sent by other bots (default: false)
86+
#[serde(default)]
87+
pub allow_bots: bool,
88+
/// Optional string prepended to every outbound message sent by the bot
89+
#[serde(default)]
90+
pub response_prefix: Option<String>,
8091
}
8192

8293
impl Config {
@@ -155,18 +166,32 @@ impl Config {
155166
{
156167
"off" => ReactionMode::Off,
157168
"own" => ReactionMode::Own,
169+
"allowlist" => ReactionMode::Allowlist,
158170
_ => ReactionMode::All,
159171
};
160172

173+
let reaction_allowlist =
174+
parse_id_list(&env.var("DISCORD_REACTION_ALLOWLIST").unwrap_or_default());
175+
161176
let ack_reaction = env.var("DISCORD_ACK_REACTION").filter(|s| !s.is_empty());
162177

178+
let allow_bots = env
179+
.var_or("DISCORD_ALLOW_BOTS", "false")
180+
.to_lowercase()
181+
== "true";
182+
183+
let response_prefix = env.var("DISCORD_RESPONSE_PREFIX").filter(|s| !s.is_empty());
184+
163185
Ok(Config {
164186
discord: DiscordBotConfig {
165187
bot_token,
166188
presence_enabled,
167189
guild_commands_guild_id,
168190
reaction_mode,
191+
reaction_allowlist,
169192
ack_reaction,
193+
allow_bots,
194+
response_prefix,
170195
access: AccessConfig {
171196
dm_policy,
172197
guild_policy,

rsworkspace/crates/discord-bot/src/handlers/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,13 @@ impl EventHandler for Handler {
100100
}
101101

102102
async fn message(&self, ctx: Context, msg: Message) {
103-
// Skip bot messages
103+
// Skip bot messages unless allow_bots is enabled
104104
if msg.author.bot {
105-
return;
105+
let data = ctx.data.read().await;
106+
let allow = data.get::<DiscordBridge>().map(|b| b.allow_bots).unwrap_or(false);
107+
if !allow {
108+
return;
109+
}
106110
}
107111

108112
let bridge = {
@@ -338,7 +342,8 @@ impl EventHandler for Handler {
338342
let is_bot_msg = add_reaction.message_author_id
339343
.map(|author| author.get() == bridge.bot_user_id())
340344
.unwrap_or(false);
341-
if !bridge.should_publish_reaction(is_bot_msg) {
345+
let reactor_id = add_reaction.user_id.map(|u| u.get());
346+
if !bridge.should_publish_reaction(is_bot_msg, reactor_id) {
342347
return;
343348
}
344349

@@ -378,7 +383,8 @@ impl EventHandler for Handler {
378383
let is_bot_msg = removed_reaction.message_author_id
379384
.map(|author| author.get() == bridge.bot_user_id())
380385
.unwrap_or(false);
381-
if !bridge.should_publish_reaction(is_bot_msg) {
386+
let reactor_id = removed_reaction.user_id.map(|u| u.get());
387+
if !bridge.should_publish_reaction(is_bot_msg, reactor_id) {
382388
return;
383389
}
384390

rsworkspace/crates/discord-bot/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ async fn main() -> Result<()> {
120120
config.discord.presence_enabled,
121121
config.discord.guild_commands_guild_id,
122122
config.discord.reaction_mode.clone(),
123+
config.discord.reaction_allowlist.clone(),
123124
config.discord.ack_reaction.clone(),
125+
config.discord.allow_bots,
124126
));
125127

126128
// Extract pairing_state before bridge is moved into TypeMap so it can be
@@ -183,6 +185,7 @@ async fn main() -> Result<()> {
183185
prefix_for_outbound,
184186
Some(shard_manager_for_outbound),
185187
pairing_state,
188+
config.discord.response_prefix.clone(),
186189
);
187190
if let Err(e) = processor.run().await {
188191
error!("Outbound processor error: {}", e);

0 commit comments

Comments
 (0)