Message Model
Message Schema
Every field on a normalized message, including which are always present and which only appear sometimes.
Every chat event — a plain message, a Super Chat, a membership, a deletion — arrives in the same flat shape. Fields that do not apply are present but empty rather than missing, so you never have to branch just to read a value.
Message
| Field | Type | Always present | Description |
|---|---|---|---|
type |
string |
yes | Which kind of event this is — see Message Types |
id |
string |
yes | YouTube's message id. "" for events that carry none, such as deletions |
timestamp |
string |
yes | Display timestamp as YouTube renders it, e.g. "1:23 PM" or "1:02:03". Not a parseable date |
author |
string |
yes | Display name, falling back to the channel id. "" when there is no author |
message |
string |
yes | Flattened plain-text body. Emoji become their shortcut, e.g. :yt: |
amount |
string |
yes | Formatted purchase amount for paid events, e.g. "$5.00". "" otherwise |
color |
string |
yes | Background colour as #rrggbb for paid events. "" otherwise |
parts |
Part[] |
yes | Structured body — render chat UI from this, not from message |
iconType |
string |
no | Present on engagement events, e.g. YOUTUBE_ROUND |
isMember |
boolean |
no | Present and true only when the author is a member |
isModerator |
boolean |
no | Present and true only when the author is a moderator |
isOwner |
boolean |
no | Present and true only when the author owns the channel |
memberBadge |
MemberBadge |
no | Present only when the author has a custom member badge |
authorPhoto |
Thumbnail[] |
no | Present only when the author has an avatar |
The optional fields are omitted entirely rather than set to false or
null. Test with message.isMember === true, or just truthiness — never
'isMember' in message ? ... : ... expecting a false value to exist.
Part
A discriminated union on kind. See
Rendering Message Bodies for how to draw it.
Text part
| Field | Type | Description |
|---|---|---|
kind |
"text" |
Discriminant |
text |
string |
A literal run of text |
Emoji part
| Field | Type | Description |
|---|---|---|
kind |
"emoji" |
Discriminant |
shortcut |
string |
The :shortcut: form, e.g. ":yt:" |
emoji_id |
string |
YouTube's emoji id. Sometimes a raw emoji character |
is_custom |
boolean |
true for channel-specific emoji that must be drawn as an image |
mapped_unicode |
string |
A unicode equivalent when one is known, otherwise "" |
thumbnails |
Thumbnail[] |
Image sources, ascending by size |
Thumbnail
| Field | Type | Description |
|---|---|---|
url |
string |
Image URL |
width |
number |
Width in pixels, 0 when YouTube omitted it |
height |
number |
Height in pixels, 0 when YouTube omitted it |
Arrays are ordered smallest to largest, so the last entry is the highest resolution.
MemberBadge
| Field | Type | Description |
|---|---|---|
tooltip |
string |
Badge tooltip, e.g. "Member (6 months)" |
thumbnails |
Thumbnail[] |
Badge images, ascending by size |
Types in TypeScript
The schemas are published by the client package and are the same ones the server validates against, so there is one definition rather than two:
import type {
MessageSchema,
PartSchema,
ThumbnailSchema,
MemberBadgeSchema,
} from '@gettersethya/yt-livechat-client'
function onMessage(message: MessageSchema) {
if (message.isOwner === true) highlight(message)
}These are effect schemas: each export is both a runtime validator and a type of the same name.
import { Schema } from 'effect'
import { MessageSchema } from '@gettersethya/yt-livechat-client'
const message = Schema.decodeUnknownSync(MessageSchema)(payload)