diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9f13570..ce2785d 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -14,7 +14,7 @@ services: web: image: zzbslayer/kokkoro_bot:latest ports: - - "5100:5001" + - "5560:5001" volumes: - ~/.kokkoro:/root/.kokkoro command: python3.8 -u /bot/run_web.py diff --git a/docker-compose.yml b/docker-compose.yml index 7bf8f35..a8ef863 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: web: image: zzbslayer/kokkoro_bot:latest ports: - - "5100:5001" + - "5560:5001" volumes: - ./:/bot/ - ~/.kokkoro:/root/.kokkoro diff --git a/kokkoro/bot/discord/bot.py b/kokkoro/bot/discord/bot.py index b70d4fd..95d6095 100644 --- a/kokkoro/bot/discord/bot.py +++ b/kokkoro/bot/discord/bot.py @@ -55,7 +55,10 @@ async def kkr_send(self, ev: DiscordEvent, msg: SupportedMessageType, at_sender= channel = ev.get_channel() await self._send_by_channel(channel, msg, filename) - + + @overrides(KokkoroBot) + async def kkr_send_dm(self, ev: DiscordEvent, msg: SupportedMessageType) + await ev.get_author().get_raw_user().send(msg) @overrides(KokkoroBot) async def kkr_send_by_group(self, gid, msg: SupportedMessageType, tag=BroadcastTag.default, filename="image.png"): diff --git a/kokkoro/bot/discord/discord_util.py b/kokkoro/bot/discord/discord_util.py index b30129e..3643f0a 100644 --- a/kokkoro/bot/discord/discord_util.py +++ b/kokkoro/bot/discord/discord_util.py @@ -16,12 +16,12 @@ def remove_mention_me(raw_message) -> (str, bool): Pattern in official document is <@![a-z0-9A-Z]+> e.g. <@!12345> -But the `at` sent by ios discord is <@[a-z0-9A-Z]> +But the `at` sent by iOS discord is <@[a-z0-9A-Z]> e.g. <@12345> So temp solution is <@!?[a-z0-9A-Z]+> ''' -dc_at_pattern = r'<@!?[a-z0-9A-Z]+>' +dc_at_pattern = r'<@!?[a-z0-9A-Z]+>' def normalize_message(raw_message: str) -> str: """ 规范化 at 信息,"<@!123> waht<@!312>a12" => "@123 waht @312 a12" @@ -33,7 +33,7 @@ def normalize_message(raw_message: str) -> str: def normalize_at(raw_at): """ - 规范化 at 信息,"<@!123>" => " @123 " + 规范化 at 信息,"<@!123>" => " @123 " ios: "<@123>" => " @123 " """ - return f' @{raw_at[3:-1]} ' if '!' in raw_at else f' @{raw_at[2:-1]} ' \ No newline at end of file + return f' @{raw_at[3:-1]} ' if '!' in raw_at else f' @{raw_at[2:-1]} ' diff --git a/kokkoro/common_interface.py b/kokkoro/common_interface.py index 812af7e..e53bbab 100644 --- a/kokkoro/common_interface.py +++ b/kokkoro/common_interface.py @@ -98,6 +98,9 @@ def kkr_event_adaptor(self, raw_event) -> EventInterface: async def kkr_send(self, ev: EventInterface, msg: SupportedMessageType, at_sender=False, filename="image.png"): raise NotImplementedError + async def kkr_send_dm(self, ev: EventInterface, msg: SupportedMessageType): + raise NotImplementedError + async def kkr_send_by_group(self, gid, msg: SupportedMessageType, tag): raise NotImplementedError @@ -121,4 +124,4 @@ def get_groups(self) -> List[GroupInterface]: raise NotImplementedError def get_members_in_group(self, gid) -> List[UserInterface]: - raise NotImplementedError \ No newline at end of file + raise NotImplementedError diff --git a/kokkoro/config_example/__init__.py b/kokkoro/config_example/__init__.py index 417d829..a401482 100644 --- a/kokkoro/config_example/__init__.py +++ b/kokkoro/config_example/__init__.py @@ -4,6 +4,7 @@ import matplotlib.font_manager as font_manager from .__bot__ import * +from .__web__ import * for module in MODULES_ON: try: diff --git a/kokkoro/config_example/__web__.py b/kokkoro/config_example/__web__.py new file mode 100644 index 0000000..508c88b --- /dev/null +++ b/kokkoro/config_example/__web__.py @@ -0,0 +1,2 @@ +PUBLIC_ADDRESS="http://DOMAIN_OR_IP_ADDRESS:5560" +PUBLIC_BASEPATH="/" diff --git a/kokkoro/modules/pcrclanbattle/clanbattle/__init__.py b/kokkoro/modules/pcrclanbattle/clanbattle/__init__.py index 244c0c6..1d0d7ae 100644 --- a/kokkoro/modules/pcrclanbattle/clanbattle/__init__.py +++ b/kokkoro/modules/pcrclanbattle/clanbattle/__init__.py @@ -5,6 +5,7 @@ from .argparse import ArgParser from .exception import * +from kokkoro import config from kokkoro.typing import * from kokkoro.common_interface import KokkoroBot, EventInterface from kokkoro.service import Service @@ -22,7 +23,7 @@ def cb_cmd(prefixes, parser:ArgParser) -> Callable: prefixes = cb_prefix(prefixes) if not isinstance(prefixes, Iterable): raise ValueError('`name` of cb_cmd must be `str` or `Iterable[str]`') - + def deco(func): @wraps(func) async def wrapper(bot: KokkoroBot, ev: EventInterface): @@ -43,7 +44,9 @@ async def wrapper(bot: KokkoroBot, ev: EventInterface): return deco -from .cmdv2 import * +from .cmd_battle import * +if config.ENABLE_WEB: + from .cmd_web import * QUICK_START = f''' @@ -105,4 +108,4 @@ async def cb_help(bot: KokkoroBot, ev:EventInterface): # content='命令一览表') # await session.send(msg) -from . import report \ No newline at end of file +from . import report diff --git a/kokkoro/modules/pcrclanbattle/clanbattle/battlemaster.py b/kokkoro/modules/pcrclanbattle/clanbattle/battlemaster.py index 4e736d9..fe21fc9 100644 --- a/kokkoro/modules/pcrclanbattle/clanbattle/battlemaster.py +++ b/kokkoro/modules/pcrclanbattle/clanbattle/battlemaster.py @@ -10,7 +10,7 @@ def get_config(): class BattleMaster(object): ''' - Different bits represent different damage kind: + Different bits represent different damage kind: ''' NORM = BattleDao.NORM # 0 LAST = BattleDao.LAST # 1<<0 @@ -41,7 +41,7 @@ def damage_kind_to_string(src): SERVER_JP_NAME = ('jp', 'JP', 'Jp', '日', '日服', str(SERVER_JP)) SERVER_TW_NAME = ('tw', 'TW', 'Tw', '台', '台服', str(SERVER_TW)) SERVER_CN_NAME = ('cn', 'CN', 'Cn', '国', '国服', 'B', 'B服', str(SERVER_CN)) - + def __init__(self, group): super().__init__() self.group = group @@ -54,7 +54,7 @@ def __init__(self, group): def get_timezone_num(server): return 9 if BattleMaster.SERVER_JP == server else 8 - + @staticmethod def get_yyyymmdd(time, zone_num:int=8): ''' @@ -133,99 +133,109 @@ def get_server_code(server_name): return -1 - def get_battledao(self, cid, time): - clan = self.get_clan(cid) + def get_battledao(self, time): + clan = self.get_clan() zone_num = self.get_timezone_num(clan['server']) yyyy, mm, _ = self.get_yyyymmdd(time, zone_num) - return BattleDao(self.group, cid, yyyy, mm) - - - def add_clan(self, cid, name, server): - return self.clandao.add({'gid': self.group, 'cid': cid, 'name': name, 'server': server}) - def del_clan(self, cid): - return self.clandao.delete(self.group, cid) - def mod_clan(self, cid, name, server): - return self.clandao.modify({'gid': self.group, 'cid': cid, 'name': name, 'server': server}) - def has_clan(self, cid): - return True if self.clandao.find_one(self.group, cid) else False - def get_clan(self, cid): - return self.clandao.find_one(self.group, cid) + return BattleDao(self.group, yyyy, mm) + + + def add_clan(self, name, server): + return self.clandao.add({'gid': self.group, 'name': name, 'server': server}) + def get_clan(self): + return self.clandao.find_one(self.group) + def has_clan(self): + return True if self.clandao.find_one(self.group) else False def list_clan(self): - return self.clandao.find_by_gid(self.group) - - - def add_member(self, uid, alt, name, cid): - return self.memberdao.add({'uid': uid, 'alt': alt, 'name': name, 'gid': self.group, 'cid': cid}) - def del_member(self, uid, alt): - return self.memberdao.delete(uid, alt) - def clear_member(self, cid=None): - return self.memberdao.delete_by(gid=self.group, cid=cid) - def mod_member(self, uid, alt, new_name, new_cid): - return self.memberdao.modify({'uid': uid, 'alt': alt, 'name': new_name, 'gid': self.group, 'cid': new_cid}) - def has_member(self, uid, alt): - mem = self.memberdao.find_one(uid, alt) - return True if mem and mem['gid'] == self.group else False - def get_member(self, uid, alt): - mem = self.memberdao.find_one(uid, alt) - return mem if mem and mem['gid'] == self.group else None - def list_member(self, cid=None): - return self.memberdao.find_by(gid=self.group, cid=cid) + return [self.clandao.find_one(self.group)] + def list_clan_by_uid(self, uid): + return self.clandao.find_by_uid(uid) + def mod_clan(self, name, server): + return self.clandao.modify({'gid': self.group, 'name': name, 'server': server}) + def del_clan(self): + return self.clandao.delete(self.group) + + + def add_member(self, uid, name): + return self.memberdao.add({'uid': uid, 'gid': self.group, 'name': name}) + def get_member(self, uid): + mem = self.memberdao.find_one(uid, self.group) + return mem if mem else None + def has_member(self, uid): + mem = self.memberdao.find_one(uid, self.group) + return True if mem else False + def list_member(self): + return self.memberdao.find_by(gid=self.group) def list_account(self, uid): return self.memberdao.find_by(gid=self.group, uid=uid) - - - def add_challenge(self, uid, alt, round_, boss, dmg, flag, time): - mem = self.get_member(uid, alt) + def mod_member(self, uid, new_name, new_sl, new_auth): + return self.memberdao.modify( + { + 'uid': uid, + 'gid': self.group, + 'name': new_name, + 'last_sl': new_sl, + 'authority_group': new_auth + } + ) + def del_member(self, uid): + return self.memberdao.delete(uid, self.group) + def clear_member(self): + return self.memberdao.delete_by(gid=self.group) + + + def add_challenge(self, uid, round_, boss, dmg, remain, flag, time): + mem = self.get_member(uid) if not mem or mem['gid'] != self.group: raise NotFoundError('未找到成员') challenge = { - 'uid': uid, - 'alt': alt, - 'time': time, - 'round': round_, - 'boss': boss, - 'dmg': dmg, - 'flag': flag + 'uid': uid, + 'time': time, + 'round': round_, + 'boss': boss, + 'dmg': dmg, + 'remain': remain, + 'flag': flag } - dao = self.get_battledao(mem['cid'], time) + dao = self.get_battledao(time) return dao.add(challenge) - def mod_challenge(self, eid, uid, alt, round_, boss, dmg, flag, time): - mem = self.get_member(uid, alt) + def get_challenge(self, eid, time): + dao = self.get_battledao(time) + return dao.find_one(eid) + + def list_challenge(self, time): + dao = self.get_battledao(time) + return dao.find_all() + + def list_challenge_of_user(self, uid, time): + mem = self.memberdao.find_one(uid, self.group) + if not mem or mem['gid'] != self.group: + return [] + dao = self.get_battledao(time) + return dao.find_by(uid=uid) + + def mod_challenge(self, eid, uid, round_, boss, dmg, remain, flag, time): + mem = self.get_member(uid) if not mem or mem['gid'] != self.group: raise NotFoundError('未找到成员') challenge = { - 'eid': eid, - 'uid': uid, - 'alt': alt, - 'time': time, - 'round': round_, - 'boss': boss, - 'dmg': dmg, - 'flag': flag + 'eid': eid, + 'uid': uid, + 'time': time, + 'round': round_, + 'boss': boss, + 'dmg': dmg, + 'remain': remain, + 'flag': flag } - dao = self.get_battledao(mem['cid'], time) + dao = self.get_battledao(time) return dao.modify(challenge) - def del_challenge(self, eid, cid, time): - dao = self.get_battledao(cid, time) + def del_challenge(self, eid, time): + dao = self.get_battledao(time) return dao.delete(eid) - def get_challenge(self, eid, cid, time): - dao = self.get_battledao(cid, time) - return dao.find_one(eid) - - def list_challenge(self, cid, time): - dao = self.get_battledao(cid, time) - return dao.find_all() - - def list_challenge_of_user(self, uid, alt, time): - mem = self.memberdao.find_one(uid, alt) - if not mem or mem['gid'] != self.group: - return [] - dao = self.get_battledao(mem['cid'], time) - return dao.find_by(uid=uid, alt=alt) - @staticmethod def filt_challenge_of_day(challenge_list, time, zone_num:int=8): @@ -233,40 +243,40 @@ def filt_challenge_of_day(challenge_list, time, zone_num:int=8): return list(filter(lambda challen: day == BattleMaster.get_yyyymmdd(challen['time'], zone_num)[2], challenge_list)) - def list_challenge_of_day(self, cid, time, zone_num:int=8): - return self.filt_challenge_of_day(self.list_challenge(cid, time), time, zone_num) + def list_challenge_of_day(self, time, zone_num:int=8): + return self.filt_challenge_of_day(self.list_challenge(time), time, zone_num) - def list_challenge_of_user_of_day(self, uid, alt, time, zone_num:int=8): - return self.filt_challenge_of_day(self.list_challenge_of_user(uid, alt, time), time, zone_num) + def list_challenge_of_user_of_day(self, uid, time, zone_num:int=8): + return self.filt_challenge_of_day(self.list_challenge_of_user(uid, time), time, zone_num) - def stat_challenge(self, cid, time, only_one_day=True, zone_num:int=8): + def stat_challenge(self, time, only_one_day=True, zone_num:int=8): ''' 统计每个成员的出刀 return [(member, [challenge])] ''' ret = [] - mem = self.list_member(cid) - dao = self.get_battledao(cid, time) + mem = self.list_member() + dao = self.get_battledao(time) for m in mem: - challens = dao.find_by(uid=m['uid'], alt=m['alt']) + challens = dao.find_by(uid=m['uid']) if only_one_day: challens = self.filt_challenge_of_day(challens, time, zone_num) ret.append((m, challens)) return ret - - def stat_damage(self, cid, time): + + def stat_damage(self, time): ''' - 统计cid会各成员的本月各Boss伤害总量 - :return: [(uid, alt, name, [total_dmg, dmg1, ..., dmg5])] + 统计各成员的本月各Boss伤害总量 + :return: [(uid, name, [total_dmg, dmg1, ..., dmg5])] ''' - clan = self.get_clan(cid) + clan = self.get_clan() if not clan: - raise NotFoundError(f'未找到公会{cid}') + raise NotFoundError(f'未找到公会') server = clan['server'] - stat = self.stat_challenge(cid, time, only_one_day=False, zone_num=self.get_timezone_num(server)) + stat = self.stat_challenge(time, only_one_day=False, zone_num=self.get_timezone_num(server)) ret = [] for mem, challens in stat: dmgs = [0] * 6 @@ -274,30 +284,30 @@ def stat_damage(self, cid, time): d = ch['dmg'] dmgs[0] += d dmgs[ch['boss']] += d - ret.append((mem['uid'], mem['alt'], mem['name'], dmgs)) + ret.append((mem['uid'], mem['name'], dmgs)) return ret - def stat_score(self, cid, time): + def stat_score(self, time): ''' - 统计cid会各成员的本月总分数 - :return: [(uid,alt,name,score)] + 统计各成员的本月总分数 + :return: [(uid,name,score)] ''' - clan = self.get_clan(cid) + clan = self.get_clan() if not clan: - raise NotFoundError(f'未找到公会{cid}') + raise NotFoundError(f'未找到公会') server = clan['server'] - stat = self.stat_challenge(cid, time, only_one_day=False, zone_num=self.get_timezone_num(server)) + stat = self.stat_challenge(time, only_one_day=False, zone_num=self.get_timezone_num(server)) ret = [ - (mem['uid'], mem['alt'], mem['name'], sum(map(lambda ch: round(self.get_score_rate(ch['round'], ch['boss'], server) * ch['dmg']), challens))) + (mem['uid'], mem['name'], sum(map(lambda ch: round(self.get_score_rate(ch['round'], ch['boss'], server) * ch['dmg']), challens))) for mem, challens in stat ] return ret - def list_challenge_remain(self, cid, time): + def list_challenge_remain(self, time): ''' - return [(uid,alt,name,remain_n,remain_e)] + return [(uid,name,remain_n,remain_e)] norm + timeout + last == 3 - remain_n // 正常出刀数 == 3 - 余刀数 last - ext == remain_e // 尾刀数 - 补时刀数 == 补时余刀 @@ -323,15 +333,16 @@ def count(challens): norm = norm + 1 return norm, last, ext, timeout - clan = self.get_clan(cid) + clan = self.get_clan() if not clan: - raise NotFoundError(f'未找到公会{cid}') + raise NotFoundError(f'未找到公会') ret = [] - stat = self.stat_challenge(cid, time, only_one_day=True, zone_num=self.get_timezone_num(clan['server'])) + stat = self.stat_challenge(time, only_one_day=True, zone_num=self.get_timezone_num(clan['server'])) for mem, challens in stat: norm, last, ext, timeout = count(challens) r = ( - mem['uid'], mem['alt'], mem['name'], + mem['uid'], + mem['name'], 3 - (norm + timeout + last), last - ext, ) @@ -339,15 +350,15 @@ def count(challens): return ret - def get_challenge_progress(self, cid, time): + def get_challenge_progress(self, time): ''' return (round, boss, remain_hp) ''' - clan = self.get_clan(cid) + clan = self.get_clan() if not clan: return None server = clan['server'] - dao = self.get_battledao(cid, time) + dao = self.get_battledao(time) challens = dao.find_all() if not len(challens): return ( 1, 1, self.get_boss_hp(1, 1, server) ) diff --git a/kokkoro/modules/pcrclanbattle/clanbattle/cmdv2.py b/kokkoro/modules/pcrclanbattle/clanbattle/cmd_battle.py similarity index 89% rename from kokkoro/modules/pcrclanbattle/clanbattle/cmdv2.py rename to kokkoro/modules/pcrclanbattle/clanbattle/cmd_battle.py index 26ade76..df32ee0 100644 --- a/kokkoro/modules/pcrclanbattle/clanbattle/cmdv2.py +++ b/kokkoro/modules/pcrclanbattle/clanbattle/cmd_battle.py @@ -45,13 +45,13 @@ def _check_clan(bm:BattleMaster): - clan = bm.get_clan(1) + clan = bm.get_clan() if not clan: raise NotFoundError(ERROR_CLAN_NOTFOUND) return clan -def _check_member(bm:BattleMaster, uid:str, alt:str, tip=None): - mem = bm.get_member(uid, alt) or bm.get_member(uid, 0) # 兼容cmdv1 +def _check_member(bm:BattleMaster, uid:str, tip=None): + mem = bm.get_member(uid) # 时代变了 if not mem: raise NotFoundError(tip or ERROR_MEMBER_NOTFOUND) return mem @@ -67,22 +67,22 @@ def _check_admin(ev:EventInterface, tip:str='') -> bool: async def add_clan(bot: KokkoroBot, ev: EventInterface, args:ParseResult): _check_admin(ev) bm = BattleMaster(ev.get_group_id()) - if bm.has_clan(1): - bm.mod_clan(1, args.N, args.S) + if bm.has_clan(): + bm.mod_clan(args.N, args.S) await bot.kkr_send(ev, f'公会信息已修改!\n{args.N} {server_name(args.S)}', at_sender=True) else: - bm.add_clan(1, args.N, args.S) + bm.add_clan(args.N, args.S) await bot.kkr_send(ev, f'公会建立成功!{args.N} {server_name(args.S)}', at_sender=True) - + @cb_cmd(('查看公会', 'list-clan'), ArgParser('!查看公会')) async def list_clan(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) clans = bm.list_clan() if len(clans): - clans = map(lambda x: f"{x['cid']}会:{x['name']} {server_name(x['server'])}", clans) - msg = ['本群公会:', *clans] + clans = map(lambda x: f"{x['name']}({server_name(x['server'])})", clans) + msg = ['本群指定唯一公会:', *clans] await bot.kkr_send(ev, '\n'.join(msg), at_sender=True) else: raise NotFoundError(ERROR_CLAN_NOTFOUND) @@ -95,10 +95,10 @@ async def add_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) clan = _check_clan(bm) - uid = args['@'] or args.uid - name = args[''] or args.name + uid = args['@'] or args.uid + name = args[''] or args.name author = ev.get_author() - + if uid == None: uid = ev.get_author_id() else: @@ -113,12 +113,12 @@ async def add_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): name = name or author.get_nick_name() or author.get_name() - mem = bm.get_member(uid, bm.group) or bm.get_member(uid, 0) # 兼容cmdv1 + mem = bm.get_member(uid) if mem: - bm.mod_member(uid, mem['alt'], name, 1) + bm.mod_member(uid, name, mem['last_sl'], mem['authority_group']) await bot.kkr_send(ev, f'成员{bot.kkr_at(uid)}昵称已修改为{name}') else: - bm.add_member(uid, bm.group, name, 1) + bm.add_member(uid, name) await bot.kkr_send(ev, f"成员{bot.kkr_at(uid)}添加成功!欢迎{name}加入{clan['name']}") @@ -127,7 +127,7 @@ async def list_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) clan = _check_clan(bm) - mems = bm.list_member(1) + mems = bm.list_member() if l := len(mems): # 数字太多会被腾讯ban mems = map(lambda x: '{uid} | {name}'.format_map(x), mems) @@ -142,11 +142,11 @@ async def list_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): async def del_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) uid = args['@'] or args.uid or ev.get_author_id() - mem = _check_member(bm, uid, bm.group, '公会内无此成员') + mem = _check_member(bm, uid, '公会内无此成员') if uid != ev.get_author_id(): _check_admin(ev, '才能踢人') - bm.del_member(uid, mem['alt']) + bm.del_member(uid) await bot.kkr_send(ev, f"成员{mem['name']}已从公会删除", at_sender=True) @@ -156,7 +156,7 @@ async def clear_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): clan = _check_clan(bm) _check_admin(ev) - msg = f"{clan['name']}已清空!" if bm.clear_member(1) else f"{clan['name']}已无成员" + msg = f"{clan['name']}已清空!" if bm.clear_member() else f"{clan['name']}已无成员" await bot.kkr_send(ev, msg, at_sender=True) @@ -166,8 +166,8 @@ async def batch_add_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): clan = _check_clan(bm) _check_admin(ev) - mlist = ev.get_members_in_group() - + mlist = ev.get_members_in_group() + if len(mlist) > 50: raise ClanBattleError('群员过多!一键入会仅限50人以内群使用') @@ -176,7 +176,7 @@ async def batch_add_member(bot:KokkoroBot, ev:EventInterface, args:ParseResult): for m in mlist: if m.get_id() != self_id: try: - bm.add_member(m.get_id(), bm.group, m.get_nick_name() or m.get_name() or m.get_id(), 1) + bm.add_member(m.get_id(), m.get_nick_name() or m.get_name() or m.get_id()) succ += 1 except DatabaseError: fail += 1 @@ -196,15 +196,23 @@ async def process_challenge(bot:KokkoroBot, ev:EventInterface, ch:ParseResult): bm = BattleMaster(ev.get_group_id()) now = datetime.now() - timedelta(days=ch.get('dayoffset', 0)) clan = _check_clan(bm) - mem = _check_member(bm, ch.uid, ch.alt) + mem = _check_member(bm, ch.uid) - cur_round, cur_boss, cur_hp = bm.get_challenge_progress(1, now) + cur_round, cur_boss, cur_hp = bm.get_challenge_progress(now) round_ = ch.round or cur_round boss = ch.boss or cur_boss - + is_current = (round_ == cur_round) and (boss == cur_boss) + is_future = (round_ > cur_round) or (round_ == cur_round and boss > cur_boss) + flag = ch.flag + msg = [''] + if not is_current: + msg.append('⚠️上报与当前进度不一致') + if is_future: + cur_hp = bm.get_boss_hp(round_, boss, clan['server']) + damage = None - if not (ch.round or ch.boss): - # 当前周目并且是尾刀,则自动将伤害设置为当前血量 + if (is_current or is_future): + # 当前或未来boss并且是尾刀,则自动将伤害设置为当前血量 if BattleMaster.has_damage_kind_for(ch.flag, BattleMaster.LAST): damage = cur_hp if not damage: @@ -212,23 +220,14 @@ async def process_challenge(bot:KokkoroBot, ev:EventInterface, ch:ParseResult): raise NotFoundError('请给出伤害值') damage = ch.damage - - flag = ch.flag - - # if (ch.flag == BattleMaster.LAST) and (ch.round or ch.boss) and (not damage): - # raise NotFoundError('补报尾刀请给出伤害值') # 补报尾刀必须给出伤害值 - - msg = [''] - # 上一刀如果是尾刀,这一刀就是补偿刀 - challenges = bm.list_challenge_of_user_of_day(mem['uid'], mem['alt'], now) + challenges = bm.list_challenge_of_user_of_day(mem['uid'], now) if len(challenges) > 0 and challenges[-1]['flag'] == BattleMaster.LAST: flag = flag | BattleMaster.EXT msg.append('⚠️已自动标记为补时刀') - if round_ != cur_round or boss != cur_boss: - msg.append('⚠️上报与当前进度不一致') - else: # 伤害校对 + if (is_current or is_future): + # 伤害校对 if damage >= cur_hp: if damage > cur_hp: damage = cur_hp @@ -237,14 +236,14 @@ async def process_challenge(bot:KokkoroBot, ev:EventInterface, ch:ParseResult): if not BattleMaster.has_damage_kind_for(flag, BattleMaster.LAST): flag = flag | BattleMaster.LAST msg.append('⚠️已自动标记为尾刀') - elif BattleMaster.has_damage_kind_for(flag, BattleMaster.LAST): - if damage < cur_hp: - damage = cur_hp - msg.append(f'⚠️尾刀伤害已自动修正为{damage}') + damage = cur_hp + msg.append(f'⚠️尾刀伤害已自动修正为{damage}') - eid = bm.add_challenge(mem['uid'], mem['alt'], round_, boss, damage, flag, now) - aft_round, aft_boss, aft_hp = bm.get_challenge_progress(1, now) + remain_hp = cur_hp - damage if (is_current or is_future) else -1 + + eid = bm.add_challenge(mem['uid'], round_, boss, damage, remain_hp, flag, now) + aft_round, aft_boss, aft_hp = bm.get_challenge_progress(now) max_hp, score_rate = bm.get_boss_info(aft_round, aft_boss, clan['server']) msg.append(f"记录编号E{eid}:\n{mem['name']}给予{round_}周目{bm.int2kanji(boss)}王{damage:,d}点伤害\n") msg.append(_gen_progress_text(clan['name'], aft_round, aft_boss, aft_hp, max_hp, score_rate)) @@ -278,7 +277,6 @@ async def add_challenge(bot:KokkoroBot, ev:EventInterface, args:ParseResult): 'boss': args.B, 'damage': args.get(''), 'uid': args['@'] or args.uid or ev.get_author_id(), - 'alt': ev.get_group_id(), 'flag': BattleMaster.NORM, 'dayoffset': args.get('D', 0) }) @@ -287,7 +285,7 @@ async def add_challenge(bot:KokkoroBot, ev:EventInterface, args:ParseResult): damage = args.get('') if isDD(damage): await jiuzhe(bot, ev) - + @cb_cmd(('出尾刀', '收尾', '尾刀', 'add-challenge-last'), ArgParser(usage='!出尾刀 (<伤害值>) (@)', arg_dict={ @@ -301,7 +299,6 @@ async def add_challenge_last(bot:KokkoroBot, ev:EventInterface, args:ParseResult 'boss': args.B, 'damage': args.get(''), 'uid': args['@'] or args.uid or ev.get_author_id(), - 'alt': ev.get_group_id(), 'flag': BattleMaster.LAST }) await process_challenge(bot, ev, challenge) @@ -318,7 +315,6 @@ async def add_challenge_ext(bot:KokkoroBot, ev:EventInterface, args:ParseResult) 'boss': args.B, 'damage': args.get(''), 'uid': args['@'] or args.uid or ev.get_author_id(), - 'alt': ev.get_group_id(), 'flag': BattleMaster.EXT }) await process_challenge(bot, ev, challenge) @@ -334,7 +330,6 @@ async def add_challenge_timeout(bot:KokkoroBot, ev:EventInterface, args:ParseRes 'boss': args.B, 'damage': 0, 'uid': args['@'] or args.uid or ev.get_author_id(), - 'alt': ev.get_group_id(), 'flag': BattleMaster.TIMEOUT }) await process_challenge(bot, ev, challenge) @@ -347,12 +342,12 @@ async def del_challenge(bot:KokkoroBot, ev:EventInterface, args:ParseResult): now = datetime.now() clan = _check_clan(bm) - ch = bm.get_challenge(args.E, 1, now) + ch = bm.get_challenge(args.E, now) if not ch: raise NotFoundError(f'未找到出刀记录E{args.E}') if ch['uid'] != ev.get_author_id(): _check_admin(ev, '才能删除其他人的记录') - bm.del_challenge(args.E, 1, now) + bm.del_challenge(args.E, now) await bot.kkr_send(ev, f"{clan['name']}已删除{bot.kkr_at(ch['uid'])}的出刀记录E{args.E}", at_sender=True) @@ -375,7 +370,7 @@ def __init__(self, data:dict): if 'max' not in data or len(data['max']) != 6: data['max'] = [99, 6, 6, 6, 6, 6] self._data = data - + @staticmethod def default(): return SubscribeData({ @@ -384,13 +379,13 @@ def default(): 'tree':[], 'lock':[], 'max': [99, 6, 6, 6, 6, 6] }) - + def get_sub_list(self, boss:int): return self._data[str(boss)] - + def get_memo_list(self, boss:int): return self._data[f'm{boss}'] - + def get_tree_list(self): return self._data['tree'] @@ -413,13 +408,13 @@ def remove_sub(self, boss:int, uid:str): def add_tree(self, uid:str): self._data['tree'].append(uid) - + def clear_tree(self): self._data['tree'].clear() - + def get_lock_info(self): return self._data['lock'] - + def set_lock(self, uid:str, ts): self._data['lock'] = [ (uid, ts) ] @@ -449,7 +444,7 @@ def _gen_namelist_text(bot:KokkoroBot, bm:BattleMaster, uidlist:List[str], memol if do_at: mems = map(lambda x: str(bot.kkr_at(x)), uidlist) else: - mems = map(lambda x: bm.get_member(x, bm.group) or bm.get_member(x, 0) or {'name': str(x)}, uidlist) + mems = map(lambda x: bm.get_member(x) or {'name': str(x)}, uidlist) mems = map(lambda x: x['name'], mems) if memolist: mems = list(mems) @@ -468,7 +463,7 @@ async def subscribe(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) uid = ev.get_author_id() _check_clan(bm) - _check_member(bm, uid, bm.group) + _check_member(bm, uid) sub = _load_sub(bm.group) boss = args[''] @@ -498,20 +493,20 @@ async def unsubscribe(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) uid = ev.get_author_id() _check_clan(bm) - _check_member(bm, uid, bm.group) + _check_member(bm, uid) sub = _load_sub(bm.group) boss = args[''] - boss_name = bm.int2kanji(boss) + boss_name = bm.int2kanji(boss) slist = sub.get_sub_list(boss) mlist = sub.get_memo_list(boss) - limit = sub.get_sub_limit(boss) + limit = sub.get_sub_limit(boss) if uid not in slist: raise NotFoundError(f'您没有预约{boss_name}王') sub.remove_sub(boss, uid) _save_sub(sub, bm.group) msg = [ f'\n已为您取消预约{boss_name}王!' ] - msg.append(f'=== 当前队列 {len(slist)}/{limit} ===') + msg.append(f'=== 当前队列 {len(slist)}/{limit} ===') msg.extend(_gen_namelist_text(bot, bm, slist, mlist)) await bot.kkr_send(ev, '\n'.join(msg), at_sender=True) @@ -596,7 +591,7 @@ async def set_subscribe_limit(bot:KokkoroBot, ev, args:ParseResult): if not (0 < limit <= 30): raise ClanBattleError('预约上限只能为1~30内的整数') sub = _load_sub(bm.group) - sub.set_sub_limit(args.B, limit) + sub.set_sub_limit(args.B, limit) _save_sub(sub, bm.group) await bot.kkr_send(ev, f'{bm.int2kanji(args.B)}王预约上限已设置为:{limit}') @@ -606,7 +601,7 @@ async def add_sos(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) uid = ev.get_author_id() clan = _check_clan(bm) - _check_member(bm, uid, bm.group) + _check_member(bm, uid) sub = _load_sub(bm.group) tree = sub.get_tree_list() @@ -637,14 +632,14 @@ async def list_sos(bot:KokkoroBot, ev:EventInterface, args:ParseResult): async def lock_boss(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) _check_clan(bm) - _check_member(bm, ev.get_author_id(), bm.group) + _check_member(bm, ev.get_author_id()) sub = _load_sub(bm.group) lock = sub.get_lock_info() if lock: uid, ts = lock[0] time = datetime.fromtimestamp(ts) - mem = bm.get_member(uid, bm.group) or bm.get_member(uid, 0) or {'name': str(uid)} + mem = bm.get_member(uid) or {'name': str(uid)} delta = datetime.now() - time delta = timedelta(seconds=round(delta.total_seconds())) # ignore miliseconds msg = f"\n锁定失败:{mem['name']}已于{delta}前锁定了Boss" @@ -669,7 +664,7 @@ async def unlock_boss(bot:KokkoroBot, ev:EventInterface, args:ParseResult): uid, ts = lock[0] time = datetime.fromtimestamp(ts) if uid != ev.get_author_id(): - mem = bm.get_member(uid, bm.group) or bm.get_member(uid, 0) or {'name': str(uid)} + mem = bm.get_member(uid) or {'name': str(uid)} delta = datetime.now() - time delta = timedelta(seconds=round(delta.total_seconds())) # ignore miliseconds _check_admin(ev, f"才能解锁其他人\n解锁失败:{mem['name']}于{delta}前锁定了Boss") @@ -689,7 +684,7 @@ async def auto_unlock_boss(bot:KokkoroBot, ev:EventInterface, bm:BattleMaster): uid, ts = lock[0] time = datetime.fromtimestamp(ts) if uid != ev.get_author_id(): - mem = bm.get_member(uid, bm.group) or bm.get_member(uid, 0) or {'name': str(uid)} + mem = bm.get_member(uid) or {'name': str(uid)} delta = datetime.now() - time delta = timedelta(seconds=round(delta.total_seconds())) # ignore miliseconds msg = f"⚠️{mem['name']}于{delta}前锁定了Boss,您出刀前未申请锁定!" @@ -706,7 +701,7 @@ async def show_progress(bot:KokkoroBot, ev:EventInterface, args:ParseResult): bm = BattleMaster(ev.get_group_id()) clan = _check_clan(bm) - r, b, hp = bm.get_challenge_progress(1, datetime.now()) + r, b, hp = bm.get_challenge_progress(datetime.now()) max_hp, score_rate = bm.get_boss_info(r, b, clan['server']) msg = _gen_progress_text(clan['name'], r, b, hp, max_hp, score_rate) await bot.kkr_send(ev, '\n' + msg, at_sender=True) @@ -719,16 +714,16 @@ async def stat_damage(bot:KokkoroBot, ev:EventInterface, args:ParseResult): clan = _check_clan(bm) yyyy, mm, _ = bm.get_yyyymmdd(now) - stat = bm.stat_damage(1, now) + stat = bm.stat_damage(now) yn = len(stat) if not yn: await bot.kkr_send(ev, f"{clan['name']}{yyyy}年{mm}月会战统计数据为空", at_sender=True) return - stat.sort(key=lambda x: x[3][0], reverse=True) - total = [ s[3][0] for s in stat ] - name = [ s[2] for s in stat ] + stat.sort(key=lambda x: x[2][0], reverse=True) + total = [ s[2][0] for s in stat ] + name = [ s[1] for s in stat ] y_pos = list(range(yn)) y_size = 0.3 * yn + 1.0 unit = 1e4 @@ -736,12 +731,12 @@ async def stat_damage(bot:KokkoroBot, ev:EventInterface, args:ParseResult): # convert to pre-sum for s in stat: - d = s[3] + d = s[2] d[0] = 0 for i in range(2, 6): d[i] += d[i - 1] pre_sum_dmg = [ - [ s[3][b] for s in stat ] for b in range(6) + [ s[2][b] for s in stat ] for b in range(6) ] # generate statistic figure @@ -770,7 +765,7 @@ async def stat_damage(bot:KokkoroBot, ev:EventInterface, args:ParseResult): await bot.kkr_send(ev, fig) plt.close() - + msg = f"※分数统计请发送“!分数统计”" await bot.kkr_send(ev, msg, at_sender=True) @@ -782,9 +777,9 @@ async def stat_score(bot:KokkoroBot, ev:EventInterface, args:ParseResult): clan = _check_clan(bm) yyyy, mm, _ = bm.get_yyyymmdd(now) - stat = bm.stat_score(1, now) - stat.sort(key=lambda x: x[3], reverse=True) - + stat = bm.stat_score(now) + stat.sort(key=lambda x: x[2], reverse=True) + if not len(stat): await bot.kkr_send(ev, f"{clan['name']}{yyyy}年{mm}月会战统计数据为空", at_sender=True) return @@ -797,9 +792,9 @@ async def stat_score(bot:KokkoroBot, ev:EventInterface, args:ParseResult): # generate statistic figure fig, ax = plt.subplots() - score = list(map(lambda i: i[3], stat)) + score = list(map(lambda i: i[2], stat)) yn = len(stat) - name = list(map(lambda i: i[2], stat)) + name = list(map(lambda i: i[1], stat)) y_pos = list(range(yn)) if score[0] >= 1e8: @@ -835,15 +830,15 @@ async def _do_show_remain(bot:KokkoroBot, ev:EventInterface, args:ParseResult, a if at_user: _check_admin(ev, '才能催刀。您可以用【!查刀】查询余刀') - rlist = bm.list_challenge_remain(1, datetime.now() - timedelta(days=args.get('D', 0))) - rlist.sort(key=lambda x: x[3] + x[4], reverse=True) + rlist = bm.list_challenge_remain(datetime.now() - timedelta(days=args.get('D', 0))) + rlist.sort(key=lambda x: x[2] + x[3], reverse=True) msg = [ f"\n{clan['name']}今日余刀:" ] sum_remain = 0 - for uid, _, name, r_n, r_e in rlist: + for uid, name, r_n, r_e in rlist: if r_n or r_e: msg.append(f"剩{r_n}刀 补时{r_e}刀 | {bot.kkr_at(uid) if at_user else name}") sum_remain += r_n - + if len(msg) == 1: await bot.kkr_send(ev, f"今日{clan['name']}所有成员均已下班!各位辛苦了!", at_sender=True) else: @@ -876,15 +871,15 @@ async def list_challenge(bot:KokkoroBot, ev:EventInterface, args:ParseResult): zone = bm.get_timezone_num(clan['server']) uid = args['@'] or args.uid if uid: - mem = _check_member(bm, uid, bm.group, '公会内无此成员') - challen = bm.list_challenge_of_user_of_day(mem['uid'], mem['alt'], now, zone) + mem = _check_member(bm, uid, '公会内无此成员') + challen = bm.list_challenge_of_user_of_day(mem['uid'], now, zone) else: - challen = bm.list_challenge_of_day(clan['cid'], now, zone) + challen = bm.list_challenge_of_day(now, zone) msg = [ f'{clan["name"]}出刀记录:\n编号|出刀者|周目|Boss|伤害|标记' ] challenstr = 'E{eid:0>3d}|{name}|r{round}|b{boss}|{dmg: >7,d}{flag_str}' for c in challen: - mem = bm.get_member(c['uid'], c['alt']) + mem = bm.get_member(c['uid']) c['name'] = mem['name'] if mem else c['uid'] flag = c['flag'] c['flag_str'] = '|' + ','.join(BattleMaster.damage_kind_to_string(flag)) @@ -904,22 +899,18 @@ async def boss_slayer(bot, ev: EventInterface, args: ParseResult): ext0 = 110 # 日服补偿刀20秒起 remain = ev.get_param().remain - prm = re.findall("\d+[wW万]", remain) - if len(prm) == 2: - dmg1 = int(prm[0][:-1]) * 10000 - dmg2 = int(prm[1][:-1]) * 10000 - else: - prm = re.findall("\d+", remain) - if len(prm) == 2: - dmg1 = int(prm[0]) - dmg2 = int(prm[1]) - else: - usage = "【用法/用例】\n!补偿刀计算 50w 60w" - await bot.kkr_send(ev, usage, at_sender=True) - return + prm = re.findall("(\d+)([kK千]?)([wW万]?)", remain) - r, b, hp = bm.get_challenge_progress(1, datetime.now()) + if len(prm) != 2: + usage = "【用法/用例】\n!补偿刀计算 伤害1 伤害2" + await bot.kkr_send(ev, usage, at_sender=True) + return + r, b, hp = bm.get_challenge_progress(datetime.now()) + dmg1 = int(prm[0][0]) + dmg2 = int(prm[1][0]) + dmg1 = dmg1 * (1000 if prm[0][1] else 1) * (10000 if prm[0][2] else 1) + dmg2 = dmg2 * (1000 if prm[1][1] else 1) * (10000 if prm[1][2] else 1) if dmg1 + dmg2 < hp: msg = '0x0 这两刀合起来还打不死BOSS喔' else: @@ -969,7 +960,7 @@ async def clan_rank(bot:KokkoroBot, ev:EventInterface, args:ParseResult): clan_name = args.get('C') leader = args.get('L') rank = args.get('R') - + # default if clan_name == '' and leader == '' and rank == '': bm = BattleMaster(ev.get_group_id()) @@ -993,13 +984,14 @@ async def clan_rank(bot:KokkoroBot, ev:EventInterface, args:ParseResult): "Cache-Control": "no-cache", "Connection": "keep-alive", "Content-Type": "application/json", + "Custom-Source": "KokkoroBot", "Host": "service-kjcbcnmw-1254119946.gz.apigw.tencentcs.com", "Origin": "https://kengxxiao.github.io", "Referer": "https://kengxxiao.github.io/Kyouka/", "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" } - + try: cookies = await get_cookies("https://kengxxiao.github.io/Kyouka/") cookies.set('fav', '[]', domain='kengxxiao.github.io') # cookies is a must @@ -1021,4 +1013,4 @@ async def clan_rank(bot:KokkoroBot, ev:EventInterface, args:ParseResult): info += f" 会长 {data['leader_name']}" msg.append(info) await bot.kkr_send(ev, '\n'.join(msg), at_sender=True) - + diff --git a/kokkoro/modules/pcrclanbattle/clanbattle/cmd_web.py b/kokkoro/modules/pcrclanbattle/clanbattle/cmd_web.py new file mode 100644 index 0000000..134aae7 --- /dev/null +++ b/kokkoro/modules/pcrclanbattle/clanbattle/cmd_web.py @@ -0,0 +1,89 @@ +import json +import os +from datetime import datetime, timedelta +import time +from hashlib import sha256 +from typing import Dict, Union +from urllib.parse import urljoin + +from .webmaster import WebMaster + +from .argparse import ArgParser, ArgHolder, ParseResult +from .argparse.argtype import * +from . import sv, cb_cmd, cb_prefix +from . import cb_cmd +from kokkoro import config +from kokkoro.common_interface import KokkoroBot, EventInterface +from kokkoro.service import Service +from kokkoro.priv import ADMIN, SUPERUSER +from kokkoro.util import rand_string, add_salt_and_hash + +svweb = Service('web') +@svweb.scheduled_job('cron', hour='5') +async def drop_expired_logins(): + now = datetime.now() + wm = WebMaster() + wm.del_login_by_time(now) + +EXPIRED_TIME = 7 * 24 * 60 * 60 # 7 days +LOGIN_AUTH_COOKIE_NAME = 'yobot_login' +# this need be same with static/password.js +FRONTEND_SALT = '14b492a3-a40a-42fc-a236-e9a9307b47d2' + + +@cb_cmd(('登录', 'login'), ArgParser('!登录')) +async def login(bot:KokkoroBot, ev:EventInterface, args:ParseResult): + if False and "this message is from group": # FIXME + return '请私聊使用' + + wm = WebMaster() + uid = ev.get_author_id() + gid = ev.get_group_id() + auth = ev.get_author().get_priv() + + member = wm.get_member(uid, gid) + if member is None: + await bot.kkr_send(ev, '请先加入公会') + return + member['authority_group'] = 100 + if auth == SUPERUSER: + member['authority_group'] = 1 + elif auth == ADMIN: + member['authority_group'] = 10 + wm.mod_member(member) + + user = wm.get_or_add_user(uid, rand_string(16)) + login_code = rand_string(6) + user['login_code'] = login_code + user['login_code_available'] = True + user['login_code_expire_time'] = int(time.time()) + 60 + wm.mod_user(user) + + url = urljoin( + config.PUBLIC_ADDRESS, + '{}login/c/#uid={}&key={}'.format( + config.PUBLIC_BASEPATH, + user['uid'], + login_code, + ) + ) + await bot.kkr_send_dm(ev, url) + +@cb_cmd(('重置密码', 'reset-password'), ArgParser('!重置密码')) +async def reset_password(bot:KokkoroBot, ev:EventInterface, args:ParseResult): + if False and "this message is from group": # FIXME + return '请私聊使用' + reply = f'您的临时密码是:{reset_pwd(ev)}' + await bot.kkr_send_dm(ev, reply) + +def reset_pwd(ev:EventInterface): + wm = WebMaster() + user = wm.get_or_add_user(ev.get_author_id(), rand_string(16)) + raw_pwd = rand_string(8) + frontend_salted_pwd = add_salt_and_hash(raw_pwd + user['uid'], FRONTEND_SALT) + user['password'] = add_salt_and_hash(frontend_salted_pwd, user['salt']) + user['privacy'] = 0 + user['must_change_password'] = 1 + wm.mod_user(user) + wm.del_login(user['uid']) + return raw_pwd diff --git a/kokkoro/modules/pcrclanbattle/clanbattle/dao/sqlitedao.py b/kokkoro/modules/pcrclanbattle/clanbattle/dao/sqlitedao.py index cf0ddc7..3c85475 100644 --- a/kokkoro/modules/pcrclanbattle/clanbattle/dao/sqlitedao.py +++ b/kokkoro/modules/pcrclanbattle/clanbattle/dao/sqlitedao.py @@ -37,64 +37,38 @@ class ClanDao(SqliteDao): def __init__(self): super().__init__( table='clan', - columns='gid, cid, name, server', + columns='gid, name, server', fields=''' - gid TEXT NOT NULL, - cid INT NOT NULL, + gid TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, - server INT NOT NULL, - PRIMARY KEY (gid, cid) + server INT NOT NULL ''') @staticmethod def row2item(r): - return {'gid': r[0], 'cid': r[1], 'name': r[2], 'server': r[3]} if r else None + return {'gid': r[0], 'name': r[1], 'server': r[2]} if r else None def add(self, clan): with self._connect() as conn: try: conn.execute(''' - INSERT INTO {0} ({1}) VALUES (?, ?, ?, ?) + INSERT INTO {0} ({1}) VALUES (?, ?, ?) '''.format(self._table, self._columns), - (clan['gid'], clan['cid'], clan['name'], clan['server']) ) + (clan['gid'], clan['name'], clan['server']) ) except (sqlite3.DatabaseError) as e: logger.error(f'[ClanDao.add] {e}') raise DatabaseError('添加公会失败') - - def delete(self, gid, cid): - with self._connect() as conn: - try: - conn.execute(''' - DELETE FROM {0} WHERE gid=? AND cid=? - '''.format(self._table), - (gid, cid) ) - except (sqlite3.DatabaseError) as e: - logger.error(f'[ClanDao.delete] {e}') - raise DatabaseError('删除公会失败') - - def modify(self, clan): - with self._connect() as conn: - try: - conn.execute(''' - UPDATE {0} SET name=?, server=? WHERE gid=? AND cid=? - '''.format(self._table), - (clan['name'], clan['server'], clan['gid'], clan['cid']) ) - except (sqlite3.DatabaseError) as e: - logger.error(f'[ClanDao.modify] {e}') - raise DatabaseError('修改公会失败') - - - def find_one(self, gid, cid): + def find_one(self, gid): with self._connect() as conn: try: ret = conn.execute(''' - SELECT {1} FROM {0} WHERE gid=? AND cid=? + SELECT {1} FROM {0} WHERE gid=? '''.format(self._table, self._columns), - (gid, cid) ).fetchone() + (gid,) ).fetchone() return self.row2item(ret) except (sqlite3.DatabaseError) as e: logger.error(f'[ClanDao.find_one] {e}') @@ -111,20 +85,51 @@ def find_all(self): return [self.row2item(r) for r in ret] except (sqlite3.DatabaseError) as e: logger.error(f'[ClanDao.find_all] {e}') - raise DatabaseError('查找公会失败') + raise DatabaseError('查找公会失败') - def find_by_gid(self, gid): + def find_by_uid(self, uid): with self._connect() as conn: try: ret = conn.execute(''' - SELECT {1} FROM {0} WHERE gid=? - '''.format(self._table, self._columns), - (gid,) ).fetchall() - return [self.row2item(r) for r in ret] + SELECT M.uid, C.gid, C.name + FROM MEMBER M INNER JOIN CLAN C + ON M.gid = C.gid + WHERE M.uid=? + ''', + (uid,) ).fetchall() + return [ + {'uid': r[0], 'gid': r[1], 'name': r[2]} + if r else None + for r in ret + ] except (sqlite3.DatabaseError) as e: - logger.error(f'[ClanDao.find_by_gid] {e}') - raise DatabaseError('查找公会失败') + logger.error(f'[MemberDao.find_by_uid] {e}') + raise DatabaseError('查找成员及公会失败') + + + def modify(self, clan): + with self._connect() as conn: + try: + conn.execute(''' + UPDATE {0} SET name=?, server=? WHERE gid=? + '''.format(self._table), + (clan['name'], clan['server'], clan['gid']) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[ClanDao.modify] {e}') + raise DatabaseError('修改公会失败') + + + def delete(self, gid): + with self._connect() as conn: + try: + conn.execute(''' + DELETE FROM {0} WHERE gid=? + '''.format(self._table), + (gid,) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[ClanDao.delete] {e}') + raise DatabaseError('删除公会失败') @@ -132,20 +137,25 @@ class MemberDao(SqliteDao): def __init__(self): super().__init__( table='member', - columns='uid, alt, name, gid, cid', + columns='uid, gid, name, last_sl, authority_group', fields=''' uid TEXT NOT NULL, - alt TEXT NOT NULL, - name TEXT NOT NULL, gid TEXT NOT NULL, - cid INT NOT NULL, - PRIMARY KEY (uid, alt) + name TEXT NOT NULL, + last_sl INT, + authority_group INT, + PRIMARY KEY (uid, gid) ''') - @staticmethod def row2item(r): - return {'uid': r[0], 'alt': r[1], 'name': r[2], 'gid': r[3], 'cid': r[4]} if r else None + return { + 'uid': r[0], + 'gid': r[1], + 'name': r[2], + 'last_sl': r[3], + 'authority_group': r[4] + } if r else None def add(self, member): @@ -154,43 +164,20 @@ def add(self, member): conn.execute(''' INSERT INTO {0} ({1}) VALUES (?, ?, ?, ?, ?) '''.format(self._table, self._columns), - (member['uid'], member['alt'], member['name'], member['gid'], member['cid']) ) + (member['uid'], member['gid'], member['name'], None, None) + ) except (sqlite3.DatabaseError) as e: logger.error(f'[MemberDao.add] {e}') raise DatabaseError('添加成员失败') - - def delete(self, uid, alt): - with self._connect() as conn: - try: - conn.execute(''' - DELETE FROM {0} WHERE uid=? AND alt=? - '''.format(self._table), - (uid, alt) ) - except (sqlite3.DatabaseError) as e: - logger.error(f'[MemberDao.delete] {e}') - raise DatabaseError('删除成员失败') - - - def modify(self, member): - with self._connect() as conn: - try: - conn.execute(''' - UPDATE {0} SET name=?, gid=?, cid=? WHERE uid=? AND alt=? - '''.format(self._table), - (member['name'], member['gid'], member['cid'], member['uid'], member['alt']) ) - except (sqlite3.DatabaseError) as e: - logger.error(f'[MemberDao.modify] {e}') - raise DatabaseError('修改成员失败') - - def find_one(self, uid, alt): + def find_one(self, uid, gid): with self._connect() as conn: try: ret = conn.execute(''' - SELECT {1} FROM {0} WHERE uid=? AND alt=? + SELECT {1} FROM {0} WHERE uid=? AND gid=? '''.format(self._table, self._columns), - (uid, alt) ).fetchone() + (uid, gid) ).fetchone() return self.row2item(ret) except (sqlite3.DatabaseError) as e: logger.error(f'[MemberDao.find_one] {e}') @@ -210,29 +197,26 @@ def find_all(self): raise DatabaseError('查找成员失败') - def find_by(self, gid=None, cid=None, uid=None): + def find_by(self, uid=None, gid=None): cond_str = [] cond_tup = [] if not gid is None: cond_str.append('gid=?') cond_tup.append(gid) - if not cid is None: - cond_str.append('cid=?') - cond_tup.append(cid) if not uid is None: cond_str.append('uid=?') cond_tup.append(uid) if 0 == len(cond_tup): return self.find_all() - + cond_str = " AND ".join(cond_str) - + with self._connect() as conn: try: ret = conn.execute(''' SELECT {1} FROM {0} WHERE {2} - '''.format(self._table, self._columns, cond_str), + '''.format(self._table, self._columns, cond_str), cond_tup ).fetchall() return [self.row2item(r) for r in ret] except (sqlite3.DatabaseError) as e: @@ -240,34 +224,56 @@ def find_by(self, gid=None, cid=None, uid=None): raise DatabaseError('查找成员失败') - def delete_by(self, gid=None, cid=None, uid=None): + def modify(self, member): + with self._connect() as conn: + try: + conn.execute(''' + UPDATE {0} SET name=?, last_sl=?, authority_group=? WHERE uid=? AND gid=? + '''.format(self._table), + (member['name'], member['last_sl'], member['authority_group'], + member['uid'], member['gid']) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[MemberDao.modify] {e}') + raise DatabaseError('修改成员失败') + + + def delete(self, uid, gid): + with self._connect() as conn: + try: + conn.execute(''' + DELETE FROM {0} WHERE uid=? AND gid=? + '''.format(self._table), + (uid, gid) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[MemberDao.delete] {e}') + raise DatabaseError('删除成员失败') + + + def delete_by(self, uid=None, gid=None): cond_str = [] cond_tup = [] if not gid is None: cond_str.append('gid=?') cond_tup.append(gid) - if not cid is None: - cond_str.append('cid=?') - cond_tup.append(cid) if not uid is None: cond_str.append('uid=?') cond_tup.append(uid) if 0 == len(cond_tup): raise DatabaseError('删除成员的条件有误') - + cond_str = " AND ".join(cond_str) - + with self._connect() as conn: try: cur = conn.execute(''' DELETE FROM {0} WHERE {1} - '''.format(self._table, cond_str), + '''.format(self._table, cond_str), cond_tup ) return cur.rowcount except (sqlite3.DatabaseError) as e: - logger.error(f'[MemberDao.find_by] {e}') - raise DatabaseError('查找成员失败') + logger.error(f'[MemberDao.delete_by] {e}') + raise DatabaseError('删除成员失败') class BattleDao(SqliteDao): @@ -276,33 +282,34 @@ class BattleDao(SqliteDao): EXT = 0x02 TIMEOUT = 0x04 - def __init__(self, gid, cid, yyyy, mm): + def __init__(self, gid, yyyy, mm): super().__init__( - table=self.get_table_name(gid, cid, yyyy, mm), - columns='eid, uid, alt, time, round, boss, dmg, flag', + table=self.get_table_name(gid, yyyy, mm), + columns='eid, uid, time, round, boss, dmg, remain, flag', fields=''' eid INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, - alt TEXT NOT NULL, time TIMESTAMP NOT NULL, - round INT NOT NULL, - boss INT NOT NULL, - dmg INT NOT NULL, - flag INT NOT NULL + round INT NOT NULL, + boss INT NOT NULL, + dmg INT NOT NULL, + remain INT NOT NULL, + flag INT NOT NULL ''') @staticmethod - def get_table_name(gid, cid, yyyy, mm): - return 'battle_%s_%d_%04d%02d' % (gid, cid, yyyy, mm) + def get_table_name(gid, yyyy, mm): + return 'battle_%s_%04d%02d' % (gid, yyyy, mm) @staticmethod def row2item(r): return { - 'eid': r[0], 'uid': r[1], 'alt': r[2], - 'time': r[3], 'round': r[4], 'boss': r[5], - 'dmg': r[6], 'flag': r[7] } if r else None + 'eid': r[0], 'uid': r[1], 'time': r[2], + 'round': r[3], 'boss': r[4], 'dmg': r[5], + 'remain': r[6], 'flag': r[7] + } if r else None def add(self, challenge): @@ -311,36 +318,17 @@ def add(self, challenge): cur = conn.execute(''' INSERT INTO {0} ({1}) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?) '''.format(self._table, self._columns), - (challenge['uid'], challenge['alt'], challenge['time'], challenge['round'], challenge['boss'], challenge['dmg'], challenge['flag']) ) + ( + challenge['uid'], challenge['time'], challenge['round'], + challenge['boss'], challenge['dmg'], challenge['remain'], + challenge['flag'] + ) + ) return cur.lastrowid except (sqlite3.DatabaseError) as e: logger.error(f'[BattleDao.add] {e}') raise DatabaseError('添加记录失败') - - def delete(self, eid): - with self._connect() as conn: - try: - conn.execute(''' - DELETE FROM {0} WHERE eid=? - '''.format(self._table), - (eid, ) ) - except (sqlite3.DatabaseError) as e: - logger.error(f'[BattleDao.delete] {e}') - raise DatabaseError('删除记录失败') - - - def modify(self, challenge): - with self._connect() as conn: - try: - conn.execute(''' - UPDATE {0} SET uid=?, alt=?, time=?, round=?, boss=?, dmg=?, flag=? WHERE eid=? - '''.format(self._table), - (challenge['uid'], challenge['alt'], challenge['time'], challenge['round'], challenge['boss'], challenge['dmg'], challenge['flag'], challenge['eid']) ) - except (sqlite3.DatabaseError) as e: - logger.error(f'[BattleDao.modify] {e}') - raise DatabaseError('修改记录失败') - def find_one(self, eid): with self._connect() as conn: @@ -368,26 +356,23 @@ def find_all(self): raise DatabaseError('查找记录失败') - def find_by(self, uid=None, alt=None, order_by_user=False): + def find_by(self, uid=None, order_by_user=False): cond_str = [] cond_tup = [] - order = 'round, boss, eid' if not order_by_user else 'uid, alt, round, boss, eid' + order = 'round, boss, eid' if not order_by_user else 'uid, round, boss, eid' if not uid is None: cond_str.append('uid=?') cond_tup.append(uid) - if not alt is None: - cond_str.append('alt=?') - cond_tup.append(alt) if 0 == len(cond_tup): return self.find_all() - + cond_str = " AND ".join(cond_str) - + with self._connect() as conn: try: ret = conn.execute(''' SELECT {1} FROM {0} WHERE {2} ORDER BY {3} - '''.format(self._table, self._columns, cond_str, order), + '''.format(self._table, self._columns, cond_str, order), cond_tup ).fetchall() return [self.row2item(r) for r in ret] except (sqlite3.DatabaseError) as e: @@ -395,3 +380,210 @@ def find_by(self, uid=None, alt=None, order_by_user=False): raise DatabaseError('查找记录失败') + def modify(self, challenge): + with self._connect() as conn: + try: + conn.execute(''' + UPDATE {0} SET + uid=?, time=?, round=?, boss=?, dmg=?, remain=?, flag=? + WHERE eid=? + '''.format(self._table), + ( + challenge['uid'], challenge['time'], challenge['round'], + challenge['boss'], challenge['dmg'], challenge['remain'], + challenge['flag'], challenge['eid'] + ) + ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[BattleDao.modify] {e}') + raise DatabaseError('修改记录失败') + + + def delete(self, eid): + with self._connect() as conn: + try: + conn.execute(''' + DELETE FROM {0} WHERE eid=? + '''.format(self._table), + (eid, ) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[BattleDao.delete] {e}') + raise DatabaseError('删除记录失败') + + +class UserDao(SqliteDao): + def __init__(self): + super().__init__( + table='user', + columns='uid, last_login_time, last_login_ipaddr, ' + \ + 'login_code, login_code_available, login_code_expire_time, ' + \ + 'must_change_password, password, privacy, salt', + fields=''' + uid TEXT NOT NULL PRIMARY KEY, + last_login_time INT, + last_login_ipaddr INT, + login_code CHAR(6), + login_code_available INT DEFAULT 0, + login_code_expire_time INT DEFAULT 0, + must_change_password INT DEFAULT 1, + password CHAR(64), + privacy INT DEFAULT 0, + salt VARCHAR(16) NOT NULL + ''') + + @staticmethod + def row2item(r): + return { + 'uid': r[0], + 'last_login_time': r[1], + 'last_login_ipaddr': r[2], + 'login_code': r[3], + 'login_code_available': r[4], + 'login_code_expire_time': r[5], + 'must_change_password': r[6], + 'password': r[7], + 'privacy': r[8], + 'salt': r[9] + } if r else None + + + def get_or_add(self, uid, salt): + with self._connect() as conn: + try: + conn.execute(''' + INSERT OR IGNORE INTO {0} (uid, salt) VALUES (?, ?) + '''.format(self._table), + (uid, salt) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[UserDao.get_or_add] {e}') + raise DatabaseError('添加用户失败') + return self.find_one(uid) + + + def find_one(self, uid): + with self._connect() as conn: + try: + ret = conn.execute(''' + SELECT {1} FROM {0} WHERE uid=? + '''.format(self._table, self._columns), + (uid,) ).fetchone() + return self.row2item(ret) + except (sqlite3.DatabaseError) as e: + logger.error(f'[UserDao.find_one] {e}') + raise DatabaseError('查找用户失败') + + + def modify(self, user:dict): + with self._connect() as conn: + try: + conn.execute(''' + UPDATE {0} SET last_login_time=?, last_login_ipaddr=?, + login_code=?, login_code_available=?, login_code_expire_time=?, + must_change_password=?, password=?, privacy=?, salt=? + WHERE uid=? + '''.format(self._table), + ( + user['last_login_time'], user['last_login_ipaddr'], + user['login_code'], user['login_code_available'], user['login_code_expire_time'], + user['must_change_password'], user['password'], user['privacy'], + user['salt'], user['uid']) + ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[UserDao.modify] {e}') + raise DatabaseError('修改用户失败') + + + def delete(self, uid): + with self._connect() as conn: + try: + conn.execute(''' + DELETE FROM {0} WHERE uid=? + '''.format(self._table), + (uid,) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[UserDao.delete] {e}') + raise DatabaseError('删除用户失败') + + +class LoginDao(SqliteDao): + def __init__(self): + super().__init__( + table='login', + columns='uid, auth_cookie, auth_cookie_expire_time', + fields=''' + uid TEXT NOT NULL, + auth_cookie CHAR(64) NOT NULL, + auth_cookie_expire_time INT NOT NULL, + PRIMARY KEY(uid, auth_cookie) + ''') + + @staticmethod + def row2item(r): + return { + 'uid': r[0], + 'auth_cookie': r[1], + 'auth_cookie_expire_time': r[2] + } if r else None + + def add(self, uid, auth_cookie, auth_cookie_expire_time): + with self._connect() as conn: + try: + conn.execute(''' + INSERT INTO {0} ({1}) VALUES (?, ?, ?) + '''.format(self._table, self._columns), + (uid, auth_cookie, auth_cookie_expire_time) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[LoginDao.add] {e}') + raise DatabaseError('添加登录状态失败') + + def find_one(self, uid, auth_cookie): + with self._connect() as conn: + try: + ret = conn.execute(''' + SELECT {1} FROM {0} WHERE uid=? AND auth_cookie=? + '''.format(self._table, self._columns), + (uid, auth_cookie) ).fetchone() + return self.row2item(ret) + except (sqlite3.DatabaseError) as e: + logger.error(f'[LoginDao.find_one] {e}') + raise DatabaseError('查找登录状态失败') + + def modify(self, login:dict): + with self._connect() as conn: + try: + ret = conn.execute(''' + UPDATE {0} SET auth_cookie_expire_time=? + WHERE uid=? AND auth_cookie=? + '''.format(self._table), + (login['auth_cookie_expire_time'], login['uid'], login['auth_cookie']) ) + ret = conn.execute(''' + UPDATE user SET last_login_time=?, last_login_ipaddr=? WHERE uid=? + ''', + (login['last_login_time'], login['last_login_ipaddr'], login['uid']) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[LoginDao.find_one] {e}') + raise DatabaseError('修改登录状态失败') + + def delete(self, uid): + with self._connect() as conn: + try: + conn.execute(''' + DELETE FROM {0} WHERE uid=? + '''.format(self._table), + (uid,) ) + except (sqlite3.DatabaseError) as e: + logger.error(f'[LoginDao.delete] {e}') + raise DatabaseError('删除登录状态失败') + + + def delete_by_time(self, time): + with self._connect() as conn: + try: + conn.execute(''' + DELETE FROM {0} WHERE auth_cookie_expire_time str: s = s.replace('&', '&') \ .replace('[', '[') \ @@ -118,7 +122,7 @@ def left_time(self, key) -> float: class DailyNumberLimiter: tz = pytz.timezone('Asia/Shanghai') - + def __init__(self, max_num): self.today = -1 self.count = defaultdict(int) @@ -167,3 +171,8 @@ def load_modules(module_dir, module_prefix): load_module(f'{module_prefix}.{m.group(1)}') +def rand_string(n=16): + return ''.join(random.choice(charset) for _ in range(n)) + +def add_salt_and_hash(raw: str, salt: str): + return sha256((raw + salt).encode()).hexdigest() diff --git a/kokkoro/web/__init__.py b/kokkoro/web/__init__.py index 12ac8a5..b4aacf5 100644 --- a/kokkoro/web/__init__.py +++ b/kokkoro/web/__init__.py @@ -1,8 +1,14 @@ +import random from quart import Quart quart_app = Quart(__name__) +# generate random secret_key +if(quart_app.secret_key is None): + quart_app.secret_key = bytes( + (random.randint(0, 255) for _ in range(16))) + def get_app(): return quart_app -from .homepage import * +from .route_elucidator import * diff --git a/kokkoro/web/clan.py b/kokkoro/web/clan.py new file mode 100644 index 0000000..82057c8 --- /dev/null +++ b/kokkoro/web/clan.py @@ -0,0 +1,159 @@ +from quart import jsonify, redirect, request, session, url_for +from urllib.parse import urljoin +from kokkoro import config +import kokkoro.web.universal_executor as ue +from . import get_app +app = get_app() + +from .templating import render_template, static_folder, template_folder + +PATH = config.PUBLIC_BASEPATH + +@app.route( urljoin(PATH, 'clan//'), + methods=['GET']) +async def yobot_clan(group_id): + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return await render_template( '404.html', item='公会' ), 404 + member = ue.get_member(bm, uid=session.get('yobot_user')) + if not member and member['authority_group'] >= 10: + return await render_template('clan/unauthorized.html') + return await render_template( 'clan/panel.html', is_member=member ) + +@app.route( urljoin(PATH, 'clan//subscribers/'), + methods=['GET']) +async def yobot_clan_subscribers(group_id): + return '敬请期待' + +@app.route( urljoin(PATH, 'clan//progress/'), + methods=['GET']) +async def yobot_clan_progress(group_id): + return await render_template( 'clan/progress.html', ) + +@app.route( urljoin(PATH, 'clan//statistics/'), + methods=['GET']) +async def yobot_clan_statistics(group_id): + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return await render_template( '404.html', item='公会' ), 404 + return await render_template( + 'clan/statistics.html', + ) + +@app.route( urljoin(PATH, 'clan//statistics//'), + methods=['GET']) +async def yobot_clan_statistics_details(group_id, sid): + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return await render_template( '404.html', item='公会' ), 404 + return await render_template( f'clan/statistics/statistics{sid}.html', ) + +@app.route( urljoin(PATH, 'clan///'), + methods=['GET']) +async def yobot_clan_user(group_id, uid): + if 'yobot_user' not in session: + return redirect(url_for('yobot_login', callback=request.path)) + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return await render_template('404.html', item='公会'), 404 + member = ue.get_member(bm, uid=session.get('yobot_user')) + if not member and member['authority_group'] >= 10: + return await render_template('clan/unauthorized.html') + return await render_template( + 'clan/user.html', + uid=uid, + ) + +@app.route( urljoin(PATH, 'clan//my/'), + methods=['GET']) +async def yobot_clan_user_auto(group_id): + if 'yobot_user' not in session: + return redirect(url_for('yobot_login', callback=request.path)) + return redirect(url_for( + 'yobot_clan_user', + group_id=group_id, + uid=session.get('yobot_user'), + )) + +@app.route( urljoin(PATH, 'clan//setting/'), + methods=['GET']) +async def yobot_clan_setting(group_id): + if 'yobot_user' not in session: + return redirect(url_for('yobot_login', callback=request.path)) + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return await render_template('404.html', item='公会'), 404 + member = ue.get_member(bm, uid=session.get('yobot_user')) + if not member: + return await render_template( + 'unauthorized.html', + limit='本公会成员', + auth='无') + if member['authority_group'] >= 100: + return await render_template( + 'unauthorized.html', + limit='公会战管理员', + auth='成员') + return await render_template('clan/setting.html') + +@app.route( urljoin(PATH, 'clan//api/'), + methods=['POST']) +async def yobot_clan_api(group_id): + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return jsonify(code=20, message="Group dosen't exist") + if 'yobot_user' not in session: + if not(False and 'privacy'): + return jsonify(code=10, message='Not logged in') + uid = 0 + else: + uid = session.get('yobot_user') + member = ue.get_member(bm, uid=uid) + if not member or member['authority_group'] >= 100: + return jsonify(code=11, message='Insufficient authority') + payload = await request.get_json() + if payload is None: + return jsonify(code=30, message='Invalid payload') + else: + payload['uid'] = uid + if uid != 0 and payload.get('csrf_token') != session['csrf_token']: + return jsonify(code=15, message='Invalid csrf_token') + return ue.clan_api(bm, payload) + +@app.route( urljoin(PATH, 'clan//setting/api/'), + methods=['POST']) +async def yobot_clan_setting_api(group_id): + if 'yobot_user' not in session: + return jsonify(code=10, message='Not logged in') + uid = session.get('yobot_user') + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return jsonify(code=20, message="Group dosen't exist") + member = ue.get_member(bm, uid=uid) + if not member or member['authority_group'] >= 100: + return jsonify(code=11, message='Insufficient authority') + payload = await request.get_json() + if payload is None: + return jsonify(code=30, message='Invalid payload') + else: + payload['uid'] = uid + if payload.get('csrf_token') != session['csrf_token']: + return jsonify(code=15, message='Invalid csrf_token') + return ue.clan_setting_api(bm, payload) + +@app.route( urljoin(PATH, 'clan//statistics/api/'), + methods=['GET']) +async def yobot_clan_statistics_api(group_id): + bm = ue.get_bm(group_id) + group = ue.get_group(bm) + if group is None: + return jsonify(code=20, message="Group dosen't exist") + apikey = request.args.get('apikey') or 'apikey' + return await ue.clan_statistics_api(bm, apikey) diff --git a/kokkoro/web/homepage.py b/kokkoro/web/homepage.py deleted file mode 100644 index 5383bf8..0000000 --- a/kokkoro/web/homepage.py +++ /dev/null @@ -1,8 +0,0 @@ -from . import get_app - -app = get_app() - -@app.route('/') -async def hello(): - return "Hello!" - diff --git a/kokkoro/web/login.py b/kokkoro/web/login.py new file mode 100644 index 0000000..c6301de --- /dev/null +++ b/kokkoro/web/login.py @@ -0,0 +1,306 @@ +import os +from quart import Response, jsonify, make_response, redirect, request, send_file, session, url_for +from urllib.parse import urljoin +from kokkoro import config +from kokkoro.priv import is_super_user +from kokkoro.util import rand_string, add_salt_and_hash +import kokkoro.web.universal_executor as ue +from . import get_app +app = get_app() + +from .templating import render_template, static_folder, template_folder + +PATH = config.PUBLIC_BASEPATH +MAX_TRY_TIMES = 3 +EXPIRED_TIME = 7 * 24 * 60 * 60 # 7 days +LOGIN_AUTH_COOKIE_NAME = 'yobot_login' + +class ExceptionWithAdvice(RuntimeError): + def __init__(self, reason: str, advice=''): + super(ExceptionWithAdvice, self).__init__(reason) + self.reason = reason + self.advice = advice + +def check_pwd(user, pwd) -> bool: + if not user or not user['password'] or not user['salt']: + raise ExceptionWithAdvice( + 'uid错误 或 您尚未设置密码', + '请私聊机器人“!登录”后,再次选择[修改密码]修改' + ) + if user['privacy'] >= MAX_TRY_TIMES: + raise ExceptionWithAdvice( + '密码错误次数过多,账号已锁定', + '请私聊机器人“!重置密码”后,重新登录' + ) + if not user['password'] == add_salt_and_hash(pwd, user['salt']): + user['privacy'] += 1 + ue.get_wm().mod_user(user) + raise ExceptionWithAdvice( + '密码错误', + '如果忘记密码,请私聊机器人“!登录”后,再次选择[修改密码]修改,' + \ + '或私聊机器人“!重置密码”后,重新登录' + ) + return True + +def check_key(user, key): + now = ue.now() + if user is None or user['login_code'] != key: + raise ExceptionWithAdvice( + '无效的登录地址', + '请检查登录地址是否完整且为最新' + ) + if user['login_code_expire_time'] < now: + raise ExceptionWithAdvice( + '这个登录地址已过期', + '请私聊机器人“!登录”获取新登录地址' + ) + if not user['login_code_available']: + raise ExceptionWithAdvice( + '这个登录地址已被使用', + '请私聊机器人“!登录”获取新登录地址' + ) + return True + +def recall_from_cookie(auth_cookie): + advice = '请私聊机器人“!登录” 或 重新登录' + if not auth_cookie: + raise ExceptionWithAdvice('登录已过期', advice) + s = auth_cookie.split(':') + if len(s) != 2: + raise ExceptionWithAdvice('Cookie异常', advice) + uid, auth = s + + user = ue.get_wm().get_user(uid) + advice = '请先加入一个公会 或 私聊机器人“!登录”' + if user is None: + raise ExceptionWithAdvice('用户不存在', advice) + salty_cookie = add_salt_and_hash(auth, user['salt']) + userlogin = ue.get_wm().get_login(uid, salty_cookie) + if userlogin is None: + raise ExceptionWithAdvice('Cookie异常', advice) + now = ue.now() + if userlogin['auth_cookie_expire_time'] < now: + raise ExceptionWithAdvice('登录已过期', advice) + user['last_login_time'] = now + user['last_login_ipaddr'] = request.headers.get('X-Real-IP', request.remote_addr) + ue.get_wm().mod_user(user) + + return user + +def set_auth_info(user:dict, res: Response = None, save_user=True): + now = ue.now() + session['yobot_user'] = user['uid'] + session['csrf_token'] = rand_string(16) + session['last_login_time'] = user['last_login_time'] + session['last_login_ipaddr'] = user['last_login_ipaddr'] + user['last_login_time'] = now + user['last_login_ipaddr'] = request.headers.get('X-Real-IP', request.remote_addr) + if res: + new_key = rand_string(32) + ue.get_wm().add_login( + uid=user['uid'], + auth_cookie=add_salt_and_hash(new_key, user['salt']), + auth_cookie_expire_time=now + EXPIRED_TIME + ) + new_cookie = f"{user['uid']}:{new_key}" + res.set_cookie(LOGIN_AUTH_COOKIE_NAME, new_cookie, max_age=EXPIRED_TIME) + if save_user: + ue.get_wm().mod_user(user) + +@app.route( urljoin(PATH, 'login/'), + methods=['GET', 'POST']) +async def yobot_login(): + def get_params(k: str) -> str: + return request.args.get(k) \ + if request.method == "GET" \ + else (form and k in form and form[k]) + + if request.method == "POST": + form = await request.form + + try: + uid = get_params('uid') + key = get_params('key') + pwd = get_params('pwd') + callback_page = get_params('callback') or url_for('yobot_user') + auth_cookie = request.cookies.get(LOGIN_AUTH_COOKIE_NAME) + + if not uid and not auth_cookie: + return await render_template( + 'login.html', + advice=f'请私聊机器人“!登录”获取登录地址 ', + ) + key_failure = None + if uid: + user = ue.get_wm().get_user(uid) + if key: + try: + check_key(user, key) + except ExceptionWithAdvice as e: + if auth_cookie: + uid = None + key_failure = e + else: + raise e from e + if pwd: + check_pwd(user, pwd) + if auth_cookie and not uid: + # 可能用于用cookie寻回session + + if 'yobot_user' in session: + # 会话未过期 + return redirect(callback_page) + try: + user = recall_from_cookie(auth_cookie) + except ExceptionWithAdvice as e: + if key_failure is not None: + raise key_failure + else: + raise e from e + set_auth_info(user) + if user['must_change_password']: + callback_page = url_for('yobot_reset_pwd') + return redirect(callback_page) + if not key and not pwd: + raise ExceptionWithAdvice( + "无效的登录地址", + "请检查登录地址是否完整" + ) + if user['must_change_password']: + callback_page = url_for('yobot_reset_pwd') + res = await make_response(redirect(callback_page)) + set_auth_info(user, res, save_user=False) + ue.get_wm().mod_user(user) + return res + except ExceptionWithAdvice as e: + return await render_template( + 'login.html', + reason=e.reason, + advice=e.advice or '请私聊机器人“!登录”获取登录地址 ', + ) + +@app.route( urljoin(PATH, 'login/c/'), + methods=['GET', 'POST']) +async def yobot_login_code(): + return await send_file(os.path.join(template_folder, "login-code.html")) + +@app.route( urljoin(PATH, 'logout/'), + methods=['GET', 'POST']) +async def yobot_logout(): + session.clear() + res = await make_response(redirect(url_for('yobot_login'))) + res.delete_cookie(LOGIN_AUTH_COOKIE_NAME) + return res + +@app.route( urljoin(PATH, 'user/'), + endpoint='yobot_user', + methods=['GET']) +@app.route( urljoin(PATH, 'admin/'), + endpoint='yobot_admin', + methods=['GET']) +async def yobot_user(): + if 'yobot_user' not in session: + return redirect(url_for('yobot_login', callback=request.path)) + user = ue.get_wm().get_user(session.get('yobot_user')) + user['authority_group'] = 1 if is_super_user(user['uid']) else 100 + groups = ue.get_wm().get_clan_by_uid(user['uid']) + return await render_template( + 'user.html', + user=user, + clan_groups=[{ + 'group_id': g['gid'], + 'group_name': g['name'] or g['gid'] + } for g in groups], + ) + +@app.route( + urljoin(PATH, 'user//'), + methods=['GET']) +async def yobot_user_info(uid): + if 'yobot_user' not in session: + return redirect(url_for('yobot_login', callback=request.path)) + if session.get('yobot_user') == uid: + visited_user_info = ue.get_wm().get_user(uid) + else: + visited_user = ue.get_wm().get_user(uid) + if visited_user is None: + return '没有此用户', 404 + visited_user_info = visited_user + return await render_template( + 'user-info.html', + user=visited_user_info, + visitor=ue.get_wm().get_user(session.get('yobot_user')), + ) + +@app.route( + urljoin(PATH, + 'user//api/'), + methods=['GET', 'PUT']) +async def yobot_user_info_api(uid): + if 'yobot_user' not in session: + return jsonify(code=10, message='未登录') + user = ue.get_wm().get_user(session.get('yobot_user')) + if user['uid'] != uid and not is_super_user(user['uid']): + return jsonify(code=11, message='权限不足') + user_data = ue.get_wm().get_user(uid) + if user_data is None: + return jsonify(code=20, message='用户不存在') + member_data = ue.get_wm().get_member_by_uid(uid) + clans = ue.get_wm().get_clan_by_uid(user['uid']) + clan_name = {c['gid']: c['name'] for c in clans} + if request.method == 'GET': + return jsonify( + code=0, + userid=uid, + authority_group=1 if is_super_user(user['uid']) else 100, + last_login_time=user_data['last_login_time'], + last_login_ipaddr=user_data['last_login_ipaddr'], + member_data=member_data, + clan_name=clan_name, + ) + new_setting = await request.get_json() + if new_setting is None: + return jsonify(code=30, message='消息体格式错误') + new_nickname = new_setting.get('nickname') + index = new_setting.get('index') + if new_nickname is None or index is None: + return jsonify(code=32, message='消息体内容错误') + bm = ue.get_bm(member_data[index]['gid']) + ue.mod_member(bm, user_data['uid'], new_nickname, + member_data[index]['last_sl'], + member_data[index]['authority_group']) + return jsonify(code=0, message='success') + + +@app.route( + urljoin(PATH, 'user/reset-password/'), + methods=['GET', 'POST']) +async def yobot_reset_pwd(): + try: + if 'yobot_user' not in session: + return redirect(url_for('yobot_login', callback=request.path)) + if request.method == "GET": + return await render_template('password.html') + + wm = ue.get_wm() + uid = session.get('yobot_user') + user = wm.get_user(uid) + if user is None: + raise Exception("请先加公会") + form = await request.form + pwd = form["pwd"] + user['password'] = add_salt_and_hash(pwd, user['salt']) + user['privacy'] = 0 + user['must_change_password'] = False + wm.mod_user(user) + # 踢掉过去的登录 + wm.del_login(uid) + return await render_template( + 'password.html', + success="密码设置成功", + ) + except Exception as e: + return await render_template( + 'password.html', + error=str(e) + ) diff --git a/kokkoro/web/route_elucidator.py b/kokkoro/web/route_elucidator.py new file mode 100644 index 0000000..0781842 --- /dev/null +++ b/kokkoro/web/route_elucidator.py @@ -0,0 +1,69 @@ +import os +from urllib.parse import urljoin +from quart import send_file, send_from_directory, session +from kokkoro import config +import kokkoro.web.universal_executor as ue +from . import get_app +app = get_app() + +from .templating import render_template, static_folder, template_folder + +PATH = config.PUBLIC_BASEPATH + +''' routing start ---------------------------------------------------------- ''' +# add route for static files +@app.route("/assets/", methods=["GET"]) +async def yobot_static(filename): + return await send_file(os.path.join(static_folder, filename)) + +@app.route("/favicon.ico", ["GET"]) +async def yobot_favicon(): + return await send_from_directory(static_folder, "small.ico") + +from .clan import * +from .login import * +from .settings import * + +@app.route(PATH, ["GET"]) +async def yobot_homepage(): + return await render_template( + "homepage.html", + # verinfo=self.setting["verinfo"]["ver_name"], + # show_icp=self.setting["show_icp"], + # icp_info=self.setting["icp_info"], + # gongan_info=self.setting["gongan_info"], + ) + +@app.route( urljoin(PATH, 'about/'), + methods=['GET']) +async def yobot_about(): + return await render_template( + "about.html", + # verinfo=self.setting["verinfo"]["ver_name"], + ) + +@app.route( urljoin(PATH, 'help/'), + methods=['GET']) +async def yobot_help(): + return await send_from_directory(template_folder, "help.html") + +@app.route( urljoin(PATH, 'manual/'), + methods=['GET']) +async def yobot_manual(): + return await send_from_directory(template_folder, "manual.html") + +@app.route( urljoin(PATH, 'api/ip-location/'), + methods=['GET']) +async def yobot_api_iplocation(): + if 'yobot_user' not in session: + return jsonify(['unauthorized']) + ip = request.args.get('ip') + if ip is None: + return jsonify(['unknown']) + try: + location = await ue.ip_location(ip) + except: + location = ['unknown'] + return jsonify(location) + +''' routing end ------------------------------------------------------------ ''' diff --git a/kokkoro/web/settings.py b/kokkoro/web/settings.py new file mode 100644 index 0000000..1439749 --- /dev/null +++ b/kokkoro/web/settings.py @@ -0,0 +1,41 @@ +from quart import Response, jsonify, make_response, redirect, request, send_file, session, url_for +from urllib.parse import urljoin +from kokkoro import config +from kokkoro.util import rand_string, add_salt_and_hash +import kokkoro.web.universal_executor as ue +from . import get_app +app = get_app() + +from .templating import render_template, static_folder, template_folder + +PATH = config.PUBLIC_BASEPATH + +@app.route( urljoin(PATH, 'admin/setting/'), + methods=['GET']) +async def yobot_setting(): + return "敬请期待" + +@app.route( urljoin(PATH, 'admin/setting/api/'), + methods=['GET', 'PUT']) +async def yobot_setting_api(): + return "敬请期待" + +@app.route( urljoin(PATH, 'admin/users/'), + methods=['GET']) +async def yobot_users_managing(): + return "敬请期待" + +@app.route( urljoin(PATH, 'admin/users/api/'), + methods=['POST']) +async def yobot_users_api(): + return "敬请期待" + +@app.route( urljoin(PATH, 'admin/groups/'), + methods=['GET']) +async def yobot_groups_managing(): + return "敬请期待" + +@app.route( urljoin(PATH, 'admin/groups/api/'), + methods=['POST']) +async def yobot_groups_api(): + return "敬请期待" diff --git a/kokkoro/web/static/clan/panel.js b/kokkoro/web/static/clan/panel.js new file mode 100644 index 0000000..2ad8d17 --- /dev/null +++ b/kokkoro/web/static/clan/panel.js @@ -0,0 +1,277 @@ +if (!Object.defineProperty) { + alert('浏览器版本过低'); +} +var vm = new Vue({ + el: '#app', + data: { + activeIndex: "1", + groupData: {}, + bossData: { cycle: 0, full_health: 0, health: 0, num: 0 }, + is_admin: false, + self_id: 0, + today_sl: false, + members: [], + damage: 0, + defeat: null, + behalf: null, + boss_num: null, + recordFormVisible: false, + recordDefeatVisible: false, + recordBehalfVisible: false, + lockBossVisible: false, + subscribe: null, + message: '', + subscribeFormVisible: false, + subscribeCancelVisible: false, + suspendVisible: false, + statusFormVisible: false, + leavePage: false, + }, + mounted() { + var thisvue = this; + axios.post("./api/", { + action: 'get_data', + csrf_token: csrf_token, + }).then(function (res) { + if (res.data.code == 0) { + thisvue.groupData = res.data.groupData; + thisvue.bossData = res.data.bossData; + thisvue.is_admin = res.data.selfData.is_admin; + thisvue.self_id = res.data.selfData.user_id; + thisvue.today_sl = res.data.selfData.today_sl; + document.title = res.data.groupData.group_name + ' - 公会战'; + } else { + thisvue.$alert(res.data.message, '加载数据错误'); + } + }).catch(function (error) { + thisvue.$alert(error, '加载数据错误'); + }); + axios.post("./api/", { + action: 'get_member_list', + csrf_token: csrf_token, + }).then(function (res) { + if (res.data.code == 0) { + thisvue.members = res.data.members; + } else { + thisvue.$alert(res.data.message, '获取成员失败'); + } + }).catch(function (error) { + thisvue.$alert(error, '获取成员失败'); + }); + // this.status_long_polling(); + }, + destroyed: function () { + this.leavePage = true; + }, + computed: { + damageHint: function () { + if (this.damage < 10000) { + return ''; + } else if (this.damage < 100000) { + return '万'; + } else if (this.damage < 1000000) { + return '十万'; + } else if (this.damage < 10000000) { + return '百万'; + } else if (this.damage < 100000000) { + return '千万'; + } else { + return '`(*>﹏<*)′'; + } + }, + }, + methods: { + find_name: function (uid) { + for (m of this.members) { + if (m.uid == uid) { + return m.nickname; + } + }; + return uid; + }, + /* + status_long_polling: function () { + var thisvue = this; + axios.post("./api/", { + action: 'update_boss', + timeout: 30, + csrf_token: csrf_token, + }, { + timeout: 40000, + }).then(function (res) { + if (res.data.code == 0) { + thisvue.bossData = res.data.bossData; + thisvue.status_long_polling(); + if (res.data.notice) { + thisvue.$notify({ + title: '通知', + message: '(' + (new Date()).toLocaleTimeString('chinese', { hour12: false }) + ') ' + res.data.notice, + duration: 60000, + }); + } + } else if (res.data.code == 1) { + thisvue.status_long_polling(); + } else { + thisvue.$confirm(res.data.message, '刷新boss数据错误', { + confirmButtonText: '重试', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + thisvue.status_long_polling(); + }); + } + }).catch(function (error) { + if (thisvue.leavePage) { + return; + } + thisvue.$confirm(error, '刷新boss错误', { + confirmButtonText: '重试', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + thisvue.status_long_polling(); + }); + }); + }, + */ + callapi: function (payload) { + var thisvue = this; + payload.csrf_token = csrf_token; + axios.post("./api/", payload).then(function (res) { + if (res.data.code == 0) { + if (res.data.bossData) { + thisvue.bossData = res.data.bossData; + } + if (res.data.notice) { + thisvue.$notify({ + title: '通知', + message: res.data.notice, + duration: 60000, + }); + } + } else { + thisvue.$alert(res.data.message, '数据错误'); + } + }).catch(function (error) { + thisvue.$alert(error, '数据错误'); + }); + }, + recordselfdamage: function (event) { + this.callapi({ + action: 'addrecord', + defeat: false, + damage: this.damage, + behalf: null, + message:this.message, + }); + this.recordFormVisible = false; + }, + recordselfdefeat: function (event) { + this.callapi({ + action: 'addrecord', + defeat: true, + behalf: null, + message:this.message, + }); + this.recordDefeatVisible = false; + }, + recorddamage: function (event) { + this.callapi({ + action: 'addrecord', + defeat: this.defeat, + behalf: this.behalf, + damage: this.damage, + message:this.message, + }); + this.recordBehalfVisible = false; + }, + recordundo: function (event) { + this.callapi({ + action: 'undo', + }); + }, + challengeapply: function (appli_type) { + this.callapi({ + action: 'apply', + extra_msg:this.message, + appli_type:appli_type, + }); + this.lockBossVisible=false; + }, + cancelapply: function (event) { + this.callapi({ + action: 'cancelapply', + }); + }, + addsuspend: function (event) { + this.callapi({ + action: 'addsubscribe', + boss_num: 0, + message: this.message, + }); + this.suspendVisible=false; + }, + cancelsuspend: function (event) { + this.callapi({ + action: 'cancelsubscribe', + boss_num: 0, + }); + }, + save_slot: function (event) { + this.today_sl = !this.today_sl; + this.callapi({ + action: 'save_slot', + today: this.today_sl, + }); + }, + addsubscribe: function (event) { + this.callapi({ + action: 'addsubscribe', + boss_num: parseInt(this.subscribe), + message: this.message, + }); + this.subscribeFormVisible = false; + }, + cancelsubscribe: function (event) { + this.callapi({ + action: 'cancelsubscribe', + boss_num: parseInt(this.subscribe), + }); + this.subscribeCancelVisible = false + }, + startmodify: function (event) { + if (this.is_admin) { + this.statusFormVisible = true; + } else { + this.$alert('此功能仅公会战管理员可用'); + } + }, + modify: function (event) { + this.callapi({ + action: 'modify', + cycle: this.bossData.cycle, + boss_num: this.bossData.num, + health: this.bossData.health, + }); + this.statusFormVisible = false; + }, + handleSelect(key, keyPath) { + this.leavePage = true; + switch (key) { + case '2': + window.location = './subscribers/'; + break; + case '3': + window.location = './progress/'; + break; + case '4': + window.location = './statistics/'; + break; + case '5': + window.location = `./${this.self_id}/`; + break; + } + }, + }, + delimiters: ['[[', ']]'], +}) \ No newline at end of file diff --git a/kokkoro/web/static/clan/progress.js b/kokkoro/web/static/clan/progress.js new file mode 100644 index 0000000..4a0d187 --- /dev/null +++ b/kokkoro/web/static/clan/progress.js @@ -0,0 +1,255 @@ +if (!Object.defineProperty) { + alert('浏览器版本过低'); +} +var vm = new Vue({ + el: '#app', + data: { + progressData: [], + members: [], + tailsData: [], + tailsDataVisible: false, + group_name: null, + reportDate: null, + activeIndex: '3', + multipleSelection: [], + sendRemindVisible: false, + send_via_private: false, + dropMemberVisible: false, + today: 0, + }, + mounted() { + var thisvue = this; + axios.all([ + axios.post('../api/', { + action: 'get_challenge', + csrf_token: csrf_token, + ts: (thisvue.get_now() / 1000), + }), + axios.post('../api/', { + action: 'get_member_list', + csrf_token: csrf_token, + }), + ]).then(axios.spread(function (res, memres) { + if (res.data.code != 0) { + thisvue.$alert(res.data.message, '获取记录失败'); + return; + } + if (memres.data.code != 0) { + thisvue.$alert(memres.data.message, '获取成员失败'); + return; + } + thisvue.members = memres.data.members; + for (m of thisvue.members) { + m.finished = 0; + m.detail = []; + } + thisvue.today = res.data.today; + thisvue.reportDate = thisvue.get_now(); + thisvue.refresh(res.data.challenges); + })).catch(function (error) { + thisvue.$alert(error, '获取数据失败'); + }); + }, + methods: { + get_now: function () { + let d = new Date(); + return Date.parse(d); + }, + csummary: function (cha) { + if (cha == undefined) { + return ''; + } + return `(${cha.cycle}-${cha.boss_num}) ${cha.damage}`; + }, + cdetail: function (cha) { + if (cha == undefined) { + return ''; + } + var nd = new Date(); + nd.setTime(cha.challenge_time * 1000); + var detailstr = nd.toLocaleString('chinese', { hour12: false, timeZone: 'asia/shanghai' }) + '\n'; + detailstr += cha.cycle + '周目' + cha.boss_num + '号boss\n'; + detailstr += (cha.health_remain + cha.damage).toLocaleString(options = { timeZone: 'asia/shanghai' }) + '→' + cha.health_remain.toLocaleString(options = { timeZone: 'asia/shanghai' }); + if (cha.message) { + detailstr += '\n留言:' + cha.message; + } + return detailstr; + }, + arraySpanMethod: function ({ row, column, rowIndex, columnIndex }) { + if (columnIndex >= 4) { + if (columnIndex % 2 == 0) { + var detail = row.detail[columnIndex - 4]; + if (detail != undefined && detail.health_remain != 0) { + return [1, 2]; + } + } else { + var detail = row.detail[columnIndex - 5]; + if (detail != undefined && detail.health_remain != 0) { + return [0, 0]; + } + } + } + }, + report_day: function (event) { + var thisvue = this; + axios.post('../api/', { + action: 'get_challenge', + csrf_token: csrf_token, + ts: (thisvue.reportDate ? ((thisvue.reportDate.getTime() - thisvue.reportDate.getTimezoneOffset()*60000) / 1000) : null), + }).then(function (res) { + if (res.data.code != 0) { + thisvue.$alert(res.data.message, '获取记录失败'); + } else { + thisvue.refresh(res.data.challenges); + } + }).catch(function (error) { + thisvue.$alert(error, '获取记录失败'); + }) + this.today = -1; + }, + refresh: function (challenges) { + challenges.sort((a, b) => a.uid - b.uid); + this.progressData = [...this.members]; + // for (m of this.progressData) m.today_total_damage = 0; + var thisvue = this; + var m = { uid: -1 }; + for (c of challenges) { + if (m.uid != c.uid) { + thisvue.update_member_info(m); + m = { + uid: c.uid, + finished: 0, + detail: [], + // today_total_damage: 0, + } + } + m.detail[2 * m.finished] = c; + // m.today_total_damage += c.damage; + if (c.is_continue) { + m.finished += 0.5; + } else { + if (c.health_remain != 0) { + m.finished += 1; + } else { + m.finished += 0.5; + } + } + } + thisvue.update_member_info(m); + }, + viewTails: function () { + this.tailsData = []; + for (const m of this.progressData) { + if (m.finished % 1 != 0) { + let c = m.detail[m.finished * 2 - 1]; + this.tailsData.push({ + uid: m.uid, + nickname: m.nickname, + boss: c.cycle + '-' + c.boss_num, + damage: c.damage, + message: c.message, + }); + } + } + this.tailsDataVisible = true; + }, + update_member_info: function (m) { + if (m.uid == -1) { + return + } + for (let index = 0; index < this.progressData.length; index++) { + if (m.uid == this.progressData[index].uid) { + m.nickname = this.progressData[index].nickname; + m.sl = this.progressData[index].sl; + this.progressData[index] = m; + return + } + } + m.nickname = '(未加入)'; + this.progressData.push(m); + }, + find_name: function (uid) { + for (m of this.members) { + if (m.uid == uid) { + return m.nickname; + } + }; + return uid; + }, + viewInExcel: function () { + var icons = document.getElementsByTagName('span'); + while (icons[0]) { + icons[0].remove(); + } + var uri = 'data:application/vnd.ms-excel;base64,'; + var ctx = '' + document.getElementsByTagName('thead')[0].innerHTML + document.getElementsByTagName('tbody')[0].innerHTML + '
'; + window.location.href = uri + window.btoa(unescape(encodeURIComponent(ctx))); + document.documentElement.innerHTML = "请在Excel中查看(如果无法打开,请安装最新版本Excel)\n或者将整个表格复制,粘贴到Excel中使用"; + }, + handleTitleSelect(key, keyPath) { + switch (key) { + case '1': + window.location = '../'; + break; + case '2': + window.location = '../subscribers/'; + break; + case '3': + window.location = '../progress/'; + break; + case '4': + window.location = '../statistics/'; + break; + case '5': + window.location = `../my/`; + break; + } + }, + handleSelectionChange(val) { + this.multipleSelection = val; + }, + selectUnfinished(event) { + this.progressData.forEach(row => { + if (row.finished < 3) { + this.$refs.multipleTable.toggleRowSelection(row, true); + } else { + this.$refs.multipleTable.toggleRowSelection(row, false); + } + }); + }, + sendRequest(action) { + if (this.multipleSelection.length === 0) { + this.$alert('请先勾选成员', '失败'); + } + var memberlist = []; + this.multipleSelection.forEach(row => { + memberlist.push(row.uid); + }); + var thisvue = this; + var payload = { + action: action, + csrf_token: csrf_token, + memberlist: memberlist, + }; + if (action === 'send_remind') { + payload.send_private_msg = thisvue.send_via_private; + } + axios.post('../api/', payload).then(function (res) { + if (res.data.code != 0) { + if (res.data.code == 11) { + res.data.message = '你的权限不足'; + } + thisvue.$alert(res.data.message, '请求失败'); + } else { + thisvue.$notify({ + title: '提醒', + message: res.data.notice, + }); + } + }).catch(function (error) { + thisvue.$alert(error, '请求失败'); + }) + }, + }, + delimiters: ['[[', ']]'], +}) diff --git a/kokkoro/web/static/clan/setting.js b/kokkoro/web/static/clan/setting.js new file mode 100644 index 0000000..66eb533 --- /dev/null +++ b/kokkoro/web/static/clan/setting.js @@ -0,0 +1,163 @@ +if (!Object.defineProperty) { + alert('浏览器版本过低'); +} +var vm = new Vue({ + el: '#app', + data: { + activeIndex: null, + groupData: {}, + battle_id: null, + data_slot_record_count: [], + form: { + game_server: null, + privacy: { + allow_guest: false, + allow_statistics_api: false, + }, + notify: { + challenge: false, + undo: false, + apply: false, + cancelapply: false, + subscribe: false, + cancelsubscribe: false, + suspend: false, + cancelsuspend: false, + modify: false, + sl: false, + }, + }, + switchVisible: false, + confirmVisible: false, + }, + mounted() { + var thisvue = this; + axios.post('./api/', { + action: 'get_setting', + csrf_token: csrf_token, + }).then(function (res) { + if (res.data.code == 0) { + thisvue.groupData = res.data.groupData; + thisvue.battle_id = res.data.groupData.battle_id; + thisvue.form.game_server = res.data.groupData.game_server; + thisvue.form.privacy.allow_guest = Boolean(res.data.privacy & 0x1); + thisvue.form.privacy.allow_statistics_api = Boolean(res.data.privacy & 0x2); + document.title = res.data.groupData.group_name + ' - 公会战设置'; + var notify_code = res.data.notification; + for (key in thisvue.form.notify) { + thisvue.form.notify[key] = Boolean(notify_code & 1); + notify_code >>= 1; + } + } else { + thisvue.$alert(res.data.message, '加载数据失败'); + } + }).catch(function (error) { + thisvue.$alert(error, '加载数据失败'); + }); + }, + methods: { + submit: function (event) { + var thisvue = this; + var privacy = (thisvue.form.privacy.allow_guest * 0x1) + (thisvue.form.privacy.allow_statistics_api * 0x2); + var notify_code = 0; + var magnitude = 1; + for (key in thisvue.form.notify) { + notify_code += thisvue.form.notify[key] * magnitude; + magnitude <<= 1; + } + axios.post('./api/', { + action: 'put_setting', + csrf_token: csrf_token, + game_server: thisvue.form.game_server, + privacy: privacy, + notification: notify_code, + }).then(function (res) { + if (res.data.code == 0) { + thisvue.$notify({ + title: '通知', + message: '设置成功', + }); + } else { + thisvue.$alert(res.data.message, '保存设置失败'); + } + }).catch(function (error) { + thisvue.$alert(error, '保存设置失败'); + }); + }, + export_data: function (event) { + window.location = '../statistics/api/'; + }, + call_api: function (payload) { + var thisvue = this; + payload.csrf_token = csrf_token; + axios.post('./api/', payload).then(function (res) { + if (res.data.code == 0) { + thisvue.$notify({ + title: '通知', + message: '成功', + }); + } else { + thisvue.$alert(res.data.message, '失败'); + } + }).catch(function (error) { + thisvue.$alert(error, '失败'); + }); + }, + clear_data_slot: function (event) { + this.call_api({ + action: 'clear_data_slot', + }); + this.confirmVisible = false; + }, + // new_data_slot: function (event) { + // this.call_api({ + // action: 'new_data_slot', + // }); + // }, + switch_data_slot: function (event) { + this.call_api({ + action: 'switch_data_slot', + battle_id: this.battle_id, + }); + this.switchVisible = false; + }, + get_data_slot_record_count: function () { + if (this.data_slot_record_count.length !== 0) { + return + } + var thisvue = this; + axios.post('./api/', { + action: 'get_data_slot_record_count', + csrf_token: csrf_token, + }).then(function (res) { + if (res.data.code == 0) { + thisvue.data_slot_record_count = res.data.counts; + } else { + thisvue.$alert(res.data.message, '失败'); + } + }).catch(function (error) { + thisvue.$alert(error, '失败'); + }); + }, + handleSelect(key, keyPath) { + switch (key) { + case '1': + window.location = '../'; + break; + case '2': + window.location = '../subscribers/'; + break; + case '3': + window.location = '../progress/'; + break; + case '4': + window.location = '../statistics/'; + break; + case '5': + window.location = `../my/`; + break; + } + }, + }, + delimiters: ['[[', ']]'], +}) \ No newline at end of file diff --git a/kokkoro/web/static/clan/statistics/pie.png b/kokkoro/web/static/clan/statistics/pie.png new file mode 100644 index 0000000..e16430b Binary files /dev/null and b/kokkoro/web/static/clan/statistics/pie.png differ diff --git a/kokkoro/web/static/clan/statistics/statistics2.js b/kokkoro/web/static/clan/statistics/statistics2.js new file mode 100644 index 0000000..5bd7d75 --- /dev/null +++ b/kokkoro/web/static/clan/statistics/statistics2.js @@ -0,0 +1,1095 @@ +/* +* TODO: +* 公会伤害成长曲线 +* 公会成员成长曲线合图 +* 各个玩家与BOSS均伤偏差值百分比统计 +*/ + +const numberFormatter = num => { + if (num < 10000) + return `${num.toLocaleString()}` + if (num < 100000000) + return `${(num / 10000).toLocaleString()} W` + return `${(num / 100000000).toLocaleString()} E` +} + +if (!Object.defineProperty) { + alert('浏览器版本过低'); +} +// 这代码很乱,到时候重构一下 +var vm = new Vue({ + el: '#app', + data: { + members: [], + range: '', + pickerOptions: { + shortcuts: [{ + text: '最近一周', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近半个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 15); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近三个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一年', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 365); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + picker.$emit('pick', [start, end]); + } + }] + }, + allChallenges: [], + activeIndex: '4', + challengeMap: {}, + challenges: [], + playerDamages: {}, + totalDamage: {}, + containTailAndContinue: true, + globalTableData: [], + playerData: { + damage: [ + + ], + }, + colorList: ['#c23531','#2f4554', '#61a0a8', '#d48265', '#91c7ae','#749f83', '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3'], + challengeChart: null, + bossDmgChart: null, + missChart: null, + lastChart: null, + bossBloodChart: null, + totalTimeChart: null, + bossHitChart: null, + personalProgressChart: null, + personalTimeChart: null, + totalDamageChart: null, + isLoading: true, + selectingTab: "table", + selectinguid: parseInt(uid) + }, + mounted() { + this.bossDmgChart = echarts.init(document.getElementById("bossDmgChart")); + this.challengeChart = echarts.init(document.getElementById("challengeChart")); + this.sumDmgChart = echarts.init(document.getElementById("sumDmgChart")); + this.missChart = echarts.init(document.getElementById("missChart")); + this.lastChart = echarts.init(document.getElementById("lastChart")); + this.bossBloodChart = echarts.init(document.getElementById("bossBloodChart")); + this.totalTimeChart = echarts.init(document.getElementById("totalTimeChart")); + this.bossHitChart = echarts.init(document.getElementById("bossHitChart")); + this.personalProgressChart = echarts.init(document.getElementById("personalProgressChart")); + this.personalTimeChart = echarts.init(document.getElementById("personalTimeChart")); + this.totalDamageChart = echarts.init(document.getElementById("totalDamageChart")); + this.selectingTab = "total"; + this.fetchData(); + }, + watch: { + containTailAndContinue: function() { + this.init(); + }, + selectinguid: function() { + this.initChart(this.playerDamage(this.selectinguid).bossDamageList); + this.initPlayerData(); + }, + selectingTab: function() { + this.init(); + setTimeout("vm.resizeAll()", 100); + }, + range: function() { + this.refreshData(); + this.init(); + }, + }, + + methods: { + // tools function + sum: (iterable) => { + let sum = 0; + iterable.forEach(v => sum += v); + return sum; + }, + formatTo2: (num) => { return (num >= 10) ? num.toString() : '0' + num.toString() }, + + fetchData: function() { + const that = this; + axios.get('../api/').then(res=> { + if (res.data.code != 0) { + that.$alert(res.data.message, '获取记录失败'); + that.isLoading = false; + return; + } + that.allChallenges = res.data.challenges; + that.members = res.data.members; + if (that.members.filter((elem) => {return elem.uid == that.selectinguid}).length == 0) { + that.selectinguid = that.members[0].uid; + } + that.refreshData(); + }).catch(function (error) { + that.$alert(error, '获取数据失败,请联系维护人员'); + that.isLoading = false; + console.error(error); + console.error(error.stack); + }); + }, + + filtedChallenge: function() { + if (!this.range) return this.allChallenges; + const leftRange = this.range[0].getTime() / 1000 + 18000; + const rightRange = this.range[1].getTime() / 1000 + 18000 + 86400; + return this.allChallenges.filter((elem) => {return elem.challenge_time <= rightRange && elem.challenge_time >= leftRange}); + }, + + refreshData: function() { + this.challenges = this.filtedChallenge(); + this.challengeMap = {}; + for (let challenge of this.challenges) { + let arr = this.challengeMap[challenge.uid]; + if (arr == undefined) { + arr = []; + this.challengeMap[challenge.uid] = arr; + } + arr.push(challenge); + } + for (let member of this.members) { + member.challenges = this.challengeMap[member.uid]; + } + this.sortAndDivide(); + this.init(); + this.isLoading = false; + }, + + init: function() { + this.initTotalDamage(); + this.initPlayerDamage(); + this.initGlobalTableData(); + this.initPlayerData(); + if (this.selectingTab === 'total') { + this.initChart(this.totalDamage.bossDamageList); + } + else if (this.selectingTab === 'channel') { + this.initChart(this.totalDamage.bossDamageList); + } + else { + this.initChart(this.playerDamage(this.selectinguid).bossDamageList); + } + }, + // function for init + initChart: function(bossDamageList) { + let temp = this.bossAverageDamageForChart(bossDamageList, this.containTailAndContinue); + let option = { + title: { + text: '不同 Boss 刀均伤害' + }, + tooltip: {}, + legend: { + data: ['伤害'] + }, + xAxis: { + data: temp[0], + }, + yAxis: { + axisLabel: { + formatter: numberFormatter + } + }, + series: [{ + name: '伤害', + type: 'bar', + data: temp[1], + itemStyle: { + color: (params) => { + let bossId = parseInt(temp[0][params.dataIndex][0]) - 1; + return this.colorList[bossId]; + }, + } + }] + } + + let temp2 = this.bossChallengeCountForChart(bossDamageList, true); + let option2 = { + title: { + text: '不同 Boss 出刀数' + }, + tooltip: {}, + series: [{ + type: 'pie', + center: ['50%', '50%'], + data: temp2, + itemStyle: { + color: (params) => { + let bossId = parseInt(temp2[params.dataIndex].name[0]) - 1; + return this.colorList[bossId]; + } + } + }] + } + let temp3 = this.bossSumDamageForChart(bossDamageList); + let option3 = { + title: { + text: 'Boss 总伤害' + }, + tooltip: {}, + legend: { + data: ['伤害'] + }, + xAxis: { + data: temp3[0] + }, + yAxis: { + axisLabel: { + formatter: numberFormatter + } + }, + series: [{ + name: '伤害', + type: 'bar', + data: temp3[1], + itemStyle: { + color: (params) => { + let bossId = parseInt(temp3[0][params.dataIndex][0]) - 1; + return this.colorList[bossId]; + }, + } + }] + } + let temp4 = this.bossMissForChart(this.globalTableData); + let option4 = { + title: { + text: '成员出刀考勤' + }, + tooltip: {}, + legend: { + data: ['次数'] + }, + // grid: { + // bottom: 60 + // }, + xAxis: { + type: 'category', + data: temp4[0], + axisLabel: { + interval: 0, + rotate: 45 + } + }, + yAxis: { + type: 'value', + max: Math.max.apply(temp4[1]), + min: 0 + }, + series: [{ + name: '出刀次数', + data: temp4[1], + type: 'bar', + showBackground: true, + backgroundStyle: { + color: 'rgba(220, 220, 220, 0.8)' + } + }] + } + let temp5 = this.bossLastForChart(); + let option5 = { + title: { + text: '尾刀统计', + }, + tooltip: { + trigger: 'item', + formatter: '{a}
{b} : {c} ({d}%)' + }, + series: [ + { + name: '次数', + type: 'pie', + radius: '55%', + data: temp5, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ] + }; + let temp6 = this.bossBloodForChart(); + let option6 = { + title: { + text: 'BOSS血量曲线', + }, + grid: { + bottom: 80 + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + animation: false, + label: { + formatter: params => { + if (params.axisDimension === "x") { + return (new Date(params.value)).toLocaleString(options = { timeZone: 'asia/shanghai' }); + } + if (params.axisDimension === "y") { + return params.value.toLocaleString(); + } + return params.value; + } + } + }, + formatter: (params) => { + const series = params[0]; + const [ts, value] = series.data; + const matched = temp6[1].find(f => (!f.gte || f.gte <= ts) && (!f.lt || f.lt > ts)); + return `${(new Date(ts)).toLocaleString(options = { timeZone: 'asia/shanghai' })}
${series.marker}${(matched && matched.label) + "
" || ""}血量:${value.toLocaleString()}` + } + }, + toolbox: { + show: true, + feature: { + dataZoom: { + yAxisIndex: 'none' + }, + restore: {}, + saveAsImage: {} + } + }, + xAxis: { + type: 'time', + boundaryGap: false, + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: numberFormatter + }, + axisPointer: { + snap: true + } + }, + dataZoom: [ + { + type: 'inside' + }, + { + show: true, + realtime: true, + handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z', + handleSize: '80%', + handleStyle: { + color: '#fff', + shadowBlur: 3, + shadowColor: 'rgba(0, 0, 0, 0.6)', + shadowOffsetX: 2, + shadowOffsetY: 2 + }, + } + ], + visualMap: { + type: "piecewise", + show: false, + dimension: 0, + seriesIndex: 0, + pieces: temp6[1] + }, + series: [ + { + name: '血量', + type: 'line', + // smooth: true, + data: temp6[0], + areaStyle: {}, + } + ] + }; + let temp7 = this.timeForChart(this.challenges); + let option7 = { + title: { + text: '出刀时间', + }, + tooltip: { + trigger: 'axis', + axisPointer: { + animation: true, + label: { + backgroundColor: '#505765' + } + } + }, + xAxis: { + type: 'category', + boundaryGap: true, + axisLine: {onZero: false}, + data: [...Array(24).keys()].map(i => `${i}时`), + }, + yAxis: { + name: '刀', + type: 'value' + }, + visualMap: [{ + type: "piecewise", + show: false, + dimension: 0, + seriesIndex: 0, + pieces: [ + {lte: 5, label: '凌晨', color: 'grey'}, + {gt: 5,lte: 12, label: '上午', color: '#9cc5b0'}, + {gt: 12,lte: 18, label: '下午', color: '#c54730'}, + {gt: 18, label: '晚上', color: '#384b5a'}, + ] + }], + series: { + name: '刀数', + type: 'bar', + animation: true, + lineStyle: { + width: 2 + }, + data: temp7 + } + } + let temp8 = this.bossPlayerHitCountForChart(); + let option8 = { + title: { + text: '成员BOSS出刀数' + }, + tooltip: { + position: 'top' + }, + animation: true, + xAxis: { + type: 'category', + data: temp8[1], + splitArea: { + show: true + }, + axisLabel: { + interval: 0, + rotate: 45 + } + }, + yAxis: { + type: 'category', + data: temp8[0], + splitArea: { + show: true + } + }, + visualMap: { + min: 0, + max: 10, + calculable: true, + orient: 'horizontal', + left: 'center', + top: 0 + }, + series: [{ + name: 'Punch Card', + type: 'heatmap', + data: temp8[2], + label: { + show: true + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + }] + }; + let temp9 = this.membersDamageForChart(this.globalTableData); + let option9 = { + title: { + text: '成员伤害统计' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + crossStyle: { + color: '#999' + } + } + }, + toolbox: { + feature: { + dataView: {show: true, readOnly: false}, + magicType: {show: true, type: ['line', 'bar']}, + restore: {show: true}, + saveAsImage: {show: true} + } + }, + legend: { + data: ['总伤害', '刀均伤害'] + }, + xAxis: [ + { + type: 'category', + data: temp9[0], + axisPointer: { + type: 'shadow' + }, + axisLabel: { + interval: 0, + rotate: 45 + }, + boundaryGap: true, + } + ], + yAxis: [ + { + type: 'value', + name: '总伤害', + axisLabel: { + formatter: numberFormatter + } + }, + { + type: 'value', + name: '刀均伤害', + axisLabel: { + formatter: numberFormatter + } + } + ], + series: [ + { + name: '总伤害', + type: 'bar', + data: temp9[1] + }, + { + name: '刀均伤害', + type: 'bar', + yAxisIndex: 1, + data: temp9[2] + } + ] + }; + + + this.bossDmgChart.setOption(option); + this.challengeChart.setOption(option2); + this.sumDmgChart.setOption(option3); + this.missChart.setOption(option4); + this.lastChart.setOption(option5); + try{ + // 这里有时会出现一个错误,原因未知 + this.bossBloodChart.setOption(option6); + }catch(e){console.error(e)} + this.totalTimeChart.setOption(option7); + this.bossHitChart.setOption(option8); + this.totalDamageChart.setOption(option9); + }, + + resizeAll: function() { + this.sumDmgChart.resize(); + this.missChart.resize(); + this.lastChart.resize(); + this.bossBloodChart.resize(); + this.totalTimeChart.resize(); + this.personalProgressChart.resize(); + this.personalTimeChart.resize(); + this.bossHitChart.resize(); + this.totalDamageChart.resize(); + }, + + initPlayerData: function() { + let max = 0, min = 2147483647, s = [0, 0, 0], c = [0, 0, 0]; + let pchallenge = this.challengeMap[this.selectinguid]; + if (pchallenge != undefined) { + for (let date in pchallenge) { + let clist = pchallenge[date]; + let dmglist = [] + for (let i = 0; i < clist.length; i++) { + let damage = 0; + if (clist[i].health_ramain != 0) { + damage = clist[i].damage; + } + else if (clist[i+1] && clist[i+1].is_continue) { + damage = clist[i].damage + clist[i+1].damage + i++; + } + if (max < damage) max = damage; + if (min > damage) min = damage; + dmglist.push(damage); + dmglist.sort((a, b) => {return b - a}); + } + for (let i = 0; i < dmglist.length; i++) { + s[i] += dmglist[i]; + c[i]++; + } + } + this.playerData.damage = [ + {label: '最高单次伤害', value: max}, + {label: '最低单次伤害', value: min}, + {label: '伤害最高刀均伤害', value: Math.floor(s[0] / c[0])}, + {label: '伤害次高刀均伤害', value: Math.floor(s[1] / c[1])}, + {label: '伤害最低刀均伤害', value: Math.floor(s[2] / c[2])} + ] + } else { + this.playerData.damage = [ + {label: '最高单次伤害', value: 0}, + {label: '最低单次伤害', value: 0}, + {label: '伤害最高刀均伤害', value: 0}, + {label: '伤害次高刀均伤害', value: 0}, + {label: '伤害最低刀均伤害', value: 0} + ] + } + const playerChalls = this.challenges.filter(c => c.uid == this.selectinguid); + const param1 = this.timeForChart(playerChalls); + const option1 = { + title: { + text: '出刀时间', + }, + tooltip: { + trigger: 'axis', + axisPointer: { + animation: true, + label: { + backgroundColor: '#505765' + } + } + }, + xAxis: { + type: 'category', + boundaryGap: true, + axisLine: {onZero: false}, + data: [...Array(24).keys()].map(i => `${i}时`), + }, + yAxis: { + name: '刀', + type: 'value' + }, + visualMap: [{ + type: "piecewise", + show: false, + dimension: 0, + seriesIndex: 0, + pieces: [ + {lte: 5, label: '凌晨', color: 'grey'}, + {gt: 5,lte: 12, label: '上午', color: '#9cc5b0'}, + {gt: 12,lte: 18, label: '下午', color: '#c54730'}, + {gt: 18, label: '晚上', color: '#384b5a'}, + ] + }], + series: { + name: '刀数', + type: 'bar', + + animation: true, + lineStyle: { + width: 2 + }, + data: param1 + } + }; + const param2 = this.dayDamageForChart(playerChalls); + const option2 = { + title: { + text: "伤害成长曲线" + }, + xAxis: { + type: 'category', + boundaryGap: false + }, + yAxis: { + type: 'value', + scale: true, + axisLabel: { + formatter: numberFormatter + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + animation: true, + } + }, + series: [ + { + type: 'line', + name: '三刀总伤害', + smooth: 0.6, + symbolSize: 10, + color: 'green', + lineStyle: { + width: 5 + }, + data: param2 + } + ] + }; + + this.personalTimeChart.setOption(option1) + this.personalProgressChart.setOption(option2) + + }, + + initTotalDamage: function() { + this.totalDamage = []; + let bossDamageList = {}; + let result = {normalDamage: [], continueDamage: [], tailDamage: [], count: 0, countContinue: 0, countTail: 0}; + for (let challenge of this.challenges) { + let dict = bossDamageList[challenge.boss_num]; + if (dict == undefined) { + dict = {normalDamage: [], continueDamage: [], tailDamage: [], count: 0, countContinue: 0, countTail: 0} + bossDamageList[challenge.boss_num] = dict; + } + let damage = challenge.damage; + if (challenge.health_ramain == 0) { + result.tailDamage.push(damage); + result.countTail++; + dict.tailDamage.push(damage); + dict.countTail++; + continue; + } + if (challenge.is_continue) { + result.continueDamage.push(damage); + result.countContinue++; + dict.continueDamage.push(damage); + dict.countContinue++; + continue; + } + result.normalDamage.push(damage); + result.count++; + dict.normalDamage.push(damage); + dict.count++; + } + result.bossDamageList = bossDamageList; + this.totalDamage = result; + }, + + initPlayerDamage: function () { + for (const elem of this.members) { + const playeruid = elem.uid; + let challenges = this.challengeMap[playeruid]; + let bossDamageList = {}; + let result = {normalDamage: [], continueDamage: [], tailDamage: [], count: 0, countContinue: 0, countTail: 0}; + for (let day in challenges) { + for (let challenge of challenges[day]) { + let dict = bossDamageList[challenge.boss_num]; + if (dict == undefined) { + dict = {normalDamage: [], continueDamage: [], tailDamage: [], count: 0, countContinue: 0, countTail: 0} + bossDamageList[challenge.boss_num] = dict; + } + let damage = challenge.damage; + if (challenge.health_ramain == 0) { + result.tailDamage.push(damage); + result.countTail++; + dict.tailDamage.push(damage); + dict.countTail++; + continue; + } + if (challenge.is_continue) { + result.continueDamage.push(damage); + result.countContinue++; + dict.continueDamage.push(damage); + dict.countContinue++; + continue; + } + result.normalDamage.push(damage); + result.count++; + dict.normalDamage.push(damage); + dict.count++; + } + } + result.bossDamageList = bossDamageList; + this.playerDamages[playeruid] = result; + } + }, + + initGlobalTableData: function () { + this.globalTableData = []; + for (const member of this.members) { + const sum = this.totalSumDamage(); + const pdmg = this.playerDamage(member.uid); + const playerSum = this.playerSumDamage(member.uid); + const tempAvgDmg = this.playerAverageDamage(member.uid, this.containTailAndContinue); + const tempSumDmgRate = (100 * playerSum / sum); + let dict = { + uid: member.uid, + nickname: member.nickname, + count: pdmg.count + pdmg.countContinue / 2 + pdmg.countTail / 2, + countContinue: pdmg.countContinue, + countTail: pdmg.countTail, + avgDmg: isNaN(tempAvgDmg) ? 0 : tempAvgDmg, + sumDmg: playerSum, + sumDmgRate: isNaN(tempSumDmgRate) ? '--' : tempSumDmgRate.toFixed(2) + '%' + } + this.globalTableData.push(dict); + } + }, + + tsToDay: function (ts) { + // 减去5点 + let date = new Date((ts - 18000) * 1000); + return date.getFullYear() + '-' + this.formatTo2(date.getMonth() + 1) + '-' + this.formatTo2(date.getDate()); + }, + sortChallengeByTime: function(c1, c2) { + return c1.challenge_time - c2.challenge_time; + }, + sortAndDivide: function() { + for (let m of this.members) { + let detail = {}; + let challenges = m.challenges; + if (!challenges) { + continue; + } + for (let challenge of challenges) { + if (detail[this.tsToDay(challenge.challenge_time)] == undefined) { + detail[this.tsToDay(challenge.challenge_time)] = []; + } + detail[this.tsToDay(challenge.challenge_time)].push(challenge); + } + for (let key in detail) { + detail[key].sort(this.sortChallengeByTime); + } + m.challenges = detail; + this.challengeMap[m.uid] = detail; + } + }, + + totalAverageDamage: function(containTailAndContinue = false) { + return this.averageDamage(this.totalDamage, containTailAndContinue); + }, + playerAverageDamage: function(playeruid, containTailAndContinue = false) { + return this.averageDamage(this.playerDamage(playeruid), containTailAndContinue); + }, + + bossAverageDamageForChart: function(bossDamageList, containTailAndContinue = false) { + let l1 = [], l2 = []; + for (let index in bossDamageList) { + let damage = bossDamageList[index]; + let ret = this.averageDamage(damage, containTailAndContinue); + if (!isNaN(ret)) { + l1.push(index + "号Boss"); + l2.push(ret); + } + } + return [l1, l2]; + }, + bossMissForChart: function(globalTableData) { + const counts = globalTableData.map(elem => elem.count); + const names = globalTableData.map(elem => elem.nickname); + return [names, counts]; + }, + bossLastForChart: function() { + const map = {}; + for (const i in this.challenges) { + if (this.challenges[i].is_continue) { + const name = this.getPlayer(this.challenges[i].uid).nickname; + if (name in map) + map[name] += 1; + else + map[name] = 1; + } + } + return Object.keys(map).map(name => ({name: name, value: map[name]})); + }, + bossBloodForChart: function() { + const challs = this.challenges.sort((a, b) => a.challenge_time - b.challenge_time); + let bosses = []; + let nowBoss, lastPosition, lastCircle; + for (const i in challs) { + if (nowBoss === undefined) + nowBoss = challs[i].boss_num; + if (lastPosition === undefined) + lastPosition = challs[i].challenge_time * 1000; + if (lastCircle === undefined) + lastCircle = challs[i].cycle; + if (challs[i].boss_num !== nowBoss) { + const time = challs[i].challenge_time * 1000; + bosses.push({ + gte: lastPosition, + lt: time, + color: this.colorList[nowBoss - 1], + label: `${lastCircle}周目${nowBoss}王` + }); + nowBoss = challs[i].boss_num; + lastPosition = time; + lastCircle = challs[i].cycle; + } + } + if (nowBoss && lastPosition) { + bosses.push({ + gte: lastPosition, + color: this.colorList[nowBoss - 1], + label: `${lastCircle}周目${nowBoss}王` + }); + } + return [challs.map(c => [c.challenge_time * 1000, c.health_ramain]), bosses]; + }, + bossSumDamageForChart: function(bossDamageList) { + let l1 = [], l2 = []; + for (let index in bossDamageList) { + let damage = bossDamageList[index]; + let ret = this.sumDamage(damage); + if (!isNaN(ret)) { + l1.push(index + "号Boss"); + l2.push(ret); + } + } + return [l1, l2]; + }, + bossChallengeCountForChart: function(bossDamageList, containTailAndContinue = false) { + let l1 = [] + for (let index in bossDamageList) { + let damage = bossDamageList[index]; + let ret = damage.count + (containTailAndContinue ? (damage.countContinue + damage.countTail) / 2 : 0); + if (ret != 0) l1.push({name: index + "号Boss", value: ret}); + } + return l1; + }, + bossPlayerHitCountForChart: function() { + const names = [], counter = {}; + const hanzi = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"] + const maxBossNum = Math.max.apply(Math, [0, ...this.challenges.map(c => c.boss_num)]); + const bosses = [...Array(maxBossNum || 0).keys()].map(k => `${k in hanzi ? hanzi[k] : k}王`).reverse() + + this.challenges.forEach(c => { + const boss = c.boss_num; + const name = this.getPlayer(c.uid).nickname; + if (!(boss in counter)) { + counter[boss] = {} + } + const bossCount = counter[boss]; + if (!(name in bossCount)) { + bossCount[name] = 0 + } + const isFull = c.health_ramain && !c.is_continue; + bossCount[name] += isFull ? 1 : 0.5; + }) + const result = []; + const getNicknameIndex = name => { + if (!names.includes(name)) + names.push(name) + return names.findIndex(n => n === name); + } + const getBossIndex = num => maxBossNum - parseInt(num); + Object.keys(counter).forEach(num => { + Object.keys(counter[num]).forEach(name => { + result.push([ + getBossIndex(num), + getNicknameIndex(name), + counter[num][name] + ]) + }) + }) + return [ + bosses, + names, + result.map(function (item) { + return [item[1], item[0], item[2] || '-']; + }) + ]; + }, + timeForChart: function(challenges) { + const time = {}; + [...Array(24).keys()].forEach(i => time[i] = 0); + for (const i in challenges) { + const t = new Date(challenges[i].challenge_time * 1000); + time[t.getHours()] += 1; + } + return Object.values(time); + }, + dayDamageForChart: function(challenges) { + const dates = {}; + challenges.forEach(c => { + const date = this.tsToDay(c.challenge_time); + if (date in dates) { + dates[date] += c.damage; + } else { + dates[date] = c.damage; + } + }); + return Object.entries(dates).sort( + (a, b) => + new Date(a[0]) - new Date(b[0]) + ); + }, + + membersDamageForChart: function(globalTableData) { + const data = globalTableData.sort((a, b) => b.sumDmg - a.sumDmg); + const full = data.map(elem => elem.sumDmg); + const average = data.map(elem => elem.avgDmg); + const names = data.map(elem => elem.nickname); + return [names, full, average]; + }, + + averageDamage: function(damage, containTailAndContinue) { + let sum = this.sum(damage.normalDamage); + let count = damage.count; + if (containTailAndContinue) { + sum += this.sum(damage.continueDamage) + this.sum(damage.tailDamage); + count += (damage.countContinue + damage.countTail) / 2; + } + return Math.floor(sum / count); + }, + + totalSumDamage: function() { + return this.sumDamage(this.totalDamage); + }, + playerSumDamage: function(playeruid) { + return this.sumDamage(this.playerDamage(playeruid)); + }, + sumDamage: function(damage) { + return this.sum(damage.normalDamage) + this.sum(damage.continueDamage) + this.sum(damage.tailDamage); + }, + + challengeCount: function(damage, containTailAndContinue) { + return damage.count + (containTailAndContinue ? (damage.countTail + damage.countContinue) / 2 : 0); + }, + + getPlayer: function(uid) { + return this.members.find(o => o.uid === uid) || {nickname:'未加入',uid:uid,sl:null}; + }, + + playerDamage: function(playeruid) { + return this.playerDamages[playeruid]; + }, + getToday: function () { + let d = new Date(); + d -= 18000000; + d = new Date(d).setHours(0, 0, 0, 0); + return d; + }, + }, + delimiters: ['[[', ']]'], +}) \ No newline at end of file diff --git a/kokkoro/web/static/clan/user.js b/kokkoro/web/static/clan/user.js new file mode 100644 index 0000000..efca6ac --- /dev/null +++ b/kokkoro/web/static/clan/user.js @@ -0,0 +1,136 @@ +var gs_offset = { jp: 4, tw: 5, kr: 4, cn: 5 }; +function pad2(num) { + return String(num).padStart(2, '0'); +} +function ts2ds(timestamp) { + var d = new Date(); + d.setTime(timestamp * 1000); + return d.getFullYear() + '/' + pad2(d.getMonth() + 1) + '/' + pad2(d.getDate()); +} +var vm = new Vue({ + el: '#app', + data: { + isLoading: true, + challengeData: [], + activeIndex: '5', + uid: 0, + nickname: '', + }, + mounted() { + var thisvue = this; + var pathname = window.location.pathname.split('/'); + thisvue.uid = parseInt(pathname[pathname.length - 2]); + axios.post('../api/', { + action: 'get_user_challenge', + csrf_token: csrf_token, + target_uid: thisvue.uid, + }).then(function (res) { + if (res.data.code != 0) { + thisvue.$alert(res.data.message, '获取记录失败'); + return; + } + thisvue.nickname = res.data.user_info.nickname; + thisvue.refresh(res.data.challenges, res.data.game_server); + thisvue.isLoading = false; + }).catch(function (error) { + thisvue.$alert(error, '获取数据失败'); + }); + }, + methods: { + csummary: function (cha) { + if (cha == undefined) { + return ''; + } + return `(${cha.cycle}-${cha.boss_num}) ${cha.damage}`; + }, + cdetail: function (cha) { + if (cha == undefined) { + return ''; + } + var nd = new Date(); + nd.setTime(cha.challenge_time * 1000); + var detailstr = nd.toLocaleString('chinese', { hour12: false, timeZone: 'asia/shanghai' }) + '\n'; + detailstr += cha.cycle + '周目' + cha.boss_num + '号boss\n'; + detailstr += (cha.health_ramain + cha.damage).toLocaleString(options = { timeZone: 'asia/shanghai' }) + '→' + cha.health_ramain.toLocaleString(options = { timeZone: 'asia/shanghai' }); + if (cha.message) { + detailstr += '\n留言:' + cha.message; + } + return detailstr; + }, + arraySpanMethod: function ({ row, column, rowIndex, columnIndex }) { + if (columnIndex >= 2) { + if (columnIndex % 2 == 0) { + var detail = row.detail[columnIndex - 2]; + if (detail != undefined && detail.health_ramain != 0) { + return [1, 2]; + } + } else { + var detail = row.detail[columnIndex - 3]; + if (detail != undefined && detail.health_ramain != 0) { + return [0, 0]; + } + } + } + }, + refresh: function (challenges, game_server) { + var thisvue = this; + var m = { pcrdate: -1 }; + for (c of challenges) { + var pcrdate = ts2ds(c.challenge_time - (gs_offset[game_server] * 3600)); + if (m.pcrdate != pcrdate) { + if (m.pcrdate != -1) { + thisvue.challengeData.push(m); + } + m = { + pcrdate: pcrdate, + finished: 0, + detail: [], + } + } + m.detail[2 * m.finished] = c; + if (c.is_continue) { + m.finished += 0.5; + } else { + if (c.health_ramain != 0) { + m.finished += 1; + } else { + m.finished += 0.5; + } + } + } + if (m.pcrdate != -1) { + thisvue.challengeData.push(m); + } + }, + viewInExcel: function () { + var icons = document.getElementsByTagName('span'); + while (icons[0]) { + icons[0].remove(); + } + var uri = 'data:application/vnd.ms-excel;base64,'; + var ctx = '' + document.getElementsByTagName('thead')[0].innerHTML + document.getElementsByTagName('tbody')[0].innerHTML + '
'; + window.location.href = uri + window.btoa(unescape(encodeURIComponent(ctx))); + document.documentElement.innerHTML = '请在Excel中查看(如果无法打开,请安装最新版本Excel)\n或者将整个表格复制,粘贴到Excel中使用'; + }, + handleTitleSelect(key, keyPath) { + switch (key) { + case '1': + window.location = '../'; + break; + case '2': + window.location = '../subscribers/'; + break; + case '3': + window.location = '../progress/'; + break; + case '4': + window.location = '../statistics/'; + break; + case '5': + window.location = `../my/`; + break; + } + }, + }, + delimiters: ['[[', ']]'], +}) diff --git a/kokkoro/web/static/password.js b/kokkoro/web/static/password.js new file mode 100644 index 0000000..96f13a3 --- /dev/null +++ b/kokkoro/web/static/password.js @@ -0,0 +1,344 @@ +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined + * in FIPS 180-2 + * Version 2.2 Copyright Angel Marin, Paul Johnston 2000 - 2009. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + * Also http://anmar.eu.org/projects/jssha2/ + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha256(s) { return rstr2hex(rstr_sha256(str2rstr_utf8(s))); } +function b64_sha256(s) { return rstr2b64(rstr_sha256(str2rstr_utf8(s))); } +function any_sha256(s, e) { return rstr2any(rstr_sha256(str2rstr_utf8(s)), e); } +function hex_hmac_sha256(k, d) + { return rstr2hex(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d))); } +function b64_hmac_sha256(k, d) + { return rstr2b64(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d))); } +function any_hmac_sha256(k, d, e) + { return rstr2any(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d)), e); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha256_vm_test() +{ + return hex_sha256("abc").toLowerCase() == + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; +} + +/* + * Calculate the sha256 of a raw string + */ +function rstr_sha256(s) +{ + return binb2rstr(binb_sha256(rstr2binb(s), s.length * 8)); +} + +/* + * Calculate the HMAC-sha256 of a key and some data (raw strings) + */ +function rstr_hmac_sha256(key, data) +{ + var bkey = rstr2binb(key); + if(bkey.length > 16) bkey = binb_sha256(bkey, key.length * 8); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = binb_sha256(ipad.concat(rstr2binb(data)), 512 + data.length * 8); + return binb2rstr(binb_sha256(opad.concat(hash), 512 + 256)); +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) +{ + try { hexcase } catch(e) { hexcase=0; } + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var output = ""; + var x; + for(var i = 0; i < input.length; i++) + { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt( x & 0x0F); + } + return output; +} + +/* + * Convert a raw string to a base-64 string + */ +function rstr2b64(input) +{ + try { b64pad } catch(e) { b64pad=''; } + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var output = ""; + var len = input.length; + for(var i = 0; i < len; i += 3) + { + var triplet = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i+2) : 0); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); + } + } + return output; +} + +/* + * Convert a raw string to an arbitrary string encoding + */ +function rstr2any(input, encoding) +{ + var divisor = encoding.length; + var remainders = Array(); + var i, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for(i = 0; i < dividend.length; i++) + { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. We stop when the dividend is zero. + * All remainders are stored for later use. + */ + while(dividend.length > 0) + { + quotient = Array(); + x = 0; + for(i = 0; i < dividend.length; i++) + { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if(quotient.length > 0 || q > 0) + quotient[quotient.length] = q; + } + remainders[remainders.length] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ""; + for(i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + /* Append leading zero equivalents */ + var full_length = Math.ceil(input.length * 8 / + (Math.log(encoding.length) / Math.log(2))) + for(i = output.length; i < full_length; i++) + output = encoding[0] + output; + + return output; +} + +/* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ +function str2rstr_utf8(input) +{ + var output = ""; + var i = -1; + var x, y; + + while(++i < input.length) + { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) + { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); + i++; + } + + /* Encode output as utf-8 */ + if(x <= 0x7F) + output += String.fromCharCode(x); + else if(x <= 0x7FF) + output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), + 0x80 | ( x & 0x3F)); + else if(x <= 0xFFFF) + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + else if(x <= 0x1FFFFF) + output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + } + return output; +} + +/* + * Encode a string as utf-16 + */ +function str2rstr_utf16le(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode( input.charCodeAt(i) & 0xFF, + (input.charCodeAt(i) >>> 8) & 0xFF); + return output; +} + +function str2rstr_utf16be(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, + input.charCodeAt(i) & 0xFF); + return output; +} + +/* + * Convert a raw string to an array of big-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binb(input) +{ + var output = Array(input.length >> 2); + for(var i = 0; i < output.length; i++) + output[i] = 0; + for(var i = 0; i < input.length * 8; i += 8) + output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32); + return output; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2rstr(input) +{ + var output = ""; + for(var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF); + return output; +} + +/* + * Main sha256 function, with its support functions + */ +function sha256_S (X, n) {return ( X >>> n ) | (X << (32 - n));} +function sha256_R (X, n) {return ( X >>> n );} +function sha256_Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} +function sha256_Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} +function sha256_Sigma0256(x) {return (sha256_S(x, 2) ^ sha256_S(x, 13) ^ sha256_S(x, 22));} +function sha256_Sigma1256(x) {return (sha256_S(x, 6) ^ sha256_S(x, 11) ^ sha256_S(x, 25));} +function sha256_Gamma0256(x) {return (sha256_S(x, 7) ^ sha256_S(x, 18) ^ sha256_R(x, 3));} +function sha256_Gamma1256(x) {return (sha256_S(x, 17) ^ sha256_S(x, 19) ^ sha256_R(x, 10));} +function sha256_Sigma0512(x) {return (sha256_S(x, 28) ^ sha256_S(x, 34) ^ sha256_S(x, 39));} +function sha256_Sigma1512(x) {return (sha256_S(x, 14) ^ sha256_S(x, 18) ^ sha256_S(x, 41));} +function sha256_Gamma0512(x) {return (sha256_S(x, 1) ^ sha256_S(x, 8) ^ sha256_R(x, 7));} +function sha256_Gamma1512(x) {return (sha256_S(x, 19) ^ sha256_S(x, 61) ^ sha256_R(x, 6));} + +var sha256_K = new Array +( + 1116352408, 1899447441, -1245643825, -373957723, 961987163, 1508970993, + -1841331548, -1424204075, -670586216, 310598401, 607225278, 1426881987, + 1925078388, -2132889090, -1680079193, -1046744716, -459576895, -272742522, + 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, + -1740746414, -1473132947, -1341970488, -1084653625, -958395405, -710438585, + 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, + 1695183700, 1986661051, -2117940946, -1838011259, -1564481375, -1474664885, + -1035236496, -949202525, -778901479, -694614492, -200395387, 275423344, + 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, + 1537002063, 1747873779, 1955562222, 2024104815, -2067236844, -1933114872, + -1866530822, -1538233109, -1090935817, -965641998 +); + +function binb_sha256(m, l) +{ + var HASH = new Array(1779033703, -1150833019, 1013904242, -1521486534, + 1359893119, -1694144372, 528734635, 1541459225); + var W = new Array(64); + var a, b, c, d, e, f, g, h; + var i, j, T1, T2; + + /* append padding */ + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >> 9) << 4) + 15] = l; + + for(i = 0; i < m.length; i += 16) + { + a = HASH[0]; + b = HASH[1]; + c = HASH[2]; + d = HASH[3]; + e = HASH[4]; + f = HASH[5]; + g = HASH[6]; + h = HASH[7]; + + for(j = 0; j < 64; j++) + { + if (j < 16) W[j] = m[j + i]; + else W[j] = safe_add(safe_add(safe_add(sha256_Gamma1256(W[j - 2]), W[j - 7]), + sha256_Gamma0256(W[j - 15])), W[j - 16]); + + T1 = safe_add(safe_add(safe_add(safe_add(h, sha256_Sigma1256(e)), sha256_Ch(e, f, g)), + sha256_K[j]), W[j]); + T2 = safe_add(sha256_Sigma0256(a), sha256_Maj(a, b, c)); + h = g; + g = f; + f = e; + e = safe_add(d, T1); + d = c; + c = b; + b = a; + a = safe_add(T1, T2); + } + + HASH[0] = safe_add(a, HASH[0]); + HASH[1] = safe_add(b, HASH[1]); + HASH[2] = safe_add(c, HASH[2]); + HASH[3] = safe_add(d, HASH[3]); + HASH[4] = safe_add(e, HASH[4]); + HASH[5] = safe_add(f, HASH[5]); + HASH[6] = safe_add(g, HASH[6]); + HASH[7] = safe_add(h, HASH[7]); + } + return HASH; +} + +function safe_add (x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + + +let salt = "14b492a3-a40a-42fc-a236-e9a9307b47d2"; + +var getHashWithSalt = function (pwd) { + return hex_sha256(pwd + salt) +}; diff --git a/kokkoro/web/template/404.html b/kokkoro/web/template/404.html new file mode 100644 index 0000000..7a15762 --- /dev/null +++ b/kokkoro/web/template/404.html @@ -0,0 +1,14 @@ + + + + + 404 + + + +

404: not found

+

这个{{ item }}不存在

+

返回

+ + + \ No newline at end of file diff --git a/kokkoro/web/template/about.html b/kokkoro/web/template/about.html new file mode 100644 index 0000000..7d58d65 --- /dev/null +++ b/kokkoro/web/template/about.html @@ -0,0 +1,38 @@ + + + + 关于yobot + + + + +

+ 返回 +

+

+ 关于yobot +

+

+ 版本:
+ {{ verinfo | replace("\n", "
") }} +

+

+ 主页:https://yobot.win/ +

+

+ 源码:yobot +

+

+ 联系邮箱:yobot@pcrbot.com +

+

+ 交流群:
1群:770947581
2群:1044314369
4群:1067699252
5群:774394459 +

+

+ 其他 +

+

+ 本项目没有开启赞助、打赏的渠道,所有付款行为与本项目无关。
+ 如果你付费获取了本工具,可能是第三方收取的服务器费用和托管费用。 +

+ \ No newline at end of file diff --git a/kokkoro/web/template/clan/panel.html b/kokkoro/web/template/clan/panel.html new file mode 100644 index 0000000..da3dd84 --- /dev/null +++ b/kokkoro/web/template/clan/panel.html @@ -0,0 +1,219 @@ + + + + 公会战 + + + + + + + + + +
+ + + 面板 + 预约 + 查刀 + 统计 + 我的 + + + Boss状态 + + + [[ bossData.cycle ]]周目 + [[ bossData.num ]]号boss + + + [[ bossData.health.toLocaleString() ]]/[[ bossData.full_health.toLocaleString() ]] + + + + + + {% if is_member -%} + + 上报伤害 + + + + + + + + + + + + + + 上报尾刀 + + + + + + + + + 代理上报 + + + + + + + + + + + + + + + + + + + + + + + + + 撤销上报 + + + 申请出刀 + [[ (bossData.challenger)&&(bossData.challenger!=self_id)?'强制解锁':'解锁' ]] + 锁定boss + + + + + + + + + [[ today_sl?'取消SL':'SL']] + + + 挂树 + + + + + + + + + 取消挂树 + 预约boss + + + + + + + + + + + + 取消预约 + + + + + + + + + + + + + 修改状态 + 设置 + + + + + + + + + + + + + + + + + {% else -%} +

非公会战成员只允许查看

+ {% endif -%} +
+
+
+ + + + + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/progress.html b/kokkoro/web/template/clan/progress.html new file mode 100644 index 0000000..cb8debf --- /dev/null +++ b/kokkoro/web/template/clan/progress.html @@ -0,0 +1,164 @@ + + + + 出刀记录 + + + + + + + + + +
+ + + 面板 + 预约 + 查刀 + 统计 + 我的 + +

出刀记录

+ + 选中未完成 + 提醒出刀 + +

您确定要向[[ multipleSelection.length ]]名成员发送提醒吗

+ + + + + + + + + 取消 + 确定 + +
+ 删除成员 + +

您确定要删除这[[ multipleSelection.length ]]名成员吗

+ + 取消 + 确定 + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/setting.html b/kokkoro/web/template/clan/setting.html new file mode 100644 index 0000000..46b4be9 --- /dev/null +++ b/kokkoro/web/template/clan/setting.html @@ -0,0 +1,92 @@ + + + + 公会战设置 + + + + + + + + +
+ + + 面板 + 预约 + 查刀 + 统计 + 我的 + +

设置

+ + + + + + + + + + + 出刀表无需登录 + 允许api获取数据 + + + 伤害上报 + 撤销上报 + 申请出刀 + 取消申请 + 预约boss + 取消预约 + 挂树 + 取消挂树 + 修改状态 + 使用SL + + + 确定 + 返回 + + + 现在档案编号:[[ battle_id ]]
+ 导出数据 + {#- 新建档案 -#} + 切换档案 + + +
    +
  • + [[ item.battle_id ]]号存档:[[ item.record_count ]]条记录 +
  • +
+ + + + + + + 取消 + 切换 + +
+ 删除数据 + +

此操作会删除 [[ battle_id ]] 号存档中所有数据

+ + 取消 + 确定 + +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/statistics.html b/kokkoro/web/template/clan/statistics.html new file mode 100644 index 0000000..c16df4e --- /dev/null +++ b/kokkoro/web/template/clan/statistics.html @@ -0,0 +1,65 @@ + + + + 公会战 - 数据分析 + + + + +

返回

+

图表

+



+
数据图表
by @Diving-Fish & @Winrey +




+

更多分析

+

>出刀顺序表 by @Ai-Himmel

+

>多维度分析(需要开启api访问) by @Tan90Qian

+ +

+ 原始数据(json格式)
+ 查看本轮 下载本轮
+ 查看全部 下载全部
+ {% if allow_api -%} + api地址: + {% else -%} + api访问已禁用,如需开启请前往公会设置 + {% endif -%} +

+ + + + + + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/statistics/statistics1.html b/kokkoro/web/template/clan/statistics/statistics1.html new file mode 100644 index 0000000..1e34ac8 --- /dev/null +++ b/kokkoro/web/template/clan/statistics/statistics1.html @@ -0,0 +1,94 @@ + + + + 总统计 + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/statistics/statistics2.html b/kokkoro/web/template/clan/statistics/statistics2.html new file mode 100644 index 0000000..ea05e82 --- /dev/null +++ b/kokkoro/web/template/clan/statistics/statistics2.html @@ -0,0 +1,177 @@ + + + + 公会战 - 数据分析 + + + + + + + + + +
+ +

数据分析

+ + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/unauthorized.html b/kokkoro/web/template/clan/unauthorized.html new file mode 100644 index 0000000..7684a88 --- /dev/null +++ b/kokkoro/web/template/clan/unauthorized.html @@ -0,0 +1,14 @@ + + + + + 权限不足 + + + +

权限不足

+

这个公会的信息是私密的,只有其成员可以查看

+

返回

+ + + \ No newline at end of file diff --git a/kokkoro/web/template/clan/user.html b/kokkoro/web/template/clan/user.html new file mode 100644 index 0000000..578ea17 --- /dev/null +++ b/kokkoro/web/template/clan/user.html @@ -0,0 +1,106 @@ + + + + 出刀记录 + + + + + + + + + +
+ + + 面板 + 预约 + 查刀 + 统计 + 我的 + +

[[ nickname ]]的出刀记录

+ + +

查看用户:[[ nickname ]]

+
+ + + + + + \ No newline at end of file diff --git a/kokkoro/web/template/help.html b/kokkoro/web/template/help.html new file mode 100644 index 0000000..cc029c2 --- /dev/null +++ b/kokkoro/web/template/help.html @@ -0,0 +1,231 @@ + + + + 功能表 + + + + + + + +

功能列表

+

此页指令不需要at机器人

+

(权)标记表示功能需要权限

+

(自动)标记表示无需触发词,机器人主动推送

+

方括号表示参数可以省略

+

系统类

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
关键词说明
登录登录进入Web模式
重置密码随机生成一个新密码(不发链接)
更新(权)更新机器人
重启(权)重新启动机器人
退出此群(权:管理员)命令机器人退出当前群聊1
version查看机器人版本
帮助查看帮助
+

+ 1当管理员希望机器人离开群聊时,应该使用此条命令而不是直接踢出群聊,避免增加封号风险 +

+

公会战类

+

注:本类功能仅限群聊

+

公会战功能使用方法请查看手册

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
关键词说明
手册查看公会战使用手册
面板进入公会战面板
创建日服公会日/韩/台/国,创建后可在后台修改
加入公会 [@某人]加入到公会名单,如果有at则为加入他人
加入全部成员
报刀2000000 [@某人] [昨日] [:留言] + 对boss造成伤害但未击败时用,记录伤害,可以使用 200w/200万/2000k 等(一般公会精确到万即可)
+ 如果有at则为代报,有冒号则为留言
+ 如果有“昨日”则将记录添加到前一天1 +
尾刀 [@某人] [昨日] [:留言] + 对boss造成伤害并击败时用,记录伤害
+ 如果有at则为代报,有冒号则为留言
+ 如果有“昨日”则将记录添加到前一天1 +
SL [@某人] [?]挑战boss强制取消后用,记录本日SL2,用“?”查询今日是否已 SL,如果有at则为代报/代查
撤销撤销上一次报刀(非管理员只能撤销自己的记录)
状态显示boss状态
预约1 [:留言]预约boss,当boss出现时通知,有冒号则为留言
挂树 [:留言]挂树,当boss被击败时通知
查1 / 查树查询预约boss的成员,查询挂树的成员
取消预约1 / 取消1取消预约
申请出刀锁定boss,提醒后面申请出刀的人有人正在挑战boss
锁定:留言锁定boss,提醒后面申请出刀的人,冒号后为留言
解锁解锁boss,其他人可以继续申请3
+

+ 1此功能是在日期变更后,将出刀记录添加到前一天。例如日服中03:59完成出刀,在04:01时向机器人报刀则需要加上[昨日]
+ 2在公会战面板的“查刀”页中,可以查看所有人今日是否已使用SL
+ 3此功能既可以用于主动结束锁定,也可以用户强制取消他人的解锁。本人或管理员可以随意解锁,3分钟后“出刀”可以被任何人解锁,“锁定”没有时间限制 +

+

查询类

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
关键词说明
jjc查询 +5个角色名空格分隔查找jjc解法,指定区服可改用“jjc国服/jjc台服/jjc日服”
(自动:新闻推送)推送最新的新闻
日程(今日/明日/x月x日)(可自动)查看活动日程
日程表查看一周日程
挖矿计算 +当前名次计算剩余可获得的奖励钻石
+

娱乐类

+ + + + + + + + + + + + + + + + + + + + + + + + + +
关键词说明
十连
仓库[@某人[@某人[…]]]查看抽到过的所有角色查看他人抽到过的角色
在线十连在线抽卡,不扰民
人偶(权:主人)人偶功能
+ + + \ No newline at end of file diff --git a/kokkoro/web/template/homepage.html b/kokkoro/web/template/homepage.html new file mode 100644 index 0000000..ca16b96 --- /dev/null +++ b/kokkoro/web/template/homepage.html @@ -0,0 +1,40 @@ + + + + + yobot + + + + + + + +

yobot homepage

+

yobot is running

+

version:

+

{{ verinfo | replace("\n", "
") }}

+
+ 登录 +
+

关于

+ + + + + \ No newline at end of file diff --git a/kokkoro/web/template/login-code.html b/kokkoro/web/template/login-code.html new file mode 100644 index 0000000..97cea8f --- /dev/null +++ b/kokkoro/web/template/login-code.html @@ -0,0 +1,18 @@ + + + + yobot登录 + + + +

正在登录中……

+ + + \ No newline at end of file diff --git a/kokkoro/web/template/login.html b/kokkoro/web/template/login.html new file mode 100644 index 0000000..7145aa4 --- /dev/null +++ b/kokkoro/web/template/login.html @@ -0,0 +1,119 @@ + + + + +yobot登录 + + + + + + + + +
+
+

登录

+ {% if reason -%} + + {% endif -%} + {% if advice -%} + + {% endif -%} +
+ + + + + + + +

* 如果不知道密码,您可以私聊机器人“{{ prefix }}登录”或“{{ prefix }}重置密码”

+ + 登录 + +
+
+ +
+ + + \ No newline at end of file diff --git a/kokkoro/web/template/manual.html b/kokkoro/web/template/manual.html new file mode 100644 index 0000000..7668756 --- /dev/null +++ b/kokkoro/web/template/manual.html @@ -0,0 +1,81 @@ + + + + 使用手册 + + + + + + + +

公会战功能使用手册

+ +

使用 Bot 管理公会战,需要所有成员遵守使用规则
Bot 只是辅助作用,与成员多沟通才能提高分数

+ +

具体指令请以查看帮助页面

+ +

开始前

+ +
    +
  1. 机器人管理员进入后台设置,确认设置中的 boss 生命值符合当期公会战
  2. +
  3. 在群聊中发送创建日服公会(日、韩、台、国)
  4. +
  5. 所有公会战成员在群聊中发送加入公会,或者由群管理员发送加入全部成员
  6. +
  7. 成员向 bot 私聊发送登录重置密码,进入后台确认,同时修改登录密码
  8. +
  9. 成员进入公会战面板,并将网页地址保存到桌面快捷方式(手机、电脑均可),以便使用网页报刀和查看数据
  10. +
  11. 管理员在公会设置中,新建一个空白档案用来存放公会战数据
  12. +
+ +

进行中:成员

+ +
    +
  • 成员准备挑战 boss 时,需要在群聊中发送申请出刀,当 bot 允许后开始挑战 boss
  • +
  • 如果挑战 boss 完毕,需要向 bot 上报挑战结果
  • +
  • 如果挑战 boss 不成功,需要向 bot 上报挂树情况或 SL 使用情况
  • +
  • 如果意外掉刀,需要上报一次 0 点伤害的挑战
  • +
  • 成员可以使用预约功能,在 boss 出现时获得 bot 的 at 提醒
  • +
  • (可选择执行)如果希望进行合刀筛刀等多人出刀的操作,可以在群聊中发送锁定:留言,然后等待其他成员共同挑战 boss。等到所有挑战完毕后,按结算顺序向 bot 依次上报
  • +
  • (可选择执行)上报伤害和预约 boss 时,通过留言功能帮助排刀人员制定计划
  • +
+ +

进行中:管理员

+ +
    +
  • 在“查刀”页面中,管理员可以选中当日未完成出刀的成员,一键发送提醒
  • +
  • 在“查刀”页面中,排刀人员可以一览所有成员的 SL 使用情况
  • +
  • 在“查刀”页面中,排刀人员可以一览所有成员未完成的尾刀
  • +
+ +

特殊情况

+ +
    +
  • 如果有人挑战了 boss 却没有上报,可以由他人代理上报
  • +
  • 如果前一个上报数据出错,可以由上报者或管理员发送撤销来撤销
  • +
  • 如果有人申请了挑战,但没有报伤害也没有报挂树或 SL,其他成员可以在 3 分钟后在群聊中发送解锁来强制解锁
  • +
  • 如果出刀时间在日期变更附近导致未及时上报,需要将伤害上报到前一天的记录中
  • +
+ +

查看数据

+ +

公会战面板“查刀”页面中能查看记录的所有数据,并进行快速的筛选、过滤。如果需要原始数据,可以从“统计”页面导出。

+ + + \ No newline at end of file diff --git a/kokkoro/web/template/password.html b/kokkoro/web/template/password.html new file mode 100644 index 0000000..689ed7c --- /dev/null +++ b/kokkoro/web/template/password.html @@ -0,0 +1,128 @@ + + + + + yobot登录 + + + + + + + + +
+
+ + {% if error -%} + + + {% endif -%} + {% if success -%} + + + {% endif -%} + + +
+ + + + + + + + + + 修改 + + +
+
+ + + + + \ No newline at end of file diff --git a/kokkoro/web/template/unauthorized.html b/kokkoro/web/template/unauthorized.html new file mode 100644 index 0000000..7bfbccd --- /dev/null +++ b/kokkoro/web/template/unauthorized.html @@ -0,0 +1,16 @@ + + + + + yobot权限不足 + + + +

权限不足

+

浏览这个页面需要权限:{{ limit }}

+

你的权限:{{ auth }}

+

+ 返回 + + + diff --git a/kokkoro/web/template/user-info.html b/kokkoro/web/template/user-info.html new file mode 100644 index 0000000..6028efe --- /dev/null +++ b/kokkoro/web/template/user-info.html @@ -0,0 +1,104 @@ + + + + + yobot用户信息 + + + + + + + +
+ + + +
+ + + + diff --git a/kokkoro/web/template/user.html b/kokkoro/web/template/user.html new file mode 100644 index 0000000..172b159 --- /dev/null +++ b/kokkoro/web/template/user.html @@ -0,0 +1,95 @@ + + + + + yobot用户面板 + + + + + + + +

yobot用户面板

+
+

欢迎{{ user.nickname }}

+
上次登录:[[ from_ts({{ session.get('last_login_time') }}) ]] at {{ session.get('last_login_ipaddr') }}([[ addr.join('') ]]) +
+ {% if user.authority_group < 10 -%} + + + 设置 + + + 用户管理 + + + 群管理 + + +
+ {%- endif %} + + + 个人中心 + + + 修改密码 + + + 登出 + + + + {% if not clan_groups -%} + +
你还没有选择默认公会
请在你的公会群里发送“加入公会”来设置默认公会
+ 无公会 +
+ {%- else -%} + {% for group in clan_groups -%} + + 公会:{{ group['group_name'] }} + + {% endfor -%} + {%- endif %} +
+
+

关于yobot

+ + + + + diff --git a/kokkoro/web/templating.py b/kokkoro/web/templating.py new file mode 100644 index 0000000..2676fb5 --- /dev/null +++ b/kokkoro/web/templating.py @@ -0,0 +1,24 @@ +import os +import jinja2 +from quart import session, url_for + +static_folder = os.path.abspath(os.path.join( + os.path.dirname(__file__), './static')) +template_folder = os.path.abspath(os.path.join( + os.path.dirname(__file__), './template')) + +def _vertioned_url_for(endpoint, *args, **kwargs): + if endpoint == 'yobot_static': + kwargs['v'] = 'unknown' + return url_for(endpoint, *args, **kwargs) + +_env = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_folder), + enable_async=True, +) +_env.globals['session'] = session +_env.globals['url_for'] = _vertioned_url_for + +async def render_template(template, **context): + t = _env.get_template(template) + return await t.render_async(**context) diff --git a/kokkoro/web/universal_executor.py b/kokkoro/web/universal_executor.py new file mode 100644 index 0000000..25d110b --- /dev/null +++ b/kokkoro/web/universal_executor.py @@ -0,0 +1,243 @@ +from kokkoro.modules.pcrclanbattle.clanbattle.battlemaster import BattleMaster +from kokkoro.modules.pcrclanbattle.clanbattle.webmaster import WebMaster +from kokkoro import logger +from typing import Any, Dict, List, NewType, Optional, Tuple, Union +import time +from datetime import datetime, timedelta +from quart import jsonify, make_response + +SERVER_NAME = { 0x00: 'jp', 0x01: 'tw', 0x02: 'cn' } +ClanBattleReport = NewType('ClanBattleReport', List[Dict[str, Any]]) + + +''' route_elucidator side start -------------------------------------------- ''' +def now(): + return int(time.time()) + +def clan_api(bm:BattleMaster, payload): + try: + clan = check_clan(bm) + zone = bm.get_timezone_num(clan['server']) + action = payload['action'] + if payload['uid'] == 0: + # 允许游客查看 + if action not in ['get_member_list', 'get_challenge']: + return jsonify(code=10, message='Not logged in') + if action == 'get_member_list': + mems = bm.list_member() + members = [{'uid': m['uid'], 'nickname': m['name']} for m in mems] + return jsonify(code=0, members=members) + elif action == 'get_data': + return jsonify( + code=0, + bossData=get_boss_data(bm), + groupData={ + 'group_id': bm.group, + 'group_name': clan['name'], + 'game_server': SERVER_NAME[clan['server']], + 'level_4': False + }, + selfData={ + 'is_admin': False, + 'user_id': payload['uid'], + 'today_sl': False + } + ) + elif action == 'get_challenge': + d = int((datetime.now().timestamp()+(zone-5)*3600)/86400) + 1 + report = get_report(bm, None, payload['ts']) + if report is None: + return jsonofy(code=20, message="Group dosen't exist") + return jsonify( + code=0, + challenges=report, + today=d, + ) + elif action == 'get_user_challenge': + report = get_report(bm, payload['target_uid'], 0) + try: + visited_user = get_member(bm, payload['target_uid']) + except: + return jsonify(code=20, message='user not found') + return jsonify( + code=0, + challenges=report, + game_server=SERVER_NAME[clan['server']], + user_info={ + 'uid': payload['target_uid'], + 'nickname': visited_user['name'] + } + ) + elif action == 'send_remind': + + return jsonify(code=0, notice='发送成功') + else: + return jsonify(code=32, message='unknown action') + except KeyError as e: + logger.error(e) + return jsonify(code=31, message='missing key: '+str(e)) + except Exception as e: + logger.exception(e) + return jsonify(code=40, message='server error') + +async def clan_statistics_api(bm:BattleMaster, apikey): + try: + clan = check_clan(bm) + report = get_report(bm, None, 0) + mems = bm.list_member() + members = [{'uid': m['uid'], 'nickname': m['name']} for m in mems] + groupinfo = { + 'group_id': bm.group, + 'group_name': clan['name'], + 'game_server': SERVER_NAME[clan['server']], + 'battle_id': 0, + } + response = await make_response(jsonify( + code=0, + message='OK', + api_version=1, + challenges=report, + groupinfo=groupinfo, + members=members, + )) + #if (group.privacy & 0x2): + # response.headers['Access-Control-Allow-Origin'] = '*' + return response + except KeyError as e: + logger.error(e) + return jsonify(code=31, message='missing key: '+str(e)) + except Exception as e: + logger.exception(e) + return jsonify(code=40, message='server error') + +def clan_setting_api(bm:BattleMaster, payload): + try: + action = payload['action'] + clan = check_clan(bm) + if action == 'get_setting': + return jsonify( + code=0, + groupData={ + 'group_name': clan['name'], + 'game_server': SERVER_NAME[clan['server']], + 'battle_id': 0, + }, + privacy=3, + notification=1023, + ) + elif action == 'put_setting': + # clan['server'] = payload['game_server'] + # clan['notification = payload['notification'] + # clan['privacy'] = payload['privacy'] + # clan.save() + # logger.info('网页 成功 {} {} {}'.format( + # uid, group_id, action)) + # return jsonify(code=0, message='success') + return jsonify(code=22, message='unfinished action') + elif action == 'get_data_slot_record_count': + # counts = self.get_data_slot_record_count(group_id) + # logger.info('网页 成功 {} {} {}'.format( + # uid, group_id, action)) + # return jsonify(code=0, message='success', counts=counts) + return jsonify(code=22, message='unfinished action') + else: + return jsonify(code=32, message='unknown action') + except KeyError as e: + logger.error(e) + return jsonify(code=31, message='missing key: '+str(e)) + except Exception as e: + logger.exception(e) + return jsonify(code=40, message='server error') + +''' route_elucidator side end ---------------------------------------------- ''' + +''' BattleMaster side start ------------------------------------------------ ''' +def get_bm(group_id) -> BattleMaster: + bm = BattleMaster(group_id) + return bm + +def check_clan(bm:BattleMaster): + clan = bm.get_clan() + return None if not clan else clan + +def get_group(bm:BattleMaster): + clan = check_clan(bm) + if not clan: + return jsonify(code=20, message="Group dosen't exist") + return clan + +def list_group_by_member(bm:BattleMaster, uid): + return bm.list_clan_by_uid(uid) + +def get_boss_data(bm:BattleMaster): + clan = check_clan(bm) + if not clan: + return jsonify(code=20, message="Group dosen't exist") + r, b, hp = bm.get_challenge_progress(datetime.now()) + max_hp, score_rate = bm.get_boss_info(r, b, clan['server']) + boss_data = { + "challenger": None, + "challenging_comment": "", + "cycle": r, + "num": b, + "health": hp, + "full_health": max_hp, + "lock_type": 1 + } + return boss_data + +def get_member(bm:BattleMaster, uid): + member = bm.get_member(uid) + return None if not member else member + +def mod_member(bm:BattleMaster, uid, new_name, new_sl, new_auth): + return bm.mod_member(uid, new_name, new_sl, new_auth) + +def get_report(bm: BattleMaster, + userid: str = None, + ts: int = None, + ) -> ClanBattleReport: + clan = bm.get_clan() + if not clan: + return None + zone = bm.get_timezone_num(clan['server']) + report = [] + if userid is None: + if ts == 0: # get all of the challenge + dt = datetime.now() + challen = bm.list_challenge(dt) + else: # get challenge of one day + dt = datetime.fromtimestamp(ts) if ts is not None else datetime.now() + challen = bm.list_challenge_of_day(dt, zone) + else: + if ts == 0: # get all challenge of the user + dt = datetime.now() + challen = bm.list_challenge_of_user(userid, dt) + else: # get challenge of the user of one day + dt = datetime.fromtimestamp(ts) if ts is not None else datetime.now() + challen = bm.list_challenge_of_user_of_day(userid, dt, zone) + for c in challen: + ctime = int(c['time'].timestamp()) + report.append({ + 'battle_id': 0, + 'uid': c['uid'], + 'challenge_time': ctime, + 'challenge_pcrdate': int(ctime/86400) + 1, + 'challenge_pcrtime': int(ctime%86400), + 'cycle': c['round'], + 'boss_num': c['boss'], + 'damage': c['dmg'], + 'health_remain': c['remain'], + 'is_continue': bool(c['flag'] & bm.EXT), + 'message': None, + 'behalf': None, + }) + return report + +''' BattleMaster side end -------------------------------------------------- ''' + +''' WebMaster side start --------------------------------------------------- ''' +def get_wm(): + return WebMaster() + +''' WebMaster side end ----------------------------------------------------- '''