Skip to main content

LINE Webhook Gateway

Overview

POST /api/line/:id is the single endpoint that the LINE Platform calls directly whenever an event occurs on a LINE Official Account — messages, button taps, follows, blocks, group joins and leaves, beacons, and so on. The :id path parameter is the webhook id bound to one specific LINE OA (the line_oa.webhook_id column).

One thing to understand before reading further: this service does not implement a separate handler per event type. It acts as a thin router and filter that asks only three questions before delegating the work elsewhere:

  1. Does this OA need its raw webhook forwarded to a customer endpoint?
  2. Does this event match the mbox (human agent handoff) conditions or a special postback?
  3. If neither applies, publish the whole payload to the line_webhook queue and let worker-go sort it out.

As a result, events such as follow, unfollow, join, leave, beacon, and videoPlayComplete have no handling code in this project at all. They all fall through to the default path into the line_webhook queue. The real logic for auto-response, friend tracking, and the line_user_friend table lives entirely in worker-go.

The endpoint is fire-and-forget: it immediately returns 200 with {"code":"RES_SUCCESS_001","message":"OK"} to LINE, then processes the event in a background goroutine. LINE enforces a short timeout and retries on slow responses, so responding first is mandatory.

Business Flow

Request intake (handler)

  1. Read :id as webhookId.
  2. Collect every header and lowercase the keys. LINE and Express send them lowercase, but Go canonicalises them, so they must be converted back — otherwise the x-line-signature lookup fails.
  3. Read the body while also retaining the raw bytes in BodyRaw, separate from the parsed map (see Forwarding the Raw Body and Signature).
  4. Spawn a goroutine calling Service.ProcessLine on context.Background(), guarded by recover() so a panic cannot take down the process, then return 200 immediately.

Processing (ProcessLine)

  1. Load the config with HGETALL webhook_config:{webhookId} from Redis (see Redis Cache for Webhook Config).
    • On a cache miss (empty hash), publish the entire payload to line_webhook and stop. worker-go will look the config up from the database itself.
    • On a read error, log and stop immediately without publishing. The message is lost — this matches the original behaviour.
  2. If the config contains forwardWebhookUrl, fire the request onward without waiting for a result (see Forwarding Webhooks to Customer Systems).
  3. Filter events where type == "message". If mboxEnabled == "1" and a message event exists, enter the mbox path (see Handoff to Human Agents). When this path matches it returns immediately without publishing to line_webhook, which effectively silences the bot.
  4. Filter events where type == "postback" when mboxEnabled == "1".
    • postback.data starting with mbox_team: triggers a handoff to the selected team.
    • postback.data starting with appt_cancel: triggers appointment cancellation (see Cancelling Appointments via Postback).
  5. Default path: publish a payload of webhookId, headers, and body to the line_webhook queue as bare JSON for worker-go to run the bot logic.
note

Steps 7 and 8 inspect only the first event of each type (messageEvents[0] and postbackEvents[0]), even though LINE may deliver multiple events per request. This preserves parity with the original NestJS implementation. Also, once mbox decides to hand off, every other event in the same batch is discarded.

Key Files & Functions

MethodRouteAuthHandler
POST/api/line/:idNoneHandler.processLine
POST/api/Line/:idNoneHandler.processLine (same handler)

Both spellings are registered because NestJS and Express match paths case-insensitively (the original declared @Controller('Line')), whereas gin is case-sensitive and real LINE traffic arrives lowercase.

All code lives in the internal/line/ package.

FileKey functions
internal/line/handler.goRegister, Handler.processLine, toLowerHeader, decodeJSONObject
internal/line/service.goService.ProcessLine (the main dispatcher), eventList, eventType, sourceUserID, messageText, postbackData, Service.publish, parseLineOaID, splitSecond

The key type is LineWebhookPayload, holding webhookId, headers, and body, with a custom MarshalJSON that emits the raw bytes as the body field.

Connections to Other Services

  • Redis — reads webhook_config:{webhookId} (read-only) and reads/writes agent_mode:{lineOaId}:{userId} through internal/cache.Cache, which automatically prefixes keys with LINE_MANAGEMENT:.
  • RabbitMQ — the line_exchange exchange with the line_webhook, mbox_handoff, and booking_notification queues, using the queue name as the routing key and bare JSON bodies.
  • No database access — the line module never touches PostgreSQL; everything comes from the Redis cache.
  • worker-go — consumes line_webhook and performs auto-response, friend tracking, and writes to the line_user and line_user_friend tables.
  • cms-api — its line-oa-management module writes webhook_config:* into Redis whenever an OA's configuration changes.