Skip to content

Commit

Permalink
changed meaning of privacy_match_hist=3 from "not tracked" to "anonym…
Browse files Browse the repository at this point in the history
…ous".

unregistered player are now by default added to the database and treated as anonymous. only steam-id + current rating is stored, no match history or nick names.
- hardened account deletion to prevent deleting system-internal player IDs (<= 2)
- added function to erase/anonymize match history and aliases without deleting the account
- added UI for players who decided to have their account deleted
- added "last_used_dt" to player_nicks
- daily maintenance script to delete aliases that have not been used for 60 days
- daily maintenance script to purge hashkeys/steam-ids of deleted users after 60 days so they can re-register
- changed getOrCreatePlayer stored proc, which is now used by submision.py
  • Loading branch information
PredatH0r committed May 16, 2018
1 parent 1c12e64 commit 04faaef
Show file tree
Hide file tree
Showing 15 changed files with 240 additions and 62 deletions.
3 changes: 2 additions & 1 deletion feeder/feeder.njsproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</NodeExeArguments>
<StartWebBrowser>True</StartWebBrowser>
<ScriptArguments>-c mycfg.json</ScriptArguments>
<Environment>foo=ql-match-jsons\2016-01\06\f84a663f-f448-4920-9622-e9a9b1a6e154.json.gz
<Environment>ql-match-jsons\a6558e8a-b1d4-4c69-acac-5cf3cf13c588.json.gz
-f -u tdm
sample-data\combined_stats_duel.json
-g 19498 sample-data\combined_stats_ca.json</Environment>
Expand Down Expand Up @@ -73,6 +73,7 @@ sample-data\combined_stats_duel.json
<Content Include="sample-data\player_switchteam.json" />
<Content Include="sample-data\round_over.json" />
<Content Include="views\account.ejs" />
<Content Include="views\account_deleted.ejs" />
<Content Include="views\account_register.ejs" />
<Content Include="views\footer.ejs" />
<Content Include="views\header.ejs" />
Expand Down
3 changes: 1 addition & 2 deletions feeder/modules/gamerating.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ function loadPlayers(cli, steamIds) {
var query = "select h.hashkey, p.player_id, p.nick, p.active_ind, pe.g2_r, pe.g2_rd, pe.g2_dt, pe.g2_games, pe.b_r, pe.b_rd, pe.b_dt, pe.b_games "
+ " from hashkeys h"
+ " inner join players p on p.player_id=h.player_id"
+ " left outer join player_elos pe on pe.player_id=h.player_id and pe.game_type_cd=$1"
+ " where p.privacy_match_hist<>3";
+ " left outer join player_elos pe on pe.player_id=h.player_id and pe.game_type_cd=$1";
var params = [gametype];

if (steamIds) {
Expand Down
56 changes: 40 additions & 16 deletions feeder/modules/webui.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function loadUserSettings(req) {
return Q()
.then(function () {
var data = [req.user.id];
return Q.ninvoke(cli, "query", "select * from players where player_id=(select player_id from hashkeys where hashkey=$1)", data);
return Q.ninvoke(cli, "query", "select h.active_ind as allow_tracking, h.delete_dt, p.* from hashkeys h left outer join players p on p.player_id=h.player_id where h.hashkey=$1", data);
})
.then(function (result) {
return result.rows && result.rows.length > 0 ? result.rows[0] : {};
Expand All @@ -139,14 +139,16 @@ function loadUserSettings(req) {
}

function saveUserSettings(req, res) {

return utils.dbConnect(_config.webapi.database)
.then(function (cli) {
return Q()
.then(function () {
if (req.body.action === "register")
return registerPlayer(req, res, cli);

if (req.body.action === "anonymize")
return anonymizePlayer(req, res, cli);

if (req.body.action === "delete")
return deletePlayer(req, res, cli);

Expand All @@ -156,7 +158,7 @@ function saveUserSettings(req, res) {
set = set.substring(1);

var data = [req.user.id];
return Q.ninvoke(cli, "query", "update players set " + set + " where player_id=(select player_id from hashkeys where hashkey=$1)", data)
return Q.ninvoke(cli, "query", "update players set " + set + " where player_id=(select player_id from hashkeys where hashkey=$1 and player_id>2)", data)
.then(function (status) { return "Your settings have been saved"; });
})
.finally(function () { cli.release(); });
Expand All @@ -180,13 +182,20 @@ function registerPlayer(req, res, cli) {
function deletePlayer(req, res, cli) {
if (!req.body.confirm)
return "You didn't select the confirmation to delete your account";
return deletePlayerBySteamId(cli, req.user.id)
return deletePlayerBySteamId(cli, req.user.id, true)
.then(function () { return "Your account has been deleted"; });
}

function anonymizePlayer(req, res, cli) {
if (!req.body.confirm)
return "You didn't select the confirmation to anonymize your existing data";
return deletePlayerBySteamId(cli, req.user.id, false)
.then(function () { return "Your existing data has been anonymized"; });
}

/**
* Remove color coding from nicknames (^1Nic^7k => Nick)
* @param {any} nick
* @param {string} nick
*/
function strippedNick(nick) {
var stripped = "";
Expand All @@ -201,37 +210,52 @@ function strippedNick(nick) {
}

/**
* Deletes the player with the given steam-id, including his aliases and ratings and ranks.
* Games and game stats are anonymized by replacing the deleted player with a "Deleted Player #" placeholder (negative player_id)
* @param {any} cli database client
* @param {any} steamId Steam-ID of the player to be deleted
* Delete or anonymize the player with the given internal id, including his aliases and ranks. Ratings are kept when anonymizing.
* Games and game stats are anonymized by replacing the deleted player with a "Deleted Player #" placeholder (negative player_id).
* @param {any} cli - database client
* @param {string} steamId - Steam-ID of the player to be deleted
* @param {bool} deletePlayer - true to delete, false to anonymize
*/
function deletePlayerBySteamId(cli, steamId) {
function deletePlayerBySteamId(cli, steamId, deletePlayer) {
return Q()
.then(function () { return Q.ninvoke(cli, "query", "select player_id from hashkeys where hashkey=$1", [steamId]) })
.then(function (result) {
return result.rowCount === 0 ? Q() : deletePlayerByInternalId(cli, result.rows[0].player_id);
return result.rowCount === 0 ? Q() : deletePlayerByInternalId(cli, result.rows[0].player_id, deletePlayer);
});
}

function deletePlayerByInternalId(cli, playerId) {
/**
* Delete or anonymize the player with the given internal id, including his aliases and ranks. Ratings are kept when anonymizing.
* Games and game stats are anonymized by replacing the deleted player with a "Deleted Player #" placeholder (negative player_id).
* @param {any} cli - database client
* @param {int} playerId - internal ID of the player to be deleted
* @param {bool} deletePlayer - true to delete, false to anonymize
*/
function deletePlayerByInternalId(cli, playerId, deletePlayer) {
if (!playerId || parseInt(playerId) <= 2)
return Q(); // special player IDs must not be deleted
return Q()
.then(function () { return Q.ninvoke(cli, "query", "select game_id, min(pid) as min_player_id from xonstat.games g, unnest(g.players) pid where players @> ARRAY[$1::int] group by 1", [playerId]) })
.then(function (result) {
return result.rows.reduce(function (chain, row) {
var newId = row.min_player_id < 0 ? row.min_player_id - 1 : -1;
var args = [row.game_id, playerId, newId];
var args4 = [row.game_id, playerId, newId, "Deleted Player " + (-newId)];
var args4 = [row.game_id, playerId, newId, "Anonymous Player " + (-newId)];
return chain
.then(function () { return Q.ninvoke(cli, "query", { name: "anon1", text: "update games set players=array_replace(players, $2, $3) where game_id=$1", values: args }); })
.then(function () { return Q.ninvoke(cli, "query", { name: "anon2", text: "update player_game_stats set player_id=$3, nick=$4, stripped_nick=$4 where game_id=$1 and player_id=$2", values: args4 }); })
.then(function () { return Q.ninvoke(cli, "query", { name: "anon3", text: "update player_weapon_stats set player_id=$3 where game_id=$1 and player_id=$2", values: args }); });
}, Q());
})
.then(function () { return Q.ninvoke(cli, "query", "delete from player_nicks where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "delete from player_elos where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "delete from player_ranks where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "delete from player_ranks_history where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "update hashkeys set active_ind=false, player_id=-1, delete_dt=now() where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "delete from players where player_id=$1", [playerId]); });
.then(function () {
if (!deletePlayer)
return Q();
return Q()
.then(function () { return Q.ninvoke(cli, "query", "delete from player_elos where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "update hashkeys set active_ind=false, player_id=-1, delete_dt=timezone('utc',now()) where player_id=$1", [playerId]); })
.then(function () { return Q.ninvoke(cli, "query", "delete from players where player_id=$1", [playerId]); });
});
}
40 changes: 33 additions & 7 deletions feeder/views/account.ejs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<%- include("header"); -%>

<style>
h2 { margin-top: 50px }
</style>

<!--<h1>Hello, <%= user.displayName %></h1>-->
<!--Your Steam-ID is: <%= user.id; %> <br>-->

Expand All @@ -9,7 +13,9 @@
</div>
<% } %>

<%if (!player.player_id) { %>
<% if (player.allow_tracking === false) { %>
<%- include("account_deleted"); -%>
<% } else if ( !player.player_id || parseInt(player.player_id) < 0) { %>
<%- include("account_register"); -%>
<% } else { %>
<a href="/player/<%=player.player_id%>" target="_parent">My Stats</a>
Expand All @@ -20,20 +26,40 @@
<p>NOTE: You have not set a preference yet. Until May 24th you will be treated as public, after that you are no longer tracked.</p>
<% } %>
<form method="post">
<input type="radio" name="matchHistory" value="3" <%= player.privacy_match_hist == 0 || player.privacy_match_hist == 3 ? "checked" : "" %>><span class="opt-out">Not tracked:</span> Don't record new data and don't provide rating information.
<br><input type="radio" name="matchHistory" value="1" <%= player.privacy_match_hist == 1 ? "checked" : "" %>><span class="opt-in-private">Private:</span> Your ratings and aliases are accessible, your match history is private
<br><input type="radio" name="matchHistory" value="2" <%= player.privacy_match_hist == 2 ? "checked" : "" %>><span class="opt-in-public">Public:</span> Everyone can see your ratings and match history
<input type="radio" name="matchHistory" value="3" <%= player.privacy_match_hist == 0 || player.privacy_match_hist == 3 ? "checked" : "" %>><span class="opt-out">Anonymous:</span> Ratings + cheat indicator only. No nicks/aliases/match history on record, no profile page, "Anonymous Player" in match results.
<br><input type="radio" name="matchHistory" value="1" <%= player.privacy_match_hist == 1 ? "checked" : "" %>><span class="opt-in-private">Private:</span> Your nick and ID in match results, aliases on your profile. No match history on your profile page.
<br><input type="radio" name="matchHistory" value="2" <%= player.privacy_match_hist == 2 ? "checked" : "" %>><span class="opt-in-public">Public:</span> Full match history and weapon stats on your profile page.
<p>
<input type="checkbox" name="locate" value="1" <%= player.privacy_nowplaying == 1 ? "checked" : "" %>>Show which server I'm currently playing on ("Now Playing")
</p>
<button type="submit" name="action" value="update" class="button">Save Settings</button>
</form>
<h2>Anonymize existing data</h2>
<p>
This will remove your ID and nick name from all existing match data replacing you with "Anonymous Player".
<br/>It will clear your match history and your aliases.
<br/>Your current ratings won't be affected by this.
</p>
<form method="post">
<p>
<input type="checkbox" name="confirm" value="1"/>Confirm anonymization
</p>
<button type="submit" name="action" value="anonymize" class="button" style="background-color:#c80;color:white">Anonymize existing data</button>
</form>
<h2>Delete account</h2>
When you delete your account, qlstats will not allow you to sign-up again for 2 months to prevent abusive reset of ratings.
When you delete your account, qlstats will not allow you to sign-up again for 60 days to prevent abusive reset of ratings.
<br/>For this purpose qlstats will keep your Steam-ID and the deletion date on record for said amount of time. All other information will be erased immediately.
<br />The matches you participated in will be updated so that you show up as an Untracked Player.
<br/>
<br/>In all existing match results you will be replaced with "Anonymous Player".
<p>In future match results you will show up as "Untracked Player" and since we can't rate you anymore, we can't rate the match, affecting all other players.
<br><b>Server owners may deny you joining their ranked servers</b>, if they want to make sure matches can get rated.
</p>
<form method="post">
<p>
<input type="checkbox" name="confirm" value="1"/>Confirm account deletion
Expand Down
5 changes: 5 additions & 0 deletions feeder/views/account_deleted.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<h1>Account deleted</h1>
On <%= player.delete_dt.toLocaleDateString(undefined, { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }) %> you chose to delete your account.
<br/>As you were informed back then, you won't be able to re-register for a total period of 60 days.
<br/>This step is necessary to avoid abusive reset of ratings by repeated deleting + registering.
<p><b>Sorry!</b></p>
4 changes: 2 additions & 2 deletions feeder/views/account_register.ejs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1>Welcome!</h1>
<p><b style="color:red">Your Steam-ID is not registered yet.</b> qlstats has no data about you.</p>
<p><span style="color:lightgreen">If you want to register</span>, please read and accept the following privacy policy:</p>

<p><span style="color:lightgreen">If you want to register on qlstats</span>, please read and accept the following privacy policy:</p>

<%- include("privacy_policy_text"); %>

Expand Down
29 changes: 17 additions & 12 deletions feeder/views/privacy_policy_text.ejs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<h2>Privacy Policy</h2>
Last update: 2018-05-12 (UTC)
Last update: 2018-05-16 (UTC)

<h3>Purpose of data processing</h3>
<ul>
<li>Provide skill ratings about players to support matchmaking (team shuffling, server browsing)</li>
<li>Provide a leader board based on skill ratings</li>
<li>Provide indication if a player has been using cheats</li>
<li>Provide leader boards based on skill ratings</li>
<li>Provide players a match history showing how skill ratings got updated</li>
<li>Provide server owners a match history showing the server activity</li>
<li>Provide players a means of finding other players on servers</li>
Expand All @@ -18,36 +19,40 @@ to get live data of current games on these registered server. Players are identi
<li>Match results (including weapon statistics)</li>
<li>Skill ratings for various game types</li>
<li>Nick names used by a player (aliases)</li>
<li>Status information, i.e. if a player is a cheater</li>
<li>Status information, i.e. if a player is a cheater or wants to stay untracked</li>
<li>Global region (continent) of a player, based on the servers a player plays on</li>
<li>Game server a player is currently playing on</li>
<li>Version of the privacy policy accepted by a player</li>
</ul>

<p>For the purpose of keeping the site secure and operational, IP addresses may be temporarily written to log files.</p>
<p>For the purpose of keeping the site secure and operational, IP addresses may be temporarily written to log files. Logs will be kept no longer than 24 hours.</p>

<p>If you decide to delete your account, qlstats will keep your Steam-ID on record for 2 months and will not let you sign-up again during this time to prevent abusive reset of your ratings.</p>
<p>If you decide to delete your account, qlstats will keep your Steam-ID on record for 60 days and will not let you sign-up again during this time to prevent abusive reset of your ratings.</p>

<p>qlstats does NOT know a player's real name, home address, e-mail address or ip-address.</p>
<p>
qlstats does NOT know a player's real name, home address, e-mail address or ip-address.
<br/>In your privacy settings you can choose to stay anonymous. In this case only your steam-id and current ratings are stored by qlstats, nothing else.
</p>


<h3>Data sharing</h3>
qlstats provides public APIs to allow automated retrieval of specific data. This information is available to anyone:
qlstats provides public access to specific personal data:
<ul>
<li>Skill ratings for specific Steam-IDs (used by game server plugins for team composition)</li>
<li>List of all players and their skill ratings who currently play on registered servers (used by QL server browsers to show server skill ratings)</li>
<li>Skill ratings for specific Steam-IDs (used by game server plugins for team composition).</li>
<li>List of all players and their skill ratings who currently play on registered servers (used by QL server browsers to show server skill ratings) [optional]</li>
</ul>
Raw match data may be given to 3rd parties (i.e. server owners), if they guarantee compliance with the General Data Protection Regulation. These 3rd parties are:
Raw match data may be given to 3rd parties (i.e. game server owners), if they guarantee compliance with the General Data Protection Regulation. These 3rd parties are:
<ul>
<li>currently none</li>
</ul>

<p>In addition to intended data sharing it is beyond the control of qlstats if 3rd parties scrape information from the publicly available web pages.</p>

<h3>Privacy settings</h3>
Registered players can set preferences about which information they want to share:
Registered players can set preferences about the information they want to share:
<ul>
<li>skill rating, aliases, "cheater"-status: mandatory</li>
<li>skill rating, "cheater"-status: mandatory</li>
<li>nick name, aliases: optional</li>
<li>match history: optional</li>
<li>locating players on servers ("now playing"): optional</li>
</ul>
70 changes: 70 additions & 0 deletions sql/delete-match.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
set search_path=xonstat,public;

update players set privacy_match_hist=3 where player_id in (24024, 26236);

do $$
declare gid bigint;
begin
select game_id into gid from games where match_id='a6558e8a-b1d4-4c69-acac-5cf3cf13c588';
delete from player_weapon_stats where game_id=gid;
delete from player_game_stats where game_id=gid;
delete from games where game_id=gid;
end
$$;


select * from players where player_id=14;
select * from hashkeys where hashkey='76561198137367816'
delete from hashkeys where hashkey='76561198137367816'
delete from hashkeys where active_ind=false and timezone('UTC', now()) - delete_dt >= '-1 days';
select *,timezone('UTC', now()) - delete_dt from hashkeys where active_ind=false and timezone('UTC', now()) - delete_dt >= '-1 days';
delete from player_nicks where timezone('UTC', now()) - create_dt >= '60 days' and (last_used_dt is null or timezone('UTC', now()) - create_dt >= '60 days');
select * from player_nicks where player_id=23525
select * from players where player_id=23525

select timezone('utc',now()), now() - timezone('utc',now())

create or replace function xonstat.getOrCreatePlayer(steamid varchar(30), rawNick varchar(64), strippedNick varchar(64)) returns integer as $$
declare
id integer;
dummy integer;
oldNick varchar(64);
active boolean;
privacyMode integer;
rowcount integer;
begin
loop
select h.active_ind, p.player_id, nick, privacy_match_hist
into active, id, oldNick, privacyMode
from xonstat.hashkeys h left outer join xonstat.players p on p.player_id=h.player_id where h.hashkey=steamid;
if found then
if not active then
-- player requested to be erased and forgotten
return null;
end if;
if privacyMode<>3 then
update xonstat.players set nick=rawNick, stripped_nick=strippedNick where player_id=id;
update xonstat.player_nicks set last_used_dt=now() where player_id=id and stripped_nick=strippedNick;
get diagnostics rowcount = row_count;
if rowcount = 0 then
insert into xonstat.player_nicks(player_id, stripped_nick, nick) values (id, strippedNick, rawNick);
end if;
end if;
return id;
end if;

begin
select nextval('xonstat.players_player_id_seq'::regclass) into id;
insert into xonstat.players (player_id,privacy_match_hist,nick,stripped_nick) values (id, 3, 'Anonymous Player', 'Anonymous Player');
insert into xonstat.hashkeys (player_id, hashkey) values (id, steamid);
return id;
exception when unique_violation then
-- try again
end;

return null;
end loop;
end;
$$ language plpgsql;

select getOrCreatePlayer('12345','foo','foo')
2 changes: 1 addition & 1 deletion sql/purge_deleted_steamids.sql
Original file line number Diff line number Diff line change
@@ -1 +1 @@
-- delete from hashkeys where active_ind=false and now() - delete_dt >= 60;
delete from hashkeys where active_ind=false and timezone('UTC', now()) - delete_dt >= '60 days';
Loading

0 comments on commit 04faaef

Please sign in to comment.