完成上传表情文件

This commit is contained in:
John Smith 2022-02-27 22:05:37 +08:00
parent 903cfecab9
commit 24d0671dae
16 changed files with 157 additions and 65 deletions

View File

@ -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;

View File

@ -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)

View File

@ -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()
# 不允许自动翻译的提示

View File

@ -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

View File

@ -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

View File

@ -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

View File

View 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
View 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
}

View File

@ -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',

View File

@ -43,6 +43,7 @@ export default {
emoticonUrl: 'URL',
operation: '操作',
addEmoticon: 'スタンプを追加',
emoticonFileTooLarge: 'ファイルサイズが大きすぎます。最大サイズは1MBです',
roomUrl: 'ルームのURL',
enterRoom: 'ルームに入る',

View File

@ -43,6 +43,7 @@ export default {
emoticonUrl: 'URL',
operation: '操作',
addEmoticon: '添加表情',
emoticonFileTooLarge: '文件尺寸太大最大1MB',
roomUrl: '房间URL',
enterRoom: '进入房间',

View File

@ -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)

View File

@ -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() {

View File

@ -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}`

View File

@ -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)