mirror of
https://github.com/PaiGramTeam/MibooGram.git
synced 2024-11-16 04:45:27 +00:00
✨ Support zzz action log
This commit is contained in:
parent
bc4c4053a3
commit
0e51d50a05
3
core/dependence/influxdb.py
Normal file
3
core/dependence/influxdb.py
Normal file
@ -0,0 +1,3 @@
|
||||
from gram_core.dependence.influxdb import InfluxDatabase
|
||||
|
||||
__all__ = ("InfluxDatabase",)
|
0
core/services/self_help/__init__.py
Normal file
0
core/services/self_help/__init__.py
Normal file
31
core/services/self_help/models.py
Normal file
31
core/services/self_help/models.py
Normal file
@ -0,0 +1,31 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from influxdb_client import Point
|
||||
from influxdb_client.client.flux_table import FluxRecord
|
||||
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
|
||||
|
||||
from modules.action_log.date import TZ
|
||||
|
||||
FIX = timedelta(minutes=6)
|
||||
|
||||
|
||||
class ActionLogModel:
|
||||
@staticmethod
|
||||
def en(data: "ZZZSelfHelpActionLog") -> Point:
|
||||
return (
|
||||
Point.measurement("action_log")
|
||||
.tag("uid", data.uid)
|
||||
.field("id", data.id)
|
||||
.field("status", data.status)
|
||||
.field("reason", data.reason.value)
|
||||
.field("client_ip", data.client_ip)
|
||||
.time(data.time.replace(tzinfo=TZ) + FIX)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def de(data: "FluxRecord") -> "ZZZSelfHelpActionLog":
|
||||
utc_time = data.get_time()
|
||||
time = utc_time.astimezone(TZ)
|
||||
return ZZZSelfHelpActionLog(
|
||||
id=data["id"], uid=data["uid"], datetime=time, action_name=data["reason"], client_ip=data["client_ip"]
|
||||
)
|
56
core/services/self_help/repositories.py
Normal file
56
core/services/self_help/repositories.py
Normal file
@ -0,0 +1,56 @@
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from gram_core.base_service import BaseService
|
||||
from gram_core.dependence.influxdb import InfluxDatabase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from influxdb_client.client.flux_table import FluxTable
|
||||
from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync
|
||||
from influxdb_client import Point
|
||||
|
||||
|
||||
class ActionLogRepository(BaseService.Component):
|
||||
|
||||
def __init__(self, influxdb: InfluxDatabase):
|
||||
self.influxdb = influxdb
|
||||
self.client = influxdb.client
|
||||
self.bucket = "zzz"
|
||||
|
||||
async def add(self, p: List["Point"]) -> bool:
|
||||
async with self.client() as client:
|
||||
client: "InfluxDBClientAsync"
|
||||
return await client.write_api().write(self.bucket, record=p)
|
||||
|
||||
async def count_uptime_period(self, uid: int) -> "FluxTable":
|
||||
async with self.client() as client:
|
||||
client: "InfluxDBClientAsync"
|
||||
query = (
|
||||
'import "date"'
|
||||
'import "timezone"'
|
||||
'option location = timezone.location(name: "Asia/Shanghai")'
|
||||
'from(bucket: "{}")'
|
||||
"|> range(start: -180d)"
|
||||
'|> filter(fn: (r) => r["_measurement"] == "action_log")'
|
||||
'|> filter(fn: (r) => r["_field"] == "status")'
|
||||
'|> filter(fn: (r) => r["_value"] == 1)'
|
||||
'|> filter(fn: (r) => r["uid"] == "{}")'
|
||||
"|> aggregateWindow(every: 1h, fn: count)"
|
||||
).format(self.bucket, uid)
|
||||
query += '|> map(fn: (r) => ({r with hour: date.hour(t: r._time)}))|> yield(name: "hourly_count")'
|
||||
tables = await client.query_api().query(query)
|
||||
for table in tables:
|
||||
return table
|
||||
|
||||
async def get_data(self, uid: int, day: int = 30) -> "FluxTable":
|
||||
async with self.client() as client:
|
||||
client: "InfluxDBClientAsync"
|
||||
query = (
|
||||
'from(bucket: "{}")'
|
||||
"|> range(start: -{}d)"
|
||||
'|> filter(fn: (r) => r["_measurement"] == "action_log")'
|
||||
'|> filter(fn: (r) => r["uid"] == "{}")'
|
||||
'|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")'
|
||||
).format(self.bucket, day, uid)
|
||||
tables = await client.query_api().query(query)
|
||||
for table in tables:
|
||||
return table
|
37
core/services/self_help/services.py
Normal file
37
core/services/self_help/services.py
Normal file
@ -0,0 +1,37 @@
|
||||
from typing import List, TYPE_CHECKING, Dict
|
||||
|
||||
from core.services.self_help.models import ActionLogModel
|
||||
from core.services.self_help.repositories import ActionLogRepository
|
||||
from gram_core.base_service import BaseService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
|
||||
|
||||
|
||||
class ActionLogService(BaseService):
|
||||
def __init__(self, repository: ActionLogRepository):
|
||||
self.repository = repository
|
||||
|
||||
async def add(self, p: List["ZZZSelfHelpActionLog"]) -> bool:
|
||||
return await self.repository.add([ActionLogModel.en(data) for data in p])
|
||||
|
||||
async def count_uptime_period(self, uid: int) -> Dict[int, int]:
|
||||
"""计算最近一个月不同时间点的登录次数"""
|
||||
data = {k: 0 for k in range(24)}
|
||||
r = await self.repository.count_uptime_period(uid)
|
||||
if not r:
|
||||
return data
|
||||
for record in r.records:
|
||||
if record.get_value():
|
||||
data[record["hour"]] += 1
|
||||
return data
|
||||
|
||||
async def get_data(self, uid: int, day: int = 30) -> List["ZZZSelfHelpActionLog"]:
|
||||
"""获取指定天数内的某用户的登录记录"""
|
||||
data = []
|
||||
r = await self.repository.get_data(uid, day)
|
||||
if not r:
|
||||
return data
|
||||
for record in r.records:
|
||||
data.append(ActionLogModel.de(record))
|
||||
return data
|
1
modules/action_log/__init__.py
Normal file
1
modules/action_log/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""登录日志分析"""
|
150
modules/action_log/client.py
Normal file
150
modules/action_log/client.py
Normal file
@ -0,0 +1,150 @@
|
||||
from typing import List, Dict, Union
|
||||
|
||||
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
|
||||
|
||||
from modules.action_log.date import DateUtils
|
||||
from modules.action_log.models import ActionLogPair
|
||||
|
||||
|
||||
class ActionLogAnalyse(DateUtils):
|
||||
def __init__(self, data: List[ZZZSelfHelpActionLog], data2: Dict[int, int]):
|
||||
super().__init__()
|
||||
self.data = data
|
||||
self.data2 = data2
|
||||
self.pairs = self.init_pair(data)
|
||||
self.this_week_data: List[ActionLogPair] = []
|
||||
self.last_week_data: List[ActionLogPair] = []
|
||||
self.this_month_data: List[ActionLogPair] = []
|
||||
self.last_month_data: List[ActionLogPair] = []
|
||||
self.init_pair_data()
|
||||
|
||||
def init_pair_data(self):
|
||||
"""通过时间点判断数据属于哪个时间段"""
|
||||
for d in self.pairs:
|
||||
if self.week_start <= d.start_time < self.week_end:
|
||||
self.this_week_data.append(d)
|
||||
elif self.week_last_start <= d.start_time < self.week_last_end:
|
||||
self.last_week_data.append(d)
|
||||
if self.month_start <= d.start_time < self.month_end:
|
||||
self.this_month_data.append(d)
|
||||
elif self.month_last_start <= d.start_time < self.month_last_end:
|
||||
self.last_month_data.append(d)
|
||||
|
||||
@staticmethod
|
||||
def init_pair(data: List[ZZZSelfHelpActionLog]) -> List[ActionLogPair]:
|
||||
# 确保第一个数据为登入,最后一条数据为登出
|
||||
if data[0].status == 0:
|
||||
data.pop(0)
|
||||
if data[-1].status == 1:
|
||||
data.pop(-1)
|
||||
pairs = []
|
||||
for i in range(0, len(data), 2):
|
||||
pairs.append(ActionLogPair(start=data[i], end=data[i + 1]))
|
||||
return pairs
|
||||
|
||||
def get_this_week_duration(self) -> int:
|
||||
"""本周时长"""
|
||||
return (sum(d.duration.seconds for d in self.this_week_data)) if self.this_week_data else 0
|
||||
|
||||
def get_last_week_duration(self) -> int:
|
||||
"""上周时长"""
|
||||
return (sum(d.duration.seconds for d in self.last_week_data)) if self.last_week_data else 0
|
||||
|
||||
def get_this_month_duration(self) -> int:
|
||||
"""本月时长"""
|
||||
return (sum(d.duration.seconds for d in self.this_month_data)) if self.this_month_data else 0
|
||||
|
||||
def get_last_month_duration(self) -> int:
|
||||
"""上月时长"""
|
||||
return (sum(d.duration.seconds for d in self.last_month_data)) if self.last_month_data else 0
|
||||
|
||||
def get_this_month_avg_duration(self) -> float:
|
||||
"""本月平均时长"""
|
||||
data_len = len(self.this_month_data)
|
||||
return (self.get_this_month_duration() / data_len) if data_len else 0
|
||||
|
||||
def get_last_month_avg_duration(self) -> float:
|
||||
"""上月平均时长"""
|
||||
data_len = len(self.last_month_data)
|
||||
return (self.get_last_month_duration() / data_len) if data_len else 0
|
||||
|
||||
def get_this_week_long_duration(self) -> int:
|
||||
"""周最长会话"""
|
||||
return (max(d.duration.seconds for d in self.this_week_data)) if self.this_week_data else 0
|
||||
|
||||
def get_this_week_short_duration(self) -> int:
|
||||
"""周最短会话"""
|
||||
return (min(d.duration.seconds for d in self.this_week_data)) if self.this_week_data else 0
|
||||
|
||||
def get_this_month_long_duration(self) -> int:
|
||||
"""月最长会话"""
|
||||
return (max(d.duration.seconds for d in self.this_month_data)) if self.this_month_data else 0
|
||||
|
||||
def get_this_month_short_duration(self) -> int:
|
||||
"""月最短会话"""
|
||||
return (min(d.duration.seconds for d in self.this_month_data)) if self.this_month_data else 0
|
||||
|
||||
@staticmethod
|
||||
def format_sec(sec: Union[int, float]) -> str:
|
||||
hour = sec // 3600
|
||||
minute = (sec % 3600) // 60
|
||||
second = sec % 60
|
||||
if hour:
|
||||
return f"{int(hour)}时{int(minute)}分"
|
||||
if minute:
|
||||
return f"{int(minute)}分{int(second)}秒"
|
||||
return f"{int(second)}秒"
|
||||
|
||||
def get_data(self) -> List[Dict[str, str]]:
|
||||
data = {
|
||||
"本周时长": self.get_this_week_duration(),
|
||||
"上周时长": self.get_last_week_duration(),
|
||||
"本月时长": self.get_this_month_duration(),
|
||||
"上月时长": self.get_last_month_duration(),
|
||||
"本月平均": self.get_this_month_avg_duration(),
|
||||
"上月平均": self.get_last_month_avg_duration(),
|
||||
"周最长会话": self.get_this_week_long_duration(),
|
||||
"月最长会话": self.get_this_month_long_duration(),
|
||||
"周最短会话": self.get_this_week_short_duration(),
|
||||
"月最短会话": self.get_this_month_short_duration(),
|
||||
}
|
||||
datas = []
|
||||
for k, v in data.items():
|
||||
datas.append(
|
||||
{
|
||||
"name": k,
|
||||
"value": self.format_sec(v),
|
||||
}
|
||||
)
|
||||
|
||||
max_hour = max(self.data2, key=self.data2.get)
|
||||
min_hour = min(self.data2, key=self.data2.get)
|
||||
datas.append({"name": "最常上线", "value": f"{max_hour}点"})
|
||||
datas.append({"name": "最少上线", "value": f"{min_hour}点"})
|
||||
|
||||
return datas
|
||||
|
||||
def get_line_data(self) -> List[Dict[str, str]]:
|
||||
data = []
|
||||
for k, v in self.data2.items():
|
||||
data.append(
|
||||
{
|
||||
"month": f"{k}点",
|
||||
"value": v,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def get_record_data(self) -> List[Dict[str, str]]:
|
||||
data = []
|
||||
limit = 4
|
||||
data_len = len(self.data) - 1
|
||||
for i in range(data_len, data_len - limit, -1):
|
||||
record = self.data[i]
|
||||
data.append(
|
||||
{
|
||||
"time": record.time.strftime("%Y年%m月%d日 %H:%M:%S"),
|
||||
"reason": record.reason.value,
|
||||
}
|
||||
)
|
||||
return data
|
36
modules/action_log/date.py
Normal file
36
modules/action_log/date.py
Normal file
@ -0,0 +1,36 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pytz import timezone
|
||||
|
||||
TZ = timezone("Asia/Shanghai")
|
||||
|
||||
|
||||
class DateUtils:
|
||||
def __init__(self):
|
||||
self.now = datetime.now(tz=TZ)
|
||||
# 本周
|
||||
self.week_start = self.date_start(self.now - timedelta(days=self.now.weekday()))
|
||||
self.week_end = self.week_start + timedelta(days=7)
|
||||
# 上周
|
||||
self.week_last_start = self.week_start - timedelta(days=7)
|
||||
self.week_last_end = self.week_start
|
||||
# 本月
|
||||
self.month_start = self.date_start(self.now.replace(day=1))
|
||||
self.month_end = self.get_month_end(self.month_start)
|
||||
# 上月
|
||||
month_last = (self.month_start - timedelta(days=1)).replace(day=1)
|
||||
self.month_last_start = self.date_start(month_last)
|
||||
self.month_last_end = self.get_month_end(month_last)
|
||||
|
||||
@staticmethod
|
||||
def date_start(date: datetime) -> datetime:
|
||||
return date.replace(hour=4, minute=0, second=0, microsecond=0)
|
||||
|
||||
def get_week_start(self) -> datetime:
|
||||
day = self.now - timedelta(days=self.now.weekday())
|
||||
return self.date_start(day)
|
||||
|
||||
def get_month_end(self, date: datetime) -> datetime:
|
||||
next_month = date.replace(day=28) + timedelta(days=5)
|
||||
next_month_start = next_month.replace(day=1)
|
||||
return self.date_start(next_month_start)
|
21
modules/action_log/models.py
Normal file
21
modules/action_log/models.py
Normal file
@ -0,0 +1,21 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pydantic import BaseModel
|
||||
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
|
||||
|
||||
|
||||
class ActionLogPair(BaseModel):
|
||||
start: ZZZSelfHelpActionLog
|
||||
end: ZZZSelfHelpActionLog
|
||||
|
||||
@property
|
||||
def start_time(self) -> datetime:
|
||||
return self.start.time
|
||||
|
||||
@property
|
||||
def end_time(self) -> datetime:
|
||||
return self.end.time
|
||||
|
||||
@property
|
||||
def duration(self) -> timedelta:
|
||||
return self.end.time - self.start.time
|
2
pdm.lock
2
pdm.lock
@ -2212,7 +2212,7 @@ name = "simnet"
|
||||
version = "0.1.22"
|
||||
requires_python = "<4.0,>=3.8"
|
||||
git = "https://github.com/PaiGramTeam/SIMNet"
|
||||
revision = "185733af85cabb57bf7e64bf6c46a35c515fef2e"
|
||||
revision = "9839a06bb87403443ac2cbea0ffde4156b05d902"
|
||||
summary = "Modern API wrapper for Genshin Impact & Honkai: Star Rail built on asyncio and pydantic."
|
||||
groups = ["default"]
|
||||
dependencies = [
|
||||
|
29
plugins/jobs/import_action_log.py
Normal file
29
plugins/jobs/import_action_log.py
Normal file
@ -0,0 +1,29 @@
|
||||
import datetime
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from gram_core.plugin import Plugin, job, handler
|
||||
from plugins.tools.action_log_system import ActionLogSystem
|
||||
from utils.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
|
||||
class ImportActionLogJob(Plugin):
|
||||
def __init__(self, action_log_system: ActionLogSystem = None):
|
||||
self.action_log_system = action_log_system
|
||||
|
||||
@job.run_daily(time=datetime.time(hour=12, minute=1, second=0), name="ImportActionLogJob")
|
||||
async def refresh(self, _: "ContextTypes.DEFAULT_TYPE"):
|
||||
await self.action_log_system.daily_import_login(_)
|
||||
|
||||
@handler.command(command="action_log_import_all", block=False, admin=True)
|
||||
async def action_log_import_all(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"):
|
||||
user = update.effective_user
|
||||
logger.info("用户 %s[%s] action_log_import_all 命令请求", user.full_name, user.id)
|
||||
message = update.effective_message
|
||||
reply = await message.reply_text("正在执行导入登录记录任务,请稍后...")
|
||||
await self.refresh(context)
|
||||
await reply.edit_text("全部账号导入登录记录任务完成")
|
80
plugins/tools/action_log_system.py
Normal file
80
plugins/tools/action_log_system.py
Normal file
@ -0,0 +1,80 @@
|
||||
from asyncio import sleep
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from simnet.errors import (
|
||||
TimedOut as SimnetTimedOut,
|
||||
BadRequest as SimnetBadRequest,
|
||||
InvalidCookies,
|
||||
)
|
||||
|
||||
from core.services.self_help.services import ActionLogService
|
||||
from gram_core.basemodel import RegionEnum
|
||||
from gram_core.plugin import Plugin
|
||||
from gram_core.services.cookies import CookiesService
|
||||
from gram_core.services.cookies.models import CookiesStatusEnum
|
||||
from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError
|
||||
from utils.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from simnet import ZZZClient
|
||||
|
||||
|
||||
class ActionLogSystem(Plugin):
|
||||
"""登录记录系统"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cookies: CookiesService,
|
||||
helper: GenshinHelper,
|
||||
action_log_service: ActionLogService,
|
||||
):
|
||||
self.cookies = cookies
|
||||
self.helper = helper
|
||||
self.action_log_service = action_log_service
|
||||
|
||||
async def import_action_log(self, client: "ZZZClient", authkey: str) -> bool:
|
||||
data = await client.get_zzz_action_log(authkey=authkey)
|
||||
# 确保第一个数据为登出、最后一条数据为登入
|
||||
if not data:
|
||||
return False
|
||||
if data[0].status == 1:
|
||||
data.pop(0)
|
||||
if data[-1].status == 0:
|
||||
data.pop(-1)
|
||||
return await self.action_log_service.add(data)
|
||||
|
||||
async def daily_import_login(self, _: "ContextTypes.DEFAULT_TYPE"):
|
||||
logger.info("正在执行每日刷新登录记录任务")
|
||||
for cookie_model in await self.cookies.get_all(
|
||||
region=RegionEnum.HYPERION, status=CookiesStatusEnum.STATUS_SUCCESS
|
||||
):
|
||||
user_id = cookie_model.user_id
|
||||
cookies = cookie_model.data
|
||||
if cookies.get("stoken") is None:
|
||||
continue
|
||||
try:
|
||||
async with self.helper.genshin(user_id, region=RegionEnum.HYPERION) as client:
|
||||
client: "StarRailClient"
|
||||
try:
|
||||
authkey = await client.get_authkey_by_stoken("csc")
|
||||
except ValueError:
|
||||
logger.warning("用户 user_id[%s] 请求登录记录失败 无 stoken", user_id)
|
||||
continue
|
||||
await self.import_action_log(client, authkey)
|
||||
except (InvalidCookies, PlayerNotFoundError, CookiesNotFoundError):
|
||||
continue
|
||||
except SimnetBadRequest as exc:
|
||||
logger.warning(
|
||||
"用户 user_id[%s] 请求登录记录失败 [%s]%s", user_id, exc.ret_code, exc.original or exc.message
|
||||
)
|
||||
continue
|
||||
except SimnetTimedOut:
|
||||
logger.info("用户 user_id[%s] 请求登录记录超时", user_id)
|
||||
continue
|
||||
except Exception as exc:
|
||||
logger.error("执行自动刷新登录记录时发生错误 user_id[%s]", user_id, exc_info=exc)
|
||||
continue
|
||||
await sleep(1)
|
155
plugins/zzz/action_log.py
Normal file
155
plugins/zzz/action_log.py
Normal file
@ -0,0 +1,155 @@
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import filters
|
||||
|
||||
from simnet import Region
|
||||
|
||||
from core.services.self_help.services import ActionLogService
|
||||
from gram_core.config import config
|
||||
from gram_core.plugin import Plugin, handler
|
||||
from gram_core.plugin.methods.inline_use_data import IInlineUseData
|
||||
from gram_core.services.template.services import TemplateService
|
||||
from modules.action_log.client import ActionLogAnalyse
|
||||
from plugins.tools.action_log_system import ActionLogSystem
|
||||
from plugins.tools.genshin import GenshinHelper
|
||||
from utils.const import RESOURCE_DIR
|
||||
from utils.log import logger
|
||||
from utils.uid import mask_number
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from simnet import ZZZClient
|
||||
|
||||
from gram_core.services.template.models import RenderResult
|
||||
|
||||
|
||||
class NotSupport(Exception):
|
||||
"""不支持的服务器"""
|
||||
|
||||
def __init__(self, msg: str = None):
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class ActionLogPlugins(Plugin):
|
||||
"""登录记录信息查询"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
helper: GenshinHelper,
|
||||
action_log_service: ActionLogService,
|
||||
action_log_system: ActionLogSystem,
|
||||
template_service: TemplateService,
|
||||
):
|
||||
self.helper = helper
|
||||
self.action_log_service = action_log_service
|
||||
self.action_log_system = action_log_system
|
||||
self.template_service = template_service
|
||||
|
||||
@handler.command(command="action_log_import", filters=filters.ChatType.PRIVATE, cookie=True, block=False)
|
||||
async def action_log_import(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
|
||||
user_id = await self.get_real_user_id(update)
|
||||
message = update.effective_message
|
||||
uid, offset = self.get_real_uid_or_offset(update)
|
||||
self.log_user(update, logger.info, "导入登录记录")
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
|
||||
try:
|
||||
async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client:
|
||||
client: "ZZZClient"
|
||||
if client.region != Region.CHINESE:
|
||||
raise NotSupport("不支持的服务器")
|
||||
try:
|
||||
authkey = await client.get_authkey_by_stoken("csc")
|
||||
except ValueError as e:
|
||||
raise NotSupport("未绑定 stoken") from e
|
||||
|
||||
notice = await message.reply_text(f"{config.notice.bot_name}需要收集整理数据,还请耐心等待哦~")
|
||||
|
||||
bo = await self.action_log_system.import_action_log(client, authkey)
|
||||
text = "导入登录记录成功" if bo else "导入登录记录失败,可能没有新记录"
|
||||
await notice.edit_text(text)
|
||||
self.log_user(update, logger.success, text)
|
||||
except NotSupport as e:
|
||||
msg = await message.reply_text(e.msg)
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
self.add_delete_message_job(message, delay=60)
|
||||
self.add_delete_message_job(msg, delay=60)
|
||||
|
||||
async def get_render_data(self, uid: int):
|
||||
r = await self.action_log_service.get_data(uid, 63)
|
||||
r2 = await self.action_log_service.count_uptime_period(uid)
|
||||
if not r or not r2:
|
||||
raise NotSupport("未查询到登录记录")
|
||||
d = ActionLogAnalyse(r, r2)
|
||||
data = d.get_data()
|
||||
line_data = d.get_line_data()
|
||||
records = d.get_record_data()
|
||||
return {
|
||||
"uid": mask_number(uid),
|
||||
"datas": data,
|
||||
"line_data": line_data,
|
||||
"records": records,
|
||||
}
|
||||
|
||||
async def add_theme_data(self, data: Dict, player_id: int):
|
||||
res = RESOURCE_DIR / "img"
|
||||
data["avatar"] = (res / "avatar.png").as_uri()
|
||||
data["background"] = (res / "home.png").as_uri()
|
||||
return data
|
||||
|
||||
async def render(self, client: "ZZZClient") -> "RenderResult":
|
||||
data = await self.get_render_data(client.player_id)
|
||||
return await self.template_service.render(
|
||||
"zzz/action_log/action_log.html",
|
||||
await self.add_theme_data(data, client.player_id),
|
||||
full_page=True,
|
||||
query_selector=".container",
|
||||
)
|
||||
|
||||
@handler.command(command="action_log", cookie=True, block=False)
|
||||
async def action_log(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
|
||||
user_id = await self.get_real_user_id(update)
|
||||
message = update.effective_message
|
||||
uid, offset = self.get_real_uid_or_offset(update)
|
||||
self.log_user(update, logger.info, "查询登录记录")
|
||||
|
||||
try:
|
||||
async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client:
|
||||
client: "ZZZClient"
|
||||
render = await self.render(client)
|
||||
await render.reply_photo(message)
|
||||
except NotSupport as e:
|
||||
msg = await message.reply_text(e.msg)
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
self.add_delete_message_job(message, delay=60)
|
||||
self.add_delete_message_job(msg, delay=60)
|
||||
|
||||
async def action_log_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
|
||||
callback_query = update.callback_query
|
||||
user = update.effective_user
|
||||
user_id = user.id
|
||||
uid = IInlineUseData.get_uid_from_context(context)
|
||||
self.log_user(update, logger.info, "查询登录记录")
|
||||
|
||||
try:
|
||||
async with self.helper.genshin(user_id, player_id=uid) as client:
|
||||
client: "ZZZClient"
|
||||
render = await self.render(client)
|
||||
except NotSupport as e:
|
||||
await callback_query.answer(e.msg, show_alert=True)
|
||||
return
|
||||
await render.edit_inline_media(callback_query)
|
||||
|
||||
async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]:
|
||||
return [
|
||||
IInlineUseData(
|
||||
text="登录统计",
|
||||
hash="action_log",
|
||||
callback=self.action_log_use_by_inline,
|
||||
cookie=True,
|
||||
player=True,
|
||||
)
|
||||
]
|
@ -88,7 +88,7 @@ rich==13.7.1
|
||||
sentry-sdk==2.7.1
|
||||
setuptools==70.2.0
|
||||
shellingham==1.5.4
|
||||
simnet @ git+https://github.com/PaiGramTeam/SIMNet@185733af85cabb57bf7e64bf6c46a35c515fef2e
|
||||
simnet @ git+https://github.com/PaiGramTeam/SIMNet@9839a06bb87403443ac2cbea0ffde4156b05d902
|
||||
six==1.16.0
|
||||
smmap==5.0.1
|
||||
sniffio==1.3.1
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 11 KiB |
134
resources/zzz/action_log/action_log.css
Normal file
134
resources/zzz/action_log/action_log.css
Normal file
@ -0,0 +1,134 @@
|
||||
@font-face {
|
||||
font-family: "tttgbnumber";
|
||||
src: url("../../fonts/tttgbnumber.ttf");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
width: 700px;
|
||||
color: #1e1f20;
|
||||
transform: scale(1.3);
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 700px;
|
||||
padding: 20px 15px 10px 15px;
|
||||
background-color: #f5f6fb;
|
||||
}
|
||||
|
||||
.head_box {
|
||||
border-radius: 15px;
|
||||
font-family: tttgbnumber;
|
||||
padding: 10px 20px;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.head_box_avatar {
|
||||
height: 60px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.head_box .id_text {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.head_box .day_text {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.head_box .genshin_logo {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
width: 97px;
|
||||
}
|
||||
|
||||
.base_info {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.uid {
|
||||
font-family: tttgbnumber;
|
||||
}
|
||||
|
||||
.data_box {
|
||||
border-radius: 15px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 15px 5px 15px;
|
||||
background: #fff;
|
||||
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab_lable {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -8px;
|
||||
background: #d4b98c;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 15px 0 15px 15px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.data_line {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.data_line_item {
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
/*margin: 0 20px;*/
|
||||
}
|
||||
|
||||
.num {
|
||||
font-family: tttgbnumber;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.data_box .lable {
|
||||
font-size: 14px;
|
||||
color: #7f858a;
|
||||
line-height: 1;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
#chartContainer {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
#chartContainer2 {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 14px;
|
||||
font-family: "tttgbnumber";
|
||||
text-align: center;
|
||||
color: #7994a7;
|
||||
}
|
||||
|
||||
.small_font {
|
||||
font-size: 15px;
|
||||
}
|
133
resources/zzz/action_log/action_log.html
Normal file
133
resources/zzz/action_log/action_log.html
Normal file
@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" type="text/css" href="action_log.css" />
|
||||
<link rel="preload" href="../../fonts/tttgbnumber.ttf" as="font">
|
||||
<style>
|
||||
.head_box {
|
||||
background: #fff url("{{ background }}") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container" id="container">
|
||||
<div class="head_box">
|
||||
<img class="head_box_avatar" src="{{ avatar }}" alt="Avatar">
|
||||
<div>
|
||||
<div class="id_text">ID: {{ uid }}</div>
|
||||
<h2 class="day_text">登录统计</h2>
|
||||
</div>
|
||||
<img class="genshin_logo" src="./../../img/logo.png" alt=""/>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">数据总览</div>
|
||||
<div class="data_line">
|
||||
{% for data in datas[:4] %}
|
||||
<div class="data_line_item">
|
||||
<div class="num">{{ data.value }}</div>
|
||||
<div class="lable">{{ data.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="data_line">
|
||||
{% for data in datas[4:8] %}
|
||||
<div class="data_line_item">
|
||||
<div class="num">{{ data.value }}</div>
|
||||
<div class="lable">{{ data.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="data_line">
|
||||
{% for data in datas[8:] %}
|
||||
<div class="data_line_item">
|
||||
<div class="num">{{ data.value }}</div>
|
||||
<div class="lable">{{ data.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">登录时间点统计</div>
|
||||
<div id="chartContainer"></div>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">最近日志</div>
|
||||
{% for record in records %}
|
||||
<div class="data_line">
|
||||
<div class="data_line_item">
|
||||
<div class="num small_font">{{ record.time }}</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num small_font">{{ record.reason }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="logo"> Generate by MibooGram</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="../../genshin/pay_log/echarts.min.js"></script>
|
||||
|
||||
<script>
|
||||
const barData = {{ line_data | tojson }};
|
||||
const myChart1 = echarts.init(document.querySelector('#chartContainer'), null, {renderer: 'svg'});
|
||||
const xData = barData.map(v => v.month)
|
||||
const yData = barData.map(v => v.value)
|
||||
const option = {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData
|
||||
},
|
||||
legend: {
|
||||
x:'left',
|
||||
y:'top',
|
||||
show: true,
|
||||
data: [{ name: '次' }]
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name:'次',
|
||||
data: yData,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
position: 'top',
|
||||
show: true,
|
||||
textStyle: {
|
||||
color: '#1e1f20',
|
||||
fontSize: 14,
|
||||
fontFamily: "tttgbnumber",
|
||||
}
|
||||
},
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [{
|
||||
offset: 0,
|
||||
color: "#1268f3"
|
||||
}, {
|
||||
offset: 0.6,
|
||||
color: "#08a4fa"
|
||||
}, {
|
||||
offset: 1,
|
||||
color: "#01ccfe"
|
||||
}], false)
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart1.setOption(option);
|
||||
</script>
|
||||
|
||||
</html>
|
@ -52,7 +52,7 @@
|
||||
{% if hasMore %}
|
||||
<div class="hasMore">*完整数据请私聊查看</div>
|
||||
{% endif %}
|
||||
<div class="logo">Template By Yunzai-Bot x Generated By PamGram</div>
|
||||
<div class="logo">Template By Yunzai-Bot x Generated By MibooGram</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -76,7 +76,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo">Template By Yunzai-Bot x Generated By PamGram</div>
|
||||
<div class="logo">Template By Yunzai-Bot x Generated By MibooGram</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
Loading…
Reference in New Issue
Block a user