Rich message
Overview
Rich message is the content library: you assemble several content items (text, image, video, tappable image, Flex, or a reference to a Template message) into one record, and LINE delivers them as consecutive message bubbles.
The thing to understand up front: a Rich message is never sent from this menu. It's purely a library that Campaign, Auto-response message, and other automation pull from to do the actual sending.
The other thing to know is that the two kinds of reference here behave differently — an attached Quick reply set is a live reference (resolved fresh on every send), but a Template message picked as a content item is copied into this record at save time, and then kept in sync by a cascade that the template menu runs when a template is edited. That cascade is best-effort, and it does not run when a template is deleted (see Key details).
Who uses it — content and marketing teams preparing material ahead of time for campaigns or auto-reply rules (this module carries a real, enforced ModuleGate on the backend — see Access control).
User journey
1. Review the Rich message list
Open the Rich message management menu. The list is a single table with columns No. / Title / Date created (labeled "(BKK Time)") / Action. The filter bar has 2 fields — search by title, and a created-date range. Filter values are remembered in sessionStorage.
This list has no status column and no on/off switch, unlike the Quick reply and Auto-response message menus — because this module has no concept of deactivation at all (see Key details). Each row has 3 buttons: edit, copy (opens the create form pre-filled from the original as a template), and delete (with confirmation).
2. Create a new Rich message
- Click Create message to open the two-column form (fields on the left, chat preview on the right).
- Enter a title — max 100 characters, and it must not duplicate another Rich message in the same OA (the form spells this out: "Please use a new title that hasn't been used for any other Rich Messages"). Below the title field, a Data Entry Conditions box lists the supported characters and symbols, plus the restriction that it can't start with a symbol.
- Optionally attach one Quick reply set via the Quick reply field, whose hint reads "Attach a saved quick reply. LINE will show its chips when this message is sent" — the chips land on the last bubble of the set.
- Add content items one at a time under Message setting — up to 5 items (the add button is disabled at the cap, with a hint stating the limit). The order in the form is the bubble order LINE users actually see.
- Pick each item's content type from 6 options:
- Text — max 500 characters.
- Image — JPG / JPEG / PNG only, up to 10 MB.
- Video — recommended formats MP4 / M4V / MOV, up to 200 MB. The system generates a thumbnail automatically by grabbing a frame in the browser and uploading it to temporary storage through a dedicated endpoint (
POST /rich-message/gen-thumbnail-video). Save stays disabled until the upload genuinely completes — the form checks thatthumbnailandfileNameactually came back, not merely that a file was picked. - Tappable Image — JPG / JPEG / PNG with a minimum width of 1040px, then a modal opens to draw up to 20 tappable areas, each with one of 3 actions — open a link, send a message, or place a call.
- Flex Message — paste the JSON yourself (the form notes you can copy-paste it in).
- Template Message — pick a template built in the Template message menu; the system fetches its content for the preview right away, with a Reload button in case you just edited the template.
- Watch the live preview in the right column, which shows only the items that pass their type's completeness bar (each type has its own: a text item needs non-blank text, a template item needs its content fetched, and so on).
- Save once every item passes and the title is filled in. On create or copy the system submits directly; on edit it opens a confirmation dialog first ("Are you sure to edit...").
3. Consume it from another menu
- Downstream menus call a dedicated endpoint,
GET /rich-message/find-all-object, which returns three different shapes depending on theformatparameter — object (the default),dropdown, anddropdownGroup— so each screen picks the shape that suits it. - Campaign — selects a Rich message as the content to send (one containing a merge tag can't be used with Broadcast).
- Auto-response message — attaches a Rich message to a keyword for automatic replies.
Key details verified against the source
Confirmed from internal/modules/richmessage/{controller,service,dto}.go in line-management-cms-api-go and internal/autoresponse/transform.go in line-management-worker-go:
- Flex messages are validated against the real LINE API, not just checked for valid JSON — on save the backend wraps your JSON as a flex message and asks LINE to validate it (
ValidateJsonFlexMessage). A rejection returns a 400 pinpointing the index (content[0][flex],content[2][flex], …), and the system collects every item's failure through the whole loop before responding once, rather than stopping at the first broken item — so you see all the problems in one round. Three distinct errors are possible: "Flex message is required" (empty), "Invalid JSON format for flex message" (broken JSON), and "Failed to validate with LINE API" (parseable JSON that LINE won't accept). - A flex message's
altTextis always "New Flex Message" — the backend accepts analtTextfield and uses it as the fallback text when validating with LINE, but the frontend never sends it, so the hardcoded default applies every time. - A "Template Message" item is snapshotted into this record on save, then kept current by a cascade — at save time the backend reads the template row and writes that template's flex into the content item's
flexfield, and at send time the worker reads that storedflexdirectly, never going back to the template row. What keeps the content from going stale lives on the Template message side: every time a template is edited, the system patches the storedflexin every Rich message referencing it (see the Template message page for how that cascade works). But the cascade runs on edit only, never on delete — a deleted template leaves its last content sitting in those Rich messages indefinitely, and they keep sending it fine. If the template is already deleted at the moment you save the Rich message, you get anAPP_007error instead. - Almost every ceiling you see in the form is a frontend-only restriction — the backend DTO imposes no limit on the number of content items at all (the 5-item cap is a constant in a frontend component). Same for the 100-character title, the 500-character text, the 10 MB image, the 1040px minimum width, and the 20-tappable-area cap — none of these are re-validated on the backend. What the backend does enforce is that an image's file type must be
image/jpegorimage/png(no GIF, no WebP) and that the file size must be at most 200,000,000 bytes — the same figure for both images and videos, not the 10 MB the form claims. - The Quick reply reference is a tri-state, and ownership is genuinely verified — the
quickReplyIdfield distinguishes "key not sent at all" (leave the stored value alone), "sent as null or an empty string" (clear the column to NULL), and "sent with a value" (persist it). Before every save the backend confirms that id exists, isn't deleted, and belongs to the caller's OA — otherwise 404, which blocks a silent cross-tenant attach. In practice the frontend always sends the key (an empty string for "None"), so the "key absent" state is never exercised from the UI. - Rich messages have no active/inactive state — every record is active for life — on create the backend hardcodes
status = activeandtype = text(so thetypecolumn is alwaystextregardless of what's actually inside), and the update statement never touches the status column. The module has no status-change route either. So "an enabled Rich message" in other menus really just means "not deleted." The list endpoint's status filter accepts onlyactive/inactiveand rejectsdeletewith "Invalid status." - Deleting a Rich message also cleans out the Auto-response message cache — both before and after the soft delete, the system walks the auto-response Redis hash (
AUTO_RESPONSE:LINE_OA_ID:<id>) and removes every field pointing at this Rich message. The code calls this step twice, and the comment says outright it's preserving the original system's behavior verbatim ("called twice in TS"). What users see is that auto-reply rules attached to a deleted Rich message stop replying immediately, without waiting for a cache expiry. - Moving files from temporary to permanent storage was deliberately fixed to fail loudly — a code comment records that the video copy's result used to be discarded, so a failed copy still persisted a "permanent" URL for an object that was never actually placed there, and the problem stayed invisible until the record was reopened for editing. It now returns a 500 the moment a copy fails, for both the video file and its thumbnail.
- There's one spot where the Go port intentionally diverges from the original system — the
textfield was originally declared optional, which let whitespace-only text (" ") pass validation and then break the send downstream. The Go code therefore requires a non-blank value once an item's type istext, with the comment stating the reason directly: "whitespace-only input slipped through the optional check and broke sends." - The list route has an opt-in cache mode whose key is byte-for-byte compatible with the old system — passing
cache=truetakes a Redis-backed path whose key is built by replicating JavaScript'sJSON.stringify(query), key order included (page, orderBy, order, limit first, then the remaining parameters in the order they appear in the URL), so keys the old system wrote remain usable. And every write — create, update, delete — purges this module's entire cache. - A "Tappable Image" file is never validated at the DTO layer — the DTO validates only the
imagefield;tappableImagearrives as a surplus field (whitelisting is off) and gets consumed later during media processing. So if no file is actually attached, you get a 500 carrying the messageCannot read properties of undefined (reading 'originalName')— a JavaScript TypeError string deliberately preserved for compatibility with the original system. - Reading one record and updating it return different error codes for the same situation —
GET /rich-message/:idanswers 400APP_007when the record is missing or belongs to another OA, whilePUT /rich-message/:idanswers 404 "Rich message not found" in that same case. Both routes do pin the caller'sline_oa_id(the comments note this closed a real cross-tenant read/write hole).
Related menus in this journey
- Quick reply — the attached set is a live reference, and Rich message is the only table the Quick reply menu's deactivate/delete guards scan (deactivating a set a Rich message references warns first; deleting it is blocked outright).
- Template message — the source of "Template Message" content items, as a copy taken at save time, which the template menu then cascades back into when a template is edited (but not when one is deleted).
- Campaign — the main downstream consumer: a campaign creates no content of its own but picks a Rich message to send, and it's where the merge-tag rule applies (a Rich message with a merge tag can't be used with Broadcast).
- Auto-response message — attaches a Rich message to a keyword, and deleting a Rich message here clears the auto-response cache entries pointing at it.
Things to watch out for
- The template-content cascade is best-effort and can fail silently — if the patch fails, the system only writes a log line; it neither fails the template edit nor tells the user. Some Rich messages can therefore be left holding stale content with no warning. If you suspect content didn't update, open that Rich message and re-save it to pull the latest in for certain (the form's Reload button fetches it for the preview, but it only takes effect once you actually save).
- Deleting a template leaves Rich messages sending the old content — the cascade doesn't run on delete, so a Rich message referencing a deleted template keeps sending the last copied content indefinitely, with no error. You only hit the problem when you reopen that record and try to save it, which returns
APP_007. - Editing a Rich message already attached to a campaign or an auto-reply rule changes what actually gets sent, from the very next send — there's no versioning or approval step, and the screen never tells you who references this Rich message (unlike the Quick reply menu's "Used by" card).
- The tappable-image size error message doesn't match the rule actually applied — the form checks 10 MB (the same value as a regular image) and the hint above the upload correctly says 10 MB, but the over-size error still reads "Image must be smaller than 1 MB!" — stale copy, not the real rule.
- GIF and WebP files won't save — both the form and the backend accept only JPEG and PNG (the backend checks the MIME type directly).
- Save can look broken on a video item — this is by design: it stays disabled until the video upload and thumbnail generation genuinely finish. If an upload stalls or fails, the button never becomes ready — re-upload the file.
- A duplicate title is rejected per-field, not as a general message — the response is a 400 pointing at the
titlefield with "Title is already exist," so the UI shows the error right under the title input. - Deletion is a soft delete — the row stays in the database. A true hard delete requires Super Admin and a direct API call.
Access control (verified against the source)
- Frontend — wrapped in a
PermissionGuardchecking theVIEWaction on therich-message-managementsubject. - Backend — all 8 routes in this module are wrapped in a real, enforced
ModuleGate(d, "rich-message")(like the Campaign module, and unlike Quick reply, which has no ModuleGate at all). The code comment states the behavior precisely: a customer-type caller whose org has the "rich-message" module closed by the platform admin gets a 403, while internal (onemoby) callers bypass the gate. - Action-level policy annotations (
create/update/delete/read/readAll) are all declared but run through the same no-op middleware as most other modules — the real enforcement is the module-level ModuleGate, not the action-level policy. DELETE /api/rich-message/:idis a soft delete. A true hard delete,DELETE /api/rich-message/:id/hard, requires Super Admin plus aconfirm=truequery parameter — omit it and you get a 400 whose message is the raw stringErrorResponse.AUS_10(an untranslated string, deliberately preserved from the original system). Aconfirmvalue that isn't literallytrue/falseis rejected with "Validation failed (boolean string is expected)."- Cross-tenant isolation is enforced on every route — reads, lists, and updates all pin the caller's
line_oa_id(the comments note this closed holes that previously allowed reading, editing, and attaching a quick reply across organizations).
Source file reference
Frontend (cms-web-2026)
src/app/rich-message-management/page.tsx,src/app/rich-message-management/form/page.tsx— list and form entry points, withPermissionGuardsrc/components/rich-message-management/list/rich-message-management.container.tsx— table, copy/delete buttons, filterssrc/components/rich-message-management/form/rich-message-management-form.tsx— the 5-item cap, per-type file validation, the 1040px minimum widthsrc/components/rich-message-management/form/rich-message-management-form.container.tsx— how theFormDatapayload is assembled, template content fetching, the pre-edit confirmation stepsrc/components/rich-message-management/form/utils/validate-rich-message-item.ts— each content type's completeness barsrc/components/rich-message-management/form/modal/action-spot.tsx— the tappable-area drawing modal and its 20-area capsrc/services/rich-message-management.service.ts— every endpoint the frontend callsmessages/th/rich-message-management.json,messages/en/rich-message-management.json— all UI copy
Backend (line-management-cms-api-go)
internal/modules/richmessage/controller.go— all 8 routes,ModuleGate("rich-message"), the hard-deleteSuperAdminGuard, and the multipartcontent[i][field]decodinginternal/modules/richmessage/service.go— media processing, flex validation against the LINE API, template content copying, the Auto-response cache purgeinternal/modules/richmessage/dto.go— inbound validation rules, thequickReplyIdtri-state, file size and MIME-type ceilings
Worker (line-management-worker-go)
internal/autoresponse/transform.go— how each content type is mapped into LINE's message shape at send time
See the documentation team's earlier technical write-up at Rich Message Management