diff --git a/README.md b/README.md index 9ab3cb6..2b30928 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/api/base.py b/api/base.py index 81e1e72..b9d0f25 100644 --- a/api/base.py +++ b/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) diff --git a/api/chat.py b/api/chat.py index 79388c5..81b5172 100644 --- a/api/chat.py +++ b/api/chat.py @@ -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() # 不允许自动翻译的提示 diff --git a/api/main.py b/api/main.py index 1a91da8..118819f 100644 --- a/api/main.py +++ b/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 diff --git a/config.py b/config.py index 5a6378c..0dcaf30 100644 --- a/config.py +++ b/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 diff --git a/data/config.example.ini b/data/config.example.ini index 062e02f..c0eef9b 100644 --- a/data/config.example.ini +++ b/data/config.example.ini @@ -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 diff --git a/frontend/public/upload/.gitkeep b/frontend/public/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/api/chat/ChatClientRelay.js b/frontend/src/api/chat/ChatClientRelay.js index c49a47c..552dd20 100644 --- a/frontend/src/api/chat/ChatClientRelay.js +++ b/frontend/src/api/chat/ChatClientRelay.js @@ -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) diff --git a/frontend/src/api/main.js b/frontend/src/api/main.js new file mode 100644 index 0000000..fd9571a --- /dev/null +++ b/frontend/src/api/main.js @@ -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 +} diff --git a/frontend/src/lang/en.js b/frontend/src/lang/en.js index 0ffb1f2..5a98318 100644 --- a/frontend/src/lang/en.js +++ b/frontend/src/lang/en.js @@ -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', diff --git a/frontend/src/lang/ja.js b/frontend/src/lang/ja.js index cdd94b3..5adbf24 100644 --- a/frontend/src/lang/ja.js +++ b/frontend/src/lang/ja.js @@ -43,6 +43,7 @@ export default { emoticonUrl: 'URL', operation: '操作', addEmoticon: 'スタンプを追加', + emoticonFileTooLarge: 'ファイルサイズが大きすぎます。最大サイズは1MBです', roomUrl: 'ルームのURL', enterRoom: 'ルームに入る', diff --git a/frontend/src/lang/zh.js b/frontend/src/lang/zh.js index c133bbe..9f48ff1 100644 --- a/frontend/src/lang/zh.js +++ b/frontend/src/lang/zh.js @@ -43,6 +43,7 @@ export default { emoticonUrl: 'URL', operation: '操作', addEmoticon: '添加表情', + emoticonFileTooLarge: '文件尺寸太大,最大1MB', roomUrl: '房间URL', enterRoom: '进入房间', diff --git a/frontend/src/main.js b/frontend/src/main.js index 44d8874..84483e8 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -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) diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index dae4f8d..8a1f466 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -130,7 +130,9 @@ @@ -165,10 +167,10 @@