@gryt/core 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,8 @@ export interface ImportDeps {
27
27
  store: ImportFileStore;
28
28
  sleep: (ms: number) => Promise<void>;
29
29
  newId: () => string;
30
+ /** Downloads and uploads running together. A phone holding each file in memory wants fewer. */
31
+ transfersAtOnce?: number;
30
32
  }
31
33
  export interface ImportProgress {
32
34
  messages: {
@@ -1,5 +1,5 @@
1
1
  import { importBatches } from "../mls/content.js";
2
- const DOWNLOADS_AT_ONCE = 4;
2
+ const TRANSFERS_AT_ONCE = 16;
3
3
  /** `mls:send` allows about one send every two seconds sustained (RL_SEND in the server). */
4
4
  const SEND_GAP_MS = 2_000;
5
5
  const FILE_BATCH = 20;
@@ -51,7 +51,6 @@ export async function runDiscordImport(messages, deps, control, report) {
51
51
  };
52
52
  await sendAll(messages.filter((m) => m.attachments.length === 0).map((m) => ({ id: importId(m.discordId), sentAt: m.sentAt, text: m.text })));
53
53
  const missing = new Set();
54
- let next = 0;
55
54
  let clearAt = 0;
56
55
  const downloadFile = async (url) => {
57
56
  for (let attempt = 0; attempt <= DOWNLOAD_RETRIES && !control.stopped(); attempt++) {
@@ -66,47 +65,51 @@ export async function runDiscordImport(messages, deps, control, report) {
66
65
  }
67
66
  return null;
68
67
  };
69
- const worker = async () => {
70
- while (!control.stopped()) {
71
- await hold();
72
- if (next >= urls.length)
73
- break;
74
- const url = urls[next++];
75
- if (!(await deps.store.has(url))) {
76
- const file = await downloadFile(url);
77
- if (file) {
78
- p.bytes += file.bytes.length;
79
- await deps.store.put(url, file);
80
- }
81
- else {
82
- missing.add(url);
83
- p.missing = missing.size;
84
- }
68
+ const eachAtOnce = async (run) => {
69
+ let next = 0;
70
+ await Promise.all(Array.from({ length: deps.transfersAtOnce ?? TRANSFERS_AT_ONCE }, async () => {
71
+ while (!control.stopped()) {
72
+ await hold();
73
+ if (next >= urls.length)
74
+ break;
75
+ await run(urls[next++]);
85
76
  }
86
- p.downloaded.done++;
87
- tick();
88
- }
77
+ }));
89
78
  };
90
- await Promise.all(Array.from({ length: DOWNLOADS_AT_ONCE }, worker));
79
+ await eachAtOnce(async (url) => {
80
+ if (!(await deps.store.has(url))) {
81
+ const file = await downloadFile(url);
82
+ if (file) {
83
+ p.bytes += file.bytes.length;
84
+ await deps.store.put(url, file);
85
+ }
86
+ else {
87
+ missing.add(url);
88
+ p.missing = missing.size;
89
+ }
90
+ }
91
+ p.downloaded.done++;
92
+ tick();
93
+ });
91
94
  const uploaded = new Map();
95
+ await eachAtOnce(async (url) => {
96
+ const stored = missing.has(url) ? null : await deps.store.take(url);
97
+ if (stored) {
98
+ uploaded.set(url, await deps.upload({ name: nameOf(url), ...stored }));
99
+ await deps.store.drop(url);
100
+ }
101
+ p.sent.done++;
102
+ tick();
103
+ });
92
104
  let pending = [];
93
105
  for (const m of withFiles) {
94
106
  if (control.stopped())
95
107
  break;
96
108
  const attachments = {};
97
109
  for (const url of m.attachments) {
98
- await hold();
99
- let up = uploaded.get(url);
100
- const stored = up || missing.has(url) ? null : await deps.store.take(url);
101
- if (stored) {
102
- up = await deps.upload({ name: nameOf(url), ...stored });
103
- uploaded.set(url, up);
104
- await deps.store.drop(url);
105
- }
110
+ const up = uploaded.get(url);
106
111
  if (up)
107
112
  attachments[up.id] = up.key;
108
- p.sent.done++;
109
- tick();
110
113
  }
111
114
  const lost = m.attachments.length - Object.keys(attachments).length;
112
115
  const note = lost ? `${lost === 1 ? "A file" : `${lost} files`} from Discord couldn’t be downloaded.` : "";
@@ -32,5 +32,7 @@ export declare function parseDiscordTime(v: unknown): number | null;
32
32
  export declare function listDiscordDms(files: PackageFiles): Promise<DiscordPackageDm[]>;
33
33
  /** One DM's messages, oldest first. */
34
34
  export declare function readDiscordDm(files: PackageFiles, channelId: string): Promise<DiscordPackageMessage[]>;
35
+ /** Discord's own markup as text a person can read. A custom emoji can't come over, so it keeps its name. */
36
+ export declare function readableDiscordText(text: string, people: ReadonlyMap<string, string>, channels: Readonly<Record<string, unknown>>): string;
35
37
  /** The earliest `ex` (hex Unix seconds) among signed CDN links, or null when none carry one. */
36
38
  export declare function linksExpireAt(urls: string[]): number | null;
@@ -189,7 +189,41 @@ export async function listDiscordDms(files) {
189
189
  export async function readDiscordDm(files, channelId) {
190
190
  const idx = indexNames(files.names);
191
191
  const dir = channelFolder(idx, channelId);
192
- return dir ? messagesIn(files, idx, dir) : [];
192
+ if (!dir)
193
+ return [];
194
+ const names = await peopleIn(files, idx, channelId);
195
+ const channels = ((await readJson(files, idx, "messages/index.json")) ?? {});
196
+ return (await messagesIn(files, idx, dir)).map((m) => ({ ...m, text: readableDiscordText(m.text, names, channels) }));
197
+ }
198
+ async function peopleIn(files, idx, channelId) {
199
+ const names = new Map();
200
+ const user = (await readJson(files, idx, "account/user.json"));
201
+ const add = (u) => {
202
+ const name = u?.global_name || u?.username;
203
+ if (typeof u?.id === "string" && typeof name === "string")
204
+ names.set(u.id, name);
205
+ };
206
+ add(user);
207
+ for (const rel of Array.isArray(user?.relationships) ? user.relationships : [])
208
+ add(rel?.user);
209
+ const channel = (await readJson(files, idx, `messages/c${channelId}/channel.json`));
210
+ const label = (await readJson(files, idx, "messages/index.json"))?.[channelId];
211
+ const peer = Array.isArray(channel?.recipients) ? channel.recipients.find((id) => id !== user?.id) : undefined;
212
+ if (typeof peer === "string" && typeof label === "string" && !names.has(peer))
213
+ names.set(peer, displayName(label));
214
+ return names;
215
+ }
216
+ /** Discord's own markup as text a person can read. A custom emoji can't come over, so it keeps its name. */
217
+ export function readableDiscordText(text, people, channels) {
218
+ return text
219
+ .replace(/<a?:(\w+):\d+>/g, ":$1:")
220
+ .replace(/<@!?(\d+)>/g, (_, id) => `@${people.get(id) ?? "someone"}`)
221
+ .replace(/<@&\d+>/g, "@role")
222
+ .replace(/<#(\d+)>/g, (_, id) => {
223
+ const label = channels[id];
224
+ const name = typeof label === "string" && !/^unknown/i.test(label) ? label.split(",")[0].trim() : "channel";
225
+ return `#${name}`;
226
+ });
193
227
  }
194
228
  /** The earliest `ex` (hex Unix seconds) among signed CDN links, or null when none carry one. */
195
229
  export function linksExpireAt(urls) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/core",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Gryt's shared application logic: one implementation of the things the desktop and mobile apps both do.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {