Slowly getting more usable
This commit is contained in:
parent
3455f323fc
commit
cbb93dd2e0
|
@ -3,5 +3,8 @@
|
||||||
nixCargoIntegration.url = "github:yusdacra/nix-cargo-integration";
|
nixCargoIntegration.url = "github:yusdacra/nix-cargo-integration";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs: inputs.nixCargoIntegration.lib.makeOutputs { root = ./.; };
|
outputs = inputs:
|
||||||
|
(inputs.nixCargoIntegration.lib.makeOutputs { root = ./.; }) // {
|
||||||
|
nixosModules.glados = { imports = [ ./glados-module.nix ]; };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
112
glados-module.nix
Normal file
112
glados-module.nix
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
{ pkgs, lib, config, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.glados;
|
||||||
|
ownerArgs = list: toString (map (id: "--owner ${id}") list);
|
||||||
|
in {
|
||||||
|
options.services.glados = {
|
||||||
|
enable = lib.mkEnableOption "GLaDOS systemd service";
|
||||||
|
|
||||||
|
discord = {
|
||||||
|
guildId = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Discord guild ID";
|
||||||
|
example = "000000000000000000";
|
||||||
|
};
|
||||||
|
|
||||||
|
ownerIds = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
description = "Discord guild owner IDs";
|
||||||
|
example = "[ 000000000000000000 ]";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminChannel = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Discord Admin channel ID";
|
||||||
|
example = "000000000000000000";
|
||||||
|
};
|
||||||
|
|
||||||
|
infoChannel = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Discord Info channel ID";
|
||||||
|
example = "000000000000000000";
|
||||||
|
};
|
||||||
|
|
||||||
|
botToken = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Discord bot token";
|
||||||
|
example = "AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAA-AAAAAAA";
|
||||||
|
};
|
||||||
|
|
||||||
|
camRole = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Role used for Minecraft cam accounts";
|
||||||
|
example = "000000000000000000";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
influx = {
|
||||||
|
host = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Influx db host url";
|
||||||
|
example = "localhost:8000";
|
||||||
|
};
|
||||||
|
|
||||||
|
database = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Influx database to store time series in";
|
||||||
|
example = "minecraft";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Influx database user";
|
||||||
|
example = "minecraft";
|
||||||
|
};
|
||||||
|
|
||||||
|
password = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Influx database user password";
|
||||||
|
example = "minecraft";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
rcon = {
|
||||||
|
address = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "RCON address";
|
||||||
|
example = "your-host.com:12345";
|
||||||
|
};
|
||||||
|
|
||||||
|
password = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "RCON password";
|
||||||
|
example = "your-secret-password";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
systemd.services.glados = {
|
||||||
|
wantedBy = [ "default.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = ''
|
||||||
|
${pkgs.glados}/bin/glados
|
||||||
|
--guild-id ${cfg.discord.guildId}
|
||||||
|
${ownerArgs cfg.discord.ownerIds}
|
||||||
|
--admin-channel ${cfg.discord.adminChannel}
|
||||||
|
--info-channel ${cfg.discord.infoChannel}
|
||||||
|
--discord-token ${cfg.discord.botToken}
|
||||||
|
--cam-group-name ${cfg.discord.camRole}
|
||||||
|
--influx-host ${cfg.influx.host}
|
||||||
|
--influx-db ${cfg.influx.database}
|
||||||
|
--influx-user ${cfg.influx.user}
|
||||||
|
--influx-password ${cfg.influx.password}
|
||||||
|
--rcon-address ${cfg.rcon.address}
|
||||||
|
--rcon-password ${cfg.rcon.password}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -77,6 +77,9 @@ pub struct Args {
|
||||||
|
|
||||||
#[structopt(long, value_name = "PW", env = "INFLUX_PW", hide_env_values = true)]
|
#[structopt(long, value_name = "PW", env = "INFLUX_PW", hide_env_values = true)]
|
||||||
pub influx_password: String,
|
pub influx_password: String,
|
||||||
|
|
||||||
|
#[structopt(long, value_name = "NAME", env = "MC_CAM_GROUP")]
|
||||||
|
pub cam_group_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_token(raw: String) -> std::result::Result<(), String> {
|
fn validate_token(raw: String) -> std::result::Result<(), String> {
|
||||||
|
|
|
@ -22,6 +22,8 @@ pub enum Error {
|
||||||
RconTpsCmd(#[source] nom::Err<nom::error::Error<String>>),
|
RconTpsCmd(#[source] nom::Err<nom::error::Error<String>>),
|
||||||
#[error("Parsing results of `list` command: {_0}")]
|
#[error("Parsing results of `list` command: {_0}")]
|
||||||
RconListCmd(#[source] nom::Err<nom::error::Error<String>>),
|
RconListCmd(#[source] nom::Err<nom::error::Error<String>>),
|
||||||
|
#[error("Parsing results of `whois` command: {_0}")]
|
||||||
|
RconWhoIsCmd(#[source] nom::Err<nom::error::Error<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ResultExt<T> {
|
pub trait ResultExt<T> {
|
||||||
|
|
|
@ -14,6 +14,8 @@ mod tracking;
|
||||||
use discord::Bot;
|
use discord::Bot;
|
||||||
use error::Result;
|
use error::Result;
|
||||||
|
|
||||||
|
type UUID<'s> = &'s str;
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
match real_main().await {
|
match real_main().await {
|
||||||
|
|
38
src/rcon.rs
38
src/rcon.rs
|
@ -2,6 +2,10 @@ use std::time::Duration;
|
||||||
|
|
||||||
use serenity::client::Context;
|
use serenity::client::Context;
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
pub use commands::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
conf::ARGS,
|
conf::ARGS,
|
||||||
data,
|
data,
|
||||||
|
@ -29,19 +33,41 @@ impl RconHandle {
|
||||||
where
|
where
|
||||||
C: RconCommand,
|
C: RconCommand,
|
||||||
{
|
{
|
||||||
|
let cmd_str = cmd.as_string();
|
||||||
let mut rcon = ctx.data.write().await;
|
let mut rcon = ctx.data.write().await;
|
||||||
let rcon = rcon.get_mut::<data::Rcon>().expect("BUG: No rcon client");
|
let rcon = rcon.get_mut::<data::Rcon>().expect("BUG: No rcon client");
|
||||||
rcon.reconnect_until_success().await;
|
rcon.reconnect_until_success().await;
|
||||||
let cmd_str = cmd.as_string();
|
let result = rcon.execute_raw_command(&cmd_str).await;
|
||||||
let raw_output = rcon
|
let raw_output = match result {
|
||||||
.inner
|
Ok(raw_output) => raw_output,
|
||||||
|
Err(why) => {
|
||||||
|
match why {
|
||||||
|
rcon::Error::Io(_) => {
|
||||||
|
// Reset the connection and retry
|
||||||
|
tracing::warn!("IO error while executing RCON command, retrying");
|
||||||
|
rcon.inner = None;
|
||||||
|
rcon.reconnect_until_success().await;
|
||||||
|
rcon.execute_raw_command(&cmd_str)
|
||||||
|
.await
|
||||||
|
.map_err(|why| Error::RconCommand(cmd_str, why))?
|
||||||
|
}
|
||||||
|
why => return Err(Error::RconCommand(cmd_str, why)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cmd.parse(raw_output)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_raw_command(
|
||||||
|
&mut self,
|
||||||
|
cmd: &str,
|
||||||
|
) -> ::std::result::Result<String, rcon::Error> {
|
||||||
|
self.inner
|
||||||
.as_mut()
|
.as_mut()
|
||||||
// We must be connected here
|
// We must be connected here
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.cmd(&cmd_str)
|
.cmd(&cmd)
|
||||||
.await
|
.await
|
||||||
.map_err(|why| Error::RconCommand(cmd_str, why))?;
|
|
||||||
cmd.parse(raw_output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reconnect_until_success(&mut self) {
|
async fn reconnect_until_success(&mut self) {
|
||||||
|
|
5
src/rcon/commands.rs
Normal file
5
src/rcon/commands.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod luckperms_info;
|
||||||
|
mod whois;
|
||||||
|
|
||||||
|
pub use luckperms_info::LuckPermsParentOfCmd;
|
||||||
|
pub use whois::{WhoIs, WhoIsCmd};
|
18
src/rcon/commands/luckperms_info.rs
Normal file
18
src/rcon/commands/luckperms_info.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use crate::{error::Result, rcon::RconCommand, UUID};
|
||||||
|
|
||||||
|
type ParentGroup = String;
|
||||||
|
|
||||||
|
pub struct LuckPermsParentOfCmd<'s>(pub UUID<'s>);
|
||||||
|
|
||||||
|
impl RconCommand for LuckPermsParentOfCmd<'_> {
|
||||||
|
type Output = ParentGroup;
|
||||||
|
|
||||||
|
fn as_string(&self) -> String {
|
||||||
|
format!("lp user {} parent info", self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(&self, raw: String) -> Result<Self::Output> {
|
||||||
|
// TODO: Currently broken, lp commands don't work via rcon
|
||||||
|
Ok(raw)
|
||||||
|
}
|
||||||
|
}
|
99
src/rcon/commands/whois.rs
Normal file
99
src/rcon/commands/whois.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{is_not, tag},
|
||||||
|
combinator::{map, value},
|
||||||
|
sequence::{preceded, tuple},
|
||||||
|
IResult, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{Error, Result},
|
||||||
|
rcon::RconCommand,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct WhoIsCmd<'s>(pub &'s str);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct WhoIs {
|
||||||
|
pub nick: String,
|
||||||
|
pub uuid: String,
|
||||||
|
pub afk: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RconCommand for WhoIsCmd<'_> {
|
||||||
|
type Output = WhoIs;
|
||||||
|
|
||||||
|
fn as_string(&self) -> String {
|
||||||
|
format!("whois {}", self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(&self, raw: String) -> Result<Self::Output> {
|
||||||
|
parse_whois_result(&raw)
|
||||||
|
.map(|(_, whois)| whois)
|
||||||
|
.map_err(|why| Error::RconWhoIsCmd(why.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// §6 ====== WhoIs:§c gloom_17 §6======\n§6 - Nick:§r gloom_17\n§6 - UUID:§r 1fea9fa1-7ee0-4c5f-b4d8-97185709cf33\n§6 - Health:§r 18.56/20\n§6 - Hunger:§r 16/20 (+0 saturation)\n§6 - Exp:§r 1,545 (Level 31)\n§6 - Location:§r (world, 443, 64, 73)\n§6 - Playtime:§r 2 days 2 hours 32 minutes\n§6 - Money:§r $0\n§6 - IP Address:§r /213.142.97.187\n§6 - Gamemode:§r survival\n§6 - God mode:§r §4false§r\n§6 - OP:§r §4false§r\n§6 - Fly mode:§r §4false§r (not flying)\n§6 - Speed:§r 0.2\n§6 - AFK:§r §4false§r\n§6 - Jail:§r §4false§r\n§6 - Muted:§r §4false§r\n
|
||||||
|
|
||||||
|
pub fn parse_whois_result(inp: &str) -> IResult<&str, WhoIs> {
|
||||||
|
let list = tuple((
|
||||||
|
prop("Nick", parse_string),
|
||||||
|
prop("UUID", parse_string),
|
||||||
|
prop("Health", ignore),
|
||||||
|
prop("Hunger", ignore),
|
||||||
|
prop("Exp", ignore),
|
||||||
|
prop("Location", ignore),
|
||||||
|
prop("Playtime", ignore),
|
||||||
|
prop("Money", ignore),
|
||||||
|
prop("IP Address", ignore),
|
||||||
|
prop("Gamemode", ignore),
|
||||||
|
prop("God mode", ignore),
|
||||||
|
prop("OP", ignore),
|
||||||
|
prop("Fly mode", ignore),
|
||||||
|
prop("Speed", ignore),
|
||||||
|
prop("AFK", parse_bool),
|
||||||
|
prop("Jail", ignore),
|
||||||
|
prop("Muted", ignore),
|
||||||
|
));
|
||||||
|
let parser = preceded(header, list);
|
||||||
|
map(
|
||||||
|
parser,
|
||||||
|
|(nick, uuid, (), (), (), (), (), (), (), (), (), (), (), (), afk, (), ())| WhoIs {
|
||||||
|
nick,
|
||||||
|
uuid,
|
||||||
|
afk,
|
||||||
|
},
|
||||||
|
)(inp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(inp: &str) -> IResult<&str, ()> {
|
||||||
|
value(
|
||||||
|
(),
|
||||||
|
tuple((tag("§6 ====== WhoIs:§c "), is_not(" "), tag(" §6======\n"))),
|
||||||
|
)(inp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prop<'i, P, O, E>(name: &'i str, parser: P) -> impl Parser<&'i str, O, E>
|
||||||
|
where
|
||||||
|
P: Parser<&'i str, O, E>,
|
||||||
|
E: nom::error::ParseError<&'i str>,
|
||||||
|
{
|
||||||
|
map(
|
||||||
|
tuple((tag("§6 - "), tag(name), tag(":§r "), parser, tag("\n"))),
|
||||||
|
|(_, _, _, res, _)| res,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_string(inp: &str) -> IResult<&str, String> {
|
||||||
|
map(is_not("\n"), |s: &str| s.to_owned())(inp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignore(inp: &str) -> IResult<&str, ()> {
|
||||||
|
value((), is_not("\n"))(inp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_bool(inp: &str) -> IResult<&str, bool> {
|
||||||
|
alt((value(true, tag("§4true§r")), value(false, tag("§4false§r"))))(inp)
|
||||||
|
}
|
63
src/state.rs
63
src/state.rs
|
@ -8,12 +8,15 @@ use crate::{
|
||||||
conf::ARGS,
|
conf::ARGS,
|
||||||
data,
|
data,
|
||||||
error::{Error, Result, ResultExt},
|
error::{Error, Result, ResultExt},
|
||||||
|
rcon::{LuckPermsParentOfCmd, RconHandle, WhoIsCmd},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
/// Keeps track of the current guild members
|
/// Keeps track of the current guild members
|
||||||
pub guild_members: HashSet<GuildMember>,
|
pub guild_members: HashSet<GuildMember>,
|
||||||
|
/// Players we've seen on the Minecraft server
|
||||||
|
pub known_players: HashSet<MinecraftPlayer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
@ -22,6 +25,16 @@ pub struct GuildMember {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MinecraftPlayer {
|
||||||
|
/// Unique identifier
|
||||||
|
pub uuid: String,
|
||||||
|
/// Current ingame name
|
||||||
|
pub ign: String,
|
||||||
|
/// Is this account a cam account
|
||||||
|
pub is_cam_acc: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub async fn load() -> Result<FileDatabase<Self, Yaml>> {
|
pub async fn load() -> Result<FileDatabase<Self, Yaml>> {
|
||||||
let db = FileDatabase::load_from_path_or_default(&ARGS.database_path)?;
|
let db = FileDatabase::load_from_path_or_default(&ARGS.database_path)?;
|
||||||
|
@ -68,6 +81,32 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MinecraftPlayer {
|
||||||
|
pub async fn from_uuid(ctx: &Context, uuid: String) -> Result<Self> {
|
||||||
|
// Try fetching the player from our state
|
||||||
|
let player = State::read(ctx, |state| state.known_players.get(&uuid).cloned()).await?;
|
||||||
|
// If player is none, query the rcon for missing information
|
||||||
|
let player = match player {
|
||||||
|
Some(player) => player,
|
||||||
|
None => {
|
||||||
|
let whois = RconHandle::cmd(&WhoIsCmd(&uuid), ctx).await?;
|
||||||
|
let parent_group = RconHandle::cmd(&LuckPermsParentOfCmd(&uuid), ctx).await?;
|
||||||
|
let player = MinecraftPlayer {
|
||||||
|
uuid: whois.uuid,
|
||||||
|
ign: whois.nick,
|
||||||
|
is_cam_acc: parent_group == ARGS.cam_group_name,
|
||||||
|
};
|
||||||
|
State::write(ctx, |state| state.known_players.insert(player.clone()))
|
||||||
|
.await
|
||||||
|
.log_warn("adding player to set of known players");
|
||||||
|
player
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<UserId> for GuildMember {
|
impl From<UserId> for GuildMember {
|
||||||
fn from(id: UserId) -> Self {
|
fn from(id: UserId) -> Self {
|
||||||
Self { id }
|
Self { id }
|
||||||
|
@ -86,10 +125,30 @@ impl PartialEq for GuildMember {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for GuildMember {}
|
|
||||||
|
|
||||||
impl Borrow<UserId> for GuildMember {
|
impl Borrow<UserId> for GuildMember {
|
||||||
fn borrow(&self) -> &UserId {
|
fn borrow(&self) -> &UserId {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for GuildMember {}
|
||||||
|
|
||||||
|
impl Hash for MinecraftPlayer {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.uuid.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for MinecraftPlayer {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.uuid.eq(&other.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for MinecraftPlayer {}
|
||||||
|
|
||||||
|
impl Borrow<String> for MinecraftPlayer {
|
||||||
|
fn borrow(&self) -> &String {
|
||||||
|
&self.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ pub async fn track_server_status(ctx: Context) {
|
||||||
loop {
|
loop {
|
||||||
tracing::info!("Collecting status information");
|
tracing::info!("Collecting status information");
|
||||||
// Fetch status via rcon and if successful, push it to the influx db
|
// Fetch status via rcon and if successful, push it to the influx db
|
||||||
if let Some(status) = Status::get_from_rcon(&ctx)
|
if let Some(status) = Status::assemble(&ctx)
|
||||||
.await
|
.await
|
||||||
.log_warn("fetching status via rcon")
|
.log_warn("fetching status via rcon")
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
use influx_db_client::Point;
|
use influx_db_client::Point;
|
||||||
use serenity::client::Context;
|
use serenity::client::Context;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
mod player_info;
|
mod player_info;
|
||||||
mod tps;
|
mod tps;
|
||||||
|
|
||||||
use crate::{error::Result, influx::InfluxDb, rcon::RconHandle};
|
use crate::{
|
||||||
|
error::Result,
|
||||||
|
influx::InfluxDb,
|
||||||
|
rcon::RconHandle,
|
||||||
|
state::{MinecraftPlayer, State},
|
||||||
|
};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
player_info::{ListCmd, PlayerInfo},
|
player_info::{ListCmd, PlayerInfo},
|
||||||
|
@ -13,14 +20,37 @@ use self::{
|
||||||
|
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub tps: Tps,
|
pub tps: Tps,
|
||||||
pub player_info: PlayerInfo,
|
pub amount_online: u32,
|
||||||
|
/// List of players associated with their online status.
|
||||||
|
pub players: HashMap<MinecraftPlayer, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
pub async fn get_from_rcon(ctx: &Context) -> Result<Self> {
|
pub async fn assemble(ctx: &Context) -> Result<Self> {
|
||||||
let tps = RconHandle::cmd(&TpsCmd, ctx).await?;
|
let tps = RconHandle::cmd(&TpsCmd, ctx).await?;
|
||||||
let player_info = RconHandle::cmd(&ListCmd, ctx).await?;
|
let PlayerInfo {
|
||||||
Ok(Status { tps, player_info })
|
amount_online,
|
||||||
|
players: online_players,
|
||||||
|
} = RconHandle::cmd(&ListCmd, ctx).await?;
|
||||||
|
// Fetch all known offline players aswell
|
||||||
|
let mut players: HashMap<_, _> = State::read(ctx, |state| {
|
||||||
|
state
|
||||||
|
.known_players
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|player| (player, false))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
for (_ign, uuid) in online_players {
|
||||||
|
players.insert(MinecraftPlayer::from_uuid(ctx, uuid).await?, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Status {
|
||||||
|
tps,
|
||||||
|
amount_online,
|
||||||
|
players,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_to_influx_db(&self, ctx: &Context) -> Result {
|
pub async fn send_to_influx_db(&self, ctx: &Context) -> Result {
|
||||||
|
@ -28,12 +58,16 @@ impl Status {
|
||||||
let status = Point::new("status")
|
let status = Point::new("status")
|
||||||
.add_field("tps", self.tps.min1)
|
.add_field("tps", self.tps.min1)
|
||||||
// Safe, since we started with u32
|
// Safe, since we started with u32
|
||||||
.add_field("player_count", self.player_info.amount_online as i64);
|
.add_field("player_count", self.amount_online as i64);
|
||||||
points.push(status);
|
points.push(status);
|
||||||
for player in &self.player_info.players {
|
let mut activity = Point::new("activity");
|
||||||
let point = Point::new("activity").add_tag("user_id", player.uuid.clone());
|
for (player, is_online) in &self.players {
|
||||||
points.push(point);
|
// Map is_online to {0, 1} as usual, to make grafana more happy
|
||||||
|
let is_online = *is_online as i64;
|
||||||
|
let name = format!("{} ({})", player.ign.clone(), player.uuid.clone());
|
||||||
|
activity = activity.add_field(name, is_online);
|
||||||
}
|
}
|
||||||
|
points.push(activity);
|
||||||
|
|
||||||
InfluxDb::write_points(points.into_iter(), ctx).await
|
InfluxDb::write_points(points.into_iter(), ctx).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,11 @@ use crate::{
|
||||||
|
|
||||||
pub struct ListCmd;
|
pub struct ListCmd;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct PlayerInfo {
|
pub struct PlayerInfo {
|
||||||
pub amount_online: u32,
|
pub amount_online: u32,
|
||||||
pub players: Vec<Player>,
|
/// (IGN, UUID)
|
||||||
}
|
pub players: Vec<(String, String)>,
|
||||||
|
|
||||||
pub struct Player {
|
|
||||||
pub uuid: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RconCommand for ListCmd {
|
impl RconCommand for ListCmd {
|
||||||
|
@ -49,7 +47,7 @@ fn parse_list_result(inp: &str) -> IResult<&str, PlayerInfo> {
|
||||||
tag(" of a max of "),
|
tag(" of a max of "),
|
||||||
complete::u32,
|
complete::u32,
|
||||||
tag(" players online: "),
|
tag(" players online: "),
|
||||||
separated_list0(tag(", "), parse_player),
|
separated_list0(tag(", "), parse_players),
|
||||||
)),
|
)),
|
||||||
|(_, num, _, _max, _, players)| PlayerInfo {
|
|(_, num, _, _max, _, players)| PlayerInfo {
|
||||||
amount_online: num,
|
amount_online: num,
|
||||||
|
@ -58,11 +56,14 @@ fn parse_list_result(inp: &str) -> IResult<&str, PlayerInfo> {
|
||||||
))(inp)
|
))(inp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_player(inp: &str) -> IResult<&str, Player> {
|
/// Parse `"IGN (0000-...-0000)"` into `("IGN", "0000-...-0000")`.
|
||||||
|
fn parse_players(inp: &str) -> IResult<&str, (String, String)> {
|
||||||
map(
|
map(
|
||||||
tuple((is_not(" "), delimited(char('('), is_not(")"), char(')')))),
|
tuple((
|
||||||
|(_name, uuid): (&str, &str)| Player {
|
is_not(" "),
|
||||||
uuid: uuid.to_owned(),
|
char(' '),
|
||||||
},
|
delimited(char('('), is_not(")"), char(')')),
|
||||||
|
)),
|
||||||
|
|(name, _, uuid): (&str, _, &str)| (name.to_owned(), uuid.to_owned()),
|
||||||
)(inp)
|
)(inp)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue