fix: clean p1 p2 audit issues
This commit is contained in:
+40
-4
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import { SButton, SButtonLoading, SCard, SConfigProvider, SInput, STag } from '@soybeanjs/ui';
|
||||
import LotteryParticipantsDialog from './components/LotteryParticipantsDialog.vue';
|
||||
|
||||
type ChatItem = Record<string, any> & { _deleting?: boolean; _ruling?: boolean };
|
||||
type BanItem = Record<string, any> & { _loading?: boolean };
|
||||
@@ -19,6 +20,9 @@ const shopItems = ref<ShopItem[]>([]);
|
||||
const shopTransactions = ref<Record<string, any>[]>([]);
|
||||
const lotteries = ref<LotteryItem[]>([]);
|
||||
const lotteryParticipants = ref<Record<string, any>[]>([]);
|
||||
const lotteryParticipantsOpen = ref(false);
|
||||
const lotteryParticipantsLoading = ref(false);
|
||||
const lotteryParticipantsTitle = ref('');
|
||||
const spamTestText = ref('');
|
||||
const spamUseAi = ref(true);
|
||||
const spamTesting = ref(false);
|
||||
@@ -271,7 +275,7 @@ async function saveShopItem() {
|
||||
finally { shopSaving.value = false; }
|
||||
}
|
||||
function newShopItem() { Object.assign(shopForm, { item_key: '', name: '', cost: 0, stock: -1, desc: '', enabled: true }); shopDialogOpen.value = true; }
|
||||
async function editShopItem(item: ShopItem) { Object.assign(shopForm, item); shopDialogOpen.value = true; }
|
||||
async function editShopItem(item: ShopItem) { Object.assign(shopForm, { item_key: item.item_key || '', name: item.name || '', cost: item.cost || 0, stock: item.stock ?? -1, desc: item.desc || '', enabled: item.enabled !== false }); shopDialogOpen.value = true; }
|
||||
async function deleteShopItem(item: ShopItem) {
|
||||
if (item._loading) return;
|
||||
if (!await askConfirm('删除商品', `确认删除商品「${item.name}」?交易记录会保留。`, { confirmText: '删除', danger: true })) return;
|
||||
@@ -286,8 +290,15 @@ async function loadLotteries() {
|
||||
try { lotteries.value = (await fetchJson(`/api/lotteries${selectedQuery.value}`)).map((x: LotteryItem) => ({ ...x, _loading: false })); } catch {}
|
||||
}
|
||||
function resetLotteryForm() { Object.assign(lotteryForm, { id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, prize_count: 1, remaining_count: 1, draw_at: '', status: 'active' }); }
|
||||
function newLottery() { resetLotteryForm(); lotteryDialogOpen.value = true; }
|
||||
function editLottery(item: LotteryItem) { if (Number(item.participants_count || 0) > 0) return showToast('已有用户参与,不能编辑,仅可开奖或关闭'); Object.assign(lotteryForm, { ...item, id: item.id || 0 }); lotteryDialogOpen.value = true; }
|
||||
function newLottery() { resetLotteryForm(); lotteryParticipants.value = []; lotteryDialogOpen.value = true; }
|
||||
function editLottery(item: LotteryItem) {
|
||||
if (Number(item.participants_count || 0) > 0) return showToast('已有用户参与,不能编辑,仅可开奖或关闭');
|
||||
const keys = ['id', 'title', 'prize', 'description', 'condition_type', 'condition_value', 'draw_type', 'end_type', 'end_value', 'max_participants', 'prize_count', 'remaining_count', 'draw_at', 'status'];
|
||||
const picked: Record<string, any> = {};
|
||||
keys.forEach((key) => { picked[key] = item[key]; });
|
||||
Object.assign(lotteryForm, picked, { id: item.id || 0 });
|
||||
lotteryDialogOpen.value = true;
|
||||
}
|
||||
async function saveLottery() {
|
||||
if (!lotteryForm.title || !lotteryForm.prize) return showToast('请填写抽奖标题和奖品');
|
||||
if (lotteryForm.condition_type === 'points' && Number(lotteryForm.condition_value || 0) <= 0) return showToast('请填写积分门槛');
|
||||
@@ -337,6 +348,23 @@ async function deleteLottery(item: LotteryItem) {
|
||||
async function loadLotteryParticipants(item: LotteryItem) {
|
||||
lotteryParticipants.value = await fetchJson(`/api/lotteries/${item.id}/participants`);
|
||||
}
|
||||
async function showParticipants(item: LotteryItem) {
|
||||
if (lotteryParticipantsLoading.value) return;
|
||||
lotteryParticipantsTitle.value = item.title || item.prize || `抽奖 #${item.id}`;
|
||||
lotteryParticipantsOpen.value = true;
|
||||
lotteryParticipantsLoading.value = true;
|
||||
try {
|
||||
await loadLotteryParticipants(item);
|
||||
} catch (e: any) {
|
||||
showToast(`加载参与记录失败:${e.message}`);
|
||||
} finally {
|
||||
lotteryParticipantsLoading.value = false;
|
||||
}
|
||||
}
|
||||
function closeParticipantsDialog() {
|
||||
lotteryParticipantsOpen.value = false;
|
||||
lotteryParticipants.value = [];
|
||||
}
|
||||
async function cancelLottery(item: LotteryItem) {
|
||||
if (item._loading) return;
|
||||
if (!await askConfirm('取消抽奖', '确认取消这个抽奖?已参与用户不会自动补偿。', { confirmText: '取消抽奖', danger: true })) return;
|
||||
@@ -819,7 +847,6 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div v-if="!lotteries.length" class="empty">暂无抽奖项目</div>
|
||||
</div>
|
||||
<div v-if="lotteryParticipants.length" class="sub-list"><h4>参与记录</h4><div v-for="p in lotteryParticipants" :key="p.id" class="mini-row">用户 {{ p.user_id }} · {{ formatTime(p.created_at) }}</div></div>
|
||||
</SCard>
|
||||
</section>
|
||||
|
||||
@@ -845,6 +872,15 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LotteryParticipantsDialog
|
||||
:open="lotteryParticipantsOpen"
|
||||
:title="lotteryParticipantsTitle"
|
||||
:loading="lotteryParticipantsLoading"
|
||||
:participants="lotteryParticipants"
|
||||
:format-time="formatTime"
|
||||
@close="closeParticipantsDialog"
|
||||
/>
|
||||
|
||||
<div v-if="toast" class="toast">{{ toast }}</div>
|
||||
<div v-if="confirmState.open" class="confirm-mask" @click.self="closeConfirm(false)">
|
||||
<div class="confirm-card">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { SButton, STag } from '@soybeanjs/ui';
|
||||
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
loading: boolean;
|
||||
participants: Record<string, any>[];
|
||||
formatTime: (value?: string | number | Date | null) => string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="open" class="confirm-mask participants-mask" @click.self="emit('close')">
|
||||
<div class="confirm-card participants-dialog">
|
||||
<div class="dialog-head">
|
||||
<div>
|
||||
<h3>参与记录</h3>
|
||||
<p>{{ title }} · {{ participants.length }} 人</p>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" @click="emit('close')">×</button>
|
||||
</div>
|
||||
<div class="participants-list">
|
||||
<div v-if="loading" class="empty">加载中...</div>
|
||||
<div v-else-if="!participants.length" class="empty">暂无参与记录</div>
|
||||
<div v-for="p in participants" v-else :key="p.id" class="participant-row">
|
||||
<div>
|
||||
<b>@{{ p.username || p.user_id }}</b>
|
||||
<span>用户 ID:{{ p.user_id }}</span>
|
||||
</div>
|
||||
<div class="participant-meta">
|
||||
<STag v-if="p.is_winner" size="xs" color="success" variant="soft">中奖</STag>
|
||||
<span>{{ formatTime(p.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="confirm-actions">
|
||||
<SButton size="xs" variant="outline" @click="emit('close')">关闭</SButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -445,3 +445,16 @@ body { background: #f3f6fb; }
|
||||
.admin-shell .log-panel,
|
||||
.admin-shell .overview-ban-panel { height: 32vh; min-height: 240px; }
|
||||
}
|
||||
|
||||
/* Lottery participants dialog */
|
||||
.confirm-mask { z-index: 70; }
|
||||
.participants-mask { z-index: 80; }
|
||||
.participants-dialog { width: min(560px, calc(100vw - 28px)); max-height: min(76vh, 620px); display: flex; flex-direction: column; padding: 0; overflow: hidden; }
|
||||
.participants-dialog .dialog-head { padding: 16px 18px 10px; border-bottom: 1px solid #e2e8f0; }
|
||||
.participants-list { min-height: 180px; max-height: 430px; overflow-y: auto; padding: 12px 14px; display: grid; gap: 8px; }
|
||||
.participant-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border: 1px solid #e2e8f0; border-radius: 14px; background: #f8fafc; }
|
||||
.participant-row div:first-child { min-width: 0; display: grid; gap: 3px; }
|
||||
.participant-row b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #0f172a; }
|
||||
.participant-row span { color: #64748b; font-size: 12px; }
|
||||
.participant-meta { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; color: #64748b; font-size: 12px; }
|
||||
.participants-dialog .confirm-actions { padding: 10px 14px 14px; border-top: 1px solid #e2e8f0; }
|
||||
|
||||
@@ -268,7 +268,7 @@ async def create_lottery_event(chat_id: int, creator_id: int, creator_name: str,
|
||||
async def lottery_condition_text(event: LotteryEvent) -> str:
|
||||
if event.condition_type == "checkin":
|
||||
return "今日已签到用户"
|
||||
if event.condition_type == "min_points":
|
||||
if event.condition_type in {"min_points", "points"}:
|
||||
return f"总积分 ≥ {event.condition_value}"
|
||||
return "所有人可参与"
|
||||
|
||||
@@ -287,11 +287,11 @@ async def count_lottery_participants(event_id: int) -> int:
|
||||
|
||||
async def check_lottery_eligible(event: LotteryEvent, user_id: int) -> tuple[bool, str]:
|
||||
if event.condition_type == "checkin":
|
||||
score = await GameScore.filter(user_id=user_id, game_type="checkin").first()
|
||||
score = await GameScore.filter(chat_id=event.chat_id, user_id=user_id, game_type="checkin").first()
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
if not score or not score.last_played or score.last_played.strftime("%Y-%m-%d") != today:
|
||||
return False, "需要今日已签到后才能参与"
|
||||
elif event.condition_type == "min_points":
|
||||
elif event.condition_type in {"min_points", "points"}:
|
||||
summary = await get_scores_summary(user_id, event.chat_id)
|
||||
if summary["total"] < event.condition_value:
|
||||
return False, f"总积分需要 ≥ {event.condition_value},当前 {summary['total']}"
|
||||
@@ -311,6 +311,10 @@ async def join_lottery_event(event_id: int, user_id: int, username: str = "", fi
|
||||
return {"ok": False, "reason": "你已经参与过了"}
|
||||
|
||||
is_instant = event.draw_type == "instant" or event.end_type == "instant"
|
||||
if (not is_instant) and event.end_type == "people" and int(event.end_value or 0) > 0:
|
||||
current_count = await LotteryParticipant.filter(event_id=event_id).using_db(conn).count()
|
||||
if current_count >= int(event.end_value or 0):
|
||||
return {"ok": False, "reason": "参与人数已满,等待开奖"}
|
||||
if is_instant and int(event.remaining_count or 0) <= 0:
|
||||
event.status = "ended"
|
||||
event.ended_at = datetime.now()
|
||||
|
||||
+137
-66
@@ -15,8 +15,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Query, Request, Response
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from db.models import Action, Ban, GameScore, Message, LotteryEvent, LotteryParticipant, ShopItem, ShopTransaction
|
||||
from config import WEB_AUTH_ENABLED, WEB_AUTH_PASSWORD, WEB_AUTH_USERNAME, WEB_SESSION_SECRET, WEB_TRUST_SAME_SUBNET
|
||||
@@ -28,6 +30,13 @@ from services import spam_detector
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
app = FastAPI(title="TG Spam Guard Manager")
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
return JSONResponse({"ok": False, "error": "请求参数无效", "details": exc.errors()}, status_code=422)
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
||||
|
||||
# SSE
|
||||
@@ -191,7 +200,12 @@ async def api_auth_status(request: Request):
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
async def api_auth_login(request: Request):
|
||||
data = await request.json()
|
||||
try:
|
||||
data = await request.json()
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
return JSONResponse({"ok": False, "error": "请求体必须是合法 JSON"}, status_code=400)
|
||||
username = str(data.get("username") or "")
|
||||
password = str(data.get("password") or "")
|
||||
if not (hmac.compare_digest(username, WEB_AUTH_USERNAME) and hmac.compare_digest(password, WEB_AUTH_PASSWORD)):
|
||||
@@ -209,6 +223,71 @@ async def api_auth_logout():
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class LoginPayload(BaseModel):
|
||||
username: str = Field(default='', max_length=80)
|
||||
password: str = Field(default='', max_length=200)
|
||||
|
||||
|
||||
class SendPayload(BaseModel):
|
||||
text: str = Field(default='', max_length=4000)
|
||||
chat_id: int | None = None
|
||||
|
||||
|
||||
class ShopItemPayload(BaseModel):
|
||||
chat_id: int = 0
|
||||
item_key: str = Field(default='', max_length=64)
|
||||
name: str = Field(default='', max_length=80)
|
||||
cost: int = Field(default=0, ge=0, le=1_000_000)
|
||||
desc: str = Field(default='', max_length=500)
|
||||
enabled: bool = True
|
||||
stock: int = Field(default=-1, ge=-1, le=1_000_000)
|
||||
|
||||
|
||||
class LotteryPayload(BaseModel):
|
||||
chat_id: int = 0
|
||||
creator_id: int = 0
|
||||
creator_name: str = Field(default='web-admin', max_length=80)
|
||||
title: str = Field(default='', max_length=120)
|
||||
description: str = Field(default='', max_length=500)
|
||||
prize: str = Field(default='', max_length=120)
|
||||
condition_type: str = Field(default='all', max_length=32)
|
||||
condition_value: int = Field(default=0, ge=0, le=1_000_000)
|
||||
draw_type: str = Field(default='people', max_length=32)
|
||||
draw_at: str | None = None
|
||||
end_type: str = Field(default='people', max_length=32)
|
||||
end_value: int = Field(default=0, ge=0, le=1_000_000)
|
||||
max_participants: int = Field(default=0, ge=0, le=1_000_000)
|
||||
prize_count: int = Field(default=1, ge=1, le=1_000_000)
|
||||
remaining_count: int | None = Field(default=None, ge=0, le=1_000_000)
|
||||
status: str = Field(default='active', max_length=32)
|
||||
|
||||
|
||||
class SpamTestPayload(BaseModel):
|
||||
text: str = Field(default='', max_length=5000)
|
||||
use_ai: bool = False
|
||||
|
||||
|
||||
class SpamRulePayload(BaseModel):
|
||||
type: str = Field(default='keyword', max_length=16)
|
||||
pattern: str = Field(default='', max_length=500)
|
||||
score: int = Field(default=60, ge=1, le=200)
|
||||
async def safe_json(request: Request) -> dict:
|
||||
if not request.headers.get("content-type", "").startswith("application/json"):
|
||||
return {}
|
||||
try:
|
||||
data = await request.json()
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def normalize_condition_type(value: str) -> str:
|
||||
value = (value or "all").strip()
|
||||
return "min_points" if value == "points" else value
|
||||
|
||||
|
||||
def json_safe(value: Any) -> Any:
|
||||
"""将 ORM values() 结果转成 JSONResponse 可序列化对象。"""
|
||||
if isinstance(value, datetime):
|
||||
@@ -379,16 +458,19 @@ async def api_shop_items(chat_id: int = 0):
|
||||
|
||||
|
||||
@app.post("/api/shop-items")
|
||||
async def api_save_shop_item(request: Request):
|
||||
data = await request.json()
|
||||
async def api_save_shop_item(data: ShopItemPayload):
|
||||
item_key = data.item_key.strip()
|
||||
name = data.name.strip()
|
||||
if not item_key or not name:
|
||||
return JSONResponse({"ok": False, "error": "商品编号和名称不能为空"}, status_code=400)
|
||||
item = await upsert_shop_item(
|
||||
chat_id=int(data.get("chat_id") or 0),
|
||||
item_key=str(data.get("item_key") or "").strip(),
|
||||
name=str(data.get("name") or "").strip(),
|
||||
cost=int(data.get("cost") or 0),
|
||||
desc=str(data.get("desc") or ""),
|
||||
enabled=bool(data.get("enabled", True)),
|
||||
stock=int(data.get("stock", -1) if data.get("stock", -1) not in (None, "") else -1),
|
||||
chat_id=data.chat_id,
|
||||
item_key=item_key,
|
||||
name=name,
|
||||
cost=data.cost,
|
||||
desc=data.desc,
|
||||
enabled=data.enabled,
|
||||
stock=data.stock,
|
||||
)
|
||||
return JSONResponse(json_safe({"ok": True, "item": {"id": item.id, "chat_id": item.chat_id, "item_key": item.item_key, "name": item.name, "cost": item.cost, "stock": item.stock, "desc": item.desc, "enabled": item.enabled}}))
|
||||
|
||||
@@ -416,25 +498,29 @@ async def api_lotteries(chat_id: int | None = None, limit: int = Query(default=1
|
||||
|
||||
|
||||
@app.post("/api/lotteries")
|
||||
async def api_create_lottery(request: Request):
|
||||
data = await request.json()
|
||||
async def api_create_lottery(data: LotteryPayload):
|
||||
title = (data.title or data.prize or "抽奖活动").strip()
|
||||
prize = (data.prize or data.title or "奖品").strip()
|
||||
if not title or not prize:
|
||||
return JSONResponse({"ok": False, "error": "抽奖标题和奖品不能为空"}, status_code=400)
|
||||
draw_type = data.draw_type or data.end_type or "manual"
|
||||
event = await LotteryEvent.create(
|
||||
chat_id=int(data.get("chat_id") or 0),
|
||||
creator_id=int(data.get("creator_id") or 0),
|
||||
creator_name=str(data.get("creator_name") or "web-admin"),
|
||||
title=str(data.get("title") or data.get("prize") or "抽奖活动"),
|
||||
description=str(data.get("description") or ""),
|
||||
prize=str(data.get("prize") or data.get("title") or "奖品"),
|
||||
condition_type=str(data.get("condition_type") or "all"),
|
||||
condition_value=int(data.get("condition_value") or 0),
|
||||
draw_type=str(data.get("draw_type") or data.get("end_type") or "manual"),
|
||||
draw_at=parse_draw_at(data.get("draw_at")),
|
||||
end_type=str(data.get("end_type") or data.get("draw_type") or "manual"),
|
||||
end_value=int(data.get("end_value") or data.get("max_participants") or 0),
|
||||
max_participants=int(data.get("max_participants") or 0),
|
||||
prize_count=max(1, int(data.get("prize_count") or 1)),
|
||||
remaining_count=max(0, int(data.get("remaining_count") if data.get("remaining_count") is not None else data.get("prize_count") or 1)),
|
||||
status=str(data.get("status") or "draft"),
|
||||
chat_id=data.chat_id,
|
||||
creator_id=data.creator_id,
|
||||
creator_name=data.creator_name or "web-admin",
|
||||
title=title,
|
||||
description=data.description,
|
||||
prize=prize,
|
||||
condition_type=normalize_condition_type(data.condition_type),
|
||||
condition_value=data.condition_value,
|
||||
draw_type=draw_type,
|
||||
draw_at=parse_draw_at(data.draw_at),
|
||||
end_type=data.end_type or draw_type,
|
||||
end_value=data.end_value or data.max_participants,
|
||||
max_participants=data.max_participants,
|
||||
prize_count=max(1, data.prize_count),
|
||||
remaining_count=max(0, data.remaining_count if data.remaining_count is not None else data.prize_count),
|
||||
status=data.status or "draft",
|
||||
)
|
||||
return JSONResponse(json_safe({"ok": True, "event": {
|
||||
"id": event.id, "chat_id": event.chat_id, "title": event.title, "description": event.description,
|
||||
@@ -446,35 +532,26 @@ async def api_create_lottery(request: Request):
|
||||
|
||||
|
||||
@app.put("/api/lotteries/{event_id}")
|
||||
async def api_update_lottery(event_id: int, request: Request):
|
||||
async def api_update_lottery(event_id: int, data: LotteryPayload):
|
||||
event = await LotteryEvent.filter(id=event_id).first()
|
||||
if not event:
|
||||
return JSONResponse({"ok": False, "error": "抽奖不存在"}, status_code=404)
|
||||
participant_count = await LotteryParticipant.filter(event_id=event_id).count()
|
||||
if participant_count > 0:
|
||||
return JSONResponse({"ok": False, "error": "已有用户参与,不能编辑抽奖;仅可强制开奖或关闭"}, status_code=409)
|
||||
data = await request.json()
|
||||
for key in ["title", "description", "prize", "condition_type", "draw_type", "end_type", "status"]:
|
||||
if key in data:
|
||||
setattr(event, key, str(data.get(key) or ""))
|
||||
if "draw_at" in data:
|
||||
event.draw_at = parse_draw_at(data.get("draw_at"))
|
||||
for key in ["condition_value", "end_value", "max_participants", "prize_count", "remaining_count"]:
|
||||
if key in data:
|
||||
value = int(data.get(key) or 0)
|
||||
if key == "prize_count":
|
||||
value = max(1, value)
|
||||
if key == "remaining_count":
|
||||
value = max(0, value)
|
||||
setattr(event, key, value)
|
||||
for key in ["title", "description", "prize", "draw_type", "end_type", "status"]:
|
||||
value = getattr(data, key)
|
||||
if value is not None:
|
||||
setattr(event, key, str(value or ""))
|
||||
event.condition_type = normalize_condition_type(data.condition_type)
|
||||
event.draw_at = parse_draw_at(data.draw_at)
|
||||
event.condition_value = data.condition_value
|
||||
event.end_value = data.end_value
|
||||
event.max_participants = data.max_participants
|
||||
event.prize_count = max(1, data.prize_count)
|
||||
event.remaining_count = max(0, data.remaining_count if data.remaining_count is not None else data.prize_count)
|
||||
await event.save()
|
||||
return JSONResponse(json_safe({"ok": True, "event": {
|
||||
"id": event.id, "chat_id": event.chat_id, "title": event.title, "description": event.description,
|
||||
"prize": event.prize, "condition_type": event.condition_type, "condition_value": event.condition_value,
|
||||
"draw_type": event.draw_type, "end_type": event.end_type, "end_value": event.end_value,
|
||||
"max_participants": event.max_participants, "prize_count": event.prize_count, "remaining_count": event.remaining_count,
|
||||
"draw_at": event.draw_at, "status": event.status, "created_at": event.created_at,
|
||||
}}))
|
||||
return JSONResponse(json_safe({"ok": True, "event": await LotteryEvent.filter(id=event_id).values().first()}))
|
||||
|
||||
|
||||
@app.delete("/api/lotteries/{event_id}")
|
||||
@@ -570,10 +647,9 @@ async def api_list_spam_rules(limit: int = Query(default=500, ge=1, le=2000)):
|
||||
|
||||
|
||||
@app.post("/api/spam-test")
|
||||
async def api_spam_test(request: Request):
|
||||
data = await request.json()
|
||||
text = str(data.get("text") or "").strip()
|
||||
use_ai = bool(data.get("use_ai"))
|
||||
async def api_spam_test(data: SpamTestPayload):
|
||||
text = data.text.strip()
|
||||
use_ai = data.use_ai
|
||||
if not text:
|
||||
return JSONResponse({"ok": False, "error": "请输入要检测的文本"}, status_code=400)
|
||||
decision, reason, score = spam_detector.local_classify(text)
|
||||
@@ -595,14 +671,10 @@ async def api_spam_test(request: Request):
|
||||
|
||||
|
||||
@app.post("/api/spam-rules")
|
||||
async def api_add_spam_rule(request: Request):
|
||||
data = await request.json()
|
||||
rule_type = str(data.get("type") or "keyword").strip().lower()
|
||||
pattern = str(data.get("pattern") or "").strip()
|
||||
try:
|
||||
score = int(data.get("score") or 60)
|
||||
except (TypeError, ValueError):
|
||||
score = 60
|
||||
async def api_add_spam_rule(data: SpamRulePayload):
|
||||
rule_type = data.type.strip().lower()
|
||||
pattern = data.pattern.strip()
|
||||
score = data.score
|
||||
if rule_type not in {"keyword", "regex"}:
|
||||
return JSONResponse({"ok": False, "error": "规则类型只能是 keyword 或 regex"}, status_code=400)
|
||||
if not pattern or len(pattern) > 500:
|
||||
@@ -649,10 +721,9 @@ async def api_restart():
|
||||
|
||||
|
||||
@app.post("/api/send")
|
||||
async def api_send(request: Request):
|
||||
async def api_send(data: SendPayload):
|
||||
import config
|
||||
data = await request.json()
|
||||
text = str(data.get("text", "")).strip()
|
||||
text = data.text.strip()
|
||||
if not text:
|
||||
return JSONResponse({"ok": False, "error": "消息不能为空"}, status_code=400)
|
||||
if len(text) > 3500:
|
||||
@@ -733,7 +804,7 @@ async def api_delete_message(message_id: int, request: Request):
|
||||
import config
|
||||
from telegram.error import BadRequest
|
||||
|
||||
data = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
|
||||
data = await safe_json(request)
|
||||
ban_user = bool(data.get("ban_user") or data.get("ban"))
|
||||
|
||||
rec = await Message.get_or_none(id=message_id)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
|
||||
<title>TG Spam Guard</title>
|
||||
<script type="module" crossorigin src="/static/assets/index-hdRhBQiV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-wm58xfzu.css">
|
||||
<script type="module" crossorigin src="/static/assets/index-BtxpMffg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-es9pSdo5.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user