Changes in / [f549cf5:c34ef26] in Klonkt


Ignore:
Files:
1 deleted
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rf549cf5 rc34ef26  
    283283});
    284284
    285 // ── De poorten van een lezer, op EEN plek (FEP-633c) ─────────────
    286 //
    287 // De inbox-lezing rekende ze inline uit. Nu er meer lezingen zijn die
    288 // dezelfde poorten moeten eerbiedigen (de gesprekken, de geschiedenis), zou
    289 // dat evenveel kopieen worden -- en een poort die op een van die plekken
    290 // vergeten wordt, levert stil iets uit dat dicht hoorde te staan.
    291 function leesPoorten(site) {
    292   const isWard = (() => { try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; } })();
    293   const embeds = Guardianship.externalEmbedsAllowed(site.external_embeds, isWard);
    294   const gate = (col) => Guardianship.wardGateAllowed(site[col], isWard);
    295   const emoji = gate('gate_custom_emoji');
    296   return {
    297     isWard,
    298     embedsAllowed: embeds,
    299     playbackAllowed: embeds && Guardianship.externalPlaybackAllowed(site.external_playback, isWard),
    300     imagesAllowed: gate('gate_images'),
    301     musicAllowed: gate('gate_music'),
    302     quotesAllowed: gate('gate_quote_cards'),
    303     emojiAllowed: emoji,
    304     messagesAllowed: gate('gate_messages'),
    305     composeAllowed: gate('gate_compose'),
    306     repliesAllowed: gate('gate_replies'),
    307     threadsAllowed: gate('external_threads'),
    308     followingAllowed: gate('gate_following'),
    309     // Emoji dicht raakt ook de bylines: de plaatjes in een naam komen net zo
    310     // goed van een vreemde server. De naam zelf blijft, met :shortcode: als tekst.
    311     gateAuthor: (a) => (a && !emoji ? { ...a, emojis: undefined } : a),
    312   };
    313 }
    314 
    315 /** De naam waaronder deze lezer zichzelf herkent in een Mention. */
    316 function eigenHandle(base, slug) {
    317   try { return `@${slug}@${new URL(base).host}`; } catch { return `@${slug}`; }
    318 }
    319 
    320 // ── Een bericht als AS2-item: EEN beschrijving van de kaartvorm ──
    321 //
    322 // Gebruikt door de inbox-lezing en door de gesprekslezingen. Twee keer
    323 // opschrijven is twee vormen die uit de pas kunnen lopen, en dat merk je pas
    324 // als een kaart ergens anders rendert dan waar je keek.
    325 function berichtItem(m, { base, me, myHandle, p }) {
    326   return {
    327     id: `${m.object_uri}#create`,
    328     type: 'Create',
    329     actor: m.actor_uri,
    330     published: AP.isoStamp(m.published || m.created_at),
    331     object: {
    332       id: m.object_uri,
    333       type: 'Note',
    334       attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? p.gateAuthor({
    335         name: m.actor_name || undefined, handle: m.actor_handle || undefined,
    336         icon: m.actor_icon || undefined, url: m.actor_url || undefined,
    337         emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
    338       }) : undefined),
    339       content: AP.stripLeadingMentions(m.content),
    340       url: m.note_url || undefined,
    341       published: AP.isoStamp(m.published || m.created_at),
    342       // Addressed to us and to nobody we know of: the other recipients of a
    343       // note to several people are not ours to see, so we serve what we know.
    344       to: [me],
    345       // The Mention is how the client recognises itself as the addressee and
    346       // groups the note into a conversation. No FEP-e232 link tags here: a
    347       // mention row keeps the resolved quote, not the raw tags.
    348       tag: [{ type: 'Mention', href: me, name: myHandle }, ...(p.emojiAllowed ? (AP.timelineEmojis(m.emoji_json) || []) : [])],
    349       attachment: AP.gateAttachments(AP.timelineAttachments(m.media_json), { images: p.imagesAllowed, audio: p.musicAllowed }),
    350       // FEP-633c: what kind of message this is. The wave is a gentle nudge from
    351       // a guardian; the help request is the buoy. Both render differently.
    352       'shaer:wave': m.wave ? true : undefined,
    353       'shaer:helpRequest': m.help_request ? true : undefined,
    354       quote: p.quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
    355       preview: p.embedsAllowed ? AP.previewObject(m.embed_json, { playback: p.playbackAllowed }) : undefined,
    356     },
    357   };
    358 }
    359 
    360 /** Een eigen verzonden note als AS2-item, zelfde vorm als de inbox-leg. */
    361 function verzondenItem(n, { me, mine }) {
    362   return {
    363     id: `${n.id}#create`,
    364     type: 'Create',
    365     actor: me,
    366     published: n.published,
    367     // The leading mention anchor is addressing, not prose (the DM leg strips
    368     // it the same way); the Mention tags built from the full content stay.
    369     object: {
    370       ...n, content: AP.stripLeadingMentions(n.content),
    371       attributedTo: AP.actorObject(typeof n.attributedTo === 'string' ? n.attributedTo : me, mine),
    372     },
    373   };
    374 }
    375 
    376 // ── Gesprekken: eerst wie, dan pas wat (shaer-frontend-yso) ──────
    377 //
    378 // Twee lezingen naast de bestaande inbox-lezing, niet in de plaats ervan: de
    379 // apps in het veld lezen die nog. /conversations geeft EEN rij per tegenpartij
    380 // -- compleet van vorm, dus de avatarhemel kan niemand kwijtraken doordat een
    381 // ander druk was -- en /messages geeft een gesprek met een cursor, zodat een
    382 // 'load more' eerlijk kan verschijnen in plaats van dat de geschiedenis stil
    383 // ophoudt.
    384 //
    385 // Beide lopen langs dezelfde poorten als de inbox-lezing (leesPoorten) en
    386 // dezelfde kaartvorm (berichtItem/verzondenItem). Messages dicht sluit ook
    387 // hier vreemden en vrienden, maar nooit het guardian-kanaal en nooit de boei.
    388 function gesprekItems(req, auth, refs) {
    389   const base = baseUrl(req);
    390   const P = leesPoorten(auth.site);
    391   const me = AP.actorId(base, auth.site.slug);
    392   const ctx = { base, me, myHandle: eigenHandle(base, auth.site.slug), p: P };
    393   const mine = AP.selfAuthor(base, auth.site);
    394   const guardianUris = (() => { try { return new Set(Guardianship.listGuardians(auth.site.slug).map((g) => g.other_uri)); } catch { return new Set(); } })();
    395 
    396   const binnen = new Map(AP.messageRowsByUri(auth.site.slug, refs.filter((r) => r.richting === 'in').map((r) => r.ref))
    397     .map((m) => [m.object_uri, m]));
    398   const uit = [];
    399   for (const r of refs) {
    400     if (r.richting === 'in') {
    401       const m = binnen.get(r.ref);
    402       if (!m) continue;
    403       if (!(P.messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))) continue;
    404       uit.push(berichtItem(m, ctx));
    405     } else {
    406       const n = AP.getOutboxNote(base, r.ref);
    407       // Je eigen woorden blijven van jou: een dichte messages-poort verbergt
    408       // niet wat je zelf gezegd hebt.
    409       if (n) uit.push(verzondenItem(n, { me, mine }));
    410     }
    411   }
    412   return uit;
    413 }
    414 
    415 router.get('/ap/users/:slug/conversations', (req, res) => {
    416   const auth = OAuth.verifyBearer(req.headers.authorization);
    417   if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
    418   const koppen = AP.conversationHeads(auth.site.slug);
    419   const items = gesprekItems(req, auth, koppen);
    420   AP.sendAP(res, {
    421     '@context': AP.AP_CONTEXT,
    422     id: `${baseUrl(req)}/ap/users/${encodeURIComponent(auth.site.slug)}/conversations`,
    423     type: 'OrderedCollection',
    424     totalItems: items.length,
    425     orderedItems: items,
    426     'shaer:cursor': AP.feedCursor(auth.site.slug),
    427   }, 'private, no-store');
    428 });
    429 
    430 router.get('/ap/users/:slug/messages', (req, res) => {
    431   const auth = OAuth.verifyBearer(req.headers.authorization);
    432   if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
    433   const other = String(req.query.with || '');
    434   if (!/^https?:\/\//i.test(other)) return res.status(400).json({ error: 'with must be an actor URI' });
    435   const uit = AP.conversationHistory(auth.site.slug, other, {
    436     before: req.query.before ? String(req.query.before) : null,
    437     limit: req.query.limit,
    438   });
    439   const items = gesprekItems(req, auth, uit.rijen);
    440   // De paginagrootte reist mee in next: vroeg je om 30, dan hoort de volgende
    441   // pagina er ook 30 te zijn. Zonder dit wordt hij stilletjes de standaard, en
    442   // dan klopt het ritme van een 'load more' niet meer met wat de gebruiker ziet.
    443   const maat = req.query.limit ? `&limit=${encodeURIComponent(String(req.query.limit))}` : '';
    444   const zelf = `${baseUrl(req)}/ap/users/${encodeURIComponent(auth.site.slug)}/messages?with=${encodeURIComponent(other)}`;
    445   AP.sendAP(res, {
    446     '@context': AP.AP_CONTEXT,
    447     id: req.query.before ? `${zelf}${maat}&before=${encodeURIComponent(String(req.query.before))}` : `${zelf}${maat}`,
    448     type: 'OrderedCollectionPage',
    449     partOf: zelf,
    450     orderedItems: items,
    451     // De volgende pagina is de standaardvorm van 'er is meer' (AS2). Ontbreekt
    452     // hij, dan is het gesprek op -- en dat mag de client weten zonder gokken,
    453     // want anders kan een 'load more' niet eerlijk verschijnen.
    454     next: uit.meer && uit.oudste ? `${zelf}${maat}&before=${encodeURIComponent(uit.oudste)}` : undefined,
    455   }, 'private, no-store');
    456 });
    457 
    458285// ── Long-poll (owner only, Robins verzoek 31-7) ───────────────────
    459286// Hold the request until something push-worthy lands for this account, then
     
    587414  // here, at serialisation: a blocked embed is never sent, because an embed the
    588415  // client merely hides has still been delivered to the device.
    589   // De poorten van deze lezer (leesPoorten): een plek waar ze berekend worden,
    590   // zodat de gesprekslezingen dezelfde stand eerbiedigen en niet hun eigen
    591   // kopie krijgen die kan gaan afwijken.
    592   const P = leesPoorten(auth.site);
    593   const {
    594     embedsAllowed, playbackAllowed, imagesAllowed, musicAllowed, quotesAllowed,
    595     emojiAllowed, messagesAllowed, composeAllowed, repliesAllowed, threadsAllowed,
    596     followingAllowed, gateAuthor,
    597   } = P;
    598   // De rechten-lijst hieronder vraagt er nog een paar rechtstreeks op.
    599   const gate = (col) => Guardianship.wardGateAllowed(auth.site[col], P.isWard);
     416  const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
     417  const embedsAllowed = Guardianship.externalEmbedsAllowed(auth.site.external_embeds, isWard);
     418  // The heavier sibling (5.6): may a third party's PLAYER run inside the app,
     419  // and may a link hand the child over to a browser? Both are the guardians'
     420  // call, both default to off for a ward, and both need the preview gate open
     421  // first: you cannot play, or follow, what you may not see. Served here so
     422  // the app knows what it may offer instead of guessing.
     423  const playbackAllowed = embedsAllowed
     424    && Guardianship.externalPlaybackAllowed(auth.site.external_playback, isWard);
     425  // De rest van de familie (shaer-ahy.1, 8-8): zelfde regel, zelfde plek --
     426  // de poort zit bij de serialisatie, wat dicht is wordt nooit geleverd.
     427  const gate = (col) => Guardianship.wardGateAllowed(auth.site[col], isWard);
     428  const imagesAllowed = gate('gate_images');
     429  const musicAllowed = gate('gate_music');
     430  const quotesAllowed = gate('gate_quote_cards');
     431  const emojiAllowed = gate('gate_custom_emoji');
     432  const messagesAllowed = gate('gate_messages');
     433  const composeAllowed = gate('gate_compose');
     434  const repliesAllowed = gate('gate_replies');
     435  const threadsAllowed = gate('external_threads');
     436  // Zelf iemand volgen (shaer-p729). Anders dan de rest betekent dicht hier niet
     437  // "kan niet" maar "moet eerst gevraagd worden": het verzoek gaat naar de
     438  // guardians. Juist dat hoort de app VOORAF te weten, zodat de knop kan zeggen
     439  // dat je het gaat vragen in plaats van te doen alsof het al gelukt is en het
     440  // kind het pas bij het antwoord te laten ontdekken.
     441  const followingAllowed = gate('gate_following');
     442  // Emoji dicht raakt ook de bylines: de plaatjes in een naam komen net zo
     443  // goed van een vreemde server. De naam zelf blijft, met :shortcode: als tekst.
     444  const gateAuthor = (a) => (a && !emojiAllowed ? { ...a, emojis: undefined } : a);
    600445  // ── Standaardvormen naast het dialect (shaer-nmw) ────────────────
    601446  //
     
    676521  // beschermt niemand. De hulpvraag zelf gaat aan de innamekant al altijd voor.
    677522  const guardianUris = (() => { try { return new Set(Guardianship.listGuardians(auth.site.slug).map((g) => g.other_uri)); } catch { return new Set(); } })();
    678   const berichtCtx = { base, me, myHandle, p: P };
    679523  const messages = AP.getDirectMessages(auth.site.slug, 60)
    680524    .filter((m) => messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))
    681     .map((m) => berichtItem(m, berichtCtx));
     525    .map((m) => ({
     526    id: `${m.object_uri}#create`,
     527    type: 'Create',
     528    actor: m.actor_uri,
     529    published: AP.isoStamp(m.published || m.created_at),
     530    object: {
     531      id: m.object_uri,
     532      type: 'Note',
     533      attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? gateAuthor({
     534        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
     535        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
     536        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
     537      }) : undefined),
     538      content: AP.stripLeadingMentions(m.content),
     539      url: m.note_url || undefined,
     540      published: AP.isoStamp(m.published || m.created_at),
     541      // Addressed to us and to nobody we know of: the other recipients of a
     542      // note to several people are not ours to see, so we serve what we know.
     543      to: [me],
     544      // The Mention is how the client recognises itself as the addressee and
     545      // groups the note into a conversation. No FEP-e232 link tags here: a
     546      // mention row keeps the resolved quote, not the raw tags.
     547      tag: [{ type: 'Mention', href: me, name: myHandle }, ...(emojiAllowed ? (AP.timelineEmojis(m.emoji_json) || []) : [])],
     548      attachment: AP.gateAttachments(AP.timelineAttachments(m.media_json), { images: imagesAllowed, audio: musicAllowed }),
     549      // FEP-633c: what kind of message this is. The wave is a gentle nudge from
     550      // a guardian; the help request is the buoy. Both render differently.
     551      'shaer:wave': m.wave ? true : undefined,
     552      'shaer:helpRequest': m.help_request ? true : undefined,
     553      quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
     554      preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined,
     555    },
     556  }));
    682557  // Inbound REPLIES on your own posts: stored as interactions (the web's
    683558  // comment machinery), never as mentions, so this read missed them and a
  • src/services/ActivityPubService.js

    rf549cf5 rc34ef26  
    41914191}
    41924192
    4193 // ── Gesprekken: eerst wie, dan pas wat (shaer-frontend-yso) ──────────
    4194 //
    4195 // De oude lezing gaf de nieuwste 60 berichten over ALLE gesprekken samen. Dat
    4196 // knipt geschiedenis weg zonder dat iemand het merkt, en het is bij DM's veel
    4197 // erger dan bij posts: dat zijn er meer en het zijn kortere berichten, dus een
    4198 // druk gesprek kan de 60 in zijn eentje opeten en de rest uit de lezing duwen.
    4199 // Viel het laatste bericht van iemand erbuiten, dan verdween die persoon
    4200 // helemaal uit Messages -- de avatarhemel plaatst mensen op de leeftijd van hun
    4201 // laatste bericht, dus geen bericht is geen gezicht.
    4202 //
    4203 // Vandaar twee lezingen. Deze geeft EEN rij per tegenpartij, hoe druk iemand
    4204 // ook is, en conversationHistory hieronder geeft het gesprek zelf met een
    4205 // cursor. Wat de client van de hemel nodig heeft -- wie, wanneer, en waarmee --
    4206 // zit in die ene nieuwste note.
    4207 //
    4208 // Een gesprek is hier hetzelfde als in de app: incoming zijn de ap_mentions
    4209 // (die tabel IS de aan ons gerichte post), uitgaand zijn de eigen notes met
    4210 // visibility 'direct'. Een publiek antwoord is geen gesprek en hoort niet als
    4211 // gezicht in de hemel.
    4212 const GESPREK_UNIE = `
    4213   SELECT m.actor_uri AS other, COALESCE(m.published, m.created_at) AS stamp,
    4214          'in' AS richting, m.object_uri AS ref
    4215     FROM ap_mentions m
    4216    WHERE m.slug = @slug AND m.actor_uri IS NOT NULL AND m.actor_uri <> ''
    4217   UNION ALL
    4218   SELECT j.value AS other, o.created_at AS stamp,
    4219          'uit' AS richting, o.id AS ref
    4220     FROM ap_outbox o
    4221     JOIN json_each(COALESCE(NULLIF(o.to_actors, ''), json_array(o.to_actor))) j
    4222    WHERE o.site_slug = @slug AND o.visibility = 'direct'
    4223      AND j.value IS NOT NULL AND j.value <> ''`;
    4224 
    4225 /**
    4226  * Een rij per tegenpartij: zijn nieuwste bericht, nieuwste gesprek eerst.
    4227  *
    4228  * Compleet van vorm -- het aantal rijen is het aantal mensen, niet het aantal
    4229  * berichten -- dus de hemel kan niemand meer kwijtraken doordat een ander druk
    4230  * was. Zonder limiet, en dat mag: dit schaalt met je kring.
    4231  */
    4232 export function conversationHeads(slug) {
    4233   try {
    4234     return db.prepare(`
    4235       SELECT other, stamp, richting, ref FROM (
    4236         SELECT *, ROW_NUMBER() OVER (PARTITION BY other ORDER BY stamp DESC, ref DESC) AS rn
    4237           FROM (${GESPREK_UNIE})
    4238       ) WHERE rn = 1
    4239       ORDER BY stamp DESC, ref DESC`).all({ slug });
    4240   } catch { return []; }
    4241 }
    4242 
    4243 /**
    4244  * Een gesprek, nieuwste eerst, met een cursor.
    4245  *
    4246  * BEIDE KANTEN ONDER EEN LIMIET. In de oude lezing werden jouw kant
    4247  * (getSentNotes) en hun kant apart afgekapt, waardoor een gesprek eenzijdig
    4248  * kon lijken -- alsof iemand nooit geantwoord had. Hier is de limiet er een
    4249  * voor het gesprek als geheel.
    4250  *
    4251  * `before` is de cursor van het OUDSTE bericht dat je al hebt; je krijgt wat
    4252  * daarvoor ligt. Er komt er een extra op om te weten of er nog meer is: de
    4253  * client hoort dat te weten zonder te moeten gokken, en zonder dat weten kan
    4254  * 'load more' niet eerlijk verschijnen.
    4255  *
    4256  * DE CURSOR IS SAMENGESTELD -- '<stempel>|<ref>' -- en niet alleen de stempel.
    4257  * Twee berichten in dezelfde seconde is bij DM's geen randgeval maar een
    4258  * gesprek, en met 'stamp < before' zou alles wat die grensseconde deelt stil
    4259  * overgeslagen worden. Je zou het niet merken: de pagina komt gewoon, er
    4260  * ontbreekt alleen iets in het midden.
    4261  */
    4262 const cursorVan = (r) => (r ? `${r.stamp}|${r.ref}` : null);
    4263 
    4264 export function conversationHistory(slug, other, { before = null, limit = 60 } = {}) {
    4265   try {
    4266     const n = Math.min(Math.max(parseInt(limit, 10) || 60, 1), 200);
    4267     const knip = String(before || '').indexOf('|');
    4268     const bStamp = before && knip > 0 ? String(before).slice(0, knip) : null;
    4269     const bRef = before && knip > 0 ? String(before).slice(knip + 1) : null;
    4270     const rijen = db.prepare(`
    4271       SELECT other, stamp, richting, ref FROM (${GESPREK_UNIE})
    4272        WHERE other = @other
    4273          AND (@bStamp IS NULL OR stamp < @bStamp OR (stamp = @bStamp AND ref < @bRef))
    4274        ORDER BY stamp DESC, ref DESC LIMIT @n`).all({ slug, other, bStamp, bRef, n: n + 1 });
    4275     const meer = rijen.length > n;
    4276     const uit = meer ? rijen.slice(0, n) : rijen;
    4277     return { rijen: uit, meer, oudste: cursorVan(uit[uit.length - 1]) };
    4278   } catch { return { rijen: [], meer: false, oudste: null }; }
    4279 }
    4280 
    4281 // De kolommen die een bericht tot kaart maken. Een constante, want de
    4282 // gesprekslezing haalt dezelfde rijen op: twee lijsten die uiteenlopen leveren
    4283 // een kaart die op de ene plek een plaatje heeft en op de andere niet.
    4284 const BERICHT_KOLOMMEN = `
    4285       m.object_uri, m.note_url, m.actor_uri, m.actor_name, m.actor_handle, m.actor_icon, m.actor_url,
    4286       m.content, m.published, m.created_at, m.wave, m.help_request,
    4287       m.emoji_json, m.actor_emoji_json, m.media_json, m.quote_json, m.embed_json`;
    4288 
    4289 /** Dezelfde berichtrijen, maar op object-uri -- voor een gesprek. */
    4290 export function messageRowsByUri(slug, uris) {
    4291   const lijst = (uris || []).filter((u) => typeof u === 'string' && u);
    4292   if (!lijst.length) return [];
    4293   try {
    4294     const gaten = lijst.map(() => '?').join(',');
    4295     return db.prepare(`SELECT ${BERICHT_KOLOMMEN} FROM ap_mentions m
    4296                         WHERE m.slug = ? AND m.object_uri IN (${gaten})`).all(slug, ...lijst);
    4297   } catch { return []; }
    4298 }
    4299 
    43004193export function getDirectMessages(slug, limit) {
    43014194  try {
    43024195    return db.prepare(`
    4303       SELECT ${BERICHT_KOLOMMEN}
     4196      SELECT m.object_uri, m.note_url, m.actor_uri, m.actor_name, m.actor_handle, m.actor_icon, m.actor_url,
     4197             m.content, m.published, m.created_at, m.wave, m.help_request,
     4198             m.emoji_json, m.actor_emoji_json, m.media_json, m.quote_json, m.embed_json
    43044199      FROM ap_mentions m
    43054200      WHERE m.slug = ?
     
    63916286  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    63926287  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    6393   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, messageRowsByUri, conversationHeads, conversationHistory, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
     6288  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
    63946289  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    63956290  gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note: See TracChangeset for help on using the changeset viewer.