feat: complete Ancestor quote bot with production-ready Docker support

This commit is contained in:
unfunny
2026-09-13 14:14:38 -04:00
parent f2016da05a
commit fe7351a7dc
1145 changed files with 30890 additions and 97684 deletions

View File

@@ -1,5 +1,18 @@
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ReadReceipt = void 0;
exports.synthesizeReceipt = synthesizeReceipt;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _read_receipts = require("../@types/read_receipts");
var _typedEventEmitter = require("./typed-event-emitter");
var _utils = require("../utils");
var _event = require("./event");
var _event2 = require("../@types/event");
var _room = require("./room");
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,44 +26,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../@types/read_receipts.js";
import { TypedEventEmitter } from "./typed-event-emitter.js";
import { isSupportedReceiptType } from "../utils.js";
import { MatrixEvent } from "./event.js";
import { EventType } from "../@types/event.js";
import { MapWithDefault } from "../utils.js";
import { NotificationCountType } from "./room.js";
import { logger } from "../logger.js";
import { inMainTimelineForReceipt, threadIdForReceipt } from "../client.js";
/**
* Create a synthetic receipt for the given event
* @param userId - The user ID if the receipt sender
* @param event - The event that is to be acknowledged
* @param receiptType - The type of receipt
* @param unthreaded - the receipt is unthreaded
* @returns a new event with the synthetic receipt in it
*/
export function synthesizeReceipt(userId, event, receiptType, unthreaded = false) {
return new MatrixEvent({
function synthesizeReceipt(userId, event, receiptType) {
var _event$threadRootId;
return new _event.MatrixEvent({
content: {
[event.getId()]: {
[receiptType]: {
[userId]: _objectSpread({
ts: event.getTs()
}, !unthreaded && {
thread_id: threadIdForReceipt(event)
})
[userId]: {
ts: event.getTs(),
thread_id: (_event$threadRootId = event.threadRootId) !== null && _event$threadRootId !== void 0 ? _event$threadRootId : _read_receipts.MAIN_ROOM_TIMELINE
}
}
}
},
type: EventType.Receipt,
type: _event2.EventType.Receipt,
room_id: event.getRoomId()
});
}
const ReceiptPairRealIndex = 0;
const ReceiptPairSyntheticIndex = 1;
export class ReadReceipt extends TypedEventEmitter {
class ReadReceipt extends _typedEventEmitter.TypedEventEmitter {
constructor(...args) {
super(...args);
// receipts should clobber based on receipt_type and user_id pairs hence
@@ -58,8 +53,9 @@ export class ReadReceipt extends TypedEventEmitter {
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
// Map: receipt type → user Id → receipt
_defineProperty(this, "receipts", new MapWithDefault(() => new Map()));
_defineProperty(this, "receiptCacheByEventId", new Map());
(0, _defineProperty2.default)(this, "receipts", new _utils.MapWithDefault(() => new Map()));
(0, _defineProperty2.default)(this, "receiptCacheByEventId", new Map());
(0, _defineProperty2.default)(this, "timeline", void 0);
}
/**
* Gets the latest receipt for a given user in the room
@@ -68,142 +64,55 @@ export class ReadReceipt extends TypedEventEmitter {
* @param receiptType - Optional. The type of the receipt we want to get
* @returns the latest receipts of the chosen type for the chosen user
*/
getReadReceiptForUserId(userId, ignoreSynthesized = false, receiptType = ReceiptType.Read) {
const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null];
getReadReceiptForUserId(userId, ignoreSynthesized = false, receiptType = _read_receipts.ReceiptType.Read) {
var _this$receipts$get$ge, _this$receipts$get;
const [realReceipt, syntheticReceipt] = (_this$receipts$get$ge = (_this$receipts$get = this.receipts.get(receiptType)) === null || _this$receipts$get === void 0 ? void 0 : _this$receipts$get.get(userId)) !== null && _this$receipts$get$ge !== void 0 ? _this$receipts$get$ge : [null, null];
if (ignoreSynthesized) {
return realReceipt;
}
return syntheticReceipt ?? realReceipt;
return syntheticReceipt !== null && syntheticReceipt !== void 0 ? syntheticReceipt : realReceipt;
}
compareReceipts(a, b) {
var _this$getUnfilteredTi;
// Try compare them in our unfiltered timeline set order, falling back to receipt timestamp which should be
// relatively sane as receipts are set only by the originating homeserver so as long as its clock doesn't
// jump around then it should be valid.
return this.getUnfilteredTimelineSet().compareEventOrdering(a.eventId, b.eventId) ?? a.data.ts - b.data.ts;
return (_this$getUnfilteredTi = this.getUnfilteredTimelineSet().compareEventOrdering(a.eventId, b.eventId)) !== null && _this$getUnfilteredTi !== void 0 ? _this$getUnfilteredTi : a.data.ts - b.data.ts;
}
/**
* Get the ID of the event that a given user has read up to, or null if:
* - we have received no read receipts for them, or
* - the receipt we have points at an event we don't have, or
* - the thread ID in the receipt does not match the thread root of the
* referenced event.
*
* (The event might not exist if it is not loaded, and the thread ID might
* not match if the event has moved thread because it was redacted.)
*
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
* @param userId - The user ID to get read receipt event ID for
* @param ignoreSynthesized - If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @returns ID of the latest existing event that the given user has read, or null.
* sent by the server, not implicit ones generated
* by the JS SDK.
* @returns ID of the latest event that the given user has read, or null.
*/
getEventReadUpTo(userId, ignoreSynthesized = false) {
// Find what the latest receipt says is the latest event we have read
const latestReceipt = this.getLatestReceipt(userId, ignoreSynthesized);
if (!latestReceipt) {
return null;
}
return this.receiptPointsAtConsistentEvent(latestReceipt) ? latestReceipt.eventId : null;
}
/**
* Returns true if the event pointed at by this receipt exists, and its
* threadRootId is consistent with the thread information in the receipt.
*/
receiptPointsAtConsistentEvent(receipt) {
const event = this.findEventById(receipt.eventId);
if (!event) {
// If the receipt points at a non-existent event, we have multiple
// possibilities:
//
// 1. We don't have the event because it's not loaded yet - probably
// it's old and we're best off ignoring the receipt - we can just
// send a new one when we read a new event.
//
// 2. We have a bug e.g. we misclassified this event into the wrong
// thread.
//
// 3. The referenced event moved out of this thread (e.g. because it
// was deleted.)
//
// 4. The receipt had the incorrect thread ID (due to a bug in a
// client, or malicious behaviour).
// This receipt is not "valid" because it doesn't point at an event
// we have. We want to pretend it doesn't exist.
return false;
}
if (!receipt.data?.thread_id) {
// If this is an unthreaded receipt, it could point at any event, so
// there is no need to validate further - this receipt is valid.
return true;
}
// Otherwise it is a threaded receipt...
if (receipt.data.thread_id === MAIN_ROOM_TIMELINE) {
// The receipt is for the main timeline: we check that the event is
// in the main timeline.
// Check if the event is in the main timeline
const eventIsInMainTimeline = inMainTimelineForReceipt(event);
if (eventIsInMainTimeline) {
// The receipt is for the main timeline, and so is the event, so
// the receipt is valid.
return true;
}
} else {
// The receipt is for a different thread (not the main timeline)
if (event.threadRootId === receipt.data.thread_id) {
// If the receipt and event agree on the thread ID, the receipt
// is valid.
return true;
}
}
// The receipt thread ID disagrees with the event thread ID. There are 2
// possibilities:
//
// 1. The event moved to a different thread after the receipt was
// created. This can happen if the event was redacted because that
// moves it to the main timeline.
//
// 2. There is a bug somewhere - either we put the event into the wrong
// thread, or someone sent an incorrect receipt.
//
// In many cases, we won't get here because the call to findEventById
// would have already returned null. We include this check to cover
// cases when `this` is a room, meaning findEventById will find events
// in any thread, and to be defensive against unforeseen code paths.
logger.warn(`Ignoring receipt because its thread_id (${receipt.data.thread_id}) disagrees ` + `with the thread root (${event.threadRootId}) of the referenced event ` + `(event ID = ${receipt.eventId})`);
// This receipt is not "valid" because it disagrees with us about what
// thread the event is in. We want to pretend it doesn't exist.
return false;
}
getLatestReceipt(userId, ignoreSynthesized) {
var _ref, _privateReadReceipt$e, _ref2;
// XXX: This is very very ugly and I hope I won't have to ever add a new
// receipt type here again. IMHO this should be done by the server in
// some more intelligent manner or the client should just use timestamps
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.ReadPrivate);
// If we have both, compare them
let comparison;
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
if (publicReadReceipt !== null && publicReadReceipt !== void 0 && publicReadReceipt.eventId && privateReadReceipt !== null && privateReadReceipt !== void 0 && privateReadReceipt.eventId) {
comparison = this.compareReceipts(publicReadReceipt, privateReadReceipt);
}
// The public receipt is more likely to drift out of date so the private
// one has precedence
if (!comparison) return privateReadReceipt ?? publicReadReceipt ?? null;
if (!comparison) return (_ref = (_privateReadReceipt$e = privateReadReceipt === null || privateReadReceipt === void 0 ? void 0 : privateReadReceipt.eventId) !== null && _privateReadReceipt$e !== void 0 ? _privateReadReceipt$e : publicReadReceipt === null || publicReadReceipt === void 0 ? void 0 : publicReadReceipt.eventId) !== null && _ref !== void 0 ? _ref : null;
// If public read receipt is older, return the private one
return (comparison < 0 ? privateReadReceipt : publicReadReceipt) ?? null;
return (_ref2 = comparison < 0 ? privateReadReceipt === null || privateReadReceipt === void 0 ? void 0 : privateReadReceipt.eventId : publicReadReceipt === null || publicReadReceipt === void 0 ? void 0 : publicReadReceipt.eventId) !== null && _ref2 !== void 0 ? _ref2 : null;
}
addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic) {
var _pair$ReceiptPairSynt2, _pair$ReceiptPairSynt3;
const receiptTypesMap = this.receipts.getOrCreate(receiptType);
let pair = receiptTypesMap.get(userId);
if (!pair) {
@@ -212,7 +121,8 @@ export class ReadReceipt extends TypedEventEmitter {
}
let existingReceipt = pair[ReceiptPairRealIndex];
if (synthetic) {
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
var _pair$ReceiptPairSynt;
existingReceipt = (_pair$ReceiptPairSynt = pair[ReceiptPairSyntheticIndex]) !== null && _pair$ReceiptPairSynt !== void 0 ? _pair$ReceiptPairSynt : pair[ReceiptPairRealIndex];
}
const wrappedReceipt = {
eventId,
@@ -236,7 +146,7 @@ export class ReadReceipt extends TypedEventEmitter {
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
// Take the current cached receipt before we overwrite the pair elements.
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
const cachedReceipt = (_pair$ReceiptPairSynt2 = pair[ReceiptPairSyntheticIndex]) !== null && _pair$ReceiptPairSynt2 !== void 0 ? _pair$ReceiptPairSynt2 : pair[ReceiptPairRealIndex];
if (synthetic && preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
} else if (!synthetic) {
@@ -245,7 +155,7 @@ export class ReadReceipt extends TypedEventEmitter {
pair[ReceiptPairSyntheticIndex] = null;
}
}
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
const newCachedReceipt = (_pair$ReceiptPairSynt3 = pair[ReceiptPairSyntheticIndex]) !== null && _pair$ReceiptPairSynt3 !== void 0 ? _pair$ReceiptPairSynt3 : pair[ReceiptPairRealIndex];
if (cachedReceipt === newCachedReceipt) return;
// clean up any previous cache entry
@@ -280,13 +190,6 @@ export class ReadReceipt extends TypedEventEmitter {
getReceiptsForEvent(event) {
return this.receiptCacheByEventId.get(event.getId()) || [];
}
/**
* Look in this room/thread's timeline to find an event. If `this` is a
* room, we look in all threads, but if `this` is a thread, we look only
* inside this thread.
*/
/**
* This issue should also be addressed on synapse's side and is tracked as part
* of https://github.com/matrix-org/synapse/issues/14837
@@ -301,9 +204,9 @@ export class ReadReceipt extends TypedEventEmitter {
fixupNotifications(userId) {
const receipt = this.getReadReceiptForUserId(userId, false);
const lastEvent = this.timeline[this.timeline.length - 1];
if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
this.setUnread(NotificationCountType.Total, 0);
this.setUnread(NotificationCountType.Highlight, 0);
if (lastEvent && (receipt === null || receipt === void 0 ? void 0 : receipt.eventId) === lastEvent.getId() && userId === lastEvent.getSender()) {
this.setUnread(_room.NotificationCountType.Total, 0);
this.setUnread(_room.NotificationCountType.Highlight, 0);
}
}
@@ -313,10 +216,9 @@ export class ReadReceipt extends TypedEventEmitter {
* @param userId - The user ID if the receipt sender
* @param e - The event that is to be acknowledged
* @param receiptType - The type of receipt
* @param unthreaded - the receipt is unthreaded
*/
addLocalEchoReceipt(userId, e, receiptType, unthreaded = false) {
this.addReceipt(synthesizeReceipt(userId, e, receiptType, unthreaded), true);
addLocalEchoReceipt(userId, e, receiptType) {
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
}
/**
@@ -326,7 +228,7 @@ export class ReadReceipt extends TypedEventEmitter {
*/
getUsersReadUpTo(event) {
return this.getReceiptsForEvent(event).filter(function (receipt) {
return isSupportedReceiptType(receipt.type);
return (0, _utils.isSupportedReceiptType)(receipt.type);
}).map(function (receipt) {
return receipt.userId;
});
@@ -340,6 +242,29 @@ export class ReadReceipt extends TypedEventEmitter {
* @param eventId - The event ID to check if the user read.
* @returns True if the user has read the event, false otherwise.
*/
hasUserReadEvent(userId, eventId) {
var _this$timeline;
const readUpToId = this.getEventReadUpTo(userId, false);
if (readUpToId === eventId) return true;
if ((_this$timeline = this.timeline) !== null && _this$timeline !== void 0 && _this$timeline.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) {
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return true;
}
for (let i = ((_this$timeline2 = this.timeline) === null || _this$timeline2 === void 0 ? void 0 : _this$timeline2.length) - 1; i >= 0; --i) {
var _this$timeline2;
const ev = this.timeline[i];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if (ev.getId() === eventId) return false;
if (ev.getId() === readUpToId) return true;
}
// We don't know if the user has read it, so assume not.
return false;
}
/**
* Returns the most recent unthreaded receipt for a given user
@@ -347,8 +272,7 @@ export class ReadReceipt extends TypedEventEmitter {
* @returns an unthreaded Receipt. Can be undefined if receipts have been disabled
* or a user chooses to use private read receipts (or we have simply not received
* a receipt from this user yet).
*
* @deprecated use `hasUserReadEvent` or `getEventReadUpTo` instead
*/
}
exports.ReadReceipt = ReadReceipt;
//# sourceMappingURL=read-receipt.js.map