Changeset f549cf5 in Klonkt


Ignore:
Timestamp:
08/10/2026 02:03:21 PM (4 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
3bbf73d
Parents:
c34ef26 (diff), 09fc5fb (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge branch 'incoming'

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rc34ef26 rf549cf5  
    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.
     291function 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. */
     316function 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.
     325function 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. */
     361function 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.
     388function 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
     415router.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
     430router.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
    285458// ── Long-poll (owner only, Robins verzoek 31-7) ───────────────────
    286459// Hold the request until something push-worthy lands for this account, then
     
    414587  // here, at serialisation: a blocked embed is never sent, because an embed the
    415588  // client merely hides has still been delivered to the device.
    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);
     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);
    445600  // ── Standaardvormen naast het dialect (shaer-nmw) ────────────────
    446601  //
     
    521676  // beschermt niemand. De hulpvraag zelf gaat aan de innamekant al altijd voor.
    522677  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 };
    523679  const messages = AP.getDirectMessages(auth.site.slug, 60)
    524680    .filter((m) => messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))
    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   }));
     681    .map((m) => berichtItem(m, berichtCtx));
    557682  // Inbound REPLIES on your own posts: stored as interactions (the web's
    558683  // comment machinery), never as mentions, so this read missed them and a
  • src/services/ActivityPubService.js

    rc34ef26 rf549cf5  
    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.
     4212const 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 */
     4232export 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 */
     4262const cursorVan = (r) => (r ? `${r.stamp}|${r.ref}` : null);
     4263
     4264export 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.
     4284const 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. */
     4290export 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
    41934300export function getDirectMessages(slug, limit) {
    41944301  try {
    41954302    return db.prepare(`
    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
     4303      SELECT ${BERICHT_KOLOMMEN}
    41994304      FROM ap_mentions m
    42004305      WHERE m.slug = ?
     
    62866391  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    62876392  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    6288   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
     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,
    62896394  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    62906395  gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note: See TracChangeset for help on using the changeset viewer.