From 52f15961fec4e2cfbc56d4a53804276618b759ec Mon Sep 17 00:00:00 2001
From: Mondus <p.richmond@sheffield.ac.uk>
Date: Wed, 20 Nov 2024 12:07:42 +0000
Subject: [PATCH 1/9] Added User ID to telemetry

---
 README.md                       |  2 +-
 include/flamegpu/io/Telemetry.h | 18 +++++++
 src/flamegpu/io/Telemetry.cpp   | 96 ++++++++++++++++++++++++++++++++-
 3 files changed, 114 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index a25f4959d..d4edfd658 100644
--- a/README.md
+++ b/README.md
@@ -328,7 +328,7 @@ Information is collected when a simulation, ensemble or test suite run have comp
 The [TelemetryDeck](https://telemetrydeck.com/) service is used to store telemetry data. 
 All data is sent to their API endpoint of https://nom.telemetrydeck.com/v1/ via https. For more details please review the [TelmetryDeck privacy policy](https://telemetrydeck.com/privacy/).
 
-We do not collect any personal data such as usernames, email addresses or machine identifiers.
+We do not collect any personal data such as usernames, email addresses or hardware identifiers but we do generate a random user identifier. This identifier is salted and hashed by Telemetry deck.
 
 More information can be found in the [FLAMEGPU documentation](https://docs.flamegpu.com/guide/telemetry).
 
diff --git a/include/flamegpu/io/Telemetry.h b/include/flamegpu/io/Telemetry.h
index 1b6c87481..a99e39147 100644
--- a/include/flamegpu/io/Telemetry.h
+++ b/include/flamegpu/io/Telemetry.h
@@ -84,6 +84,24 @@ static void encourageUsage();
  * @return if telemetry is currently in test mode or not.
  */
 static bool isTestMode();
+
+/**
+ * Gets a cross platform location for storing user configuration data. Required to store a per user Id. On windows this uses the AppData folder (CSIDL_APPDATA) and in Linux this uses XDG_CONFIG_HOME.
+ * @return Root directory for storing configuration files
+ */
+static std::string getConfigDirectory();
+
+/**
+ * Generates a randomised 36 character alphanumeric string for use as a User Id. 
+ * @return A 36 character randomised alphanumeric string
+ */
+static std::string generateRandomId();
+
+/**
+ * Obtains a unique user Id. If a configuration file (flamegpu_user.cfg) exists this will be loaded from disk otherwise it will be generated and stored in the configuration location. If the configuration location is not writeable a new user Id will be generated each time. The user Id will be further obfuscated by Telemetry Deck which will salt and hash the Id.
+ * @return A 36 character randomised alphanumeric string representing a unique user Id
+ */
+static std::string getUserId();
 };
 }  // namespace io
 }  // namespace flamegpu
diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index e298cb136..a37df1a18 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -6,11 +6,22 @@
 #include <memory>
 #include <array>
 #include <iostream>
+#include <fstream>
+#include <filesystem>
 #include <cstdio>
 #include <cctype>
 #include <sstream>
 #include <string>
 #include <map>
+#include <random>
+#ifdef _WIN32
+#include <windows.h>
+#include <shlobj.h>
+#else
+#include <cstdlib>
+#include <sys/types.h>
+#include <pwd.h>
+#endif
 
 #include "flamegpu/version.h"
 
@@ -159,7 +170,7 @@ std::string Telemetry::generateData(std::string event_name, std::map<std::string
     // check ENV for test variable FLAMEGPU_TEST_ENVIRONMENT
     std::string testmode = isTestMode() ? "true" : "false";
     std::string appID = TELEMETRY_APP_ID;
-    std::string telemetryRandomID = flamegpu::TELEMETRY_RANDOM_ID;
+    std::string telemetryRandomID = Telemetry::getUserId(); // Obtain a unique user ID
 
     // Differentiate pyflamegpu in the payload via the SWIG compiler macro, which we only define when building for pyflamegpu.
     // A user could potentially static link against a build using that macro, but that's not a use-case we are currently concerned with.
@@ -178,6 +189,7 @@ std::string Telemetry::generateData(std::string event_name, std::map<std::string
     payload_items["appVersionPatch"] = std::to_string(flamegpu::VERSION_PATCH);
     payload_items["appVersionPreRelease"] = flamegpu::VERSION_PRERELEASE;
     payload_items["buildNumber"] = flamegpu::VERSION_BUILDMETADATA;  // e.g. '0553592f' (graphed in Telemetry deck)
+    payload_items["buildID"] = flamegpu::TELEMETRY_RANDOM_ID;  // e.g. 'e7e0fe30325c83a3ad52e2cb2180e50979d3e6bcb0692789977a06ad09889843' (ID generated a cmake time)
 
     // OS
 #ifdef _WIN32
@@ -291,5 +303,87 @@ void Telemetry::encourageUsage() {
     }
 }
 
+std::string Telemetry::getConfigDirectory() {
+#ifdef _WIN32
+    char path[MAX_PATH];
+    if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
+        return std::string(path);
+    }
+    throw std::runtime_error("Unable to retrieve config directory on Windows");
+#else
+    const char* configHome = std::getenv("XDG_CONFIG_HOME");
+    if (configHome) {
+        return std::string(configHome);
+    }
+    const char* home = std::getenv("HOME");
+    if (home) {
+        return std::string(home) + "/.config";
+    }
+    else {
+        struct passwd* pwd = getpwuid(getuid());
+        if (pwd) {
+            return std::string(pwd->pw_dir) + "/.config";
+        }
+    }
+    throw std::runtime_error("Unable to retrieve config directory on Linux");
+#endif
+}
+
+std::string Telemetry::generateRandomId() {
+    const char charset[] =
+        "0123456789"
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        "abcdefghijklmnopqrstuvwxyz";
+    const size_t length = 36;
+    std::random_device rd;
+    std::mt19937 generator(rd());
+    std::uniform_int_distribution<size_t> distribution(0, sizeof(charset) - 2);
+
+    std::string randomId;
+    for (size_t i = 0; i < length; ++i) {
+        randomId += charset[distribution(generator)];
+    }
+    return randomId;
+}
+
+std::string Telemetry::getUserId() {
+    // Generate and a new random 36-character user ID
+    std::string userId = Telemetry::generateRandomId();
+
+    // Try to load an existing ID from file if it exists
+    try {
+        // Determine config file location
+        std::string configDir = Telemetry::getConfigDirectory() + "/FLAMEGPU";
+        std::filesystem::create_directories(configDir); // Ensure the directory exists
+        std::string filePath = configDir + "/flamegpu_user.cfg";
+
+        // Check if the file exists
+        if (std::filesystem::exists(filePath)) {
+            std::ifstream file(filePath);
+            if (file.is_open()) {
+                std::getline(file, userId); // overwrite existing Id
+                file.close();
+                return userId; // Config file and user id found so return it
+            }
+            else {
+                throw std::runtime_error("Unable to open user ID file for reading");
+            }
+        }
+
+        std::ofstream file(filePath);
+        if (file.is_open()) {
+            file << userId;
+            file.close();
+        }
+        else {
+            throw std::runtime_error("Unable to create user ID file");
+        }
+    } catch (const std::exception&) {
+        fprintf(stderr, "Warning: Telemetry User Id file is not read/writeable from config file. A new User Id will be used.");
+    }
+    return userId;
+}
+
+
 }  // namespace io
 }  // namespace flamegpu

From ab972a7b5fbe933ccb50e06bc494119bac99e814 Mon Sep 17 00:00:00 2001
From: Mondus <p.richmond@sheffield.ac.uk>
Date: Wed, 20 Nov 2024 12:29:22 +0000
Subject: [PATCH 2/9] Fixed linting

---
 src/flamegpu/io/Telemetry.cpp | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index a37df1a18..719f17eda 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -18,7 +18,6 @@
 #include <windows.h>
 #include <shlobj.h>
 #else
-#include <cstdlib>
 #include <sys/types.h>
 #include <pwd.h>
 #endif
@@ -170,7 +169,7 @@ std::string Telemetry::generateData(std::string event_name, std::map<std::string
     // check ENV for test variable FLAMEGPU_TEST_ENVIRONMENT
     std::string testmode = isTestMode() ? "true" : "false";
     std::string appID = TELEMETRY_APP_ID;
-    std::string telemetryRandomID = Telemetry::getUserId(); // Obtain a unique user ID
+    std::string telemetryRandomID = Telemetry::getUserId();  // Obtain a unique user ID
 
     // Differentiate pyflamegpu in the payload via the SWIG compiler macro, which we only define when building for pyflamegpu.
     // A user could potentially static link against a build using that macro, but that's not a use-case we are currently concerned with.
@@ -320,7 +319,7 @@ std::string Telemetry::getConfigDirectory() {
         return std::string(home) + "/.config";
     }
     else {
-        struct passwd* pwd = getpwuid(getuid());
+        struct passwd* pwd = getpwuid_r(getuid());
         if (pwd) {
             return std::string(pwd->pw_dir) + "/.config";
         }
@@ -354,16 +353,16 @@ std::string Telemetry::getUserId() {
     try {
         // Determine config file location
         std::string configDir = Telemetry::getConfigDirectory() + "/FLAMEGPU";
-        std::filesystem::create_directories(configDir); // Ensure the directory exists
+        std::filesystem::create_directories(configDir);  // Ensure the directory exists
         std::string filePath = configDir + "/flamegpu_user.cfg";
 
         // Check if the file exists
         if (std::filesystem::exists(filePath)) {
             std::ifstream file(filePath);
             if (file.is_open()) {
-                std::getline(file, userId); // overwrite existing Id
+                std::getline(file, userId);  // overwrite existing Id
                 file.close();
-                return userId; // Config file and user id found so return it
+                return userId;  // Config file and user id found so return it
             }
             else {
                 throw std::runtime_error("Unable to open user ID file for reading");

From 8ea5fa951873fed57ec7eeaf432d5b882cc95e66 Mon Sep 17 00:00:00 2001
From: Mondus <p.richmond@sheffield.ac.uk>
Date: Wed, 20 Nov 2024 12:43:22 +0000
Subject: [PATCH 3/9] fixing unix errors

---
 src/flamegpu/io/Telemetry.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index 719f17eda..a72b8e59b 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -19,6 +19,7 @@
 #include <shlobj.h>
 #else
 #include <sys/types.h>
+#include <unistd.h>
 #include <pwd.h>
 #endif
 

From 85b59cb3f1060d8d5063410e68508b09c0e30ffd Mon Sep 17 00:00:00 2001
From: Mondus <p.richmond@sheffield.ac.uk>
Date: Wed, 20 Nov 2024 12:54:28 +0000
Subject: [PATCH 4/9] more linux faff

---
 src/flamegpu/io/Telemetry.cpp | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index a72b8e59b..cbdb19aaf 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -320,8 +320,9 @@ std::string Telemetry::getConfigDirectory() {
         return std::string(home) + "/.config";
     }
     else {
-        struct passwd* pwd = getpwuid_r(getuid());
-        if (pwd) {
+        struct passwd* pwd;
+        int success = getpwuid_r(getuid(), pwd);
+        if (success) {
             return std::string(pwd->pw_dir) + "/.config";
         }
     }

From 237f09863c2e0bf0ce60477f805e9877b52e1665 Mon Sep 17 00:00:00 2001
From: Mondus <p.richmond@sheffield.ac.uk>
Date: Wed, 20 Nov 2024 13:07:25 +0000
Subject: [PATCH 5/9] use of thread safe linux functions fixed

---
 src/flamegpu/io/Telemetry.cpp | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index cbdb19aaf..886cca9d9 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -319,13 +319,15 @@ std::string Telemetry::getConfigDirectory() {
     if (home) {
         return std::string(home) + "/.config";
     }
-    else {
-        struct passwd* pwd;
-        int success = getpwuid_r(getuid(), pwd);
-        if (success) {
-            return std::string(pwd->pw_dir) + "/.config";
-        }
+    // try and get the user directory if home is not set
+    struct passwd pwd;
+    struct passwd* result = nullptr;
+    char buffer[4096];
+    int ret = getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result);
+    if (ret == 0 && result != nullptr) {
+        return std::string(pwd.pw_dir) + "/.config";
     }
+
     throw std::runtime_error("Unable to retrieve config directory on Linux");
 #endif
 }

From 3f93cb1e6eb3a844cce1af9c948383bbfc263682 Mon Sep 17 00:00:00 2001
From: Mondus <p.richmond@sheffield.ac.uk>
Date: Wed, 20 Nov 2024 14:12:26 +0000
Subject: [PATCH 6/9] fixing review points

---
 src/flamegpu/io/Telemetry.cpp | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index 886cca9d9..e28503b34 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -364,9 +364,14 @@ std::string Telemetry::getUserId() {
         if (std::filesystem::exists(filePath)) {
             std::ifstream file(filePath);
             if (file.is_open()) {
-                std::getline(file, userId);  // overwrite existing Id
+                std::string cached_id;
+                std::getline(file, cached_id);  // overwrite existing Id
                 file.close();
-                return userId;  // Config file and user id found so return it
+                // Config file and user id found so return it if not empty (either because file is externally modified or file is a directory)
+                if (!cached_id.empty())
+                    return cached_id;
+                else
+                    return userId;
             }
             else {
                 throw std::runtime_error("Unable to open user ID file for reading");
@@ -382,7 +387,7 @@ std::string Telemetry::getUserId() {
             throw std::runtime_error("Unable to create user ID file");
         }
     } catch (const std::exception&) {
-        fprintf(stderr, "Warning: Telemetry User Id file is not read/writeable from config file. A new User Id will be used.");
+        fprintf(stderr, "Warning: Telemetry User Id file is not read/writeable from config file. A new User Id will be used.\n");
     }
     return userId;
 }

From 7916a9c84685cc8c11fe249df4d509bca694b59b Mon Sep 17 00:00:00 2001
From: Peter Heywood <peethwd@gmail.com>
Date: Thu, 21 Nov 2024 13:30:39 +0000
Subject: [PATCH 7/9] Telemetry: Change telemetry config dir to lower case,
 matching the jitify cache dir

---
 src/flamegpu/io/Telemetry.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index e28503b34..e9135097d 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -356,7 +356,7 @@ std::string Telemetry::getUserId() {
     // Try to load an existing ID from file if it exists
     try {
         // Determine config file location
-        std::string configDir = Telemetry::getConfigDirectory() + "/FLAMEGPU";
+        std::string configDir = Telemetry::getConfigDirectory() + "/flamegpu";
         std::filesystem::create_directories(configDir);  // Ensure the directory exists
         std::string filePath = configDir + "/flamegpu_user.cfg";
 

From bb4c106aec9575667d31425ad3be4bdb2a143f65 Mon Sep 17 00:00:00 2001
From: Peter Heywood <peethwd@gmail.com>
Date: Thu, 21 Nov 2024 13:51:34 +0000
Subject: [PATCH 8/9] Telemetry: Change telemtery user file name for clarity

---
 include/flamegpu/io/Telemetry.h | 2 +-
 src/flamegpu/io/Telemetry.cpp   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/include/flamegpu/io/Telemetry.h b/include/flamegpu/io/Telemetry.h
index a99e39147..efefadf46 100644
--- a/include/flamegpu/io/Telemetry.h
+++ b/include/flamegpu/io/Telemetry.h
@@ -98,7 +98,7 @@ static std::string getConfigDirectory();
 static std::string generateRandomId();
 
 /**
- * Obtains a unique user Id. If a configuration file (flamegpu_user.cfg) exists this will be loaded from disk otherwise it will be generated and stored in the configuration location. If the configuration location is not writeable a new user Id will be generated each time. The user Id will be further obfuscated by Telemetry Deck which will salt and hash the Id.
+ * Obtains a unique user Id. If a configuration file (i.e. ${XDG_CONFIG_HOME}/flamegpu/telemetry_user.cfg on linux) exists this will be loaded from disk otherwise it will be generated and stored in the configuration location. If the configuration location is not writeable a new user Id will be generated each time. The user Id will be further obfuscated by Telemetry Deck which will salt and hash the Id.
  * @return A 36 character randomised alphanumeric string representing a unique user Id
  */
 static std::string getUserId();
diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index e9135097d..73d48c24a 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -358,7 +358,7 @@ std::string Telemetry::getUserId() {
         // Determine config file location
         std::string configDir = Telemetry::getConfigDirectory() + "/flamegpu";
         std::filesystem::create_directories(configDir);  // Ensure the directory exists
-        std::string filePath = configDir + "/flamegpu_user.cfg";
+        std::string filePath = configDir + "/telemetry_user.cfg";
 
         // Check if the file exists
         if (std::filesystem::exists(filePath)) {

From 4516103d0e1a44b794696635eff9c54713ed91fa Mon Sep 17 00:00:00 2001
From: Peter Heywood <peethwd@gmail.com>
Date: Thu, 21 Nov 2024 13:51:56 +0000
Subject: [PATCH 9/9] Telemetry: Open telemetry cfg file in binary mode

---
 src/flamegpu/io/Telemetry.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/flamegpu/io/Telemetry.cpp b/src/flamegpu/io/Telemetry.cpp
index 73d48c24a..cc2b292ee 100644
--- a/src/flamegpu/io/Telemetry.cpp
+++ b/src/flamegpu/io/Telemetry.cpp
@@ -362,7 +362,7 @@ std::string Telemetry::getUserId() {
 
         // Check if the file exists
         if (std::filesystem::exists(filePath)) {
-            std::ifstream file(filePath);
+            std::ifstream file(filePath, std::ios_base::binary);
             if (file.is_open()) {
                 std::string cached_id;
                 std::getline(file, cached_id);  // overwrite existing Id
@@ -378,7 +378,7 @@ std::string Telemetry::getUserId() {
             }
         }
 
-        std::ofstream file(filePath);
+        std::ofstream file(filePath, std::ios_base::binary);
         if (file.is_open()) {
             file << userId;
             file.close();