You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
364 lines
16 KiB
364 lines
16 KiB
from collections import defaultdict, Counter
|
|
import math, traceback
|
|
|
|
from discord.ext import tasks
|
|
import discord
|
|
import asyncio
|
|
import aiohttp
|
|
import os
|
|
|
|
# ============================================================
|
|
# ХРАНИЛИЩЕ ДАННЫХ (in-memory, наполняется при инициализации)
|
|
# ============================================================
|
|
class DataStore:
|
|
def __init__(self):
|
|
self.reports = {} # id -> report dict
|
|
self.actions = defaultdict(list) # report_id -> [action, ...]
|
|
# индексы для быстрого поиска по обвиняемому
|
|
self.by_accused = defaultdict(list) # r_steam']['steam2'] -> [report_id]
|
|
self.by_author = defaultdict(list) # a_steam']['steam2'] -> [report_id]
|
|
|
|
def load(self, reports):
|
|
"""Полная загрузка/перезагрузка данных от главного приложения."""
|
|
self.reports.clear()
|
|
self.actions.clear()
|
|
self.by_accused.clear()
|
|
self.by_author.clear()
|
|
|
|
permitions = []
|
|
for r in reports:
|
|
try:
|
|
if r['r_permition'] not in permitions:
|
|
permitions.append(r['r_permition'])
|
|
|
|
if r['a_permition'] not in permitions:
|
|
permitions.append(r['a_permition'])
|
|
|
|
self.reports[r['id']] = r
|
|
if r['r_steam']:
|
|
self.by_accused[r['r_steam']['steam2']].append(r['id'])
|
|
if r['a_steam']:
|
|
self.by_author[r['a_steam']['steam2']].append(r['id'])
|
|
except:
|
|
print(r)
|
|
traceback.print_exc()
|
|
raise Exception
|
|
|
|
for a in reports:
|
|
self.actions[a['id']] = a['actions']
|
|
print(permitions)
|
|
|
|
def add_report(self, report):
|
|
"""Добавить новую жалобу (после того как модератор принял решение)."""
|
|
self.reports[report['id']] = report
|
|
self.by_accused[report['r_steam']['steam2']].append(report['id'])
|
|
self.by_author[report['a_steam']['steam2']].append(report['id'])
|
|
|
|
def add_action(self, report_id, action):
|
|
self.actions[report_id].append(action)
|
|
|
|
class KamazAi2:
|
|
action_translate = {
|
|
'ban': "забанить игрока",
|
|
'inspect': "проверить профиль стима и решить что делать дальше",
|
|
'kick': "кикнуть игрока",
|
|
'ban30': "легкий бан на 30 минут",
|
|
'ban120': "бан на пару часов",
|
|
'noreason': "не вводить причину",
|
|
'author_kick': "кикнуть автора репорта",
|
|
'mute': "замьютить игрока",
|
|
'unban': "разбанить игрока если тот в бане, ебанутое решение",
|
|
'author_inspect': 'глянуть профиль автора репорта',
|
|
'none': 'ничего не делать, лучше не лезть'
|
|
}
|
|
# ============================================================
|
|
# КЛАССИФИКАЦИЯ ACTION
|
|
# ============================================================
|
|
# Действия делятся на: применённые к обвиняемому, к автору, нейтральные.
|
|
ACTION_GROUPS = {
|
|
# против обвиняемого (жалоба обоснована)
|
|
'ban': {'target': 'accused', 'severity': 3, 'guilty': True},
|
|
'kick': {'target': 'accused', 'severity': 2, 'guilty': True},
|
|
'inspect': {'target': 'accused', 'severity': 1, 'guilty': None},
|
|
# против автора (жалоба ложная / абуз репорта)
|
|
'author_kick': {'target': 'author', 'severity': 2, 'guilty': False},
|
|
'author_inspect': {'target': 'author', 'severity': 1, 'guilty': None},
|
|
# нейтральные / отклонение
|
|
'noreason': {'target': 'none', 'severity': 0, 'guilty': False},
|
|
'none': {"target": 'none', 'severity': 0, 'guilty': False}
|
|
}
|
|
|
|
def permition_translate(self, permition):
|
|
if not permition or permition == 'DONT_HAVE':
|
|
return 0
|
|
if permition in ['VIP', 'FREE']:
|
|
return 1
|
|
if permition in ['MODERATOR']:
|
|
return 4
|
|
if permition in ['ADMIN', 'ROOT']:
|
|
return 5
|
|
|
|
return 0
|
|
|
|
def __init__(self, reports):
|
|
self.store = DataStore()
|
|
self.store.load(reports)
|
|
|
|
# ============================================================
|
|
# ЭВРИСТИКИ ПО ИГРОВЫМ МЕТРИКАМ
|
|
# ============================================================
|
|
async def kd_ratio(self, kills, deads):
|
|
try:
|
|
return kills / max(deads, 1)
|
|
except Exception as e:
|
|
return 1
|
|
|
|
|
|
async def analyze_metrics(self, report):
|
|
"""
|
|
Возвращает список факторов-сигналов (флагов) с весом и пояснением.
|
|
Положительный вес score -> жалоба скорее обоснована (вина обвиняемого).
|
|
Отрицательный -> жалоба скорее ложная.
|
|
"""
|
|
factors = []
|
|
|
|
a_kd = await self.kd_ratio(report['a_kills'], report['a_deads'])
|
|
r_kd = await self.kd_ratio(report['r_kills'], report['r_deads'])
|
|
reason = (report.get('reasons') or '').lower()
|
|
|
|
# --- Подозрение на читы: аномально высокий KD у обвиняемого ---
|
|
if any(w in reason for w in ['чит', 'cheat', 'аим', 'aim', 'wh', 'хак']):
|
|
if r_kd >= 5 and report['r_kills'] >= 30:
|
|
factors.append((+0.4, f"Обвиняемый имеет очень высокий KD ({r_kd:.1f}) "
|
|
f"при {report['r_kills']} убийствах — признак читов"))
|
|
elif r_kd < 2:
|
|
factors.append((-0.3, f"Жалоба на читы, но KD обвиняемого низкий ({r_kd:.1f}) "
|
|
f"— читы маловероятны"))
|
|
else:
|
|
factors.append((+0.1, f"Жалоба на читы, KD обвиняемого {r_kd:.1f} — требуется проверка"))
|
|
|
|
# --- Абуз прав (VIP/FreeVIP абуз) ---
|
|
if 'абуз' in reason or 'vip' in reason:
|
|
if self.permition_translate(report['r_permition']) > 0:
|
|
factors.append((+0.2, f"Обвиняемый имеет права (permition={report['r_permition']}), "
|
|
f"жалоба на абуз прав правдоподобна"))
|
|
else:
|
|
factors.append((-0.4, "Жалоба на абуз прав, но у обвиняемого нет прав — необоснованно"))
|
|
|
|
# --- Обвиняемый — администратор/модератор (защита от ложных жалоб) ---
|
|
if self.permition_translate(report['r_permition']) >= 4:
|
|
factors.append((-0.3, f"Обвиняемый имеет высокие права (permition={report['r_permition']}) "
|
|
f"— возможна ложная жалоба от обиженного игрока"))
|
|
|
|
# --- Автор играет плохо и жалуется на сильного игрока (тильт-репорт) ---
|
|
if a_kd < 0.5 and r_kd > 2 and 'чит' not in reason:
|
|
factors.append((-0.2, f"Автор играет слабо (KD {a_kd:.1f}) против сильного оппонента "
|
|
f"(KD {r_kd:.1f}) — возможен репорт «на эмоциях»"))
|
|
|
|
# --- Слишком мало времени на сервере у обвиняемого (нечего оценивать) ---
|
|
if report['r_seconds'] < 120:
|
|
factors.append((-0.15, f"Обвиняемый провёл на сервере мало времени "
|
|
f"({report['r_seconds']} сек) — мало данных"))
|
|
|
|
return factors
|
|
|
|
|
|
# ============================================================
|
|
# ПОИСК ПОХОЖИХ ИСТОРИЧЕСКИХ ЖАЛОБ
|
|
# ============================================================
|
|
async def reason_similarity(self, r1, r2):
|
|
"""Простое сравнение причин по словам (Жаккар)."""
|
|
s1 = set((r1 or '').lower().split())
|
|
s2 = set((r2 or '').lower().split())
|
|
if not s1 or not s2:
|
|
return 0.0
|
|
return len(s1 & s2) / len(s1 | s2)
|
|
|
|
|
|
async def similarity(self, new_report, old_report):
|
|
"""Оценка схожести двух жалоб (0..1)."""
|
|
score = 0.0
|
|
|
|
# тот же обвиняемый — очень сильный сигнал
|
|
if new_report['r_steam'] and old_report['r_steam'] and new_report['r_steam']['steam2'] == old_report['r_steam']['steam2']:
|
|
score += 0.5
|
|
# тот же автор
|
|
if new_report['a_steam'] and old_report['a_steam'] and new_report['a_steam']['steam2'] == old_report['a_steam']['steam2']:
|
|
score += 0.15
|
|
# похожая причина
|
|
score += 0.25 * await self.reason_similarity(new_report.get('reasons'), old_report.get('reasons'))
|
|
|
|
# схожесть KD обвиняемого
|
|
new_kd = await self.kd_ratio(new_report['r_kills'], new_report['r_deads'])
|
|
old_kd = await self.kd_ratio(old_report['r_kills'], old_report['r_deads'])
|
|
kd_diff = abs(new_kd - old_kd)
|
|
score += 0.10 * math.exp(-kd_diff / 3.0) # затухание
|
|
|
|
return min(score, 1.0)
|
|
|
|
|
|
async def find_similar(self, new_report, top_k=5, min_sim=0.15):
|
|
results = []
|
|
for rid, old in self.store.reports.items():
|
|
acts = self.store.actions.get(rid)
|
|
if not acts:
|
|
continue # без решения история бесполезна
|
|
sim = await self.similarity(new_report, old)
|
|
if sim >= min_sim:
|
|
results.append((sim, rid, old, acts))
|
|
results.sort(key=lambda x: x[0], reverse=True)
|
|
return results[:top_k]
|
|
|
|
|
|
# ============================================================
|
|
# ИТОГОВОЕ РЕШЕНИЕ
|
|
# ============================================================
|
|
async def decide(self, new_report):
|
|
# 1. Метрические факторы
|
|
factors = await self.analyze_metrics(new_report)
|
|
metric_score = sum(w for w, _ in factors)
|
|
|
|
# 2. Похожие прошлые жалобы
|
|
similar = await self.find_similar(new_report)
|
|
|
|
# Взвешенное голосование по action из похожих кейсов
|
|
action_votes = Counter()
|
|
guilty_score = 0.0
|
|
sim_total = 0.0
|
|
|
|
similar_payload = []
|
|
for sim, rid, old, acts in similar:
|
|
sim_total += sim
|
|
for act in acts:
|
|
action_votes[act] += sim
|
|
meta = self.ACTION_GROUPS.get(act, {})
|
|
if meta.get('guilty') is True:
|
|
guilty_score += sim
|
|
elif meta.get('guilty') is False:
|
|
guilty_score -= sim
|
|
similar_payload.append({
|
|
'report_id': rid,
|
|
'similarity': round(sim, 3),
|
|
'reasons': old.get('reasons'),
|
|
'r_steam2': old['r_steam']['steam2'],
|
|
'actions': acts,
|
|
})
|
|
|
|
# Нормализуем вклад истории
|
|
history_score = (guilty_score / sim_total) if sim_total > 0 else 0.0
|
|
|
|
# 3. Финальный счёт: история весит больше, чем эвристики
|
|
final_score = 0.6 * history_score + 0.4 * metric_score
|
|
|
|
# 4. Рекомендованное действие
|
|
if action_votes:
|
|
recommended_action = action_votes.most_common(1)[0][0]
|
|
else:
|
|
recommended_action = None
|
|
|
|
# Маппинг финального счёта в вердикт
|
|
if final_score >= 0.35:
|
|
verdict = 'GUILTY' # жалоба обоснована — наказать обвиняемого
|
|
if not recommended_action or self.ACTION_GROUPS.get(recommended_action, {}).get('target') != 'accused':
|
|
recommended_action = 'inspect'
|
|
elif final_score <= -0.25:
|
|
verdict = 'FALSE_REPORT' # ложная жалоба
|
|
if not recommended_action or self.ACTION_GROUPS.get(recommended_action, {}).get('target') == 'accused':
|
|
recommended_action = 'none'
|
|
else:
|
|
verdict = 'UNCERTAIN'
|
|
recommended_action = recommended_action or 'inspect'
|
|
|
|
confidence = min(abs(final_score) + 0.1 * len(similar), 1.0)
|
|
|
|
return {
|
|
'verdict': verdict,
|
|
'recommended_action': recommended_action,
|
|
'action_translate': self.action_translate.get(recommended_action, recommended_action),
|
|
'confidence': round(confidence, 3),
|
|
'final_score': round(final_score, 3),
|
|
'metric_score': round(metric_score, 3),
|
|
'history_score': round(history_score, 3),
|
|
'action_distribution': dict(action_votes),
|
|
'explanations': [text for _, text in factors],
|
|
'similar_reports': similar_payload,
|
|
}
|
|
|
|
class Extension:
|
|
core = None
|
|
|
|
def __init__(self, core):
|
|
self.core = core
|
|
self.kamazai2 = None
|
|
self.reports_list = []
|
|
|
|
async def task(self, timeout = 15):
|
|
if os.getenv('BACKEND_URL') and os.getenv("BACKEND_SECRETKEY"):
|
|
pass
|
|
else:
|
|
print("Cannot init kamazAI2, BACKEND_URL or BACKEND_SECRETKEY is missing in env")
|
|
return
|
|
|
|
await self.core.wait_until_ready()
|
|
while True:
|
|
await self.updater()
|
|
await asyncio.sleep(timeout)
|
|
|
|
async def updater(self):
|
|
try:
|
|
if self.kamazai2 == None:
|
|
print("Sync report list")
|
|
async with aiohttp.ClientSession(cookies={
|
|
"secretkey":os.getenv("BACKEND_SECRETKEY")}) as session:
|
|
async with session.get(f"{os.getenv('BACKEND_URL')}/api/discord/report/s", ssl = False) as response:
|
|
self.reports_list = await response.json()
|
|
self.kamazai2 = KamazAi2(self.reports_list)
|
|
print("KamazAI2 Enabled")
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
async def __call__(self, message: discord.Message = None):
|
|
if self.kamazai2 == None:
|
|
print("call kamazAi2 but he is not init")
|
|
return await message.reply(content=f'KamazAI2 не иницилизирован')
|
|
|
|
try:
|
|
report_id = message.embeds[0].color.value
|
|
#print("Found report id", report_id)
|
|
except:
|
|
return await message.reply(content=f'KamazAI2 не может получить индификатор репорта')
|
|
|
|
async with aiohttp.ClientSession(cookies={
|
|
"secretkey":os.getenv("BACKEND_SECRETKEY")}) as session:
|
|
async with session.get(f"{os.getenv('BACKEND_URL')}/api/discord/report/{report_id}", ssl = False) as response:
|
|
report = await response.json()
|
|
|
|
result = await self.kamazai2.decide(report)
|
|
|
|
content = f'KamazAI2 решил что с этим репортом надо сделать: \n' + result['action_translate'] + '\n' + 'Обоснование: ' + "\n".join(result['explanations']) + "\nОцените решение камаза где :thumbsup: - норм или :thumbsdown: - не очень"
|
|
response = await message.reply(content=content)
|
|
try:
|
|
await response.add_reaction('👍')
|
|
await response.add_reaction('👎')
|
|
except:
|
|
pass
|
|
|
|
return response
|
|
|
|
if __name__ == "__main__":
|
|
async def run():
|
|
print("run")
|
|
from json import load
|
|
with open("/Users/gsd/Downloads/reports.json", "r", encoding="utf8") as report_list:
|
|
kamazAi = KamazAi2(load(report_list))
|
|
|
|
test_report1 = {"id":23059,"a_nickname":"серийный чувак","a_permition":"DONT_HAVE","a_kills":29,"a_deads":38,"a_seconds":3237,"r_nickname":"Корабль Бомж 1","r_permition":"DONT_HAVE","r_kills":44,"r_deads":31,"r_seconds":3598,"reasons":"Читы","utime":1780842262,"srv":"srv9","online":18,"type":"IN_GAME","actions":[],"serverName":"Норильск 2019","a_steam":{"steam3":"[U:1:1675616295]","steam2":"STEAM_0:1:837808147","steam64":"76561199635882023","community_url":"https://steamcommunity.com/profiles/76561199635882023","account_id":1675616295},"r_steam":{"steam3":"[U:1:1546493291]","steam2":"STEAM_0:1:773246645","steam64":"76561199506759019","community_url":"https://steamcommunity.com/profiles/76561199506759019","account_id":1546493291}}
|
|
test_report2 = {"id":23048,"a_nickname":"Евгений Задротов","a_permition":"FREE","a_kills":63,"a_deads":68,"a_seconds":5031,"r_nickname":"Kapusta_KvSH","r_permition":"VIP","r_kills":11,"r_deads":2,"r_seconds":436,"reasons":"VIP абуз","utime":1780837315,"srv":"srv5","online":21,"type":"IN_GAME","actions":[],"serverName":"Завод Ultimate","a_steam":{"steam3":"[U:1:1623272121]","steam2":"STEAM_0:1:811636060","steam64":"76561199583537849","community_url":"https://steamcommunity.com/profiles/76561199583537849","account_id":1623272121},"r_steam":{"steam3":"[U:1:196806785]","steam2":"STEAM_0:1:98403392","steam64":"76561198157072513","community_url":"https://steamcommunity.com/profiles/76561198157072513","account_id":196806785}}
|
|
|
|
print(test_report1)
|
|
print(await kamazAi.decide(test_report1))
|
|
print()
|
|
print(test_report2)
|
|
print(await kamazAi.decide(test_report2))
|
|
import asyncio
|
|
asyncio.run(run())
|