fedipod 1.49.2 → 1.50.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.
- package/bin/fedipod.mjs +2 -5
- package/cli.md +5 -10
- package/lib/client/masto/index.mjs +0 -3
- package/lib/client/masto/oauth.mjs +23 -112
- package/lib/core/agent.mjs +3 -12
- package/lib/device/cli/commands/setup.mjs +1 -24
- package/lib/device/setup.mjs +1 -5
- package/lib/shared/guard.mjs +2 -2
- package/lib/surface/routes/owner.mjs +2 -12
- package/lib/surface/surface.mjs +1 -2
- package/package.json +1 -1
- package/web/admin/index.html +1 -1
- package/web/admin/record.js +0 -3
- package/web/app/admin-facade.mjs +0 -6
- package/web/app/dist/sw.js +186 -143
- package/web/app/dist/sw.js.map +3 -3
package/web/app/dist/sw.js
CHANGED
|
@@ -46164,6 +46164,15 @@ var PodStore = class {
|
|
|
46164
46164
|
getStatuses() {
|
|
46165
46165
|
return this.read("statuses.json", []);
|
|
46166
46166
|
}
|
|
46167
|
+
// When each open poll closes, read in place: asked every 30 seconds, where
|
|
46168
|
+
// a copy of the whole timeline each time was the whole cost of asking.
|
|
46169
|
+
openPollEnds() {
|
|
46170
|
+
const out = [];
|
|
46171
|
+
for (const s of this.cache.get("statuses.json") || []) {
|
|
46172
|
+
if (s?.kind === "post" && s.poll && !s.poll.closed && s.poll.expiresAt) out.push(Date.parse(s.poll.expiresAt));
|
|
46173
|
+
}
|
|
46174
|
+
return out;
|
|
46175
|
+
}
|
|
46167
46176
|
// A count must not pay for a clone: nodeinfo and the instance document are
|
|
46168
46177
|
// public and answer strangers, so they read the cache directly.
|
|
46169
46178
|
countStatuses(kind = null) {
|
|
@@ -46205,6 +46214,9 @@ var PodStore = class {
|
|
|
46205
46214
|
const all = this.getStatuses();
|
|
46206
46215
|
const i = all.findIndex((x) => x.noteId === noteId);
|
|
46207
46216
|
if (i < 0) return null;
|
|
46217
|
+
if (typeof patch?.content === "string" && patch.content.length > MAX_CONTENT) {
|
|
46218
|
+
patch = { ...patch, content: clamp(patch.content, MAX_CONTENT), truncated: true };
|
|
46219
|
+
}
|
|
46208
46220
|
all[i] = { ...all[i], ...patch };
|
|
46209
46221
|
this.write("statuses.json", all);
|
|
46210
46222
|
return all[i];
|
|
@@ -57691,7 +57703,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
|
|
|
57691
57703
|
reply?.inbox,
|
|
57692
57704
|
...addressedInboxes
|
|
57693
57705
|
].filter(Boolean))];
|
|
57694
|
-
|
|
57706
|
+
const fanout = publisher.deliverer.deliverToAll(inboxes, create);
|
|
57707
|
+
if (publisher.background) fanout.catch((e) => publisher.log(`delivering ${note.id}: ${e.message}`));
|
|
57708
|
+
else await fanout;
|
|
57695
57709
|
publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
|
|
57696
57710
|
if (request) {
|
|
57697
57711
|
const [inbox] = await inboxesFor(publisher, [quote.actor]);
|
|
@@ -58502,7 +58516,8 @@ var Publisher = class {
|
|
|
58502
58516
|
probeFetch = null,
|
|
58503
58517
|
resolveMention = null,
|
|
58504
58518
|
resolveActor = null,
|
|
58505
|
-
clientOrigin = null
|
|
58519
|
+
clientOrigin = null,
|
|
58520
|
+
background = false
|
|
58506
58521
|
}) {
|
|
58507
58522
|
this.config = config;
|
|
58508
58523
|
this.remote = remote;
|
|
@@ -58512,6 +58527,7 @@ var Publisher = class {
|
|
|
58512
58527
|
this.publicKeyPem = publicKeyPem;
|
|
58513
58528
|
this.assertionKey = assertionKey;
|
|
58514
58529
|
this.clientOrigin = clientOrigin;
|
|
58530
|
+
this.background = background;
|
|
58515
58531
|
const publicBase = config.gateway?.frontActor ? config.gateway.frontActor.replace(/ap\/actor\/?$/, "") : null;
|
|
58516
58532
|
this.urls = apUrls2(config.remotePod, config.root, { publicBase });
|
|
58517
58533
|
if (this.urls.toPod && this.remote?.setUrlMap) this.remote.setUrlMap(this.urls.toPod);
|
|
@@ -65361,6 +65377,7 @@ function describeShapeFailure(failure) {
|
|
|
65361
65377
|
// lib/core/intake/index.mjs
|
|
65362
65378
|
var POLL_MS = 2 * 6e4;
|
|
65363
65379
|
var POLL_PUSH_OK_MS = 10 * 6e4;
|
|
65380
|
+
var POLL_IN_PROCESS_MS = 60 * 6e4;
|
|
65364
65381
|
var DRAIN_COOLDOWN_MIN_MS = 2 * 6e4;
|
|
65365
65382
|
var DRAIN_COOLDOWN_MAX_MS = 30 * 6e4;
|
|
65366
65383
|
var DELETE_GAP_MS = 150;
|
|
@@ -65417,13 +65434,15 @@ var Intake = class {
|
|
|
65417
65434
|
// what makes the fallback poll a slow one.
|
|
65418
65435
|
_pollMs() {
|
|
65419
65436
|
if (this.pollSeconds) return this.pollSeconds * 1e3;
|
|
65420
|
-
|
|
65437
|
+
if (this.wsState === "in-process") return POLL_IN_PROCESS_MS;
|
|
65438
|
+
return this.wsState === "open" ? POLL_PUSH_OK_MS : POLL_MS;
|
|
65421
65439
|
}
|
|
65422
65440
|
// `drainNow: false` and `subscribe: false` are for a browser worker the
|
|
65423
65441
|
// browser restarted: it drained moments ago, and a socket would die with it.
|
|
65424
65442
|
async start({ drainNow = true, subscribe: subscribe2 = true } = {}) {
|
|
65425
65443
|
this.stopped = false;
|
|
65426
65444
|
if (drainNow) await this.drain().catch((e) => this.log(`drain: ${e.message}`));
|
|
65445
|
+
if (!this.push) this.wsState = "in-process";
|
|
65427
65446
|
const tick = () => {
|
|
65428
65447
|
this.pollTimer = setTimeout(() => {
|
|
65429
65448
|
this.drain().catch((e) => this.log(`drain: ${e.message}`)).finally(() => {
|
|
@@ -65433,8 +65452,7 @@ var Intake = class {
|
|
|
65433
65452
|
this.pollTimer.unref?.();
|
|
65434
65453
|
};
|
|
65435
65454
|
tick();
|
|
65436
|
-
if (
|
|
65437
|
-
else if (subscribe2) this.subscribe().catch((e) => this.log(`subscribe: ${e.message}`));
|
|
65455
|
+
if (this.push && subscribe2) this.subscribe().catch((e) => this.log(`subscribe: ${e.message}`));
|
|
65438
65456
|
}
|
|
65439
65457
|
stop() {
|
|
65440
65458
|
this.stopped = true;
|
|
@@ -67324,10 +67342,11 @@ var web_push_default = { generateVAPIDKeys, setVapidDetails, sendNotification };
|
|
|
67324
67342
|
// lib/client/webpush.mjs
|
|
67325
67343
|
var Push = class {
|
|
67326
67344
|
constructor({ store, subject, log: log2 = () => {
|
|
67327
|
-
} }) {
|
|
67345
|
+
}, liveKeys = null }) {
|
|
67328
67346
|
this.store = store;
|
|
67329
67347
|
this.subject = subject;
|
|
67330
67348
|
this.log = log2;
|
|
67349
|
+
this.liveKeys = liveKeys;
|
|
67331
67350
|
}
|
|
67332
67351
|
state() {
|
|
67333
67352
|
return this.store.read("webpush.json", { subs: {} });
|
|
@@ -67387,14 +67406,27 @@ var Push = class {
|
|
|
67387
67406
|
}
|
|
67388
67407
|
// Push one notification to every subscription whose alerts allow the type.
|
|
67389
67408
|
// A push service answering 404/410 means the browser dropped the
|
|
67390
|
-
// subscription — it is removed rather than retried forever.
|
|
67409
|
+
// subscription — it is removed rather than retried forever. With
|
|
67410
|
+
// `liveKeys`, every subscription whose sign-in is no longer in force is
|
|
67411
|
+
// forgotten first: an app signed out, expired or pushed off the list hears
|
|
67412
|
+
// nothing more.
|
|
67391
67413
|
async notify(n, payload) {
|
|
67392
67414
|
const st2 = this.state();
|
|
67415
|
+
let dropped = false;
|
|
67416
|
+
const live = this.liveKeys?.() || null;
|
|
67417
|
+
if (live) {
|
|
67418
|
+
for (const key of Object.keys(st2.subs)) if (!live.has(key)) {
|
|
67419
|
+
delete st2.subs[key];
|
|
67420
|
+
dropped = true;
|
|
67421
|
+
}
|
|
67422
|
+
}
|
|
67393
67423
|
const entries = Object.entries(st2.subs);
|
|
67394
|
-
if (!entries.length)
|
|
67424
|
+
if (!entries.length) {
|
|
67425
|
+
if (dropped) this.save(st2);
|
|
67426
|
+
return;
|
|
67427
|
+
}
|
|
67395
67428
|
const { publicKey, privateKey } = this.vapid();
|
|
67396
67429
|
const details = { subject: this.subject(), publicKey, privateKey };
|
|
67397
|
-
let dropped = false;
|
|
67398
67430
|
for (const [key, sub] of entries) {
|
|
67399
67431
|
if (sub.alerts && sub.alerts[n.type] === false) continue;
|
|
67400
67432
|
try {
|
|
@@ -67545,103 +67577,54 @@ function readMultipart(req, limit = 12e6) {
|
|
|
67545
67577
|
|
|
67546
67578
|
// lib/client/masto/oauth.mjs
|
|
67547
67579
|
var TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
67548
|
-
var AUTHZ_WINDOW_MS = 6e4;
|
|
67549
|
-
var AUTHZ_MAX_ATTEMPTS = 5;
|
|
67550
67580
|
var CODE_TTL_MS = 5 * 6e4;
|
|
67551
67581
|
var MAX_APPS = 200;
|
|
67582
|
+
var MAX_REDIRECTS = 10;
|
|
67583
|
+
var MAX_REDIRECT_CHARS = 2048;
|
|
67584
|
+
var REGISTRATION_WINDOW_MS = 6e4;
|
|
67585
|
+
var MAX_REGISTRATIONS = 10;
|
|
67552
67586
|
var CLIENT_DOC_TTL_MS = 10 * 6e4;
|
|
67553
67587
|
var CLIENT_DOC_MAX_CACHED = 200;
|
|
67554
67588
|
var CLIENT_DOC_WINDOW_MS = 6e4;
|
|
67555
67589
|
var CLIENT_DOC_MAX_FETCHES = 20;
|
|
67556
67590
|
var CLIENT_DOC_MAX = 64 * 1024;
|
|
67557
|
-
function hashPassword(password) {
|
|
67558
|
-
const salt = node_crypto_default.randomBytes(16);
|
|
67559
|
-
const hash = node_crypto_default.scryptSync(String(password), salt, 32);
|
|
67560
|
-
return { saltHex: salt.toString("hex"), hashHex: hash.toString("hex") };
|
|
67561
|
-
}
|
|
67562
|
-
function checkPassword(rec, password) {
|
|
67563
|
-
try {
|
|
67564
|
-
const hash = node_crypto_default.scryptSync(String(password), Buffer.from(rec.saltHex, "hex"), 32);
|
|
67565
|
-
return node_crypto_default.timingSafeEqual(hash, Buffer.from(rec.hashHex, "hex"));
|
|
67566
|
-
} catch {
|
|
67567
|
-
return false;
|
|
67568
|
-
}
|
|
67569
|
-
}
|
|
67570
67591
|
var escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
67571
67592
|
var parseRedirects = (v) => (Array.isArray(v) ? v : String(v || "").split(/\s+/)).map((s) => s.trim()).filter(Boolean);
|
|
67572
|
-
|
|
67573
|
-
|
|
67574
|
-
|
|
67575
|
-
|
|
67576
|
-
|
|
67577
|
-
|
|
67578
|
-
|
|
67579
|
-
|
|
67580
|
-
|
|
67581
|
-
asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and you will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
|
|
67593
|
+
var ownAddress = (api) => {
|
|
67594
|
+
const handle7 = api.store.getConfig()?.handle;
|
|
67595
|
+
return handle7 && api.host ? `@${handle7}@${api.host}` : null;
|
|
67596
|
+
};
|
|
67597
|
+
function sendPodSigninPage(res, client, webId, mount = "", address = null) {
|
|
67598
|
+
let where = "";
|
|
67599
|
+
try {
|
|
67600
|
+
where = client?.redirect ? new URL(client.redirect).host : "";
|
|
67601
|
+
} catch {
|
|
67582
67602
|
}
|
|
67603
|
+
const app = escapeHtml(client?.name || where || "this app");
|
|
67604
|
+
const question = `Allow ${app} to access your ${address ? `${escapeHtml(address)} ` : ""}account?`;
|
|
67583
67605
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
|
|
67584
67606
|
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67585
|
-
<title
|
|
67607
|
+
<title>${question}</title>
|
|
67586
67608
|
<style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
|
|
67587
67609
|
body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
|
|
67588
67610
|
h1{color:var(--heading);font-size:1.5rem}
|
|
67589
|
-
|
|
67611
|
+
h1,p{overflow-wrap:anywhere}
|
|
67590
67612
|
button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
|
|
67591
67613
|
button[hidden]{display:none}
|
|
67614
|
+
#cancel{background:transparent;color:inherit;border:2px solid currentColor;margin-left:.6rem}
|
|
67592
67615
|
#signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
|
|
67593
|
-
|
|
67616
|
+
#signin-status:empty{min-height:0;padding:0;border:0;margin:0}
|
|
67617
|
+
@media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#4a7a40}}</style></head><body>
|
|
67594
67618
|
<main>
|
|
67595
|
-
<h1
|
|
67596
|
-
${
|
|
67597
|
-
<p
|
|
67598
|
-
<p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
|
|
67619
|
+
<h1>${question}</h1>
|
|
67620
|
+
<a id="webid" href="${escapeHtml(webId)}" hidden></a>
|
|
67621
|
+
<p><button type="button" id="signin" hidden>Allow</button><button type="button" id="cancel" hidden>Cancel</button></p>
|
|
67599
67622
|
<p id="signin-status" role="status" aria-live="polite"></p>
|
|
67600
67623
|
</main>
|
|
67601
67624
|
<script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"><\/script>
|
|
67602
67625
|
</body></html>`);
|
|
67603
67626
|
return true;
|
|
67604
67627
|
}
|
|
67605
|
-
function sendLoginForm(res, params, error2 = "", client = null, status2 = null, headers = {}) {
|
|
67606
|
-
const hidden = [...params.entries()].filter(([k]) => k !== "password").map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join("\n");
|
|
67607
|
-
let asking = "<p>Enter the agent password to authorize this client.</p>";
|
|
67608
|
-
if (client && (client.name || client.redirect)) {
|
|
67609
|
-
let where = "";
|
|
67610
|
-
try {
|
|
67611
|
-
where = client.redirect ? new URL(client.redirect).host : "";
|
|
67612
|
-
} catch {
|
|
67613
|
-
}
|
|
67614
|
-
const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
|
|
67615
|
-
asking = `<p><strong>${who}</strong> is asking to access your account${where ? `, sending the authorization to <code>${escapeHtml(where)}</code>` : ""}.</p><p>Scope: <code>${escapeHtml(client.scope || "read")}</code>. Enter the agent password to allow it.</p>`;
|
|
67616
|
-
}
|
|
67617
|
-
res.writeHead(
|
|
67618
|
-
status2 || (error2 ? 401 : 200),
|
|
67619
|
-
{ "content-type": "text/html; charset=utf-8", ...headers }
|
|
67620
|
-
);
|
|
67621
|
-
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67622
|
-
<title>FediPod \u2014 authorize</title>
|
|
67623
|
-
<style>:root{color-scheme:light dark;font-size:125%;--heading:#1a4f8a}
|
|
67624
|
-
body{font:1rem system-ui,sans-serif;max-width:22rem;margin:15vh auto;padding:0 1rem}
|
|
67625
|
-
h1{color:var(--heading)}
|
|
67626
|
-
code{word-break:break-all}
|
|
67627
|
-
@media (prefers-color-scheme:dark){:root{--heading:#7fb3e8}}
|
|
67628
|
-
input,button{font:inherit;width:100%;padding:.5rem;margin:.3rem 0;box-sizing:border-box}
|
|
67629
|
-
.err{color:#b00020}
|
|
67630
|
-
@media (prefers-color-scheme:dark){.err{color:#ff8a8a}}</style></head><body>
|
|
67631
|
-
<main>
|
|
67632
|
-
<h1>FediPod</h1>
|
|
67633
|
-
${asking}
|
|
67634
|
-
${error2 ? `<p class="err" id="login-err" role="alert">${escapeHtml(error2)}</p>` : ""}
|
|
67635
|
-
<form method="POST" action="/oauth/authorize">
|
|
67636
|
-
${hidden}
|
|
67637
|
-
<label for="password">Agent password</label>
|
|
67638
|
-
<input type="password" id="password" name="password" autofocus autocomplete="current-password"
|
|
67639
|
-
${error2 ? 'aria-invalid="true" aria-describedby="login-err"' : ""}>
|
|
67640
|
-
<button type="submit">Authorize</button>
|
|
67641
|
-
</form>
|
|
67642
|
-
</main></body></html>`);
|
|
67643
|
-
return true;
|
|
67644
|
-
}
|
|
67645
67628
|
function scopeFor(method, pathname) {
|
|
67646
67629
|
if (/^\/api\/v\d\/push\//u.test(pathname)) return "push";
|
|
67647
67630
|
const relationship2 = /^\/api\/v1\/(accounts\/[a-f0-9]+\/(follow|unfollow|block|unblock|mute|unmute|remove_from_followers)|follow_requests\/)/u;
|
|
@@ -67780,12 +67763,43 @@ function registerApp(api, { name, website, redirectUris, scopes }) {
|
|
|
67780
67763
|
name: String(name || "client").slice(0, 200),
|
|
67781
67764
|
website: String(website || "").slice(0, 500),
|
|
67782
67765
|
redirectUris,
|
|
67783
|
-
scopes: String(scopes || "read"),
|
|
67784
|
-
createdAt: Date.now()
|
|
67766
|
+
scopes: String(scopes || "read").slice(0, 500),
|
|
67767
|
+
createdAt: Date.now(),
|
|
67768
|
+
// Set when the owner first lets it sign in (touchApp): a client nobody
|
|
67769
|
+
// has used is the first to go when the list is full.
|
|
67770
|
+
usedAt: null
|
|
67785
67771
|
};
|
|
67786
|
-
|
|
67772
|
+
const kept = api.apps();
|
|
67773
|
+
while (kept.length >= MAX_APPS) kept.splice(kept.indexOf(leastUsed(kept)), 1);
|
|
67774
|
+
api.store.write("oauth-apps.json", [...kept, app]);
|
|
67787
67775
|
return app;
|
|
67788
67776
|
}
|
|
67777
|
+
function leastUsed(list3) {
|
|
67778
|
+
const never = list3.filter((a) => a.usedAt === null);
|
|
67779
|
+
const when = (a) => a.usedAt ?? a.createdAt ?? 0;
|
|
67780
|
+
return (never.length ? never : list3).reduce((a, b) => when(b) < when(a) ? b : a);
|
|
67781
|
+
}
|
|
67782
|
+
function touchApp(api, clientId) {
|
|
67783
|
+
const all = api.apps();
|
|
67784
|
+
const app = all.find((a) => a.clientId === clientId);
|
|
67785
|
+
if (!app) return;
|
|
67786
|
+
app.usedAt = Date.now();
|
|
67787
|
+
api.store.write("oauth-apps.json", all);
|
|
67788
|
+
}
|
|
67789
|
+
function redirectsProblem(raw) {
|
|
67790
|
+
if (Array.isArray(raw) && raw.some((v) => typeof v !== "string")) return "redirect_uris must be text";
|
|
67791
|
+
const list3 = parseRedirects(raw);
|
|
67792
|
+
if (list3.length > MAX_REDIRECTS) return `at most ${MAX_REDIRECTS} redirect_uris`;
|
|
67793
|
+
if (list3.some((u) => u.length > MAX_REDIRECT_CHARS)) return `a redirect_uri is at most ${MAX_REDIRECT_CHARS} characters`;
|
|
67794
|
+
return null;
|
|
67795
|
+
}
|
|
67796
|
+
function registrationLimited(api) {
|
|
67797
|
+
const now = Date.now();
|
|
67798
|
+
api.appRegistrations = (api.appRegistrations || []).filter((t) => now - t < REGISTRATION_WINDOW_MS);
|
|
67799
|
+
if (api.appRegistrations.length >= MAX_REGISTRATIONS) return true;
|
|
67800
|
+
api.appRegistrations.push(now);
|
|
67801
|
+
return false;
|
|
67802
|
+
}
|
|
67789
67803
|
function mintCode(api, { clientId, redirectUri, scope, challenge = null, challengeMethod = null }) {
|
|
67790
67804
|
const code = node_crypto_default.randomBytes(24).toString("hex");
|
|
67791
67805
|
const now = Date.now();
|
|
@@ -67804,6 +67818,10 @@ function mintCode(api, { clientId, redirectUri, scope, challenge = null, challen
|
|
|
67804
67818
|
}].slice(-50));
|
|
67805
67819
|
return code;
|
|
67806
67820
|
}
|
|
67821
|
+
function peekCode(api, code) {
|
|
67822
|
+
const now = Date.now();
|
|
67823
|
+
return api.store.read("oauth-codes.json", []).find((c) => c.code === code && now - c.createdAt < CODE_TTL_MS) || null;
|
|
67824
|
+
}
|
|
67807
67825
|
function consumeCode(api, code) {
|
|
67808
67826
|
const now = Date.now();
|
|
67809
67827
|
const all = api.store.read("oauth-codes.json", []);
|
|
@@ -67828,22 +67846,27 @@ function redirectAllowed(api, redirect) {
|
|
|
67828
67846
|
try {
|
|
67829
67847
|
const u = new URL(redirect);
|
|
67830
67848
|
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
67831
|
-
|
|
67849
|
+
if (!api.allowed.has(u.host.toLowerCase())) return false;
|
|
67850
|
+
const mount = api.mount || "";
|
|
67851
|
+
return !mount || u.pathname === mount || u.pathname.startsWith(`${mount}/`);
|
|
67832
67852
|
} catch {
|
|
67833
67853
|
return false;
|
|
67834
67854
|
}
|
|
67835
67855
|
}
|
|
67836
|
-
function rateLimited(api) {
|
|
67837
|
-
const now = Date.now();
|
|
67838
|
-
api.authzAttempts = api.authzAttempts.filter((t) => now - t < AUTHZ_WINDOW_MS);
|
|
67839
|
-
if (api.authzAttempts.length >= AUTHZ_MAX_ATTEMPTS) return true;
|
|
67840
|
-
api.authzAttempts.push(now);
|
|
67841
|
-
return false;
|
|
67842
|
-
}
|
|
67843
67856
|
async function handle(api, ctx) {
|
|
67844
67857
|
const { req, res, pathname, url, send } = ctx;
|
|
67845
67858
|
if (pathname === "/api/v1/apps" && req.method === "POST") {
|
|
67846
67859
|
const body = await readBody2(req);
|
|
67860
|
+
const problem = redirectsProblem(body.redirect_uris);
|
|
67861
|
+
if (problem) return send(422, { error: problem });
|
|
67862
|
+
if (registrationLimited(api)) {
|
|
67863
|
+
api.log("app registration refused: too many this minute");
|
|
67864
|
+
return send(
|
|
67865
|
+
429,
|
|
67866
|
+
{ error: "too many apps registered just now \u2014 try again in a minute" },
|
|
67867
|
+
{ "retry-after": String(REGISTRATION_WINDOW_MS / 1e3) }
|
|
67868
|
+
);
|
|
67869
|
+
}
|
|
67847
67870
|
const redirectUris = parseRedirects(body.redirect_uris);
|
|
67848
67871
|
const app = api.registerApp({
|
|
67849
67872
|
name: body.client_name,
|
|
@@ -67862,7 +67885,6 @@ async function handle(api, ctx) {
|
|
|
67862
67885
|
});
|
|
67863
67886
|
}
|
|
67864
67887
|
if (pathname === "/oauth/authorize" && (req.method === "GET" || req.method === "POST")) {
|
|
67865
|
-
const pw = api.store.getConfig()?.uiPassword;
|
|
67866
67888
|
let params = url.searchParams;
|
|
67867
67889
|
let body = null;
|
|
67868
67890
|
if (req.method === "POST") {
|
|
@@ -67878,7 +67900,7 @@ async function handle(api, ctx) {
|
|
|
67878
67900
|
}
|
|
67879
67901
|
const ownerWebId = api.ownerWebId?.() || null;
|
|
67880
67902
|
if (req.method === "GET" && !params.get("client_id") && params.get("code") && params.get("state") && ownerWebId) {
|
|
67881
|
-
return sendPodSigninPage(res, null, ownerWebId, api.mount || "");
|
|
67903
|
+
return sendPodSigninPage(res, null, ownerWebId, api.mount || "", ownAddress(api));
|
|
67882
67904
|
}
|
|
67883
67905
|
const app = api.findApp(params.get("client_id") || "");
|
|
67884
67906
|
const doc = app ? null : await api.resolveClientDocument(params.get("client_id") || "");
|
|
@@ -67888,11 +67910,15 @@ async function handle(api, ctx) {
|
|
|
67888
67910
|
return send(403, { error: "a cross-site navigation may not authorize a client" });
|
|
67889
67911
|
}
|
|
67890
67912
|
const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
|
|
67891
|
-
|
|
67892
|
-
|
|
67893
|
-
|
|
67913
|
+
const challengeMethod = params.get("code_challenge_method");
|
|
67914
|
+
if (params.get("code_challenge") && challengeMethod && challengeMethod !== "S256" && challengeMethod !== "plain") {
|
|
67915
|
+
return send(400, { error: "code_challenge_method must be S256 or plain" });
|
|
67916
|
+
}
|
|
67917
|
+
if (external && !proved && !api.redirectAllowed(redirect)) {
|
|
67918
|
+
if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "", ownAddress(api));
|
|
67919
|
+
api.log(`authorize refused: "${redirect}" is not an address of this agent, and nothing approved it`);
|
|
67894
67920
|
return send(403, {
|
|
67895
|
-
error: "this client asks to be sent somewhere other than this
|
|
67921
|
+
error: "this client asks to be sent somewhere other than this account; approve it by signing in at your pod"
|
|
67896
67922
|
});
|
|
67897
67923
|
}
|
|
67898
67924
|
if (app) {
|
|
@@ -67913,28 +67939,11 @@ async function handle(api, ctx) {
|
|
|
67913
67939
|
api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
|
|
67914
67940
|
return send(400, { error: "redirect_uri must be an address of this agent" });
|
|
67915
67941
|
}
|
|
67916
|
-
if (
|
|
67917
|
-
if (api.
|
|
67918
|
-
|
|
67919
|
-
return sendLoginForm(
|
|
67920
|
-
res,
|
|
67921
|
-
params,
|
|
67922
|
-
"too many attempts \u2014 wait a minute",
|
|
67923
|
-
client,
|
|
67924
|
-
429,
|
|
67925
|
-
{ "retry-after": String(Math.ceil(AUTHZ_WINDOW_MS / 1e3)) }
|
|
67926
|
-
);
|
|
67927
|
-
}
|
|
67928
|
-
if (!pw || !checkPassword(pw, body.password || "")) {
|
|
67929
|
-
return sendLoginForm(res, params, "wrong password \u2014 try again", client);
|
|
67930
|
-
}
|
|
67931
|
-
} else if (pw && !proved) {
|
|
67932
|
-
return sendLoginForm(res, params, "", client);
|
|
67933
|
-
} else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
|
|
67934
|
-
if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
|
|
67935
|
-
api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
|
|
67942
|
+
if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
|
|
67943
|
+
if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "", ownAddress(api));
|
|
67944
|
+
api.log(`authorize refused: not approved at the pod, and "${req.headers.host}" is not this machine`);
|
|
67936
67945
|
return send(403, {
|
|
67937
|
-
error: api.embedded ? "Open your account's management page first, then sign in from there." : "Signing in from another machine
|
|
67946
|
+
error: api.embedded ? "Open your account's management page first, then sign in from there." : "Signing in from another machine goes through your pod: sign in there to approve this app."
|
|
67938
67947
|
});
|
|
67939
67948
|
}
|
|
67940
67949
|
const code = external ? api.mintCode({
|
|
@@ -67944,6 +67953,7 @@ async function handle(api, ctx) {
|
|
|
67944
67953
|
challenge: params.get("code_challenge") || null,
|
|
67945
67954
|
challengeMethod: params.get("code_challenge_method") || null
|
|
67946
67955
|
}) : api.mintToken(client.scope);
|
|
67956
|
+
if (app) touchApp(api, app.clientId);
|
|
67947
67957
|
if (!redirect || redirect === "urn:ietf:wg:oauth:2.0:oob") return send(200, { code });
|
|
67948
67958
|
const target = new URL(redirect);
|
|
67949
67959
|
target.searchParams.set("code", code);
|
|
@@ -67957,7 +67967,7 @@ async function handle(api, ctx) {
|
|
|
67957
67967
|
const body = await readBody2(req);
|
|
67958
67968
|
const app = api.findApp(body.client_id || "");
|
|
67959
67969
|
if (!app && body.code_verifier && /^https:\/\//iu.test(String(body.client_id || ""))) {
|
|
67960
|
-
const rec = api.
|
|
67970
|
+
const rec = api.peekCode(body.code || "");
|
|
67961
67971
|
if (!rec || rec.clientId !== body.client_id || body.redirect_uri && rec.redirectUri !== body.redirect_uri) {
|
|
67962
67972
|
api.log("token refused: code is not a live authorization for that client document");
|
|
67963
67973
|
return send(400, { error: "invalid_grant" });
|
|
@@ -67966,6 +67976,7 @@ async function handle(api, ctx) {
|
|
|
67966
67976
|
api.log("token refused: the verifier does not answer the challenge this code was made with");
|
|
67967
67977
|
return send(400, { error: "invalid_grant" });
|
|
67968
67978
|
}
|
|
67979
|
+
if (!api.consumeCode(body.code || "")) return send(400, { error: "invalid_grant" });
|
|
67969
67980
|
return send(200, {
|
|
67970
67981
|
access_token: api.mintToken(rec.scope || "read"),
|
|
67971
67982
|
token_type: "Bearer",
|
|
@@ -67975,7 +67986,7 @@ async function handle(api, ctx) {
|
|
|
67975
67986
|
});
|
|
67976
67987
|
}
|
|
67977
67988
|
if (app && body.code_verifier) {
|
|
67978
|
-
const rec = api.
|
|
67989
|
+
const rec = api.peekCode(body.code || "");
|
|
67979
67990
|
if (!rec || rec.clientId !== app.clientId || body.redirect_uri && rec.redirectUri !== body.redirect_uri) {
|
|
67980
67991
|
api.log("token refused: code is not a live authorization for this client");
|
|
67981
67992
|
return send(400, { error: "invalid_grant" });
|
|
@@ -67984,6 +67995,7 @@ async function handle(api, ctx) {
|
|
|
67984
67995
|
api.log("token refused: the verifier does not answer the challenge this code was made with");
|
|
67985
67996
|
return send(400, { error: "invalid_grant" });
|
|
67986
67997
|
}
|
|
67998
|
+
if (!api.consumeCode(body.code || "")) return send(400, { error: "invalid_grant" });
|
|
67987
67999
|
return send(200, {
|
|
67988
68000
|
access_token: api.mintToken(rec.scope || "read"),
|
|
67989
68001
|
token_type: "Bearer",
|
|
@@ -68000,7 +68012,7 @@ async function handle(api, ctx) {
|
|
|
68000
68012
|
api.log("token refused: client secret mismatch");
|
|
68001
68013
|
return send(401, { error: "invalid_client" });
|
|
68002
68014
|
}
|
|
68003
|
-
const rec = api.
|
|
68015
|
+
const rec = api.peekCode(body.code || "");
|
|
68004
68016
|
if (!rec || rec.clientId !== app.clientId || body.redirect_uri && rec.redirectUri !== body.redirect_uri) {
|
|
68005
68017
|
api.log("token refused: code is not a live authorization for this client");
|
|
68006
68018
|
return send(400, { error: "invalid_grant" });
|
|
@@ -68009,6 +68021,7 @@ async function handle(api, ctx) {
|
|
|
68009
68021
|
api.log("token refused: this code was made with a challenge and the verifier does not answer it");
|
|
68010
68022
|
return send(400, { error: "invalid_grant" });
|
|
68011
68023
|
}
|
|
68024
|
+
if (!api.consumeCode(body.code || "")) return send(400, { error: "invalid_grant" });
|
|
68012
68025
|
return send(200, {
|
|
68013
68026
|
access_token: api.mintToken(rec.scope || "read"),
|
|
68014
68027
|
token_type: "Bearer",
|
|
@@ -68039,6 +68052,7 @@ async function handle(api, ctx) {
|
|
|
68039
68052
|
if (gone) {
|
|
68040
68053
|
const kept = api.tokenRecords().filter((r) => r.token !== gone);
|
|
68041
68054
|
api.store.write("masto-tokens.json", kept);
|
|
68055
|
+
if (api.push.get(gone)) api.push.drop(gone);
|
|
68042
68056
|
api.log("client token revoked");
|
|
68043
68057
|
}
|
|
68044
68058
|
return send(200, {});
|
|
@@ -68878,6 +68892,11 @@ init_node_crypto();
|
|
|
68878
68892
|
var write6 = (pod, url, bytes, contentType) => pod.put(url, bytes, contentType);
|
|
68879
68893
|
|
|
68880
68894
|
// lib/client/masto/accounts.mjs
|
|
68895
|
+
var PROFILE_IMAGE_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "image/avif": "avif" };
|
|
68896
|
+
var profileImageType = (f) => {
|
|
68897
|
+
const t = String(f?.contentType || "").split(";")[0].trim().toLowerCase();
|
|
68898
|
+
return PROFILE_IMAGE_EXT[t] ? t : null;
|
|
68899
|
+
};
|
|
68881
68900
|
async function handle3(api, ctx) {
|
|
68882
68901
|
const { req, res, pathname, url, send } = ctx;
|
|
68883
68902
|
if (pathname === "/api/v1/accounts/verify_credentials") {
|
|
@@ -68902,12 +68921,17 @@ async function handle3(api, ctx) {
|
|
|
68902
68921
|
if (ct.includes("multipart/form-data")) ({ fields: form, files } = await readMultipart(req));
|
|
68903
68922
|
else form = await readBody2(req);
|
|
68904
68923
|
const cfg = { ...api.store.getConfig() };
|
|
68924
|
+
for (const f of [files.avatar, files.header]) {
|
|
68925
|
+
if (f?.data?.length && !profileImageType(f)) {
|
|
68926
|
+
return send(422, { error: "a profile picture or header must be a PNG, JPEG, GIF, WebP or AVIF image" });
|
|
68927
|
+
}
|
|
68928
|
+
}
|
|
68905
68929
|
const putImage = async (f) => {
|
|
68906
|
-
const
|
|
68907
|
-
const slug = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex") + "." +
|
|
68930
|
+
const type = profileImageType(f);
|
|
68931
|
+
const slug = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex") + "." + PROFILE_IMAGE_EXT[type];
|
|
68908
68932
|
const url2 = api.urls.media + slug;
|
|
68909
68933
|
await api.agent.publisher.ensureMediaContainer();
|
|
68910
|
-
await write6(api.agent.remote, url2, f.data,
|
|
68934
|
+
await write6(api.agent.remote, url2, f.data, type);
|
|
68911
68935
|
return url2;
|
|
68912
68936
|
};
|
|
68913
68937
|
if ("display_name" in form) cfg.name = String(form.display_name).trim() || cfg.handle;
|
|
@@ -69886,7 +69910,6 @@ var MastoApi = class _MastoApi {
|
|
|
69886
69910
|
this.streaming = streaming;
|
|
69887
69911
|
this.webPush = webPush;
|
|
69888
69912
|
this.scheduling = scheduling;
|
|
69889
|
-
this.authzAttempts = [];
|
|
69890
69913
|
}
|
|
69891
69914
|
get store() {
|
|
69892
69915
|
return this.agent.store;
|
|
@@ -69898,7 +69921,8 @@ var MastoApi = class _MastoApi {
|
|
|
69898
69921
|
this._push ||= new Push({
|
|
69899
69922
|
store: this.store,
|
|
69900
69923
|
subject: () => this.urls?.actor || "https://localhost/",
|
|
69901
|
-
log: this.log
|
|
69924
|
+
log: this.log,
|
|
69925
|
+
liveKeys: () => new Set(this.tokens().map((t) => this._push.keyOf(t)))
|
|
69902
69926
|
});
|
|
69903
69927
|
return this._push;
|
|
69904
69928
|
}
|
|
@@ -69997,6 +70021,9 @@ var MastoApi = class _MastoApi {
|
|
|
69997
70021
|
consumeCode(...a) {
|
|
69998
70022
|
return consumeCode(this, ...a);
|
|
69999
70023
|
}
|
|
70024
|
+
peekCode(...a) {
|
|
70025
|
+
return peekCode(this, ...a);
|
|
70026
|
+
}
|
|
70000
70027
|
tokenOf(...a) {
|
|
70001
70028
|
return tokenOf(this, ...a);
|
|
70002
70029
|
}
|
|
@@ -70006,9 +70033,6 @@ var MastoApi = class _MastoApi {
|
|
|
70006
70033
|
redirectAllowed(...a) {
|
|
70007
70034
|
return redirectAllowed(this, ...a);
|
|
70008
70035
|
}
|
|
70009
|
-
rateLimited(...a) {
|
|
70010
|
-
return rateLimited(this, ...a);
|
|
70011
|
-
}
|
|
70012
70036
|
// render.mjs
|
|
70013
70037
|
selfAccount(...a) {
|
|
70014
70038
|
return selfAccount(this, ...a);
|
|
@@ -70106,6 +70130,20 @@ var DEFAULTS2 = {
|
|
|
70106
70130
|
intervalMin: 15
|
|
70107
70131
|
};
|
|
70108
70132
|
var PER_TAG = 20;
|
|
70133
|
+
var SHARED_TTL_MS = 12 * 6e4;
|
|
70134
|
+
var sharedTimelines = /* @__PURE__ */ new Map();
|
|
70135
|
+
function timelineOnce(key, load2) {
|
|
70136
|
+
const now = Date.now();
|
|
70137
|
+
const held = sharedTimelines.get(key);
|
|
70138
|
+
if (held && now - held.at < SHARED_TTL_MS) return held.answer;
|
|
70139
|
+
const answer = load2().catch((e) => {
|
|
70140
|
+
sharedTimelines.delete(key);
|
|
70141
|
+
throw e;
|
|
70142
|
+
});
|
|
70143
|
+
sharedTimelines.set(key, { at: now, answer });
|
|
70144
|
+
if (sharedTimelines.size > 500) sharedTimelines.delete(sharedTimelines.keys().next().value);
|
|
70145
|
+
return answer;
|
|
70146
|
+
}
|
|
70109
70147
|
var MAX_NEW_PER_SWEEP = 8;
|
|
70110
70148
|
var MAX_NEW_PER_TAG = 2;
|
|
70111
70149
|
var TAGS_PER_SWEEP = 4;
|
|
@@ -70197,12 +70235,17 @@ var TagFeed = class {
|
|
|
70197
70235
|
try {
|
|
70198
70236
|
const { safeFetch: safeFetch2, retryAfterMs: retryAfterMs3, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
|
|
70199
70237
|
const url = `${instance}/api/v1/timelines/tag/${encodeURIComponent(tag)}?limit=${PER_TAG}`;
|
|
70200
|
-
const
|
|
70201
|
-
|
|
70202
|
-
|
|
70238
|
+
const load2 = async () => {
|
|
70239
|
+
const res = this.fetcher === globalThis.fetch ? await safeFetch2(url, { headers: { accept: "application/json" } }) : await this.fetcher(url, { headers: { accept: "application/json" } });
|
|
70240
|
+
if (res.status >= 400) return { status: res.status, retryAfter: retryAfterMs3(res), list: null };
|
|
70241
|
+
return { status: res.status, list: JSON.parse(await readCapped3(res, MAX_TIMELINE_BYTES)) };
|
|
70242
|
+
};
|
|
70243
|
+
const got = this.fetcher === globalThis.fetch ? await timelineOnce(`${instance}|${tag}`, load2) : await load2();
|
|
70244
|
+
if (got.status >= 400) {
|
|
70245
|
+
this._backOff(got.status, got.retryAfter);
|
|
70203
70246
|
return;
|
|
70204
70247
|
}
|
|
70205
|
-
list3 =
|
|
70248
|
+
list3 = got.list;
|
|
70206
70249
|
} catch (e) {
|
|
70207
70250
|
this.log(`tagfeed #${tag}: ${e.message}`);
|
|
70208
70251
|
this._backOff(0, null);
|
|
@@ -70480,7 +70523,7 @@ async function attachProof(activity, { privateKey, verificationMethod, created =
|
|
|
70480
70523
|
|
|
70481
70524
|
// lib/core/deliver.mjs
|
|
70482
70525
|
init_safefetch();
|
|
70483
|
-
var
|
|
70526
|
+
var MAX_REDIRECTS2 = 3;
|
|
70484
70527
|
async function withQueryInTarget(signed, privateKey) {
|
|
70485
70528
|
const url = new URL(signed.url);
|
|
70486
70529
|
if (!url.search) return signed;
|
|
@@ -70537,7 +70580,8 @@ var Deliverer = class {
|
|
|
70537
70580
|
log: log2 = console.log,
|
|
70538
70581
|
passive = false,
|
|
70539
70582
|
defer = false,
|
|
70540
|
-
onGone = null
|
|
70583
|
+
onGone = null,
|
|
70584
|
+
parallel = 1
|
|
70541
70585
|
}) {
|
|
70542
70586
|
this.store = store;
|
|
70543
70587
|
this.defer = defer;
|
|
@@ -70548,7 +70592,8 @@ var Deliverer = class {
|
|
|
70548
70592
|
this.edPrivate = edPrivate;
|
|
70549
70593
|
this.proofKeyId = proofKeyId;
|
|
70550
70594
|
this.log = log2;
|
|
70551
|
-
this.batchSize =
|
|
70595
|
+
this.batchSize = parallel;
|
|
70596
|
+
this.parallel = parallel;
|
|
70552
70597
|
if (!passive) this.startQueue();
|
|
70553
70598
|
}
|
|
70554
70599
|
startQueue() {
|
|
@@ -70568,7 +70613,7 @@ var Deliverer = class {
|
|
|
70568
70613
|
signal: init.signal || AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
70569
70614
|
headers: { "user-agent": USER_AGENT, ...init.headers || {} }
|
|
70570
70615
|
};
|
|
70571
|
-
for (let hop = 0; hop <=
|
|
70616
|
+
for (let hop = 0; hop <= MAX_REDIRECTS2; hop++) {
|
|
70572
70617
|
const dispatcher = await pinnedFor(current);
|
|
70573
70618
|
const req = new Request(current, withUa);
|
|
70574
70619
|
const signed = await withQueryInTarget(
|
|
@@ -70629,6 +70674,9 @@ var Deliverer = class {
|
|
|
70629
70674
|
// with the error deliverNow would have thrown. Here one at a time; the
|
|
70630
70675
|
// relay deliverer sends the whole list in one call.
|
|
70631
70676
|
async deliverManyNow(targets) {
|
|
70677
|
+
if (this.parallel > 1) {
|
|
70678
|
+
return Promise.all(targets.map((t) => this.deliverNow(t.inbox, t.activity).then(() => ({ ok: true }), (error2) => ({ error: error2 }))));
|
|
70679
|
+
}
|
|
70632
70680
|
const out = [];
|
|
70633
70681
|
for (const t of targets) {
|
|
70634
70682
|
try {
|
|
@@ -71454,7 +71502,6 @@ var AdminFacade = class {
|
|
|
71454
71502
|
fields: cfg.fields || [],
|
|
71455
71503
|
aliases: cfg.aliases || [],
|
|
71456
71504
|
autoAcceptFollows: cfg.autoAcceptFollows !== false,
|
|
71457
|
-
hasUiPassword: !!cfg.uiPassword,
|
|
71458
71505
|
quiescedAt: cfg.quiescedAt || null,
|
|
71459
71506
|
movedTo: cfg.movedTo || null,
|
|
71460
71507
|
mode: a.status().mode,
|
|
@@ -71570,10 +71617,6 @@ var AdminFacade = class {
|
|
|
71570
71617
|
cfg.fields = (Array.isArray(body.fields) ? body.fields : []).filter((f) => f?.name?.trim()).map((f) => ({ name: String(f.name).trim(), value: String(f.value ?? "").trim() }));
|
|
71571
71618
|
}
|
|
71572
71619
|
if ("autoAcceptFollows" in body) cfg.autoAcceptFollows = !!body.autoAcceptFollows;
|
|
71573
|
-
if ("password" in body) {
|
|
71574
|
-
if (body.password) cfg.uiPassword = hashPassword(body.password);
|
|
71575
|
-
else delete cfg.uiPassword;
|
|
71576
|
-
}
|
|
71577
71620
|
const republish = WIRE_CONFIG.some((k) => k in body);
|
|
71578
71621
|
a.store.setConfig(cfg);
|
|
71579
71622
|
if ("autoAcceptFollows" in body && a.publisher) a.publisher.config.autoAcceptFollows = cfg.autoAcceptFollows;
|