[Enhance][Bug fix] Improve setting file & ...

- Fix a small bug that `best30` of API cannot have scores whose songs are not in database
- At present the setting file can be a module or a file with some of options
- Limiter can have multiple rules together now
This commit is contained in:
Lost-MSth
2022-10-16 15:49:49 +08:00
parent 68a83a29d2
commit ba36190f30
23 changed files with 458 additions and 86 deletions

16
.gitignore vendored
View File

@@ -1,5 +1,19 @@
# log files
*.log
# SSL cert
*.pem
*.key
# sqlite3 database
*.db
*.db.bak*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*$py.class
# setting/config files
latest version/config/
latest version/config.py

View File

@@ -2,10 +2,10 @@ from functools import wraps
from traceback import format_exc
from core.api_user import APIUser
from core.config_manager import Config
from core.error import ArcError, NoAccess, PostError
from core.sql import Connect
from flask import current_app
from setting import Config
from .api_code import error_return

View File

@@ -0,0 +1,259 @@
class Config():
'''
This is the example setting file.
The user's setting file's name is `config.py`.
'''
'''
--------------------
主机的地址和端口号
Host and port of your server
'''
HOST = '0.0.0.0'
PORT = 80
'''
--------------------
'''
'''
--------------------
游戏API地址前缀
Game API's URL prefix
'''
GAME_API_PREFIX = '/join/21'
'''
--------------------
'''
'''
--------------------
允许使用的游戏版本,若为空,则默认全部允许
Allowed game versions
If it is blank, all are allowed.
'''
ALLOW_APPVERSION = ['3.12.6', '3.12.6c',
'4.0.256', '4.0.256c', '4.1.0', '4.1.0c']
'''
--------------------
'''
'''
--------------------
联机功能相关设置请确保与Link Play服务器端的设置一致
Setting of your link play server
Please ensure that the settings on the side of Link Play server are consistent.
'''
# SET_LINKPLAY_SERVER_AS_SUB_PROCESS: 是否同时在本地启动Link Play服务器
# SET_LINKPLAY_SERVER_AS_SUB_PROCESS: If it is `True`, the link play server will run with the main server locally at the same time.
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
# LINKPLAY_HOST: 对主服务器来说的Link Play服务器的地址
# LINKPLAY_HOST: The address of the linkplay server based on the main server. If it is blank, the link play feature will be disabled.
LINKPLAY_HOST = '0.0.0.0'
LINKPLAY_UDP_PORT = 10900
LINKPLAY_TCP_PORT = 10901
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
# LINKPLAY_DISPLAY_HOST: 对客户端来说的Link Play服务器地址如果为空则自动获取
# LINKPLAY_DISPLAY_HOST: The address of the linkplay server based on the client. If it is blank, the host of link play server for the client will be obtained automatically.
LINKPLAY_DISPLAY_HOST = ''
'''
--------------------
'''
'''
--------------------
SSL证书路径
留空则使用HTTP
SSL certificate path
If left blank, use HTTP.
'''
SSL_CERT = '' # *.pem
SSL_KEY = '' # *.key
'''
--------------------
'''
'''
--------------------
愚人节模式开关
Switch of April Fool's Day
'''
IS_APRILFOOLS = True
'''
--------------------
'''
'''
--------------------
世界排名的最大显示数量
The largest number of global rank
'''
WORLD_RANK_MAX = 200
'''
--------------------
'''
'''
--------------------
世界模式当前活动图设置
Current available maps in world mode
'''
AVAILABLE_MAP = [] # Ex. ['test', 'test2']
'''
--------------------
'''
'''
--------------------
Web后台管理页面的用户名和密码
Username and password of web background management page
'''
USERNAME = 'admin'
PASSWORD = 'admin'
'''
--------------------
'''
'''
--------------------
Web后台管理页面的session秘钥如果不知道是什么请不要修改
Session key of web background management page
If you don't know what it is, please don't modify it.
'''
SECRET_KEY = '1145141919810'
'''
--------------------
'''
'''
--------------------
API接口完全控制权限Token留空则不使用
API interface full control permission Token
If you don't want to use it, leave it blank.
'''
API_TOKEN = ''
'''
--------------------
'''
'''
--------------------
歌曲下载地址前缀,留空则自动获取
Song download address prefix
If left blank, it will be obtained automatically.
'''
DOWNLOAD_LINK_PREFIX = '' # http://***.com/download/
'''
--------------------
'''
'''
--------------------
玩家歌曲下载的24小时次数限制每个文件算一次
Player's song download limit times in 24 hours, once per file
'''
DOWNLOAD_TIMES_LIMIT = 3000
'''
歌曲下载链接的有效时长,单位:秒
Effective duration of song download link, unit: seconds
'''
DOWNLOAD_TIME_GAP_LIMIT = 1000
'''
--------------------
'''
'''
--------------------
Arcaea登录的最大允许设备数量最小值为1
The maximum number of devices allowed to log in Arcaea, minimum: 1
'''
LOGIN_DEVICE_NUMBER_LIMIT = 1
'''
是否允许同设备多应用共存登录
请注意这个选项设置为True时下一个选项将自动变为False
If logging in from multiple applications on the same device is allowed
Note that when this option is set to True, the next option automatically becomes False
'''
ALLOW_LOGIN_SAME_DEVICE = False
'''
24小时内登陆设备数超过最大允许设备数量时是否自动封号1天、3天、7天、15天、31天
When the number of login devices exceeds the maximum number of devices allowed to log in Arcaea within 24 hours, whether the account will be automatically banned (1 day, 3 days, 7 days, 15 days, 31 days)
'''
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
'''
--------------------
'''
'''
--------------------
是否记录详细的服务器日志
If recording detailed server logs is enabled
'''
ALLOW_INFO_LOG = False
ALLOW_WARNING_LOG = False
'''
--------------------
'''
'''
--------------------
用户注册时的默认记忆源点数量
The default amount of memories at the time of user registration
'''
DEFAULT_MEMORIES = 0
'''
--------------------
'''
'''
--------------------
数据库更新时,是否采用最新的角色数据,如果你想采用最新的官方角色数据
注意:如果是,旧的数据将丢失;如果否,某些角色的数据变动将无法同步
If using the latest character data when updating database. If you want to only keep newest official character data, please set it `True`.
Note: If `True`, the old data will be lost; If `False`, the data changes of some characters will not be synchronized.
'''
UPDATE_WITH_NEW_CHARACTER_DATA = True
'''
--------------------
'''
'''
--------------------
是否全解锁搭档
If unlocking all partners is enabled
'''
CHARACTER_FULL_UNLOCK = True
'''
--------------------
'''
'''
--------------------
是否全解锁世界歌曲
If unlocking all world songs is enabled
'''
WORLD_SONG_FULL_UNLOCK = True
'''
--------------------
'''
'''
--------------------
是否全解锁世界场景
If unlocking all world sceneries is enabled
'''
WORLD_SCENERY_FULL_UNLOCK = True
'''
--------------------
'''
'''
--------------------
是否强制使用全解锁云端存档
If forcing full unlocked cloud save is enabled
请注意,当前对于最终结局的判定为固定在`Testify`解锁之前
Please note that the current setting of the finale state is before the unlock of `Testify`
'''
SAVE_FULL_UNLOCK = False
'''
--------------------
'''

View File

@@ -1,6 +1,6 @@
from setting import Config
from .error import ArcError, InputError, NoData, ItemNotEnough
from .config_manager import Config
from .constant import Constant
from .error import ArcError, InputError, ItemNotEnough, NoData
from .item import Item, ItemCore

View File

@@ -0,0 +1,81 @@
class Config:
'''
Default config
'''
HOST = '0.0.0.0'
PORT = 80
GAME_API_PREFIX = '/join/21'
ALLOW_APPVERSION = [] # list[str]
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
LINKPLAY_HOST = '0.0.0.0'
LINKPLAY_UDP_PORT = 10900
LINKPLAY_TCP_PORT = 10901
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
LINKPLAY_DISPLAY_HOST = ''
SSL_CERT = ''
SSL_KEY = ''
IS_APRILFOOLS = True
WORLD_RANK_MAX = 200
AVAILABLE_MAP = [] # list[str]
USERNAME = 'admin'
PASSWORD = 'admin'
SECRET_KEY = '1145141919810'
API_TOKEN = ''
DOWNLOAD_LINK_PREFIX = ''
DOWNLOAD_TIMES_LIMIT = 3000
DOWNLOAD_TIME_GAP_LIMIT = 1000
LOGIN_DEVICE_NUMBER_LIMIT = 1
ALLOW_LOGIN_SAME_DEVICE = False
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
ALLOW_INFO_LOG = False
ALLOW_WARNING_LOG = False
DEFAULT_MEMORIES = 0
UPDATE_WITH_NEW_CHARACTER_DATA = True
CHARACTER_FULL_UNLOCK = True
WORLD_SONG_FULL_UNLOCK = True
WORLD_SCENERY_FULL_UNLOCK = True
SAVE_FULL_UNLOCK = False
# ------------------------------------------
# You can change this to make another PTT mechanism.
BEST30_WEIGHT = 1 / 40
RECENT10_WEIGHT = 1 / 40
MAX_FRIEND_COUNT = 50
WORLD_MAP_FOLDER_PATH = './database/map/'
SONG_FILE_FOLDER_PATH = './database/songs/'
SONGLIST_FILE_PATH = './database/songs/songlist'
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
class ConfigManager:
@staticmethod
def load(config):
for k, v in config.__dict__.items():
if k.startswith('__') or k.endswith('__'):
continue
if hasattr(Config, k):
setattr(Config, k, v)

View File

@@ -1,4 +1,4 @@
from setting import Config
from .config_manager import Config
class Constant:
@@ -9,6 +9,8 @@ class Constant:
STAMINA_RECOVER_TICK = 1800000
COURSE_STAMINA_COST = 4
CORE_EXP = 250
LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100,
@@ -19,19 +21,18 @@ class Constant:
AYU_UNCAP_BONUS_PROGRESS = 5
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
MAX_FRIEND_COUNT = 50
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
MY_RANK_MAX_LOCAL_POSITION = 5
MY_RANK_MAX_GLOBAL_POSITION = 9999
# You can change this to make another PTT mechanism.
BEST30_WEIGHT = 1 / 40
RECENT10_WEIGHT = 1 / 40
BEST30_WEIGHT = Config.BEST30_WEIGHT
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
WORLD_MAP_FOLDER_PATH = './database/map/'
SONG_FILE_FOLDER_PATH = './database/songs/'
SONGLIST_FILE_PATH = './database/songs/songlist'
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH
SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
@@ -45,8 +46,6 @@ class Constant:
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
COURSE_STAMINA_COST = 4
# Well, I can't say a word when I see this.
FINALE_SWITCH = [
(0x0015F0, 0x00B032), (0x014C9A, 0x014408), (0x062585, 0x02783B),

View File

@@ -1,91 +1,111 @@
class ArcError(Exception):
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
self.message = message
self.error_code = error_code
self.api_error_code = api_error_code
self.message: str = message
self.error_code: int = error_code
self.api_error_code: int = api_error_code
self.extra_data = extra_data
self.status = status
self.status: int = status
def __str__(self) -> str:
return repr(self.message)
class InputError(ArcError):
# 输入类型错误
'''输入类型错误'''
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class DataExist(ArcError):
# 数据存在
'''数据存在'''
pass
class NoData(ArcError):
# 数据不存在
'''数据不存在'''
def __init__(self, message=None, error_code=108, api_error_code=-2, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class PostError(ArcError):
# 缺少输入
'''缺少输入'''
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class UserBan(ArcError):
# 用户封禁
'''用户封禁'''
def __init__(self, message=None, error_code=121, api_error_code=-202, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class ItemNotEnough(ArcError):
# 物品数量不足
'''物品数量不足'''
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class ItemUnavailable(ArcError):
# 物品不可用
'''物品不可用'''
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class RedeemUnavailable(ArcError):
# 兑换码不可用
'''兑换码不可用'''
def __init__(self, message=None, error_code=505, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class MapLocked(ArcError):
# 地图锁定
'''地图锁定'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class StaminaNotEnough(ArcError):
# 体力不足
'''体力不足'''
def __init__(self, message=None, error_code=107, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class TicketNotEnough(ArcError):
# 记忆源点不足
'''记忆源点不足'''
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class FriendError(ArcError):
# 好友系统出错
'''好友系统出错'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class NoAccess(ArcError):
# 无权限
pass
'''无权限'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=403) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class Timeout(ArcError):
# 超时
'''超时'''
pass
class RateLimit(ArcError):
'''频率达到限制'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=429) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)

View File

@@ -1,5 +1,5 @@
from .config_manager import Config
from .error import InputError, ItemNotEnough, ItemUnavailable, NoData
from setting import Config
class Item:

View File

@@ -1,4 +1,4 @@
from limits import parse, strategies
from limits import parse_many, strategies
from limits.storage import storage_from_string
@@ -6,23 +6,26 @@ class ArcLimiter:
storage = storage_from_string("memory://")
strategy = strategies.FixedWindowRateLimiter(storage)
def __init__(self, limit: str = None, namespace: str = None):
self._limit = None
self.limit = limit
def __init__(self, limit_str: str = None, namespace: str = None):
self._limits = None
self.limits = limit_str
self.namespace = namespace
@property
def limit(self):
return self._limit
def limits(self):
return self._limits
@limit.setter
def limit(self, value):
@limits.setter
def limits(self, value: str):
if value is None:
return
self._limit = parse(value)
self._limits = parse_many(value)
def hit(self, key: str, cost: int = 1) -> bool:
return self.strategy.hit(self.limit, self.namespace, key, cost=cost)
flag = True
for limit in self.limits:
flag &= self.strategy.hit(limit, self.namespace, key, cost)
return flag
def test(self, key: str) -> bool:
return self.strategy.test(self.limit, self.namespace, key)
return all(self.strategy.test(limit, self.namespace, key) for limit in self.limits)

View File

@@ -1,10 +1,8 @@
import json
from time import time
from setting import Config
from core.constant import Constant
from .config_manager import Config
from .constant import Constant
from .error import InputError
from .util import md5

View File

@@ -580,4 +580,5 @@ class UserScoreList:
for score in self.scores:
self.c.execute(
'''select name from chart where song_id = ?''', (score.song.song_id,))
score.song.song_name = self.c.fetchone()[0]
x = self.c.fetchone()
score.song.song_name = x[0] if x else ''

View File

@@ -3,9 +3,8 @@ import hashlib
import time
from os import urandom
from setting import Config
from .character import UserCharacter, UserCharacterList
from .config_manager import Config
from .constant import Constant
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
NoData, UserBan)

View File

@@ -207,11 +207,6 @@ def main(path='./'):
time int,
primary key(user_id, song_id, file_name)
);''')
# c.execute('''create table if not exists user_download(user_id int,
# time int,
# token text,
# primary key(user_id, time, token)
# );''')
c.execute('''create table if not exists item(item_id text,
type text,
is_available int,

View File

@@ -2,12 +2,19 @@
import os
import sys
from importlib import import_module
from logging.config import dictConfig
from multiprocessing import Process, set_start_method
from traceback import format_exc
from flask import Flask, make_response, request, send_from_directory
from core.config_manager import Config, ConfigManager
if os.path.exists('config.py') or os.path.exists('config'):
ConfigManager.load(import_module('config').Config)
import api
import server
import server.init
@@ -16,10 +23,9 @@ import web.login
from core.constant import Constant
from core.download import (UserDownload, get_only_3_song_ids,
initialize_songfile)
from core.error import ArcError
from core.error import ArcError, NoAccess, RateLimit
from core.sql import Connect
from server.func import error_return
from setting import Config
app = Flask(__name__)
@@ -31,6 +37,7 @@ app = Flask(__name__)
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
app.config['SESSION_TYPE'] = 'filesystem'
app.register_blueprint(web.login.bp)
@@ -62,10 +69,9 @@ def download(file_path):
x.song_id, x.file_name = file_path.split('/', 1)
x.select_for_check()
if x.is_limited:
raise ArcError(
'You have reached the download limit.', 903, status=429)
raise RateLimit('You have reached the download limit.', 903)
if not x.is_valid:
raise ArcError('Expired token.', status=403)
raise NoAccess('Expired token.')
x.download_hit()
# response = make_response()
# response.headers['Content-Type'] = 'application/octet-stream'

View File

@@ -1,5 +1,5 @@
from core.config_manager import Config
from flask import Blueprint
from setting import Config
from . import (auth, course, friend, multiplayer, others, present, purchase,
score, user, world)

View File

@@ -1,11 +1,11 @@
import base64
from functools import wraps
from core.config_manager import Config
from core.error import ArcError, NoAccess
from core.sql import Connect
from core.user import UserAuth, UserLogin
from flask import Blueprint, jsonify, request
from setting import Config
from .func import arc_try, error_return

View File

@@ -1,9 +1,9 @@
from functools import wraps
from traceback import format_exc
from core.config_manager import Config
from core.error import ArcError
from flask import current_app, jsonify
from setting import Config
default_error = ArcError('Unknown Error', status=500)

View File

@@ -25,10 +25,6 @@ def check_before_run(app):
f = True
if not os.path.exists('setting.py'):
app.logger.warning('File `setting.py` is missing.')
f = False
if not os.path.exists('database'):
app.logger.warning('Folder `database` is missing.')
f = False

View File

@@ -1,8 +1,8 @@
from core.config_manager import Config
from core.error import ArcError
from core.linkplay import Player, RemoteMultiPlayer, Room
from core.sql import Connect
from flask import Blueprint, request
from setting import Config
from .auth import auth_required
from .func import arc_try, success_return

View File

@@ -2,7 +2,7 @@ import json
from urllib.parse import parse_qs, urlparse
from core.download import DownloadList
from core.error import ArcError
from core.error import RateLimit
from core.sql import Connect
from core.system import GameInfo
from core.user import UserOnline
@@ -34,7 +34,7 @@ def download_song(user_id):
x.song_ids = request.args.getlist('sid')
x.url_flag = json.loads(request.args.get('url', 'true'))
if x.url_flag and x.is_limited:
raise ArcError('You have reached the download limit.', 903)
raise RateLimit('You have reached the download limit.', 903)
x.add_songs()
return success_return(x.urls)

View File

@@ -1,11 +1,11 @@
from core.character import UserCharacter
from core.config_manager import Config
from core.error import ArcError, NoAccess
from core.item import ItemCore
from core.save import SaveData
from core.sql import Connect
from core.user import User, UserLogin, UserOnline, UserRegister
from flask import Blueprint, request
from setting import Config
from .auth import auth_required
from .func import arc_try, success_return

View File

@@ -1,10 +1,10 @@
#import sqlite3
from flask import (Blueprint, flash, g, redirect,
render_template, request, session, url_for)
import functools
from setting import Config
import hashlib
from core.config_manager import Config
from flask import (Blueprint, flash, g, redirect, render_template, request,
session, url_for)
bp = Blueprint('login', __name__, url_prefix='/web')

View File

@@ -1,9 +1,10 @@
import os
from core.sql import Connect
import time
import hashlib
import os
import time
from random import Random
from setting import Config
from core.config_manager import Config
from core.sql import Connect
def int2b(x):
@@ -216,10 +217,10 @@ def update_database():
update_one_table(c1, c2, 'api_login')
update_one_table(c1, c2, 'chart')
update_one_table(c1, c2, 'user_course')
update_one_table(c1, c2, 'course')
update_one_table(c1, c2, 'course_item')
update_one_table(c1, c2, 'course_chart')
update_one_table(c1, c2, 'course_requirement')
# update_one_table(c1, c2, 'course')
# update_one_table(c1, c2, 'course_item')
# update_one_table(c1, c2, 'course_chart')
# update_one_table(c1, c2, 'course_requirement')
update_one_table(c1, c2, 'user_char')