bulk_merge_users 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #!/usr/bin/env python
  2. from __future__ import annotations
  3. from gzip import GzipFile
  4. from http.client import HTTPResponse, HTTPSConnection
  5. from io import BytesIO
  6. from time import time
  7. from typing import Any
  8. from urllib.parse import urlencode
  9. import click
  10. from sentry.utils import json
  11. help = """
  12. A file with entries separated by newlines, with each entry taking the form:
  13. <USER_BEING_MERGED_ID> -> <USER_MERGING_INTO_ID>. For example, the following file will result in
  14. user 1 being merged into user 2, and 3 into 4:
  15. 1 -> 2
  16. 3 -> 4
  17. """
  18. @click.command(
  19. help="Merge many users at a time using sequential POST calls. The output will be a multiline list indicating success or failure."
  20. )
  21. @click.argument(
  22. "merging",
  23. type=click.File("r"),
  24. # help=help,
  25. )
  26. @click.option(
  27. "--api",
  28. required=True,
  29. help="The Sentry API server to hit. Ex: `sentry.io`.",
  30. )
  31. @click.option(
  32. "--csrf",
  33. required=True,
  34. help="The X-CSRF token.",
  35. )
  36. @click.option(
  37. "--cookie",
  38. required=True,
  39. help="The superadmin cookie, copied verbatim.",
  40. )
  41. def merge_users(merging, api: str, csrf: str, cookie: str):
  42. # Setup the connection.
  43. conn = HTTPSConnection(api)
  44. heartbeat = int(time())
  45. # Validate file format.
  46. calls = []
  47. with merging as fp:
  48. for line in fp:
  49. [from_user, into_user] = line.split("->")
  50. from_user_id = int(from_user.strip())
  51. into_user_id = int(into_user.strip())
  52. calls.append((from_user_id, into_user_id))
  53. results = []
  54. success = 0
  55. # Make actual requests, one at a time.
  56. for args in calls:
  57. t = int(time())
  58. if t - heartbeat > 5:
  59. heartbeat = t
  60. print(
  61. f"attempted: {len(results)}, succeeded: {success}, failed: {len(results) - success}"
  62. )
  63. (merging_from_user_id, merging_into_user_id) = args
  64. result = do_merge(conn, merging_from_user_id, merging_into_user_id, api, csrf, cookie)
  65. results.append(f"{merging_from_user_id} -> {merging_into_user_id}: {result}")
  66. if result // 100 == 2:
  67. success += 1
  68. print(f"attempted: {len(results)}, succeeded: {success}, failed: {len(results) - success}")
  69. print("\n\n")
  70. print("\n".join(results))
  71. def make_headers(csrf: str, cookie: str) -> dict[str, str]:
  72. return {
  73. "Accept": "application/json; charset=utf-8",
  74. "Accept-Encoding": "gzip, deflate, br, zstd",
  75. "Accept-Language": "en-US,en;q=0.9",
  76. "Cache-Control": "no-cache",
  77. "Content-Type": "application/json",
  78. "Cookie": cookie,
  79. "Pragma": "no-cache",
  80. "Priority": "u=1, i",
  81. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
  82. "X-Csrftoken": csrf,
  83. }
  84. def do_merge(
  85. conn: HTTPSConnection,
  86. merging_from_user_id: int,
  87. merging_into_user_id: int,
  88. api: str,
  89. csrf: str,
  90. cookie: str,
  91. ) -> int:
  92. """
  93. Perform a single merge.
  94. """
  95. # Set inputs
  96. endpoint = f"/api/0/users/{merging_into_user_id}/merge-accounts/"
  97. full_url = f"http://{api}{endpoint}"
  98. print(f"merging user {merging_from_user_id} into {merging_into_user_id} at {full_url}")
  99. # Make the underlying request, and load it into JSON.
  100. conn.request(
  101. "POST",
  102. full_url,
  103. headers=make_headers(csrf, cookie),
  104. body=json.dumps({"users": [merging_from_user_id]}),
  105. )
  106. response = conn.getresponse()
  107. response.close()
  108. return response.code
  109. if __name__ == "__main__":
  110. merge_users()