Skip to content

Commit e28d4af

Browse files
feat(api-service,js,react): Telegram subscriber-link SDK + shared linking module fixes NV-8095 (#11619)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: George Djabarov <djabarovgeorge@users.noreply.github.com>
1 parent 1e03227 commit e28d4af

63 files changed

Lines changed: 2073 additions & 506 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/src/app/agents/agents.module.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,10 @@ import { EventsModule } from '../events/events.module';
2929
import { IntegrationModule } from '../integrations/integrations.module';
3030
import { KeylessModule } from '../keyless/keyless.module';
3131
import { SharedModule } from '../shared/shared.module';
32+
import { TelegramLinkingModule } from '../telegram-linking/telegram-linking.module';
3233
import { AgentConfigResolver } from './channels/agent-config-resolver.service';
3334
import { AgentIntegrationsController } from './channels/integrations/agent-integrations.controller';
34-
import { AgentsPublicController } from './channels/telegram-linking/agents-public.controller';
35-
import { TelegramMobileLinkTokenService } from './channels/telegram-linking/telegram-mobile-link-token.service';
36-
import { TelegramStartCodeService } from './channels/telegram-linking/telegram-start-code.service';
35+
import { AgentsPublicController } from './channels/slack-linking/agents-public.controller';
3736
import { InboundAckService } from './conversation-runtime/ack/inbound-ack.service';
3837
import { AgentActionTokenService } from './conversation-runtime/action-token/agent-action-token.service';
3938
import { AgentAttachmentStorage } from './conversation-runtime/conversation/agent-attachment-storage.service';
@@ -84,6 +83,7 @@ import { USE_CASES } from './usecases';
8483
ChannelEndpointsModule,
8584
ConnectModule,
8685
KeylessModule,
86+
TelegramLinkingModule,
8787
forwardRef(() => IntegrationModule),
8888
],
8989
controllers: [
@@ -144,8 +144,6 @@ import { USE_CASES } from './usecases';
144144
AgentEmailSender,
145145
OutboundGateway,
146146
McpOAuthDiscoveryService,
147-
TelegramMobileLinkTokenService,
148-
TelegramStartCodeService,
149147
CalculateLimitNovuIntegration,
150148
CalculateDemoClaudeQuota,
151149
CreateOrUpdateSubscriberUseCase,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { AgentConfigResolver } from './agent-config-resolver.service';
22
export { AgentIntegrationsController } from './integrations/agent-integrations.controller';
3-
export { AgentsPublicController } from './telegram-linking/agents-public.controller';
3+
export { AgentsPublicController } from './slack-linking/agents-public.controller';

apps/api/src/app/agents/channels/integrations/add-agent-integration/add-agent-integration.usecase.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@novu/dal';
2121
import {
2222
ApiServiceLevelEnum,
23+
ChatProviderIdEnum,
2324
EmailProviderIdEnum,
2425
EnvironmentTypeEnum,
2526
FeatureNameEnum,
@@ -144,6 +145,21 @@ export class AddAgentIntegration {
144145
throw new ConflictException('This integration is already linked to the agent.');
145146
}
146147

148+
if (integration.providerId === ChatProviderIdEnum.Telegram) {
149+
const linkedElsewhere = await this.agentIntegrationRepository.findOne(
150+
{
151+
_integrationId: integration._id,
152+
_environmentId: command.environmentId,
153+
_organizationId: command.organizationId,
154+
},
155+
['_agentId']
156+
);
157+
158+
if (linkedElsewhere && linkedElsewhere._agentId !== agent._id) {
159+
throw new ConflictException('Integration is already linked to a different agent');
160+
}
161+
}
162+
147163
// Revives a tombstoned (disconnected) link when one exists for this pair —
148164
// a plain create would violate the unique (_agentId, _integrationId) index.
149165
const link = await this.agentIntegrationRepository.createOrReviveLink({

apps/api/src/app/agents/channels/integrations/agent-integrations.controller.ts

Lines changed: 0 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,8 @@ import {
4646
UpdateAgentInboxSharedRequestDto,
4747
UpdateAgentIntegrationRequestDto,
4848
} from '../../shared/dtos';
49-
import { ConfigureTelegramWebhookResponseDto } from '../../shared/dtos/configure-telegram-webhook-response.dto';
5049
import { ConfigureWhatsAppWebhookResponseDto } from '../../shared/dtos/configure-whatsapp-webhook-response.dto';
5150
import { IssueSlackSetupLinkResponseDto } from '../../shared/dtos/issue-slack-setup-link-response.dto';
52-
import { IssueTelegramMobileLinkRequestDto } from '../../shared/dtos/issue-telegram-mobile-link-request.dto';
53-
import { IssueTelegramMobileLinkResponseDto } from '../../shared/dtos/issue-telegram-mobile-link-response.dto';
54-
import { IssueTelegramSubscriberLinkRequestDto } from '../../shared/dtos/issue-telegram-subscriber-link-request.dto';
55-
import { IssueTelegramSubscriberLinkResponseDto } from '../../shared/dtos/issue-telegram-subscriber-link-response.dto';
5651
import { SendAgentTestEmailRequestDto } from '../../shared/dtos/send-agent-test-email-request.dto';
5752
import { SendAgentWelcomeMessageRequestDto } from '../../shared/dtos/send-agent-welcome-message-request.dto';
5853
import {
@@ -61,12 +56,6 @@ import {
6156
} from '../../shared/dtos/send-whatsapp-test-template.dto';
6257
import { IssueSlackSetupLinkCommand } from '../slack-linking/issue-slack-setup-link/issue-slack-setup-link.command';
6358
import { IssueSlackSetupLink } from '../slack-linking/issue-slack-setup-link/issue-slack-setup-link.usecase';
64-
import { ConfigureTelegramAgentWebhookCommand } from '../telegram/configure-telegram-agent-webhook/configure-telegram-agent-webhook.command';
65-
import { ConfigureTelegramAgentWebhook } from '../telegram/configure-telegram-agent-webhook/configure-telegram-agent-webhook.usecase';
66-
import { IssueTelegramMobileLinkCommand } from '../telegram-linking/issue-telegram-mobile-link/issue-telegram-mobile-link.command';
67-
import { IssueTelegramMobileLink } from '../telegram-linking/issue-telegram-mobile-link/issue-telegram-mobile-link.usecase';
68-
import { IssueTelegramSubscriberLinkCommand } from '../telegram-linking/issue-telegram-subscriber-link/issue-telegram-subscriber-link.command';
69-
import { IssueTelegramSubscriberLink } from '../telegram-linking/issue-telegram-subscriber-link/issue-telegram-subscriber-link.usecase';
7059
import { ConfigureWhatsAppWebhookCommand } from '../whatsapp/configure-whatsapp-webhook/configure-whatsapp-webhook.command';
7160
import { ConfigureWhatsAppWebhook } from '../whatsapp/configure-whatsapp-webhook/configure-whatsapp-webhook.usecase';
7261
import { SendWhatsAppTestTemplateCommand } from '../whatsapp/send-whatsapp-test-template/send-whatsapp-test-template.command';
@@ -96,10 +85,7 @@ export class AgentIntegrationsController {
9685
private readonly sendAgentWelcomeMessageUsecase: SendAgentWelcomeMessage,
9786
private readonly configureWhatsAppWebhookUsecase: ConfigureWhatsAppWebhook,
9887
private readonly sendWhatsAppTestTemplateUsecase: SendWhatsAppTestTemplate,
99-
private readonly configureTelegramAgentWebhookUsecase: ConfigureTelegramAgentWebhook,
100-
private readonly issueTelegramMobileLinkUsecase: IssueTelegramMobileLink,
10188
private readonly issueSlackSetupLinkUsecase: IssueSlackSetupLink,
102-
private readonly issueTelegramSubscriberLinkUsecase: IssueTelegramSubscriberLink,
10389
private readonly updateAgentInboxSharedUsecase: UpdateAgentInboxShared
10490
) {}
10591

@@ -365,70 +351,6 @@ export class AgentIntegrationsController {
365351
);
366352
}
367353

368-
@Post('/:identifier/integrations/:integrationId/telegram/configure')
369-
@ExternalApiAccessible()
370-
@KeylessAccessible()
371-
@HttpCode(HttpStatus.OK)
372-
@ApiResponse(ConfigureTelegramWebhookResponseDto, 200)
373-
@ApiOperation({
374-
summary: 'Configure Telegram bot webhook',
375-
description: `Registers the Novu agent webhook URL with Telegram for the specified integration,
376-
generates a cryptographic secret token for webhook verification,
377-
and persists it on the integration. Re-running rotates the secret.`,
378-
})
379-
@ApiNotFoundResponse({
380-
description: 'The agent, integration, or agent-integration link was not found.',
381-
})
382-
@RequirePermissions(PermissionsEnum.AGENT_WRITE)
383-
updateTelegramWebhook(
384-
@UserSession() user: UserSessionData,
385-
@Param('identifier') identifier: string,
386-
@Param('integrationId') integrationId: string
387-
): Promise<ConfigureTelegramWebhookResponseDto> {
388-
return this.configureTelegramAgentWebhookUsecase.execute(
389-
ConfigureTelegramAgentWebhookCommand.create({
390-
userId: user._id,
391-
environmentId: user.environmentId,
392-
organizationId: user.organizationId,
393-
agentIdentifier: identifier,
394-
integrationId,
395-
})
396-
);
397-
}
398-
399-
@Post('/:identifier/integrations/:integrationId/telegram/mobile-link')
400-
@ExternalApiAccessible()
401-
@KeylessAccessible()
402-
@HttpCode(HttpStatus.OK)
403-
@ApiResponse(IssueTelegramMobileLinkResponseDto, 200)
404-
@ApiOperation({
405-
summary: 'Issue a short-lived Telegram mobile setup link',
406-
description:
407-
'Issues a signed, single-use link (TTL = 5 minutes) that can be opened on a mobile device to finish ' +
408-
'configuring a Telegram bot without re-authenticating. Telegram-only.',
409-
})
410-
@ApiNotFoundResponse({
411-
description: 'The agent, integration, or agent-integration link was not found.',
412-
})
413-
@RequirePermissions(PermissionsEnum.AGENT_WRITE)
414-
createTelegramMobileLink(
415-
@UserSession() user: UserSessionData,
416-
@Param('identifier') identifier: string,
417-
@Param('integrationId') integrationId: string,
418-
@Body() body?: IssueTelegramMobileLinkRequestDto
419-
): Promise<IssueTelegramMobileLinkResponseDto> {
420-
return this.issueTelegramMobileLinkUsecase.execute(
421-
IssueTelegramMobileLinkCommand.create({
422-
userId: user._id,
423-
environmentId: user.environmentId,
424-
organizationId: user.organizationId,
425-
agentIdentifier: identifier,
426-
integrationId,
427-
subscriberId: body?.subscriberId,
428-
})
429-
);
430-
}
431-
432354
@Post('/:identifier/integrations/:integrationId/slack/setup-link')
433355
@ExternalApiAccessible()
434356
@KeylessAccessible()
@@ -459,38 +381,4 @@ export class AgentIntegrationsController {
459381
})
460382
);
461383
}
462-
463-
@Post('/:identifier/integrations/:integrationId/telegram/subscriber-link')
464-
@ExternalApiAccessible()
465-
@KeylessAccessible()
466-
@HttpCode(HttpStatus.OK)
467-
@ApiResponse(IssueTelegramSubscriberLinkResponseDto, 200)
468-
@ApiOperation({
469-
summary: 'Issue a Telegram subscriber-link deep link',
470-
description:
471-
'Issues a short-lived opaque start code and returns a Telegram `t.me/<bot>?start=<code>` deep link. When ' +
472-
'opened, Telegram sends `/start <code>` to the bot; the agent webhook consumes the code server-side and ' +
473-
'creates a `telegram_chat` channel endpoint so notifications can reach that subscriber via Telegram.',
474-
})
475-
@ApiNotFoundResponse({
476-
description: 'The agent, integration, agent-integration link, or subscriber was not found.',
477-
})
478-
@RequirePermissions(PermissionsEnum.AGENT_WRITE)
479-
createTelegramSubscriberLink(
480-
@UserSession() user: UserSessionData,
481-
@Param('identifier') identifier: string,
482-
@Param('integrationId') integrationId: string,
483-
@Body() body: IssueTelegramSubscriberLinkRequestDto
484-
): Promise<IssueTelegramSubscriberLinkResponseDto> {
485-
return this.issueTelegramSubscriberLinkUsecase.execute(
486-
IssueTelegramSubscriberLinkCommand.create({
487-
userId: user._id,
488-
environmentId: user.environmentId,
489-
organizationId: user.organizationId,
490-
agentIdentifier: identifier,
491-
integrationId,
492-
subscriberId: body.subscriberId,
493-
})
494-
);
495-
}
496384
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
2+
import { ApiExcludeController, ApiOperation } from '@nestjs/swagger';
3+
import { ApiRateLimitCategoryEnum } from '@novu/shared';
4+
5+
import { ThrottlerCategory } from '../../../rate-limiting/guards';
6+
import { ApiCommonResponses, ApiResponse } from '../../../shared/framework/response.decorator';
7+
import {
8+
ConsumeSlackSetupLinkRequestDto,
9+
ConsumeSlackSetupLinkResponseDto,
10+
} from '../../shared/dtos/consume-slack-setup-link.dto';
11+
import { SlackSetupLinkStatusResponseDto } from '../../shared/dtos/slack-setup-link-status-response.dto';
12+
import { ConsumeSlackSetupLinkCommand } from './consume-slack-setup-link/consume-slack-setup-link.command';
13+
import { ConsumeSlackSetupLink } from './consume-slack-setup-link/consume-slack-setup-link.usecase';
14+
import { GetSlackSetupLinkStatusCommand } from './get-slack-setup-link-status/get-slack-setup-link-status.command';
15+
import {
16+
GetSlackSetupLinkStatus,
17+
type GetSlackSetupLinkStatusResult,
18+
} from './get-slack-setup-link-status/get-slack-setup-link-status.usecase';
19+
20+
/**
21+
* Public, unauthenticated agent endpoints (no session) for Slack setup links.
22+
*/
23+
@ThrottlerCategory(ApiRateLimitCategoryEnum.CONFIGURATION)
24+
@ApiCommonResponses()
25+
@Controller('/agents/public')
26+
@ApiExcludeController()
27+
export class AgentsPublicController {
28+
constructor(
29+
private readonly getSlackSetupLinkStatusUsecase: GetSlackSetupLinkStatus,
30+
private readonly consumeSlackSetupLinkUsecase: ConsumeSlackSetupLink
31+
) {}
32+
33+
@Get('slack/setup/status')
34+
@HttpCode(HttpStatus.OK)
35+
@ApiResponse(SlackSetupLinkStatusResponseDto, 200)
36+
@ApiOperation({
37+
summary: 'Check the status of a Slack setup link',
38+
description:
39+
'Returns whether a Slack setup token is still usable. Designed to be called from the ' +
40+
'setup landing page before showing the credentials form.',
41+
})
42+
async getSlackSetupStatus(@Query('token') token: string): Promise<GetSlackSetupLinkStatusResult> {
43+
return this.getSlackSetupLinkStatusUsecase.execute(GetSlackSetupLinkStatusCommand.create({ token: token ?? '' }));
44+
}
45+
46+
@Post('slack/setup')
47+
@HttpCode(HttpStatus.OK)
48+
@ApiResponse(ConsumeSlackSetupLinkResponseDto, 200)
49+
@ApiOperation({
50+
summary: 'Consume a Slack setup link',
51+
description:
52+
'Validates the setup token, runs Slack quick-setup with the supplied App Configuration Token, ' +
53+
'and creates the Slack app from the Novu manifest. The token becomes invalid after a successful call.',
54+
})
55+
async consumeSlackSetup(@Body() body: ConsumeSlackSetupLinkRequestDto): Promise<ConsumeSlackSetupLinkResponseDto> {
56+
return this.consumeSlackSetupLinkUsecase.execute(
57+
ConsumeSlackSetupLinkCommand.create({
58+
token: body.token,
59+
configToken: body.configToken,
60+
})
61+
);
62+
}
63+
}

apps/api/src/app/agents/channels/slack-linking/consume-slack-setup-link/consume-slack-setup-link.usecase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
InvalidTelegramMobileTokenError,
1515
SlackAgentSetupLinkPayload,
1616
TelegramMobileLinkTokenService,
17-
} from '../../telegram-linking/telegram-mobile-link-token.service';
17+
} from '../../../../telegram-linking/telegram-mobile-link-token.service';
1818
import { ConsumeSlackSetupLinkCommand } from './consume-slack-setup-link.command';
1919

2020
export interface ConsumeSlackSetupLinkResult {

apps/api/src/app/agents/channels/slack-linking/get-slack-setup-link-status/get-slack-setup-link-status.usecase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
InvalidTelegramMobileTokenError,
77
SlackAgentSetupLinkPayload,
88
TelegramMobileLinkTokenService,
9-
} from '../../telegram-linking/telegram-mobile-link-token.service';
9+
} from '../../../../telegram-linking/telegram-mobile-link-token.service';
1010
import { GetSlackSetupLinkStatusCommand } from './get-slack-setup-link-status.command';
1111

1212
export type GetSlackSetupLinkStatusResult =

apps/api/src/app/agents/channels/slack-linking/issue-slack-setup-link/issue-slack-setup-link.usecase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
22
import { AgentIntegrationRepository, AgentRepository, IntegrationRepository } from '@novu/dal';
33
import { ChatProviderIdEnum } from '@novu/shared';
44

5-
import { TelegramMobileLinkTokenService } from '../../telegram-linking/telegram-mobile-link-token.service';
5+
import { TelegramMobileLinkTokenService } from '../../../../telegram-linking/telegram-mobile-link-token.service';
66
import { IssueSlackSetupLinkCommand } from './issue-slack-setup-link.command';
77

88
export interface IssueSlackSetupLinkResult {

0 commit comments

Comments
 (0)