Client LuaCsForBarotrauma
GameAnalyticsConsent.cs
1 #nullable enable
2 using Barotrauma.Steam;
3 using RestSharp;
4 using System;
5 using System.Linq;
6 using System.Net;
7 using System.Threading.Tasks;
8 
9 namespace Barotrauma
10 {
11  static partial class GameAnalyticsManager
12  {
17  private const string RemoteRequestVersion = "3";
18 
19  public enum Consent
20  {
24  Unknown,
25 
29  Error,
30 
34  Ask,
35 
39  No,
40 
44  Yes
45  }
46 
47  public static Consent UserConsented { get; private set; } = Consent.Unknown;
48 
49  public static bool SendUserStatistics => UserConsented == Consent.Yes && loadedImplementation != null;
50 
51  private static bool ConsentTextAvailable
52  => TextManager.ContainsTag("statisticsconsentheader")
53  && TextManager.ContainsTag("statisticsconsenttext");
54 
55  private const string consentServerUrl = "https://barotraumagame.com/baromaster/";
56  private const string consentServerFile = "consentserver.php";
57 
58  enum Platform
59  {
60  Steam,
61  EOS,
62  None
63  }
64 
65  private class AuthTicket
66  {
67  public readonly string Token;
68  public readonly Platform Platform;
69 
70  public AuthTicket(string token, Platform platform)
71  {
72  Token = token ?? string.Empty;
73  Platform = platform;
74  }
75  }
76 
77  private static async Task<AuthTicket> GetAuthTicket()
78  {
79  if (SteamManager.IsInitialized)
80  {
81  return await GetSteamAuthTicket();
82  }
83  else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
84  {
85  return await GetEOSAuthTicket();
86  }
87  return new AuthTicket(string.Empty, Platform.None);
88  }
89 
90  private static async Task<AuthTicket> GetSteamAuthTicket()
91  {
92  var authTicket = await SteamManager.GetAuthTicketForGameAnalyticsConsent();
93  return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 }
94  ? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex
95  : throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics");
96  }
97 
98  private static async Task<AuthTicket> GetEOSAuthTicket()
99  {
100  var puid = EosInterface.IdQueries.GetLoggedInPuids().First();
101  var tokenResult = EosInterface.EosIdToken.FromProductUserId(puid);
102  if (tokenResult.TryUnwrapFailure(out var error))
103  {
104  throw new Exception($"Could not retrieve EOS authentication ticket for GameAnalytics. {error}");
105  }
106  else if (tokenResult.TryUnwrapSuccess(out var token))
107  {
108  return new AuthTicket(token.JsonWebToken.ToString(), Platform.EOS);
109  }
110  throw new UnreachableCodeException();
111  }
112 
119  public static void SetConsent(Consent consent, Action? onAnswerSent = null)
120  {
121  if (consent == Consent.Yes)
122  {
123  throw new Exception(
124  "Cannot call SetConsent with value Consent.Yes, must only be set to this value via consent prompt");
125  }
126  SetConsentInternal(consent, onAnswerSent);
127  }
128 
133  private static void SetConsentInternal(Consent consent, Action? onAnswerSent)
134  {
135  if (UserConsented == consent)
136  {
137  onAnswerSent?.Invoke();
138  return;
139  }
140 
141  if (consent == Consent.Ask)
142  {
143 #if CLIENT
144  GameMain.ExecuteAfterContentFinishedLoading(CreateConsentPrompt);
145 #endif
146  }
147 
148  if (consent != Consent.No && consent != Consent.Yes)
149  {
150  UserConsented = consent;
151  ShutDown();
152  return;
153  }
154  if (consent == Consent.No)
155  {
156  UserConsented = consent;
157  ShutDown();
158  }
159 
160  TaskPool.Add(
161  "GameAnalyticsConsent.SendAnswerToRemoteDatabase",
162  SendAnswerToRemoteDatabase(consent),
163  t =>
164  {
165  onAnswerSent?.Invoke();
166  if (!t.TryGetResult(out bool success) || !success) { return; }
167 
168  UserConsented = consent;
169  if (consent == Consent.Yes)
170  {
171  Init();
172  }
173  });
174  }
175 
180  private static async Task<bool> SendAnswerToRemoteDatabase(Consent consent)
181  {
182  AuthTicket authTicket;
183  try
184  {
185  authTicket = await GetAuthTicket();
186  }
187  catch (Exception e)
188  {
189  DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. Could not get an authentication ticket.", e);
190  return false;
191  }
192  if (authTicket.Platform == Platform.None)
193  {
194  DebugConsole.AddWarning($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. Not logged in to any platform.");
195  return false;
196  }
197  if (string.IsNullOrEmpty(authTicket.Token))
198  {
199  DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. {authTicket.Platform} authentication ticket was empty.");
200  return false;
201  }
202 
203  IRestResponse response;
204  try
205  {
206  var client = new RestClient(consentServerUrl);
207 
208  var request = new RestRequest(consentServerFile, Method.GET);
209  request.AddParameter("authticket", authTicket.Token);
210  if (consent == Consent.Ask)
211  {
212  request.AddParameter("action", "resetconsent");
213  }
214  else
215  {
216  request.AddParameter("action", "setconsent");
217  request.AddParameter("consent", consent == Consent.Yes ? 1 : 0);
218  }
219  request.AddParameter("request_version", RemoteRequestVersion);
220  request.AddParameter("platform", authTicket.Platform);
221 
222  response = await client.ExecuteAsync(request, Method.GET);
223  }
224  catch (Exception e)
225  {
226  DebugConsole.ThrowError("Error while connecting to consent server", e);
227  return false;
228  }
229 
230  if (!CheckResponse(response)) { return false; }
231 
232  if (!string.IsNullOrWhiteSpace(response.Content))
233  {
234  DebugConsole.ThrowError($"Error in GameAnalyticsManager.SetContent. Consent server reported an error: {response.Content.Trim()}");
235  return false;
236  }
237  return true;
238  }
239 
240  public static void ResetConsent()
241  {
242  TaskPool.Add(
243  "GameAnalyticsConsent.ResetConsentInternal",
244  SendAnswerToRemoteDatabase(Consent.Ask),
245  t =>
246  {
247  if (!t.TryGetResult(out bool success) || !success) { return; }
248  DebugConsole.NewMessage("Reset GameAnalytics consent.");
249  });
250  }
251 
252  static partial void CreateConsentPrompt();
253 
254  public static void InitIfConsented()
255  {
256  #if DEBUG
257  return;
258  #endif
259 
260  if (!ConsentTextAvailable)
261  {
262  SetConsent(Consent.Unknown);
263  return;
264  }
265 
266  if (!SteamManager.IsInitialized && EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 })
267  {
268  DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam or EOS authentication ticket (not connected to Steam or EOS).");
269  SetConsent(Consent.Error);
270  return;
271  }
272 
273  TaskPool.Add(
274  "GameAnalyticsConsent.RequestAnswerFromRemoteDatabase",
275  RequestAnswerFromRemoteDatabase(),
276  t =>
277  {
278  if (!t.TryGetResult(out Consent consent)) { return; }
279  SetConsentInternal(consent, onAnswerSent: null);
280  });
281  }
282 
283  private static async Task<Consent> RequestAnswerFromRemoteDatabase()
284  {
285  static void error(string reason, Exception? exception)
286  {
287  DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(RequestAnswerFromRemoteDatabase)}: {reason}", exception);
288  SetConsent(Consent.Error);
289  }
290 
291  AuthTicket authTicket;
292  try
293  {
294  authTicket = await GetAuthTicket();
295  }
296  catch (Exception e)
297  {
298  error("Could not get an authentication ticket.", e);
299  return Consent.Error;
300  }
301  if (authTicket.Platform == Platform.None)
302  {
303  error($"Could not get an authentication ticket. Not logged in to any platform.", exception: null);
304  return Consent.Error;
305  }
306 
307  RestClient client;
308  try
309  {
310  client = new RestClient(consentServerUrl);
311  }
312  catch (Exception e)
313  {
314  error("Error while connecting to consent server.", e);
315  return Consent.Error;
316  }
317 
318  var request = new RestRequest(consentServerFile, Method.GET);
319  request.AddParameter("authticket", authTicket.Token);
320  request.AddParameter("action", "getconsent");
321  request.AddParameter("request_version", RemoteRequestVersion);
322  request.AddParameter("platform", authTicket.Platform);
323 
324  IRestResponse response;
325  try
326  {
327  response = await client.ExecuteAsync(request);
328  }
329  catch (Exception e)
330  {
331  error("Error executing the request to the consent server.", e.GetInnermost());
332  return Consent.Error;
333  }
334 
335  if (!CheckResponse(response))
336  {
337  return Consent.Error;
338  }
339  if (string.IsNullOrEmpty(response.Content))
340  {
341  return Consent.Ask;
342  }
343  return response.Content[0] == '1'
344  ? Consent.Yes
345  : Consent.No;
346  }
347 
348  private static bool CheckResponse(IRestResponse response)
349  {
350  if (response.ErrorException != null)
351  {
352  DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString()));
353  return false;
354  }
355 
356  if (response.StatusCode == HttpStatusCode.OK) { return true; }
357 
358  switch (response.StatusCode)
359  {
360  case HttpStatusCode.NotFound:
361  DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl));
362  break;
363  case HttpStatusCode.ServiceUnavailable:
364  DebugConsole.ThrowErrorLocalized(TextManager.Get("MasterServerErrorUnavailable"));
365  break;
366  default:
367  DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariables("MasterServerErrorDefault",
368  ("[statuscode]", response.StatusCode.ToString()),
369  ("[statusdescription]", response.StatusDescription)));
370  break;
371  }
372  return false;
373  }
374  }
375 }