From 96fbd263426187e9e3a7b34170f48e302d459e57 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Thu, 28 Apr 2022 18:29:26 +0800 Subject: [PATCH] Update to v2.8.5 --- README.md | 29 ++- latest version/api/users.py | 6 +- latest version/core/character.py | 213 +++++++++++++++ latest version/core/constant.py | 16 ++ latest version/core/error.py | 33 ++- latest version/core/item.py | 7 + latest version/core/user.py | 180 ++++++++++++- latest version/database/arcsong.db | Bin 122880 -> 122880 bytes .../database/database_initialize.py | 4 +- latest version/database/packs.json | 18 ++ latest version/database/singles.json | 18 ++ latest version/main.py | 94 +------ latest version/run.bat | 1 + latest version/server/__init__.py | 8 + latest version/server/auth.py | 244 +++--------------- latest version/server/config.py | 24 +- latest version/server/func.py | 66 +++++ latest version/server/user.py | 73 ++++++ latest version/setting.py | 4 +- 19 files changed, 708 insertions(+), 330 deletions(-) create mode 100644 latest version/core/character.py create mode 100644 latest version/core/constant.py create mode 100644 latest version/core/item.py create mode 100644 latest version/server/func.py create mode 100644 latest version/server/user.py diff --git a/README.md b/README.md index 765e411..7f0bfef 100644 --- a/README.md +++ b/README.md @@ -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 -- ♠适用于Arcaea 4.0.∞版本♠ For Arcaea 3.12.6 -- ♥修复616==sb?PTT:PPT缺少Bug的问题♥ 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: - - ♣更新了█████[](歌曲数据库)♣ Update the song database. - - ♦尝试对!@#$%^提供支持♦ Try to add support for Anniversary 5 ticket. - - ¿新搭档' or name = 'Taikari'--已解锁¿ Unlock the character **Linka**. - ## 运行环境与依赖 Running environment and requirements diff --git a/latest version/api/users.py b/latest version/api/users.py index f7caa94..7b2e0e9 100644 --- a/latest version/api/users.py +++ b/latest version/api/users.py @@ -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']) diff --git a/latest version/core/character.py b/latest version/core/character.py new file mode 100644 index 0000000..4838ca9 --- /dev/null +++ b/latest version/core/character.py @@ -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 diff --git a/latest version/core/constant.py b/latest version/core/constant.py new file mode 100644 index 0000000..bd59eec --- /dev/null +++ b/latest version/core/constant.py @@ -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 diff --git a/latest version/core/error.py b/latest version/core/error.py index a20cfca..b9e20f0 100644 --- a/latest version/core/error.py +++ b/latest version/core/error.py @@ -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 diff --git a/latest version/core/item.py b/latest version/core/item.py new file mode 100644 index 0000000..7170eb5 --- /dev/null +++ b/latest version/core/item.py @@ -0,0 +1,7 @@ +class Item: + item_id = None + item_type = None + is_available = None + + def __init__(self) -> None: + pass diff --git a/latest version/core/user.py b/latest version/core/user.py index cf63631..501633b 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -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}) diff --git a/latest version/database/arcsong.db b/latest version/database/arcsong.db index f63a9edce52f365f68531a620d882a1763fe7b63..73556f45aaeee37eebd8639293e46b96a92eeded 100644 GIT binary patch delta 1726 zcmb`He{2**6vtL2BNrv#2?YXw%w!4-R)lP zwvsy&uXiDkCTJv8szid25Ya^CEVWkrqY)E`##o}n9|5VJshXhH#3%&gxqGF*XbFk& zWp`%t-t5e0zVqhwjamD~tk1jPnf-;{oHL+mGB?$Iv>8nS!-{QFLwPM1Y_~ZJ*a{|M zc!~(?f;+X|uqsDelj6G~gxf2a6v&4$U~}iaO0UqB`~mU@DaStUH2O04V$Ov1*_>wf zvLyv$tTUC^1mj6hk}vVBrz;reR)(|K9d>9K!!WR>riQk+w^N5wxS<#Hi1YKNxUn+>|6!t*{F!|SjoI1tXh?3lzNcHb22-POMI0wdiPLFr%4weS z+?8+|-Wl+SE=82cmTc>8s0Al)fPFyHVwwsL8lcRA{I2ToXIPPaE$8>loW9)-4eQIachIH(I&+uH>FunCS)!;zp0G}4I<+Agy4c#q6fI{y>Hq)$ delta 170 zcmV;b09F5hzz2Z92ap>9l#v`m1(X0T5c{!YqCW`d000009uMcU4nW)w79A7=D*|(M zZEa<8W@&O|cW-iQa{%;$P_|G30bn5pw*Ugsbv4Oq?x8St_TLPEwwgFm~OSb_-x7fG=EC&kd0rCJ3?+@Y+(6bIe*$/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('//uncap', True), methods=['POST']) @server.auth.auth_required(request) diff --git a/latest version/run.bat b/latest version/run.bat index e4d9074..53d757d 100644 --- a/latest version/run.bat +++ b/latest version/run.bat @@ -1,2 +1,3 @@ cd /d %~dp0 +:: Develop server python -B main.py \ No newline at end of file diff --git a/latest version/server/__init__.py b/latest version/server/__init__.py index e69de29..1bedd18 100644 --- a/latest version/server/__init__.py +++ b/latest version/server/__init__.py @@ -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) \ No newline at end of file diff --git a/latest version/server/auth.py b/latest version/server/auth.py index c4c636f..b5e9868 100644 --- a/latest version/server/auth.py +++ b/latest version/server/auth.py @@ -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 diff --git a/latest version/server/config.py b/latest version/server/config.py index e22a954..068168f 100644 --- a/latest version/server/config.py +++ b/latest version/server/config.py @@ -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 diff --git a/latest version/server/func.py b/latest version/server/func.py new file mode 100644 index 0000000..12571f0 --- /dev/null +++ b/latest version/server/func.py @@ -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 + }) diff --git a/latest version/server/user.py b/latest version/server/user.py new file mode 100644 index 0000000..8d685d5 --- /dev/null +++ b/latest version/server/user.py @@ -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//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() diff --git a/latest version/setting.py b/latest version/setting.py index 8757af0..ac1563e 100644 --- a/latest version/setting.py +++ b/latest version/setting.py @@ -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 ''' -------------------- '''