Skip to content

Commit f612ed3

Browse files
cln-ioclaude
andcommitted
feat: add automatic channel joining for bot accounts
Bot accounts need to be channel members to access channel information. This update detects 403 permission errors and automatically joins the bot to the target channel if it's not already a member. Changes: - Add isChannelMember() method to check bot membership status - Add joinChannel() method to automatically join bot to channels - Update bridge startup to detect 403 errors and auto-join target channel - Add comprehensive logging for bot membership operations - Improve error messages for permission-related issues Fixes issue where bot accounts could not access target channels they weren't members of, resulting in 403 Forbidden errors on startup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9091b2b commit f612ed3

File tree

2 files changed

+68
-5
lines changed

2 files changed

+68
-5
lines changed

src/bridge.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,41 @@ export class MattermostBridge {
9898

9999
// Resolve target channel info
100100
console.log(`${this.LOG_PREFIX} ${emoji('🔍')}[${this.config.right.name}] Looking up channel (${this.targetChannelId})[${this.targetChannelId}]`.trim());
101-
this.targetChannelInfo = await this.rightClient.getChannelById(this.targetChannelId);
102-
101+
102+
// Check if we can access the channel, and if not (403 error), try to join it first
103+
try {
104+
this.targetChannelInfo = await this.rightClient.getChannelById(this.targetChannelId);
105+
} catch (error: any) {
106+
if (error.response?.status === 403) {
107+
console.log(`${this.LOG_PREFIX} ${emoji('🔒')}[${this.config.right.name}] Permission denied - checking if bot needs to join channel...`.trim());
108+
109+
// Check if bot is a member
110+
const isMember = await this.rightClient.isChannelMember(this.targetChannelId);
111+
112+
if (!isMember) {
113+
console.log(`${this.LOG_PREFIX} ${emoji('🤖')}[${this.config.right.name}] Bot is not a member - attempting to join channel [${this.targetChannelId}]...`.trim());
114+
const joined = await this.rightClient.joinChannel(this.targetChannelId);
115+
116+
if (!joined) {
117+
throw new Error(`Failed to join target channel '${this.targetChannelId}' on ${this.config.right.name}. Bot may need to be manually added to the channel.`);
118+
}
119+
120+
// Try to get channel info again after joining
121+
this.targetChannelInfo = await this.rightClient.getChannelById(this.targetChannelId);
122+
} else {
123+
// Bot is a member but still got 403 - something else is wrong
124+
throw error;
125+
}
126+
} else {
127+
// Not a 403 error, re-throw
128+
throw error;
129+
}
130+
}
131+
103132
if (!this.targetChannelInfo) {
104133
throw new Error(`Target channel '${this.targetChannelId}' not found on ${this.config.right.name}`);
105134
}
106-
135+
107136
console.log(`${this.LOG_PREFIX} ${emoji('✅')}[${this.config.right.name}] Found channel: (${this.targetChannelInfo.name})[${this.targetChannelId}]`.trim());
108137
console.log('');
109138

src/mattermost-client.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export class MattermostClient {
251251
try {
252252
const response = await this.api.get(`/channels/${channelId}`);
253253
const channel = response.data;
254-
254+
255255
const channelInfo: ChannelInfo = {
256256
id: channel.id,
257257
name: channel.name,
@@ -261,7 +261,7 @@ export class MattermostClient {
261261

262262
// Cache the result
263263
this.channelCache.set(channelId, channelInfo);
264-
264+
265265
return channelInfo;
266266
} catch (error: any) {
267267
if (error.response?.status === 404) {
@@ -270,11 +270,45 @@ export class MattermostClient {
270270
this.channelCache.set(channelId, null as any);
271271
return null;
272272
}
273+
if (error.response?.status === 403) {
274+
console.warn(`${this.LOG_PREFIX} ${emoji('🔒')}[${this.config.name}] Permission denied accessing channel (unknown)[${channelId}] - bot may not be a member`.trim());
275+
throw error;
276+
}
273277
console.error(`${this.LOG_PREFIX} ${emoji('❌')}[${this.config.name}] Error getting channel (unknown)[${channelId}]:`.trim(), error.response?.data || error.message);
274278
throw error;
275279
}
276280
}
277281

282+
async isChannelMember(channelId: string, userId?: string): Promise<boolean> {
283+
try {
284+
const userIdToCheck = userId || this.userId;
285+
const response = await this.api.get(`/channels/${channelId}/members/${userIdToCheck}`);
286+
console.log(`${this.LOG_PREFIX} ${emoji('✅')}[${this.config.name}] User (${userIdToCheck}) is a member of channel [${channelId}]`.trim());
287+
return true;
288+
} catch (error: any) {
289+
if (error.response?.status === 404 || error.response?.status === 403) {
290+
console.log(`${this.LOG_PREFIX} ${emoji('❌')}[${this.config.name}] User (${userId || this.userId}) is NOT a member of channel [${channelId}]`.trim());
291+
return false;
292+
}
293+
console.error(`${this.LOG_PREFIX} ${emoji('❌')}[${this.config.name}] Error checking channel membership:`.trim(), error.response?.data || error.message);
294+
throw error;
295+
}
296+
}
297+
298+
async joinChannel(channelId: string): Promise<boolean> {
299+
try {
300+
console.log(`${this.LOG_PREFIX} ${emoji('🚪')}[${this.config.name}] Attempting to join channel [${channelId}]...`.trim());
301+
await this.api.post(`/channels/${channelId}/members`, {
302+
user_id: this.userId
303+
});
304+
console.log(`${this.LOG_PREFIX} ${emoji('✅')}[${this.config.name}] Successfully joined channel [${channelId}]`.trim());
305+
return true;
306+
} catch (error: any) {
307+
console.error(`${this.LOG_PREFIX} ${emoji('❌')}[${this.config.name}] Failed to join channel [${channelId}]:`.trim(), error.response?.data || error.message);
308+
return false;
309+
}
310+
}
311+
278312
async getChannelByName(channelName: string): Promise<Channel | null> {
279313
try {
280314
// First get teams

0 commit comments

Comments
 (0)