limit by UID and server (#1251)

This commit is contained in:
NotSoToothless
2026-05-15 02:56:55 -07:00
committed by GitHub
parent 0c3fc27832
commit 311ae875fb
13 changed files with 124 additions and 23 deletions
+55 -10
View File
@@ -127,8 +127,10 @@ from .utils import (
LocaleJsonTranslator, LocaleJsonTranslator,
COMP_FREE_UNTIL_TS, COMP_FREE_UNTIL_TS,
COMP_LIMIT_PER_TIMESLOT, COMP_LIMIT_PER_TIMESLOT,
COMP_LIMIT_PER_USER_PER_TIMESLOT,
get_current_timeslot_start_ts, get_current_timeslot_start_ts,
get_comp_usage_in_timeslot, get_comp_usage_in_timeslot,
get_comp_usage_in_timeslot_by_user,
SQB_SLOTS_POSTED, SQB_SLOTS_POSTED,
RECAP_LANGS, RECAP_LANGS,
RecapError, RecapError,
@@ -403,24 +405,57 @@ async def comp(interaction: discord.Interaction, squadron_short: str):
if slot_start is not None: if slot_start is not None:
entitled = await is_guild_entitled(guild.id) entitled = await is_guild_entitled(guild.id)
if not entitled: if not entitled:
used = await get_comp_usage_in_timeslot(guild.id, slot_start) used_server = await get_comp_usage_in_timeslot(guild.id, slot_start)
used_user = await get_comp_usage_in_timeslot_by_user(
guild.id, interaction.user.id, slot_start
)
logging.info( logging.info(
"[COMP-USAGE] guild=%s user=%s squadron=%s used=%s limit=%s slot_start=%s entitled=%s", "[COMP-USAGE] guild=%s user=%s squadron=%s used_server=%s/%s used_user=%s/%s slot_start=%s entitled=%s",
guild.id, guild.id,
interaction.user.id, interaction.user.id,
squadron_short, squadron_short,
used, used_server,
COMP_LIMIT_PER_TIMESLOT, COMP_LIMIT_PER_TIMESLOT,
used_user,
COMP_LIMIT_PER_USER_PER_TIMESLOT,
slot_start, slot_start,
entitled, entitled,
) )
if used >= COMP_LIMIT_PER_TIMESLOT: if used_user >= COMP_LIMIT_PER_USER_PER_TIMESLOT:
logging.warning( logging.warning(
"[COMP-LIMIT] guild=%s user=%s squadron=%s used=%s limit=%s slot_start=%s action=blocked", "[COMP-LIMIT] guild=%s user=%s squadron=%s scope=user used_user=%s/%s slot_start=%s action=blocked",
guild.id, guild.id,
interaction.user.id, interaction.user.id,
squadron_short, squadron_short,
used, used_user,
COMP_LIMIT_PER_USER_PER_TIMESLOT,
slot_start,
)
embed_limit = discord.Embed(
title=t(lang, "comp.limit_reached_title"),
description=t(
lang,
"comp.user_limit_reached_desc",
limit=COMP_LIMIT_PER_USER_PER_TIMESLOT,
),
color=discord.Color.red()
)
embed_limit.set_footer(
text=t(
lang,
"comp.user_remaining_footer",
remaining=0,
limit=COMP_LIMIT_PER_USER_PER_TIMESLOT,
)
)
return await interaction.followup.send(embed=embed_limit)
if used_server >= COMP_LIMIT_PER_TIMESLOT:
logging.warning(
"[COMP-LIMIT] guild=%s user=%s squadron=%s scope=server used_server=%s/%s slot_start=%s action=blocked",
guild.id,
interaction.user.id,
squadron_short,
used_server,
COMP_LIMIT_PER_TIMESLOT, COMP_LIMIT_PER_TIMESLOT,
slot_start, slot_start,
) )
@@ -431,17 +466,27 @@ async def comp(interaction: discord.Interaction, squadron_short: str):
) )
embed_limit.set_footer(text=t(lang, "comp.remaining_footer", remaining=0, limit=COMP_LIMIT_PER_TIMESLOT)) embed_limit.set_footer(text=t(lang, "comp.remaining_footer", remaining=0, limit=COMP_LIMIT_PER_TIMESLOT))
return await interaction.followup.send(embed=embed_limit) return await interaction.followup.send(embed=embed_limit)
remaining = COMP_LIMIT_PER_TIMESLOT - used - 1 user_remaining = COMP_LIMIT_PER_USER_PER_TIMESLOT - used_user - 1
server_remaining = COMP_LIMIT_PER_TIMESLOT - used_server - 1
logging.info( logging.info(
"[COMP-REMAINING] guild=%s user=%s squadron=%s remaining=%s limit=%s slot_start=%s", "[COMP-REMAINING] guild=%s user=%s squadron=%s user_remaining=%s/%s server_remaining=%s/%s slot_start=%s",
guild.id, guild.id,
interaction.user.id, interaction.user.id,
squadron_short, squadron_short,
remaining, user_remaining,
COMP_LIMIT_PER_USER_PER_TIMESLOT,
server_remaining,
COMP_LIMIT_PER_TIMESLOT, COMP_LIMIT_PER_TIMESLOT,
slot_start, slot_start,
) )
comp_footer = t(lang, "comp.remaining_footer", remaining=remaining, limit=COMP_LIMIT_PER_TIMESLOT) comp_footer = t(
lang,
"comp.remaining_footer_combined",
user_remaining=user_remaining,
user_limit=COMP_LIMIT_PER_USER_PER_TIMESLOT,
server_remaining=server_remaining,
server_limit=COMP_LIMIT_PER_TIMESLOT,
)
squadron_short = squadron_short.upper() squadron_short = squadron_short.upper()
comp_dir = STORAGE_DIR / "COMPS" comp_dir = STORAGE_DIR / "COMPS"
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Žádní hráči nezaznamenáni.", "no_players_recorded": "Žádní hráči nezaznamenáni.",
"limit_reached_title": "Limit sestav dosažen", "limit_reached_title": "Limit sestav dosažen",
"limit_reached_desc": "Tento server vyčerpal všech {limit} vyhledávání sestav pro tento časový slot. Předplaťte si (pomocí /unlock) neomezený přístup nebo počkejte na další časový slot.", "limit_reached_desc": "Tento server vyčerpal všech {limit} vyhledávání sestav pro tento časový slot. Předplaťte si (pomocí /unlock) neomezený přístup nebo počkejte na další časový slot.",
"remaining_footer": "{remaining}/{limit} vyhledávání sestav zbývá v tomto časovém slotu" "user_limit_reached_desc": "Vyčerpal jsi všech {limit} svých osobních vyhledávání sestav pro tento časový slot. Předplaťte si (pomocí /unlock) neomezený přístup nebo počkejte na další časový slot — ostatní členové serveru mohou stále využít zbývající serverovou kvótu.",
"remaining_footer": "{remaining}/{limit} vyhledávání sestav zbývá v tomto časovém slotu",
"user_remaining_footer": "{remaining}/{limit} osobních vyhledávání sestav zbývá v tomto časovém slotu",
"remaining_footer_combined": "{user_remaining}/{user_limit} osobních · {server_remaining}/{server_limit} serverových vyhledávání sestav zbývá v tomto časovém slotu"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Typ lze nastavit pouze na Logy, Body, Žebříček, Týdenní BR nebo Oba.", "invalid_type": "Typ lze nastavit pouze na Logy, Body, Žebříček, Týdenní BR nebo Oba.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Keine Spieler erfasst.", "no_players_recorded": "Keine Spieler erfasst.",
"limit_reached_title": "Aufstellungslimit erreicht", "limit_reached_title": "Aufstellungslimit erreicht",
"limit_reached_desc": "Dieser Server hat alle {limit} Aufstellungsabfragen für diesen Zeitslot verbraucht. Abonniere (mit /unlock) für unbegrenzten Zugang oder warte auf den nächsten Zeitslot.", "limit_reached_desc": "Dieser Server hat alle {limit} Aufstellungsabfragen für diesen Zeitslot verbraucht. Abonniere (mit /unlock) für unbegrenzten Zugang oder warte auf den nächsten Zeitslot.",
"remaining_footer": "{remaining}/{limit} Aufstellungsabfragen übrig in diesem Zeitslot" "user_limit_reached_desc": "Du hast alle {limit} deiner persönlichen Aufstellungsabfragen für diesen Zeitslot verbraucht. Abonniere (mit /unlock) für unbegrenzten Zugang oder warte auf den nächsten Zeitslot — andere Mitglieder dieses Servers können das verbleibende Server-Kontingent weiterhin nutzen.",
"remaining_footer": "{remaining}/{limit} Aufstellungsabfragen übrig in diesem Zeitslot",
"user_remaining_footer": "{remaining}/{limit} persönliche Aufstellungsabfragen übrig in diesem Zeitslot",
"remaining_footer_combined": "{user_remaining}/{user_limit} persönlich · {server_remaining}/{server_limit} Server-Aufstellungsabfragen übrig in diesem Zeitslot"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Typ kann nur auf Logs, Punkte, Leaderboard, Wöchentlicher BR oder Beide gesetzt werden.", "invalid_type": "Typ kann nur auf Logs, Punkte, Leaderboard, Wöchentlicher BR oder Beide gesetzt werden.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "No players recorded.", "no_players_recorded": "No players recorded.",
"limit_reached_title": "Comp Limit Reached", "limit_reached_title": "Comp Limit Reached",
"limit_reached_desc": "This server has used all {limit} comp lookups for this timeslot. Subscribe (with /unlock) for unlimited access or wait for the next timeslot.", "limit_reached_desc": "This server has used all {limit} comp lookups for this timeslot. Subscribe (with /unlock) for unlimited access or wait for the next timeslot.",
"remaining_footer": "{remaining}/{limit} comp lookups remaining this timeslot" "user_limit_reached_desc": "You have used all {limit} of your personal comp lookups for this timeslot. Subscribe (with /unlock) for unlimited access or wait for the next timeslot — other members of this server can still use the remaining server quota.",
"remaining_footer": "{remaining}/{limit} comp lookups remaining this timeslot",
"user_remaining_footer": "{remaining}/{limit} personal comp lookups remaining this timeslot",
"remaining_footer_combined": "{user_remaining}/{user_limit} personal · {server_remaining}/{server_limit} server comp lookups remaining this timeslot"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Type can only be set to Logs, Points, Leaderboard, Weekly BR, or Both.", "invalid_type": "Type can only be set to Logs, Points, Leaderboard, Weekly BR, or Both.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "No hay jugadores registrados.", "no_players_recorded": "No hay jugadores registrados.",
"limit_reached_title": "Límite de comps alcanzado", "limit_reached_title": "Límite de comps alcanzado",
"limit_reached_desc": "Este servidor ha usado las {limit} consultas de comps para este horario. Suscríbete (con /unlock) para acceso ilimitado o espera al siguiente horario.", "limit_reached_desc": "Este servidor ha usado las {limit} consultas de comps para este horario. Suscríbete (con /unlock) para acceso ilimitado o espera al siguiente horario.",
"remaining_footer": "{remaining}/{limit} consultas de comps restantes en este horario" "user_limit_reached_desc": "Has usado las {limit} consultas de comps personales para este horario. Suscríbete (con /unlock) para acceso ilimitado o espera al siguiente horario — otros miembros del servidor aún pueden usar la cuota restante del servidor.",
"remaining_footer": "{remaining}/{limit} consultas de comps restantes en este horario",
"user_remaining_footer": "{remaining}/{limit} consultas de comps personales restantes en este horario",
"remaining_footer_combined": "{user_remaining}/{user_limit} personales · {server_remaining}/{server_limit} de servidor consultas de comps restantes en este horario"
}, },
"quick_log": { "quick_log": {
"invalid_type": "El tipo solo puede ser Logs, Puntos, Clasificación, BR Semanal o Ambos.", "invalid_type": "El tipo solo puede ser Logs, Puntos, Clasificación, BR Semanal o Ambos.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Aucun joueur enregistré.", "no_players_recorded": "Aucun joueur enregistré.",
"limit_reached_title": "Limite de comps atteinte", "limit_reached_title": "Limite de comps atteinte",
"limit_reached_desc": "Ce serveur a utilisé les {limit} recherches de comps pour ce créneau. Abonnez-vous (avec /unlock) pour un accès illimité ou attendez le prochain créneau.", "limit_reached_desc": "Ce serveur a utilisé les {limit} recherches de comps pour ce créneau. Abonnez-vous (avec /unlock) pour un accès illimité ou attendez le prochain créneau.",
"remaining_footer": "{remaining}/{limit} recherches de comps restantes pour ce créneau" "user_limit_reached_desc": "Tu as utilisé tes {limit} recherches de comps personnelles pour ce créneau. Abonnez-vous (avec /unlock) pour un accès illimité ou attendez le prochain créneau — les autres membres du serveur peuvent encore utiliser le quota restant du serveur.",
"remaining_footer": "{remaining}/{limit} recherches de comps restantes pour ce créneau",
"user_remaining_footer": "{remaining}/{limit} recherches de comps personnelles restantes pour ce créneau",
"remaining_footer_combined": "{user_remaining}/{user_limit} personnelles · {server_remaining}/{server_limit} serveur recherches de comps restantes pour ce créneau"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Le type ne peut être défini que sur Logs, Points, Classement, BR Hebdomadaire ou Les deux.", "invalid_type": "Le type ne peut être défini que sur Logs, Points, Classement, BR Hebdomadaire ou Les deux.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Nessun giocatore registrato.", "no_players_recorded": "Nessun giocatore registrato.",
"limit_reached_title": "Limite composizioni raggiunto", "limit_reached_title": "Limite composizioni raggiunto",
"limit_reached_desc": "Questo server ha esaurito tutte le {limit} ricerche di composizioni per questo slot. Abbonati (con /unlock) per accesso illimitato o attendi il prossimo slot.", "limit_reached_desc": "Questo server ha esaurito tutte le {limit} ricerche di composizioni per questo slot. Abbonati (con /unlock) per accesso illimitato o attendi il prossimo slot.",
"remaining_footer": "{remaining}/{limit} ricerche di composizioni rimanenti in questo slot" "user_limit_reached_desc": "Hai esaurito tutte le {limit} ricerche di composizioni personali per questo slot. Abbonati (con /unlock) per accesso illimitato o attendi il prossimo slot — gli altri membri del server possono ancora usare la quota rimanente del server.",
"remaining_footer": "{remaining}/{limit} ricerche di composizioni rimanenti in questo slot",
"user_remaining_footer": "{remaining}/{limit} ricerche di composizioni personali rimanenti in questo slot",
"remaining_footer_combined": "{user_remaining}/{user_limit} personali · {server_remaining}/{server_limit} server ricerche di composizioni rimanenti in questo slot"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Il tipo può essere impostato solo su Log, Punti, Classifica, BR Settimanale o Entrambi.", "invalid_type": "Il tipo può essere impostato solo su Log, Punti, Classifica, BR Settimanale o Entrambi.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Brak zarejestrowanych graczy.", "no_players_recorded": "Brak zarejestrowanych graczy.",
"limit_reached_title": "Limit składów osiągnięty", "limit_reached_title": "Limit składów osiągnięty",
"limit_reached_desc": "Ten serwer wykorzystał wszystkie {limit} wyszukiwań składów w tym slocie czasowym. Subskrybuj (za pomocą /unlock) aby uzyskać nieograniczony dostęp lub poczekaj na następny slot.", "limit_reached_desc": "Ten serwer wykorzystał wszystkie {limit} wyszukiwań składów w tym slocie czasowym. Subskrybuj (za pomocą /unlock) aby uzyskać nieograniczony dostęp lub poczekaj na następny slot.",
"remaining_footer": "{remaining}/{limit} wyszukiwań składów pozostało w tym slocie czasowym" "user_limit_reached_desc": "Wykorzystałeś wszystkie {limit} swoich osobistych wyszukiwań składów w tym slocie czasowym. Subskrybuj (za pomocą /unlock) aby uzyskać nieograniczony dostęp lub poczekaj na następny slot — pozostali członkowie serwera nadal mogą korzystać z pozostałej puli serwera.",
"remaining_footer": "{remaining}/{limit} wyszukiwań składów pozostało w tym slocie czasowym",
"user_remaining_footer": "{remaining}/{limit} osobistych wyszukiwań składów pozostało w tym slocie czasowym",
"remaining_footer_combined": "{user_remaining}/{user_limit} osobistych · {server_remaining}/{server_limit} serwerowych wyszukiwań składów pozostało w tym slocie czasowym"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Typ można ustawić tylko na Logi, Punkty, Tabela liderów, Tygodniowy BR lub Oba.", "invalid_type": "Typ można ustawić tylko na Logi, Punkty, Tabela liderów, Tygodniowy BR lub Oba.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Nenhum jogador registrado.", "no_players_recorded": "Nenhum jogador registrado.",
"limit_reached_title": "Limite de composições atingido", "limit_reached_title": "Limite de composições atingido",
"limit_reached_desc": "Este servidor usou todas as {limit} consultas de composições para este horário. Assine (com /unlock) para acesso ilimitado ou aguarde o próximo horário.", "limit_reached_desc": "Este servidor usou todas as {limit} consultas de composições para este horário. Assine (com /unlock) para acesso ilimitado ou aguarde o próximo horário.",
"remaining_footer": "{remaining}/{limit} consultas de composições restantes neste horário" "user_limit_reached_desc": "Você usou todas as {limit} consultas de composições pessoais para este horário. Assine (com /unlock) para acesso ilimitado ou aguarde o próximo horário — outros membros do servidor ainda podem usar a cota restante do servidor.",
"remaining_footer": "{remaining}/{limit} consultas de composições restantes neste horário",
"user_remaining_footer": "{remaining}/{limit} consultas de composições pessoais restantes neste horário",
"remaining_footer_combined": "{user_remaining}/{user_limit} pessoais · {server_remaining}/{server_limit} servidor consultas de composições restantes neste horário"
}, },
"quick_log": { "quick_log": {
"invalid_type": "O tipo só pode ser definido como Logs, Pontos, Classificação, BR Semanal ou Ambos.", "invalid_type": "O tipo só pode ser definido como Logs, Pontos, Classificação, BR Semanal ou Ambos.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Нет зафиксированных игроков.", "no_players_recorded": "Нет зафиксированных игроков.",
"limit_reached_title": "Лимит составов достигнут", "limit_reached_title": "Лимит составов достигнут",
"limit_reached_desc": "Этот сервер использовал все {limit} запросов составов для этого таймслота. Подпишитесь (через /unlock) для безлимитного доступа или дождитесь следующего таймслота.", "limit_reached_desc": "Этот сервер использовал все {limit} запросов составов для этого таймслота. Подпишитесь (через /unlock) для безлимитного доступа или дождитесь следующего таймслота.",
"remaining_footer": "{remaining}/{limit} запросов составов осталось в этом таймслоте" "user_limit_reached_desc": "Вы использовали все {limit} ваших личных запросов составов для этого таймслота. Подпишитесь (через /unlock) для безлимитного доступа или дождитесь следующего таймслота — другие участники сервера могут продолжать использовать оставшуюся квоту сервера.",
"remaining_footer": "{remaining}/{limit} запросов составов осталось в этом таймслоте",
"user_remaining_footer": "{remaining}/{limit} личных запросов составов осталось в этом таймслоте",
"remaining_footer_combined": "{user_remaining}/{user_limit} личных · {server_remaining}/{server_limit} серверных запросов составов осталось в этом таймслоте"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Тип может быть только Логи, Очки, Таблица лидеров, Еженедельный BR или Все.", "invalid_type": "Тип может быть только Логи, Очки, Таблица лидеров, Еженедельный BR или Все.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "Гравців не зафіксовано.", "no_players_recorded": "Гравців не зафіксовано.",
"limit_reached_title": "Ліміт складів досягнуто", "limit_reached_title": "Ліміт складів досягнуто",
"limit_reached_desc": "Цей сервер використав усі {limit} запитів складів для цього таймслоту. Підпишіться (через /unlock) для безлімітного доступу або зачекайте наступного таймслоту.", "limit_reached_desc": "Цей сервер використав усі {limit} запитів складів для цього таймслоту. Підпишіться (через /unlock) для безлімітного доступу або зачекайте наступного таймслоту.",
"remaining_footer": "{remaining}/{limit} запитів складів залишилось у цьому таймслоті" "user_limit_reached_desc": "Ви використали всі {limit} ваших особистих запитів складів для цього таймслоту. Підпишіться (через /unlock) для безлімітного доступу або зачекайте наступного таймслоту — інші учасники серверу все ще можуть використовувати залишок серверної квоти.",
"remaining_footer": "{remaining}/{limit} запитів складів залишилось у цьому таймслоті",
"user_remaining_footer": "{remaining}/{limit} особистих запитів складів залишилось у цьому таймслоті",
"remaining_footer_combined": "{user_remaining}/{user_limit} особистих · {server_remaining}/{server_limit} серверних запитів складів залишилось у цьому таймслоті"
}, },
"quick_log": { "quick_log": {
"invalid_type": "Тип може бути тільки Логи, Очки, Таблиця лідерів, Тижневий BR або Усі.", "invalid_type": "Тип може бути тільки Логи, Очки, Таблиця лідерів, Тижневий BR або Усі.",
+4 -1
View File
@@ -118,7 +118,10 @@
"no_players_recorded": "没有记录到玩家。", "no_players_recorded": "没有记录到玩家。",
"limit_reached_title": "阵容查询次数已用完", "limit_reached_title": "阵容查询次数已用完",
"limit_reached_desc": "此服务器已用完本时段的 {limit} 次阵容查询。订阅(使用 /unlock)可获得无限访问,或等待下一个时段。", "limit_reached_desc": "此服务器已用完本时段的 {limit} 次阵容查询。订阅(使用 /unlock)可获得无限访问,或等待下一个时段。",
"remaining_footer": "本时段剩余 {remaining}/{limit} 次免费阵容查询" "user_limit_reached_desc": "您已用完本时段的 {limit} 次个人阵容查询。订阅(使用 /unlock)可获得无限访问,或等待下一个时段——此服务器的其他成员仍可使用剩余的服务器配额。",
"remaining_footer": "本时段剩余 {remaining}/{limit} 次免费阵容查询",
"user_remaining_footer": "本时段剩余 {remaining}/{limit} 次个人阵容查询",
"remaining_footer_combined": "本时段剩余阵容查询:个人 {user_remaining}/{user_limit} · 服务器 {server_remaining}/{server_limit}"
}, },
"quick_log": { "quick_log": {
"invalid_type": "类型只能设置为 Logs、Points、排行榜、周BR 或 全部。", "invalid_type": "类型只能设置为 Logs、Points、排行榜、周BR 或 全部。",
+25 -2
View File
@@ -222,8 +222,12 @@ def higher_tier(a: Optional[str], b: Optional[str]) -> Optional[str]:
if ra < 0 and rb < 0: if ra < 0 and rb < 0:
return None return None
return a if ra >= rb else b return a if ra >= rb else b
# Free-tier /comp cap per timeslot. # Free-tier /comp caps per timeslot.
COMP_LIMIT_PER_TIMESLOT: int = 15 # Server-wide cap counts every invocation in the guild during the window.
# Per-user cap counts each user's invocations and is enforced in addition to
# the server cap, so one user maxing out can't drain the rest of the server.
COMP_LIMIT_PER_TIMESLOT: int = 25
COMP_LIMIT_PER_USER_PER_TIMESLOT: int = 10
# ── SQB schedule (UTC, DST-immune) ─────────────────────────────────────────── # ── SQB schedule (UTC, DST-immune) ───────────────────────────────────────────
# Edit SQB_SLOTS_POSTED and the margin constants when Gaijin changes the # Edit SQB_SLOTS_POSTED and the margin constants when Gaijin changes the
@@ -1080,6 +1084,25 @@ async def get_comp_usage_in_timeslot(guild_id: int, since_ts: int) -> int:
return 0 return 0
async def get_comp_usage_in_timeslot_by_user(
guild_id: int, user_id: int, since_ts: int
) -> int:
"""Count /comp invocations for a specific user in a guild since a timestamp."""
try:
async with aiosqlite.connect(COMMAND_DATA_DB_PATH, timeout=5.0) as db:
await db.execute("PRAGMA busy_timeout=5000;")
cur = await db.execute(
"SELECT COUNT(*) FROM command_usage "
"WHERE command_name='comp' AND guild_id=? AND user_id=? AND timestamp >= ?",
(str(guild_id), str(user_id), since_ts),
)
row = await cur.fetchone()
return row[0] if row else 0
except Exception:
logging.debug("get_comp_usage_in_timeslot_by_user query failed", exc_info=True)
return 0
def minutes_ago(unix_timestamp: int) -> str: def minutes_ago(unix_timestamp: int) -> str:
"""Convert a unix timestamp to a human-readable 'X minutes ago' string.""" """Convert a unix timestamp to a human-readable 'X minutes ago' string."""
if not unix_timestamp: if not unix_timestamp: