mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
424 lines
11 KiB
Vue
424 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { useDanmakuClient } from '@/store/useDanmakuClient';
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
import MessageRender from './blivechat/MessageRender.vue';
|
|
// @ts-ignore
|
|
import * as constants from './blivechat/constants';
|
|
// @ts-ignore
|
|
import * as pronunciation from './blivechat/utils/pronunciation';
|
|
// @ts-ignore
|
|
import { DownloadConfig, useAccount } from '@/api/account';
|
|
import { QueryGetAPI } from '@/api/query';
|
|
import { VTSURU_API_URL } from '@/data/constants';
|
|
import { DanmakuInfo, GiftInfo, GuardInfo, SCInfo } from '@/data/DanmakuClients/OpenLiveClient';
|
|
import { useWebRTC } from '@/store/useRTC';
|
|
import { NAlert } from 'naive-ui';
|
|
import { useRoute } from 'vue-router';
|
|
// @ts-ignore
|
|
import * as trie from './blivechat/utils/trie';
|
|
|
|
export interface DanmujiConfig {
|
|
minGiftPrice: number,
|
|
showDanmaku: boolean,
|
|
showGift: boolean,
|
|
showGiftName: boolean,
|
|
mergeSimilarDanmaku: boolean,
|
|
mergeGift: boolean,
|
|
maxNumber: number,
|
|
|
|
blockLevel: number,
|
|
blockKeywords: string,
|
|
blockUsers: string,
|
|
blockMedalLevel: number,
|
|
|
|
giftUsernamePronunciation: string,
|
|
importPresetCss: boolean
|
|
|
|
emoticons: {
|
|
keyword: string,
|
|
url: string
|
|
}[]
|
|
}
|
|
|
|
defineExpose({ setCss })
|
|
const { customCss, isOBS = true } = defineProps<{
|
|
customCss?: string
|
|
isOBS?: boolean,
|
|
active?: boolean,
|
|
visible?: boolean,
|
|
}>()
|
|
|
|
const messageRender = ref()
|
|
const client = await useDanmakuClient().initOpenlive()
|
|
const pronunciationConverter = new pronunciation.PronunciationConverter()
|
|
const accountInfo = useAccount()
|
|
const route = useRoute()
|
|
|
|
const defaultConfig: DanmujiConfig = {
|
|
minGiftPrice: 0.1,
|
|
showDanmaku: true,
|
|
showGift: true,
|
|
showGiftName: true,
|
|
mergeSimilarDanmaku: false,
|
|
mergeGift: true,
|
|
maxNumber: 60,
|
|
|
|
blockLevel: 0,
|
|
blockKeywords: '',
|
|
blockUsers: '',
|
|
blockMedalLevel: 0,
|
|
|
|
giftUsernamePronunciation: '',
|
|
importPresetCss: false,
|
|
|
|
emoticons: []
|
|
} as DanmujiConfig
|
|
let textEmoticons: { keyword: string, url: string }[] = []
|
|
const config = ref<DanmujiConfig>(JSON.parse(JSON.stringify(defaultConfig)))
|
|
const rtc = await useWebRTC().Init('slave')
|
|
|
|
const emoticonsTrie = computed(() => {
|
|
let res = new trie.Trie()
|
|
for (let emoticons of [config.value.emoticons, textEmoticons]) {
|
|
for (let emoticon of emoticons) {
|
|
if (emoticon.keyword !== '' && emoticon.url !== '') {
|
|
res.set(emoticon.keyword, emoticon)
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
})
|
|
const blockKeywordsTrie = computed(() => {
|
|
let blockKeywords = config.value.blockKeywords.split('\n')
|
|
let res = new trie.Trie()
|
|
for (let keyword of blockKeywords) {
|
|
if (keyword !== '') {
|
|
res.set(keyword, true)
|
|
}
|
|
}
|
|
return res
|
|
})
|
|
|
|
function setCss(css: string) {
|
|
messageRender.value?.setCss(css)
|
|
}
|
|
|
|
/** @param {chatModels.AddTextMsg} data */
|
|
async function onAddText(data: DanmakuInfo, command: unknown) {
|
|
if (!config.value.showDanmaku || !filterTextMessage(data)) {
|
|
return
|
|
}
|
|
|
|
let richContent = await getRichContent(data)
|
|
// 合并要放在异步调用后面,因为异步调用后可能有新的消息,会漏合并
|
|
if (mergeSimilarText(data.msg)) {
|
|
return
|
|
}
|
|
let message = {
|
|
id: data.msg_id,
|
|
type: constants.MESSAGE_TYPE_TEXT,
|
|
avatarUrl: data.uface,
|
|
time: new Date(data.timestamp * 1000),
|
|
authorName: data.uname,
|
|
authorType: getAuthorType(data.open_id, data.guard_level),
|
|
content: data.msg,
|
|
richContent: richContent,
|
|
privilegeType: data.guard_level,
|
|
repeated: 1,
|
|
translation: ''
|
|
}
|
|
messageRender.value.addMessage(message)
|
|
}
|
|
/** @param {chatModels.AddGiftMsg} data */
|
|
function onAddGift(data: GiftInfo, command: any) {
|
|
if (!config.value.showGift) {
|
|
return
|
|
}
|
|
let price = (data.price * data.gift_num) / 1000
|
|
if (mergeSimilarGift(data.uname, price, !data.paid ? price : 0, data.gift_name, data.gift_num)) {
|
|
return
|
|
}
|
|
if (price < (config.value.minGiftPrice ?? 0)) { // 丢人
|
|
return
|
|
}
|
|
let message = {
|
|
id: data.msg_id,
|
|
type: constants.MESSAGE_TYPE_GIFT,
|
|
avatarUrl: data.uface,
|
|
time: new Date(data.timestamp * 1000),
|
|
authorName: data.uname,
|
|
authorNamePronunciation: getPronunciation(data.uname),
|
|
price: price,
|
|
// freePrice: data.totalFreeCoin, // 暂时没用到
|
|
giftName: data.gift_name,
|
|
num: data.gift_num
|
|
}
|
|
messageRender.value.addMessage(message)
|
|
}
|
|
/** @param {chatModels.AddMemberMsg} data */
|
|
function onAddMember(data: GuardInfo, command: any) {
|
|
if (!config.value.showGift || !filterNewMemberMessage(data)) {
|
|
return
|
|
}
|
|
let message = {
|
|
id: data.msg_id,
|
|
type: constants.MESSAGE_TYPE_MEMBER,
|
|
avatarUrl: data.user_info.uface,
|
|
time: new Date(data.timestamp * 1000),
|
|
authorName: data.user_info.uname,
|
|
authorNamePronunciation: getPronunciation(data.user_info.uname),
|
|
privilegeType: data.guard_level,
|
|
title: '新舰长'
|
|
}
|
|
messageRender.value.addMessage(message)
|
|
}
|
|
/** @param {chatModels.AddSuperChatMsg} data */
|
|
function onAddSuperChat(data: SCInfo) {
|
|
if (!config.value.showGift || !filterSuperChatMessage(data)) {
|
|
return
|
|
}
|
|
if (data.rmb < (config.value.minGiftPrice ?? 0)) { // 丢人
|
|
return
|
|
}
|
|
let message = {
|
|
id: data.msg_id,
|
|
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
|
avatarUrl: data.uface,
|
|
authorName: data.uname,
|
|
authorNamePronunciation: getPronunciation(data.uname),
|
|
price: data.rmb,
|
|
time: new Date(data.timestamp * 1000),
|
|
content: data.msg_id.trim(),
|
|
translation: ''
|
|
}
|
|
messageRender.value.addMessage(message)
|
|
}
|
|
/** @param {chatModels.DelSuperChatMsg} data */
|
|
function onDelSuperChat(data: any) {
|
|
messageRender.value.deleteMessage(data.id)
|
|
}
|
|
function getAuthorType(open_id: string, guard_level: number) {
|
|
let authorType
|
|
if (open_id === client.authInfo?.anchor_info.open_id) {
|
|
authorType = 3
|
|
} else if (guard_level !== 0) {
|
|
authorType = 1
|
|
} else {
|
|
authorType = 0
|
|
}
|
|
}
|
|
type RichContentType = {
|
|
type: string,
|
|
text: string,
|
|
url?: string,
|
|
width?: number,
|
|
height?: number
|
|
}
|
|
async function getRichContent(data: DanmakuInfo) {
|
|
let richContent: RichContentType[] = []
|
|
// 官方的非文本表情
|
|
if (data.emoji_img_url) {
|
|
richContent.push({
|
|
type: constants.CONTENT_TYPE_IMAGE,
|
|
text: data.msg,
|
|
url: data.emoji_img_url + '@256w_256h_1e_1c',
|
|
width: 256,
|
|
height: 256
|
|
})
|
|
//await fillImageContentSizes(richContent)
|
|
return richContent
|
|
}
|
|
|
|
// 没有文本表情,只能是纯文本
|
|
if (config.value.emoticons.length === 0 && textEmoticons.length === 0) {
|
|
richContent.push({
|
|
type: constants.CONTENT_TYPE_TEXT,
|
|
text: data.msg
|
|
})
|
|
return richContent
|
|
}
|
|
|
|
// 可能含有文本表情,需要解析
|
|
let startPos = 0
|
|
let pos = 0
|
|
while (pos < data.msg.length) {
|
|
let remainContent = data.msg.substring(pos)
|
|
let matchEmoticon = emoticonsTrie.value.lazyMatch(remainContent)
|
|
if (matchEmoticon === null) {
|
|
pos++
|
|
continue
|
|
}
|
|
|
|
// 加入之前的文本
|
|
if (pos !== startPos) {
|
|
richContent.push({
|
|
type: constants.CONTENT_TYPE_TEXT,
|
|
text: data.msg.slice(startPos, pos)
|
|
})
|
|
}
|
|
|
|
// 加入表情
|
|
richContent.push({
|
|
type: constants.CONTENT_TYPE_IMAGE,
|
|
text: matchEmoticon.keyword,
|
|
url: matchEmoticon.url,
|
|
width: 0,
|
|
height: 0
|
|
})
|
|
pos += matchEmoticon.keyword.length
|
|
startPos = pos
|
|
}
|
|
// 加入尾部的文本
|
|
if (pos !== startPos) {
|
|
richContent.push({
|
|
type: constants.CONTENT_TYPE_TEXT,
|
|
text: data.msg.slice(startPos, pos)
|
|
})
|
|
}
|
|
|
|
await fillImageContentSizes(richContent)
|
|
return richContent
|
|
}
|
|
async function fillImageContentSizes(richContent: RichContentType[]) {
|
|
let urlSizeMap = new Map()
|
|
for (let content of richContent) {
|
|
if (content.type === constants.CONTENT_TYPE_IMAGE) {
|
|
urlSizeMap.set(content.url, { width: 0, height: 0 })
|
|
}
|
|
}
|
|
if (urlSizeMap.size === 0) {
|
|
return
|
|
}
|
|
|
|
let promises = []
|
|
for (let url of urlSizeMap.keys()) {
|
|
let urlInClosure = url
|
|
promises.push(new Promise(
|
|
resolve => {
|
|
let img = document.createElement('img')
|
|
img.onload = () => {
|
|
let size = urlSizeMap.get(urlInClosure)
|
|
size.width = img.naturalWidth
|
|
size.height = img.naturalHeight
|
|
// @ts-expect-error 忽略这里测错误
|
|
resolve()
|
|
}
|
|
// 获取失败了默认为0
|
|
img.onerror = resolve
|
|
// 超时保底
|
|
window.setTimeout(resolve, 5000)
|
|
img.src = urlInClosure
|
|
}
|
|
))
|
|
}
|
|
await Promise.all(promises)
|
|
|
|
for (let content of richContent) {
|
|
if (content.type === constants.CONTENT_TYPE_IMAGE) {
|
|
let size = urlSizeMap.get(content.url)
|
|
content.width = size.width
|
|
content.height = size.height
|
|
}
|
|
}
|
|
}
|
|
function getPronunciation(text: string) {
|
|
if (pronunciationConverter === null) {
|
|
return ''
|
|
}
|
|
return pronunciationConverter.getPronunciation(text)
|
|
}
|
|
function filterSuperChatMessage(data: SCInfo) {
|
|
return filterByContent(data.message) && filterByAuthorName(data.uname)
|
|
}
|
|
function filterNewMemberMessage(data: GuardInfo) {
|
|
return filterByAuthorName(data.user_info.uname)
|
|
}
|
|
function filterByContent(content: string) {
|
|
for (let i = 0; i < content.length; i++) {
|
|
let remainContent = content.substring(i)
|
|
if (blockKeywordsTrie.value.lazyMatch(remainContent) !== null) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
function filterByAuthorName(id: string) {
|
|
return Object.keys(accountInfo.value.biliBlackList).indexOf(id) === -1
|
|
}
|
|
function filterTextMessage(data: DanmakuInfo) {
|
|
if (config.value.blockLevel > 0 && data.guard_level < config.value.blockLevel) {
|
|
return false
|
|
} else if (config.value.blockMedalLevel > 0 && data.fans_medal_level < config.value.blockMedalLevel) {
|
|
return false
|
|
}
|
|
return filterByContent(data.msg) && filterByAuthorName(data.uname)
|
|
}
|
|
function mergeSimilarText(content: string) {
|
|
if (!config.value.mergeSimilarDanmaku) {
|
|
return false
|
|
}
|
|
return messageRender.value.mergeSimilarText(content)
|
|
}
|
|
function mergeSimilarGift(authorName: string, price: number, freePrice: number, giftName: string, num: number) {
|
|
if (!config.value.mergeGift) {
|
|
return false
|
|
}
|
|
return messageRender.value.mergeSimilarGift(authorName, price, freePrice, giftName, num)
|
|
}
|
|
|
|
function onReceiveConfig(data: DanmujiConfig) {
|
|
config.value = data
|
|
}
|
|
|
|
onMounted(async () => {
|
|
client.on('danmaku', onAddText)
|
|
client.on('gift', onAddGift)
|
|
client.on('sc', onAddSuperChat)
|
|
//client.onEvent('delsc', onSuperChatDel)
|
|
client.on('guard', onAddMember)
|
|
|
|
rtc?.on('danmuji.config', onReceiveConfig)
|
|
|
|
QueryGetAPI<{ keyword: string, url: string }[]>(VTSURU_API_URL + 'blivechat/emoticon').then((data) => {
|
|
if (data.code === 200) {
|
|
textEmoticons = data.data
|
|
}
|
|
})
|
|
return
|
|
while (true) {
|
|
const result = await DownloadConfig('OBS.Danmuji')
|
|
if (result.msg === undefined) {
|
|
config.value = result.data as DanmujiConfig
|
|
break
|
|
}
|
|
else if (result.status == 'error') {
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
}
|
|
})
|
|
onUnmounted(() => {
|
|
client.off('danmaku', onAddText)
|
|
client.off('gift', onAddGift)
|
|
client.off('sc', onAddSuperChat)
|
|
//client.offEvent('delsc', onSuperChatDel)
|
|
client.off('guard', onAddMember)
|
|
|
|
rtc?.off('danmuji.config', onReceiveConfig)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<NAlert
|
|
v-if="!$route.query.token && isOBS"
|
|
type="error"
|
|
>
|
|
未携带token参数
|
|
</NAlert>
|
|
<MessageRender
|
|
v-else
|
|
ref="messageRender"
|
|
:custom-css="customCss"
|
|
:show-gift-name="config.showGiftName"
|
|
style="height: 100%; width: 100%"
|
|
/>
|
|
</template> |