Changeset f549cf5 in Klonkt
- Timestamp:
- 08/10/2026 02:03:21 PM (4 weeks ago)
- 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. - Files:
-
- 1 added
- 2 edited
-
src/routes/activitypub.js (modified) (3 diffs)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
test/conversations.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
rc34ef26 rf549cf5 283 283 }); 284 284 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 285 458 // ── Long-poll (owner only, Robins verzoek 31-7) ─────────────────── 286 459 // Hold the request until something push-worthy lands for this account, then … … 414 587 // here, at serialisation: a blocked embed is never sent, because an embed the 415 588 // 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); 445 600 // ── Standaardvormen naast het dialect (shaer-nmw) ──────────────── 446 601 // … … 521 676 // beschermt niemand. De hulpvraag zelf gaat aan de innamekant al altijd voor. 522 677 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 }; 523 679 const messages = AP.getDirectMessages(auth.site.slug, 60) 524 680 .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)); 557 682 // Inbound REPLIES on your own posts: stored as interactions (the web's 558 683 // comment machinery), never as mentions, so this read missed them and a -
src/services/ActivityPubService.js
rc34ef26 rf549cf5 4191 4191 } 4192 4192 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 4193 4300 export function getDirectMessages(slug, limit) { 4194 4301 try { 4195 4302 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} 4199 4304 FROM ap_mentions m 4200 4305 WHERE m.slug = ? … … 6286 6391 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote, 6287 6392 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, 6289 6394 acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision, 6290 6395 gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)