Update to v2.8.5

This commit is contained in:
Lost-MSth
2022-04-28 18:29:26 +08:00
parent aa77a4c903
commit 96fbd26342
19 changed files with 708 additions and 330 deletions

View File

@@ -66,20 +66,25 @@ It is just so interesting. What it can do is under exploration.
> Tips: When updating, please keep the original database in case of data loss.
### Version 2.8.4
- &spades;适用于Arcaea 4.0.&infin;<!--3.12.6-->版本&spades; For Arcaea 3.12.6
- &hearts;修复616==sb?PTT:PPT<!--**凛可**-->缺少Bug<!--语音-->的问题&hearts; Fix missing voices of **Linka**.
### Version 2.8.5
- 适用于Arcaea 3.12.8版本 For Arcaea 3.12.8
- 更新了歌曲数据库 Update the song database.
- 修复一个导致无法升级角色的拼写错误 Fix a typing error, which makes giving characters Exp wrong.
- 尝试强制指定多进程启动方式为spawn这可能对UNIX系统中UDP服务器的启动有所帮助 Try to forcibly specify the multiprocess startup mode as spawn, which may be helpful for the startup of UDP server in UNIX system.
- 添加了注册API对外接口 Add an external API interface of user register.
- 重构一些代码,顺手修复了登陆时因多设备登录封号的用户没有正确显示错误提示的问题 Refactoring some codes and in passing fix the problem that users which has been banned because of multiple devices do not show error messages correctly when logging in.
> 注意:
> - 现在Flask最低版本要求提高到2.0
> - 服务端可能不再完全支持低版本客户端
> - 对3.12.6c版本,愚人节开关打开时点击`网络`按钮会闪退,原因不明
>
> Note:
> - Now the version of Flask which is required should be up to 2.0.
> - The server may no longer fully support lower version clients.
> - For version 3.12.6c, when the switch of April Fool's Day is on, clicking the `Network` button will make the client break down. The reason is not clear now.
- 以下大概可能也许不得不是累积更新 The following are cumulative updates:
- &clubs;更新了█████[](歌曲数据库)&clubs; Update the song database.
- &diams;尝试对!@#$%^<!--5周年兑换券-->提供支持&diams; Try to add support for Anniversary 5 ticket.
- &iquest;新搭档' or name = 'Taikari'--<!--**凛可**-->已解锁&iquest; Unlock the character **Linka**.
<!--
本项目不会像lowiro一样为各位用户制造各种奇奇怪怪的麻烦即便是在愚人节这种盛大的节日大家也能放心使用
Happy April Fool's Day!
Sorry for no joke in English.
-->
## 运行环境与依赖 Running environment and requirements

View File

@@ -1,10 +1,10 @@
from flask import (
Blueprint, request, jsonify
Blueprint, request
)
from .api_code import code_get_msg, return_encode
from .api_auth import role_required
from core.user import RegisterUser
from core.user import UserRegister
from core.error import ArcError, PostError
from server.sql import Connect
from server.sql import Sql
@@ -20,7 +20,7 @@ bp = Blueprint('users', __name__, url_prefix='/users')
def users_post(user):
# 注册用户
with Connect() as c:
new_user = RegisterUser(c)
new_user = UserRegister(c)
try:
if 'name' in request.json:
new_user.set_name(request.json['name'])

View File

@@ -0,0 +1,213 @@
from setting import Config
from .error import ArcError, NoData
from .constant import Constant
from .item import Item
class Level:
max_level = None
mid_level = 20
min_level = 1
level = None
exp = None
def __init__(self) -> None:
pass
@property
def level_exp(self):
return Constant.LEVEL_STEPS[self.level]
class Skill:
skill_id = None
skill_id_uncap = None
skill_unlock_level = None
skill_requires_uncap = None
def __init__(self) -> None:
pass
class Core(Item):
item_type = 'core'
amount = None
def __init__(self, core_type: str = '', amount: int = 0) -> None:
super().__init__()
self.item_id = core_type
self.amount = amount
def to_dict(self):
return {'core_type': self.item_id, 'amount': self.amount}
class CharacterValue:
start = None
mid = None
end = None
def __init__(self) -> None:
pass
@staticmethod
def _calc_char_value_20(level, stata, statb, lva=1, lvb=20):
# 计算1~20级搭档数值的核心函数返回浮点数来自https://redive.estertion.win/arcaea/calc/
n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946,
0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1]
e = n[lva] - n[lvb]
a = stata - statb
r = a / e
d = stata - n[lva] * r
return d + r * n[level]
@staticmethod
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
# 计算21~30级搭档数值返回浮点数
return (level - lva) * (statb - stata) / (lvb - lva) + stata
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
self.start = start
self.mid = mid
self.end = end
def get_value(self, level: Level):
if level.min_level <= level.level <= level.mid_level:
return self._calc_char_value_20(level.level, self.start, self.mid)
elif level.mid_level < level.level <= level.max_level:
return self._calc_char_value_30(level.level, self.mid, self.end)
else:
return 0
class Character:
character_id = None
name = None
char_type = None
is_uncapped = None
is_uncapped_override = None
skill = Skill()
level = Level()
frag = CharacterValue()
prog = CharacterValue()
overdrive = CharacterValue()
uncap_cores = None
def __init__(self) -> None:
pass
@property
def uncap_cores_to_dict(self):
return [x.to_dict() for x in self.uncap_cores]
class UserCharacter(Character):
def __init__(self, c, character_id=None) -> None:
super().__init__()
self.c = c
self.character_id = character_id
def select_character_core(self):
# 获取此角色所需核心
self.c.execute(
'''select item_id, amount from char_item where character_id = ? and type="core"''', (self.character_id,))
x = self.c.fetchall()
if x:
self.uncap_cores = []
for i in x:
self.uncap_cores.append(Core(i[0], i[1]))
def select_character_uncap_condition(self, user):
# parameter: user - User类或子类的实例
# 获取此角色的觉醒信息
if Config.CHARACTER_FULL_UNLOCK:
self.c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id = :a and character_id = :b''',
{'a': user.user_id, 'b': self.character_id})
else:
self.c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
{'a': user.user_id, 'b': self.character_id})
x = self.c.fetchone()
if not x:
raise NoData('The character of the user does not exist.')
self.is_uncapped = x[0] == 1
self.is_uncapped_override = x[1] == 1
def select_character_info(self, user):
# parameter: user - User类或子类的实例
# 获取所给用户此角色信息
if not Config.CHARACTER_FULL_UNLOCK:
self.c.execute('''select * from user_char a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
(user.user_id, self.character_id))
else:
self.c.execute('''select * from user_char_full a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
(user.user_id, self.character_id))
y = self.c.fetchone()
if y is None:
raise NoData('The character of the user does not exist.')
self.name = y[7]
self.char_type = y[22]
self.is_uncapped = y[4] == 1
self.is_uncapped_override = y[5] == 1
self.level.level = y[2]
self.level.exp = y[3]
self.level.max_level = y[8]
self.frag.set_parameter(y[9], y[12], y[15])
self.prog.set_parameter(y[10], y[13], y[16])
self.overdrive.set_parameter(y[11], y[14], y[17])
self.skill.skill_id = y[18]
self.skill.skill_id_uncap = y[21]
self.skill.skill_unlock_level = y[19]
self.skill.skill_requires_uncap = y[20] == 1
self.select_character_core()
@property
def to_dict(self):
return {"is_uncapped_override": self.is_uncapped_override,
"is_uncapped": self.is_uncapped,
"uncap_cores": self.uncap_cores_to_dict,
"char_type": self.char_type,
"skill_id_uncap": self.skill.skill_id_uncap,
"skill_requires_uncap": self.skill.skill_requires_uncap,
"skill_unlock_level": self.skill.skill_unlock_level,
"skill_id": self.skill.skill_id,
"overdrive": self.overdrive.get_value(self.level),
"prog": self.overdrive.get_value(self.level),
"frag": self.overdrive.get_value(self.level),
"level_exp": self.level.level_exp,
"exp": self.level.exp,
"level": self.level.level,
"name": self.name,
"character_id": self.character_id
}
def change_uncap_override(self, user):
# parameter: user - User类或子类的实例
# 切换觉醒状态
if not Config.CHARACTER_FULL_UNLOCK:
self.c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
{'a': user.user_id, 'b': self.character_id})
else:
self.c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id = :a and character_id = :b''',
{'a': user.user_id, 'b': self.character_id})
x = self.c.fetchone()
if x is None or x[0] == 0:
raise ArcError('Unknown Error')
self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
'a': 1 if x[1] == 0 else 0, 'b': user.user_id})
if not Config.CHARACTER_FULL_UNLOCK:
self.c.execute('''update user_char set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
'a': 1 if x[1] == 0 else 0, 'b': user.user_id, 'c': self.character_id})
else:
self.c.execute('''update user_char_full set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
'a': 1 if x[1] == 0 else 0, 'b': user.user_id, 'c': self.character_id})
def character_uncap(self):
# 觉醒角色
pass

View File

@@ -0,0 +1,16 @@
class Constant:
BAN_TIME = [1, 3, 7, 15, 31]
MAX_STAMINA = 12
STAMINA_RECOVER_TICK = 1800000
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,
17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000}
ETO_UNCAP_BONUS_PROGRESS = 7
LUNA_UNCAP_BONUS_PROGRESS = 7
AYU_UNCAP_BONUS_PROGRESS = 5

View File

@@ -2,27 +2,50 @@ class ArcError(Exception):
api_error_code = -999
error_code = 108
message = None
extra_data = None
def __init__(self, message=None, error_code=None, api_error_code=None) -> None:
def __init__(self, message=None, error_code=None, api_error_code=None, extra_data=None) -> None:
self.message = message
if error_code:
self.error_code = error_code
if api_error_code:
self.api_error_code = api_error_code
if extra_data:
self.extra_data = extra_data
def __str__(self) -> str:
return repr(self.message)
class InputError(ArcError):
def __init__(self, message=None, error_code=None, api_error_code=-100) -> None:
super().__init__(message, error_code, api_error_code)
# 输入类型错误
def __init__(self, message=None, error_code=None, api_error_code=-100, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data)
class DataExist(ArcError):
# 数据存在
pass
class NoData(ArcError):
# 数据不存在
def __init__(self, message=None, error_code=None, api_error_code=-2, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data)
class PostError(ArcError):
def __init__(self, message=None, error_code=None, api_error_code=-100) -> None:
super().__init__(message, error_code, api_error_code)
# 缺少输入
def __init__(self, message=None, error_code=None, api_error_code=-100, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data)
class UserBan(ArcError):
# 用户封禁
def __init__(self, message=None, error_code=121, api_error_code=None, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data)
class NoAccess(ArcError):
# 无权限
pass

View File

@@ -0,0 +1,7 @@
class Item:
item_id = None
item_type = None
is_available = None
def __init__(self) -> None:
pass

View File

@@ -1,7 +1,11 @@
from .error import ArcError, InputError, DataExist
from .error import ArcError, InputError, DataExist, NoAccess, NoData, UserBan
from .constant import Constant
from .character import UserCharacter
from setting import Config
import hashlib
import base64
import time
from os import urandom
class User:
@@ -15,7 +19,9 @@ class User:
pass
class RegisterUser(User):
class UserRegister(User):
hash_pwd = None
def __init__(self, c) -> None:
super().__init__()
self.c = c
@@ -52,6 +58,7 @@ class RegisterUser(User):
raise InputError('Email address is invalid.')
def _build_user_code(self):
# 生成9位的user_code用的自然是随机
from random import randint
random_times = 0
@@ -103,4 +110,171 @@ class RegisterUser(User):
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email})
self.c.execute('''insert into recent30(user_id) values(:user_id)''', {
'user_id': self.user_id})
'user_id': self.user_id})
class UserLogin(User):
# 密码和token的加密方式为 SHA-256
device_id = None
ip = None
hash_pwd = None
token = None
now = 0
def __init__(self, c) -> None:
super().__init__()
self.c = c
def set_name(self, name: str):
self.name = name
def set_password(self, password: str):
self.password = password
self.hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
def set_device_id(self, device_id: str):
self.device_id = device_id
def set_ip(self, ip: str):
self.ip = ip
def _arc_auto_ban(self):
# 多设备自动封号机制,返回封号时长
self.c.execute('''delete from login where user_id=?''',
(self.user_id, ))
self.c.execute(
'''select ban_flag from user where user_id=?''', (self.user_id,))
x = self.c.fetchone()
if x and x[0] != '' and x[0] is not None:
last_ban_time = int(x[0].split(':', 1)[0])
i = 0
while i < len(Constant.BAN_TIME) - 1 and Constant.BAN_TIME[i] <= last_ban_time:
i += 1
ban_time = Constant.BAN_TIME[i]
else:
ban_time = Constant.BAN_TIME[0]
ban_flag = ':'.join(
(str(ban_time), str(self.now + ban_time * 86400000)))
self.c.execute('''update user set ban_flag=? where user_id=?''',
(ban_flag, self.user_id))
return ban_time * 86400000
def _check_device(self, device_list):
should_delete_num = len(
device_list) + 1 - Config.LOGIN_DEVICE_NUMBER_LIMIT
if not Config.ALLOW_LOGIN_SAME_DEVICE:
if self.device_id in device_list: # 对相同设备进行删除
self.c.execute('''delete from login where login_device=:a and user_id=:b''', {
'a': self.device_id, 'b': self.user_id})
should_delete_num = len(
device_list) + 1 - device_list.count(self.device_id) - Config.LOGIN_DEVICE_NUMBER_LIMIT
if should_delete_num >= 1: # 删掉多余token
if not Config.ALLOW_LOGIN_SAME_DEVICE and Config.ALLOW_BAN_MULTIDEVICE_USER_AUTO: # 自动封号检查
self.c.execute(
'''select count(*) from login where user_id=? and login_time>?''', (self.user_id, self.now-86400000))
if self.c.fetchone()[0] >= Config.LOGIN_DEVICE_NUMBER_LIMIT:
remaining_ts = self._arc_auto_ban()
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
'remaining_ts': remaining_ts})
self.c.execute('''delete from login where rowid in (select rowid from login where user_id=:user_id limit :a);''',
{'user_id': self.user_id, 'a': int(should_delete_num)})
def login(self, name: str = '', password: str = '', device_id: str = '', ip: str = ''):
if name:
self.set_name(name)
if password:
self.set_password(password)
if device_id:
self.set_device_id(device_id)
if ip:
self.set_ip(ip)
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
'name': self.name})
x = self.c.fetchone()
if x is None:
raise NoData('Username does not exist.', 104)
self.now = int(time.time() * 1000)
if x[2] is not None and x[2] != '':
# 自动封号检查
ban_timestamp = int(x[2].split(':', 1)[1])
if ban_timestamp > self.now:
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
'remaining_ts': ban_timestamp-self.now})
if x[1] == '':
# 账号封禁
raise UserBan('The account has been banned.', 106)
if x[1] != self.hash_pwd:
raise NoAccess('Wrong password.', 104)
self.user_id = str(x[0])
self.token = base64.b64encode(hashlib.sha256(
(self.user_id + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
self.c.execute(
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
y = self.c.fetchall()
if y:
self._check_device([i[0] if i[0] else '' for i in y])
self.c.execute('''insert into login values(:access_token, :user_id, :time, :ip, :device_id)''', {
'user_id': self.user_id, 'access_token': self.token, 'device_id': self.device_id, 'time': self.now, 'ip': self.ip})
class UserAuth(User):
token = None
def __init__(self, c) -> None:
super().__init__()
self.c = c
def token_get_id(self):
# 用token获取id没有考虑不同用户token相同情况说不定会有bug
self.c.execute('''select user_id from login where access_token = :token''', {
'token': self.token})
x = self.c.fetchone()
if x is not None:
self.user_id = x[0]
else:
raise NoAccess('Wrong token.', -4)
return self.user_id
def code_get_id(self):
# 用user_code获取id
self.c.execute('''select user_id from user where user_code = :a''',
{'a': self.user_code})
x = self.c.fetchone()
if x is not None:
self.user_id = x[0]
else:
raise NoData('No user.', 401, -3)
return self.user_id
class UserOnline(User):
character = None
def __init__(self, c, user_id=None) -> None:
super().__init__()
self.c = c
self.user_id = user_id
def change_character(self, character_id: int, skill_sealed: bool = False):
# 用户角色改变,包括技能封印的改变
self.character = UserCharacter(self.c, character_id)
self.character.select_character_uncap_condition(self)
self.c.execute('''update user set is_skill_sealed = :a, character_id = :b, is_char_uncapped = :c, is_char_uncapped_override = :d where user_id = :e''', {
'a': 1 if skill_sealed else 0, 'b': self.character.character_id, 'c': self.character.is_uncapped, 'd': self.character.is_uncapped_override, 'e': self.user_id})

Binary file not shown.

View File

@@ -4,7 +4,7 @@ import json
# 数据库初始化文件删掉arcaea_database.db文件后运行即可谨慎使用
ARCAEA_SERVER_VERSION = 'v2.8.4'
ARCAEA_SERVER_VERSION = 'v2.8.5'
def main(path='./'):
@@ -387,7 +387,7 @@ def main(path='./'):
c.execute('''insert into item values(?,"core",1,'')''', (i,))
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3"]
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote"]
for i in world_songs:
c.execute('''insert into item values(?,"world_song",1,'')''', (i,))

View File

@@ -552,5 +552,23 @@
],
"price": 400,
"orig_price": 400
},
{
"name": "lanota_append_1",
"items": [
{
"type": "pack",
"id": "lanota_append_1",
"is_available": true
},
{
"type": "core",
"amount": 3,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 300,
"price": 300
}
]

View File

@@ -1162,5 +1162,23 @@
],
"orig_price": 100,
"price": 100
},
{
"name": "picopicotranslation",
"items": [
{
"type": "single",
"id": "picopicotranslation",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
}
]

View File

@@ -3,7 +3,7 @@
from flask import Flask, json, request, jsonify, send_from_directory
from logging.config import dictConfig
from setting import Config
import base64
import server
import server.auth
import server.info
import server.setme
@@ -36,6 +36,7 @@ app.config['SESSION_TYPE'] = 'filesystem'
app.register_blueprint(web.login.bp)
app.register_blueprint(web.index.bp)
app.register_blueprint(api.api_main.bp)
app.register_blueprint(server.bp)
conn1, conn2 = Pipe()
@@ -146,60 +147,6 @@ def favicon():
return app.send_static_file('favicon.ico')
@app.route(add_url_prefix('/auth/login'), methods=['POST']) # 登录接口
def login():
if 'AppVersion' in request.headers: # 版本检查
if Config.ALLOW_APPVERSION:
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return error_return(1203)
headers = request.headers
id_pwd = headers['Authorization']
id_pwd = base64.b64decode(id_pwd[6:]).decode()
name, password = id_pwd.split(':', 1)
if 'DeviceId' in headers:
device_id = headers['DeviceId']
else:
device_id = 'low_version'
token, user_id, error_code, extra = server.auth.arc_login(
name, password, device_id, request.remote_addr)
if not error_code:
r = {"success": True, "token_type": "Bearer", 'user_id': user_id}
r['access_token'] = token
return jsonify(r)
else:
if extra:
return error_return(error_code, extra)
else:
return error_return(error_code)
@app.route(add_url_prefix('/user'), methods=['POST']) # 注册接口
def register():
if 'AppVersion' in request.headers: # 版本检查
if Config.ALLOW_APPVERSION:
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return error_return(5)
name = request.form['name']
password = request.form['password']
email = request.form['email']
if 'device_id' in request.form:
device_id = request.form['device_id']
else:
device_id = 'low_version'
user_id, token, error_code = server.auth.arc_register(
name, password, device_id, email, request.remote_addr)
if user_id is not None:
r = {"success": True, "value": {
'user_id': user_id, 'access_token': token}}
return jsonify(r)
else:
return error_return(error_code)
@app.route(add_url_prefix('/purchase/bundle/pack'), methods=['GET']) # 曲包信息
@server.auth.auth_required(request)
def bundle_pack(user_id):
@@ -279,43 +226,6 @@ def user_me(user_id):
return error_return(108)
@app.route(add_url_prefix('/user/me/character'), methods=['POST']) # 角色切换
@server.auth.auth_required(request)
def character_change(user_id):
character_id = request.form['character']
skill_sealed = request.form['skill_sealed']
flag = server.setme.change_char(user_id, character_id, skill_sealed)
if flag:
return jsonify({
"success": True,
"value": {
"user_id": user_id,
"character": character_id
}
})
else:
return error_return(108)
# 角色觉醒切换
@app.route(add_url_prefix('/<path:path>/toggle_uncap', True), methods=['POST'])
@server.auth.auth_required(request)
def character_uncap(user_id, path):
character_id = int(path[path.find('character')+10:])
r = server.setme.change_char_uncap(user_id, character_id)
if r is not None:
return jsonify({
"success": True,
"value": {
"user_id": user_id,
"character": [r]
}
})
else:
return error_return(108)
# 角色觉醒
@app.route(add_url_prefix('/<path:path>/uncap', True), methods=['POST'])
@server.auth.auth_required(request)

View File

@@ -1,2 +1,3 @@
cd /d %~dp0
:: Develop server
python -B main.py

View File

@@ -0,0 +1,8 @@
from flask import Blueprint
from setting import Config
from . import user
from . import auth
bp = Blueprint('server', __name__, url_prefix=Config.GAME_API_PREFIX)
bp.register_blueprint(user.bp)
bp.register_blueprint(auth.bp)

View File

@@ -1,187 +1,43 @@
import hashlib
import time
from server.sql import Connect
from flask import Blueprint, request, jsonify
import functools
import base64
from core.error import ArcError, NoAccess
from core.user import UserAuth, UserLogin
from core.sql import Connect
from .func import error_return
from setting import Config
from flask import jsonify
BAN_TIME = [1, 3, 7, 15, 31]
def arc_login(name: str, password: str, device_id: str, ip: str): # 登录判断
# 查询数据库中的user表验证账号密码返回并记录token和user_id多返回个error code和extra
# token采用user_id和时间戳连接后hash生成真的是瞎想的没用bear
# 密码和token的加密方式为 SHA-256
bp = Blueprint('auth', __name__, url_prefix='/auth')
error_code = 108
token = None
user_id = None
@bp.route('/login', methods=['POST']) # 登录接口
def login():
if 'AppVersion' in request.headers: # 版本检查
if Config.ALLOW_APPVERSION:
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return error_return(NoAccess('Wrong app version.', 1203))
headers = request.headers
request.form['grant_type']
with Connect() as c:
hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
c.execute('''select user_id, password, ban_flag from user where name = :name''', {
'name': name})
x = c.fetchone()
if x is not None:
now = int(time.time() * 1000)
if x[2] is not None and x[2] != '':
# 自动封号检查
ban_timestamp = int(x[2].split(':', 1)[1])
if ban_timestamp > now:
return None, 105, {'remaining_ts': ban_timestamp-now}
if x[1] == '':
# 账号封禁
error_code = 106
elif x[1] == hash_pwd:
user_id = str(x[0])
token = hashlib.sha256(
(user_id + str(now)).encode("utf8")).hexdigest()
c.execute(
'''select login_device from login where user_id = :user_id''', {"user_id": user_id})
y = c.fetchall()
if y:
device_list = []
for i in y:
if i[0]:
device_list.append(i[0])
else:
device_list.append('')
should_delete_num = len(
device_list) + 1 - Config.LOGIN_DEVICE_NUMBER_LIMIT
if not Config.ALLOW_LOGIN_SAME_DEVICE:
if device_id in device_list: # 对相同设备进行删除
c.execute('''delete from login where login_device=:a and user_id=:b''', {
'a': device_id, 'b': user_id})
should_delete_num = len(
device_list) + 1 - device_list.count(device_id) - Config.LOGIN_DEVICE_NUMBER_LIMIT
if should_delete_num >= 1: # 删掉多余token
if not Config.ALLOW_LOGIN_SAME_DEVICE and Config.ALLOW_BAN_MULTIDEVICE_USER_AUTO: # 自动封号检查
c.execute(
'''select count(*) from login where user_id=? and login_time>?''', (user_id, now-86400000))
if c.fetchone()[0] >= Config.LOGIN_DEVICE_NUMBER_LIMIT:
remaining_ts = arc_auto_ban(c, user_id, now)
return None, None, 105, {'remaining_ts': remaining_ts}
c.execute('''delete from login where rowid in (select rowid from login where user_id=:user_id limit :a);''',
{'user_id': user_id, 'a': int(should_delete_num)})
c.execute('''insert into login values(:access_token, :user_id, :time, :ip, :device_id)''', {
'user_id': user_id, 'access_token': token, 'device_id': device_id, 'time': now, 'ip': ip})
error_code = None
try:
id_pwd = headers['Authorization']
id_pwd = base64.b64decode(id_pwd[6:]).decode()
name, password = id_pwd.split(':', 1)
if 'DeviceId' in headers:
device_id = headers['DeviceId']
else:
# 密码错误
error_code = 104
else:
# 用户名错误
error_code = 104
device_id = 'low_version'
return token, user_id, error_code, None
user = UserLogin(c)
user.login(name, password, device_id, request.remote_addr)
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
except ArcError as e:
return error_return(e)
def arc_register(name: str, password: str, device_id: str, email: str, ip: str): # 注册
# 账号注册记录hash密码、用户名和邮箱生成user_id和user_code自动登录返回token
# token和密码的处理同登录部分
def build_user_code(c):
# 生成9位的user_code用的自然是随机
import random
flag = True
while flag:
user_code = ''.join([str(random.randint(0, 9)) for i in range(9)])
c.execute('''select exists(select * from user where user_code = :user_code)''',
{'user_code': user_code})
if c.fetchone() == (0,):
flag = False
return user_code
def build_user_id(c):
# 生成user_id往后加1
c.execute('''select max(user_id) from user''')
x = c.fetchone()
if x[0] is not None:
return x[0] + 1
else:
return 2000001
def insert_user_char(c, user_id):
# 为用户添加初始角色
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
(user_id, 0, 1, 0, 0, 0))
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
(user_id, 1, 1, 0, 0, 0))
c.execute('''select character_id, max_level, is_uncapped from character''')
x = c.fetchall()
if x:
for i in x:
exp = 25000 if i[1] == 30 else 10000
c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
(user_id, i[0], i[1], exp, i[2], 0))
user_id = None
token = None
error_code = 108
with Connect() as c:
hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
c.execute(
'''select exists(select * from user where name = :name)''', {'name': name})
if c.fetchone() == (0,):
c.execute(
'''select exists(select * from user where email = :email)''', {'email': email})
if c.fetchone() == (0,):
user_code = build_user_code(c)
user_id = build_user_id(c)
now = int(time.time() * 1000)
c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email)
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
''', {'user_code': user_code, 'user_id': user_id, 'join_date': now, 'name': name, 'password': hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': email})
c.execute('''insert into recent30(user_id) values(:user_id)''', {
'user_id': user_id})
token = hashlib.sha256(
(str(user_id) + str(now)).encode("utf8")).hexdigest()
c.execute('''insert into login values(:access_token, :user_id, :time, :ip, :device_id)''', {
'user_id': user_id, 'access_token': token, 'device_id': device_id, 'time': now, 'ip': ip})
insert_user_char(c, user_id)
error_code = 0
else:
error_code = 102
else:
error_code = 101
return user_id, token, error_code
def token_get_id(token: str):
# 用token获取id没有考虑不同用户token相同情况说不定会有bug
user_id = None
with Connect() as c:
c.execute('''select user_id from login where access_token = :token''', {
'token': token})
x = c.fetchone()
if x is not None:
user_id = x[0]
return user_id
def code_get_id(user_code):
# 用user_code获取id
user_id = None
with Connect() as c:
c.execute('''select user_id from user where user_code = :a''',
{'a': user_code})
x = c.fetchone()
if x is not None:
user_id = x[0]
return user_id
return error_return()
def auth_required(request):
@@ -190,44 +46,22 @@ def auth_required(request):
@functools.wraps(view)
def wrapped_view(*args, **kwargs):
user_id = None
headers = request.headers
if 'AppVersion' in headers: # 版本检查
if Config.ALLOW_APPVERSION:
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return jsonify({"success": False, "error_code": 1203})
return error_return(NoAccess('Wrong app version.', 1203))
if 'Authorization' in headers:
token = headers['Authorization']
token = token[7:]
user_id = token_get_id(token)
with Connect() as c:
try:
user = UserAuth(c)
user.token = headers['Authorization'][7:]
return view(user.token_get_id(), *args, **kwargs)
except ArcError as e:
return error_return(e)
if user_id is not None:
return view(user_id, *args, **kwargs)
else:
return jsonify({"success": False, "error_code": 108})
return error_return()
return wrapped_view
return decorator
def arc_auto_ban(c, user_id, now):
# 多设备自动封号机制,返回封号时长
c.execute('''delete from login where user_id=?''', (user_id, ))
c.execute('''select ban_flag from user where user_id=?''', (user_id,))
x = c.fetchone()
if x and x[0] != '' and x[0] is not None:
last_ban_time = int(x[0].split(':', 1)[0])
i = 0
while i < len(BAN_TIME) - 1 and BAN_TIME[i] <= last_ban_time:
i += 1
ban_time = BAN_TIME[i]
else:
ban_time = BAN_TIME[0]
ban_flag = ':'.join((str(ban_time), str(now + ban_time*24*60*60*1000)))
c.execute('''update user set ban_flag=? where user_id=?''',
(ban_flag, user_id))
return ban_time*24*60*60*1000

View File

@@ -636,6 +636,9 @@ class Constant:
}, {
"unlock_key": "nhelv|2|0",
"complete": 1
}, {
"unlock_key": "nekonote|2|0",
"complete": 1
}, {
"unlock_key": "memoryforest|1|0",
"complete": 1
@@ -681,6 +684,9 @@ class Constant:
}, {
"unlock_key": "lostdesire|1|0",
"complete": 1
}, {
"unlock_key": "nekonote|1|0",
"complete": 1
}, {
"unlock_key": "lostcivilization|1|0",
"complete": 1
@@ -777,12 +783,6 @@ class Constant:
}, {
"unlock_key": "gothiveofra|1|0",
"complete": 1
}, {
"unlock_key": "etherstrike|1|0",
"complete": 1
}, {
"unlock_key": "singularity|2|0",
"complete": 1
}, {
"unlock_key": "lightningscrew|1|0",
"complete": 1
@@ -801,6 +801,9 @@ class Constant:
}, {
"unlock_key": "pragmatism|2|0",
"complete": 1
}, {
"unlock_key": "stasis|2|0",
"complete": 1
}, {
"unlock_key": "oracle|2|0",
"complete": 1
@@ -861,6 +864,12 @@ class Constant:
}, {
"unlock_key": "etherstrike|2|0",
"complete": 1
}, {
"unlock_key": "singularity|2|0",
"complete": 1
}, {
"unlock_key": "etherstrike|1|0",
"complete": 1
}, {
"unlock_key": "flyburg|1|0",
"complete": 1
@@ -1125,6 +1134,9 @@ class Constant:
}, {
"unlock_key": "vector|2|0",
"complete": 1
}, {
"unlock_key": "stasis|1|0",
"complete": 1
}, {
"unlock_key": "loschen|2|0",
"complete": 1

View File

@@ -0,0 +1,66 @@
from flask import jsonify
from core.error import ArcError
default_error = ArcError('Unknown Error')
def error_return(e: ArcError = default_error): # 错误返回
# -7 处理交易时发生了错误
# -5 所有的曲目都已经下载完毕
# -4 您的账号已在别处登录
# -3 无法连接至服务器
# 2 Arcaea服务器正在维护
# 9 新版本请等待几分钟
# 100 无法在此ip地址下登录游戏
# 101 用户名占用
# 102 电子邮箱已注册
# 103 已有一个账号由此设备创建
# 104 用户名密码错误
# 105 24小时内登入两台设备
# 106 121 账户冻结
# 107 你没有足够的体力
# 113 活动已结束
# 114 该活动已结束,您的成绩不会提交
# 115 请输入有效的电子邮箱地址
# 120 封号警告
# 122 账户暂时冻结
# 123 账户被限制
# 124 你今天不能再使用这个IP地址创建新的账号
# 150 非常抱歉您已被限制使用此功能
# 151 目前无法使用此功能
# 401 用户不存在
# 403 无法连接至服务器
# 501 502 -6 此物品目前无法获取
# 504 无效的序列码
# 505 此序列码已被使用
# 506 你已拥有了此物品
# 601 好友列表已满
# 602 此用户已是好友
# 604 你不能加自己为好友
# 903 下载量超过了限制请24小时后重试
# 905 请在再次使用此功能前等待24小时
# 1001 设备数量达到上限
# 1002 此设备已使用过此功能
# 1201 房间已满
# 1202 房间号码无效
# 1203 请将Arcaea更新至最新版本
# 1205 此房间目前无法加入
# 9801 下载歌曲时发生问题,请再试一次
# 9802 保存歌曲时发生问题,请检查设备空间容量
# 9803 下载已取消
# 9905 没有在云端发现任何数据
# 9907 更新数据时发生了问题
# 9908 服务器只支持最新的版本请更新Arcaea
# 其它 发生未知错误
r = {"success": False, "error_code": e.error_code}
if e.extra_data:
r['extra'] = e.extra_data
return jsonify(r)
def success_return(value):
return jsonify({
"success": True,
"value": value
})

View File

@@ -0,0 +1,73 @@
from flask import Blueprint, request
from core.error import ArcError, NoAccess
from core.sql import Connect
from core.user import UserRegister, UserLogin, User, UserOnline
from core.character import UserCharacter
from .func import error_return, success_return
from .auth import auth_required
from setting import Config
bp = Blueprint('user', __name__, url_prefix='/user')
@bp.route('', methods=['POST']) # 注册接口
def register():
if 'AppVersion' in request.headers: # 版本检查
if Config.ALLOW_APPVERSION:
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return error_return(NoAccess('Wrong app version.', 1203))
with Connect() as c:
try:
new_user = UserRegister(c)
new_user.set_name(request.form['name'])
new_user.set_password(request.form['password'])
new_user.set_email(request.form['email'])
if 'device_id' in request.form:
device_id = request.form['device_id']
else:
device_id = 'low_version'
new_user.register()
# 注册后自动登录
user = UserLogin(c)
user.login(new_user.name, new_user.password,
device_id, request.remote_addr)
return success_return({'user_id': user.user_id, 'access_token': user.token})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/character', methods=['POST']) # 角色切换
@auth_required(request)
def character_change(user_id):
with Connect() as c:
try:
user = UserOnline(c, user_id)
user.change_character(
int(request.form['character']), request.form['skill_sealed'] == 'true')
return success_return({'user_id': user.user_id, 'character': user.character.character_id})
except ArcError as e:
return error_return(e)
return error_return()
# 角色觉醒切换
@bp.route('/me/character/<int:character_id>/toggle_uncap', methods=['POST'])
@auth_required(request)
def toggle_uncap(user_id, character_id):
with Connect() as c:
try:
user = User()
user.user_id = user_id
character = UserCharacter(c)
character.character_id = character_id
character.change_uncap_override(user)
character.select_character_info(user)
return success_return({'user_id': user.user_id, 'character': [character.to_dict]})
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -30,7 +30,7 @@ class Config():
Allowed game versions
If it is blank, all are allowed.
'''
ALLOW_APPVERSION = ['3.5.3', '3.5.3c', '3.12.6', '3.12.6c']
ALLOW_APPVERSION = ['3.12.6', '3.12.6c', '3.12.8', '3.12.8c']
'''
--------------------
'''
@@ -75,7 +75,7 @@ class Config():
愚人节模式开关
Switch of April Fool's Day
'''
IS_APRILFOOLS = True
IS_APRILFOOLS = False
'''
--------------------
'''