Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
g.PUT("/api/v1/webhooks/{id}/toggle", perm(handleToggleWebhook, "webhooks:manage"))
g.POST("/api/v1/webhooks/{id}/test", perm(handleTestWebhook, "webhooks:manage"))

// Telegram webhook (public endpoint, no auth required - Telegram sends updates here).
g.POST("/api/v1/inboxes/telegram/{id}/webhook", handleTelegramWebhook)

// Context Links.
g.GET("/api/v1/context-links", perm(handleGetContextLinks, "context_links:manage"))
g.GET("/api/v1/context-links/active", auth(handleGetActiveContextLinks))
Expand Down
22 changes: 22 additions & 0 deletions cmd/inboxes.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/abhinavxd/libredesk/internal/inbox"
"github.com/abhinavxd/libredesk/internal/inbox/channel/email/oauth"
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
"github.com/abhinavxd/libredesk/internal/inbox/channel/telegram"
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
Expand Down Expand Up @@ -340,6 +341,13 @@ func validateInbox(app *App, inbox imodels.Inbox) error {
return err
}
}

// Validate telegram channel config.
if inbox.Channel == telegram.ChannelTelegram {
if err := validateTelegramConfig(app, inbox.Config); err != nil {
return err
}
}
return nil
}

Expand Down Expand Up @@ -453,3 +461,17 @@ func trimEmailConfig(cfg *imodels.Config) {
cfg.OAuth.TenantID = strings.TrimSpace(cfg.OAuth.TenantID)
}
}

// validateTelegramConfig validates the Telegram inbox configuration.
func validateTelegramConfig(app *App, configJSON json.RawMessage) error {
var cfg telegram.Config
if err := json.Unmarshal(configJSON, &cfg); err != nil {
return envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.somethingWentWrong"), nil)
}

if cfg.BotToken == "" {
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "bot_token"), nil)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return nil
}
31 changes: 31 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/abhinavxd/libredesk/internal/inbox"
"github.com/abhinavxd/libredesk/internal/inbox/channel/email"
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
"github.com/abhinavxd/libredesk/internal/inbox/channel/telegram"
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
"github.com/abhinavxd/libredesk/internal/macro"
"github.com/abhinavxd/libredesk/internal/media"
Expand Down Expand Up @@ -740,6 +741,34 @@ func initLiveChatInbox(inboxRecord imodels.Inbox, msgStore inbox.MessageStore, u
return inbox, nil
}

// initTelegramInbox initializes the Telegram bot inbox.
func initTelegramInbox(inboxRecord imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore) (inbox.Inbox, error) {
var config telegram.Config

// Load JSON data into Koanf.
if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}

if err := ko.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's see the actual code at lines 748-755 and surrounding context
fd cmd/init.go | head -1 | xargs -I {} bash -c 'wc -l {} && cat -n {} | sed -n "740,760p"'

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Let's also search for the function definition and related code
rg -n "initTelegramInbox" cmd/init.go -A 20 -B 2

Repository: abhinavxd/libredesk

Length of output: 2083


🏁 Script executed:

# Check where the global `ko` is declared
rg -n "^var.*ko|:= koanf\.|ko\s*=" cmd/init.go | head -20

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Look for other inbox initialization functions to see if they have similar patterns
rg -n "func init.*Inbox" cmd/init.go

Repository: abhinavxd/libredesk

Length of output: 576


🏁 Script executed:

# Look for ko variable declaration at package level in cmd/init.go
rg -n "^var.*ko|^ko\s*:=" cmd/init.go | head -10

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Check initEmailInbox implementation
rg -n "func initEmailInbox" cmd/init.go -A 40

Repository: abhinavxd/libredesk

Length of output: 1871


🏁 Script executed:

# Check initLiveChatInbox implementation
rg -n "func initLiveChatInbox" cmd/init.go -A 30

Repository: abhinavxd/libredesk

Length of output: 1407


🏁 Script executed:

# Search for all usages of 'ko' in the file to understand the full pattern
rg -n "\bko\." cmd/init.go

Repository: abhinavxd/libredesk

Length of output: 4584


🏁 Script executed:

# Check for package-level variable declarations at the beginning of the file
head -100 cmd/init.go | cat -n | grep -E "var|const"

Repository: abhinavxd/libredesk

Length of output: 391


🏁 Script executed:

# Search for ko declaration - look at the very beginning of the file
head -200 cmd/init.go | cat -n

Repository: abhinavxd/libredesk

Length of output: 8974


🏁 Script executed:

# Look for ko declaration more broadly - might be before line 74
rg -n "^var\s+ko\b|^ko\s*:=\s*koanf" cmd/init.go

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Check imports to understand Koanf source
head -50 cmd/init.go | grep -i koanf

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Look for global ko variable - check around main function and beyond line 200
tail -n +200 cmd/init.go | head -100 | cat -n

Repository: abhinavxd/libredesk

Length of output: 3454


🏁 Script executed:

# Better approach - search for "var" at the very file level (no indentation)
rg "^var\s" cmd/init.go

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Check the main function to see how ko is created/used
rg -n "func main" cmd/init.go -A 50

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

# Look for ko usage without seeing declaration - it might be in a different file
# Search for "var ko" across all Go files
fd "\.go$" | xargs rg "^var\s+ko\b" 2>/dev/null

Repository: abhinavxd/libredesk

Length of output: 47


🏁 Script executed:

# Or check all files in cmd directory
fd "\.go$" cmd/ | xargs rg "var\s+ko\s*\*koanf" 2>/dev/null

Repository: abhinavxd/libredesk

Length of output: 47


🏁 Script executed:

# Let's search the entire codebase for where ko is initialized
rg "ko\s*=\s*koanf\.New" --type go

Repository: abhinavxd/libredesk

Length of output: 105


🏁 Script executed:

# Alternative: look at the main function location
find . -name "*.go" -exec rg -l "func main" {} \;

Repository: abhinavxd/libredesk

Length of output: 77


🏁 Script executed:

# Found it! ko is in cmd/main.go. Let's see the declaration and context
rg -n "ko\s*=" cmd/main.go -B 5 -A 5 | head -30

Repository: abhinavxd/libredesk

Length of output: 331


🏁 Script executed:

# Now let's understand Koanf's Load() behavior by checking if there are any tests or comments
# in the repository that show how Load() works with multiple calls
rg -n "ko\.Load" cmd/init.go | head -20

Repository: abhinavxd/libredesk

Length of output: 666


🌐 Web query:

Koanf Load() method behavior accumulate merge or replace state

💡 Result:

The Load method in Koanf is designed to accumulate configuration state by merging new data into the existing configuration [1][2]. It does not replace the entire state [1][2]. When you call Load, Koanf performs a recursive merge of the new configuration map into the current internal state [1][2]. By default, this process merges nested maps (map[string]any) while overwriting static values such as strings, integers, or slices [1][2]. Key behaviors of this accumulation process include: 1. Incremental Merging: Every successive call to Load or Merge merges new configuration data into the existing set [1][2]. This allows you to layer configurations from multiple sources (e.g., loading a base YAML file followed by environment variable overrides) in any order [1][2]. 2. Recursive Merging: For nested structures, Koanf traverses the maps and merges them recursively rather than simply replacing the top-level key [1][2]. 3. Strict Merge Option: If the default merging behavior is not desired, you can enable StrictMerge in the Koanf configuration [1][2]. When enabled, Load will return an error if it detects conflicting keys or types during the merge process [1][2]. 4. Customization: You can further control how data is merged by providing a custom merge function using the WithMergeFunc option [1][2]. If you need to clear the existing state and start fresh, you must create a new Koanf instance (e.g., k = koanf.New(".")) rather than relying on Load to reset the state [3].

Citations:


🏁 Script executed:

# Final verification: confirm the sequence of calls to these functions to ensure they're truly sequential
rg -n "initEmailInbox|initLiveChatInbox|initTelegramInbox" cmd/init.go

Repository: abhinavxd/libredesk

Length of output: 908


🏁 Script executed:

# Check how makeInboxInitializer is used to confirm multiple inboxes are initialized in sequence
rg -n "makeInboxInitializer\|InitInboxes" cmd/init.go -B 2 -A 5

Repository: abhinavxd/libredesk

Length of output: 45


Use an isolated Koanf instance for per-inbox config parsing to prevent cross-inbox state bleed.

Loading each inbox's JSON configuration into the global ko causes accumulation across sequential inbox initializations. Koanf's Load() merges state rather than replacing it, so configs from previously loaded inboxes remain in memory and can contaminate subsequent inbox parsing. Parse each inbox config with a fresh local Koanf instance.

Proposed fix
 func initTelegramInbox(inboxRecord imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore) (inbox.Inbox, error) {
 	var config telegram.Config
+	k := koanf.New(".")

 	// Load JSON data into Koanf.
-	if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
+	if err := k.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
 		return nil, fmt.Errorf("loading config: %w", err)
 	}

-	if err := ko.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
+	if err := k.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
 		return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
 	}

Apply the same fix to initEmailInbox (line 654) and initLiveChatInbox (line 716), which have the identical pattern.

📝 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
// Load JSON data into Koanf.
if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
if err := ko.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
}
k := koanf.New(".")
// Load JSON data into Koanf.
if err := k.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
if err := k.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/init.go` around lines 748 - 755, The code currently reuses the shared ko
variable and calls ko.Load(...) which merges state across inboxes; create a
fresh local Koanf instance for each inbox (e.g., instantiate a new koanf via
koanf.New(...) or equivalent) and call that instance's Load(...) and
UnmarshalWithConf(...) against inboxRecord.Config so the parsing is isolated;
apply the same change where the pattern appears in initEmailInbox and
initLiveChatInbox so each function creates and uses its own local Koanf instead
of the shared ko.


inb, err := telegram.New(msgStore, usrStore, telegram.Opts{
ID: inboxRecord.ID,
Config: config,
Lo: initLogger("telegram_inbox"),
})

if err != nil {
return nil, fmt.Errorf("initializing `%s` inbox: `%s` error : %w", inboxRecord.Channel, inboxRecord.Name, err)
}

log.Printf("`%s` inbox successfully initialized", inboxRecord.Name)

return inb, nil
}

// makeInboxInitializer creates an inbox initializer function.
func makeInboxInitializer(mgr *inbox.Manager, signAvatarURL func(*null.String)) func(imodels.Inbox, inbox.MessageStore, inbox.UserStore) (inbox.Inbox, error) {
return func(inboxR imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore) (inbox.Inbox, error) {
Expand All @@ -748,6 +777,8 @@ func makeInboxInitializer(mgr *inbox.Manager, signAvatarURL func(*null.String))
return initEmailInbox(inboxR, msgStore, usrStore, mgr)
case inbox.ChannelLiveChat:
return initLiveChatInbox(inboxR, msgStore, usrStore, signAvatarURL)
case inbox.ChannelTelegram:
return initTelegramInbox(inboxR, msgStore, usrStore)
default:
return nil, fmt.Errorf("unknown inbox channel: %s", inboxR.Channel)
}
Expand Down
58 changes: 58 additions & 0 deletions cmd/telegram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"encoding/json"
"strconv"

"github.com/abhinavxd/libredesk/internal/inbox"
"github.com/abhinavxd/libredesk/internal/inbox/channel/telegram"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
)

func handleTelegramWebhook(r *fastglue.Request) error {
var app = r.Context.(*App)

inboxID, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
if err != nil || inboxID == 0 {
r.RequestCtx.SetStatusCode(fasthttp.StatusBadRequest)
return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

inb, err := app.inbox.Get(inboxID)
if err != nil {
r.RequestCtx.SetStatusCode(fasthttp.StatusNotFound)
return nil
}

if inb.Channel() != inbox.ChannelTelegram {
r.RequestCtx.SetStatusCode(fasthttp.StatusBadRequest)
return nil
}

tgInbox, ok := inb.(*telegram.Telegram)
if !ok {
r.RequestCtx.SetStatusCode(fasthttp.StatusInternalServerError)
return nil
}

// Verify webhook authenticity using X-Telegram-Bot-Api-Secret-Token header.
secretToken := string(r.RequestCtx.Request.Header.Peek("X-Telegram-Bot-Api-Secret-Token"))
if !tgInbox.VerifyWebhook(secretToken) {
r.RequestCtx.SetStatusCode(fasthttp.StatusUnauthorized)
return nil
}

var update telegram.Update
if err := json.Unmarshal(r.RequestCtx.PostBody(), &update); err != nil {
r.RequestCtx.SetStatusCode(fasthttp.StatusBadRequest)
return nil
}

if err := tgInbox.ProcessWebhookUpdate(update); err != nil {
app.lo.Error("telegram webhook: error processing update", "error", err)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Should return error? instead of 200?

}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

r.RequestCtx.SetStatusCode(fasthttp.StatusOK)
return nil
Comment on lines +58 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "telegram.go" -type f

Repository: abhinavxd/libredesk

Length of output: 127


🏁 Script executed:

head -70 cmd/telegram.go | tail -20

Repository: abhinavxd/libredesk

Length of output: 726


🏁 Script executed:

rg "ProcessWebhookUpdate" --type go -B 3 -A 3

Repository: abhinavxd/libredesk

Length of output: 774


🏁 Script executed:

rg "handleTelegramWebhook" --type go -B 5 -A 15

Repository: abhinavxd/libredesk

Length of output: 2678


🏁 Script executed:

rg "func.*ProcessWebhookUpdate" --type go -A 30 internal/inbox/channel/telegram/

Repository: abhinavxd/libredesk

Length of output: 2234


🏁 Script executed:

rg "idempoten|duplicate|retry" --type go cmd/telegram.go internal/inbox/channel/telegram/

Repository: abhinavxd/libredesk

Length of output: 45


🏁 Script executed:

cat -n internal/inbox/channel/telegram/telegram.go | head -100

Repository: abhinavxd/libredesk

Length of output: 2957


🏁 Script executed:

rg "func.*ProcessWebhookUpdate" --type go -A 50 internal/inbox/channel/telegram/telegram.go

Repository: abhinavxd/libredesk

Length of output: 1559


🏁 Script executed:

rg "messageStore\.|userStore\." --type go internal/inbox/channel/telegram/telegram.go | head -20

Repository: abhinavxd/libredesk

Length of output: 132


🏁 Script executed:

web_search
Telegram bot webhook HTTP status codes retry behavior

Repository: abhinavxd/libredesk

Length of output: 161


🌐 Web query:

Telegram bot webhook HTTP status codes retry behavior 2xx vs 5xx

💡 Result:

The Telegram Bot API handles webhook delivery based on the HTTP status code returned by your server [1]. If your server responds with a 2xx status code (e.g., 200 OK), Telegram considers the update successfully delivered [1][2]. If your server responds with any status code outside the 2xx range (such as 5xx or 4xx errors), Telegram considers the request unsuccessful [1]. In these cases, Telegram will automatically retry the delivery of the update [1]. The system will continue to attempt redelivery for a reasonable number of attempts before eventually giving up on that specific update [1]. To ensure reliable operation, your webhook listener should be configured to return a 2xx status code immediately upon receiving and successfully processing (or queuing for processing) an update [3][2]. If your server returns non-2xx codes, it will trigger these automatic retries, which can lead to duplicate processing if your logic is not idempotent [1]. You can monitor the status of your webhook, including the last error message and date, using the getWebhookInfo method [1].

Citations:


🏁 Script executed:

rg "func.*ProcessWebhookUpdate" --type go -A 60 internal/inbox/channel/telegram/telegram.go | tail -20

Repository: abhinavxd/libredesk

Length of output: 671


🏁 Script executed:

rg "EnqueueIncoming" --type go -B 2 -A 5

Repository: abhinavxd/libredesk

Length of output: 2164


Return a retriable status on processing failures.

If ProcessWebhookUpdate() returns an error (e.g., message enqueue fails), the update was not durably accepted, but the handler still replies 200 OK. Telegram only retries on non-2xx responses; returning success here causes message loss on transient failures. The SourceID field uniquely identifies each message (telegram_{chat_id}_{message_id}), so returning 500 Internal Server Error on processing failures allows safe retries without duplicate handling.

Return a retriable status on processing failures
 if err := tgInbox.ProcessWebhookUpdate(update); err != nil {
 	app.lo.Error("telegram webhook: error processing update", "error", err)
+	r.RequestCtx.SetStatusCode(fasthttp.StatusInternalServerError)
+	return nil
 }
 
 r.RequestCtx.SetStatusCode(fasthttp.StatusOK)
📝 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
if err := tgInbox.ProcessWebhookUpdate(update); err != nil {
app.lo.Error("telegram webhook: error processing update", "error", err)
}
r.RequestCtx.SetStatusCode(fasthttp.StatusOK)
return nil
if err := tgInbox.ProcessWebhookUpdate(update); err != nil {
app.lo.Error("telegram webhook: error processing update", "error", err)
r.RequestCtx.SetStatusCode(fasthttp.StatusInternalServerError)
return nil
}
r.RequestCtx.SetStatusCode(fasthttp.StatusOK)
return nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/telegram.go` around lines 58 - 63, When
tgInbox.ProcessWebhookUpdate(update) returns an error, do not unconditionally
return 200—log the error as you already do (app.lo.Error("telegram webhook:
error processing update", "error", err)) but also set a non-2xx retriable status
(e.g., r.RequestCtx.SetStatusCode(fasthttp.StatusInternalServerError)) and
return the error so the HTTP handler does not treat it as success; update the
handler code around tgInbox.ProcessWebhookUpdate, app.lo.Error, and
r.RequestCtx.SetStatusCode to implement this behavior so Telegram will retry
using the SourceID-based dedupe.

}
1 change: 1 addition & 0 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var migList = []migFunc{
{"v1.0.1", migrations.V1_0_1},
{"v2.0.0", migrations.V2_0_0},
{"v2.2.0", migrations.V2_2_0},
{"v2.3.0", migrations.V2_3_0},
}

// upgrade upgrades the database to the current version by running SQL migration files
Expand Down
183 changes: 183 additions & 0 deletions frontend/apps/main/src/features/admin/inbox/TelegramInboxForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<template>
<form @submit="onSubmit" class="space-y-6 w-full max-w-lg">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>{{ $t('globals.terms.name') }}</FormLabel>
<FormControl>
<Input type="text" placeholder="My Telegram Bot" v-bind="componentField" />
</FormControl>
<FormDescription>
{{ $t('admin.inbox.telegram.name.description') }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>

<FormField v-slot="{ componentField }" name="bot_token">
<FormItem>
<FormLabel>{{ $t('admin.inbox.telegram.botToken') }}</FormLabel>
<FormControl>
<Input type="password" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" v-bind="componentField" />
</FormControl>
<FormDescription>
{{ $t('admin.inbox.telegram.botToken.description') }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>

<FormField v-slot="{ componentField }" name="bot_name">
<FormItem>
<FormLabel>{{ $t('admin.inbox.telegram.botName') }}</FormLabel>
<FormControl>
<Input type="text" placeholder="@my_support_bot" v-bind="componentField" />
</FormControl>
<FormDescription>
{{ $t('admin.inbox.telegram.botName.description') }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>

<FormField v-slot="{ componentField, handleChange }" name="enabled">
<FormItem>
<SwitchField
:title="$t('globals.terms.enabled')"
:checked="componentField.modelValue"
@update:checked="handleChange"
/>
</FormItem>
</FormField>

<FormField v-slot="{ componentField, handleChange }" name="csat_enabled">
<FormItem>
<SwitchField
:title="$t('admin.inbox.csatSurveys')"
:description="$t('admin.inbox.csatSurveys.description_1')"
:checked="componentField.modelValue"
@update:checked="handleChange"
/>
</FormItem>
</FormField>

<FormField v-slot="{ componentField, handleChange }" name="prompt_tags_on_reply">
<FormItem>
<SwitchField
:title="$t('admin.inbox.promptTagsOnReply')"
:description="$t('admin.inbox.promptTagsOnReply.description')"
:checked="componentField.modelValue"
@update:checked="handleChange"
/>
</FormItem>
</FormField>

<!-- Webhook URL info (shown only in edit mode) -->
<div v-if="!isNewForm && webhookUrl" class="rounded-md border p-4 space-y-2">
<p class="text-sm font-medium">{{ $t('admin.inbox.telegram.webhookUrl') }}</p>
<div class="flex items-center gap-2">
<code class="text-xs bg-muted px-2 py-1 rounded flex-1 break-all">{{ webhookUrl }}</code>
<Button type="button" variant="outline" size="sm" @click="copyWebhookUrl">
{{ $t('globals.terms.copy') }}
</Button>
</div>
<p class="text-xs text-muted-foreground">
{{ $t('admin.inbox.telegram.webhookUrl.description') }}
</p>
</div>

<Button type="submit" :disabled="isLoading">
<Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" />
{{ isNewForm ? $t('globals.messages.create') : $t('globals.messages.save') }}
</Button>
</form>
</template>

<script setup>
import { computed } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@shared-ui/components/ui/button/index.js'
import { Input } from '@shared-ui/components/ui/input/index.js'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@shared-ui/components/ui/form/index.js'
import SwitchField from '@shared-ui/components/SwitchField.vue'
import { Loader2 } from 'lucide-vue-next'
import { useI18n } from 'vue-i18n'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import { useEmitter } from '@/composables/useEmitter'

const { t } = useI18n()
const emitter = useEmitter()

const props = defineProps({
initialValues: {
type: Object,
default: () => ({})
},
submitForm: {
type: Function,
required: true
},
isLoading: {
type: Boolean,
default: false
},
isNewForm: {
type: Boolean,
default: false
}
})

const formSchema = toTypedSchema(
z.object({
name: z.string().min(1, t('globals.messages.required')),
bot_token: z.string().min(1, t('globals.messages.required')),
bot_name: z.string().optional().default(''),
enabled: z.boolean().default(true),
csat_enabled: z.boolean().default(false),
prompt_tags_on_reply: z.boolean().default(false)
})
)

const form = useForm({
validationSchema: formSchema,
initialValues: {
name: props.initialValues?.name || '',
bot_token: props.initialValues?.config?.bot_token || '',
bot_name: props.initialValues?.config?.bot_name || '',
enabled: props.initialValues?.enabled ?? true,
csat_enabled: props.initialValues?.csat_enabled ?? false,
prompt_tags_on_reply: props.initialValues?.prompt_tags_on_reply ?? false
}
})

const webhookUrl = computed(() => {
if (props.initialValues?.id) {
const baseUrl = window.location.origin
return `${baseUrl}/api/v1/inboxes/telegram/${props.initialValues.id}/webhook`
}
return ''
})

const copyWebhookUrl = async () => {
try {
await navigator.clipboard.writeText(webhookUrl.value)
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
description: t('globals.messages.savedSuccessfully')
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (error) {
console.error('Failed to copy:', error)
}
}

const onSubmit = form.handleSubmit(async (values) => {
await props.submitForm(values)
})
</script>
Loading