Skip to content

🚀 release: v0.1.0-beta #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 19, 2025
Merged

🚀 release: v0.1.0-beta #7

merged 15 commits into from
Jun 19, 2025

Conversation

warengonzaga
Copy link
Member

@warengonzaga warengonzaga commented Jun 16, 2025

Summary by CodeRabbit

  • New Features

    • Added comprehensive Telegram bot commands and support ticket conversation flow with improved user interaction and error handling.
    • Introduced real-time ticket status update notifications and agent message replies in Telegram with fallback messaging.
    • Implemented multi-layered caching and storage system with in-memory, Redis, and PostgreSQL layers for enhanced reliability.
    • Enhanced database connection with environment-aware SSL configuration and improved type safety.
    • Added webhook consumer with typed event handling and robust event validation.
  • Bug Fixes

    • Improved error handling and logging across bot, webhook, and storage layers.
    • Added cleanup routines for blocked users and unavailable chats to maintain data integrity.
  • Documentation

    • Updated README to emphasize real-time ticket status notifications and reaction-based feedback.
    • Added notices enforcing Yarn usage and improved formatting for clarity.
  • Refactor

    • Migrated core modules to TypeScript with strong typing and modular design.
    • Replaced legacy storage and webhook SDK components with typed, maintainable implementations.
  • Chores

    • Added and updated configuration files for TypeScript compiler, Yarn, and npm to standardize development workflows.

@Copilot Copilot AI review requested due to automatic review settings June 16, 2025 16:18
@warengonzaga warengonzaga self-assigned this Jun 16, 2025
@warengonzaga warengonzaga added the release Release Status (PR) label Jun 16, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR releases version 0.1.0-beta with enhancements to webhook event handling and extended support for conversation update events.

  • Fallback handling for conversation IDs has been added across multiple modules.
  • Event validation now supports both "message_created" and "conversation_updated" events with detailed logging.
  • A new handler for conversation updates has been implemented along with corresponding documentation and subscription updates.

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/sdk/unthread-webhook/WebhookConsumer.js Updated to use a fallback for conversationId and to log additional event metadata.
src/sdk/unthread-webhook/EventValidator.js Extended validation logic to support "conversation_updated" events and improved logging.
src/index.js Added subscription for "conversation_updated" events.
src/handlers/webhookMessage.js Introduced handleConversationUpdated and message formatting for status notifications.
package.json Updated version from 0.1.0-alpha to 0.1.0-beta.
README.md Documentation updated to include status notifications and enhanced feature descriptions.
Comments suppressed due to low confidence (1)

src/sdk/unthread-webhook/EventValidator.js:39

  • [nitpick] Consider clarifying the debug output labels to explicitly indicate which value (conversationId or id) is being used, in order to improve traceability during troubleshooting.
LogEngine.debug('✅ Has conversationId:', { hasConversationId, conversationId: event.data.conversationId, id: event.data.id });

Copy link
Contributor

coderabbitai bot commented Jun 16, 2025

📝 Walkthrough
## Walkthrough

Sir, the codebase has undergone a comprehensive migration from JavaScript to TypeScript, introducing strong typing, improved error handling, and modularization throughout the system. New and refactored modules now provide robust storage, command handling, messaging, webhook processing, and database connectivity, all orchestrated under a unified TypeScript architecture. Documentation and configuration files have been updated accordingly.

## Changes

| Files/Group                                             | Change Summary |
|---------------------------------------------------------|---------------|
| README.md, .gitignore, .npmrc, .yarnrc                  | Documentation and configuration updated: README enhanced for real-time ticket status notifications; Yarn enforced as package manager; npm lockfile ignored; Yarn config refined. |
| package.json, tsconfig.json                             | Package version updated to beta; main entry changed to compiled output; Yarn enforced with scripts for build, dev, watch, clean; new dev dependencies added; TypeScript compiler configured. |
| src/types/index.ts, src/sdk/types.ts                    | Introduced comprehensive TypeScript type declarations for bot context, storage, webhook events, database, and domain entities. |
| src/bot.ts, src/commands/index.ts, src/events/message.ts| Rewritten core bot, command, and message handling modules in TypeScript with strong typing, safe messaging utilities, multi-step support ticket flows, and improved error handling. |
| src/handlers/webhookMessage.ts, src/sdk/unthread-webhook/EventValidator.ts, src/sdk/unthread-webhook/WebhookConsumer.ts | Webhook event handling, validation, and consumer logic refactored and typed in TypeScript, supporting robust event-driven integrations including conversation status updates and agent messages. |
| src/handlers/webhookMessage.js                          | Added new methods in TelegramWebhookHandler to handle conversation status updates with real-time ticket status notifications and fallback messaging logic. |
| src/database/connection.ts                              | Database connection refactored to TypeScript, with environment-aware SSL configuration, asynchronous schema initialization, strict typing, and enhanced logging. |
| src/sdk/bots-brain/BotsStore.ts                         | Enhanced with TypeScript typings, improved error handling, detailed logging, and added static methods for cache introspection and maintenance. |
| src/sdk/bots-brain/UnifiedStorage.ts                    | Replaced legacy JavaScript storage with a typed multi-layered caching and storage system supporting memory, Redis, and PostgreSQL layers with TTL and cleanup. |
| src/services/unthread.ts                                | Refactored with typed interfaces, modularized API calls, improved error handling, enriched stored data with timestamps and metadata. |
| src/index.ts                                            | Main entry point reimplemented in TypeScript: initializes bot, storage, webhook consumer, and orchestrates lifecycle, error handling, and graceful shutdown. |
| src/bot.js, src/commands/index.js, src/events/message.js, src/handlers/webhookMessage.js, src/sdk/bots-brain/UnifiedStorage.js, src/sdk/unthread-webhook/EventValidator.js, src/index.js | Legacy JavaScript modules deleted, replaced by their TypeScript counterparts. |
| src/sdk/unthread-webhook/index.ts                       | Minor documentation update: code block annotation changed from JavaScript to TypeScript. |

## Possibly related PRs

- **wgtechlabs/unthread-telegram-bot#2**: This PR initially introduced the `TelegramWebhookHandler` class with basic webhook message processing, directly related to the current enhancements adding conversation status update handling in the same class and file.

## Suggested labels

`maintainers only`
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai auto title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
src/sdk/unthread-webhook/WebhookConsumer.js (1)

184-206: Full payload logging risks PII leakage and oversized logs.

Dumping the entire event (JSON.stringify(event, null, 2)) to logs can inadvertently expose user e-mails and conversation content. Compress or redact sensitive fields before emission, e.g.:

- LogEngine.debug('🔍 Complete webhook event payload', {
-   completeEvent: JSON.stringify(event, null, 2)
- });
+ const { data, ...eventMeta } = event;
+ const redacted = {
+   ...eventMeta,
+   data: { ...data, content: '[redacted]', text: '[redacted]' }
+ };
+ LogEngine.debug('🔍 Complete webhook event payload', {
+   completeEvent: JSON.stringify(redacted, null, 2)
+ });

[security]

🧹 Nitpick comments (6)
src/handlers/webhookMessage.js (3)

236-243: Status whitelist hard-coded – promote to constant to avoid drift.

Instead of inline ['open', 'closed'], define const VALID_STATUSES = new Set(['open','closed']); at module scope for reuse and easier extension (e.g., future “pending” status).


270-287: Repeated parse_mode: 'Markdown' – consider MarkdownV2 for richer escaping.

Markdown is legacy; switching to MarkdownV2 future-proofs formatting and reduces accidental italic/bold when messages contain underscores or asterisks.


352-366: Minor: escape ticket ID to avoid accidental markdown interpretation.

If friendlyId ever contains _ or *, formatting breaks. Wrap with back-ticks or escape:

- message += `🎫 Ticket #${friendlyId}\n`;
+ message += `🎫 Ticket \`${friendlyId}\`\n`;
README.md (1)

65-70: Duplicate phrase “webhook server” – streamline wording.

Current sentence repeats “webhook server” twice.

- 2. **Unthread webhook** fires and sends event to the webhook server
- 3. **Webhook server** processes the event and queues it in Redis
+ 2. **Unthread webhook** fires and sends the event to the webhook server
+ 3. The server processes the event and queues it in Redis
src/sdk/unthread-webhook/EventValidator.js (2)

24-26: Make event-type check case-insensitive, sir.

If the upstream ever emits "Conversation_Updated" or similar casing, the current strict comparison will silently fail validation. Normalise once and compare:

-const hasCorrectType = ['message_created', 'conversation_updated'].includes(event.type);
+const hasCorrectType = ['message_created', 'conversation_updated']
+  .includes((event.type || '').toLowerCase());

This tiny tweak future-proofs the validator with negligible overhead.


38-40: 0 and empty strings are falsely rejected as IDs.

!!(event.data.conversationId || event.data.id) drops valid falsy values (0, ''). Prefer an explicit in / hasOwnProperty test to avoid accidental rejection:

-const hasConversationId = !!(event.data.conversationId || event.data.id);
+const hasConversationId = 'conversationId' in event.data || 'id' in event.data;

This guards against edge cases without loosening validation.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 488a7af and 85f2dad.

📒 Files selected for processing (6)
  • README.md (17 hunks)
  • package.json (1 hunks)
  • src/handlers/webhookMessage.js (1 hunks)
  • src/index.js (1 hunks)
  • src/sdk/unthread-webhook/EventValidator.js (3 hunks)
  • src/sdk/unthread-webhook/WebhookConsumer.js (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/handlers/webhookMessage.js (2)
src/services/unthread.js (2)
  • ticketData (288-295)
  • ticketData (333-333)
src/events/message.js (2)
  • message (162-162)
  • message (256-256)
🪛 LanguageTool
README.md

[grammar] ~66-~66: This phrase is duplicated. You should probably use “webhook server” only once.
Context: ... webhook** fires and sends event to the webhook server 3. Webhook server processes the event and queues it in ...

(PHRASE_REPETITION)


[uncategorized] ~84-~84: You might be missing the article “the” here.
Context: ...directly from Telegram group chats with /support command - Interactive ticket c...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)


[uncategorized] ~233-~233: A period might be missing here.
Context: ...ormation includes Telegram username and ID #### Responding to Users - Reply ...

(AI_EN_LECTOR_MISSING_PUNCTUATION_PERIOD)


[uncategorized] ~255-~255: It looks like there might be a possessive apostrophe missing. Consider inserting it.
Context: ...Group chat title should ideally include customer company name #### Best Practices ...

(AI_EN_LECTOR_MISSING_NOUN_POSSESSIVE)


[uncategorized] ~386-~386: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...nnection**: Verify PostgreSQL is running and connection string is correct - **Bot no...

(COMMA_COMPOUND_SENTENCE_2)

🔇 Additional comments (3)
package.json (1)

3-3: Version bump acknowledged – ensure dependent artifacts are tagged accordingly.

Build pipelines, Docker images, and changelogs should reference 0.1.0-beta; otherwise downstream deployments may still resolve the previous alpha artefact.

src/sdk/unthread-webhook/WebhookConsumer.js (1)

184-186: conversationId fallback may collide with message IDs.

event.data.id represents a message ID for message_created events, not a conversation ID. Handlers expecting a conversation identifier could mis-route messages. Consider limiting the fallback only to conversation_updated events or include an explicit event.type guard.

src/sdk/unthread-webhook/EventValidator.js (1)

67-77: Status whitelist is tight—confirm future states, sir.

Only "open" and "closed" are accepted. If Unthread later adds "resolved" or "pending", validation will abruptly fail. Either widen the enum or surface a clearer error path.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
src/sdk/unthread-webhook/EventValidator.ts (1)

113-115: Consider refactoring for enhanced maintainability.

While the current implementation works, the extensive logging within the validation method creates mixed responsibilities. Consider extracting logging logic into a separate concern.

  // Success - log conversation update with redaction enabled (info level for business events)
- LogEngine.info('✅ Conversation updated event validated', { 
-   conversationId: data.conversationId || data.id,
-   status: data.status,
-   eventType: eventObj.type,
-   sourcePlatform: eventObj.sourcePlatform,
-   timestamp: eventObj.timestamp,
-   // Use LogEngine's built-in redaction to safely log event data
-   eventData: eventObj.data
- });
+ this.logValidationSuccess(eventObj, data);
README.md (1)

66-66: Minor grammatical refinement required.

The phrase "webhook server" appears to be duplicated in the event flow description.

- 2. **Unthread webhook** fires and sends event to the webhook server
+ 2. **Unthread webhook** fires and sends event to the server
src/sdk/unthread-webhook/WebhookConsumer.ts (2)

167-172: Optimize the conditional check with optional chaining, sir.

The static analysis tool correctly identifies an opportunity to simplify the conditional logic.

Apply this diff to use optional chaining:

-      if (this.redisClient && this.redisClient.isOpen) {
+      if (this.redisClient?.isOpen) {
         const queueLength = await this.redisClient.lLen(this.queueName);
         if (queueLength > 0) {
           LogEngine.info(`Found ${queueLength} events in queue ${this.queueName}`);
         }
       }

302-303: Optimize the status check with optional chaining, sir.

Similar to the previous observation, this can be simplified for better readability.

Apply this diff:

-      isConnected: this.redisClient !== null && this.redisClient.isOpen,
-      isBlockingClientConnected: this.blockingRedisClient !== null && this.blockingRedisClient.isOpen,
+      isConnected: this.redisClient?.isOpen ?? false,
+      isBlockingClientConnected: this.blockingRedisClient?.isOpen ?? false,
src/handlers/webhookMessage.ts (1)

26-26: The options parameter could benefit from more precise typing, sir.

Rather than using any for Telegram message options, consider using the appropriate Telegraf types for better type safety.

-  async safeSendMessage(chatId: number, text: string, options: any = {}): Promise<any | null> {
+  async safeSendMessage(chatId: number, text: string, options: Parameters<Telegraf['telegram']['sendMessage']>[2] = {}): Promise<any | null> {
src/commands/index.ts (2)

284-285: Sir, I recommend utilizing optional chaining for cleaner code.

The static analysis tool correctly identified an opportunity to simplify this conditional check.

-        if (userState && userState.initiatedBy && userState.initiatedBy !== telegramUserId) {
+        if (userState?.initiatedBy && userState.initiatedBy !== telegramUserId) {

294-295: Another opportunity for optional chaining optimization, sir.

-        if (userState && userState.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
+        if (userState?.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71e4a9b and 290c412.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (26)
  • .gitignore (1 hunks)
  • .npmrc (1 hunks)
  • .yarnrc (1 hunks)
  • README.md (17 hunks)
  • package.json (1 hunks)
  • src/bot.js (0 hunks)
  • src/bot.ts (1 hunks)
  • src/commands/index.js (0 hunks)
  • src/commands/index.ts (1 hunks)
  • src/database/connection.ts (10 hunks)
  • src/events/message.js (0 hunks)
  • src/events/message.ts (1 hunks)
  • src/handlers/webhookMessage.ts (1 hunks)
  • src/index.js (0 hunks)
  • src/index.ts (1 hunks)
  • src/sdk/bots-brain/BotsStore.ts (17 hunks)
  • src/sdk/bots-brain/UnifiedStorage.js (0 hunks)
  • src/sdk/bots-brain/UnifiedStorage.ts (1 hunks)
  • src/sdk/types.ts (1 hunks)
  • src/sdk/unthread-webhook/EventValidator.js (0 hunks)
  • src/sdk/unthread-webhook/EventValidator.ts (1 hunks)
  • src/sdk/unthread-webhook/WebhookConsumer.ts (11 hunks)
  • src/sdk/unthread-webhook/index.ts (1 hunks)
  • src/services/unthread.ts (22 hunks)
  • src/types/index.ts (1 hunks)
  • tsconfig.json (1 hunks)
💤 Files with no reviewable changes (6)
  • src/sdk/unthread-webhook/EventValidator.js
  • src/index.js
  • src/sdk/bots-brain/UnifiedStorage.js
  • src/bot.js
  • src/commands/index.js
  • src/events/message.js
✅ Files skipped from review due to trivial changes (5)
  • src/sdk/unthread-webhook/index.ts
  • .yarnrc
  • .gitignore
  • .npmrc
  • tsconfig.json
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/sdk/unthread-webhook/EventValidator.ts (3)
src/sdk/unthread-webhook/index.ts (1)
  • EventValidator (25-25)
src/sdk/types.ts (1)
  • WebhookEvent (101-106)
src/types/index.ts (1)
  • WebhookEvent (83-88)
src/events/message.ts (3)
src/types/index.ts (1)
  • BotContext (5-7)
src/commands/index.ts (2)
  • processSupportConversation (240-518)
  • aboutCommand (762-762)
src/bot.ts (2)
  • safeReply (115-169)
  • safeEditMessageText (182-247)
src/sdk/unthread-webhook/WebhookConsumer.ts (2)
src/sdk/types.ts (3)
  • WebhookConsumerConfig (133-136)
  • EventHandler (138-138)
  • WebhookEvent (101-106)
src/types/index.ts (1)
  • WebhookEvent (83-88)
src/index.ts (8)
src/bot.ts (3)
  • createBot (19-24)
  • safeReply (115-169)
  • startPolling (46-48)
src/types/index.ts (1)
  • BotContext (5-7)
src/commands/index.ts (1)
  • processSupportConversation (240-518)
src/events/message.ts (1)
  • handleMessage (42-107)
src/database/connection.ts (1)
  • db (264-264)
src/sdk/bots-brain/BotsStore.ts (1)
  • BotsStore (20-608)
src/sdk/unthread-webhook/index.ts (1)
  • WebhookConsumer (24-24)
src/handlers/webhookMessage.ts (1)
  • TelegramWebhookHandler (9-515)
src/handlers/webhookMessage.ts (1)
src/types/index.ts (1)
  • BotContext (5-7)
🪛 Biome (1.9.4)
src/sdk/unthread-webhook/WebhookConsumer.ts

[error] 167-167: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 302-303: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/commands/index.ts

[error] 284-285: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 294-295: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 472-481: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 482-490: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🪛 LanguageTool
README.md

[grammar] ~66-~66: This phrase is duplicated. You should probably use “webhook server” only once.
Context: ... webhook** fires and sends event to the webhook server 3. Webhook server processes the event and queues it in ...

(PHRASE_REPETITION)


[uncategorized] ~84-~84: You might be missing the article “the” here.
Context: ...directly from Telegram group chats with /support command - Interactive ticket c...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)


[uncategorized] ~234-~234: A period might be missing here.
Context: ...ormation includes Telegram username and ID #### Responding to Users - Reply ...

(AI_EN_LECTOR_MISSING_PUNCTUATION_PERIOD)


[uncategorized] ~390-~390: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...nnection**: Verify PostgreSQL is running and connection string is correct - **Bot no...

(COMMA_COMPOUND_SENTENCE_2)

🔇 Additional comments (48)
package.json (5)

3-3: Excellent progression to beta, sir.

The version advancement from alpha to beta properly reflects the maturity of this TypeScript migration and enhanced webhook functionality.


5-5: Entry point optimally configured for production deployment.

The transition from src/index.js to dist/index.js correctly points to the compiled TypeScript output, ensuring clean separation between source and build artifacts.


14-19: Build pipeline expertly crafted, sir.

The script configuration demonstrates sophisticated build orchestration:

  • preinstall enforces Yarn usage (excellent practice)
  • build provides clean TypeScript compilation
  • dev and dev:watch offer efficient development workflows
  • clean ensures proper artifact management

The concurrent TypeScript compilation with nodemon hot-reloading is particularly elegant.


22-22: Log engine upgrade strategically deployed.

The advancement from ^1.2.1 to ^1.3.0 aligns perfectly with the enhanced logging requirements evident throughout the new TypeScript modules.


30-34: TypeScript ecosystem integration flawlessly executed.

The development dependencies are precisely selected:

  • [email protected] provides cutting-edge language features
  • @types/node and @types/pg ensure comprehensive type coverage
  • concurrently and nodemon enable sophisticated development workflows

The version pinning strategy balances stability with modern capabilities.

src/sdk/unthread-webhook/EventValidator.ts (4)

16-39: Validation architecture impeccably designed, sir.

The defensive programming approach here is exemplary:

  • Early validation with proper type guards
  • Structured error reporting with contextual data
  • Clean separation of concerns in validation logic

The logging strategy provides optimal debugging capability while maintaining performance in production.


54-61: Logging verbosity controls expertly implemented.

The conditional debug logging based on LOG_LEVEL and VERBOSE_LOGGING environment variables demonstrates sophisticated observability design. This prevents log noise in production while enabling detailed debugging when required.


63-83: Message validation logic brilliantly adaptive.

The dual field checking for both content and text shows excellent defensive programming, accommodating variations in webhook payload structure. The fallback mechanism ensures robust message processing regardless of field naming conventions.


85-118: Conversation status validation with business-level logging.

The implementation correctly validates conversation status while providing info-level logging for business events. The built-in redaction feature in LogEngine ensures sensitive data protection during event logging.

README.md (3)

9-9: Documentation enhancement superbly executed.

The addition of real-time notification capabilities to the overview effectively communicates the enhanced user experience. The emphasis on immediate status updates aligns perfectly with the new webhook infrastructure.


31-31: Status notification feature elegantly highlighted.

The clear documentation of emoji indicators and messaging provides users with precise expectations for the notification experience.


288-288: Package manager enforcement notice excellently positioned.

The clear warning about npm prevention with instructions to use Yarn demonstrates proactive user experience design. This prevents common installation issues and ensures consistent development environments.

src/bot.ts (4)

19-24: Bot instantiation with elegant defensive programming.

The token validation prevents runtime failures while the typed return ensures compile-time safety. This foundational function sets an excellent precedent for error prevention.


59-105: Safe messaging implementation masterfully crafted.

The comprehensive error handling covers all critical Telegram API failure modes:

  • User blocking detection with automatic cleanup
  • Chat not found scenarios with graceful degradation
  • Rate limiting with proper logging
  • Unexpected errors with context preservation

The cleanup integration maintains data consistency across the system.


255-310: User cleanup logic architecturally sound.

The comprehensive cleanup implementation handles:

  • Ticket removal with proper logging
  • Customer mapping cleanup
  • Graceful error handling without system disruption
  • Detailed audit trail for troubleshooting

The error isolation ensures bot stability even during cleanup failures.


295-298: User state cleanup limitation documented with precision.

The comment accurately explains why user states cannot be cleaned up without the user ID. The natural TTL expiration provides an acceptable fallback mechanism.

src/events/message.ts (5)

42-107: Message routing architecture brilliantly orchestrated.

The sequential processing logic demonstrates exceptional design:

  • Command detection with proper delegation
  • Support conversation processing with early return
  • Ticket reply handling with appropriate isolation
  • Chat type routing with auto-response prevention

The early return patterns prevent unwanted processing chains while maintaining clean separation of concerns.


169-221: Ticket confirmation reply handling expertly implemented.

The validation pipeline with status message management provides excellent user feedback:

  • Immediate acknowledgment with loading indicator
  • Comprehensive error handling with user notification
  • Automatic status message cleanup with appropriate timing

The separation of validation, processing, and status management creates maintainable code architecture.


284-314: Status message lifecycle management with precision timing.

The differentiated cleanup timeouts (3 seconds for success, 5 seconds for errors) provide optimal user experience. The silent error handling on deletion prevents unnecessary noise while ensuring cleanup completion.


412-445: Private message handling with intelligent command filtering.

The implementation correctly delegates non-command private messages to the about command while preventing duplicate processing of commands. This provides consistent user experience without interference.


450-478: Group message processing with comprehensive observability.

The detailed logging provides excellent debugging capability while maintaining the deliberate no-response policy for group messages. This prevents spam while ensuring proper audit trails.

src/sdk/types.ts (1)

1-171: Excellent foundational type architecture, sir.

The comprehensive type definitions provide a robust foundation for the entire SDK. The interfaces are well-structured with proper separation of concerns and appropriate optional properties. The discriminated union pattern for webhook events is particularly well-implemented.

src/sdk/unthread-webhook/WebhookConsumer.ts (1)

147-150: Excellent polling architecture with proper cleanup scheduling, sir.

The implementation correctly ensures that the next poll is scheduled exactly once per cycle, preventing potential race conditions or multiple concurrent polling operations. The finally block guarantees proper scheduling regardless of success or failure.

src/index.ts (4)

198-233: Exemplary retry implementation with exponential backoff, sir.

The retry logic demonstrates excellent resilience patterns with configurable parameters, proper error propagation, and comprehensive logging. The exponential backoff with maximum delay prevents overwhelming external services while ensuring reasonable retry intervals.


430-485: Robust cleanup implementation for blocked users, sir.

The global cleanup function properly handles the cascading effects of blocked users by removing tickets, customer mappings, and associated data. The error handling ensures the bot continues operating even if cleanup fails, which is the correct approach for maintaining service availability.


492-508: Proper graceful shutdown implementation, sir.

The shutdown sequence correctly orders the cleanup operations: webhook consumer first, then storage, then database connections. The error handling prevents partial shutdowns from leaving the process in an inconsistent state.


28-28: Verify the import assertion syntax compatibility, sir.

The import assertion syntax with { type: 'json' } is relatively new and may not be supported in all Node.js versions or TypeScript configurations.

Please verify the Node.js and TypeScript versions support this syntax:

#!/bin/bash
# Check package.json for Node.js and TypeScript version requirements
cat package.json | jq '.engines.node // "not specified"'
cat package.json | jq '.devDependencies.typescript // .dependencies.typescript // "not specified"'

# Check if there are any TypeScript configuration files
fd -t f "tsconfig.*\.json$" --exec cat {}
src/database/connection.ts (3)

242-260: Excellent security-conscious SSL configuration, sir.

The environment-aware SSL configuration properly enforces certificate validation in production while allowing flexibility in development. This approach effectively prevents man-in-the-middle attacks in production environments while maintaining developer productivity.


193-197: Proper async file handling implementation, sir.

The migration to asynchronous file operations using fs.promises.access and fs.promises.readFile improves the non-blocking nature of the application and follows modern Node.js best practices.


79-81: Well-designed connection pool encapsulation, sir.

The getter property provides controlled access to the connection pool while maintaining encapsulation. This pattern allows external modules to access the pool when needed while keeping the internal state protected.

src/sdk/bots-brain/UnifiedStorage.ts (5)

14-48: Sophisticated multi-layered storage architecture, sir.

The three-tier storage design with memory, Redis, and PostgreSQL provides excellent performance characteristics with proper fallback mechanisms. The TTL management for each layer is well-considered with appropriate timeframes for each storage tier.


115-156: Excellent cache coherency implementation, sir.

The get method correctly implements the cache-aside pattern, checking layers from fastest to slowest and backfilling upper layers when data is found in slower layers. This ensures optimal performance while maintaining data consistency across all storage tiers.


268-271: Graceful PostgreSQL error handling for missing tables, sir.

The silent handling of missing storage_cache tables is appropriate for this storage layer, allowing the system to degrade gracefully when the PostgreSQL schema is not fully initialized while still providing Redis and memory caching capabilities.


298-308: Efficient memory cleanup with appropriate intervals, sir.

The one-minute cleanup interval strikes an excellent balance between memory efficiency and CPU overhead. The implementation correctly removes both the cache entry and TTL tracking for expired items.


331-438: Comprehensive introspection capabilities, sir.

The detailed memory statistics and contents inspection methods provide excellent debugging and monitoring capabilities. The categorization by key prefix and size calculations will be invaluable for performance optimization and troubleshooting.

src/handlers/webhookMessage.ts (2)

232-263: Excellent message sanitization implementation, sir.

The Markdown escape logic is well-thought-out and handles the common pitfalls that could break Telegram message formatting. The approach of preserving basic formatting while escaping problematic characters is exactly what we need.


462-514: The cleanup mechanism for blocked users is impressively comprehensive, sir.

This implementation properly addresses the Telegraf issue #1513 you referenced and handles all the necessary cleanup operations. The error handling ensures the bot remains stable even when cleanup operations fail.

src/commands/index.ts (2)

166-233: The support command initialization is elegantly architected, sir.

The restriction to group chats only is properly implemented, and the state initialization with comprehensive tracking fields demonstrates excellent forethought for the conversation flow management.


554-658: The ticket creation workflow exhibits remarkable resilience, sir.

The comprehensive error handling, API integration, and user feedback mechanisms create a seamless experience. The fallback messaging and cleanup operations are particularly well-executed.

src/services/unthread.ts (4)

16-81: Exceptional interface design for the service layer, sir.

The comprehensive type definitions provide excellent clarity and type safety for all Unthread API operations. The separation of concerns between main functions and JSON helpers is architecturally sound.


171-179: Improved error handling strategy detected, sir.

The transition from process.exit() to throwing errors enables much better upstream error handling and prevents abrupt application termination. This is precisely how environment validation should be implemented.


254-294: The modularization of ticket creation logic is exemplary, sir.

Separating the business logic from the JSON payload creation enhances maintainability and testability. The error handling with explicit type casting follows best practices.


461-533: The customer management with database integration is remarkably robust, sir.

The caching strategy, comprehensive data enrichment, and error handling create an efficient and reliable customer management system. The extraction of company names from chat titles is particularly clever.

src/types/index.ts (1)

1-172: Sir, this type definition architecture is absolutely magnificent.

The comprehensive coverage of all system components - from Telegraf extensions to database interfaces, webhook events, and error types - creates a solid foundation for type safety throughout the application. The alignment between these types and their usage in other modules demonstrates excellent architectural planning.

src/sdk/bots-brain/BotsStore.ts (4)

20-52: The singleton pattern implementation is impeccably structured, sir.

The typed initialization with database connection management and proper error handling for uninitialized access demonstrates excellent architectural planning. The static factory method pattern is precisely what's needed here.


114-165: The ticket storage strategy with bidirectional mapping is ingenious, sir.

Creating multiple lookup keys for different access patterns while enriching data with metadata ensures both performance and data integrity. The comprehensive error handling maintains system stability even during storage failures.


291-327: The customer data management exhibits excellent defensive programming, sir.

The data enrichment with timestamps, comprehensive error handling, and multiple access patterns create a robust customer management system that can handle various failure scenarios gracefully.


580-608: The memory inspection capabilities are a stroke of genius, sir.

Providing runtime introspection into cache contents and statistics will prove invaluable for debugging and performance optimization. The cleanup methods ensure efficient memory management.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/commands/index.ts (1)

421-502: Switch statement implementation is properly structured, sir.

Contrary to the past review comment, the switch cases are already correctly wrapped in block statements with curly braces, preventing variable declaration conflicts. The summary confirmation flow with both button and text-based responses provides excellent flexibility for users.

🧹 Nitpick comments (1)
src/commands/index.ts (1)

280-302: Optional chain optimization opportunity detected, sir.

The static analysis tools have identified areas where optional chaining could enhance code safety and readability.

-        if (userState && userState.initiatedBy && userState.initiatedBy !== telegramUserId) {
+        if (userState?.initiatedBy && userState.initiatedBy !== telegramUserId) {
-        if (userState && userState.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
+        if (userState?.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 290c412 and 74b19fa.

📒 Files selected for processing (4)
  • src/bot.ts (1 hunks)
  • src/commands/index.ts (1 hunks)
  • src/events/message.ts (1 hunks)
  • src/handlers/webhookMessage.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/handlers/webhookMessage.ts
🧰 Additional context used
🪛 Biome (1.9.4)
src/commands/index.ts

[error] 284-285: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 294-295: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (8)
src/bot.ts (2)

60-106: Excellent implementation of robust error handling, sir.

The safeSendMessage function demonstrates a masterful approach to Telegram API error management. The comprehensive handling of 403 (blocked/not found), 429 (rate limiting), and other error scenarios ensures the bot remains stable under various conditions. The cleanup logic for blocked users is particularly well-designed, automatically removing associated tickets and customer data.


256-311: Cleanup logic implementation is quite sophisticated, sir.

The cleanupBlockedUser function provides thorough data cleanup when users block the bot or chats become unavailable. The implementation correctly handles multiple data sources (tickets, customer mappings) and includes proper error handling that prevents cleanup failures from crashing the bot. The logging throughout provides excellent observability.

src/events/message.ts (3)

35-100: Message routing logic is elegantly orchestrated, sir.

The handleMessage function provides excellent message flow control, properly distinguishing between commands, support conversations, and ticket replies. The early returns prevent unnecessary processing and the logging provides clear insights into message handling decisions. The prevention of auto-responses in group chats is a thoughtful design choice.


162-214: Ticket confirmation reply handling shows remarkable attention to detail, sir.

The validation, processing, and status message updates create a smooth user experience. The error handling ensures graceful degradation, and the automatic cleanup of status messages after success/failure provides excellent UX polish.


277-307: Status message management demonstrates refined user experience design, sir.

The automatic deletion of status messages with appropriate timing (3 seconds for success, 5 seconds for errors) prevents chat clutter while giving users sufficient time to see the results. The error handling in the deletion ensures the bot continues functioning even if message deletion fails.

src/commands/index.ts (3)

29-61: Start command implementation is remarkably comprehensive, sir.

The conditional logic for handling private versus group chats provides excellent user guidance. The detailed bot information for private chats and concise instructions for group chats demonstrate thoughtful UX design. The markdown formatting enhances readability significantly.


240-520: Support conversation processing exhibits exceptional sophistication, sir.

The processSupportConversation function masterfully handles the complex state machine for ticket creation. The callback query handling for confirmation buttons, the validation logic for ensuring messages come from the correct user and chat, and the comprehensive error handling all demonstrate excellent engineering practices.


525-683: Email field handling demonstrates exceptional completeness, sir.

The handleEmailField function orchestrates the final steps of ticket creation with remarkable thoroughness. The automatic email generation for skipped emails, the comprehensive error handling during ticket creation, and the cleanup of intermediate messages all contribute to a polished user experience. The detailed logging provides excellent observability for debugging and monitoring.

Docstrings generation was requested by @warengonzaga.

* #7 (comment)

The following files were modified:

* `src/bot.ts`
* `src/commands/index.ts`
* `src/events/message.ts`
* `src/index.ts`
* `src/services/unthread.ts`
@coderabbitai coderabbitai bot mentioned this pull request Jun 19, 2025
Copy link
Contributor

coderabbitai bot commented Jun 19, 2025

Note

Generated docstrings for this pull request at #8

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/commands/index.ts (1)

294-295: Another opportunity for optional chaining enhancement, sir.

This follows the same pattern as the previous suggestion.

-if (userState && userState.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
+if (userState?.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
🧹 Nitpick comments (6)
src/events/message.ts (1)

428-461: The private message handling logic is sound, but could benefit from enhanced efficiency, sir.

The command detection logic is duplicated from the main handler. Consider extracting this to a utility function to maintain DRY principles.

+// Extract command detection utility
+function isCommand(message: any): boolean {
+    return message && 'text' in message && message.text?.startsWith('/');
+}

 export async function handlePrivateMessage(ctx: BotContext): Promise<void> {
     try {
         // ... logging code ...
         
-        if (ctx.message && 'text' in ctx.message && ctx.message.text?.startsWith('/')) {
+        if (isCommand(ctx.message)) {
             LogEngine.debug('Skipping private message - it\'s a command', {
                 command: ctx.message.text.split(' ')[0]
             });
             return;
         }
src/index.ts (1)

107-137: The command wrapper pattern is excellent, but could be optimized, sir.

While the error handling wrapper is superb, consider using a more functional approach to reduce repetition in the command registration.

+// Create command registration helper
+const registerCommand = (command: string, handler: (ctx: BotContext) => Promise<void>) => {
+    const wrappedHandler = wrapCommandHandler(handler, command);
+    if (command === 'start') {
+        bot.start(commandMiddleware, wrappedHandler);
+    } else if (command === 'help') {
+        bot.help(commandMiddleware, wrappedHandler);
+    } else {
+        bot.command(command, commandMiddleware, wrappedHandler);
+    }
+};

-bot.start(commandMiddleware, wrapCommandHandler(startCommand, 'start'));
-bot.help(commandMiddleware, wrapCommandHandler(helpCommand, 'help'));
-bot.command('version', commandMiddleware, wrapCommandHandler(versionCommand, 'version'));
-bot.command('about', commandMiddleware, wrapCommandHandler(aboutCommand, 'about'));
-bot.command('support', commandMiddleware, wrapCommandHandler(supportCommand, 'support'));
-bot.command('cancel', commandMiddleware, wrapCommandHandler(cancelCommand, 'cancel'));
-bot.command('reset', commandMiddleware, wrapCommandHandler(resetCommand, 'reset'));
+registerCommand('start', startCommand);
+registerCommand('help', helpCommand);
+registerCommand('version', versionCommand);
+registerCommand('about', aboutCommand);
+registerCommand('support', supportCommand);
+registerCommand('cancel', cancelCommand);
+registerCommand('reset', resetCommand);
src/commands/index.ts (1)

284-285: Consider using optional chaining for cleaner code, sir.

The static analysis tools have identified an opportunity for more elegant conditional logic.

-if (userState && userState.initiatedBy && userState.initiatedBy !== telegramUserId) {
+if (userState?.initiatedBy && userState.initiatedBy !== telegramUserId) {
src/services/unthread.ts (3)

117-123: Logic enhancement opportunity detected.

While the company name extraction logic is sound, the conditional checks could be more defensive. Consider the scenario where both parts equal the company name, which would currently fall through without proper handling.

-                if (part1 === lowerCompanyName && part2 !== lowerCompanyName && part2) {
+                if (part1 === lowerCompanyName && part2 !== lowerCompanyName && part2.length > 0) {
                     // Our company is first, customer is second
                     return capitalizeCompanyName(part2);
-                } else if (part2 === lowerCompanyName && part1 !== lowerCompanyName && part1) {
+                } else if (part2 === lowerCompanyName && part1 !== lowerCompanyName && part1.length > 0) {
                     // Customer is first, our company is second
                     return capitalizeCompanyName(part1);
                 }

200-201: Non-null assertion requires attention, sir.

The use of UNTHREAD_API_KEY! employs a non-null assertion operator. While the environment validation above ensures this value exists, consider using a more explicit approach for enhanced code clarity.

-                'X-API-KEY': UNTHREAD_API_KEY!
+                'X-API-KEY': UNTHREAD_API_KEY as string

514-523: Data consistency opportunity identified.

The customer storage includes both chatId and telegramChatId with the same value, which creates unnecessary redundancy and potential inconsistency.

         await BotsStore.storeCustomer({
             id: `customer_${chatId}`,
             unthreadCustomerId: result.id,
-            telegramChatId: chatId,
             chatId: chatId,
             chatTitle: groupChatName,
             customerName: customerName,
-            name: customerName,
             createdAt: new Date().toISOString(),
             updatedAt: new Date().toISOString()
         });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74b19fa and 302c1d3.

📒 Files selected for processing (5)
  • src/bot.ts (1 hunks)
  • src/commands/index.ts (1 hunks)
  • src/events/message.ts (1 hunks)
  • src/index.ts (1 hunks)
  • src/services/unthread.ts (17 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/events/message.ts (3)
src/types/index.ts (1)
  • BotContext (5-7)
src/commands/index.ts (2)
  • processSupportConversation (240-520)
  • aboutCommand (766-766)
src/bot.ts (2)
  • safeReply (119-173)
  • safeEditMessageText (188-253)
🪛 Biome (1.9.4)
src/commands/index.ts

[error] 284-285: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 294-295: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (17)
src/events/message.ts (4)

38-103: Excellent architectural design, sir! The message routing logic is quite sophisticated.

The main handler demonstrates superb control flow with proper delegation to specialized processors. The early returns for commands and support conversations prevent message overlap, which is precisely what we want for a robust system.


20-31: Utility functions are elegantly simple and well-documented, sir.

These chat type detection functions provide clear, readable abstractions that enhance code maintainability throughout the system.


112-164: The ticket reply handling demonstrates excellent defensive programming, sir.

The validation logic and error handling are comprehensive, with proper fallback to false when no ticket or agent message is found. The logging provides excellent debugging capabilities.


294-324: The status message management is quite ingenious, sir.

The automatic cleanup with setTimeout and the differentiated delay periods (3s for success, 5s for error) provide excellent user experience without cluttering the chat interface.

src/bot.ts (3)

21-26: Excellent defensive programming for bot initialization, sir.

The token validation with proper error messaging ensures graceful failure rather than cryptic runtime errors. The type annotation Telegraf<BotContext> provides excellent type safety.


61-107: The safe messaging pattern is absolutely brilliant, sir.

This demonstrates superior error handling architecture - catching specific Telegram errors, performing appropriate cleanup, and logging comprehensive context. The distinction between re-throwable and handled errors is particularly well-designed.


262-317: The cleanup logic is remarkably thorough, sir.

The systematic approach to removing tickets, customer mappings, and handling user state expiration demonstrates excellent data consistency management. The error handling ensures bot stability even during cleanup failures.

src/index.ts (1)

208-243: The retry mechanism with exponential backoff is absolutely masterful, sir.

This demonstrates enterprise-grade resilience patterns. The configurable parameters and comprehensive logging make this utility function exceptionally robust for handling transient failures.

src/commands/index.ts (3)

29-61: The start command's adaptive behavior is exceptionally well-designed, sir.

The different responses for private vs group chats demonstrate excellent UX consideration. The comprehensive information in private chats and focused instructions in groups optimize user experience for each context.


240-520: The support conversation flow is remarkably sophisticated, sir.

The state management, callback handling, and multi-step validation demonstrate excellent architectural design. The comprehensive logging and error handling ensure robust operation.


527-685: The email handling and ticket creation logic is exceptionally robust, sir.

The auto-generation of email addresses for skipped entries, comprehensive error handling, and proper cleanup of intermediate messages demonstrate excellent attention to user experience and system reliability.

src/services/unthread.ts (6)

17-81: Excellent TypeScript interface design, sir.

The comprehensive interface definitions provide robust type safety for the entire service layer. The separation of concerns between different parameter types demonstrates excellent architectural thinking.


174-180: Improved error handling strategy implemented, sir.

The transition from process.exit() to throwing errors is a significant improvement, allowing for proper error propagation and handling upstream. This aligns perfectly with modern Node.js best practices.


264-301: Modular API architecture brilliantly implemented.

The separation of createTicket and createTicketJSON demonstrates excellent separation of concerns. The helper function handles the low-level API interaction while the main function manages business logic.


481-486: Defensive data access implementation observed.

The fallback pattern existingCustomer.customerName || existingCustomer.name provides excellent resilience against data structure variations. This defensive programming approach is commendable.


580-594: Dynamic object construction elegantly handled.

The conditional assignment of username properties demonstrates excellent TypeScript practices. The explicit type annotation userData: any is acceptable here given the dynamic nature of the object construction.


183-183: Certainly, sir. Initiating a broader search for all customerCache references and its method invocations:

#!/bin/bash
# Search everything for any occurrence of customerCache
echo "🔍 Scanning repository for 'customerCache'..."
rg --color=never "customerCache"

# Specifically look for method calls in TypeScript files
echo -e "\n🔍 Looking for customerCache.<method> in .ts files..."
rg --color=never -t ts -n "customerCache\\.(set|get|has|delete|clear)"

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
src/events/message.ts (1)

294-324: Consider extracting the status message lifecycle into a utility class, sir.

The updateStatusMessage function handles the complete lifecycle well, but the setTimeout approach for cleanup could benefit from a more robust pattern. Consider implementing a StatusMessageManager that can handle cleanup even if the process crashes.

+class StatusMessageManager {
+    private cleanupTimers = new Map<string, NodeJS.Timeout>();
+    
+    async updateAndScheduleCleanup(ctx: BotContext, statusMsg: any, isSuccess: boolean): Promise<void> {
+        const key = `${ctx.chat!.id}-${statusMsg.message_id}`;
+        
+        // Clear existing timer if any
+        if (this.cleanupTimers.has(key)) {
+            clearTimeout(this.cleanupTimers.get(key)!);
+        }
+        
+        // Update message
+        await safeEditMessageText(/* ... */);
+        
+        // Schedule cleanup with error handling
+        const delay = isSuccess ? 3000 : 5000;
+        const timer = setTimeout(async () => {
+            try {
+                await ctx.telegram.deleteMessage(ctx.chat!.id, statusMsg.message_id);
+            } catch (error) {
+                LogEngine.debug('Failed to cleanup status message', { error });
+            } finally {
+                this.cleanupTimers.delete(key);
+            }
+        }, delay);
+        
+        this.cleanupTimers.set(key, timer);
+    }
+}
src/bot.ts (1)

262-317: The cleanup orchestration is thorough, but consider transaction-like guarantees, sir.

The cleanupBlockedUser function handles multiple cleanup operations well, but if one operation fails, the others still proceed. Consider implementing a cleanup transaction pattern to ensure either all cleanup succeeds or state is properly tracked for retry.

+interface CleanupOperation {
+    name: string;
+    execute: () => Promise<void>;
+    rollback?: () => Promise<void>;
+}
+
 export async function cleanupBlockedUser(chatId: number): Promise<void> {
+    const operations: CleanupOperation[] = [
+        {
+            name: 'cleanup-tickets',
+            execute: async () => {
+                const tickets = await botsStore.getTicketsForChat(chatId);
+                for (const ticket of tickets) {
+                    await botsStore.deleteTicket(ticket.conversationId);
+                }
+            }
+        },
+        {
+            name: 'cleanup-customer',
+            execute: async () => {
+                const customer = await botsStore.getCustomerByChatId(chatId);
+                if (customer) {
+                    await botsStore.storage.delete(`customer:telegram:${chatId}`);
+                    await botsStore.storage.delete(`customer:id:${customer.unthreadCustomerId}`);
+                }
+            }
+        }
+    ];
+    
+    for (const operation of operations) {
+        try {
+            await operation.execute();
+            LogEngine.debug(`Cleanup operation ${operation.name} completed`, { chatId });
+        } catch (error) {
+            LogEngine.error(`Cleanup operation ${operation.name} failed`, { 
+                chatId, 
+                error: (error as Error).message 
+            });
+            // Continue with other operations but track failures
+        }
+    }
src/sdk/types.ts (1)

141-171: The IBotsStore interface contract is comprehensive, but consider adding batch operations, sir.

While the CRUD operations cover all current needs, high-volume scenarios might benefit from batch operations to reduce database round trips.

 export interface IBotsStore {
   storage: Storage;
   
   // Ticket operations
   storeTicket(ticketData: TicketData): Promise<boolean>;
+  storeTickets(ticketData: TicketData[]): Promise<boolean[]>;
   getTicketByConversationId(conversationId: string): Promise<TicketData | null>;
+  getTicketsByConversationIds(conversationIds: string[]): Promise<(TicketData | null)[]>;
   // ... existing methods
   
   // Batch operations for performance optimization
+  batchStoreUsers(userData: UserData[]): Promise<boolean[]>;
+  batchDeleteTickets(conversationIds: string[]): Promise<boolean[]>;
 }
src/commands/index.ts (2)

166-233: The supportCommand initialization logic is thorough, but consider extracting the state setup, sir.

The user state initialization is comprehensive but tightly coupled to the command handler. Consider extracting this into a dedicated SupportTicketInitializer class for better testability and reusability.

+class SupportTicketInitializer {
+    static async initializeTicketState(
+        ctx: BotContext,
+        telegramUserId: number
+    ): Promise<SupportFormState & { ticket: any; messageIds?: number[] }> {
+        const chatTitle = 'title' in ctx.chat! ? ctx.chat!.title : 'Group Chat';
+        
+        return {
+            field: SupportFieldEnum.SUMMARY as SupportField,
+            initiatedBy: telegramUserId,
+            initiatedInChat: ctx.chat!.id,
+            messageIds: [],
+            ticket: {
+                summary: '',
+                email: '',
+                name: ctx.from!.username || `${ctx.from!.first_name} ${ctx.from!.last_name || ''}`.trim(),
+                company: chatTitle,
+                chatId: ctx.chat!.id
+            }
+        };
+    }
+}

284-285: Implement the static analysis optimization for optional chaining, sir.

The static analysis correctly identifies opportunities to use optional chaining for cleaner code.

-        if (userState && userState.initiatedBy && userState.initiatedBy !== telegramUserId) {
+        if (userState?.initiatedBy && userState.initiatedBy !== telegramUserId) {
             // This message is from a different user, ignore it silently
             LogEngine.debug('Ignoring message from different user during support flow', {
                 messageFrom: telegramUserId,
                 supportInitiatedBy: userState.initiatedBy,
                 chatId: ctx.chat?.id
             });
             return false;
         }
         
-        if (userState && userState.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
+        if (userState?.initiatedInChat && userState.initiatedInChat !== ctx.chat?.id) {
             // This message is from a different chat, ignore it
             LogEngine.debug('Ignoring message from different chat during support flow', {
                 messageFromChat: ctx.chat?.id,
                 supportInitiatedInChat: userState.initiatedInChat,
                 userId: telegramUserId
             });
             return false;
         }

Also applies to: 294-295

src/services/unthread.ts (2)

192-192: Function signature requires refinement for optimal type safety.

The createCustomer function return type could be more specific than Promise<Customer> to align with the actual API response structure.

Consider defining a more specific interface for the API response:

+interface CreateCustomerResponse extends Customer {
+  // Add any additional fields returned by the API
+}

-export async function createCustomer(groupChatName: string): Promise<Customer> {
+export async function createCustomer(groupChatName: string): Promise<CreateCustomerResponse> {

213-213: Type assertion patterns could benefit from enhanced safety protocols.

While the as Customer and similar type assertions function adequately, they bypass TypeScript's type checking. Consider implementing runtime validation for API responses.

+// Add runtime validation helper
+function validateCustomerResponse(data: any): Customer {
+  if (!data?.id || typeof data.id !== 'string') {
+    throw new Error('Invalid customer response: missing or invalid id');
+  }
+  if (!data?.name || typeof data.name !== 'string') {
+    throw new Error('Invalid customer response: missing or invalid name');
+  }
+  return data as Customer;
+}

-const result = await response.json() as Customer;
+const result = validateCustomerResponse(await response.json());

Also applies to: 292-292, 511-511

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 302c1d3 and 5c25c24.

📒 Files selected for processing (6)
  • src/bot.ts (1 hunks)
  • src/commands/index.ts (1 hunks)
  • src/events/message.ts (1 hunks)
  • src/index.ts (1 hunks)
  • src/sdk/types.ts (1 hunks)
  • src/services/unthread.ts (18 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/events/message.ts (3)
src/types/index.ts (1)
  • BotContext (5-7)
src/commands/index.ts (2)
  • processSupportConversation (240-520)
  • aboutCommand (766-766)
src/bot.ts (2)
  • safeReply (119-173)
  • safeEditMessageText (188-253)
🪛 Biome (1.9.4)
src/commands/index.ts

[error] 284-285: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 294-295: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (22)
src/events/message.ts (2)

38-103: Excellent message routing architecture, sir.

The main handleMessage function demonstrates sophisticated flow control with proper early returns and exclusive processing paths. The logic correctly prevents command interference, processes support conversations first, and handles ticket replies appropriately. The explicit prevention of auto-responses in group chats (lines 91-93) is particularly well-designed.


173-225: The error handling hierarchy here is exemplary, sir.

The nested try-catch structure in handleTicketConfirmationReply properly isolates API errors from validation errors, ensuring that status messages are always updated appropriately. The early validation pattern with validateTicketReply maintains clean separation of concerns.

src/bot.ts (2)

21-26: Pristine error handling for bot instantiation, sir.

The token validation with early error throwing follows the fail-fast principle beautifully. This prevents downstream issues that would be much harder to debug.


66-107: The error handling taxonomy in safeSendMessage is comprehensive, sir.

The systematic approach to handling different Telegram error codes (403 for blocked users, 429 for rate limits) with appropriate cleanup actions demonstrates excellent defensive programming. The re-throwing of unhandled errors maintains the principle of failing loudly for unexpected conditions.

src/index.ts (3)

46-82: The middleware logging implementation is exemplary, sir.

The message type detection logic handles all Telegram message variants systematically, and the error handling ensures that middleware failures don't crash the bot. The comprehensive logging provides excellent observability without compromising performance.


194-229: The retry mechanism with exponential backoff is elegantly engineered, sir.

This generic retry utility with configurable parameters demonstrates excellent reusability. The exponential backoff calculation correctly caps at maxDelayMs to prevent excessive delays, and the detailed logging provides clear visibility into retry attempts.


424-439: The graceful shutdown sequence is architecturally sound, sir.

The shutdown order (webhook consumer → BotsStore → database) respects the dependency hierarchy perfectly. The error handling ensures that each shutdown step is attempted even if previous steps fail.

src/sdk/types.ts (2)

1-27: The foundational interfaces demonstrate excellent architectural foresight, sir.

The DatabaseConnection and Storage interfaces provide clean abstractions that enable dependency injection and testing. The StorageConfig with optional parameters allows for flexible deployment configurations.


29-100: The domain entity interfaces show impressive completeness, sir.

The TicketData, CustomerData, UserData, and related interfaces capture all necessary fields for the support ticket workflow. The optional fields provide flexibility while maintaining type safety. The inclusion of metadata fields allows for future extensibility.

src/commands/index.ts (2)

29-61: The adaptive messaging strategy in startCommand is brilliantly contextual, sir.

The function correctly provides different experiences for private versus group chats, offering comprehensive bot information in private while keeping group interactions concise. This demonstrates excellent user experience design.


527-685: The handleEmailField function orchestrates the ticket creation workflow excellently, sir.

The sequential API calls to create customer, user, and ticket data demonstrate proper dependency management. The comprehensive error handling ensures users receive appropriate feedback, and the message cleanup reduces visual clutter effectively.

src/services/unthread.ts (11)

20-82: Sir, your interface architecture demonstrates excellent structural integrity.

The TypeScript interfaces are well-designed and provide comprehensive type safety for the API interactions. The separation of concerns between different parameter types shows sophisticated engineering.


89-89: Function signature enhancement detected - type safety protocols engaged.

The addition of explicit TypeScript typing to the extractCustomerCompanyName function significantly improves code reliability and developer experience.


153-153: Type annotation upgrade successfully implemented.

The capitalizeCompanyName function now benefits from proper TypeScript typing, enhancing both readability and compile-time error detection.


175-175: Error handling architecture substantially improved, sir.

Replacing process.exit() calls with proper error throwing is a significant improvement. This allows for proper error propagation and testing capabilities.

Also applies to: 180-180


184-184: Type-safe cache implementation detected.

The customer cache now utilizes proper TypeScript generics with Map<string, Customer>, providing compile-time type checking for cache operations.


265-302: Modular function extraction demonstrates excellent engineering principles.

The createTicketJSON helper function provides clean separation of concerns and eliminates code duplication. The implementation is structurally sound.


310-320: Message handling architecture shows consistent design patterns.

The sendMessage and sendMessageJSON function pair follows the same excellent modular approach as the ticket creation functions, maintaining consistency throughout the service layer.

Also applies to: 329-355


400-400: Function signature enhancement noted.

The getTicketFromReply function now properly returns Promise<TicketData | null> with the imported TicketData type, significantly improving type safety.


408-408: Type safety improvement confirmed - previous concern resolved.

The metadata access no longer uses the problematic (ticketData as any).metadata pattern from the previous review. The current implementation !!ticketData.metadata is type-safe and appropriate.


475-475: Customer and user management functions demonstrate robust architecture.

The getOrCreateCustomer and getOrCreateUser functions implement excellent caching strategies and proper error handling. The database integration patterns are well-designed.

Also applies to: 558-558


620-620: Export statement demonstrates thoughtful module design.

The explicit export of customerCache provides appropriate access to the cache while maintaining encapsulation principles.

Comment on lines +237 to +270
try {
// Initialize database connection with retry logic
await retryWithBackoff(
async () => {
await db.connect();
LogEngine.info('Database connection established');
},
5, // max retries
2000, // initial delay: 2 seconds
30000, // max delay: 30 seconds
'Database connection'
);
LogEngine.info('Database initialized successfully');

// Initialize the BotsStore with retry logic
await retryWithBackoff(
async () => {
await BotsStore.initialize(db, process.env.PLATFORM_REDIS_URL);
LogEngine.info('BotsStore connection established');
},
5, // max retries
2000, // initial delay: 2 seconds
30000, // max delay: 30 seconds
'BotsStore initialization'
);
LogEngine.info('BotsStore initialized successfully');
} catch (error) {
const err = error as Error;
LogEngine.error('Failed to initialize database or storage after all retry attempts', {
error: err.message,
maxRetries: 5
});
process.exit(1);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider making the initialization sequence more atomic, sir.

While the retry logic for individual components is excellent, if database initialization succeeds but BotsStore fails, the process exits without cleaning up the database connection. Consider implementing a cleanup sequence for partial initialization failures.

+let dbInitialized = false;
+
 try {
     // Initialize database connection with retry logic
     await retryWithBackoff(/* ... */);
+    dbInitialized = true;
     LogEngine.info('Database initialized successfully');
     
     // Initialize the BotsStore with retry logic
     await retryWithBackoff(/* ... */);
     LogEngine.info('BotsStore initialized successfully');
 } catch (error) {
     const err = error as Error;
     LogEngine.error('Failed to initialize database or storage after all retry attempts', {
         error: err.message,
         maxRetries: 5
     });
+    
+    // Cleanup partial initialization
+    if (dbInitialized) {
+        try {
+            await db.close();
+            LogEngine.info('Database connection closed during cleanup');
+        } catch (cleanupError) {
+            LogEngine.error('Failed to cleanup database during initialization failure', {
+                error: (cleanupError as Error).message
+            });
+        }
+    }
+    
     process.exit(1);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
// Initialize database connection with retry logic
await retryWithBackoff(
async () => {
await db.connect();
LogEngine.info('Database connection established');
},
5, // max retries
2000, // initial delay: 2 seconds
30000, // max delay: 30 seconds
'Database connection'
);
LogEngine.info('Database initialized successfully');
// Initialize the BotsStore with retry logic
await retryWithBackoff(
async () => {
await BotsStore.initialize(db, process.env.PLATFORM_REDIS_URL);
LogEngine.info('BotsStore connection established');
},
5, // max retries
2000, // initial delay: 2 seconds
30000, // max delay: 30 seconds
'BotsStore initialization'
);
LogEngine.info('BotsStore initialized successfully');
} catch (error) {
const err = error as Error;
LogEngine.error('Failed to initialize database or storage after all retry attempts', {
error: err.message,
maxRetries: 5
});
process.exit(1);
}
let dbInitialized = false;
try {
// Initialize database connection with retry logic
await retryWithBackoff(
async () => {
await db.connect();
LogEngine.info('Database connection established');
},
5, // max retries
2000, // initial delay: 2 seconds
30000, // max delay: 30 seconds
'Database connection'
);
dbInitialized = true;
LogEngine.info('Database initialized successfully');
// Initialize the BotsStore with retry logic
await retryWithBackoff(
async () => {
await BotsStore.initialize(db, process.env.PLATFORM_REDIS_URL);
LogEngine.info('BotsStore connection established');
},
5, // max retries
2000, // initial delay: 2 seconds
30000, // max delay: 30 seconds
'BotsStore initialization'
);
LogEngine.info('BotsStore initialized successfully');
} catch (error) {
const err = error as Error;
LogEngine.error('Failed to initialize database or storage after all retry attempts', {
error: err.message,
maxRetries: 5
});
// Cleanup partial initialization
if (dbInitialized) {
try {
await db.close();
LogEngine.info('Database connection closed during cleanup');
} catch (cleanupError) {
LogEngine.error('Failed to cleanup database during initialization failure', {
error: (cleanupError as Error).message
});
}
}
process.exit(1);
}
🤖 Prompt for AI Agents
In src/index.ts around lines 237 to 270, the current initialization retries
database and BotsStore separately, but if BotsStore initialization fails after
the database connects, the database connection remains open. To fix this, add a
cleanup step in the catch block that closes or disconnects the database
connection if it was successfully established before the failure. This ensures
partial initialization does not leave resources open before exiting the process.

*/
export async function getAgentMessageFromReply(replyToMessageId) {
export async function getAgentMessageFromReply(replyToMessageId: number): Promise<any | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return type consistency requires standardization.

Several functions return Promise<any | null> or Promise<any[]> which reduces type safety benefits.

Consider defining specific interfaces for these return types:

+interface AgentMessageData {
+  // Define expected structure
+}

+interface TicketConfirmation {
+  // Define expected structure  
+}

-export async function getAgentMessageFromReply(replyToMessageId: number): Promise<any | null> {
+export async function getAgentMessageFromReply(replyToMessageId: number): Promise<AgentMessageData | null> {

-export async function getTicketsForChat(chatId: number): Promise<any[]> {
+export async function getTicketsForChat(chatId: number): Promise<TicketConfirmation[]> {

Also applies to: 452-452

🤖 Prompt for AI Agents
In src/services/unthread.ts at lines 430 and 452, the functions return
Promise<any | null> or Promise<any[]> which reduces type safety. Define specific
TypeScript interfaces or types that accurately represent the expected return
data structure and update the function return types accordingly to use these
interfaces instead of any. This will improve type safety and code clarity.

*/
export async function createTicket({ groupChatName, customerId, summary, onBehalfOf }) {
export async function createTicket(params: CreateTicketParams): Promise<any> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return type specification requires attention.

The createTicket function returns Promise<any> which undermines the type safety benefits of the TypeScript migration.

+interface CreateTicketResponse {
+  id: string;
+  friendlyId: string;
+  // Add other expected response fields
+}

-export async function createTicket(params: CreateTicketParams): Promise<any> {
+export async function createTicket(params: CreateTicketParams): Promise<CreateTicketResponse> {
🤖 Prompt for AI Agents
In src/services/unthread.ts at line 238, the createTicket function currently
returns Promise<any>, which reduces type safety. Update the return type to a
more specific interface or type that accurately represents the resolved value of
the promise returned by createTicket. Define or import the appropriate type if
necessary to ensure strong typing and improve code reliability.

Comment on lines +581 to +595
const userData: any = {
id: `user_${telegramUserId}`,
telegramUserId: telegramUserId,
telegramUsername: username || null,
unthreadName: unthreadName,
unthreadEmail: unthreadEmail,
createdAt: new Date().toISOString()
});
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};

if (username) {
userData.telegramUsername = username;
userData.username = username;
}

await BotsStore.storeUser(userData);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

User data object construction requires type safety enhancement.

Using any type for userData object construction bypasses TypeScript's benefits and could lead to runtime errors.

+interface UserData {
+  id: string;
+  telegramUserId: number;
+  unthreadName: string;
+  unthreadEmail: string;
+  createdAt: string;
+  updatedAt: string;
+  telegramUsername?: string;
+  username?: string;
+}

-const userData: any = {
+const userData: UserData = {
   id: `user_${telegramUserId}`,
   telegramUserId: telegramUserId,
   unthreadName: unthreadName,
   unthreadEmail: unthreadEmail,
   createdAt: new Date().toISOString(),
   updatedAt: new Date().toISOString()
};

if (username) {
-  userData.telegramUsername = username;
-  userData.username = username;
+  (userData as UserData & { telegramUsername: string; username: string }).telegramUsername = username;
+  (userData as UserData & { telegramUsername: string; username: string }).username = username;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userData: any = {
id: `user_${telegramUserId}`,
telegramUserId: telegramUserId,
telegramUsername: username || null,
unthreadName: unthreadName,
unthreadEmail: unthreadEmail,
createdAt: new Date().toISOString()
});
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
if (username) {
userData.telegramUsername = username;
userData.username = username;
}
await BotsStore.storeUser(userData);
// Define a proper UserData interface
interface UserData {
id: string;
telegramUserId: number;
unthreadName: string;
unthreadEmail: string;
createdAt: string;
updatedAt: string;
telegramUsername?: string;
username?: string;
}
const userData: UserData = {
id: `user_${telegramUserId}`,
telegramUserId: telegramUserId,
unthreadName: unthreadName,
unthreadEmail: unthreadEmail,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
if (username) {
// Narrow the type so we know these optional props are set
(userData as UserData & { telegramUsername: string; username: string }).telegramUsername = username;
(userData as UserData & { telegramUsername: string; username: string }).username = username;
}
await BotsStore.storeUser(userData);
🤖 Prompt for AI Agents
In src/services/unthread.ts around lines 581 to 595, the userData object is
typed as any, which disables TypeScript's type checking and risks runtime
errors. Define a proper interface or type for the userData structure that
includes all expected properties like id, telegramUserId, unthreadName,
unthreadEmail, createdAt, updatedAt, and optional username fields. Then, replace
the any type with this specific type to ensure type safety and leverage
TypeScript's static checking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release Release Status (PR)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add status notification to telegram
1 participant