mirror of
https://github.com/xfgryujk/blivechat.git
synced 2025-01-13 13:50:10 +08:00
完成上传表情文件
This commit is contained in:
parent
903cfecab9
commit
24d0671dae
@ -107,6 +107,9 @@ server {
|
||||
ssl_certificate /PATH/TO/CERT.crt;
|
||||
ssl_certificate_key /PATH/TO/CERT_KEY.key;
|
||||
|
||||
client_body_buffer_size 256k;
|
||||
client_max_body_size 1.1m;
|
||||
|
||||
# 代理header
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
|
14
api/base.py
14
api/base.py
@ -9,16 +9,6 @@ class ApiHandler(tornado.web.RequestHandler): # noqa
|
||||
super().__init__(*args, **kwargs)
|
||||
self.json_args = None
|
||||
|
||||
def set_default_headers(self):
|
||||
# 跨域测试用
|
||||
if not self.application.settings['debug']:
|
||||
return
|
||||
self.set_header('Access-Control-Allow-Origin', '*')
|
||||
self.set_header('Access-Control-Allow-Methods', 'OPTIONS, PUT, POST, GET, DELETE')
|
||||
if 'Access-Control-Request-Headers' in self.request.headers:
|
||||
self.set_header('Access-Control-Allow-Headers',
|
||||
self.request.headers['Access-Control-Request-Headers'])
|
||||
|
||||
def prepare(self):
|
||||
if not self.request.headers.get('Content-Type', '').startswith('application/json'):
|
||||
return
|
||||
@ -26,7 +16,3 @@ class ApiHandler(tornado.web.RequestHandler): # noqa
|
||||
self.json_args = json.loads(self.request.body)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
async def options(self, *_args, **_kwargs):
|
||||
# 跨域测试用
|
||||
self.set_status(204 if self.application.settings['debug'] else 405)
|
||||
|
@ -217,7 +217,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler): # noqa
|
||||
self.close()
|
||||
|
||||
async def _on_joined_room(self):
|
||||
if self.application.settings['debug']:
|
||||
if self.settings['debug']:
|
||||
await self._send_test_message()
|
||||
|
||||
# 不允许自动翻译的提示
|
||||
|
44
api/main.py
44
api/main.py
@ -1,4 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import tornado.web
|
||||
|
||||
import api.base
|
||||
@ -25,6 +29,46 @@ class ServerInfoHandler(api.base.ApiHandler): # noqa
|
||||
'version': update.VERSION,
|
||||
'config': {
|
||||
'enableTranslate': cfg.enable_translate,
|
||||
'enableUploadFile': cfg.enable_upload_file,
|
||||
'loaderUrl': cfg.loader_url
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class UploadEmoticonHandler(api.base.ApiHandler): # noqa
|
||||
async def post(self):
|
||||
cfg = config.get_config()
|
||||
if not cfg.enable_upload_file:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
try:
|
||||
file = self.request.files['file'][0]
|
||||
except LookupError:
|
||||
raise tornado.web.MissingArgumentError('file')
|
||||
if len(file.body) > 1024 * 1024:
|
||||
raise tornado.web.HTTPError(413, 'file is too large, size=%d', len(file.body))
|
||||
if not file.content_type.lower().startswith('image/'):
|
||||
raise tornado.web.HTTPError(415)
|
||||
|
||||
url = await asyncio.get_event_loop().run_in_executor(
|
||||
None, self._save_file, self.settings['WEB_ROOT'], file.body
|
||||
)
|
||||
self.write({
|
||||
'url': url
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _save_file(web_root, body):
|
||||
md5 = hashlib.md5(body).hexdigest()
|
||||
rel_path = os.path.join('upload', md5 + '.png')
|
||||
abs_path = os.path.join(web_root, rel_path)
|
||||
tmp_path = abs_path + '.tmp'
|
||||
with open(tmp_path, 'wb') as f:
|
||||
f.write(body)
|
||||
os.replace(tmp_path, abs_path)
|
||||
|
||||
url = rel_path
|
||||
if os.path.sep != '/':
|
||||
url = url.replace(os.path.sep, '/')
|
||||
url = '/' + url
|
||||
return url
|
||||
|
81
config.py
81
config.py
@ -48,6 +48,7 @@ class AppConfig:
|
||||
self.database_url = 'sqlite:///data/database.db'
|
||||
self.tornado_xheaders = False
|
||||
self.loader_url = ''
|
||||
self.enable_upload_file = True
|
||||
|
||||
self.fetch_avatar_interval = 3.5
|
||||
self.fetch_avatar_max_queue_size = 2
|
||||
@ -70,51 +71,57 @@ class AppConfig:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _load_app_config(self, config):
|
||||
def _load_app_config(self, config: configparser.ConfigParser):
|
||||
app_section = config['app']
|
||||
self.database_url = app_section['database_url']
|
||||
self.tornado_xheaders = app_section.getboolean('tornado_xheaders')
|
||||
self.loader_url = app_section['loader_url']
|
||||
self.database_url = app_section.get('database_url', self.database_url)
|
||||
self.tornado_xheaders = app_section.getboolean('tornado_xheaders', fallback=self.tornado_xheaders)
|
||||
self.loader_url = app_section.get('loader_url', self.loader_url)
|
||||
self.enable_upload_file = app_section.getboolean('enable_upload_file', fallback=self.enable_upload_file)
|
||||
|
||||
self.fetch_avatar_interval = app_section.getfloat('fetch_avatar_interval')
|
||||
self.fetch_avatar_max_queue_size = app_section.getint('fetch_avatar_max_queue_size')
|
||||
self.avatar_cache_size = app_section.getint('avatar_cache_size')
|
||||
self.fetch_avatar_interval = app_section.getfloat('fetch_avatar_interval', fallback=self.fetch_avatar_interval)
|
||||
self.fetch_avatar_max_queue_size = app_section.getint('fetch_avatar_max_queue_size',
|
||||
fallback=self.fetch_avatar_max_queue_size)
|
||||
self.avatar_cache_size = app_section.getint('avatar_cache_size', fallback=self.avatar_cache_size)
|
||||
|
||||
self.enable_translate = app_section.getboolean('enable_translate')
|
||||
self.allow_translate_rooms = _str_to_list(app_section['allow_translate_rooms'], int, set)
|
||||
self.translation_cache_size = app_section.getint('translation_cache_size')
|
||||
self.enable_translate = app_section.getboolean('enable_translate', fallback=self.enable_translate)
|
||||
self.allow_translate_rooms = _str_to_list(app_section.get('allow_translate_rooms', ''), int, set)
|
||||
self.translation_cache_size = app_section.getint('translation_cache_size', self.translation_cache_size)
|
||||
|
||||
def _load_translator_configs(self, config):
|
||||
def _load_translator_configs(self, config: configparser.ConfigParser):
|
||||
app_section = config['app']
|
||||
section_names = _str_to_list(app_section['translator_configs'])
|
||||
section_names = _str_to_list(app_section.get('translator_configs', ''))
|
||||
translator_configs = []
|
||||
for section_name in section_names:
|
||||
section = config[section_name]
|
||||
type_ = section['type']
|
||||
try:
|
||||
section = config[section_name]
|
||||
type_ = section['type']
|
||||
|
||||
translator_config = {
|
||||
'type': type_,
|
||||
'query_interval': section.getfloat('query_interval'),
|
||||
'max_queue_size': section.getint('max_queue_size')
|
||||
}
|
||||
if type_ == 'TencentTranslateFree':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
elif type_ == 'BilibiliTranslateFree':
|
||||
pass
|
||||
elif type_ == 'TencentTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['secret_id'] = section['secret_id']
|
||||
translator_config['secret_key'] = section['secret_key']
|
||||
translator_config['region'] = section['region']
|
||||
elif type_ == 'BaiduTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['app_id'] = section['app_id']
|
||||
translator_config['secret'] = section['secret']
|
||||
else:
|
||||
raise ValueError(f'Invalid translator type: {type_}')
|
||||
translator_config = {
|
||||
'type': type_,
|
||||
'query_interval': section.getfloat('query_interval'),
|
||||
'max_queue_size': section.getint('max_queue_size')
|
||||
}
|
||||
if type_ == 'TencentTranslateFree':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
elif type_ == 'BilibiliTranslateFree':
|
||||
pass
|
||||
elif type_ == 'TencentTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['secret_id'] = section['secret_id']
|
||||
translator_config['secret_key'] = section['secret_key']
|
||||
translator_config['region'] = section['region']
|
||||
elif type_ == 'BaiduTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['app_id'] = section['app_id']
|
||||
translator_config['secret'] = section['secret']
|
||||
else:
|
||||
raise ValueError(f'Invalid translator type: {type_}')
|
||||
except Exception: # noqa
|
||||
logger.exception('Failed to load translator=%s config:', section_name)
|
||||
continue
|
||||
|
||||
translator_configs.append(translator_config)
|
||||
self.translator_configs = translator_configs
|
||||
|
@ -15,6 +15,10 @@ tornado_xheaders = false
|
||||
# Use a loader so that you can run OBS before blivechat. If empty, no loader is used
|
||||
loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
|
||||
|
||||
# 允许上传自定义表情文件
|
||||
# Enable uploading custom emote file
|
||||
enable_upload_file = true
|
||||
|
||||
|
||||
# 获取头像间隔时间(秒)。如果小于3秒有很大概率被服务器拉黑
|
||||
# Interval between fetching avatars (seconds). At least 3 seconds is recommended
|
||||
|
0
frontend/public/upload/.gitkeep
Normal file
0
frontend/public/upload/.gitkeep
Normal file
@ -48,9 +48,7 @@ export default class ChatClientRelay {
|
||||
return
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
// 开发时使用localhost:12450
|
||||
const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
|
||||
const url = `${protocol}://${host}/api/chat`
|
||||
const url = `${protocol}://${window.location.host}/api/chat`
|
||||
this.websocket = new WebSocket(url)
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onclose = this.onWsClose.bind(this)
|
||||
|
11
frontend/src/api/main.js
Normal file
11
frontend/src/api/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export async function getServerInfo() {
|
||||
return (await axios.get('/api/server_info')).data
|
||||
}
|
||||
|
||||
export async function uploadEmoticon(file) {
|
||||
let body = new FormData()
|
||||
body.set('file', file)
|
||||
return (await axios.post('/api/emoticon', body)).data
|
||||
}
|
@ -43,6 +43,7 @@ export default {
|
||||
emoticonUrl: 'URL',
|
||||
operation: 'Operation',
|
||||
addEmoticon: 'Add emote',
|
||||
emoticonFileTooLarge: 'File size is too large. Max size is 1MB',
|
||||
|
||||
roomUrl: 'Room URL',
|
||||
enterRoom: 'Enter room',
|
||||
|
@ -43,6 +43,7 @@ export default {
|
||||
emoticonUrl: 'URL',
|
||||
operation: '操作',
|
||||
addEmoticon: 'スタンプを追加',
|
||||
emoticonFileTooLarge: 'ファイルサイズが大きすぎます。最大サイズは1MBです',
|
||||
|
||||
roomUrl: 'ルームのURL',
|
||||
enterRoom: 'ルームに入る',
|
||||
|
@ -43,6 +43,7 @@ export default {
|
||||
emoticonUrl: 'URL',
|
||||
operation: '操作',
|
||||
addEmoticon: '添加表情',
|
||||
emoticonFileTooLarge: '文件尺寸太大,最大1MB',
|
||||
|
||||
roomUrl: '房间URL',
|
||||
enterRoom: '进入房间',
|
||||
|
@ -16,10 +16,6 @@ import Help from './views/Help'
|
||||
import Room from './views/Room'
|
||||
import NotFound from './views/NotFound'
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 开发时使用localhost:12450
|
||||
axios.defaults.baseURL = 'http://localhost:12450'
|
||||
}
|
||||
axios.defaults.timeout = 10 * 1000
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
@ -130,7 +130,9 @@
|
||||
<el-table-column :label="$t('home.operation')" width="170">
|
||||
<template slot-scope="scope">
|
||||
<el-button-group>
|
||||
<el-button type="primary" icon="el-icon-upload2" disabled @click="uploadEmoticon(scope.row)"></el-button>
|
||||
<el-button type="primary" icon="el-icon-upload2" :disabled="!serverConfig.enableUploadFile"
|
||||
@click="uploadEmoticon(scope.row)"
|
||||
></el-button>
|
||||
<el-button type="danger" icon="el-icon-minus" @click="delEmoticon(scope.$index)"></el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
@ -165,10 +167,10 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import axios from 'axios'
|
||||
import download from 'downloadjs'
|
||||
|
||||
import { mergeConfig } from '@/utils'
|
||||
import * as mainApi from '@/api/main'
|
||||
import * as chatConfig from '@/api/chatConfig'
|
||||
|
||||
export default {
|
||||
@ -213,9 +215,10 @@ export default {
|
||||
methods: {
|
||||
async updateServerConfig() {
|
||||
try {
|
||||
this.serverConfig = (await axios.get('/api/server_info')).data.config
|
||||
this.serverConfig = (await mainApi.getServerInfo()).config
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to fetch server information: ${e}`)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
|
||||
@ -228,8 +231,27 @@ export default {
|
||||
delEmoticon(index) {
|
||||
this.form.emoticons.splice(index, 1)
|
||||
},
|
||||
uploadEmoticon() {
|
||||
// TODO WIP
|
||||
uploadEmoticon(emoticon) {
|
||||
let input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/png, image/jpeg, image/jpg, image/gif'
|
||||
input.onchange = async() => {
|
||||
let file = input.files[0]
|
||||
if (file.size > 1024 * 1024) {
|
||||
this.$message.error(this.$t('home.emoticonFileTooLarge'))
|
||||
return
|
||||
}
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await mainApi.uploadEmoticon(file)
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to upload: ${e}`)
|
||||
throw e
|
||||
}
|
||||
emoticon.url = res.url
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
|
||||
enterRoom() {
|
||||
|
@ -1,4 +1,17 @@
|
||||
const API_BASE_URL = 'http://localhost:12450'
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: API_BASE_URL,
|
||||
ws: true
|
||||
},
|
||||
'/upload': {
|
||||
target: API_BASE_URL
|
||||
}
|
||||
}
|
||||
},
|
||||
chainWebpack: config => {
|
||||
const APP_VERSION = `v${process.env.npm_package_version}`
|
||||
|
||||
|
7
main.py
7
main.py
@ -25,6 +25,8 @@ LOG_FILE_NAME = os.path.join(BASE_PATH, 'log', 'blivechat.log')
|
||||
|
||||
routes = [
|
||||
(r'/api/server_info', api.main.ServerInfoHandler),
|
||||
(r'/api/emoticon', api.main.UploadEmoticonHandler),
|
||||
|
||||
(r'/api/chat', api.chat.ChatHandler),
|
||||
(r'/api/room_info', api.chat.RoomInfoHandler),
|
||||
(r'/api/avatar_url', api.chat.AvatarHandler),
|
||||
@ -75,6 +77,7 @@ def init_logging(debug):
|
||||
def run_server(host, port, debug):
|
||||
app = tornado.web.Application(
|
||||
routes,
|
||||
WEB_ROOT=WEB_ROOT,
|
||||
websocket_ping_interval=10,
|
||||
debug=debug,
|
||||
autoreload=False
|
||||
@ -84,7 +87,9 @@ def run_server(host, port, debug):
|
||||
app.listen(
|
||||
port,
|
||||
host,
|
||||
xheaders=cfg.tornado_xheaders
|
||||
xheaders=cfg.tornado_xheaders,
|
||||
max_body_size=1024 * 1024,
|
||||
max_buffer_size=1024 * 1024
|
||||
)
|
||||
except OSError:
|
||||
logger.warning('Address is used %s:%d', host, port)
|
||||
|
Loading…
Reference in New Issue
Block a user