diff --git a/frontend/src/views/Room/TextMessage.vue b/frontend/src/views/Room/TextMessage.vue
index cd5669e..33060d4 100644
--- a/frontend/src/views/Room/TextMessage.vue
+++ b/frontend/src/views/Room/TextMessage.vue
@@ -5,6 +5,9 @@
{{time}}
{{authorName}}
{{content}}
+
@@ -16,6 +19,8 @@ const AUTHOR_TYPE_TO_TEXT = [
'moderator', // 房管
'owner' // 主播
]
+const REPEATED_MARK_COLOR_START = [0x21, 0x96, 0xF3]
+const REPEATED_MARK_COLOR_END = [0xFF, 0x57, 0x22]
export default {
name: 'TextMessage',
@@ -24,12 +29,40 @@ export default {
time: String,
authorName: String,
authorType: Number,
- content: String
+ content: String,
+ repeated: Number
},
computed: {
authorTypeText() {
return AUTHOR_TYPE_TO_TEXT[this.authorType]
+ },
+ repeatedMarkColor() {
+ let color
+ if (this.repeated <= 2) {
+ color = REPEATED_MARK_COLOR_START
+ } else if (this.repeated >= 10) {
+ color = REPEATED_MARK_COLOR_END
+ } else {
+ color = [0, 0, 0]
+ let t = (this.repeated - 2) / (10 - 2)
+ for (let i = 0; i < 3; i++) {
+ color[i] = REPEATED_MARK_COLOR_START[i] + (REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t
+ }
+ }
+ return `rgb(${color.join(', ')})`
}
}
}
+
+
diff --git a/frontend/src/views/Room/index.vue b/frontend/src/views/Room/index.vue
index 64cf2a2..ec5f68e 100644
--- a/frontend/src/views/Room/index.vue
+++ b/frontend/src/views/Room/index.vue
@@ -18,7 +18,7 @@
val)
+ cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
let styleElement = document.createElement('style')
- styleElement.innerText = config.DEFAULT_CONFIG.css
+ styleElement.innerText = cfg.css
document.head.appendChild(styleElement)
return {
- config: config.DEFAULT_CONFIG,
+ config: cfg,
styleElement,
websocket: null,
messages: [],
@@ -66,18 +69,46 @@ export default {
// 开发时使用localhost:80
const url = process.env.NODE_ENV === 'development' ? 'ws://localhost/chat' : `ws://${window.location.host}/chat`
this.websocket = new WebSocket(url)
- this.websocket.onopen = () => this.websocket.send(JSON.stringify({
- cmd: COMMAND_JOIN_ROOM,
- data: {
- roomId: parseInt(this.$route.params.roomId)
+ this.websocket.onopen = this.onWsOpen.bind(this)
+ this.websocket.onmessage = this.onWsMessage.bind(this)
+
+ if (this.$route.query.config_id) {
+ try {
+ let cfg = await config.getRemoteConfig(this.$route.query.config_id)
+ cfg.blockKeywords = cfg.blockKeywords.split('\n').filter(val => val)
+ cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
+ this.styleElement.innerText = cfg.css
+ this.config = cfg
+ } catch (e) {
+ this.$message.error('获取配置失败:' + e)
}
- }))
- this.websocket.onmessage = (event) => {
+ }
+ },
+ beforeDestroy() {
+ document.head.removeChild(this.styleElement)
+ this.websocket.close()
+ },
+ updated() {
+ window.scrollTo(0, document.body.scrollHeight)
+ },
+ methods: {
+ onWsOpen() {
+ this.websocket.send(JSON.stringify({
+ cmd: COMMAND_JOIN_ROOM,
+ data: {
+ roomId: parseInt(this.$route.params.roomId)
+ }
+ }))
+ },
+ onWsMessage(event) {
let body = JSON.parse(event.data)
let message = null
let time, price
switch(body.cmd) {
case COMMAND_ADD_TEXT:
+ if (!this.filterTextMessage(body.data) || this.mergeSimilar(body.data.content)) {
+ break
+ }
time = new Date(body.data.timestamp * 1000)
message = {
id: this.nextId++,
@@ -86,7 +117,8 @@ export default {
time: `${time.getHours()}:${time.getMinutes()}`,
authorName: body.data.authorName,
authorType: body.data.authorType,
- content: body.data.content
+ content: body.data.content,
+ repeated: 1
}
break
case COMMAND_ADD_GIFT:
@@ -114,26 +146,49 @@ export default {
}
if (message) {
this.messages.push(message)
- if (this.messages.length > 50)
- this.messages.shift()
+ if (this.messages.length > 50) {
+ this.messages.splice(0, this.messages.length - 50)
+ }
}
- }
-
- if (this.$route.query.config_id) {
- try {
- this.config = await config.getRemoteConfig(this.$route.query.config_id)
- this.styleElement.innerText = this.config.css
- } catch (e) {
- this.$message.error('获取配置失败:' + e)
+ },
+ filterTextMessage(data) {
+ if (this.config.blockGiftDanmaku && data.isGiftDanmaku) {
+ return false
+ } else if (this.config.blockLevel > 0 && data.authorLevel < this.config.blockLevel) {
+ return false
+ } else if (this.config.blockNewbie && data.isNewbie) {
+ return false
+ } else if (this.config.blockNotMobileVerified && !data.isMobileVerified) {
+ return false
}
+ for (let keyword of this.config.blockKeywords) {
+ if (data.content.indexOf(keyword) !== -1) {
+ return false
+ }
+ }
+ for (let user of this.config.blockUsers) {
+ if (data.authorName === user) {
+ return false
+ }
+ }
+ return true
+ },
+ mergeSimilar(content) {
+ if (!this.config.mergeSimilarDanmaku) {
+ return false
+ }
+ for (let i = this.messages.length - 1; i >= 0 && i >= this.messages.length - 5; i--) {
+ let message = this.messages[i]
+ if (
+ (message.content.indexOf(content) !== -1 || content.indexOf(message.content) !== -1) // 包含对方
+ && Math.abs(message.content.length - content.length) < Math.min(message.content.length, content.length) // 长度差比两者长度都小
+ ) {
+ message.repeated++
+ return true
+ }
+ }
+ return false
}
- },
- beforeDestroy() {
- document.head.removeChild(this.styleElement)
- this.websocket.close()
- },
- updated() {
- window.scrollTo(0, document.body.scrollHeight)
}
}
diff --git a/main.py b/main.py
index 1777e92..2b0f7ae 100644
--- a/main.py
+++ b/main.py
@@ -36,7 +36,7 @@ def main():
(r'/config', views.config.ConfigsHandler),
(r'/config/(.+)', views.config.ConfigHandler),
- (r'/((css|img|js)/.*)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
+ (r'/((css|fonts|img|js)/.*)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
(r'/(favicon\.ico)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
(r'/.*', views.main.MainHandler, {'path': WEB_ROOT})
],
diff --git a/views/chat.py b/views/chat.py
index 097810f..f4b39c3 100644
--- a/views/chat.py
+++ b/views/chat.py
@@ -72,7 +72,12 @@ class Room(blivedm.BLiveClient):
'timestamp': danmaku.timestamp,
'authorName': danmaku.uname,
'authorType': author_type,
- 'content': danmaku.msg
+ 'content': danmaku.msg,
+ 'privilegeType': danmaku.privilege_type,
+ 'isGiftDanmaku': bool(danmaku.msg_type),
+ 'authorLevel': danmaku.user_level,
+ 'isNewbie': danmaku.urank < 10000,
+ 'isMobileVerified': bool(danmaku.mobile_verify)
})
async def _on_receive_gift(self, gift: blivedm.GiftMessage):
@@ -80,6 +85,7 @@ class Room(blivedm.BLiveClient):
return
self.send_message(Command.ADD_GIFT, {
'avatarUrl': await get_avatar_url(gift.uid),
+ 'timestamp': gift.timestamp,
'authorName': gift.uname,
'giftName': gift.gift_name,
'giftNum': gift.num,
@@ -89,6 +95,7 @@ class Room(blivedm.BLiveClient):
async def _on_buy_guard(self, message: blivedm.GuardBuyMessage):
self.send_message(Command.ADD_VIP, {
'avatarUrl': await get_avatar_url(message.uid),
+ 'timestamp': message.start_time,
'authorName': message.username
})
@@ -128,14 +135,24 @@ class RoomManager:
'timestamp': time.time(),
'authorName': 'xfgryujk',
'authorType': 0,
- 'content': '我能吞下玻璃而不伤身体'
+ 'content': '我能吞下玻璃而不伤身体',
+ 'privilegeType': 0,
+ 'isGiftDanmaku': False,
+ 'authorLevel': 20,
+ 'isNewbie': False,
+ 'isMobileVerified': True
})
room.send_message(Command.ADD_TEXT, {
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
'timestamp': time.time(),
'authorName': '主播',
'authorType': 3,
- 'content': "I can eat glass, it doesn't hurt me."
+ 'content': "I can eat glass, it doesn't hurt me.",
+ 'privilegeType': 0,
+ 'isGiftDanmaku': False,
+ 'authorLevel': 20,
+ 'isNewbie': False,
+ 'isMobileVerified': True
})
room.send_message(Command.ADD_VIP, {
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
@@ -195,7 +212,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
logger.warning('未知的命令: %s data: %s', body['cmd'], body['data'])
def on_close(self):
- logger.info('Websocket断开 %s room: %d', self.request.remote_ip, self.room_id)
+ logger.info('Websocket断开 %s room: %s', self.request.remote_ip, self.room_id)
if self.room_id is not None:
room_manager.del_client(self.room_id, self)