7 Commits

Author SHA1 Message Date
6cf76d1019 implement CBA macros, fix for prod
- using a3go 0.3.2, no longer relies on ext callback for anything except RPT logging and waiting DB connect at postinit
- tested and functional
2023-10-12 15:42:54 -07:00
62fbe8b24c minor adjustments 2023-10-12 00:17:16 -07:00
71ec70ef6a fix pboprefix and update build commands 2023-10-10 20:37:36 -07:00
7a8356c92c bump a3go, move files, Readme updates 2023-10-10 18:34:35 -07:00
36820c57f3 Rename README to README.md 2023-10-10 21:12:57 -04:00
1892805fe9 1.1.0 - bump a3interface & refactor to new api 2023-10-08 14:40:21 -07:00
5dc3b5cb03 use player_id for session instead of UID. add build dockerfile 2023-10-07 22:56:32 -07:00
35 changed files with 646 additions and 547 deletions

4
.gitignore vendored
View File

@@ -9,3 +9,7 @@ hemtt
hemtt.exe hemtt.exe
*.biprivatekey *.biprivatekey
*.bk *.bk
releases/
mariadb/db/
AttendanceTracker.config.json

View File

@@ -1,15 +1,16 @@
name = "IFX Attendance Tracker" name ="IFX Attendance Tracker"
author = "IndigoFox" author ="IndigoFox"
prefix = "attendancetracker" prefix ="attendancetracker"
mainprefix = "x" mainprefix="x"
[version] [version]
path = "addons/main/script_version.hpp" # Default path ="addons/main/script_version.hpp" # Default
git_hash = 6 # Default: 8 git_hash=6 # Default: 8
[files] [files]
include = [ include=[
"AttendanceTracker.config.json", "AttendanceTracker.config.example.json",
# "AttendanceTracker.config.json", # used for copying active config during debugging
"LICENSE", "LICENSE",
"README", "README",
"mod.cpp", "mod.cpp",
@@ -19,20 +20,20 @@ include = [
# Launched with `hemtt launch` # Launched with `hemtt launch`
[hemtt.launch.default] [hemtt.launch.default]
workshop = [ workshop=[
"450814997", # CBA_A3's Workshop ID "450814997", # CBA_A3's Workshop ID
] ]
dlc = [] dlc=[]
optionals = [] optionals=[]
parameters = [ parameters=[
"-skipIntro", # These parameters are passed to the Arma 3 executable "-skipIntro", # These parameters are passed to the Arma 3 executable
"-noSplash", # They do not need to be added to your list "-noSplash", # They do not need to be added to your list
"-showScriptErrors", # You can add additional parameters here "-showScriptErrors", # You can add additional parameters here
"-debug", "-debug",
"-filePatching", "-filePatching",
] ]
executable = "arma3_x64" # Default: "arma3_x64" executable="arma3_x64" # Default: "arma3_x64"
[hemtt.release] [hemtt.release]
sign = false # Default: true sign =false # Default: true
archive = true # Default: true archive=true # Default: true

View File

@@ -3,12 +3,12 @@
"mysqlHost": "localhost", "mysqlHost": "localhost",
"mysqlPort": 3306, "mysqlPort": 3306,
"mysqlUser": "root", "mysqlUser": "root",
"mysqlPassword": "password", "mysqlPassword": "example",
"mysqlDatabase": "a3attendance" "mysqlDatabase": "a3attendance"
}, },
"armaConfig": { "armaConfig": {
"dbUpdateInterval": "90s", "dbUpdateInterval": "90s",
"debug": false, "debug": false,
"traceLogToFile": false "trace": false
} }
} }

View File

@@ -1,7 +1,5 @@
# Arma 3 Attendance Tracker # Arma 3 Attendance Tracker
---
## Setup ## Setup
### Set Up a Database Engine ### Set Up a Database Engine
@@ -37,7 +35,7 @@ CREATE DATABASE `arma3_attendance`;
1. Download the latest release from the [releases page](https://github.com/indig0fox/Arma3-AttendanceTracker/releases). 1. Download the latest release from the [releases page](https://github.com/indig0fox/Arma3-AttendanceTracker/releases).
1. Extract the .zip and move `@AttendanceTracker` to your Arma 3 server's root directory. 1. Extract the .zip and move `@AttendanceTracker` to your Arma 3 server's root directory.
1. Inside of `@AttendanceTracker` you will find a `config.json` file. Open this file and configure it to your circumstances. See the [Configuration](#configuration) section for more information. 1. Inside of `@AttendanceTracker` you will find an `AttendanceTracker.config.example.json` file. Copy this as `AttendanceTRacker.config.json`. Open this new file and configure it to your circumstances. See the [Configuration](#configuration) section for more information.
1. Add the mod to your server's startup parameters. For example: `-serverMod="@AttendanceTracker;"` 1. Add the mod to your server's startup parameters. For example: `-serverMod="@AttendanceTracker;"`
At next run, the Arma 3 server will launch with the mod running. At next run, the Arma 3 server will launch with the mod running.
@@ -55,10 +53,9 @@ The following table describes the configuration options.
| sqlConfig.mySqlUser | string | The username to use when connecting to your MySQL instance. | root | | sqlConfig.mySqlUser | string | The username to use when connecting to your MySQL instance. | root |
| sqlConfig.mySqlPassword | string | The password to use when connecting to your MySQL instance. | root | | sqlConfig.mySqlPassword | string | The password to use when connecting to your MySQL instance. | root |
| sqlConfig.mySqlDatabase | string | The name of the database to use. | arma3_attendance | | sqlConfig.mySqlDatabase | string | The name of the database to use. | arma3_attendance |
| armaConfig.dbUpdateIntervalSeconds | integer | The number of seconds between disconnect_time updates per user. | 90 | | armaConfig.dbUpdateInterval | string, [`time.Duration` Go type](https://pkg.go.dev/time#ParseDuration) | The number of seconds between disconnect_time updates per user. | "90s" |
| armaConfig.serverEventFillNullMinutes | integer | The max session duration to fill in for missing server disconnect_time values. | 90 |
| armaConfig.missionEventFillNullMinutes | integer | The max session duration to fill in for missing mission disconnect_time values. | 15 |
| armaConfig.debug | boolean | Whether or not to enable debug logging. | false | | armaConfig.debug | boolean | Whether or not to enable debug logging. | false |
| armaConfig.traceLogToFile | boolean | Whether or not to enable trace logging to the addon folder's log file. | false |
## Usage ## Usage
@@ -97,23 +94,16 @@ FROM mysql.time_zone_name
### Performance ### Performance
The extension will update the disconnect_time field for each player every `dbUpdateIntervalSeconds` seconds. This is to ensure that the disconnect_time field is updated in the event that the server crashes or the mission ends without a disconnect event. The extension will update the disconnect_time field for each player every `dbUpdateInterval` seconds. This is to ensure that the disconnect_time field is updated in the event that the server crashes or the mission ends without a disconnect event.
These calls are threaded in the Go runtime and will not block the Arma 3 server while processing. The default value of 90 seconds should be sufficient for most servers. Each period begins when a player connects to the server or connects to a mission, which provides a natural offset. These calls are threaded in the Go runtime and will not block the Arma 3 server while processing. The default value of 90 seconds should be sufficient for most servers. Each period begins when a player connects to the server or connects to a mission, which provides a natural offset.
### NULL disconnect_time Values
In the event that the server crashes or a disconnect event for a mission is not sent, the next join for each will update past rows based on the following:
If the join time for a row is within [`{event_type}EventFillNullMinutes`](#configuration) minutes of the previous disconnect time, the previous disconnect time will be updated to the new join time. Otherwise, it will be set as [`{event_type}EventFillNullMinutes`](#configuration) from the join time for that row.
This is an attempt to account for missing events for individual players while not attributing large gap periods to their calculated session times. If the server crashes, the extension will update all rows with a NULL disconnect_time to the current time. See [Server Crash Time Filling](#server-crash-time-filling) for more information.
#### Server Crash Time Filling #### Server Crash Time Filling
The addon will update `@AttendanceTracker/lastServerTime.txt` with Arma 3's `diag_tickTime` every 30 seconds. This is to ensure that the server time is always available to the extension, even if the server crashes. This file is not used for any other purpose. In the event that the server crashes and a user has not been in the mission longer than `dbUpdateInterval` and therefore has a NULL `disconnect_time_utc` value, upon next launch the extension will update the row procedure:
On each time update, the extension will check this file and compare the received value to it. If the lastServerTime < lastServerTime.txt, the extension will assume that the server has restarted and will update all event rows with a NULL disconnect_time to the current time OR the threshold specified in the configuration file, whichever produces the smaller session duration. - If more than `dbUpdateInterval` has passed since the row's `join_time_utc` value, the row will be updated with a `disconnect_time_utc` value of `join_time_utc + dbUpdateInterval`.
- If less than `dbUpdateInterval` has passed since the row's `join_time_utc` value, the row will be updated with a `disconnect_time_utc` value of the current time.
--- ---
@@ -123,7 +113,7 @@ On each time update, the extension will check this file and compare the received
| --- | --- | | --- | --- |
| worlds | Stores world information. | | worlds | Stores world information. |
| missions | Stores mission information. | | missions | Stores mission information. |
| attendance_items | Stores rows that indicate player information and join/disconnect times. | | sessions | Stores rows that indicate player information and join/disconnect times. |
### Worlds ### Worlds
@@ -133,9 +123,9 @@ The worlds table will store basic info about the world. This is used to link mis
The missions table will store basic info about the mission. This is used to link attendance items to missions. The missions table will store basic info about the mission. This is used to link attendance items to missions.
### Attendance Items ### Sessions
The attendance_items table will store rows that indicate player information and join/disconnect times. This can be used to calculate play time per player per mission. Each row is also linked to a mission, so that these records can be grouped. The sessions table will store rows that indicate player information and join/disconnect times. This can be used to calculate play time per player per mission. Each row is also linked to a mission, so that these records can be grouped.
--- ---
@@ -143,37 +133,9 @@ The attendance_items table will store rows that indicate player information and
### Show missions with attendance ### Show missions with attendance
This will retrieve a view showing all missions with attendance data, sorted by the most recent mission joins first. Mission events without a mission disconnect_time (due to server crash or in-progress mission) will be ignored.
See [Timezone](#timezone) for more information on converting times to your local timezone. See [Timezone](#timezone) for more information on converting times to your local timezone.
```sql TODO
select
a.server_profile as Server,
a.briefing_name as "Mission Name",
CONVERT_TZ(a.mission_start, 'UTC', 'US/Eastern') as "Start Time",
b.display_name as "World",
c.profile_name as "Player Name",
c.player_uid as "Player UID",
TIMESTAMPDIFF(
MINUTE,
c.join_time,
c.disconnect_time
) as "Play Time (m)",
CONVERT_TZ(c.join_time, 'UTC', 'US/Eastern') as "Join Time",
CONVERT_TZ(c.disconnect_time, 'UTC', 'US/Eastern') as "Leave Time"
from missions a
LEFT JOIN worlds b ON a.world_id = b.id
LEFT JOIN attendance_items c ON a.mission_hash = c.mission_hash
where
c.event_type = 'Mission'
AND c.disconnect_time IS NOT NULL
AND TIMESTAMPDIFF(
MINUTE,
c.join_time,
c.disconnect_time
) > 0
```
--- ---
@@ -187,9 +149,7 @@ Pull requests are welcome. For major changes, please open an issue first to disc
### Prerequisites ### Prerequisites
- [Go 1.16.4](https://golang.org/doc/install) - Docker
- [MinGW-w64](https://sourceforge.net/projects/mingw-w64/) (Windows only)
- [GCC](https://gcc.gnu.org/) (Linux only)
### Building Extension using Docker ### Building Extension using Docker
@@ -199,40 +159,50 @@ Once it's built, copy the file from ./dist to the project root, then build the a
#### COMPILING FOR WINDOWS #### COMPILING FOR WINDOWS
These compile commands should be run from the project root.
```ps1 ```ps1
docker pull x1unix/go-mingw:1.20 docker pull x1unix/go-mingw:1.20
# version is semantic + build date + git hash # version is semantic + build date + git hash
# e.g. 1.0.0-2021-05-30-1a2b3c4d # e.g. 1.0.0-2021-05-30-1a2b3c4d
$versionSem = '0.2.0' $versionSem = '1.1.1'
$dateStr = Get-Date -Format 'yyyyMMdd' $dateStr = Get-Date -Format 'yyyyMMdd'
$version = "$versionSem-$dateStr-$(git rev-parse --short HEAD)" $version = "$versionSem-$dateStr-$(git rev-parse --short HEAD)"
# Compile x64 Windows DLL # Compile x64 Windows DLL
docker run --rm -it -v ${PWD}:/go/work -w /go/work x1unix/go-mingw:1.20 go build -o dist/AttendanceTracker_x64.dll -buildmode=c-shared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd docker run --rm -it -v ${PWD}\extension\AttendanceTracker:/go/work -w /go/work -e GOARCH=amd64 -e CGO_ENABLED=1 x1unix/go-mingw:1.20 go build -o ./dist/AttendanceTracker_x64.dll -buildmode=c-shared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd
Move-Item -Path ./extension/AttendanceTracker/dist/AttendanceTracker_x64.dll -Destination ./AttendanceTracker_x64.dll -Force
# Compile x86 Windows DLL # Compile x86 Windows DLL
docker run --rm -it -v ${PWD}:/go/work -w /go/work -e GOARCH=386 x1unix/go-mingw:1.20 go build -o dist/AttendanceTracker.dll -buildmode=c-shared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd docker run --rm -it -v ${PWD}\extension\AttendanceTracker:/go/work -w /go/work -e GOARCH=386 -e CGO_ENABLED=1 x1unix/go-mingw:1.20 go build -o ./dist/AttendanceTracker.dll -buildmode=c-shared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd
Move-Item -Path ./extension/AttendanceTracker/dist/AttendanceTracker.dll -Destination ./AttendanceTracker.dll -Force
# Compile x64 Windows EXE # Compile x64 Windows EXE
docker run --rm -it -v ${PWD}:/go/work -w /go/work x1unix/go-mingw:1.20 go build -o dist/AttendanceTracker_x64.exe -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd docker run --rm -it -v ${PWD}:/go/work -w /go/work -e GOARCH=amd64 -e CGO_ENABLED=1 x1unix/go-mingw:1.20 go build -o ./dist/AttendanceTracker_x64.exe -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./extension/AttendanceTracker/cmd
``` ```
#### COMPILING FOR LINUX #### COMPILING FOR LINUX
```bash ```ps1
docker build -t indifox926/build-a3go:linux-so -f ./build/Dockerfile.build ./cmd docker build -t indifox926/build-a3go:linux-so -f ./build/Dockerfile.build .
# Compile x64 Linux .so # Compile x64 Linux .so
docker run --rm -it -v ${PWD}:/app -e GOOS=linux -e GOARCH=amd64 -e CGO_ENABLED=1 -e CC=gcc indifox926/build-a3go:linux-so go build -o dist/AttendanceTracker_x64.so -linkshared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd docker run --rm -it -v ${PWD}\extension\AttendanceTracker:/app -e GOOS=linux -e GOARCH=amd64 -e CGO_ENABLED=1 indifox926/build-a3go:linux-so go build -o ./dist/AttendanceTracker_x64.so -linkshared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd
Move-Item -Path ./extension/AttendanceTracker/dist/AttendanceTracker_x64.so -Destination ./AttendanceTracker_x64.so -Force
# Compile x86 Linux .so # Compile x86 Linux .so
docker run --rm -it -v ${PWD}:/app -e GOOS=linux -e GOARCH=386 -e CGO_ENABLED=1 -e CC=gcc indifox926/build-a3go:linux-so go build -o dist/AttendanceTracker.so -linkshared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd docker run --rm -it -v ${PWD}\extension\AttendanceTracker:/app -e GOOS=linux -e GOARCH=386 -e CGO_ENABLED=1 indifox926/build-a3go:linux-so go build -o ./dist/AttendanceTracker.so -linkshared -ldflags "-w -s -X main.EXTENSION_VERSION=$version" ./cmd
Move-Item -Path ./extension/AttendanceTracker/dist/AttendanceTracker.so -Destination ./AttendanceTracker.so -Force
``` ```
### Compile Addon ### Compile Addon
First, move the compiled dlls from extension/AttendanceTracker/dist to the project root. First, move the compiled dlls from `extension/AttendanceTracker/dist` to the project root.
To prepare the addon, you'll need to download the [HEMTT](https://brettmayson.github.io/HEMTT/commands/build.html) binary, place it in the project root, and run the following command: To prepare the addon, you'll need to download the [HEMTT](https://brettmayson.github.io/HEMTT/commands/build.html) binary, place it in the project root, and run the following command:

View File

@@ -1 +1 @@
x\addons\attendancetracker\main x\attendancetracker\addons\main

View File

@@ -1,7 +1,7 @@
#include "script_mod.hpp" #include "script_component.hpp"
class CfgPatches { class CfgPatches {
class AttendanceTracker { class ADDON {
units[] = {}; units[] = {};
weapons[] = {}; weapons[] = {};
requiredVersion = 2.10; requiredVersion = 2.10;
@@ -17,20 +17,17 @@ class CfgPatches {
}; };
class CfgFunctions { class CfgFunctions {
class attendanceTracker { class ADDON {
class functions { class functions {
file = "x\addons\attendancetracker\main\functions"; class postInit {
class postInit {postInit = 1;}; file = QPATHTOF(DOUBLES(fnc,postInit).sqf);
class callbackHandler {postInit = 1;}; postInit = 1;
class getMissionHash {}; };
class getMissionInfo {}; PATHTO_FNC(getMissionInfo);
class getSettings {}; PATHTO_FNC(getWorldInfo);
class getWorldInfo {}; PATHTO_FNC(log);
class log {}; PATHTO_FNC(missionLoaded);
class missionLoaded {}; PATHTO_FNC(onPlayerConnected);
class onPlayerConnected {};
class timestamp {};
class writePlayer {};
}; };
}; };
}; };

View File

@@ -0,0 +1,13 @@
#include "script_component.hpp"
[
["missionName", missionName],
["missionStart", GVAR(missionStart)],
["missionHash", GVAR(missionHash)],
["briefingName", briefingName],
["missionNameSource", missionNameSource],
["onLoadName", getMissionConfigValue ["onLoadName", "Unknown"]],
["author", getMissionConfigValue ["author", "Unknown"]],
["serverName", serverName],
["serverProfile", profileName],
["worldName", toLower worldName]
];

View File

@@ -1,3 +1,5 @@
#include "script_component.hpp"
_world = ( configfile >> "CfgWorlds" >> worldName ); _world = ( configfile >> "CfgWorlds" >> worldName );
_author = getText( _world >> "author" ); _author = getText( _world >> "author" );
_name = getText ( _world >> "description" ); _name = getText ( _world >> "description" );
@@ -5,7 +7,6 @@ _name = getText ( _world >> "description" );
_source = configSourceMod ( _world ); _source = configSourceMod ( _world );
_workshopID = ''; _workshopID = '';
{ {
if ( ( _x#1 ) == _source ) then { if ( ( _x#1 ) == _source ) then {
_workshopID = _x#7; _workshopID = _x#7;
@@ -13,8 +14,12 @@ _workshopID = '';
}; };
} foreach getLoadedModsInfo; } foreach getLoadedModsInfo;
if (_workshopID isEqualTo "") then {
_workshopID = "0";
};
// [_name, _author, _workshopID]; // [_name, _author, _workshopID];
_return = createHashMapFromArray [ _return = [
["author", _author], ["author", _author],
["workshopID", _workshopID], ["workshopID", _workshopID],
["displayName", _name], ["displayName", _name],
@@ -24,5 +29,5 @@ _return = createHashMapFromArray [
["latitude", -1 * getNumber( _world >> "latitude" )], ["latitude", -1 * getNumber( _world >> "latitude" )],
["longitude", getNumber( _world >> "longitude" )] ["longitude", getNumber( _world >> "longitude" )]
]; ];
[format["WorldInfo is: %1", _return]] call attendanceTracker_fnc_log; ["DEBUG", format["WorldInfo is: %1", _return]] call FUNC(log);
_return _return

31
addons/main/fnc_log.sqf Normal file
View File

@@ -0,0 +1,31 @@
#include "script_component.hpp"
if (!isServer) exitWith {};
if (typeName _this != "ARRAY") exitWith {
diag_log format ["[%1]: Invalid log params: %2", GVAR(logPrefix), _this];
};
params [
["_level", "INFO", [""]],
["_text", "", ["", []]]
];
if (_text isEqualType []) then {
_text = format ["%1", _text];
};
if (
_level == "DEBUG" &&
!GVAR(debug)
) exitWith {};
if (_text isEqualTo "") exitWith {};
diag_log formatText [
"[%1] %2: %3",
GVAR(logPrefix),
_level,
_text
];

View File

@@ -1 +1,3 @@
#include "script_component.hpp"
!(getClientStateNumber <= 5 || getClientStateNumber isEqualTo 11); !(getClientStateNumber <= 5 || getClientStateNumber isEqualTo 11);

View File

@@ -0,0 +1,84 @@
#include "script_component.hpp"
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
["DEBUG", format ["(EventHandler) PlayerConnected fired: %1", _this]] call FUNC(log);
if !(call FUNC(missionLoaded)) exitWith {
["DEBUG", format ["(EventHandler) PlayerConnected: Server is in Mission Asked, likely mission selection state. Skipping.."]] call FUNC(log);
};
private _userInfo = (getUserInfo _idstr);
if ((count _userInfo) isEqualTo 0) exitWith {
["DEBUG", format ["(EventHandler) PlayerConnected: No user info found for %1", _idstr]] call FUNC(log);
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[
"DEBUG",
format [
"(EventHandler) PlayerConnected: %1 is HC, skipping",
_playerID
]
] call FUNC(log);
};
// start CBA PFH
[
"DEBUG",
format [
"(EventHandler) PlayerConnected: Starting CBA PFH for %1",
_playerID
]
] call FUNC(log);
[{
params ["_args", "_handle"];
// every dbUpdateInterval, queue a wait for the mission to be logged
// times out after 30 seconds
// used to ensure joins at start of mission (during db connect) are logged
[{GVAR(missionLogged)}, {
// check if player is still connected
private _hash = _this;
private _clientStateNumber = 0;
private _userInfo = getUserInfo (_hash get "playerId");
if (_userInfo isEqualTo []) exitWith {
["DEBUG", format ["(EventHandler) PlayerConnected: %1 (UID) is no longer connected to the mission, exiting CBA PFH", _hash get "playerUID"]] call FUNC(log);
[_handle] call CBA_fnc_removePerFrameHandler;
};
_clientStateNumber = _userInfo select 6;
if (_clientStateNumber < 6) exitWith {
["DEBUG", format ["(EventHandler) PlayerConnected: %1 (UID) is no longer connected to the mission, exiting CBA PFH", _hash get "playerUID"]] call FUNC(log);
[_handle] call CBA_fnc_removePerFrameHandler;
};
["DEBUG", format [
"(EventHandler) PlayerConnected: %1 (UID) is connected to the mission, logging. data: %2",
_hash get "playerUID",
_hash
]] call FUNC(log);
GVAR(extensionName) callExtension [
":LOG:PRESENCE:", [
_hash
]];
},
_args, // args
30 // timeout
] call CBA_fnc_waitUntilAndExecute;
},
GVAR(updateInterval),
(createHashMapFromArray [ // args
["playerId", _playerID],
["playerUID", _playerUID],
["profileName", _profileName],
["steamName", _steamName],
["isJIP", _jip],
["roleDescription", if (roleDescription _unit isEqualTo "") then {"None"} else {roleDescription _unit}],
["missionHash", GVAR(missionHash)]
])
] call CBA_fnc_addPerFrameHandler;

View File

@@ -0,0 +1,94 @@
#include "script_component.hpp"
if (!isServer) exitWith {};
GVAR(attendanceTracker) = true;
GVAR(debug) = true;
GVAR(logPrefix) = "AttendanceTracker";
GVAR(extensionName) = "AttendanceTracker";
GVAR(missionLogged) = false;
addMissionEventHandler ["ExtensionCallback", {
params ["_name", "_function", "_data"];
if !(_name isEqualTo GVAR(extensionName)) exitWith {};
_dataArr = parseSimpleArray _data;
if (count _dataArr isEqualTo 0) exitWith {};
switch (_function) do {
case ":LOG:MISSION:SUCCESS:": {
GVAR(missionLogged) = true;
};
case ":LOG:": {
diag_log formatText[
"[%1] %2",
GVAR(logPrefix),
_dataArr select 0
];
};
default {
["DEBUG", format["%1", _dataArr]] call FUNC(log);
};
};
}];
// LOAD EXTENSION
GVAR(extensionName) callExtension ":START:";
// GET MISSION START TIMESTAMP AND UNIQUE HASH
private _missionHashData = parseSimpleArray ("AttendanceTracker" callExtension ":MISSION:HASH:");
if (count _missionHashData isEqualTo 0) exitWith {
["ERROR", "Failed to get mission hash, exiting"] call FUNC(log);
};
_missionHashData params ["_timestamp", "_hash"];
GVAR(missionStart) = _timestamp;
GVAR(missionHash) = _hash;
// PARSE SETTINGS
private _settings = parseSimpleArray (GVAR(extensionName) callExtension ":GET:SETTINGS:");
if (count _settings isEqualTo 0) exitWith {
["ERROR", "Failed to get settings, exiting"] call FUNC(log);
};
GVAR(settings) = createHashMapFromArray (_settings#0);
GVAR(debug) = GVAR(settings) getOrDefault ["debug", GVAR(debug)];
private _updateInterval = GVAR(settings) getOrDefault ["dbupdateinterval", 90];
// remove duration by removing the last index
_updateInterval = _updateInterval select [0, count _updateInterval - 1];
GVAR(updateInterval) = parseNumber _updateInterval;
// add player connected (to mission) handler
addMissionEventHandler ["PlayerConnected", {
_this call FUNC(onPlayerConnected);
}];
// we'll wait for the end of init (DB connect included) of the extension
// then we'll log the world and mission
// the response to THAT is handled above in the extension callback
// and will set GVAR(missionLogged) true
addMissionEventHandler ["ExtensionCallback", {
params ["_name", "_function", "_data"];
if !(_name isEqualTo GVAR(extensionName)) exitWith {};
if !(_function isEqualTo ":READY:") exitWith {};
// LOAD WORLD AND MISSION INFO
GVAR(worldInfo) = call FUNC(getWorldInfo);
GVAR(missionInfo) = call FUNC(getMissionInfo);
["INFO", (GVAR(extensionName) callExtension [
":LOG:MISSION:",
[
GVAR(worldInfo),
GVAR(missionInfo)
]
]) select 0] call FUNC(log);
// remove the handler
removeMissionEventHandler ["ExtensionCallback", _thisEventHandler];
}];

View File

@@ -1,24 +0,0 @@
addMissionEventHandler ["ExtensionCallback", {
params ["_name", "_function", "_data"];
if !(_name isEqualTo "AttendanceTracker") exitWith {};
if (ATDebug && _function isNotEqualTo ":LOG:") then {
diag_log format ["Raw callback: %1 _ %2", _function, _data];
};
_dataArr = parseSimpleArray _data;
if (count _dataArr < 1) exitWith {};
switch (_function) do {
case ":LOG:": {
diag_log formatText[
"[Attendance Tracker] %1",
_dataArr select 0
];
};
default {
[format["%1", _dataArr]] call attendanceTracker_fnc_log;
};
};
true;
}];

View File

@@ -1,19 +0,0 @@
addMissionEventHandler ["ExtensionCallback", {
params ["_extension", "_function", "_data"];
if !(_extension isEqualTo "AttendanceTracker") exitWith {};
if !(_function isEqualTo ":MISSION:HASH:") exitWith {};
_dataArr = parseSimpleArray _data;
if (count _dataArr < 1) exitWith {};
_dataArr params ["_startTime", "_hash"];
ATNamespace setVariable ["missionStartTime", call attendanceTracker_fnc_timestamp];
ATNamespace setVariable ["missionHash", _hash];
removeMissionEventHandler [
"ExtensionCallback",
_thisEventHandler
];
}];
"AttendanceTracker" callExtension ":MISSION:HASH:";

View File

@@ -1,12 +0,0 @@
createHashMapFromArray [
["missionName", missionName],
["missionStart", ATNamespace getVariable "missionStartTime"],
["missionHash", ATNamespace getVariable "missionHash"],
["briefingName", briefingName],
["missionNameSource", missionNameSource],
["onLoadName", getMissionConfigValue ["onLoadName", ""]],
["author", getMissionConfigValue ["author", ""]],
["serverName", serverName],
["serverProfile", profileName],
["worldName", toLower worldName]
];

View File

@@ -1,27 +0,0 @@
addMissionEventHandler ["ExtensionCallback", {
params ["_extension", "_function", "_data"];
if !(_extension isEqualTo "AttendanceTracker") exitWith {};
if !(_function isEqualTo ":GET:SETTINGS:") exitWith {};
_dataArr = parseSimpleArray _data;
diag_log format ["AT: Settings received: %1", _dataArr];
if (count _dataArr < 1) exitWith {};
private _settingsJSON = _dataArr select 0;
private _settingsNamespace = [_settingsJSON] call CBA_fnc_parseJSON;
{
ATNamespace setVariable [_x, _settingsNamespace getVariable _x];
} forEach (allVariables _settingsNamespace);
ATDebug = ATNamespace getVariable "debug";
ATUpdateDelay = ATNamespace getVariable "dbUpdateInterval";
// remove last character (unit of time) and parse to number
ATUpdateDelay = parseNumber (ATUpdateDelay select [0, count ATUpdateDelay - 1]);
removeMissionEventHandler [
"ExtensionCallback",
_thisEventHandler
];
}];
"AttendanceTracker" callExtension ":GET:SETTINGS:";

View File

@@ -1,17 +0,0 @@
#include "..\script_mod.hpp"
params [
["_message", "", [""]],
["_level", "INFO", [""]],
"_function"
];
if (isNil "_message") exitWith {false};
if (
missionNamespace getVariable ["ATDebug", true] &&
_level != "WARN" && _level != "ERROR"
) exitWith {};
LOG_SYS(_level, _message);
true;

View File

@@ -1,63 +0,0 @@
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 ((count _userInfo) isEqualTo 0) 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;
};
// start CBA PFH
[
format [
"(EventHandler) PlayerConnected: Starting CBA PFH for %1",
_playerID
],
"DEBUG"
] call attendanceTracker_fnc_log;
[
{
params ["_args", "_handle"];
// check if player is still connected
_args params ["_playerID", "_playerUID", "_profileName", "_steamName", "_jip", "_roleDescription"];
private _userInfo = getUserInfo _playerID;
private _clientStateNumber = 0;
if (_userInfo isEqualTo []) exitWith {
[_handle] call CBA_fnc_removePerFrameHandler;
};
_clientStateNumber = _userInfo select 6;
if (_clientStateNumber < 6) exitWith {
[format ["(EventHandler) PlayerConnected: %1 (UID) is no longer connected to the mission, exiting CBA PFH", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
[_handle] call CBA_fnc_removePerFrameHandler;
};
_args call attendanceTracker_fnc_writePlayer;
},
ATUpdateDelay,
[
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
roleDescription _unit
]
] call CBA_fnc_addPerFrameHandler;

View File

@@ -1,61 +0,0 @@
#include "..\script_mod.hpp"
if (!isServer) exitWith {};
ATNamespace = false call CBA_fnc_createNamespace;
ATDebug = true;
"AttendanceTracker" callExtension ":START:";
// we'll wait for the asynchronous init steps of the extension to finish, to confirm we have a DB connection and our config was loaded. If there are errors with either, the extension won't reply and initiate further during this mission.
addMissionEventHandler ["ExtensionCallback", {
params ["_name", "_function", "_data"];
if !(_name isEqualTo "AttendanceTracker") exitWith {};
if !(_function isEqualTo ":READY:") exitWith {};
call attendanceTracker_fnc_getMissionHash;
call attendanceTracker_fnc_getSettings;
[
{// wait until settings have been loaded from extension
!isNil {ATNamespace getVariable "missionHash"} &&
!isNil {ATDebug}
},
{
// get world and mission context
ATNamespace setVariable [
"worldContext",
call attendanceTracker_fnc_getWorldInfo
];
ATNamespace setVariable [
"missionContext",
call attendanceTracker_fnc_getMissionInfo
];
// write them to establish DB rows
"AttendanceTracker" callExtension [
":LOG:MISSION:",
[
[ATNamespace getVariable "missionContext"] call CBA_fnc_encodeJSON,
[ATNamespace getVariable "worldContext"] call CBA_fnc_encodeJSON
]
];
// add player connected (to mission) handler
addMissionEventHandler ["PlayerConnected", {
_this call attendanceTracker_fnc_onPlayerConnected;
}];
},
[],
10, // 10 second timeout
{ // timeout code
["Failed to load settings", "ERROR"] call attendanceTracker_fnc_log;
}
] call CBA_fnc_waitUntilAndExecute;
removeMissionEventHandler [
"ExtensionCallback",
_thisEventHandler
];
}];

View File

@@ -1,24 +0,0 @@
// (parseSimpleArray ("AttendanceTracker" callExtension "getTimestamp")) select 0;
// const time.RFC3339 untyped string = "2006-01-02T15:04:05Z07:00"
systemTimeUTC apply {if (_x < 10) then {"0" + str _x} else {str _x}} params [
"_year",
"_month",
"_day",
"_hour",
"_minute",
"_second",
"_millisecond"
];
format[
"%1-%2-%3T%4:%5:%6Z",
_year,
_month,
_day,
_hour,
_minute,
_second
];

View File

@@ -1,21 +0,0 @@
params [
["_playerId", ""],
["_playerUID", ""],
["_profileName", ""],
["_steamName", ""],
["_isJIP", false, [true, false]],
["_roleDescription", ""]
];
private _hash = +(ATNamespace getVariable ["missionContext", createHashMap]);
_hash set ["playerId", _playerId];
_hash set ["playerUID", _playerUID];
_hash set ["profileName", _profileName];
_hash set ["steamName", _steamName];
_hash set ["isJIP", _isJIP];
_hash set ["roleDescription", _roleDescription];
"AttendanceTracker" callExtension [":LOG:PRESENCE:", [[_hash] call CBA_fnc_encodeJSON]];
true;

View File

@@ -0,0 +1,4 @@
#define COMPONENT main
#define COMPONENT_BEAUTIFIED Main
#include "\x\attendancetracker\addons\main\script_mod.hpp"

View File

@@ -1,12 +1,8 @@
#include "script_version.hpp" #include "script_version.hpp"
#define COMPONENT main
#define COMPONENT_BEAUTIFIED Main
#define MAINPREFIX x #define MAINPREFIX x
#define PREFIX attendancetracker
#define PREFIX_BEAUTIFIED AttendanceTracker
#define SUBPREFIX addons #define SUBPREFIX addons
#define PREFIX AttendanceTracker
#include "\x\cba\addons\main\script_macros_common.hpp" #include "\x\cba\addons\main\script_macros_common.hpp"

View File

@@ -1,8 +1,8 @@
#define MAJOR 0 #define MAJOR 1
#define MINOR 2 #define MINOR 1
#define PATCH 0 #define PATCH 0
#define BUILD 20231003 #define BUILD 20231012
#define VERSION 0.2 #define VERSION 1.1
#define VERSION_STR MAJOR##.##MINOR##.##PATCH##.##BUILD #define VERSION_STR MAJOR##.##MINOR##.##PATCH##.##BUILD
#define VERSION_AR MAJOR,MINOR,PATCH,BUILD #define VERSION_AR MAJOR,MINOR,PATCH,BUILD

11
build/Dockerfile.build Normal file
View File

@@ -0,0 +1,11 @@
# build Golang app for Linux
FROM golang:1.20
WORKDIR /app
# get gcc-multilib and gcc-mingw-w64
RUN apt update
RUN apt install -y gcc-multilib gcc-mingw-w64
CMD ["/bin/sh"]

Binary file not shown.

View File

@@ -13,7 +13,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"time" "time"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
@@ -22,54 +21,63 @@ import (
"github.com/indig0fox/Arma3-AttendanceTracker/internal/util" "github.com/indig0fox/Arma3-AttendanceTracker/internal/util"
"github.com/indig0fox/a3go/a3interface" "github.com/indig0fox/a3go/a3interface"
"github.com/indig0fox/a3go/assemblyfinder" "github.com/indig0fox/a3go/assemblyfinder"
"github.com/rs/zerolog"
) )
const EXTENSION_NAME string = "AttendanceTracker" const EXTENSION_NAME string = "AttendanceTracker"
const ADDON_NAME string = "AttendanceTracker" const ADDON_NAME string = "AttendanceTracker"
const EXTENSION_VERSION string = "dev"
// file paths // file paths
const ATTENDANCE_TABLE string = "attendance" const ATTENDANCE_TABLE string = "attendance"
const MISSIONS_TABLE string = "missions" const MISSIONS_TABLE string = "missions"
const WORLDS_TABLE string = "worlds" const WORLDS_TABLE string = "worlds"
var currentMissionID uint = 0
var RVExtensionChannels = map[string]chan string{
":START:": make(chan string),
":MISSION:HASH:": make(chan string),
":GET:SETTINGS:": make(chan string),
}
var RVExtensionArgsChannels = map[string]chan []string{
":LOG:MISSION:": make(chan []string),
":LOG:PRESENCE:": make(chan []string),
}
var ( var (
EXTENSION_VERSION string = "DEVELOPMENT"
modulePath string modulePath string
modulePathDir string modulePathDir string
initSuccess bool // default false loadedMission *Mission
loadedWorld *World
) )
// configure log output // configure log output
func init() { func init() {
a3interface.SetVersion(EXTENSION_VERSION) a3interface.SetVersion(EXTENSION_VERSION)
a3interface.RegisterRvExtensionChannels(RVExtensionChannels) a3interface.NewRegistration(":START:").
a3interface.RegisterRvExtensionArgsChannels(RVExtensionArgsChannels) SetFunction(onStartCommand).
SetRunInBackground(false).
Register()
a3interface.NewRegistration(":MISSION:HASH:").
SetFunction(onMissionHashCommand).
SetRunInBackground(false).
Register()
a3interface.NewRegistration(":GET:SETTINGS:").
SetFunction(onGetSettingsCommand).
SetRunInBackground(false).
Register()
a3interface.NewRegistration(":LOG:MISSION:").
SetDefaultResponse(`Logging mission data`).
SetArgsFunction(onLogMissionArgsCommand).
SetRunInBackground(true).
Register()
a3interface.NewRegistration(":LOG:PRESENCE:").
SetDefaultResponse(`Logging presence data`).
SetArgsFunction(onLogPresenceArgsCommand).
SetRunInBackground(true).
Register()
go func() { go func() {
var err error var err error
modulePath = assemblyfinder.GetModulePath() modulePath = assemblyfinder.GetModulePath()
// get absolute path of module path modulePathDir = filepath.Dir(modulePath)
modulePathAbs, err := filepath.Abs(modulePath)
if err != nil {
panic(err)
}
modulePathDir = filepath.Dir(modulePathAbs)
result, configErr := util.LoadConfig(modulePathDir) result, configErr := util.LoadConfig(modulePathDir)
logger.InitLoggers(&logger.LoggerOptionsType{ logger.InitLoggers(&logger.LoggerOptionsType{
@@ -82,9 +90,11 @@ func init() {
)), )),
AddonName: ADDON_NAME, AddonName: ADDON_NAME,
ExtensionName: EXTENSION_NAME, ExtensionName: EXTENSION_NAME,
ExtensionVersion: EXTENSION_VERSION,
Debug: util.ConfigJSON.GetBool("armaConfig.debug"), Debug: util.ConfigJSON.GetBool("armaConfig.debug"),
Trace: util.ConfigJSON.GetBool("armaConfig.traceLogToFile"), Trace: util.ConfigJSON.GetBool("armaConfig.trace"),
}) })
logger.RotateLogs()
if configErr != nil { if configErr != nil {
logger.Log.Error().Err(configErr).Msgf(`Error loading config`) logger.Log.Error().Err(configErr).Msgf(`Error loading config`)
return return
@@ -92,9 +102,7 @@ func init() {
logger.Log.Info().Msgf(result) logger.Log.Info().Msgf(result)
} }
logger.RotateLogs() logger.Log.Info().Msgf(`%s v%s started`, EXTENSION_NAME, EXTENSION_VERSION)
logger.ArmaOnly.Info().Msgf(`%s v%s started`, EXTENSION_NAME, "0.0.0")
logger.ArmaOnly.Info().Msgf(`Log path: %s`, logger.ActiveOptions.Path) logger.ArmaOnly.Info().Msgf(`Log path: %s`, logger.ActiveOptions.Path)
db.SetConfig(db.ConfigStruct{ db.SetConfig(db.ConfigStruct{
@@ -124,11 +132,10 @@ func init() {
) )
if err != nil { if err != nil {
logger.Log.Error().Err(err).Msgf(`Error migrating database schema`) logger.Log.Error().Err(err).Msgf(`Error migrating database schema`)
} else {
logger.Log.Info().Msgf(`Database schema migrated`)
} }
startA3CallHandlers()
initSuccess = true
a3interface.WriteArmaCallback( a3interface.WriteArmaCallback(
EXTENSION_NAME, EXTENSION_NAME,
":READY:", ":READY:",
@@ -138,56 +145,84 @@ func init() {
}() }()
} }
func startA3CallHandlers() error { func onStartCommand(
go func() { ctx a3interface.ArmaExtensionContext,
for { data string,
select { ) (string, error) {
case <-RVExtensionChannels[":START:"]: logger.Log.Debug().Msgf(`RVExtension :START: requested`)
logger.Log.Trace().Msgf(`RVExtension :START: requested`) loadedWorld = nil
if !initSuccess { loadedMission = nil
logger.Log.Warn().Msgf(`Received another :START: command before init was complete, ignoring.`) return fmt.Sprintf(
continue `["%s v%s started"]`,
} else {
logger.RotateLogs()
a3interface.WriteArmaCallback(
EXTENSION_NAME, EXTENSION_NAME,
":READY:", EXTENSION_VERSION,
) ), nil
} }
case <-RVExtensionChannels[":MISSION:HASH:"]:
logger.Log.Trace().Msgf(`RVExtension :MISSION:HASH: requested`) func onMissionHashCommand(
ctx a3interface.ArmaExtensionContext,
data string,
) (string, error) {
logger.Log.Debug().Msgf(`RVExtension :MISSION:HASH: requested`)
timestamp, hash := getMissionHash() timestamp, hash := getMissionHash()
a3interface.WriteArmaCallback( return fmt.Sprintf(
EXTENSION_NAME, `[%q, %q]`,
":MISSION:HASH:",
timestamp, timestamp,
hash, hash,
) ), nil
case <-RVExtensionChannels[":GET:SETTINGS:"]: }
logger.Log.Trace().Msg(`Settings requested`)
armaConfig, err := util.ConfigArmaFormat() func onGetSettingsCommand(
ctx a3interface.ArmaExtensionContext,
data string,
) (string, error) {
logger.Log.Debug().Msg(`RVExtension :GET:SETTINGS: requested`)
// get arma config
c := util.ConfigJSON.Get("armaConfig")
armaConfig := a3interface.ToArmaHashMap(c)
return fmt.Sprintf(
`[%s]`,
armaConfig,
), nil
}
func onLogMissionArgsCommand(
ctx a3interface.ArmaExtensionContext,
command string,
args []string,
) (string, error) {
thisLogger := logger.Log.With().Str("command", command).Interface("ctx", ctx).Logger()
thisLogger.Debug().Msgf(`RVExtension :LOG:MISSION: requested`)
var err error
world, err := writeWorldInfo(args[0], thisLogger)
if err != nil { if err != nil {
logger.Log.Error().Err(err).Msg(`Error when marshaling arma config`) return ``, err
continue
} }
logger.Log.Trace().Str("armaConfig", armaConfig).Send() loadedWorld = &world
mission, err := writeMission(args[1], thisLogger)
if err != nil {
return ``, err
}
loadedMission = &mission
a3interface.WriteArmaCallback( a3interface.WriteArmaCallback(
EXTENSION_NAME, EXTENSION_NAME,
":GET:SETTINGS:", ":LOG:MISSION:SUCCESS:",
armaConfig,
) )
case v := <-RVExtensionArgsChannels[":LOG:MISSION:"]:
go func(data []string) {
writeWorldInfo(v[1])
writeMission(v[0])
}(v)
case v := <-RVExtensionArgsChannels[":LOG:PRESENCE:"]:
go writeAttendance(v[0])
}
}
}()
return nil return ``, nil
}
func onLogPresenceArgsCommand(
ctx a3interface.ArmaExtensionContext,
command string,
args []string,
) (string, error) {
thisLogger := logger.Log.With().Str("command", command).Interface("ctx", ctx).Logger()
thisLogger.Debug().Msgf(`RVExtension :LOG:PRESENCE: requested`)
writeAttendance(args[0], thisLogger)
return ``, nil
} }
// getMissionHash will return the current time in UTC and an md5 hash of that time // getMissionHash will return the current time in UTC and an md5 hash of that time
@@ -197,7 +232,7 @@ func getMissionHash() (sqlTime, hashString string) {
nowTime := time.Now().UTC() nowTime := time.Now().UTC()
// mysql format // mysql format
sqlTime = nowTime.Format("2006-01-02 15:04:05") sqlTime = nowTime.Format(time.RFC3339)
hash := md5.Sum([]byte(sqlTime)) hash := md5.Sum([]byte(sqlTime))
hashString = fmt.Sprintf(`%x`, hash) hashString = fmt.Sprintf(`%x`, hash)
@@ -240,86 +275,162 @@ func finalizeUnendedSessions() {
logger.Log.Info().Msgf(`Filled disconnect time of %d events.`, len(events)) logger.Log.Info().Msgf(`Filled disconnect time of %d events.`, len(events))
} }
func writeWorldInfo(worldInfo string) { func writeWorldInfo(worldInfo string, thisLogger zerolog.Logger) (World, error) {
// worldInfo is json, parse it
var wi World parsedInterface, err := a3interface.ParseSQF(worldInfo)
fixedString := unescapeArmaQuotes(worldInfo)
err := json.Unmarshal([]byte(fixedString), &wi)
if err != nil { if err != nil {
logger.Log.Error().Err(err).Msgf(`Error when unmarshalling world info`) thisLogger.Error().Err(err).Msgf(`Error when parsing world info`)
return return World{}, err
}
parsedMap, err := a3interface.ParseSQFHashMap(parsedInterface)
if err != nil {
thisLogger.Error().Err(err).Msgf(`Error when parsing world info`)
return World{}, err
}
thisLogger.Trace().Msgf(`parsedMap: %+v`, parsedMap)
// create world object from map[string]interface{}
var wi = World{}
worldBytes, err := json.Marshal(parsedMap)
if err != nil {
thisLogger.Error().Err(err).Msgf(`Error when marshalling world info`)
return World{}, err
}
err = json.Unmarshal(worldBytes, &wi)
if err != nil {
thisLogger.Error().Err(err).Msgf(`Error when unmarshalling world info`)
return World{}, err
}
thisLogger.Trace().Msgf(`World info: %+v`, wi)
var dbWorld World
db.Client().Where("world_name = ?", wi.WorldName).First(&dbWorld)
// if world exists, use it
if dbWorld.ID > 0 {
thisLogger.Debug().Msgf(`World %s exists with ID %d.`, wi.WorldName, dbWorld.ID)
return dbWorld, nil
} }
// write world if not exist // write world if not exist
var dbWorld World
db.Client().Where("world_name = ?", wi.WorldName).First(&dbWorld)
if dbWorld.ID == 0 {
db.Client().Create(&wi) db.Client().Create(&wi)
if db.Client().Error != nil { if db.Client().Error != nil {
logger.Log.Error().Err(db.Client().Error).Msgf(`Error when creating world`) thisLogger.Error().Err(db.Client().Error).Msgf(`Error when creating world`)
return return World{}, db.Client().Error
}
logger.Log.Info().Msgf(`World %s created.`, wi.WorldName)
} else {
// don't do anything if exists
logger.Log.Debug().Msgf(`World %s exists with ID %d.`, wi.WorldName, dbWorld.ID)
} }
thisLogger.Info().Msgf(`World %s created.`, wi.WorldName)
return wi, nil
} }
func writeMission(missionJSON string) { func writeMission(data string, thisLogger zerolog.Logger) (Mission, error) {
var err error var err error
// writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, Mission)) parsedInterface, err := a3interface.ParseSQF(data)
// Mission is json, parse it
var mi Mission
fixedString := fixEscapeQuotes(trimQuotes(missionJSON))
err = json.Unmarshal([]byte(fixedString), &mi)
if err != nil { if err != nil {
logger.Log.Error().Err(err).Msgf(`Error when unmarshalling mission`) thisLogger.Error().Err(err).Msgf(`Error when parsing mission info`)
return return Mission{}, err
} }
// get world from WorldName parsedMap, err := a3interface.ParseSQFHashMap(parsedInterface)
var dbWorld World if err != nil {
db.Client().Where("world_name = ?", mi.WorldName).First(&dbWorld) thisLogger.Error().Err(err).Msgf(`Error when parsing mission info`)
if dbWorld.ID == 0 { return Mission{}, err
logger.Log.Error().Msgf(`World %s not found.`, mi.WorldName)
return
} }
mi.WorldID = dbWorld.ID thisLogger.Trace().Msgf(`parsedMap: %+v`, parsedMap)
var mi Mission
// create mission object from map[string]interface{}
missionBytes, err := json.Marshal(parsedMap)
if err != nil {
thisLogger.Error().Err(err).Msgf(`Error when marshalling mission info`)
return Mission{}, err
}
err = json.Unmarshal(missionBytes, &mi)
if err != nil {
thisLogger.Error().Err(err).Msgf(`Error when unmarshalling mission info`)
return Mission{}, err
}
if loadedWorld == nil {
thisLogger.Error().Msgf(`Current world ID not set, cannot create mission`)
return Mission{}, err
}
if loadedWorld.ID == 0 {
thisLogger.Error().Msgf(`Current world ID is 0, cannot create mission`)
return Mission{}, err
}
mi.WorldID = loadedWorld.ID
// write mission to database // write mission to database
db.Client().Create(&mi) db.Client().Create(&mi)
if db.Client().Error != nil { if db.Client().Error != nil {
logger.Log.Error().Err(db.Client().Error).Msgf(`Error when creating mission`) thisLogger.Error().Err(db.Client().Error).Msgf(`Error when creating mission`)
return return Mission{}, db.Client().Error
} }
logger.Log.Info().Msgf(`Mission %s created with ID %d`, mi.MissionName, mi.ID) thisLogger.Info().Msgf(`Mission %s created with ID %d`, mi.MissionName, mi.ID)
currentMissionID = mi.ID
a3interface.WriteArmaCallback(
EXTENSION_NAME,
":LOG:MISSION:SUCCESS:",
"World and mission logged successfully.",
)
return mi, nil
} }
func writeAttendance(data string) { func writeAttendance(data string, thisLogger zerolog.Logger) {
var err error var err error
// data is json, parse it
stringjson := unescapeArmaQuotes(data) parsedInterface, err := a3interface.ParseSQF(data)
var event Session
err = json.Unmarshal([]byte(stringjson), &event)
if err != nil { if err != nil {
logger.Log.Error().Err(err).Msgf(`Error when unmarshalling attendance`) thisLogger.Error().Err(err).Str("data", data).Msgf(`Error when parsing attendance info`)
return return
} }
parsedMap, err := a3interface.ParseSQFHashMap(parsedInterface)
if err != nil {
thisLogger.Error().Err(err).Str("data", data).Msgf(`Error when parsing attendance info`)
return
}
thisLogger.Trace().Msgf(`parsedMap: %+v`, parsedMap)
var thisSession Session
// create session object from map[string]interface{}
sessionBytes, err := json.Marshal(parsedMap)
if err != nil {
thisLogger.Error().Err(err).Str("data", data).Msgf(`Error when marshalling attendance info`)
return
}
err = json.Unmarshal(sessionBytes, &thisSession)
if err != nil {
thisLogger.Error().Err(err).Str("data", data).Msgf(`Error when unmarshalling attendance info`)
return
}
thisLogger2 := thisLogger.With().
Str("playerId", thisSession.PlayerId).
Str("playerUID", thisSession.PlayerUID).
Str("profileName", thisSession.ProfileName).
Logger()
// search existing event // search existing event
var dbEvent Session var dbEvent Session
db.Client(). db.Client().
Where( Where(
"player_uid = ? AND mission_hash = ?", "player_id = ? AND mission_hash = ?",
event.PlayerUID, thisSession.PlayerId,
event.MissionHash, thisSession.MissionHash,
). ).
Order("join_time_utc desc"). Order("join_time_utc desc").
First(&dbEvent) First(&dbEvent)
if dbEvent.ID != 0 {
if dbEvent.ID > 0 {
// update disconnect time // update disconnect time
dbEvent.DisconnectTimeUTC = sql.NullTime{ dbEvent.DisconnectTimeUTC = sql.NullTime{
Time: time.Now(), Time: time.Now(),
@@ -327,34 +438,32 @@ func writeAttendance(data string) {
} }
err = db.Client().Save(&dbEvent).Error err = db.Client().Save(&dbEvent).Error
if err != nil { if err != nil {
logger.Log.Error().Err(err). thisLogger2.Error().Err(err).
Msgf(`Error when updating disconnect time for event %d`, dbEvent.ID) Msgf(`Error when updating disconnect time for event %d`, dbEvent.ID)
return return
} }
logger.Log.Debug().Msgf(`Attendance updated for %s (%s)`, thisLogger2.Debug().Msgf(`Attendance updated with ID %d`,
dbEvent.ProfileName, dbEvent.ID,
dbEvent.PlayerUID,
) )
} else { } else {
// insert new row // insert new row
event.JoinTimeUTC = sql.NullTime{ thisSession.JoinTimeUTC = sql.NullTime{
Time: time.Now(), Time: time.Now(),
Valid: true, Valid: true,
} }
if currentMissionID == 0 { if loadedMission == nil {
logger.Log.Error().Msgf(`Current mission ID not set, cannot create attendance event`) thisLogger2.Error().Msgf(`Current mission ID not set, cannot create attendance event`)
return return
} }
event.MissionID = currentMissionID thisSession.MissionID = loadedMission.ID
err = db.Client().Create(&event).Error err = db.Client().Create(&thisSession).Error
if err != nil { if err != nil {
logger.Log.Error().Err(err).Msgf(`Error when creating attendance event`) thisLogger2.Error().Err(err).Msgf(`Error when creating attendance event`)
return return
} }
logger.Log.Debug().Msgf(`Attendance created for %s (%s)`, thisLogger2.Info().Msgf(`Attendance created with ID %d`,
event.ProfileName, thisSession.ID,
event.PlayerUID,
) )
} }
} }
@@ -365,20 +474,6 @@ func getTimestamp() string {
return time.Now().Format("2006-01-02 15:04:05") return time.Now().Format("2006-01-02 15:04:05")
} }
func trimQuotes(s string) string {
// trim the start and end quotes from a string
return strings.Trim(s, `"`)
}
func fixEscapeQuotes(s string) string {
// fix the escape quotes in a string
return strings.Replace(s, `""`, `"`, -1)
}
func unescapeArmaQuotes(s string) string {
return fixEscapeQuotes(trimQuotes(s))
}
func main() { func main() {
// loadConfig() // loadConfig()
// fmt.Println("Running DB connect/migrate to build schema...") // fmt.Println("Running DB connect/migrate to build schema...")

View File

@@ -4,12 +4,12 @@ go 1.20
require ( require (
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/indig0fox/a3go v0.2.0 github.com/indig0fox/a3go v0.3.2
github.com/rs/zerolog v1.30.0 github.com/rs/zerolog v1.31.0
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.17.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.5.1 gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.4 gorm.io/gorm v1.25.5
) )
require ( require (
@@ -22,12 +22,18 @@ require (
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
golang.org/x/sys v0.12.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@@ -127,8 +127,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/indig0fox/a3go v0.2.0 h1:2r1fyUePCH9KLMBzVXBigkQT2zS39mpHZAm5egmIrNk= github.com/indig0fox/a3go v0.3.2 h1:bNL90pffeOnS6Qtjoo5JHpdpZn1f0BZmRZR8nz/xcvQ=
github.com/indig0fox/a3go v0.2.0/go.mod h1:8htVwBiIAVKpT1Jyb+5dm7GuLAAevTXgw7UKxSlOawY= github.com/indig0fox/a3go v0.3.2/go.mod h1:8htVwBiIAVKpT1Jyb+5dm7GuLAAevTXgw7UKxSlOawY=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -165,8 +165,18 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@@ -175,10 +185,13 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -198,6 +211,14 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -215,6 +236,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -331,6 +356,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -498,9 +525,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -52,9 +52,9 @@ func InitLoggers(o *LoggerOptionsType) {
ll = &lumberjack.Logger{ ll = &lumberjack.Logger{
Filename: ActiveOptions.Path, Filename: ActiveOptions.Path,
MaxSize: 5, MaxSize: 5,
MaxBackups: 10, MaxBackups: 8,
MaxAge: 14, MaxAge: 14,
Compress: true, Compress: false,
LocalTime: true, LocalTime: true,
} }
@@ -66,7 +66,7 @@ func InitLoggers(o *LoggerOptionsType) {
armaLogFormatLevel := func(i interface{}) string { armaLogFormatLevel := func(i interface{}) string {
return strings.ToUpper( return strings.ToUpper(
fmt.Sprintf( fmt.Sprintf(
"(%s)", "%s:",
i, i,
)) ))
} }
@@ -117,13 +117,17 @@ func InitLoggers(o *LoggerOptionsType) {
NoColor: true, NoColor: true,
FormatTimestamp: armaLogFormatTimestamp, FormatTimestamp: armaLogFormatTimestamp,
FormatLevel: armaLogFormatLevel, FormatLevel: armaLogFormatLevel,
FieldsExclude: []string{zerolog.CallerFieldName, "ctx"},
}, },
)).With().Timestamp().Logger() )).With().Timestamp().Caller().Logger()
if ActiveOptions.Debug { if ActiveOptions.Debug {
Log = Log.Level(zerolog.DebugLevel) Log = Log.Level(zerolog.DebugLevel)
} else { } else {
Log = Log.Level(zerolog.InfoLevel) Log = Log.Level(zerolog.InfoLevel)
} }
if ActiveOptions.Trace {
Log = Log.Level(zerolog.TraceLevel)
}
} }

3
go.work Normal file
View File

@@ -0,0 +1,3 @@
go 1.20
use ./extension/AttendanceTracker

View File

@@ -0,0 +1,15 @@
version: '3.1'
services:
db:
image: mariadb
restart: always
environment:
- "MARIADB_ROOT_PASSWORD=example"
- "MARIADB_DATABASE=a3attendance"
volumes:
- ./db:/var/lib/mysql
ports:
- "3306:3306"