mirror of
https://github.com/xfgryujk/blivechat.git
synced 2025-03-12 02:30:40 +08:00
添加自动翻译前端
This commit is contained in:
parent
0c9560f8ca
commit
d3e9300fa4
111
api/chat.py
111
api/chat.py
@ -6,13 +6,16 @@ import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
import tornado.websocket
|
||||
|
||||
import blivedm.blivedm as blivedm
|
||||
import config
|
||||
import models.avatar
|
||||
import models.translate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -25,6 +28,7 @@ class Command(enum.IntEnum):
|
||||
ADD_MEMBER = 4
|
||||
ADD_SUPER_CHAT = 5
|
||||
DEL_SUPER_CHAT = 6
|
||||
UPDATE_TRANSLATION = 7
|
||||
|
||||
|
||||
_http_session = aiohttp.ClientSession()
|
||||
@ -94,6 +98,7 @@ class Room(blivedm.BLiveClient):
|
||||
def __init__(self, room_id):
|
||||
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
|
||||
self.clients: List['ChatHandler'] = []
|
||||
self.auto_translate_count = 0
|
||||
|
||||
def stop_and_close(self):
|
||||
if self.is_running:
|
||||
@ -110,6 +115,14 @@ class Room(blivedm.BLiveClient):
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
pass
|
||||
|
||||
def send_message_if(self, can_send_func: Callable[['ChatHandler'], bool], cmd, data):
|
||||
body = json.dumps({'cmd': cmd, 'data': data})
|
||||
for client in filter(can_send_func, self.clients):
|
||||
try:
|
||||
client.write_message(body)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
pass
|
||||
|
||||
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
|
||||
asyncio.ensure_future(self.__on_receive_danmaku(danmaku))
|
||||
|
||||
@ -122,6 +135,19 @@ class Room(blivedm.BLiveClient):
|
||||
author_type = 1 # 舰队
|
||||
else:
|
||||
author_type = 0
|
||||
|
||||
need_translate = self._need_translate(danmaku.msg)
|
||||
if need_translate:
|
||||
translation = models.translate.get_translation_from_cache(danmaku.msg)
|
||||
if translation is None:
|
||||
# 没有缓存,需要后面异步翻译后通知
|
||||
translation = ''
|
||||
else:
|
||||
need_translate = False
|
||||
else:
|
||||
translation = ''
|
||||
|
||||
id_ = uuid.uuid4().hex
|
||||
# 为了节省带宽用list而不是dict
|
||||
self.send_message(Command.ADD_TEXT, [
|
||||
# 0: avatarUrl
|
||||
@ -145,15 +171,24 @@ class Room(blivedm.BLiveClient):
|
||||
# 9: isMobileVerified
|
||||
1 if danmaku.mobile_verify else 0,
|
||||
# 10: medalLevel
|
||||
0 if danmaku.room_id != self.room_id else danmaku.medal_level
|
||||
0 if danmaku.room_id != self.room_id else danmaku.medal_level,
|
||||
# 11: id
|
||||
id_,
|
||||
# 12: translation
|
||||
translation
|
||||
])
|
||||
|
||||
if need_translate:
|
||||
await self._translate_and_response(danmaku.msg, id_)
|
||||
|
||||
async def _on_receive_gift(self, gift: blivedm.GiftMessage):
|
||||
avatar_url = models.avatar.process_avatar_url(gift.face)
|
||||
models.avatar.update_avatar_cache(gift.uid, avatar_url)
|
||||
if gift.coin_type != 'gold': # 丢人
|
||||
return
|
||||
id_ = uuid.uuid4().hex
|
||||
self.send_message(Command.ADD_GIFT, {
|
||||
'id': id_,
|
||||
'avatarUrl': avatar_url,
|
||||
'timestamp': gift.timestamp,
|
||||
'authorName': gift.uname,
|
||||
@ -164,7 +199,9 @@ class Room(blivedm.BLiveClient):
|
||||
asyncio.ensure_future(self.__on_buy_guard(message))
|
||||
|
||||
async def __on_buy_guard(self, message: blivedm.GuardBuyMessage):
|
||||
id_ = uuid.uuid4().hex
|
||||
self.send_message(Command.ADD_MEMBER, {
|
||||
'id': id_,
|
||||
'avatarUrl': await models.avatar.get_avatar_url(message.uid),
|
||||
'timestamp': message.start_time,
|
||||
'authorName': message.username
|
||||
@ -173,20 +210,59 @@ class Room(blivedm.BLiveClient):
|
||||
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
|
||||
avatar_url = models.avatar.process_avatar_url(message.face)
|
||||
models.avatar.update_avatar_cache(message.uid, avatar_url)
|
||||
|
||||
need_translate = self._need_translate(message.message)
|
||||
if need_translate:
|
||||
translation = models.translate.get_translation_from_cache(message.message)
|
||||
if translation is None:
|
||||
# 没有缓存,需要后面异步翻译后通知
|
||||
translation = ''
|
||||
else:
|
||||
need_translate = False
|
||||
else:
|
||||
translation = ''
|
||||
|
||||
id_ = str(message.id)
|
||||
self.send_message(Command.ADD_SUPER_CHAT, {
|
||||
'id': id_,
|
||||
'avatarUrl': avatar_url,
|
||||
'timestamp': message.start_time,
|
||||
'authorName': message.uname,
|
||||
'price': message.price,
|
||||
'content': message.message,
|
||||
'id': message.id
|
||||
'translation': translation
|
||||
})
|
||||
|
||||
if need_translate:
|
||||
asyncio.ensure_future(self._translate_and_response(message.message, id_))
|
||||
|
||||
async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage):
|
||||
self.send_message(Command.ADD_SUPER_CHAT, {
|
||||
'ids': message.ids
|
||||
'ids': list(map(str, message.ids))
|
||||
})
|
||||
|
||||
def _need_translate(self, text):
|
||||
return (
|
||||
config.get_config().enable_translate
|
||||
and self.auto_translate_count > 0
|
||||
and models.translate.need_translate(text)
|
||||
)
|
||||
|
||||
async def _translate_and_response(self, text, msg_id):
|
||||
translation = await models.translate.translate(text)
|
||||
if translation is None:
|
||||
return
|
||||
self.send_message_if(
|
||||
lambda client: client.auto_translate,
|
||||
Command.UPDATE_TRANSLATION,
|
||||
[
|
||||
# 0: id
|
||||
msg_id,
|
||||
# 1: translation
|
||||
translation
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class RoomManager:
|
||||
def __init__(self):
|
||||
@ -200,6 +276,8 @@ class RoomManager:
|
||||
room = self._rooms[room_id]
|
||||
room.clients.append(client)
|
||||
logger.info('%d clients in room %s', len(room.clients), room_id)
|
||||
if client.auto_translate:
|
||||
room.auto_translate_count += 1
|
||||
|
||||
if client.application.settings['debug']:
|
||||
await client.send_test_message()
|
||||
@ -210,6 +288,8 @@ class RoomManager:
|
||||
room = self._rooms[room_id]
|
||||
room.clients.remove(client)
|
||||
logger.info('%d clients in room %s', len(room.clients), room_id)
|
||||
if client.auto_translate:
|
||||
room.auto_translate_count -= 1
|
||||
if not room.clients:
|
||||
self._del_room(room_id)
|
||||
|
||||
@ -243,6 +323,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._close_on_timeout_future = None
|
||||
self.room_id = None
|
||||
self.auto_translate = False
|
||||
|
||||
def open(self):
|
||||
logger.info('Websocket connected %s', self.request.remote_ip)
|
||||
@ -268,6 +349,11 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
return
|
||||
self.room_id = int(body['data']['roomId'])
|
||||
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
|
||||
try:
|
||||
cfg = body['data']['config']
|
||||
self.auto_translate = cfg['autoTranslate']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
|
||||
self._close_on_timeout_future.cancel()
|
||||
@ -320,32 +406,43 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
# 9: isMobileVerified
|
||||
1,
|
||||
# 10: medalLevel
|
||||
0
|
||||
0,
|
||||
# 11: id
|
||||
uuid.uuid4().hex,
|
||||
# 12: translation
|
||||
''
|
||||
]
|
||||
member_data = base_data.copy()
|
||||
member_data = {
|
||||
**base_data,
|
||||
'id': uuid.uuid4().hex
|
||||
}
|
||||
gift_data = {
|
||||
**base_data,
|
||||
'id': uuid.uuid4().hex,
|
||||
'totalCoin': 450000
|
||||
}
|
||||
sc_data = {
|
||||
**base_data,
|
||||
'id': str(random.randint(1, 65535)),
|
||||
'price': 30,
|
||||
'content': 'The quick brown fox jumps over the lazy dog',
|
||||
'id': random.randint(1, 65535)
|
||||
'translation': ''
|
||||
}
|
||||
self.send_message(Command.ADD_TEXT, text_data)
|
||||
text_data[2] = '主播'
|
||||
text_data[3] = 3
|
||||
text_data[4] = "I can eat glass, it doesn't hurt me."
|
||||
text_data[11] = uuid.uuid4().hex
|
||||
self.send_message(Command.ADD_TEXT, text_data)
|
||||
self.send_message(Command.ADD_MEMBER, member_data)
|
||||
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
|
||||
sc_data['id'] = str(random.randint(1, 65535))
|
||||
sc_data['price'] = 100
|
||||
sc_data['content'] = '敏捷的棕色狐狸跳过了懒狗'
|
||||
sc_data['id'] = random.randint(1, 65535)
|
||||
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
|
||||
# self.send_message(Command.DEL_SUPER_CHAT, {'ids': [sc_data['id']]})
|
||||
self.send_message(Command.ADD_GIFT, gift_data)
|
||||
gift_data['id'] = uuid.uuid4().hex
|
||||
gift_data['totalCoin'] = 1245000
|
||||
self.send_message(Command.ADD_GIFT, gift_data)
|
||||
|
||||
|
@ -30,6 +30,7 @@ def get_config():
|
||||
class AppConfig:
|
||||
def __init__(self):
|
||||
self.database_url = 'sqlite:///data/database.db'
|
||||
self.enable_translate = True
|
||||
|
||||
def load(self, path):
|
||||
config = configparser.ConfigParser()
|
||||
@ -37,6 +38,7 @@ class AppConfig:
|
||||
try:
|
||||
app_section = config['app']
|
||||
self.database_url = app_section['database_url']
|
||||
self.enable_translate = app_section.getboolean('enable_translate')
|
||||
except (KeyError, ValueError):
|
||||
logger.exception('Failed to load config:')
|
||||
return False
|
||||
|
@ -1,8 +1,11 @@
|
||||
[app]
|
||||
# See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||
database_url = sqlite:///data/database.db
|
||||
# Enable auto translate to Japanese
|
||||
enable_translate = true
|
||||
|
||||
|
||||
# DON'T modify this section
|
||||
[DEFAULT]
|
||||
database_url = sqlite:///data/database.db
|
||||
enable_translate = true
|
||||
|
@ -13,7 +13,9 @@ export const DEFAULT_CONFIG = {
|
||||
blockNotMobileVerified: true,
|
||||
blockKeywords: '',
|
||||
blockUsers: '',
|
||||
blockMedalLevel: 0
|
||||
blockMedalLevel: 0,
|
||||
|
||||
autoTranslate: false
|
||||
}
|
||||
|
||||
export function setLocalConfig (config) {
|
||||
|
@ -13,7 +13,7 @@
|
||||
<text-message :key="message.id" v-if="message.type === MESSAGE_TYPE_TEXT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:avatarUrl="message.avatarUrl" :time="message.time" :authorName="message.authorName"
|
||||
:authorType="message.authorType" :content="message.content" :privilegeType="message.privilegeType"
|
||||
:authorType="message.authorType" :content="getShowContent(message)" :privilegeType="message.privilegeType"
|
||||
:repeated="message.repeated"
|
||||
></text-message>
|
||||
<legacy-paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
|
||||
@ -24,7 +24,7 @@
|
||||
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
|
||||
:time="message.time" :content="message.content"
|
||||
:time="message.time" :content="getShowContent(message)"
|
||||
></paid-message>
|
||||
</template>
|
||||
</div>
|
||||
@ -118,6 +118,13 @@ export default {
|
||||
this.clearMessages()
|
||||
},
|
||||
methods: {
|
||||
getShowContent(message) {
|
||||
if (message.translation) {
|
||||
return `${message.content}(${message.translation})`
|
||||
}
|
||||
return message.content
|
||||
},
|
||||
|
||||
addMessage(message) {
|
||||
this.addMessages([message])
|
||||
},
|
||||
@ -212,6 +219,29 @@ export default {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
},
|
||||
updateMessage(id, newValuesObj) {
|
||||
// 遍历滚动的消息
|
||||
this.forEachRecentMessage(999999999, message => {
|
||||
if (message.id !== id) {
|
||||
return true
|
||||
}
|
||||
for (let name in newValuesObj) {
|
||||
message[name] = newValuesObj[name]
|
||||
}
|
||||
return false
|
||||
})
|
||||
// 遍历固定的消息
|
||||
for (let message of this.paidMessages) {
|
||||
if (message.id !== id) {
|
||||
continue
|
||||
}
|
||||
for (let name in newValuesObj) {
|
||||
message[name] = newValuesObj[name]
|
||||
}
|
||||
break
|
||||
}
|
||||
// TODO 解决自动滚动的问题
|
||||
},
|
||||
|
||||
enqueueMessages(messages) {
|
||||
if (this.lastEnqueueTime) {
|
||||
|
@ -28,6 +28,9 @@ export default {
|
||||
blockUsers: 'Block users',
|
||||
blockMedalLevel: 'Block medal level lower than',
|
||||
|
||||
advanced: 'Advanced',
|
||||
autoTranslate: 'Auto translate messages to Japanese',
|
||||
|
||||
roomUrl: 'Room URL',
|
||||
copy: 'Copy',
|
||||
enterRoom: 'Enter room',
|
||||
|
@ -28,6 +28,9 @@ export default {
|
||||
blockUsers: 'ブロックユーザー',
|
||||
blockMedalLevel: 'ブロック勲章等級がx未満',
|
||||
|
||||
advanced: 'アドバンス',
|
||||
autoTranslate: '自動翻訳コメントから日本語へ',
|
||||
|
||||
roomUrl: 'ルームのURL',
|
||||
copy: 'コピー',
|
||||
enterRoom: 'ルームに入る',
|
||||
|
@ -28,6 +28,9 @@ export default {
|
||||
blockUsers: '屏蔽用户',
|
||||
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
|
||||
|
||||
advanced: '高级',
|
||||
autoTranslate: '自动翻译弹幕到日语',
|
||||
|
||||
roomUrl: '房间URL',
|
||||
copy: '复制',
|
||||
enterRoom: '进入房间',
|
||||
|
@ -8,7 +8,7 @@ export function mergeConfig (config, defaultConfig) {
|
||||
|
||||
export function toBool (val) {
|
||||
if (typeof val === 'string') {
|
||||
return val !== 'false' && val !== ''
|
||||
return ['false', 'no', 'off', '0', ''].indexOf(val.toLowerCase()) === -1
|
||||
}
|
||||
return !!val
|
||||
}
|
||||
|
@ -50,6 +50,12 @@
|
||||
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="20"></el-slider>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('home.advanced')">
|
||||
<el-form-item :label="$t('home.autoTranslate')">
|
||||
<el-switch v-model="form.autoTranslate"></el-switch>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
@ -15,6 +15,7 @@ const COMMAND_ADD_GIFT = 3
|
||||
const COMMAND_ADD_MEMBER = 4
|
||||
const COMMAND_ADD_SUPER_CHAT = 5
|
||||
const COMMAND_DEL_SUPER_CHAT = 6
|
||||
const COMMAND_UPDATE_TRANSLATION = 7
|
||||
|
||||
export default {
|
||||
name: 'Room',
|
||||
@ -28,9 +29,7 @@ export default {
|
||||
websocket: null,
|
||||
retryCount: 0,
|
||||
isDestroying: false,
|
||||
heartbeatTimerId: null,
|
||||
|
||||
nextId: 0,
|
||||
heartbeatTimerId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -42,8 +41,8 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.wsConnect()
|
||||
this.updateConfig()
|
||||
this.wsConnect()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.isDestroying = true
|
||||
@ -70,6 +69,7 @@ export default {
|
||||
cfg.blockNewbie = toBool(cfg.blockNewbie)
|
||||
cfg.blockNotMobileVerified = toBool(cfg.blockNotMobileVerified)
|
||||
cfg.blockMedalLevel = toInt(cfg.blockMedalLevel, config.DEFAULT_CONFIG.blockMedalLevel)
|
||||
cfg.autoTranslate = toBool(cfg.autoTranslate)
|
||||
|
||||
this.config = cfg
|
||||
},
|
||||
@ -94,7 +94,10 @@ export default {
|
||||
this.websocket.send(JSON.stringify({
|
||||
cmd: COMMAND_JOIN_ROOM,
|
||||
data: {
|
||||
roomId: parseInt(this.$route.params.roomId)
|
||||
roomId: parseInt(this.$route.params.roomId),
|
||||
config: {
|
||||
autoTranslate: this.config.autoTranslate
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
@ -125,13 +128,15 @@ export default {
|
||||
authorLevel: data[7],
|
||||
isNewbie: !!data[8],
|
||||
isMobileVerified: !!data[9],
|
||||
medalLevel: data[10]
|
||||
medalLevel: data[10],
|
||||
id: data[11],
|
||||
translation: data[12]
|
||||
}
|
||||
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilarText(data.content)) {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
id: `text_${this.nextId++}`,
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
@ -139,7 +144,8 @@ export default {
|
||||
authorType: data.authorType,
|
||||
content: data.content,
|
||||
privilegeType: data.privilegeType,
|
||||
repeated: 1
|
||||
repeated: 1,
|
||||
translation: data.translation
|
||||
}
|
||||
break
|
||||
case COMMAND_ADD_GIFT: {
|
||||
@ -154,7 +160,7 @@ export default {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
id: `gift_${this.nextId++}`,
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
authorName: data.authorName,
|
||||
@ -169,7 +175,7 @@ export default {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
id: `member_${this.nextId++}`,
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
@ -186,7 +192,7 @@ export default {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
id: `sc_${data.id}`,
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
authorName: data.authorName,
|
||||
@ -197,10 +203,19 @@ export default {
|
||||
break
|
||||
case COMMAND_DEL_SUPER_CHAT:
|
||||
for (let id of data.ids) {
|
||||
id = `sc_${id}`
|
||||
this.$refs.renderer.delMessage(id)
|
||||
}
|
||||
break
|
||||
case COMMAND_UPDATE_TRANSLATION:
|
||||
if (!this.config.autoTranslate) {
|
||||
break
|
||||
}
|
||||
data = {
|
||||
id: data[0],
|
||||
translation: data[1]
|
||||
}
|
||||
this.$refs.renderer.updateMessage(data.id, {translation: data.translation})
|
||||
break
|
||||
}
|
||||
if (message) {
|
||||
this.$refs.renderer.addMessage(message)
|
||||
|
@ -216,7 +216,8 @@ let textMessageTemplate = {
|
||||
authorType: constants.AUTHRO_TYPE_NORMAL,
|
||||
content: '',
|
||||
privilegeType: 0,
|
||||
repeated: 1
|
||||
repeated: 1,
|
||||
translation: ''
|
||||
}
|
||||
let legacyPaidMessageTemplate = {
|
||||
id: 0,
|
||||
@ -236,20 +237,21 @@ let paidMessageTemplate = {
|
||||
authorName: '',
|
||||
price: 0,
|
||||
time: time,
|
||||
content: ''
|
||||
content: '',
|
||||
translation: ''
|
||||
}
|
||||
let nextId = 0
|
||||
const EXAMPLE_MESSAGES = [
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: 'mob路人',
|
||||
content: '8888888888',
|
||||
repeated: 12
|
||||
},
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: 'member舰长',
|
||||
authorType: constants.AUTHRO_TYPE_MEMBER,
|
||||
content: '草',
|
||||
@ -258,34 +260,34 @@ const EXAMPLE_MESSAGES = [
|
||||
},
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: 'admin房管',
|
||||
authorType: constants.AUTHRO_TYPE_ADMIN,
|
||||
content: 'kksk'
|
||||
},
|
||||
{
|
||||
...legacyPaidMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: '少年Pi',
|
||||
content: 'Welcome 少年Pi!'
|
||||
},
|
||||
{
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: '无火的残渣',
|
||||
price: 66600,
|
||||
content: 'Sent 小电视飞船x100'
|
||||
},
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: 'streamer主播',
|
||||
authorType: constants.AUTHRO_TYPE_OWNER,
|
||||
content: '老板大气,老板身体健康'
|
||||
},
|
||||
{
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
id: (nextId++).toString(),
|
||||
authorName: '夏色祭保護協会会長',
|
||||
price: 30,
|
||||
content: '言いたいことがあるんだよ!'
|
||||
|
Loading…
Reference in New Issue
Block a user