20240929-autumn-rewrite.mjs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. // This script is intended for migrating to the new Autumn release.
  2. // Please read all TODOs in this file as they will help guide you
  3. // to migrate your data properly. Please do Ctrl + F "TODO".
  4. import { MongoClient } from "mongodb";
  5. /**
  6. * Map of tags to S3 bucket names
  7. *
  8. * TODO: if you've used AUTUMN_S3_BUCKET_PREFIX in the past
  9. * update the bucket names below to include the prefix
  10. *
  11. * NOTE: update `files.s3.default_bucket` in Revolt.toml!
  12. */
  13. const BUCKET_MAP = {
  14. attachments: "attachments",
  15. avatars: "avatars",
  16. backgrounds: "backgrounds",
  17. icons: "icons",
  18. banners: "banners",
  19. emojis: "emojis",
  20. };
  21. /**
  22. * Connection URL for MongoDB instance
  23. *
  24. * TODO: change if necessary
  25. */
  26. const CONNECTION_URL = "mongodb://database";
  27. const objectLookup = {};
  28. const mongo = new MongoClient(CONNECTION_URL);
  29. await mongo.connect();
  30. async function determineUploaderIdAndUse(f, v, i) {
  31. if (f.tag === "attachments" && v === "attachments") {
  32. if (typeof f.message_id !== "string") {
  33. console.warn(i, "No message id specified.");
  34. return null;
  35. }
  36. if (!objectLookup[f.message_id]) {
  37. objectLookup[f.message_id] = await mongo
  38. .db("revolt")
  39. .collection("messages")
  40. .findOne({
  41. _id: f.message_id,
  42. });
  43. }
  44. if (!objectLookup[f.message_id]) {
  45. console.warn(i, "Message", f.message_id, "doesn't exist anymore!");
  46. return null;
  47. }
  48. return {
  49. uploaded_at: new Date(decodeTime(f.message_id)),
  50. uploader_id: objectLookup[f.message_id].author,
  51. used_for: {
  52. type: "Message",
  53. id: f.message_id,
  54. },
  55. };
  56. } else if (f.tag === "banners" && v === "banners") {
  57. if (typeof f.server_id !== "string") {
  58. console.warn(i, "No server id specified.");
  59. return null;
  60. }
  61. if (!objectLookup[f.server_id]) {
  62. objectLookup[f.server_id] = await mongo
  63. .db("revolt")
  64. .collection("servers")
  65. .findOne({
  66. _id: f.server_id,
  67. });
  68. }
  69. if (!objectLookup[f.server_id]) {
  70. console.warn(i, "Server", f.server_id, "doesn't exist anymore!");
  71. return null;
  72. }
  73. return {
  74. uploaded_at: new Date(),
  75. uploader_id: objectLookup[f.server_id].owner,
  76. used_for: {
  77. type: "ServerBanner",
  78. id: f.server_id,
  79. },
  80. };
  81. } else if (f.tag === "emojis" && v === "emojis") {
  82. if (typeof f.object_id !== "string") {
  83. return null;
  84. }
  85. if (!objectLookup[f.object_id]) {
  86. objectLookup[f.object_id] = await mongo
  87. .db("revolt")
  88. .collection("emojis")
  89. .findOne({
  90. _id: f.object_id,
  91. });
  92. }
  93. if (!objectLookup[f.object_id]) {
  94. console.warn(i, "Emoji", f.object_id, "doesn't exist anymore!");
  95. return null;
  96. }
  97. return {
  98. uploaded_at: new Date(decodeTime(f.object_id)),
  99. uploader_id: objectLookup[f.object_id].creator_id,
  100. used_for: {
  101. type: "Emoji",
  102. id: f.object_id,
  103. },
  104. };
  105. } else if (f.tag === "avatars" && v === "avatars") {
  106. if (typeof f.user_id !== "string") {
  107. return null;
  108. }
  109. if (!objectLookup[f.user_id]) {
  110. objectLookup[f.user_id] = await mongo
  111. .db("revolt")
  112. .collection("users")
  113. .findOne({
  114. _id: f.user_id,
  115. });
  116. }
  117. if (!objectLookup[f.user_id]) {
  118. console.warn(i, "User", f.user_id, "doesn't exist anymore!");
  119. return null;
  120. }
  121. if (objectLookup[f.user_id].avatar?._id !== f._id) {
  122. console.warn(
  123. i,
  124. "Attachment no longer in use.",
  125. f._id,
  126. "for",
  127. f.user_id,
  128. "current:",
  129. objectLookup[f.user_id].avatar?._id
  130. );
  131. return null;
  132. }
  133. return {
  134. uploaded_at: new Date(),
  135. uploader_id: f.user_id,
  136. used_for: {
  137. type: "UserAvatar",
  138. id: f.user_id,
  139. },
  140. };
  141. } else if (f.tag === "backgrounds" && v === "backgrounds") {
  142. if (typeof f.user_id !== "string") {
  143. return null;
  144. }
  145. if (!objectLookup[f.user_id]) {
  146. objectLookup[f.user_id] = await mongo
  147. .db("revolt")
  148. .collection("users")
  149. .findOne({
  150. _id: f.user_id,
  151. });
  152. }
  153. if (!objectLookup[f.user_id]) {
  154. console.warn(i, "User", f.user_id, "doesn't exist anymore!");
  155. return null;
  156. }
  157. if (objectLookup[f.user_id].profile?.background?._id !== f._id) {
  158. console.warn(
  159. i,
  160. "Attachment no longer in use.",
  161. f._id,
  162. "for",
  163. f.user_id,
  164. "current:",
  165. objectLookup[f.user_id].profile?.background?._id
  166. );
  167. return null;
  168. }
  169. return {
  170. uploaded_at: new Date(),
  171. uploader_id: f.user_id,
  172. used_for: {
  173. type: "UserProfileBackground",
  174. id: f.user_id,
  175. },
  176. };
  177. } else if (f.tag === "icons" && v === "icons") {
  178. if (typeof f.object_id !== "string") {
  179. return null;
  180. }
  181. // some bugged files at start
  182. // ... expensive to compute at worst case =(
  183. // so instead we can just disable it until everything is processed
  184. // then re-run on these!
  185. if (false) {
  186. objectLookup[f.object_id] = await mongo
  187. .db("revolt")
  188. .collection("users")
  189. .findOne({
  190. _id: f.object_id,
  191. });
  192. if (!objectLookup[f.object_id]) {
  193. console.warn(i, "No legacy match!");
  194. return null;
  195. }
  196. return {
  197. uploaded_at: new Date(),
  198. uploader_id: f.object_id,
  199. used_for: {
  200. type: "LegacyGroupIcon",
  201. id: f.object_id,
  202. },
  203. };
  204. }
  205. if (!objectLookup[f.object_id]) {
  206. objectLookup[f.object_id] = await mongo
  207. .db("revolt")
  208. .collection("servers")
  209. .findOne({
  210. _id: f.object_id,
  211. });
  212. }
  213. if (
  214. !objectLookup[f.object_id] ||
  215. // heuristic for not server
  216. !objectLookup[f.object_id].channels
  217. ) {
  218. console.warn(i, "Server", f.object_id, "doesn't exist!");
  219. if (!objectLookup[f.object_id]) {
  220. objectLookup[f.object_id] = await mongo
  221. .db("revolt")
  222. .collection("channels")
  223. .findOne({
  224. _id: f.object_id,
  225. });
  226. }
  227. if (!objectLookup[f.object_id]) {
  228. console.warn(i, "Channel", f.object_id, "doesn't exist!");
  229. return null;
  230. }
  231. let server;
  232. const serverId = objectLookup[f.object_id].server;
  233. if (serverId) {
  234. server = objectLookup[serverId];
  235. if (!server) {
  236. server = await mongo.db("revolt").collection("servers").findOne({
  237. _id: serverId,
  238. });
  239. console.info(
  240. i,
  241. "Couldn't find matching server for channel " + f.object_id + "!"
  242. );
  243. if (!server) return null;
  244. objectLookup[serverId] = server;
  245. }
  246. }
  247. return {
  248. uploaded_at: new Date(),
  249. uploader_id: (server ?? objectLookup[f.object_id]).owner,
  250. used_for: {
  251. type: "ChannelIcon",
  252. id: f.object_id,
  253. },
  254. };
  255. }
  256. return {
  257. uploaded_at: new Date(),
  258. uploader_id: objectLookup[f.object_id].owner,
  259. used_for: {
  260. type: "ServerIcon",
  261. id: f.object_id,
  262. },
  263. };
  264. } else {
  265. throw (
  266. "couldn't find uploader id for " +
  267. f._id +
  268. " expected " +
  269. v +
  270. " but got " +
  271. f.tag
  272. );
  273. }
  274. }
  275. const dirs = [
  276. "banners",
  277. "emojis",
  278. "avatars",
  279. "backgrounds",
  280. "icons",
  281. "attachments", // https://stackoverflow.com/a/18777877
  282. ];
  283. // === add `used_for` field to files
  284. const files_pt1 = await mongo
  285. .db("revolt")
  286. .collection("attachments")
  287. .find({
  288. $or: [
  289. {
  290. used_for: {
  291. $exists: false,
  292. },
  293. },
  294. {
  295. uploader_id: {
  296. $exists: false,
  297. },
  298. },
  299. {
  300. uploader_at: {
  301. $exists: false,
  302. },
  303. },
  304. ],
  305. })
  306. .toArray();
  307. let i = 1;
  308. for (const file of files_pt1) {
  309. console.info(i++, files_pt1.length, file);
  310. const meta = determineUploaderIdAndUse(file, file.tag, i);
  311. if (meta) {
  312. await mongo.db("revolt").collection("attachments").updateOne(
  313. {
  314. _id: file._id,
  315. },
  316. {
  317. $set: meta,
  318. }
  319. );
  320. }
  321. }
  322. // === set hash to id and create relevant objects
  323. const files_pt2 = await mongo
  324. .db("revolt")
  325. .collection("attachments")
  326. .find({
  327. hash: {
  328. $exists: false,
  329. },
  330. })
  331. .toArray();
  332. await mongo
  333. .db("revolt")
  334. .collection("attachment_hashes")
  335. .insertMany(
  336. files_pt2.map((file) => ({
  337. _id: file._id,
  338. processed_hash: file._id,
  339. created_at: new Date(),
  340. bucket_id: BUCKET_MAP[file.tag],
  341. path: file._id,
  342. iv: "", // disable encryption for file
  343. metadata: file.metadata,
  344. content_type: file.content_type,
  345. size: file.size,
  346. }))
  347. );
  348. for (const file of files_pt2) {
  349. await mongo
  350. .db("revolt")
  351. .collection("attachments")
  352. .updateOne(
  353. {
  354. _id: file._id,
  355. },
  356. {
  357. $set: {
  358. hash: file._id,
  359. },
  360. }
  361. );
  362. }