dump_user_data.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. #!/usr/bin/env python3
  2. # User data dumping script for dumping data from an SS14 postgres database.
  3. # Intended to service GDPR data requests or what have you.
  4. import argparse
  5. import os
  6. import psycopg2
  7. from uuid import UUID
  8. LATEST_DB_MIGRATION = "20230725193102_AdminNotesImprovementsForeignKeys"
  9. def main():
  10. parser = argparse.ArgumentParser()
  11. parser.add_argument("output", help="Directory to output data dumps into.")
  12. parser.add_argument("user", help="User name/ID to dump data into.")
  13. parser.add_argument("--ignore-schema-mismatch", action="store_true")
  14. parser.add_argument("--connection-string", required=True, help="Database connection string to use. See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING")
  15. args = parser.parse_args()
  16. arg_output: str = args.output
  17. if not os.path.exists(arg_output):
  18. print("Creating output directory (doesn't exist yet)")
  19. os.mkdir(arg_output)
  20. conn = psycopg2.connect(args.connection_string)
  21. cur = conn.cursor()
  22. check_schema_version(cur, args.ignore_schema_mismatch)
  23. user_id = normalize_user_id(cur, args.user)
  24. dump_admin(cur, user_id, arg_output)
  25. dump_admin_log(cur, user_id, arg_output)
  26. dump_admin_messages(cur, user_id, arg_output)
  27. dump_admin_notes(cur, user_id, arg_output)
  28. dump_admin_watchlists(cur, user_id, arg_output)
  29. dump_connection_log(cur, user_id, arg_output)
  30. dump_play_time(cur, user_id, arg_output)
  31. dump_player(cur, user_id, arg_output)
  32. dump_preference(cur, user_id, arg_output)
  33. dump_server_ban(cur, user_id, arg_output)
  34. dump_server_ban_exemption(cur, user_id, arg_output)
  35. dump_server_role_ban(cur, user_id, arg_output)
  36. dump_uploaded_resource_log(cur, user_id, arg_output)
  37. dump_whitelist(cur, user_id, arg_output)
  38. def check_schema_version(cur: "psycopg2.cursor", ignore_mismatch: bool):
  39. cur.execute('SELECT "MigrationId" FROM "__EFMigrationsHistory" ORDER BY "__EFMigrationsHistory" DESC LIMIT 1')
  40. schema_version = cur.fetchone()
  41. if schema_version == None:
  42. print("Unable to read database schema version.")
  43. exit(1)
  44. if schema_version[0] != LATEST_DB_MIGRATION:
  45. print(f"Unsupport schema version of DB: '{schema_version[0]}'. Supported: {LATEST_DB_MIGRATION}")
  46. if ignore_mismatch:
  47. return
  48. exit(1)
  49. def normalize_user_id(cur: "psycopg2.cursor", name_or_uid: str) -> str:
  50. try:
  51. return str(UUID(name_or_uid))
  52. except ValueError:
  53. # Must be a name, get UUID from DB.
  54. pass
  55. cur.execute("SELECT user_id FROM player WHERE last_seen_user_name = %s ORDER BY last_seen_time DESC LIMIT 1", (name_or_uid,))
  56. row = cur.fetchone()
  57. if row == None:
  58. print(f"Unable to find user '{name_or_uid}' in DB.")
  59. exit(1)
  60. print(f"Found user ID: {row[0]}")
  61. return row[0]
  62. def dump_admin(cur: "psycopg2.cursor", user_id: str, outdir: str):
  63. print("Dumping admin...")
  64. # #>> '{}' is to turn it into a string.
  65. cur.execute("""
  66. SELECT
  67. COALESCE(json_agg(to_jsonb(data) - 'admin_rank_id'), '[]') #>> '{}'
  68. FROM (
  69. SELECT
  70. *,
  71. (SELECT to_json(rank) FROM (
  72. SELECT * FROM admin_rank WHERE admin_rank.admin_rank_id = admin.admin_rank_id
  73. ) rank)
  74. as admin_rank,
  75. (SELECT COALESCE(json_agg(to_jsonb(flagg) - 'admin_id'), '[]') FROM (
  76. SELECT * FROM admin_flag WHERE admin_id = %s
  77. ) flagg)
  78. as admin_flags
  79. FROM
  80. admin
  81. WHERE
  82. user_id = %s
  83. ) as data
  84. """, (user_id, user_id))
  85. json_data = cur.fetchall()[0][0]
  86. with open(os.path.join(outdir, "admin.json"), "w", encoding="utf-8") as f:
  87. f.write(json_data)
  88. def dump_admin_log(cur: "psycopg2.cursor", user_id: str, outdir: str):
  89. print("Dumping admin_log...")
  90. cur.execute("""
  91. SELECT
  92. COALESCE(json_agg(to_jsonb(data) - 'admin_log_id'), '[]') #>> '{}'
  93. FROM (
  94. SELECT
  95. *
  96. FROM
  97. admin_log_player alp
  98. INNER JOIN
  99. admin_log al
  100. ON
  101. al.admin_log_id = alp.log_id AND al.round_id = alp.round_id
  102. WHERE
  103. player_user_id = %s
  104. ) as data
  105. """, (user_id,))
  106. json_data = cur.fetchall()[0][0]
  107. with open(os.path.join(outdir, "admin_log.json"), "w", encoding="utf-8") as f:
  108. f.write(json_data)
  109. def dump_admin_notes(cur: "psycopg2.cursor", user_id: str, outdir: str):
  110. print("Dumping admin_notes...")
  111. cur.execute("""
  112. SELECT
  113. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  114. FROM (
  115. SELECT
  116. *
  117. FROM
  118. admin_notes
  119. WHERE
  120. player_user_id = %s
  121. ) as data
  122. """, (user_id,))
  123. json_data = cur.fetchall()[0][0]
  124. with open(os.path.join(outdir, "admin_notes.json"), "w", encoding="utf-8") as f:
  125. f.write(json_data)
  126. def dump_connection_log(cur: "psycopg2.cursor", user_id: str, outdir: str):
  127. print("Dumping connection_log...")
  128. cur.execute("""
  129. SELECT
  130. COALESCE(json_agg(to_jsonb(data)), '[]') #>> '{}'
  131. FROM (
  132. SELECT
  133. *,
  134. (SELECT COALESCE(json_agg(to_jsonb(ban_hit)), '[]') FROM (
  135. SELECT * FROM server_ban_hit WHERE connection_id = connection_log_id
  136. ) ban_hit)
  137. as ban_hits
  138. FROM
  139. connection_log
  140. WHERE
  141. user_id = %s
  142. ) as data
  143. """, (user_id,))
  144. json_data = cur.fetchall()[0][0]
  145. with open(os.path.join(outdir, "connection_log.json"), "w", encoding="utf-8") as f:
  146. f.write(json_data)
  147. def dump_play_time(cur: "psycopg2.cursor", user_id: str, outdir: str):
  148. print("Dumping play_time...")
  149. cur.execute("""
  150. SELECT
  151. COALESCE(json_agg(to_jsonb(data)), '[]') #>> '{}'
  152. FROM (
  153. SELECT
  154. *
  155. FROM
  156. play_time
  157. WHERE
  158. player_id = %s
  159. ) as data
  160. """, (user_id,))
  161. json_data = cur.fetchall()[0][0]
  162. with open(os.path.join(outdir, "play_time.json"), "w", encoding="utf-8") as f:
  163. f.write(json_data)
  164. def dump_player(cur: "psycopg2.cursor", user_id: str, outdir: str):
  165. print("Dumping player...")
  166. cur.execute("""
  167. SELECT
  168. COALESCE(json_agg(to_jsonb(data)), '[]') #>> '{}'
  169. FROM (
  170. SELECT
  171. *,
  172. (SELECT COALESCE(json_agg(to_jsonb(player_round_subquery) - 'players_id'), '[]') FROM (
  173. SELECT * FROM player_round WHERE players_id = player_id
  174. ) player_round_subquery)
  175. as player_rounds
  176. FROM
  177. player
  178. WHERE
  179. user_id = %s
  180. ) as data
  181. """, (user_id,))
  182. json_data = cur.fetchall()[0][0]
  183. with open(os.path.join(outdir, "player.json"), "w", encoding="utf-8") as f:
  184. f.write(json_data)
  185. def dump_preference(cur: "psycopg2.cursor", user_id: str, outdir: str):
  186. print("Dumping preference...")
  187. # God have mercy on my soul.
  188. cur.execute("""
  189. SELECT
  190. COALESCE(json_agg(to_jsonb(data)), '[]') #>> '{}'
  191. FROM (
  192. SELECT
  193. *,
  194. (SELECT json_agg(to_jsonb(profile_subq) - 'preference_id') FROM (
  195. SELECT
  196. *,
  197. (SELECT COALESCE(json_agg(to_jsonb(job_subq) - 'profile_id'), '[]') FROM (
  198. SELECT * FROM job WHERE job.profile_id = profile.profile_id
  199. ) job_subq)
  200. as jobs,
  201. (SELECT COALESCE(json_agg(to_jsonb(antag_subq) - 'profile_id'), '[]') FROM (
  202. SELECT * FROM antag WHERE antag.profile_id = profile.profile_id
  203. ) antag_subq)
  204. as antags,
  205. (SELECT COALESCE(json_agg(to_jsonb(trait_subq) - 'profile_id'), '[]') FROM (
  206. SELECT * FROM trait WHERE trait.profile_id = profile.profile_id
  207. ) trait_subq)
  208. as traits
  209. FROM
  210. profile
  211. WHERE
  212. profile.preference_id = preference.preference_id
  213. ) profile_subq)
  214. as profiles
  215. FROM
  216. preference
  217. WHERE
  218. user_id = %s
  219. ) as data
  220. """, (user_id,))
  221. json_data = cur.fetchall()[0][0]
  222. with open(os.path.join(outdir, "preference.json"), "w", encoding="utf-8") as f:
  223. f.write(json_data)
  224. def dump_server_ban(cur: "psycopg2.cursor", user_id: str, outdir: str):
  225. print("Dumping server_ban...")
  226. cur.execute("""
  227. SELECT
  228. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  229. FROM (
  230. SELECT
  231. *,
  232. (SELECT to_jsonb(unban_sq) - 'ban_id' FROM (
  233. SELECT * FROM server_unban WHERE server_unban.ban_id = server_ban.server_ban_id
  234. ) unban_sq)
  235. as unban
  236. FROM
  237. server_ban
  238. WHERE
  239. player_user_id = %s
  240. ) as data
  241. """, (user_id,))
  242. json_data = cur.fetchall()[0][0]
  243. with open(os.path.join(outdir, "server_ban.json"), "w", encoding="utf-8") as f:
  244. f.write(json_data)
  245. def dump_server_ban_exemption(cur: "psycopg2.cursor", user_id: str, outdir: str):
  246. print("Dumping server_ban_exemption...")
  247. cur.execute("""
  248. SELECT
  249. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  250. FROM (
  251. SELECT
  252. *
  253. FROM
  254. server_ban_exemption
  255. WHERE
  256. user_id = %s
  257. ) as data
  258. """, (user_id,))
  259. json_data = cur.fetchall()[0][0]
  260. with open(os.path.join(outdir, "server_ban_exemption.json"), "w", encoding="utf-8") as f:
  261. f.write(json_data)
  262. def dump_server_role_ban(cur: "psycopg2.cursor", user_id: str, outdir: str):
  263. print("Dumping server_role_ban...")
  264. cur.execute("""
  265. SELECT
  266. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  267. FROM (
  268. SELECT
  269. *,
  270. (SELECT to_jsonb(role_unban_sq) - 'ban_id' FROM (
  271. SELECT * FROM server_role_unban WHERE server_role_unban.ban_id = server_role_ban.server_role_ban_id
  272. ) role_unban_sq)
  273. as unban
  274. FROM
  275. server_role_ban
  276. WHERE
  277. player_user_id = %s
  278. ) as data
  279. """, (user_id,))
  280. json_data = cur.fetchall()[0][0]
  281. with open(os.path.join(outdir, "server_role_ban.json"), "w", encoding="utf-8") as f:
  282. f.write(json_data)
  283. def dump_uploaded_resource_log(cur: "psycopg2.cursor", user_id: str, outdir: str):
  284. print("Dumping uploaded_resource_log...")
  285. cur.execute("""
  286. SELECT
  287. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  288. FROM (
  289. SELECT
  290. *
  291. FROM
  292. uploaded_resource_log
  293. WHERE
  294. user_id = %s
  295. ) as data
  296. """, (user_id,))
  297. json_data = cur.fetchall()[0][0]
  298. with open(os.path.join(outdir, "uploaded_resource_log.json"), "w", encoding="utf-8") as f:
  299. f.write(json_data)
  300. def dump_whitelist(cur: "psycopg2.cursor", user_id: str, outdir: str):
  301. print("Dumping whitelist...")
  302. cur.execute("""
  303. SELECT
  304. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  305. FROM (
  306. SELECT
  307. *
  308. FROM
  309. whitelist
  310. WHERE
  311. user_id = %s
  312. ) as data
  313. """, (user_id,))
  314. json_data = cur.fetchall()[0][0]
  315. with open(os.path.join(outdir, "whitelist.json"), "w", encoding="utf-8") as f:
  316. f.write(json_data)
  317. def dump_admin_messages(cur: "psycopg2.cursor", user_id: str, outdir: str):
  318. print("Dumping admin_messages...")
  319. cur.execute("""
  320. SELECT
  321. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  322. FROM (
  323. SELECT
  324. *
  325. FROM
  326. admin_messages
  327. WHERE
  328. player_user_id = %s
  329. ) as data
  330. """, (user_id,))
  331. json_data = cur.fetchall()[0][0]
  332. with open(os.path.join(outdir, "admin_messages.json"), "w", encoding="utf-8") as f:
  333. f.write(json_data)
  334. def dump_admin_watchlists(cur: "psycopg2.cursor", user_id: str, outdir: str):
  335. print("Dumping admin_watchlists...")
  336. cur.execute("""
  337. SELECT
  338. COALESCE(json_agg(to_json(data)), '[]') #>> '{}'
  339. FROM (
  340. SELECT
  341. *
  342. FROM
  343. admin_watchlists
  344. WHERE
  345. player_user_id = %s
  346. ) as data
  347. """, (user_id,))
  348. json_data = cur.fetchall()[0][0]
  349. with open(os.path.join(outdir, "admin_watchlists.json"), "w", encoding="utf-8") as f:
  350. f.write(json_data)
  351. main()
  352. # "I'm surprised you managed to write this entire Python file without spamming the word 'sus' everywhere." - Remie