-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbulk-refresh-users.mjs
136 lines (120 loc) · 4.17 KB
/
bulk-refresh-users.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// ==== Adjust the values below to configure the script ====
// 1. Set this to your instance URL.
const instance = 'https://example.com';
// 2. Set this to a moderator's native token.
// An access token may work, but has not been tested.
const token = '';
// 3. (optional) Set API options to filter the list of users.
// You can supply any supported parameter to the /api/admin/show-users endpoint, but please do not include "limit", "offset", or "origin".
// Those values are generated by the script and will cause errors if modified.
const usersFilter = {
sort: '-createdAt',
state: 'available',
};
// 4. (optional) Set minimum time in milliseconds between requests to update a user.
// The current Sharkey release supports a maximum of 2 calls per second (500 ms) with default rate limits.
// This can be increased to slow down the process, and therefore reduce server load.
const requestIntervalMs = 500;
// 5. (optional) Resume an earlier failed run.
// If the script exited early (such as from network trouble), then you can resume where you left off by adjusting this value.
// Scroll back up in the previous output to the last instance of "Updating page from offset ####:" and place that number here.
// The script will resume from that point.
const initialOffset = 0;
// ==== Stop here! Don't touch anything else! ====
try {
for (let offset = initialOffset;;) {
console.log(`Updating page from offset ${offset}:`);
const page = await api('admin/show-users', {
offset,
limit: 100,
origin: 'remote',
...usersFilter
});
// Stop looping when we stop getting results
if (page.length < 1) break;
offset += page.length;
// Process the page at the configured rate
await updateUsersAtRate(page);
}
} catch (err) {
console.error('Failed with unhandled error: ', err);
}
/**
* @typedef User
* @property {string} id
* @property {string} host
* @property {string} username
*/
/**
* Drip-feeds background requests to update users from a list.
* Maintains an average of requestIntervalMs milliseconds between calls.
* @param page {User[]}
* @returns {Promise<void>}
*/
function updateUsersAtRate(page) {
return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
try {
const res = await updateNextUser(page);
if (!res) {
clearInterval(interval);
resolve();
}
} catch (err) {
reject(err);
}
}, requestIntervalMs);
});
}
/**
* @param {User[]} page
* @returns {Promise<boolean>}
*/
async function updateNextUser(page) {
const user = page.shift();
if (!user) return false;
await api('federation/update-remote-user', { userId: user.id })
.then(() => console.log(`Successfully updated user ${user.id} (${user.username}@${user.host})`))
.catch(err => console.log(`Failed to update user ${user.id} (${user.username}@${user.host}):`, err))
;
return true;
}
/**
* Makes a POST request to Sharkey's API with automatic credentials and rate limit support.
* @param {string} endpoint API endpoint to call
* @param {unknown} [body] Optional object to send as API request payload
* @param {boolean} [retry] Do not use - for retry purposes only
* @returns {Promise<unknown>}
*/
async function api(endpoint, body = {}, retry = false) {
try {
const res = await fetch(`${instance}/api/${endpoint}`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
// Check for rate limit
if (res.status === 429 && !retry) {
const reset = res.headers.get('X-RateLimit-Reset');
const delay = reset ? Number.parseFloat(reset) : 1;
await new Promise(resolve => setTimeout(resolve, delay * 1000));
return await api(endpoint, body, true);
}
// Fucky way of handling any possible response through one code path
if (res.ok) {
const contentType = res.headers.get('Content-Type');
if (!contentType) return undefined;
if (contentType.startsWith('application/json')) return await res.json();
if (contentType.startsWith('text/')) return await res.text();
throw `Unsupported Content-Type: ${contentType}`
} else {
throw `${res.status} ${res.statusText}`;
}
} catch (err) {
throw String(err);
}
}