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
29 changes: 26 additions & 3 deletions src/lib/discord-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import type OpenAI from 'openai';
import type { Owner } from '@/lib/integrations/core/types';
import {
getInstallationByGuildId,
getAllInstallationsByGuildId,
getOwnerFromInstallation,
getModel,
} from '@/lib/integrations/discord-service';
Expand Down Expand Up @@ -220,8 +220,9 @@ export async function processDiscordBotMessage(

let cloudAgentSessionId: string | undefined;

const installation = await getInstallationByGuildId(guildId);
if (!installation) {
// Look up all Discord integrations for this guild to detect duplicates
const allInstallations = await getAllInstallationsByGuildId(guildId);
if (allInstallations.length === 0) {
return {
response:
'Error: No Discord integration found for this server. Please install the Kilo Code Discord integration.',
Expand All @@ -232,6 +233,28 @@ export async function processDiscordBotMessage(
};
}

if (allInstallations.length > 1) {
const ownerIds = allInstallations.map(
i => i.owned_by_organization_id ?? i.owned_by_user_id ?? 'unknown'
);
console.warn(
'[DiscordBot] Multiple Discord integrations found for guild:',
guildId,
'owners:',
ownerIds
);
return {
response:
'Multiple integrations found for this message. Please remove duplicate Discord integrations from your Kilo account to resolve this issue.',
modelUsed: '',
toolCallsMade: [],
error: `Multiple Discord integrations found for guild ${guildId}`,
installation: null,
};
}

const [installation] = allInstallations;

const owner = getOwnerFromInstallation(installation);
if (!owner) {
return {
Expand Down
18 changes: 18 additions & 0 deletions src/lib/integrations/discord-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ export async function getInstallationByGuildId(
return integration || null;
}

/**
* Get all Discord installations for a given guild ID.
* Used to detect duplicate integrations for the same Discord server.
*/
export async function getAllInstallationsByGuildId(
guildId: string
): Promise<PlatformIntegration[]> {
return db
.select()
.from(platform_integrations)
.where(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: This query does not filter by integration_status. A suspended or pending integration would count toward the duplicate check, potentially blocking a valid active integration from being used.

Consider adding eq(platform_integrations.integration_status, INTEGRATION_STATUS.ACTIVE) to the and(...) clause. The same applies to the Slack counterpart getAllInstallationsByTeamId.

and(
eq(platform_integrations.platform, PLATFORM.DISCORD),
eq(platform_integrations.platform_installation_id, guildId)
)
);
}

/**
* Get the owner information from a Discord installation
*/
Expand Down
18 changes: 18 additions & 0 deletions src/lib/integrations/slack-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ export async function getInstallationByTeamId(teamId: string): Promise<PlatformI
return integration || null;
}

/**
* Get all Slack installations for a given team ID.
* Used to detect duplicate integrations for the same Slack workspace.
*/
export async function getAllInstallationsByTeamId(
teamId: string
): Promise<PlatformIntegration[]> {
return db
.select()
.from(platform_integrations)
.where(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Same as the Discord counterpart — this query does not filter by integration_status. A suspended integration would be counted as a duplicate, causing the bot to reject messages even if only one integration is actually active.

and(
eq(platform_integrations.platform, PLATFORM.SLACK),
eq(platform_integrations.platform_installation_id, teamId)
)
);
}

/**
* Get the owner information from a Slack installation
*/
Expand Down
30 changes: 26 additions & 4 deletions src/lib/slack-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import type OpenAI from 'openai';
import type { Owner } from '@/lib/integrations/core/types';
import {
getInstallationByTeamId,
getAllInstallationsByTeamId,
getOwnerFromInstallation,
getModel,
getAccessTokenFromInstallation,
Expand Down Expand Up @@ -387,9 +387,9 @@ export async function processKiloBotMessage(
// Track metadata for logging
let cloudAgentSessionId: string | undefined;

// Look up the Slack integration to find the owner
const installation = await getInstallationByTeamId(teamId);
if (!installation) {
// Look up all Slack integrations for this team to detect duplicates
const allInstallations = await getAllInstallationsByTeamId(teamId);
if (allInstallations.length === 0) {
console.error('[SlackBot] No Slack installation found for team:', teamId);
return {
response:
Expand All @@ -401,6 +401,28 @@ export async function processKiloBotMessage(
};
}

if (allInstallations.length > 1) {
const ownerIds = allInstallations.map(
i => i.owned_by_organization_id ?? i.owned_by_user_id ?? 'unknown'
);
console.warn(
'[SlackBot] Multiple Slack integrations found for team:',
teamId,
'owners:',
ownerIds
);
return {
response:
'Multiple integrations found for this message. Please remove duplicate Slack integrations from your Kilo account to resolve this issue.',
modelUsed: '',
toolCallsMade: [],
error: `Multiple Slack integrations found for team ${teamId}`,
installation: null,
};
}

const [installation] = allInstallations;

const owner = getOwnerFromInstallation(installation);
if (!owner) {
console.error('[SlackBot] Could not determine owner from installation:', installation.id);
Expand Down
Loading