diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp b/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp index 3b93ee5..b191b30 100644 --- a/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp +++ b/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp @@ -14,6 +14,7 @@ class CfgFunctions { class functions { file = "\AttendanceTracker\functions"; class postInit {postInit = 1;}; + class connectDB {}; class eventHandlers {}; class callbackHandler {postInit = 1;}; class log {}; diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf index bc8321c..b5a0223 100644 --- a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf @@ -39,6 +39,21 @@ addMissionEventHandler ["ExtensionCallback", { case "connectDB": { systemChat format ["AttendanceTracker: %1", _response#0]; [_response#0, _response#1, _function] call attendanceTracker_fnc_log; + 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; + + + // log world info + private _response = "AttendanceTracker" callExtension ["logWorld", [ + [call attendanceTracker_fnc_getWorldInfo] call CBA_fnc_encodeJSON + ]]; + }; }; default { _response call attendanceTracker_fnc_log; diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_connectDB.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_connectDB.sqf new file mode 100644 index 0000000..bfcbc52 --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_connectDB.sqf @@ -0,0 +1,3 @@ +private _database = "AttendanceTracker" callExtension "connectDB"; +// systemChat "AttendanceTracker: Connecting to database..."; +["Connecting to database...", "INFO"] call attendanceTracker_fnc_log; diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_getWorldInfo.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_getWorldInfo.sqf new file mode 100644 index 0000000..75bc7d7 --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_getWorldInfo.sqf @@ -0,0 +1,23 @@ +_world = ( configfile >> "CfgWorlds" >> worldName ); +_author = getText( _world >> "author" ); +_name = getText ( _world >> "description" ); + +_source = configSourceMod ( _world ); + +_workshopID = ''; + +{ + if ( ( _x#1 ) == _source ) then { + _workshopID = _x#7; + break; + }; +} foreach getLoadedModsInfo; + +// [_name, _author, _workshopID]; + +[ + ["worldName", _name], + ["author", _author], + ["worldSize", worldSize], + ["workshopID", _workshopID] +]; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf index 7507387..be3c8ce 100644 --- a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf @@ -16,6 +16,7 @@ _hash set ["profileName", _profileName]; _hash set ["steamName", _steamName]; _hash set ["isJIP", _isJIP]; _hash set ["roleDescription", _roleDescription]; +_hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]]; "AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]]; diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf index b51679e..613ecce 100644 --- a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf @@ -15,6 +15,7 @@ _hash set ["profileName", _profileName]; _hash set ["steamName", _steamName]; _hash set ["isJIP", false]; _hash set ["roleDescription", ""]; +_hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]]; "AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]]; diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf index e088e9e..f9fe4bd 100644 --- a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf @@ -1,6 +1,9 @@ AttendanceTracker = false call CBA_fnc_createNamespace; +AttendanceTracker_missionStartTimestamp = call attendanceTracker_fnc_timestamp; +AttendanceTracker_missionHash = "AttendanceTracker" callExtension ["getMissionHash", AttendanceTracker_missionStartTimestamp]; + AttendanceTracker setVariable ["missionContext", createHashMapFromArray [ ["missionName", missionName], ["briefingName", briefingName], @@ -9,16 +12,17 @@ AttendanceTracker setVariable ["missionContext", createHashMapFromArray [ ["author", getMissionConfigValue ["author", ""]], ["serverName", serverName], ["serverProfile", profileName], - ["missionStart", call attendanceTracker_fnc_timestamp] + ["missionStart", AttendanceTracker_missionStartTimestamp], + ["missionHash", AttendanceTracker_missionHash] ]]; + + // store all user details in a hash when they connect so we can reference it in disconnect events AttendanceTracker setVariable ["allUsers", createHashMap]; -missionNamespace getVariable ["AttendanceTracker_debug", false]; +missionNamespace setVariable ["AttendanceTracker_debug", false]; -private _database = "AttendanceTracker" callExtension "connectDB"; -// systemChat "AttendanceTracker: Connecting to database..."; -["Connecting to database...", "INFO"] call attendanceTracker_fnc_log; +call attendanceTracker_fnc_connectDB; { if (!isServer) exitWith {}; diff --git a/extension/AttendanceTracker_x64.dll b/extension/AttendanceTracker_x64.dll index 483f045..f84d35e 100644 Binary files a/extension/AttendanceTracker_x64.dll and b/extension/AttendanceTracker_x64.dll differ diff --git a/extension/main.go b/extension/main.go index 91b67c7..f31640e 100644 --- a/extension/main.go +++ b/extension/main.go @@ -10,6 +10,7 @@ import "C" // This is required to import the C code import ( "context" + "crypto/md5" "database/sql" "encoding/json" "fmt" @@ -103,6 +104,18 @@ func loadConfig() { writeLog(functionName, `["Config loaded", "INFO"]`) } +func getMissionHash(time string) 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)) + + // convert to string + hashString := fmt.Sprintf("%x", hash) + writeLog(functionName, fmt.Sprintf(`["Mission hash: %s", "INFO"]`, hashString)) + return hashString +} + func connectDB() string { functionName := "connectDB" var err error @@ -142,10 +155,80 @@ func connectDB() string { return "ERROR" } writeLog(functionName, fmt.Sprintf(`["Connected to MySQL/MariaDB version %s", "INFO"]`, version)) + writeLog(functionName, `["SUCCESS", "INFO"]`) return version } -type AttendanceLogItem struct { +type WorldInfo struct { + WorldName string `json:"worldName"` + Author string `json:"author"` + WorldSize int `json:"worldSize"` + WorkshopID string `json:"workshopID"` +} + +func writeWorldInfo(worldInfo string) { + functionName := "writeWorldInfo" + // worldInfo is json, parse it + var wi WorldInfo + err := json.Unmarshal([]byte(worldInfo), &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 database + // check if world exists + var worldID int + err = db.QueryRow("SELECT id FROM worlds WHERE workshop_id = ?", wi.WorkshopID).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 (?, ?, ?, ?)") + 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) + 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(`["World inserted with ID %d", "INFO"]`, lastID)) + } else { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + } else { + // world exists, update it + stmt, err := db.Prepare("UPDATE worlds SET world_name = ?, author = ?, world_size = ? WHERE id = ?") + 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) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + rowsAffected, err := res.RowsAffected() + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + writeLog(functionName, fmt.Sprintf(`["World updated with %d rows affected", "INFO"]`, rowsAffected)) + } +} + +type MissionInfo struct { MissionName string `json:"missionName"` BriefingName string `json:"briefingName"` MissionNameSource string `json:"missionNameSource"` @@ -154,7 +237,52 @@ type AttendanceLogItem struct { ServerName string `json:"serverName"` ServerProfile string `json:"serverProfile"` MissionStart string `json:"missionStart"` - // situational + MissionHash string `json:"missionHash"` +} + +func writeMissionInfo(missionInfo string) { + functionName := "writeMissionInfo" + // missionInfo is json, parse it + var mi MissionInfo + err := json.Unmarshal([]byte(missionInfo), &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) + 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) + 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)) +} + +type AttendanceLogItem struct { EventType string `json:"eventType"` PlayerId string `json:"playerId"` PlayerUID string `json:"playerUID"` @@ -162,6 +290,7 @@ type AttendanceLogItem struct { SteamName string `json:"steamName"` IsJIP bool `json:"isJIP"` RoleDescription string `json:"roleDescription"` + MissionHash string `json:"missionHash"` } func writeAttendance(data string) { @@ -175,14 +304,6 @@ func writeAttendance(data string) { return } - // get MySQL friendly datetime - // first, convert string to int - missionStartTime, err := strconv.ParseInt(event.MissionStart, 10, 64) - if err != nil { - writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) - return - } - t := time.Unix(0, missionStartTime).Format("2006-01-02 15:04:05") // get MySQL friendly NOW now := time.Now().Format("2006-01-02 15:04:05") @@ -194,16 +315,8 @@ func writeAttendance(data string) { // send to DB - result, err := db.ExecContext(context.Background(), `INSERT INTO AttendanceLog (timestamp, mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + 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.MissionName, - event.BriefingName, - event.MissionNameSource, - event.OnLoadName, - event.Author, - event.ServerName, - event.ServerProfile, - t, event.EventType, event.PlayerId, event.PlayerUID, @@ -211,6 +324,7 @@ func writeAttendance(data string) { event.SteamName, event.IsJIP, event.RoleDescription, + event.MissionHash, ) if err != nil { @@ -262,6 +376,14 @@ func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv go writeAttendance(out[0]) } } + case "logMission": + if argc == 1 { + go writeMissionInfo(out[0]) + } + case "logWorld": + if argc == 1 { + go writeWorldInfo(out[0]) + } } // Return a result to Arma