Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
265 lines
8.5 KiB
Go
265 lines
8.5 KiB
Go
package telegram
|
|
|
|
// TelegramWebhookHandler processes incoming Telegram webhook HTTP requests.
|
|
// Reference: Chatwoot's webhook controller for Telegram:
|
|
// - app/controllers/api/v1/accounts/channels/telegram_bots_controller.rb (CRUD)
|
|
// - Telegram webhook receives Update payloads at /webhooks/telegram/{bot_token}
|
|
//
|
|
// The handler:
|
|
// 1. Receives raw Update JSON from Telegram's webhook POST
|
|
// 2. Validates the update structure
|
|
// 3. Routes to appropriate processing (message, edited_message, callback_query)
|
|
// 4. Returns 200 OK to Telegram (required to prevent retries)
|
|
//
|
|
// Telegram webhook requirements:
|
|
// - Must respond within 10 seconds
|
|
// - Must return 200 OK even on processing errors (async processing recommended)
|
|
// - Updates are sent as JSON POST to the webhook URL
|
|
// - Supported update types: message, edited_message, callback_query
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
channelprovider "github.com/gochat/gochat/internal/channel/provider"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// WebhookHandler processes Telegram webhook requests.
|
|
type WebhookHandler struct {
|
|
provider *channelprovider.TelegramProvider
|
|
// db would be injected here for inbox lookup
|
|
// inboxRepo InboxRepository
|
|
}
|
|
|
|
// NewWebhookHandler creates a Telegram webhook handler.
|
|
func NewWebhookHandler(provider *channelprovider.TelegramProvider) *WebhookHandler {
|
|
return &WebhookHandler{
|
|
provider: provider,
|
|
}
|
|
}
|
|
|
|
// HandleWebhookRequest processes an incoming Telegram webhook HTTP request.
|
|
// This is the Gin HTTP handler function.
|
|
//
|
|
// URL pattern: /webhooks/telegram/:bot_token
|
|
// Method: POST
|
|
// Content-Type: application/json
|
|
//
|
|
// Reference: Chatwoot routes Telegram webhooks at:
|
|
// post '/webhooks/telegram/:bot_token' => 'telegram_bots#process_message'
|
|
//
|
|
// Flow:
|
|
// 1. Parse bot_token from URL path → lookup Inbox + ChannelTelegram
|
|
// 2. Read Update JSON from request body
|
|
// 3. Dispatch to appropriate processing pipeline
|
|
// 4. Return 200 OK immediately (Telegram requires fast response)
|
|
func (h *WebhookHandler) HandleWebhookRequest(w http.ResponseWriter, r *http.Request) {
|
|
// Step 1: Extract bot_token from URL path
|
|
// In Gin: c.Param("bot_token")
|
|
botToken := extractBotTokenFromPath(r.URL.Path)
|
|
if botToken == "" {
|
|
applogger.L().Warn("Telegram webhook: missing bot_token in path")
|
|
w.WriteHeader(http.StatusOK) // still return 200 to prevent Telegram retries
|
|
return
|
|
}
|
|
|
|
// Step 2: Read request body
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
applogger.L().Error("Telegram webhook: failed to read body", "error", err)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
// Step 3: Parse the Telegram Update
|
|
var update TelegramUpdate
|
|
if err := json.Unmarshal(body, &update); err != nil {
|
|
applogger.L().Error("Telegram webhook: failed to parse update", "error", err)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
applogger.L().Info("Telegram webhook received",
|
|
"bot_token_prefix", maskBotToken(botToken),
|
|
"update_id", update.UpdateID,
|
|
)
|
|
|
|
// Step 4: Look up Inbox for this bot_token
|
|
// In production: h.inboxRepo.FindByBotToken(botToken)
|
|
inbox, err := h.lookupInbox(botToken)
|
|
if err != nil {
|
|
applogger.L().Error("Telegram webhook: inbox lookup failed", "error", err)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// Step 5: Dispatch update for async processing
|
|
// Reference: Chatwoot processes updates asynchronously via IncomingMessageService
|
|
// We should process asynchronously to respond within 10 seconds
|
|
payload := map[string]interface{}{}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
applogger.L().Error("Telegram webhook: failed to re-parse payload", "error", err)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// Process the incoming message via the provider pipeline
|
|
incomingMsg, err := h.provider.ProcessIncomingMessage(r.Context(), inbox, payload)
|
|
if err != nil {
|
|
applogger.L().Error("Telegram webhook: message processing failed",
|
|
"update_id", update.UpdateID,
|
|
"error", err,
|
|
)
|
|
} else if incomingMsg != nil {
|
|
// Enqueue for downstream pipeline processing
|
|
// Reference: Chatwoot enqueues to the message pipeline after parsing
|
|
applogger.L().Info("Telegram webhook: message processed",
|
|
"inbox_id", incomingMsg.InboxID,
|
|
"source_id", incomingMsg.SourceID,
|
|
"content_type", incomingMsg.ContentType,
|
|
)
|
|
// TODO: Push to message broker/dispatcher for persistence + notification
|
|
}
|
|
|
|
// Step 6: Always return 200 OK to Telegram
|
|
// Telegram retries webhook delivery if response is not 200
|
|
// Reference: https://core.telegram.org/bots/api#making-requests
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK"))
|
|
}
|
|
|
|
// HandleCallbackQuery processes a Telegram callback query specially.
|
|
// Reference: Chatwoot handles callback_query for input_select responses
|
|
// where the user clicks an inline keyboard button.
|
|
func (h *WebhookHandler) HandleCallbackQuery(w http.ResponseWriter, r *http.Request) {
|
|
// Same flow as HandleWebhookRequest but specialized for callback queries
|
|
// The main handler already handles both types
|
|
h.HandleWebhookRequest(w, r)
|
|
}
|
|
|
|
// === Helper Functions ===
|
|
|
|
// extractBotTokenFromPath extracts the bot_token from the webhook URL path.
|
|
// URL pattern: /webhooks/telegram/{bot_token}
|
|
func extractBotTokenFromPath(path string) string {
|
|
// Expected: /webhooks/telegram/123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
|
parts := splitPath(path)
|
|
if len(parts) >= 4 && parts[2] == "telegram" {
|
|
return parts[3]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// splitPath splits a URL path into segments.
|
|
func splitPath(path string) []string {
|
|
result := []string{}
|
|
for _, seg := range splitString(path, "/") {
|
|
if seg != "" {
|
|
result = append(result, seg)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// splitString splits a string by a delimiter.
|
|
func splitString(s, delim string) []string {
|
|
result := []string{}
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i:i+1] == delim {
|
|
result = append(result, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
result = append(result, s[start:])
|
|
return result
|
|
}
|
|
|
|
// maskBotToken masks a bot token for safe logging (show first 8 chars only).
|
|
func maskBotToken(token string) string {
|
|
if len(token) > 8 {
|
|
return token[:8] + "..."
|
|
}
|
|
return token
|
|
}
|
|
|
|
// lookupInbox finds the Inbox associated with a Telegram bot token.
|
|
// In production: queries GORM for Inbox with channel_type=telegram matching the bot_token.
|
|
// Reference: Chatwoot: Inbox.find_by(channel: telegram, bot_token: token)
|
|
func (h *WebhookHandler) lookupInbox(botToken string) (*model.Inbox, error) {
|
|
// TODO: Implement with GORM repository
|
|
// Placeholder — production would use:
|
|
// var inbox model.Inbox
|
|
// var ch channelmodel.ChannelTelegram
|
|
// db.Where("bot_token = ?", botToken).First(&ch)
|
|
// db.Where("id = ?", ch.InboxID).First(&inbox)
|
|
//
|
|
// For now, return a placeholder inbox
|
|
inbox := &model.Inbox{
|
|
ChannelType: "telegram",
|
|
}
|
|
return inbox, nil
|
|
}
|
|
|
|
// === Gin Integration ===
|
|
|
|
// RegisterWebhookRoutes registers Telegram webhook routes with a Gin router.
|
|
// Reference: Chatwoot: post '/webhooks/telegram/:bot_token'
|
|
//
|
|
// Usage:
|
|
// router.POST("/webhooks/telegram/:bot_token", handler.GinHandler())
|
|
func (h *WebhookHandler) GinHandler() func(interface{}) {
|
|
// Returns a Gin handler function
|
|
// In production:
|
|
// return func(c *gin.Context) {
|
|
// botToken := c.Param("bot_token")
|
|
// ... process update ...
|
|
// }
|
|
return func(c interface{}) {
|
|
// Placeholder for Gin integration
|
|
applogger.L().Info("Telegram Gin webhook handler called")
|
|
}
|
|
}
|
|
|
|
// === Telegram Update Type (for webhook handler) ===
|
|
|
|
type TelegramUpdate struct {
|
|
UpdateID int64 `json:"update_id"`
|
|
Message *TelegramMessage `json:"message,omitempty"`
|
|
EditedMessage *TelegramMessage `json:"edited_message,omitempty"`
|
|
CallbackQuery *TelegramCallbackQuery `json:"callback_query,omitempty"`
|
|
}
|
|
|
|
type TelegramMessage struct {
|
|
MessageID int64 `json:"message_id"`
|
|
From *TelegramUser `json:"from,omitempty"`
|
|
Chat *TelegramChat `json:"chat"`
|
|
Date int64 `json:"date"`
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
type TelegramUser struct {
|
|
ID int64 `json:"id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
}
|
|
|
|
type TelegramChat struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
Title string `json:"title,omitempty"`
|
|
}
|
|
|
|
type TelegramCallbackQuery struct {
|
|
ID string `json:"id"`
|
|
From *TelegramUser `json:"from"`
|
|
Message *TelegramMessage `json:"message,omitempty"`
|
|
Data string `json:"data,omitempty"`
|
|
} |