|
|
@@ -211,6 +211,13 @@ async def ensure_point_indexes() -> None:
|
|
|
await transactionsdb.create_index(
|
|
|
[("chat_id", ASCENDING), ("source", ASCENDING), ("created_at", DESCENDING)]
|
|
|
)
|
|
|
+ await transactionsdb.create_index(
|
|
|
+ [
|
|
|
+ ("chat_id", ASCENDING),
|
|
|
+ ("source", ASCENDING),
|
|
|
+ ("idempotency_key", ASCENDING),
|
|
|
+ ]
|
|
|
+ )
|
|
|
await rulesdb.create_index([("chat_id", ASCENDING)], unique=True)
|
|
|
await rate_limitsdb.create_index([("rate_key", ASCENDING)], unique=True)
|
|
|
await rate_limitsdb.create_index("expires_at", expireAfterSeconds=0)
|
|
|
@@ -676,6 +683,88 @@ async def award_checkin(
|
|
|
)
|
|
|
|
|
|
|
|
|
+async def get_checkin_metrics(
|
|
|
+ *,
|
|
|
+ chat_id: int,
|
|
|
+ user_id: int,
|
|
|
+ now: datetime | None = None,
|
|
|
+) -> dict[str, int]:
|
|
|
+ await ensure_point_indexes()
|
|
|
+ rules = await get_point_rules(chat_id)
|
|
|
+ now = _as_utc(now or utc_now())
|
|
|
+ try:
|
|
|
+ zone = ZoneInfo(rules["timezone"])
|
|
|
+ except ZoneInfoNotFoundError:
|
|
|
+ zone = ZoneInfo("Asia/Shanghai")
|
|
|
+ day = now.astimezone(zone).date().isoformat()
|
|
|
+ daily_key_pattern = (
|
|
|
+ rf"^checkin:{re.escape(str(int(chat_id)))}:\d+:{re.escape(day)}$"
|
|
|
+ )
|
|
|
+
|
|
|
+ daily_cursor = transactionsdb.find(
|
|
|
+ {
|
|
|
+ "chat_id": int(chat_id),
|
|
|
+ "source": SOURCE_CHECKIN,
|
|
|
+ "idempotency_key": {"$regex": daily_key_pattern},
|
|
|
+ },
|
|
|
+ {"user_id": 1},
|
|
|
+ ).sort([("created_at", ASCENDING), ("_id", ASCENDING)])
|
|
|
+ daily_entries = [item async for item in daily_cursor]
|
|
|
+ rank = next(
|
|
|
+ (
|
|
|
+ index
|
|
|
+ for index, item in enumerate(daily_entries, 1)
|
|
|
+ if int(item.get("user_id", 0)) == int(user_id)
|
|
|
+ ),
|
|
|
+ 0,
|
|
|
+ )
|
|
|
+
|
|
|
+ user_key_prefix = (
|
|
|
+ f"checkin:{int(chat_id)}:{int(user_id)}:"
|
|
|
+ )
|
|
|
+ month_key_pattern = (
|
|
|
+ rf"^{re.escape(user_key_prefix + day[:7])}-\d{{2}}$"
|
|
|
+ )
|
|
|
+ month_days = await transactionsdb.count_documents(
|
|
|
+ {
|
|
|
+ "chat_id": int(chat_id),
|
|
|
+ "user_id": int(user_id),
|
|
|
+ "source": SOURCE_CHECKIN,
|
|
|
+ "idempotency_key": {"$regex": month_key_pattern},
|
|
|
+ }
|
|
|
+ )
|
|
|
+ history_key_pattern = (
|
|
|
+ rf"^{re.escape(user_key_prefix)}\d{{4}}-\d{{2}}-\d{{2}}$"
|
|
|
+ )
|
|
|
+ history_cursor = transactionsdb.find(
|
|
|
+ {
|
|
|
+ "chat_id": int(chat_id),
|
|
|
+ "user_id": int(user_id),
|
|
|
+ "source": SOURCE_CHECKIN,
|
|
|
+ "idempotency_key": {"$regex": history_key_pattern},
|
|
|
+ },
|
|
|
+ {"idempotency_key": 1},
|
|
|
+ )
|
|
|
+ checkin_days = set()
|
|
|
+ async for item in history_cursor:
|
|
|
+ try:
|
|
|
+ checkin_days.add(
|
|
|
+ datetime.strptime(
|
|
|
+ str(item["idempotency_key"]).rsplit(":", 1)[-1],
|
|
|
+ "%Y-%m-%d",
|
|
|
+ ).date()
|
|
|
+ )
|
|
|
+ except (KeyError, TypeError, ValueError):
|
|
|
+ continue
|
|
|
+ expected_day = datetime.strptime(day, "%Y-%m-%d").date()
|
|
|
+ streak = 0
|
|
|
+ while expected_day in checkin_days:
|
|
|
+ streak += 1
|
|
|
+ expected_day -= timedelta(days=1)
|
|
|
+
|
|
|
+ return {"rank": rank, "streak": streak, "month_days": int(month_days)}
|
|
|
+
|
|
|
+
|
|
|
async def award_activity(
|
|
|
*,
|
|
|
chat_id: int,
|