clear
This commit is contained in:
9 files changed
+380
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
# Discord
|
||||||
|
DISCORD_TOKEN=
|
||||||
|
DISCORD_GUILD=
|
||||||
|
ADMIN_ROLE=
|
||||||
|
WEBHOOK=
|
||||||
|
|
||||||
|
# SFTP
|
||||||
|
SFTP_HOST=
|
||||||
|
SFTP_PORT=
|
||||||
|
SFTP_USERNAME=
|
||||||
|
SFTP_PASSWORD=
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Pycache
|
||||||
|
/__pycache__/*
|
||||||
|
*/__pycache__/*
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
|
from discord import Interaction
|
||||||
|
|
||||||
|
class Event(app_commands.Group):
|
||||||
|
def __init__(self, bot: commands.Bot, name: str, description: str):
|
||||||
|
super().__init__(name=name, description=description)
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@app_commands.command(name="start", description="Start an event leaderboard")
|
||||||
|
async def start(self, interaction: Interaction) -> None:
|
||||||
|
if self.bot._running: await interaction.response.send_message("Event already started")
|
||||||
|
self.bot.scrape_file.start()
|
||||||
|
self.bot._running = True
|
||||||
|
await interaction.response.send_message("Starting new event leaderboard...")
|
||||||
|
|
||||||
|
@app_commands.command(name="stop", description="Stop an event leaderboard")
|
||||||
|
async def stop(self, interaction: Interaction) -> None:
|
||||||
|
if not self.bot._running: await interaction.response.send_message("Event already stopped")
|
||||||
|
self.bot.scrape_file.stop()
|
||||||
|
|
||||||
|
# Reset trackers
|
||||||
|
self.bot._running = False
|
||||||
|
self.bot.log_file = ""
|
||||||
|
self.bot.log_index = -1
|
||||||
|
self.bot.players = []
|
||||||
|
self.bot.gamertags = []
|
||||||
|
self.bot.message_id = ""
|
||||||
|
|
||||||
|
await interaction.response.send_message("Stopping event leaderboard...")
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
bot.tree.add_command(Event(bot, name="event", description="start or stop an event leaderboard"))
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
TOKEN: str = os.getenv("DISCORD_TOKEN")
|
||||||
|
GUILD: str = os.getenv("DISCORD_GUILD")
|
||||||
|
ADMIN: str = os.getenv("ADMIN_ROLE")
|
||||||
|
WEBOOK: str = os.getenv("WEBHOOK")
|
||||||
|
|
||||||
|
# MONGO_URI: str = os.getenv("MONGO_URI")
|
||||||
|
# MONGO_DBO: str = os.getenv("MONGO_DBO")
|
||||||
|
|
||||||
|
SFTP_HOST: str = os.getenv("SFTP_HOST")
|
||||||
|
SFTP_PORT: int = int(os.getenv("SFTP_PORT"))
|
||||||
|
SFTP_USERNAME: str = os.getenv("SFTP_USERNAME")
|
||||||
|
SFTP_PASSWORD: str = os.getenv("SFTP_PASSWORD")
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from re import match
|
||||||
|
from paramiko import SFTPClient
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# Regex templates
|
||||||
|
class Templates:
|
||||||
|
Gamertag: str = r'(?P<time>.*) DEFAULT : BattlEye Server: Adding player identity=(?P<_>.*), name=\'(?P<gamertag>.*)\''
|
||||||
|
IpAddress: str = r'(?P<time>.*) DEFAULT : BattlEye Server: \'Player #(?P<_>.*) (?P<gamertag>.*) \((?P<ip_addr>.*):(?P<port>.*)\) connected\''
|
||||||
|
PlayerID: str = r'(?P<time>.*) NETWORK : ### Updating player: PlayerId=(?P<_>.*), Name=(?P<gamertag>.*), IdentityId=(?P<player_guid>.*)'
|
||||||
|
BattlEyeGUI: str = r'(?P<time>.*) DEFAULT : BattlEye Server: \'Player #(?P<_>.*) (?P<gamertag>.*) - BE GUID: (?P<battleye_guid>.*)\''
|
||||||
|
Admin: str = r'(?P<time>.*) NETWORK : Player \'(?P<gamertag>.*)\' signed in as server admin.'
|
||||||
|
Kill: str = r'(?P<time>.*) SCRIPT : ServerAdminTools \| Event serveradmintools_player_killed \| player: (?P<victim_gamertag>.*), instigator: (?P<killer_gamertag>.*), friendly: (?P<friendly>.*)'
|
||||||
|
|
||||||
|
# Function to process new logs in an array from a given starting index
|
||||||
|
def readLogFromIndex(
|
||||||
|
startingIndex: int,
|
||||||
|
logs: list[str],
|
||||||
|
identities: list[dict],
|
||||||
|
gamertags: list[str]
|
||||||
|
) -> Tuple[list[dict], list[str]]: # Return updated identities
|
||||||
|
|
||||||
|
# Check each line for data to extract and load into json
|
||||||
|
for i in range(startingIndex, len(logs)):
|
||||||
|
gt_data = match(Templates.Gamertag, logs[i])
|
||||||
|
identity_data = match(Templates.PlayerID, logs[i])
|
||||||
|
admin_data = match(Templates.Admin, logs[i])
|
||||||
|
kill_data = match(Templates.Kill, logs[i])
|
||||||
|
|
||||||
|
# Gather in game player GUID
|
||||||
|
if identity_data is not None:
|
||||||
|
for identity in identities:
|
||||||
|
if identity["gamertag"] == identity_data.group("gamertag"):
|
||||||
|
identities[identities.index(identity)]["player_guid"] = identity_data.group("player_guid")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Has player logged into admin/gamemaster
|
||||||
|
if admin_data is not None:
|
||||||
|
for identity in identities:
|
||||||
|
if identity["gamertag"] == admin_data.group("gamertag"):
|
||||||
|
identities[identities.index(identity)]["admin"] = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Contribute to playerts KDR
|
||||||
|
if kill_data is not None:
|
||||||
|
if kill_data.group("friendly") == "true": continue # Ignore friendly kills
|
||||||
|
if kill_data.group("killer_gamertag") == kill_data.group("victim_gamertag"): continue # Ignore suicide
|
||||||
|
for identity in identities:
|
||||||
|
if identity["gamertag"] == kill_data.group("killer_gamertag"):
|
||||||
|
index = identities.index(identity)
|
||||||
|
|
||||||
|
identities[index]["kills"] += 1
|
||||||
|
identities[index]["killstreak"] += 1
|
||||||
|
identities[index]["deathstreak"] = 0
|
||||||
|
identities[index]["bestKillstreak"] = identities[index]["killstreak"] if identities[index]["killstreak"] > identities[index]["bestKillstreak"] else identities[index]["bestKillstreak"]
|
||||||
|
identities[index]["KDR"] = round(identities[index]["kills"] / (1 if identities[index]["deaths"] == 0 else identities[index]["deaths"]), 2)
|
||||||
|
pass
|
||||||
|
if identity["gamertag"] == kill_data.group("victim_gamertag"):
|
||||||
|
index = identities.index(identity)
|
||||||
|
|
||||||
|
identities[index]["deaths"] += 1
|
||||||
|
identities[index]["deathstreak"] += 1
|
||||||
|
identities[index]["killstreak"] = 0
|
||||||
|
identities[index]["worstDeathstreak"] = identities[index]["deathstreak"] if identities[index]["deathstreak"] > identities[index]["worstDeathstreak"] else identities[index]["worstDeathstreak"]
|
||||||
|
identities[index]["KDR"] = round(identities[index]["kills"] / (1 if identities[index]["deaths"] == 0 else identities[index]["deaths"]), 2)
|
||||||
|
pass
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate new player data or update connections/IP info
|
||||||
|
if gt_data is not None:
|
||||||
|
ip_data = match(Templates.IpAddress, logs[i+1])
|
||||||
|
if gt_data.group("gamertag") in gamertags:
|
||||||
|
for identity in identities:
|
||||||
|
if identity["gamertag"] == gt_data.group("gamertag"):
|
||||||
|
index = identities.index(identity)
|
||||||
|
identities[index]["connections"] += 1
|
||||||
|
identities[index]["ip"] = ip_data.group("ip_addr")
|
||||||
|
identities[index]["port"] = ip_data.group("port")
|
||||||
|
else:
|
||||||
|
be_data = match(Templates.BattlEyeGUI, logs[i+3])
|
||||||
|
identities.append({
|
||||||
|
"gamertag": gt_data.group("gamertag"),
|
||||||
|
"ip": ip_data.group("ip_addr"),
|
||||||
|
"port": ip_data.group("port"),
|
||||||
|
"player_guid": None,
|
||||||
|
"battleye_guid": be_data.group("battleye_guid"),
|
||||||
|
"connections": 1,
|
||||||
|
"kills": 0,
|
||||||
|
"deaths": 0,
|
||||||
|
"KDR": 0,
|
||||||
|
"killstreak": 0,
|
||||||
|
"bestKillstreak": 0,
|
||||||
|
"deathstreak": 0,
|
||||||
|
"worstDeathstreak": 0,
|
||||||
|
"admin": False
|
||||||
|
})
|
||||||
|
gamertags.append(gt_data.group("gamertag"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
return identities, gamertags
|
||||||
|
|
||||||
|
def getLatestDir(sftp: SFTPClient, remote_path: str) -> str:
|
||||||
|
log_directory: str = ""
|
||||||
|
sub_directories: list[str] = sftp.listdir(remote_path)
|
||||||
|
base_datetime: datetime = datetime(2000, 1, 1)
|
||||||
|
|
||||||
|
# Read sub-directories to get latest modified directory
|
||||||
|
for directory in sub_directories:
|
||||||
|
date_time_modified = str(sftp.lstat(f"{remote_path}/{directory}"))
|
||||||
|
date_time_modified = date_time_modified[len(date_time_modified) - 14:][:-2]
|
||||||
|
file_datetime = datetime.strptime(date_time_modified, '%y %b %H:%M')
|
||||||
|
|
||||||
|
if file_datetime > base_datetime:
|
||||||
|
base_datetime = file_datetime
|
||||||
|
log_directory = directory
|
||||||
|
|
||||||
|
return log_directory
|
||||||
|
|
||||||
|
def scrape(bot) -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from paramiko import SSHClient, AutoAddPolicy, SFTPClient
|
||||||
|
from typing import Tuple
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
def createSSHClient(config: Config) -> Tuple[SFTPClient, SSHClient]:
|
||||||
|
# Create SSH client
|
||||||
|
ssh_client = SSHClient()
|
||||||
|
ssh_client.set_missing_host_key_policy(AutoAddPolicy)
|
||||||
|
|
||||||
|
# Connect to SFTP server && create SFTP session
|
||||||
|
ssh_client.connect(
|
||||||
|
hostname = config.SFTP_HOST,
|
||||||
|
port = config.SFTP_PORT,
|
||||||
|
username = config.SFTP_USERNAME,
|
||||||
|
password = config.SFTP_PASSWORD,
|
||||||
|
allow_agent = False,
|
||||||
|
look_for_keys = False,
|
||||||
|
disabled_algorithms = { 'pubkeys': ['rsa-sha2-256', 'rsa-sha2-512'] }
|
||||||
|
)
|
||||||
|
sftp = ssh_client.open_sftp()
|
||||||
|
return sftp, ssh_client
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from discord import Intents
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from src.ReforgerStatsBot import ReforgerStats
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
intents = Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
bot = ReforgerStats(Config(), "!", intents)
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
async with bot:
|
||||||
|
await bot.start(bot.config.TOKEN)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from paramiko import SSHClient, SFTPClient, SFTPFile
|
||||||
|
from discord import Intents
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext import tasks
|
||||||
|
from discord import Object
|
||||||
|
from os import listdir, getcwd
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from handlers.sftp import createSSHClient
|
||||||
|
from handlers.logs import getLatestDir, readLogFromIndex
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
class ReforgerStats(commands.Bot):
|
||||||
|
def __init__(self, config: Config, prefix: str, intents: Intents):
|
||||||
|
super().__init__(command_prefix=prefix, intents=intents)
|
||||||
|
self.config: Config = config
|
||||||
|
self._running: bool = False
|
||||||
|
self.log_dir: str = ""
|
||||||
|
self.log_index: int = -1
|
||||||
|
self.message_id: str = ""
|
||||||
|
self.players: list[dict] = []
|
||||||
|
self.gamertags: list[str] = []
|
||||||
|
self.ssh: SSHClient = None
|
||||||
|
self.sftp: SFTPClient = None
|
||||||
|
|
||||||
|
async def on_ready(self: commands.Bot) -> None:
|
||||||
|
print(f"Logged in as {self.user}")
|
||||||
|
await self.load_cogs()
|
||||||
|
# self.scrape_file.start()
|
||||||
|
|
||||||
|
async def load_cogs(self: commands.Bot):
|
||||||
|
for filename in listdir(f"{getcwd()}/cogs"):
|
||||||
|
if filename.endswith('.py'):
|
||||||
|
cog = filename[:-3]
|
||||||
|
try:
|
||||||
|
await self.load_extension(f"cogs.{cog}")
|
||||||
|
except Exception as e:
|
||||||
|
exception = f"{type(e).__name__}: {e}"
|
||||||
|
print(exception)
|
||||||
|
|
||||||
|
try:
|
||||||
|
guild = Object(id=self.config.GUILD)
|
||||||
|
self.tree.copy_global_to(guild=guild)
|
||||||
|
synced = await self.tree.sync(guild=guild)
|
||||||
|
print(f"[{guild.id}] Synced {len(synced)} command(s)")
|
||||||
|
except Exception as e:
|
||||||
|
exception = f"{type(e).__name__}: {e}"
|
||||||
|
print(exception)
|
||||||
|
|
||||||
|
async def createLeaderboardEmbed(players: list[dict]) -> dict:
|
||||||
|
sorted_players = sorted(players, key=lambda a: a["kills"], reverse=True)
|
||||||
|
description = ""
|
||||||
|
rank = 1
|
||||||
|
for player in sorted_players:
|
||||||
|
description += f"> **{rank}.** {player['gamertag']}\n> ***Kills*** {player['kills']}\n> ***Deaths*** {player['deaths']}\n\n"
|
||||||
|
rank += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"embeds": [{
|
||||||
|
"title": "Leaderboard",
|
||||||
|
"description": description
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def webhookSend(self: commands.Bot, content: dict) -> str | None:
|
||||||
|
params = { "wait": True }
|
||||||
|
resp = await requests.post(f"{self.config.WEBHOOK}", json=content, params=params)
|
||||||
|
return None if resp.status_code != 200 else resp.json()["id"]
|
||||||
|
|
||||||
|
async def webhookDeleteMessage(self) -> None:
|
||||||
|
if self.message_id == "": return
|
||||||
|
requests.delete(f"{self.config.WEBHOOK}/messages/{self.message_id}")
|
||||||
|
|
||||||
|
async def webhookUpdateMessage(self, content: dict) -> None:
|
||||||
|
if self.message_id == "": return
|
||||||
|
requests.patch(f"{self.config.WEBHOOK}/messages/{self.message_id}", json=content).status_code
|
||||||
|
|
||||||
|
@tasks.loop(seconds=60)
|
||||||
|
async def scrape_file(self) -> None:
|
||||||
|
# Create SSH && SFTP sessions if needed
|
||||||
|
if self.ssh is None or self.sftp is None: self.sftp, self.ssh = createSSHClient(self.config)
|
||||||
|
|
||||||
|
remote_path = "/profile/logs"
|
||||||
|
if self.log_dir == "": self.log_dir = getLatestDir(self.sftp, remote_path)
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
file: SFTPFile = self.sftp.open(f"{remote_path}/{self.log_dir}/console.log")
|
||||||
|
lines: list[str] = file.readlines()
|
||||||
|
if self.log_index == -1: self.log_index = len(lines) - 1
|
||||||
|
self.players, self.gamertags = readLogFromIndex(self.log_index, lines, self.players, self.gamertags)
|
||||||
|
self.log_index = len(lines) - 1
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
# Send leaderboard embeds via webhook
|
||||||
|
content: dict = self.createLeaderboardEmbed(self.players)
|
||||||
|
if self.message_id == "": self.webhookSend(content)
|
||||||
|
else: self.webhookUpdateMessage(content)
|
||||||
|
|
||||||
|
# Close SSH && SFTP sessions and file
|
||||||
|
self.sftp.close()
|
||||||
|
self.ssh.close()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
webhook = "https://discord.com/api/webhooks/1307501649119543398/N-SWGPSTuvaPEk_u9v7WdMJqKASmk0o8EzvjTFDC_1mqDjqWGybSOVGU1iVDZ-LmBsNJ"
|
||||||
|
|
||||||
|
identities = [
|
||||||
|
{"gamertag": "what", "kills": 19, "deaths": 6},
|
||||||
|
{"gamertag": "mcdazzzled", "kills": 52, "deaths": 11},
|
||||||
|
{"gamertag": "erebus", "kills": 24, "deaths": 14}
|
||||||
|
]
|
||||||
|
|
||||||
|
def createEmbed(players: list[dict]) -> dict:
|
||||||
|
sorted_players = sorted(players, key=lambda a: a["kills"], reverse=True)
|
||||||
|
description = ""
|
||||||
|
rank = 1
|
||||||
|
for player in sorted_players:
|
||||||
|
description += f"> **{rank}.** {player['gamertag']}\n> ***Kills*** {player['kills']}\n> ***Deaths*** {player['deaths']}\n\n"
|
||||||
|
rank += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"embeds": [{
|
||||||
|
"title": "Leaderboard",
|
||||||
|
"description": description
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
def send(content: dict) -> str | None:
|
||||||
|
params = { "wait": True }
|
||||||
|
resp = requests.post(f"{webhook}", json=content, params=params)
|
||||||
|
return None if resp.status_code != 200 else resp.json()["id"]
|
||||||
|
|
||||||
|
def delete(id: str) -> int:
|
||||||
|
return requests.delete(f"{webhook}/messages/{id}")
|
||||||
|
|
||||||
|
def update(content: dict, id: str) -> int:
|
||||||
|
resp = requests.patch(f"{webhook}/messages/{id}", json=content)
|
||||||
|
return resp.status_code
|
||||||
|
|
||||||
|
content = createEmbed(identities)
|
||||||
|
message_id = send(content)
|
||||||
|
if message_id is None:
|
||||||
|
print("failed")
|
||||||
|
exit()
|
||||||
|
print(identities[2])
|
||||||
|
identities[0]["kills"] = 30
|
||||||
|
time.sleep(4)
|
||||||
|
content = createEmbed(identities)
|
||||||
|
code = update(content, message_id)
|
||||||
|
print(code)
|
||||||
Reference in new issue
Block a user