fix: clean p1 p2 audit issues

This commit is contained in:
ngfchl
2026-07-07 17:26:18 +08:00
parent a5334d5652
commit e3a49910dc
62 changed files with 572 additions and 10921 deletions
+40 -4
View File
@@ -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>
+13
View File
@@ -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; }
+7 -3
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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>