8 Commits

Author SHA1 Message Date
79fb39b154 Merge branch 'feature/track-sessions' into main 2023-06-28 21:55:38 -07:00
fc53ecb770 bug fixes
- fixes world/mission log race condition w/ new foreign key constraint in DB
- fixes missing world_id sent with mission rows
- adds worldName to missionContext hash in sqf
2023-06-28 21:54:57 -07:00
dbd3d68537 WORKING: final bugfixes & add stored procedure for prev mission cleanup 2023-06-22 10:27:09 -07:00
3bb8c358fa bug fixing 2023-06-20 23:10:58 -07:00
8f971d9887 bug fixes/debugging 2023-06-17 22:34:52 -07:00
f6ff42e467 bug fixes, 3 table split, update readme 2023-06-14 09:59:45 -07:00
7608df9e53 change to 1 row per session (networkId) 2023-06-13 17:59:04 -07:00
2cbfdcd512 work out world & mission tables in db & gather/write
needs to be tested
2023-05-17 19:26:53 -07:00
23 changed files with 574 additions and 192 deletions

View File

@@ -1,91 +0,0 @@
[
["OnUserConnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (getUserInfo _networkId);
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {};
[
"ConnectedServer",
_playerID,
_playerUID,
_profileName,
_steamName
] call attendanceTracker_fnc_logServerEvent;
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_networkId, _userInfo];
}],
["OnUserDisconnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserDisconnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _networkId;
if (isNil "_userInfo") exitWith {};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {};
[
"DisconnectedServer",
_playerID,
_playerUID,
_profileName,
_steamName
] call attendanceTracker_fnc_logServerEvent;
}],
["PlayerConnected", {
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) PlayerConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (getUserInfo _idstr);
if (isNil "_userInfo") exitWith {};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_playerID, _userInfo];
if (_isHC) exitWith {};
[
"ConnectedMission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
roleDescription _unit
] call attendanceTracker_fnc_logMissionEvent;
}],
["PlayerDisconnected", {
// NOTE: HandleDisconnect returns a DIFFERENT _id than PlayerDisconnected and above handlers, so we can't use it here
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) HandleDisconnect fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _idstr;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) HandleDisconnect: No user info found for %1", _idstr], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {};
[
"DisconnectedMission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip
] call attendanceTracker_fnc_logMissionEvent;
false;
}]
];

View File

@@ -1 +0,0 @@
(parseSimpleArray ("AttendanceTracker" callExtension "getTimestamp")) select 0;

Binary file not shown.

View File

@@ -18,9 +18,12 @@ class CfgFunctions {
class eventHandlers {};
class callbackHandler {postInit = 1;};
class log {};
class logMissionEvent {};
class logServerEvent {};
class writeConnect {};
class writeDisconnect {};
class timestamp {};
class getMissionHash {};
class getWorldInfo {};
class missionLoaded {};
};
};
};

View File

@@ -21,11 +21,16 @@ addMissionEventHandler ["ExtensionCallback", {
false;
};
diag_log format ["Raw callback: %1: %2", _function, _data];
// Parse response from string array
private "_response";
try {
// diag_log format ["Raw callback: %1: %2", _function, _data];
_response = parseSimpleArray _data;
if (_response isEqualTo []) then {
throw "Failed to parse response as array";
};
} catch {
[
format ["Callback invalid data: %1: %2: %3", _function, _data, _exception],
@@ -42,17 +47,39 @@ addMissionEventHandler ["ExtensionCallback", {
if (_response#0 == "SUCCESS") then {
missionNamespace setVariable ["AttendanceTracker_DBConnected", true];
// log mission info and get back the row Id to send with future messages
private _response = "AttendanceTracker" callExtension ["logMission", [
[AttendanceTracker getVariable ["missionContext", createHashMap]] call CBA_fnc_encodeJSON
]];
AttendanceTracker_missionId = parseNumber _response;
// close any null disconnect values from previous mission
"AttendanceTracker" callExtension ["fillLastMissionNull", []];
// log world info
private _response = "AttendanceTracker" callExtension ["logWorld", [
[call attendanceTracker_fnc_getWorldInfo] call CBA_fnc_encodeJSON
]];
private _response = "AttendanceTracker" callExtension [
"logWorld",
[
[(call attendanceTracker_fnc_getWorldInfo)] call CBA_fnc_encodeJSON
]
];
// log mission info and get back the row Id to send with future messages
private _response = "AttendanceTracker" callExtension [
"logMission",
[
[AttendanceTracker getVariable ["missionContext", createHashMap]] call CBA_fnc_encodeJSON
]
];
};
};
case "writeMissionInfo": {
if (_response#0 == "MISSION_ID") then {
AttendanceTracker_missionId = parseNumber (_response#1);
};
};
case "writeAttendance": {
if (_response#0 == "ATT_LOG") then {
(_response#1) params ["_eventType", "_netId", "_rowId"];
private _storeIndex = ["SERVER", "MISSION"] find _eventType;
((AttendanceTracker getVariable ["rowIds", createHashMap]) getOrDefault [
_netId,
[nil, nil]
]) set [_storeIndex, _rowId];
};
};
default {

View File

@@ -0,0 +1,160 @@
[
["OnUserConnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (getUserInfo _networkId);
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) OnUserConnected: No user info found for %1", _networkId], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) OnUserConnected: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_networkId, _userInfo];
[
"Server",
_playerID,
_playerUID,
_profileName,
_steamName,
nil,
nil
] call attendanceTracker_fnc_writeConnect;
}],
["OnUserDisconnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserDisconnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) OnUserDisconnected: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _networkId;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) OnUserDisconnected: No user info found for %1", _networkId], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) OnUserDisconnected: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
[
"Server",
_playerID,
_playerUID,
_profileName,
_steamName
] call attendanceTracker_fnc_writeDisconnect;
}],
["PlayerConnected", {
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) PlayerConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) PlayerConnected: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (getUserInfo _idstr);
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) PlayerConnected: No user info found for %1", _idstr], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) PlayerConnected: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_playerID, _userInfo];
[
"Mission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
roleDescription _unit
] call attendanceTracker_fnc_writeConnect;
}],
["PlayerDisconnected", {
// NOTE: HandleDisconnect returns a DIFFERENT _id than PlayerDisconnected and above handlers, so we can't use it here
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) HandleDisconnect fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) HandleDisconnect: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _idstr;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) HandleDisconnect: No user info found for %1", _idstr], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit", "_rowId"];
if (_isHC) exitWith {
[format ["(EventHandler) HandleDisconnect: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
[
"Mission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
nil
] call attendanceTracker_fnc_writeDisconnect;
false;
}],
["OnUserKicked", {
params ["_networkId", "_kickTypeNumber", "_kickType", "_kickReason", "_kickMessageIncReason"];
[format ["(EventHandler) OnUserKicked fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) OnUserKicked: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _networkId;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) OnUserKicked: No user info found for %1", _networkId], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) OnUserKicked: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
[
"Server",
_playerID,
_playerUID,
_profileName,
_steamName,
nil,
nil
] call attendanceTracker_fnc_writeDisconnect;
[
"Mission",
_playerID,
_playerUID,
_profileName,
_steamName,
nil,
nil
] call attendanceTracker_fnc_writeDisconnect;
}]
];

View File

@@ -0,0 +1 @@
(parseSimpleArray ("AttendanceTracker" callExtension "getMissionHash")) select 0;

View File

@@ -14,10 +14,15 @@ _workshopID = '';
} foreach getLoadedModsInfo;
// [_name, _author, _workshopID];
[
["worldName", _name],
_return = createHashMapFromArray [
["author", _author],
["workshopID", _workshopID],
["displayName", _name],
["worldName", toLower worldName],
["worldNameOriginal", _name],
["worldSize", worldSize],
["workshopID", _workshopID]
["latitude", getNumber( _world >> "latitude" )],
["longitude", getNumber( _world >> "longitude" )]
];
diag_log format ["Attendance Tracker: WorldInfo is: %1", _return];
_return

View File

@@ -0,0 +1 @@
!(getClientStateNumber <= 5 || getClientStateNumber isEqualTo 11);

View File

@@ -2,7 +2,9 @@
AttendanceTracker = false call CBA_fnc_createNamespace;
AttendanceTracker_missionStartTimestamp = call attendanceTracker_fnc_timestamp;
AttendanceTracker_missionHash = "AttendanceTracker" callExtension ["getMissionHash", AttendanceTracker_missionStartTimestamp];
diag_log format ["AttendanceTracker: Mission started at %1", AttendanceTracker_missionStartTimestamp];
AttendanceTracker_missionHash = call attendanceTracker_fnc_getMissionHash;
diag_log format ["AttendanceTracker: Mission hash is %1", AttendanceTracker_missionHash];
AttendanceTracker setVariable ["missionContext", createHashMapFromArray [
["missionName", missionName],
@@ -13,13 +15,15 @@ AttendanceTracker setVariable ["missionContext", createHashMapFromArray [
["serverName", serverName],
["serverProfile", profileName],
["missionStart", AttendanceTracker_missionStartTimestamp],
["missionHash", AttendanceTracker_missionHash]
["missionHash", AttendanceTracker_missionHash],
["worldName", toLower worldName]
]];
// store all user details in a hash when they connect so we can reference it in disconnect events
AttendanceTracker setVariable ["allUsers", createHashMap];
AttendanceTracker setVariable ["rowIds", createHashMap];
missionNamespace setVariable ["AttendanceTracker_debug", false];
call attendanceTracker_fnc_connectDB;

View File

@@ -0,0 +1,24 @@
// (parseSimpleArray ("AttendanceTracker" callExtension "getTimestamp")) select 0;
// need date for MySQL in format 2006-01-02 15:04:05
systemTimeUTC params [
"_year",
"_month",
"_day",
"_hour",
"_minute",
"_second",
"_millisecond"
];
format[
"%1-%2-%3 %4:%5:%6",
_year,
_month,
_day,
_hour,
_minute,
_second
];

View File

@@ -9,6 +9,7 @@ params [
];
private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]);
_hash set ["eventType", _eventType];
_hash set ["playerId", _playerId];
_hash set ["playerUID", _playerUID];
@@ -18,6 +19,6 @@ _hash set ["isJIP", _isJIP];
_hash set ["roleDescription", _roleDescription];
_hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]];
"AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]];
"AttendanceTracker" callExtension ["writeAttendance", [[_hash] call CBA_fnc_encodeJSON]];
true;

View File

@@ -3,20 +3,22 @@ params [
["_playerId", ""],
["_playerUID", ""],
["_profileName", ""],
["_steamName", ""]
["_steamName", ""],
["_isJIP", false, [true, false]],
["_roleDescription", ""]
];
private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]);
_hash set ["eventType", _eventType];
_hash set ["playerId", _playerId];
_hash set ["playerUID", _playerUID];
_hash set ["profileName", _profileName];
_hash set ["steamName", _steamName];
_hash set ["isJIP", false];
_hash set ["roleDescription", ""];
_hash set ["isJIP", _isJIP];
_hash set ["roleDescription", _roleDescription];
_hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]];
"AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]];
"AttendanceTracker" callExtension ["writeDisconnectEvent", [[_hash] call CBA_fnc_encodeJSON]];
true;

View File

@@ -0,0 +1,7 @@
{
"mysqlHost": "127.0.0.1",
"mysqlPort": 12730,
"mysqlUser": "root",
"mysqlPassword": "i&Lz8A3RuPcY5b326ALXgjl",
"mysqlDatabase": "testdb"
}

View File

@@ -7,31 +7,68 @@
Create a database with a name of your choosing. Then, run the following SQL command against it to create a table.
```sql
CREATE TABLE `attendancelog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`timestamp` DATETIME NOT NULL,
`event_hash` VARCHAR(100) NOT NULL DEFAULT md5(concat(`server_name`,`mission_name`,`author`,`mission_start`)) COLLATE 'utf8mb3_general_ci',
`event_type` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`player_id` VARCHAR(30) NOT NULL COLLATE 'utf8mb3_general_ci',
`player_uid` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`profile_name` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`steam_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`is_jip` TINYINT(4) NULL DEFAULT NULL,
`role_description` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`mission_start` DATETIME NOT NULL,
`mission_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`briefing_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`mission_name_source` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`on_load_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`author` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`server_name` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`server_profile` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
PRIMARY KEY (`id`) USING BTREE
-- a3server.attendancelog definition
CREATE TABLE `attendance` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`join_time` DATETIME NULL DEFAULT NULL,
`disconnect_time` DATETIME NULL DEFAULT NULL,
`mission_hash` VARCHAR(100) NULL DEFAULT '' COLLATE 'utf8mb3_general_ci',
`event_type` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`player_id` VARCHAR(30) NOT NULL COLLATE 'utf8mb3_general_ci',
`player_uid` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`profile_name` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`steam_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`is_jip` TINYINT(4) NULL DEFAULT NULL,
`role_description` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8mb3_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=383
AUTO_INCREMENT=5868
;
-- a3server.`missions` definition
CREATE TABLE `missions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`mission_name` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`mission_name_source` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`briefing_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`on_load_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`author` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`server_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`server_profile` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`mission_start` DATETIME NULL DEFAULT NULL,
`mission_hash` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8mb3_general_ci'
ENGINE=InnoDB
;
-- a3server.`worlds` definition
CREATE TABLE `worlds` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`author` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`display_name` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`world_name` VARCHAR(100) NOT NULL COLLATE 'utf8mb3_general_ci',
`world_name_original` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
`world_size` INT(11) NULL DEFAULT NULL,
`latitude` FLOAT NULL DEFAULT NULL,
`longitude` FLOAT NULL DEFAULT NULL,
`workshop_id` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8mb3_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=2
;
```
Finally, copy `config.example.json` to `config.json` and update it with your database credentials.

Binary file not shown.

View File

@@ -16,7 +16,8 @@ import (
"fmt"
"log"
"os"
"strconv"
"path"
"runtime"
"strings"
"time"
"unsafe"
@@ -32,6 +33,12 @@ var ADDON_FOLDER string = getDir() + "\\@AttendanceTracker"
var LOG_FILE string = ADDON_FOLDER + "\\attendanceTracker.log"
var CONFIG_FILE string = ADDON_FOLDER + "\\config.json"
var ATTENDANCE_TABLE string = "attendance"
var MISSIONS_TABLE string = "missions"
var WORLDS_TABLE string = "worlds"
// ! TODO make a hash to save key:netId from A3 value:rowId from join event
var ATConfig AttendanceTrackerConfig
type AttendanceTrackerConfig struct {
@@ -104,14 +111,14 @@ func loadConfig() {
writeLog(functionName, `["Config loaded", "INFO"]`)
}
func getMissionHash(time string) string {
func getMissionHash() string {
functionName := "getMissionHash"
// get md5 hash of string
// https://stackoverflow.com/questions/2377881/how-to-get-a-md5-hash-from-a-string-in-golang
hash := md5.Sum([]byte(time))
hash := md5.Sum([]byte(time.Now().UTC().Format("2006-01-02 15:04:05")))
// convert to string
hashString := fmt.Sprintf("%x", hash)
hashString := fmt.Sprintf(`%x`, hash)
writeLog(functionName, fmt.Sprintf(`["Mission hash: %s", "INFO"]`, hashString))
return hashString
}
@@ -147,7 +154,7 @@ func connectDB() string {
return "ERROR"
}
// Connect and check the server version
// Check the server version
var version string
err = db.QueryRow("SELECT VERSION()").Scan(&version)
if err != nil {
@@ -160,38 +167,47 @@ func connectDB() string {
}
type WorldInfo struct {
WorldName string `json:"worldName"`
Author string `json:"author"`
WorldSize int `json:"worldSize"`
WorkshopID string `json:"workshopID"`
Author string `json:"author"`
WorkshopID string `json:"workshopID"`
DisplayName string `json:"displayName"`
WorldName string `json:"worldName"`
WorldNameOriginal string `json:"worldNameOriginal"`
WorldSize int `json:"worldSize"`
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
}
func writeWorldInfo(worldInfo string) {
functionName := "writeWorldInfo"
// writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, worldInfo))
// worldInfo is json, parse it
var wi WorldInfo
err := json.Unmarshal([]byte(worldInfo), &wi)
fixedString := fixEscapeQuotes(trimQuotes(worldInfo))
err := json.Unmarshal([]byte(fixedString), &wi)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// write to log
writeLog(functionName, fmt.Sprintf(`["WorldName:%s Author:%s WorldSize:%d WorkshopID:%s", "INFO"]`, wi.WorldName, wi.Author, wi.WorldSize, wi.WorkshopID))
// write to log as json
// writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, json.Marshal(wi)))
// write to database
// check if world exists
var worldID int
err = db.QueryRow("SELECT id FROM worlds WHERE workshop_id = ?", wi.WorkshopID).Scan(&worldID)
err = db.QueryRow("SELECT id FROM worlds WHERE world_name = ?", wi.WorldName).Scan(&worldID)
if err != nil {
if err == sql.ErrNoRows {
// world does not exist, insert it
stmt, err := db.Prepare("INSERT INTO worlds (world_name, author, world_size, workshop_id) VALUES (?, ?, ?, ?)")
stmt, err := db.Prepare(fmt.Sprintf(
"INSERT INTO %s (author, workshop_id, display_name, world_name, world_name_original, world_size, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
WORLDS_TABLE,
))
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err := stmt.Exec(wi.WorldName, wi.Author, wi.WorldSize, wi.WorkshopID)
res, err := stmt.Exec(wi.Author, wi.WorkshopID, wi.DisplayName, wi.WorldName, wi.WorldNameOriginal, wi.WorldSize, wi.Latitude, wi.Longitude)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
@@ -208,13 +224,16 @@ func writeWorldInfo(worldInfo string) {
}
} else {
// world exists, update it
stmt, err := db.Prepare("UPDATE worlds SET world_name = ?, author = ?, world_size = ? WHERE id = ?")
stmt, err := db.Prepare(fmt.Sprintf(
"UPDATE %s SET author = ?, workshop_id = ?, display_name = ?, world_name = ?, world_name_original = ?, world_size = ?, latitude = ?, longitude = ? WHERE id = ?",
WORLDS_TABLE,
))
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err := stmt.Exec(wi.WorldName, wi.Author, wi.WorldSize, worldID)
res, err := stmt.Exec(wi.Author, wi.WorkshopID, wi.DisplayName, wi.WorldName, wi.WorldNameOriginal, wi.WorldSize, wi.Latitude, wi.Longitude, worldID)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
@@ -238,48 +257,74 @@ type MissionInfo struct {
ServerProfile string `json:"serverProfile"`
MissionStart string `json:"missionStart"`
MissionHash string `json:"missionHash"`
WorldName string `json:"worldName"`
}
func writeMissionInfo(missionInfo string) {
functionName := "writeMissionInfo"
var err error
// writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, missionInfo))
// missionInfo is json, parse it
var mi MissionInfo
err := json.Unmarshal([]byte(missionInfo), &mi)
fixedString := fixEscapeQuotes(trimQuotes(missionInfo))
err = json.Unmarshal([]byte(fixedString), &mi)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// get MySQL friendly datetime
// first, convert string to int
missionStartTime, err := strconv.ParseInt(mi.MissionStart, 10, 64)
// check if mission exists based on hash
var worldID int
err = db.QueryRow("SELECT id FROM worlds WHERE world_name = ?", mi.WorldName).Scan(&worldID)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
t := time.Unix(0, missionStartTime).Format("2006-01-02 15:04:05")
// write to log
writeLog(functionName, fmt.Sprintf(`["MissionName:%s BriefingName:%s MissionNameSource:%s OnLoadName:%s Author:%s ServerName:%s ServerProfile:%s MissionStart:%s MissionHash:%s", "INFO"]`, mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, t, mi.MissionHash))
// write to database
// every mission is unique, so insert it
stmt, err := db.Prepare("INSERT INTO missions (mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err := stmt.Exec(mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, t, mi.MissionHash)
var stmt *sql.Stmt
var res sql.Result
if worldID != 0 {
sqlWorld := fmt.Sprintf(
"INSERT INTO %s (mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, mission_hash, world_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
MISSIONS_TABLE,
)
stmt, err = db.Prepare(sqlWorld)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err = stmt.Exec(mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, mi.MissionStart, mi.MissionHash, worldID)
} else {
// if no world was found, write without it
sqlNoWorld := fmt.Sprintf(
"INSERT INTO %s (mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
MISSIONS_TABLE,
)
stmt, err = db.Prepare(sqlNoWorld)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err = stmt.Exec(mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, mi.MissionStart, mi.MissionHash)
}
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
lastID, err := res.LastInsertId()
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
writeLog(functionName, fmt.Sprintf(`["Mission inserted with ID %d", "INFO"]`, lastID))
writeLog(functionName, fmt.Sprintf(`["MISSION_ID", "%d"]`, lastID))
}
type AttendanceLogItem struct {
@@ -295,17 +340,18 @@ type AttendanceLogItem struct {
func writeAttendance(data string) {
functionName := "writeAttendance"
var err error
// data is json, parse it
stringjson := fixEscapeQuotes(trimQuotes(data))
var event AttendanceLogItem
err := json.Unmarshal([]byte(stringjson), &event)
err = json.Unmarshal([]byte(stringjson), &event)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// get MySQL friendly NOW
now := time.Now().Format("2006-01-02 15:04:05")
now := time.Now().UTC().Format("2006-01-02 15:04:05")
// prevent crash
if db == nil {
@@ -314,18 +360,44 @@ func writeAttendance(data string) {
}
// send to DB
var result sql.Result
result, err := db.ExecContext(context.Background(), `INSERT INTO AttendanceLog (timestamp, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
now,
event.EventType,
event.PlayerId,
event.PlayerUID,
event.ProfileName,
event.SteamName,
event.IsJIP,
event.RoleDescription,
event.MissionHash,
)
if event.EventType == "Server" {
sql := fmt.Sprintf(
`INSERT INTO %s (join_time, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
ATTENDANCE_TABLE,
)
result, err = db.ExecContext(
context.Background(),
sql,
now,
event.EventType,
event.PlayerId,
event.PlayerUID,
event.ProfileName,
event.SteamName,
event.IsJIP,
event.RoleDescription,
)
} else if event.EventType == "Mission" {
sql := fmt.Sprintf(
`INSERT INTO %s (join_time, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
ATTENDANCE_TABLE,
)
result, err = db.ExecContext(
context.Background(),
sql,
now,
event.EventType,
event.PlayerId,
event.PlayerUID,
event.ProfileName,
event.SteamName,
event.IsJIP,
event.RoleDescription,
event.MissionHash,
)
}
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
@@ -339,9 +411,124 @@ func writeAttendance(data string) {
}
writeLog(functionName, fmt.Sprintf(`["Saved attendance for %s to row id %d", "INFO"]`, event.ProfileName, id))
if event.EventType == "Server" {
writeLog(functionName, fmt.Sprintf(`["ATT_LOG", ["SERVER", "%s", "%d"]]`, event.PlayerId, id))
} else if event.EventType == "Mission" {
writeLog(functionName, fmt.Sprintf(`["ATT_LOG", ["MISSION", "%s", "%d"]]`, event.PlayerId, id))
}
}
type DisconnectItem struct {
EventType string `json:"eventType"`
PlayerId string `json:"playerId"`
MissionHash string `json:"missionHash"`
}
func writeDisconnectEvent(data string) {
functionName := "writeDisconnectEvent"
// data is json, parse it
stringjson := fixEscapeQuotes(trimQuotes(data))
var event DisconnectItem
err := json.Unmarshal([]byte(stringjson), &event)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// get MySQL friendly NOW
now := time.Now().UTC().Format("2006-01-02 15:04:05")
// prevent crash
if db == nil {
writeLog(functionName, `["db is nil", "ERROR"]`)
return
}
// first, check if a row exists for this player
var sql string
if event.EventType == "Mission" {
sql = fmt.Sprintf(
`
SELECT id FROM attendance
WHERE player_id = '%s' and event_type = '%s' and mission_hash = '%s' and disconnect_time IS NULL and join_time >= (NOW() - INTERVAL 24 hour)
ORDER BY join_time DESC
`,
event.PlayerId,
event.EventType,
event.MissionHash,
)
} else if event.EventType == "Server" {
sql = fmt.Sprintf(
`
SELECT id FROM attendance
WHERE player_id = '%s' and event_type = '%s' and disconnect_time IS NULL and join_time >= (NOW() - INTERVAL 24 hour)
ORDER BY join_time DESC
`,
event.PlayerId,
event.EventType,
)
} else {
writeLog(functionName, fmt.Sprintf(`["Unknown event type %s", "ERROR"]`, event.EventType))
return
}
rows, err := db.QueryContext(context.Background(), sql)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer rows.Close()
// if there is a row, update it
if rows.Next() {
// create interface to hold values
var rowId int64
err = rows.Scan(&rowId)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// update the row
sql = fmt.Sprintf(
`UPDATE attendance SET disconnect_time = '%s' WHERE id = %d`,
now,
rowId,
)
_, err := db.ExecContext(context.Background(), sql)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
writeLog(functionName, fmt.Sprintf(`["Saved disconnect event for %s to row id %d", "INFO"]`, event.PlayerId, rowId))
} else {
// otherwise, log an error
writeLog(functionName, fmt.Sprintf(`["No row found for %s, %s", "ERROR"]`, event.PlayerId, event.EventType))
}
}
func fillLastMissionNull() {
functionName := "fillLastMissionNull"
// prevent crash
if db == nil {
writeLog(functionName, `["db is nil", "ERROR"]`)
return
}
sql := `call proc_filllastmissionnull`
_, err := db.ExecContext(context.Background(), sql)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
writeLog(functionName, `["Filled mission event NULLs", "INFO"]`)
}
func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int {
return C.runExtensionCallback(extensionCallbackFnc, name, function, data)
}
@@ -370,12 +557,23 @@ func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv
temp := fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc)
switch C.GoString(input) {
case "logAttendance":
{ // callExtension ["serverEvent", [_hash] call CBA_fnc_encodeJSON];
case "fillLastMissionNull":
{
go fillLastMissionNull()
}
case "writeAttendance":
{ // callExtension ["logAttendance", [_hash] call CBA_fnc_encodeJSON]];
if argc == 1 {
go writeAttendance(out[0])
}
}
case "writeDisconnectEvent":
{ // callExtension ["writeDisconnectEvent", [[_hash] call CBA_fnc_encodeJSON]];
if argc == 1 {
go writeDisconnectEvent(out[0])
}
}
case "logMission":
if argc == 1 {
go writeMissionInfo(out[0])
@@ -411,9 +609,10 @@ func callBackExample() {
}
}
func getTimestamp() int64 {
func getTimestamp() string {
// get the current unix timestamp in nanoseconds
return time.Now().UnixNano()
// return time.Now().Local().Unix()
return time.Now().UTC().Format("2006-01-02 15:04:05")
}
func trimQuotes(s string) string {
@@ -435,6 +634,9 @@ func writeLog(functionName string, data string) {
defer C.free(unsafe.Pointer(statusParam))
runExtensionCallback(statusName, statusFunction, statusParam)
// get calling function & line
_, file, line, _ := runtime.Caller(1)
log.Printf(`%s:%d: %s`, path.Base(file), line, data)
log.Printf(`%s: %s`, functionName, data)
}
@@ -451,12 +653,12 @@ func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) {
case "getDir":
temp = getDir()
case "getTimestamp":
time := getTimestamp()
temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10))
temp = fmt.Sprintf(`["%s"]`, getTimestamp())
case "connectDB":
go connectDB()
temp = fmt.Sprintf(`["%s"]`, "Connecting to DB")
case "getMissionHash":
temp = fmt.Sprintf(`["%s"]`, getMissionHash())
default:
temp = fmt.Sprintf(`["%s"]`, "Unknown Function")
}