Mudu.Room.Comment 评论组件
获取评论页数
// 返回评论页数,类型为int
var commentPage = Mudu.Room.Comment.GetPage()
发送评论
Mudu.Room.Comment.Send(
// 要发送的评论文本,类型为string
'活动很赞很给力',
// 发送完成的回调函数,参数为response对象
function (response) {
response = JSON.parse(response)
if (response.status === 'y') {
console.log('发送成功')
}
if (response.status === 'n') {
console.log('发送失败,错误码为:' + response.flag)
}
}
)
- response对象说明
{
// 成功状态,类型为string,成功时为'y',失败时为'n'
status: 'y',
// 状态码码,类型为int,成功时为0,失败不为0
flag: 100,
// 信息,类型为string,成功时为'发送消息成功!',失败时为'发送消息失败!'
info: "发送消息成功!/失败!"
}
- flag状态码码对照表
| flag | info | status |
|---|---|---|
| 100 | 发送消息成功 | y |
| 101 | 管理员禁止了聊天 | n |
| 102 | 观众被禁言 | n |
| 104 | 禁止匿名聊天 | n |
| 105 | 发送聊天内容过长 | n |
| 106 | 频繁发送 | n |
| 107 | 发送聊天内容中包含敏感词 | n |
| 135 | 系统不允许观众使用匿名身份 | n |
| 136 | 昵称中包含敏感词 | n |
| 103 | 其他错误,发送失败 | n |
获取评论
Mudu.Room.Comment.Get(
// 要获取评论的页码,类型为int
2,
// 评论获取成功的回调函数,参数为response对象
function (response) {
response = JSON.parse(response)
if (response.status === 'y') {
console.log('获取评论成功,数据为:', response.data)
}
if (response.status === 'n'){
console.log('获取评论失败')
}
}
)
- response返回结果说明
{
// 状态
"status": 'y',
// 返回的数据
"data": {
// 还剩下多少页
page: 70,
// 获取到的评论
comments: [
{
"id": 3438056,
"visitor_id": "1493401897797743654_api",
"username": "小白白",
"message": "这个真不错",
"avatar": "http://cdn12.mudu.tv/thumbnails/uploads/9/b5ed4ba472bbaa27e092be7594a2e31d.jpg",
"dateline": "2020-04-16T15:56:00+08:00",
"pushed": 1,
"priority": 10,
"top": 0,
"msg_type": 10,
"is_admin": 0
},
......
]
},
}
- comments说明
| 名称 | 说明 | 类型 |
|---|---|---|
| id | 评论id | int |
| visitor_id | 观众标志 | string |
| username | 评论者的观众名 | string |
| message | 评论的文本 | string |
| avatar | 评论这个的头像 | string |
| dateline | 评论时间 | 字符串 |
| pushed | 是否已经发送了弹幕,0未发送,1已发送 | int |
| priority | 优先级 ,0 未审核,10已审核,时间戳(置顶时间) | int |
| top | 是否置顶 ,0 不置顶,1置顶 | int |
| msg_type | 消息来源类型, 10为普通文本消息...,详情见下表 | int |
| is_admin | 是否为管理员发送, 1是, 0不是 | int |
- msg_type 和 message之间的映射关系
| 类型 | 标志位(msg_type) | 存储类型 | 示例(message) | 备注 |
|---|---|---|---|---|
| 普通评论 | 10 | 字符串 | "真香" | 表情替换 |
| 发送普通红包 | 20 | 字符串json | '{"id":"nm4dk9jm", "type":1, "name":"恭喜发财!大吉大利!", }' | type 2:钉钉 |
| 发送口令红包 | 21 | 字符串json | '{"id":"nm4dk9jm", "type":1, "name":"恭喜发财!大吉大利!"}' | type 2:钉钉 |
| 发送竞答红包 | 22 | 字符串json | '{"id":"nm4dk9jm", "name":"参与竞答领取红包!"}' | |
| 发送竞答红包(自定义) | 23 | 字符串json | '{"id":"nm4dk9jm", "name":"参与竞答领取红包!"}' | |
| 抢口令红包评论 | 31 | 字符串 | "大吉大利,今晚吃鸡" | |
| 免费道具打赏 | 40 | 字符串json | '{"src":"https://xxx.com/a.jpg". "name":"免费道具", "number":1 }' | |
| 付费道具打赏 | 41 | 字符串json | '{"src":"https://xxx.com/c.jpg". "name":"付费道具", "number":88 }' | |
| 现金打赏 | 42 | 字符串json | '{"name":"赏赐", "amount":88.99 }' | |
| 图片评论 | 50 | 字符串 | "https://xxx.com/abc.jpg" | |
| 系统自定义通知消息 | 60 | 字符串 | "欢迎大家参与讨论" | |
| 观众匿名消息 | 11 | 字符串 | "猜猜我是谁" |
若控制台中配置了聊天文字颜色或者是回复消息,SDK 使用方需要在收到消息后自行解析消息内容,参考此页底部的说明。
获取当前用户使用匿名身份聊天相关配置
var anonymousCommentConfig = Mudu.Room.Comment.GetAnonymousCommentConfig()
- anonymousCommentConfig 说明
| 属性 | 描述 | 类型 |
|---|---|---|
| allowed | 系统是否开启了允许观众使用匿名身份聊天 | boolean |
| inUse | 用户是否使用了匿名身份 | boolean |
| nickname | 观众匿名名称 | string |
| avatar | 观众匿名头像地址 | string |
设置用户匿名身份
说明:
- 如果设置开启用户匿名身份成功,用户之后发送聊天时将会使用匿名身份发送;
- 当次设置仅在本次sdk生命周期内有效,下次初始化sdk时需要重新设置聊天匿名身份;
- 为避免与默认的“匿名观众”身份混淆,建议在对观众Assign实名身份之后再允许其使用匿名身份聊天
const param = {
// 设置当前用户是否使用匿名身份聊天
inUse: true,
// 设置当前用户匿名身份的昵称,最大长度 60
nickname: '匿名用户',
// 设置当前用户匿名身份的头像
avatar: 'https://static.mudu.tv/assets/anonymized-user-avatar.png'
}
Mudu.Room.Comment.SetAnonymousCommentConfig(
// 要设置的匿名配置
param,
// 设置结果
function (response) {
response = JSON.parse(response)
if (response.status === 'y') {
console.log('设置用户匿名聊天身份成功')
}
if (response.status === 'n') {
console.log('设置用户匿名聊天身份失败:', response)
}
}
)
- response 返回结果说明
{
// 是否设置成功标志
status: 'y',
errcode: 1000
}
- errcode 说明
| errcode | 说明 | 对应status |
|---|---|---|
| 1000 | 成功 | 'y' |
| 1406 | 设置昵称过长(可设置长度不超过60的字符串) | 'n' |
| 4935 | 系统不允许观众使用匿名身份 | 'n' |
| 4936 | 昵称中包含敏感词 | 'n' |
Comment.New事件
Comment.New事件会在评论新增的时候被触发
Mudu.MsgBus.On(
// 事件名,值为Comment.New
'Comment.New',
// 事件处理函数,参数为新的评论,类型为object
function (newComment) {
newComment = JSON.parse(newComment) // newComment 结构见页面底部
console.log(newComment.username + '发送了一条新评论: ' + newComment.message)
}
)
Comment.Top事件
Comment.Top事件会在评论置顶/取消置顶时触发
Mudu.MsgBus.On(
// 事件名,值为Comment.Top
'Comment.Top',
// 事件处理函数,参数为新的评论,类型为object
function (newComment) {
newComment = JSON.parse(newComment) // newComment 结构见页面底部
console.log('一条评论被' + (newComment.top === 1 ? '置顶' : '取消置顶') + '了');
}
)
Comment.Delete事件
Comment.Delete事件会在评论被管理员删除时触发
Mudu.MsgBus.On(
// 事件名,值为Comment.Delete
'Comment.Delete',
// 事件处理函数,参数为被删除的评论,类型为object
function (newComment) {
newComment = JSON.parse(newComment) // newComment 结构见页面底部
console.log('一条评论被删除了');
}
)
- 事件处理函数参数newComment对象说明
| 名称 | 说明 | 类型 |
|---|---|---|
| id | 评论id | int |
| visitor_id | 观众标志 | string |
| username | 评论者的观众名 | string |
| message | 评论的文本 | string |
| avatar | 评论这个的头像 | string |
| dateline | 评论的时间戳 | int |
| pushed | 是否已经发送了弹幕,0未发送,1已发送 | int |
| priority | 优先级 ,0 未审核,10已审核,时间戳(置顶时间) | int |
| top | 是否置顶 ,0 不置顶,1置顶 | int |
| msg_type | 消息来源类型, 10为普通文本消息...,详情见上表 | int |
| is_admin | 是否为管理员发送, 1是, 0不是 | int |
解析结构化聊天消息
⚠️ 为了安全起见,你应该仅解析管理员发送的消息,而不是所有用户消息。
// 假设观众张三发送了一条 `hello`,管理员将字体颜色设为 `#0ea5e9` 并回复他 `你好`
// 消息经控制台发送后,被组装为一个特殊结构的字符串,如下所示:
const message = "[MSG]c=你好;m.col=#0ea5e9;m.q.id=123;m.q.s=张三;m.q.c=hello"
const parsed = isStructuredMessage(message) ? decodeStructuredMessage(message) : message
console.log(parsed) // { t: 'm', c: '你好', m: { col: '#0ea5e9', q: { id: '123', s: '张三', c: 'hello' } } }
下面为解析结构化聊天消息的工具代码,可以直接复制到项目中使用
// 消息解析工具函数
// 🔹 通用元信息接口
export interface StructuredMeta {
col?: string // color
q?: StructuredQuote // quote
}
// 🔹 回复信息(递归类型)
export interface StructuredQuote {
id: string
s: string // sender
c: string // content
m?: StructuredMeta
}
// 🔹 主消息结构
export interface StructuredMessage {
t?: 'm' // type
c: string // content
m?: StructuredMeta // meta
}
const PREFIX = '[MSG]'
const cache = new Map<string, StructuredMessage>()
/** 转义 = ; . */
function escapeStructuredValue(value: string | number): string {
return String(value).replace(/[=;.]/g, '\\$&');
}
/** 反转义 = ; . */
function unescapeStructuredValue(value: string): string {
return value.replace(/\\([=;.])/g, '$1');
}
// ----------------------
// 是否为结构化消息
// ----------------------
export function isStructuredMessage(text: string) {
return typeof text === 'string' && text.startsWith(PREFIX)
}
// ----------------------
// 编码结构化消息
// ----------------------
export function encodeStructuredMessage(obj: StructuredMessage): string {
const kvPairs: string[] = [];
function traverse(prefix: string, value: any): void {
if (typeof value === 'object' && value !== null) {
for (const key in value) {
traverse(prefix ? `${prefix}.${key}` : key, value[key]);
}
} else {
kvPairs.push(`${prefix}=${escapeStructuredValue(value)}`);
}
}
traverse('', obj);
return PREFIX + kvPairs.join(';');
}
// ----------------------
// 解码结构化消息
// ----------------------
export function decodeStructuredMessage(str: string): StructuredMessage | null {
if (cache.has(str)) return cache.get(str)!;
if (!isStructuredMessage(str)) return null;
const body = str.slice(PREFIX.length);
if (!body) return null;
const result: Record<string, any> = {};
// 拆分 key=value 对,支持转义和 HTML 实体
function splitPairs(input: string): string[] {
const res: string[] = [];
let buf = '';
let esc = false;
let inEntity = false;
for (const ch of input) {
if (esc) { buf += ch; esc = false; continue; }
if (ch === '\\') { buf += ch; esc = true; continue; }
if (!inEntity && ch === '&') { buf += ch; inEntity = true; continue; }
if (inEntity) { buf += ch; if (ch === ';') inEntity = false; continue; }
if (ch === ';') { if (buf) res.push(buf); buf = ''; continue; }
buf += ch;
}
if (buf) res.push(buf);
return res;
}
// 查找未转义的等号
function findUnescapedEqual(s: string): number {
let esc = false;
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (esc) { esc = false; continue; }
if (ch === '\\') { esc = true; continue; }
if (ch === '=') return i;
}
return -1;
}
// 拆分 key 路径,支持转义的点
function splitPath(k: string): string[] {
return k.split(/(?<!\\)\./).map(p => p.replace(/\\\./g, '.'));
}
try {
const pairs = splitPairs(body);
let hasValid = false;
for (const p of pairs) {
if (!p) continue;
const eqIdx = findUnescapedEqual(p);
if (eqIdx < 0) continue;
const path = splitPath(p.slice(0, eqIdx));
const value = unescapeStructuredValue(p.slice(eqIdx + 1));
let target = result;
path.forEach((key, i) => {
if (i === path.length - 1) {
target[key] = value;
} else {
if (!target[key] || typeof target[key] !== 'object') target[key] = {};
target = target[key];
}
});
hasValid = true;
}
if (hasValid) {
cache.set(str, result as StructuredMessage);
return result as StructuredMessage;
}
} catch (error) {
console.error('decodeStructuredMessage error:', error);
}
return null
}