mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-12 19:17:26 +08:00
[Enhance] Self account delete
- Add support for users destroy their own accounts
This commit is contained in:
@@ -26,7 +26,7 @@ This procedure is mainly used for study and research, and shall not be used for
|
|||||||
- 多设备自动封号 Auto-ban of multiple devices
|
- 多设备自动封号 Auto-ban of multiple devices
|
||||||
- :warning: 多设备登录 Multi device login
|
- :warning: 多设备登录 Multi device login
|
||||||
- 登录频次限制 Login rate limit
|
- 登录频次限制 Login rate limit
|
||||||
- :x: 销号 Destroy account
|
- :warning: 销号 Destroy account
|
||||||
- 成绩上传 Score upload
|
- 成绩上传 Score upload
|
||||||
- 成绩校验 Score check
|
- 成绩校验 Score check
|
||||||
- 成绩排名 Score rank
|
- 成绩排名 Score rank
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class Config:
|
|||||||
|
|
||||||
SAVE_FULL_UNLOCK = False
|
SAVE_FULL_UNLOCK = False
|
||||||
|
|
||||||
|
ALLOW_SELF_ACCOUNT_DELETE = False
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
# You can change this to make another PTT mechanism.
|
# You can change this to make another PTT mechanism.
|
||||||
@@ -86,6 +88,7 @@ class Config:
|
|||||||
SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/'
|
SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/'
|
||||||
DATABASE_INIT_PATH = './database/init/'
|
DATABASE_INIT_PATH = './database/init/'
|
||||||
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
|
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
|
||||||
|
SQLITE_DATABASE_DELETED_PATH = './database/arcaea_database_deleted.db'
|
||||||
|
|
||||||
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
|
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
|
||||||
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
|
|
||||||
ARCAEA_SERVER_VERSION = 'v2.11.3.3'
|
ARCAEA_SERVER_VERSION = 'v2.11.3.4'
|
||||||
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ class Constant:
|
|||||||
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
|
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
|
||||||
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
||||||
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
||||||
|
SQLITE_DATABASE_DELETED_PATH = Config.SQLITE_DATABASE_DELETED_PATH
|
||||||
|
|
||||||
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
||||||
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
||||||
|
|||||||
@@ -177,6 +177,16 @@ class LogDatabaseInit:
|
|||||||
self.table_init()
|
self.table_init()
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedDatabaseInit(DatabaseInit):
|
||||||
|
def __init__(self, db_path: str = Config.SQLITE_DATABASE_DELETED_PATH) -> None:
|
||||||
|
super().__init__(db_path)
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
with Connect(self.db_path) as c:
|
||||||
|
self.c = c
|
||||||
|
self.table_init()
|
||||||
|
|
||||||
|
|
||||||
class FileChecker:
|
class FileChecker:
|
||||||
'''文件检查及初始化类'''
|
'''文件检查及初始化类'''
|
||||||
|
|
||||||
@@ -195,7 +205,7 @@ class FileChecker:
|
|||||||
self.logger.warning('Folder `%s` is missing.' % folder_path)
|
self.logger.warning('Folder `%s` is missing.' % folder_path)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def check_update_database(self) -> bool:
|
def _check_update_database_log(self) -> bool:
|
||||||
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
|
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
|
||||||
# 新建日志数据库
|
# 新建日志数据库
|
||||||
try:
|
try:
|
||||||
@@ -232,22 +242,22 @@ class FileChecker:
|
|||||||
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
return True
|
||||||
|
|
||||||
|
def _check_update_database_main(self, db_path=Config.SQLITE_DATABASE_PATH, init_class=DatabaseInit) -> bool:
|
||||||
|
if not self.check_file(db_path):
|
||||||
# 新建数据库
|
# 新建数据库
|
||||||
try:
|
try:
|
||||||
self.logger.info(
|
self.logger.info(f'Try to new the file `{db_path}`.')
|
||||||
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
init_class().init()
|
||||||
DatabaseInit().init()
|
self.logger.info(f'Success to new the file `{db_path}`.')
|
||||||
self.logger.info(
|
|
||||||
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(format_exc())
|
self.logger.error(format_exc())
|
||||||
self.logger.warning(
|
self.logger.warning(f'Failed to new the file `{db_path}`.')
|
||||||
'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# 检查更新
|
# 检查更新
|
||||||
with Connect() as c:
|
with Connect(db_path) as c:
|
||||||
try:
|
try:
|
||||||
c.execute('''select value from config where id="version"''')
|
c.execute('''select value from config where id="version"''')
|
||||||
x = c.fetchone()
|
x = c.fetchone()
|
||||||
@@ -256,42 +266,45 @@ class FileChecker:
|
|||||||
# 数据库自动更新,不强求
|
# 数据库自动更新,不强求
|
||||||
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
|
f'Maybe the file `{db_path}` is an old version. Version: {x[0] if x else "None"}')
|
||||||
try:
|
try:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
f'Try to update the file `{db_path}` to version {ARCAEA_SERVER_VERSION}.')
|
||||||
|
|
||||||
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
|
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
|
||||||
os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH)
|
os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH)
|
||||||
|
|
||||||
backup_path = try_rename(Config.SQLITE_DATABASE_PATH, os.path.join(
|
backup_path = try_rename(db_path, os.path.join(
|
||||||
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(Config.SQLITE_DATABASE_PATH)[-1] + '.bak'))
|
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(db_path)[-1] + '.bak'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
copy2(backup_path, Config.SQLITE_DATABASE_PATH)
|
copy2(backup_path, db_path)
|
||||||
except:
|
except:
|
||||||
copy(backup_path, Config.SQLITE_DATABASE_PATH)
|
copy(backup_path, db_path)
|
||||||
|
|
||||||
temp_path = os.path.join(
|
temp_path = os.path.join(
|
||||||
*os.path.split(Config.SQLITE_DATABASE_PATH)[:-1], 'old_arcaea_database.db')
|
*os.path.split(db_path)[:-1], 'old_arcaea_database.db')
|
||||||
if os.path.isfile(temp_path):
|
if os.path.isfile(temp_path):
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
|
|
||||||
try_rename(Config.SQLITE_DATABASE_PATH, temp_path)
|
try_rename(db_path, temp_path)
|
||||||
|
|
||||||
DatabaseInit().init()
|
init_class().init()
|
||||||
self.update_database(temp_path)
|
self.update_database(temp_path, db_path)
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
f'Success to update the file `{db_path}`.')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(format_exc())
|
self.logger.error(format_exc())
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
f'Fail to update the file `{db_path}`.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def check_update_database(self) -> bool:
|
||||||
|
return self._check_update_database_main() and self._check_update_database_log() and self._check_update_database_main(Config.SQLITE_DATABASE_DELETED_PATH, DeletedDatabaseInit)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None:
|
def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None:
|
||||||
'''更新数据库,并删除旧文件'''
|
'''更新数据库,并删除旧文件'''
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .constant import Constant
|
||||||
from .download import DownloadList
|
from .download import DownloadList
|
||||||
from .error import NoData
|
from .error import NoData
|
||||||
from .save import SaveData
|
from .save import SaveData
|
||||||
@@ -245,3 +246,75 @@ class UnlockUserItem(BaseOperation):
|
|||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_one_table(c, table_name, user_id):
|
||||||
|
c.execute(
|
||||||
|
f'''insert into db_deleted.{table_name} select * from {table_name} where user_id = ?''', (user_id,))
|
||||||
|
c.execute(f'''delete from {table_name} where user_id = ?''', (user_id,))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteUserScore(BaseOperation):
|
||||||
|
'''
|
||||||
|
删除单用户成绩,不包含 recent 数据
|
||||||
|
'''
|
||||||
|
_name = 'delete_user_score'
|
||||||
|
|
||||||
|
def __init__(self, user=None):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def set_params(self, user_id: int = None, *args, **kwargs):
|
||||||
|
if user_id is not None:
|
||||||
|
self.user = User()
|
||||||
|
self.user.user_id = int(user_id)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
assert self.user is not None
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute('''attach database ? as db_deleted''',
|
||||||
|
(Constant.SQLITE_DATABASE_DELETED_PATH,))
|
||||||
|
_delete_one_table(c, 'best_score', self.user.user_id)
|
||||||
|
_delete_one_table(c, 'recent30', self.user.user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteOneUser(BaseOperation):
|
||||||
|
'''
|
||||||
|
删除单用户
|
||||||
|
'''
|
||||||
|
_name = 'delete_one_user'
|
||||||
|
|
||||||
|
TABLES = ['best_score', 'recent30', 'user_char', 'user_course', 'user_item',
|
||||||
|
'user_present', 'user_redeem', 'user_role', 'user_save', 'user_world', 'user']
|
||||||
|
|
||||||
|
def __init__(self, user=None):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def set_params(self, user_id: int = None, *args, **kwargs):
|
||||||
|
if user_id is not None:
|
||||||
|
self.user = User()
|
||||||
|
self.user.user_id = int(user_id)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
assert self.user is not None
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute('''attach database ? as db_deleted''',
|
||||||
|
(Constant.SQLITE_DATABASE_DELETED_PATH,))
|
||||||
|
|
||||||
|
self._clear_login(c)
|
||||||
|
self._data_save(c)
|
||||||
|
|
||||||
|
def _data_save(self, c):
|
||||||
|
c.execute(
|
||||||
|
f'''insert into db_deleted.friend select * from friend where user_id_me = ? or user_id_other = ?''', (self.user.user_id, self.user.user_id))
|
||||||
|
c.execute(f'''delete from friend where user_id_me = ? or user_id_other = ?''',
|
||||||
|
(self.user.user_id, self.user.user_id))
|
||||||
|
|
||||||
|
[_delete_one_table(c, x, self.user.user_id) for x in self.TABLES]
|
||||||
|
|
||||||
|
def _clear_login(self, c):
|
||||||
|
c.execute('''delete from login where user_id = ?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
c.execute('''delete from api_login where user_id = ?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from flask import Blueprint, current_app, request
|
from flask import Blueprint, current_app, request
|
||||||
|
|
||||||
from core.character import UserCharacter
|
from core.character import UserCharacter
|
||||||
|
from core.config_manager import Config
|
||||||
from core.error import ArcError
|
from core.error import ArcError
|
||||||
from core.item import ItemCore
|
from core.item import ItemCore
|
||||||
|
from core.operation import DeleteOneUser
|
||||||
from core.save import SaveData
|
from core.save import SaveData
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import User, UserLogin, UserOnline, UserRegister
|
from core.user import User, UserLogin, UserOnline, UserRegister
|
||||||
@@ -164,7 +166,10 @@ def sys_set(user_id, set_arg):
|
|||||||
@auth_required(request)
|
@auth_required(request)
|
||||||
@arc_try
|
@arc_try
|
||||||
def user_delete(user_id):
|
def user_delete(user_id):
|
||||||
raise ArcError('Cannot delete the account.', 151, status=404)
|
if not Config.ALLOW_SELF_ACCOUNT_DELETE:
|
||||||
|
raise ArcError('Cannot delete the account.', 151, status=404)
|
||||||
|
DeleteOneUser().set_params(user_id).run()
|
||||||
|
return success_return({'user_id': user_id})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from core.init import FileChecker
|
from core.init import FileChecker
|
||||||
from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem
|
from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem, DeleteUserScore
|
||||||
from core.rank import RankList
|
from core.rank import RankList
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import User
|
from core.user import User
|
||||||
@@ -1366,7 +1366,10 @@ def delete_user_score():
|
|||||||
user_id = c.fetchone()
|
user_id = c.fetchone()
|
||||||
if user_id:
|
if user_id:
|
||||||
user_id = user_id[0]
|
user_id = user_id[0]
|
||||||
web.system.clear_user_score(c, user_id)
|
c.execute('''update user set rating_ptt=0, song_id='', difficulty=0, score=0, shiny_perfect_count=0, perfect_count=0, near_count=0, miss_count=0, health=0, time_played=0, rating=0, world_rank_score=0 where user_id=:a''', {
|
||||||
|
'a': user_id})
|
||||||
|
c.connection.commit()
|
||||||
|
DeleteUserScore().set_params(user_id=user_id).run()
|
||||||
flash("用户成绩删除成功 Successfully delete the user's scores.")
|
flash("用户成绩删除成功 Successfully delete the user's scores.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -261,12 +261,3 @@ def ban_one_user(c, user_id):
|
|||||||
{'a': user_id})
|
{'a': user_id})
|
||||||
c.execute('''delete from login where user_id=:a''', {'a': user_id})
|
c.execute('''delete from login where user_id=:a''', {'a': user_id})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def clear_user_score(c, user_id):
|
|
||||||
# 清除用户所有成绩,包括best_score和recent30,以及recent数据,但不包括云端存档
|
|
||||||
c.execute('''update user set rating_ptt=0, song_id='', difficulty=0, score=0, shiny_perfect_count=0, perfect_count=0, near_count=0, miss_count=0, health=0, time_played=0, rating=0, world_rank_score=0 where user_id=:a''',
|
|
||||||
{'a': user_id})
|
|
||||||
c.execute('''delete from best_score where user_id=:a''', {'a': user_id})
|
|
||||||
c.execute('''delete from recent30 where user_id=:a''', {'a': user_id})
|
|
||||||
return
|
|
||||||
|
|||||||
Reference in New Issue
Block a user