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

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())