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,4 +1,15 @@
import _defineProperty from "@babel/runtime/helpers/defineProperty";
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SyncAccumulator = exports.Category = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _logger = require("./logger");
var _utils = require("./utils");
var _sync = require("./@types/sync");
var _receiptAccumulator = require("./receipt-accumulator");
/*
Copyright 2017 - 2023 The Matrix.org Foundation C.I.C.
@@ -14,39 +25,18 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is an internal module. See {@link SyncAccumulator} for the public class.
*/
import { logger } from "./logger.js";
import { deepCopy } from "./utils.js";
import { MAX_STICKY_DURATION_MS } from "./models/event.js";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.js";
import { ReceiptAccumulator } from "./receipt-accumulator.js";
/** A to-device message as received from the sync. */
/**
* A (possibly decrypted) to-device message after it has been successfully processed by the sdk.
*
* If the message was encrypted, the `encryptionInfo` field will contain the encryption information.
* If the message was sent in clear, this field will be null.
*
* The `message` field contains the message `type`, `content`, and `sender` as if the message was sent in clear.
*/
/**
* The "users" section of the sync update which contains extended profile updates.
*/
export let Category = /*#__PURE__*/function (Category) {
/* eslint-disable camelcase */
/* eslint-enable camelcase */
let Category = /*#__PURE__*/function (Category) {
Category["Invite"] = "invite";
Category["Leave"] = "leave";
Category["Join"] = "join";
Category["Knock"] = "knock";
return Category;
}({});
exports.Category = Category;
function isTaggedEvent(event) {
return "_localTs" in event && event["_localTs"] !== undefined;
}
@@ -61,21 +51,19 @@ function isTaggedEvent(event) {
* be loaded from disk and incremental syncs can be performed on the server,
* rather than asking the server to do an initial sync on startup.
*/
export class SyncAccumulator {
class SyncAccumulator {
constructor(opts = {}) {
_defineProperty(this, "accountData", {});
this.opts = opts;
(0, _defineProperty2.default)(this, "accountData", {});
// $event_type: Object
_defineProperty(this, "inviteRooms", {});
(0, _defineProperty2.default)(this, "inviteRooms", {});
// $roomId: { ... sync 'invite' json data ... }
_defineProperty(this, "knockRooms", {});
// $roomId: { ... sync 'knock' json data ... }
_defineProperty(this, "joinRooms", {});
(0, _defineProperty2.default)(this, "joinRooms", {});
// the /sync token which corresponds to the last time rooms were
// accumulated. We remember this so that any caller can obtain a
// coherent /sync response and know at what point they should be
// streaming from without losing events.
_defineProperty(this, "nextBatch", null);
this.opts = opts;
(0, _defineProperty2.default)(this, "nextBatch", null);
this.opts.maxTimelineEntries = this.opts.maxTimelineEntries || 50;
}
accumulate(syncResponse, fromDatabase = false) {
@@ -117,11 +105,6 @@ export class SyncAccumulator {
this.accumulateRoom(roomId, Category.Leave, syncResponse.rooms.leave[roomId], fromDatabase);
});
}
if (syncResponse.rooms.knock) {
Object.keys(syncResponse.rooms.knock).forEach(roomId => {
this.accumulateRoom(roomId, Category.Knock, syncResponse.rooms.knock[roomId], fromDatabase);
});
}
}
accumulateRoom(roomId, category, data, fromDatabase = false) {
// Valid /sync state transitions
@@ -138,20 +121,10 @@ export class SyncAccumulator {
switch (category) {
case Category.Invite:
// (5)
if (this.knockRooms[roomId]) {
// was previously knock, now invite, need to delete knock state
delete this.knockRooms[roomId];
}
this.accumulateInviteState(roomId, data);
break;
case Category.Knock:
this.accumulateKnockState(roomId, data);
break;
case Category.Join:
if (this.knockRooms[roomId]) {
// delete knock state on join
delete this.knockRooms[roomId];
} else if (this.inviteRooms[roomId]) {
if (this.inviteRooms[roomId]) {
// (1)
// was previously invite, now join. We expect /sync to give
// the entire state and timeline on 'join', so delete previous
@@ -162,10 +135,7 @@ export class SyncAccumulator {
this.accumulateJoinState(roomId, data, fromDatabase);
break;
case Category.Leave:
if (this.knockRooms[roomId]) {
// delete knock state on leave
delete this.knockRooms[roomId];
} else if (this.inviteRooms[roomId]) {
if (this.inviteRooms[roomId]) {
// (4)
delete this.inviteRooms[roomId];
} else {
@@ -174,7 +144,7 @@ export class SyncAccumulator {
}
break;
default:
logger.error("Unknown cateogory: ", category);
_logger.logger.error("Unknown cateogory: ", category);
}
}
accumulateInviteState(roomId, data) {
@@ -206,39 +176,10 @@ export class SyncAccumulator {
}
});
}
accumulateKnockState(roomId, data) {
if (!data.knock_state || !data.knock_state.events) {
// no new data
return;
}
if (!this.knockRooms[roomId]) {
this.knockRooms[roomId] = {
knock_state: data.knock_state
};
return;
}
// accumulate extra keys
// clobber based on event type / state key
// We expect knock_state to be small, so just loop over the events
const currentData = this.knockRooms[roomId];
data.knock_state.events.forEach(e => {
let hasAdded = false;
for (let i = 0; i < currentData.knock_state.events.length; i++) {
const current = currentData.knock_state.events[i];
if (current.type === e.type && current.state_key == e.state_key) {
currentData.knock_state.events[i] = e; // update
hasAdded = true;
}
}
if (!hasAdded) {
currentData.knock_state.events.push(e);
}
});
}
// Accumulate timeline and state events in a room.
accumulateJoinState(roomId, data, fromDatabase = false) {
const now = Date.now();
var _ref, _data, _data$ephemeral, _data$state, _data$timeline;
// We expect this function to be called a lot (every /sync) so we want
// this to be fast. /sync stores events in an array but we often want
// to clobber based on type/state_key. Rather than convert arrays to
@@ -284,8 +225,7 @@ export class SyncAccumulator {
_unreadNotifications: {},
_unreadThreadNotifications: {},
_summary: {},
_receipts: new ReceiptAccumulator(),
_stickyEvents: []
_receipts: new _receiptAccumulator.ReceiptAccumulator()
};
}
const currentData = this.joinRooms[roomId];
@@ -300,16 +240,17 @@ export class SyncAccumulator {
if (data.unread_notifications) {
currentData._unreadNotifications = data.unread_notifications;
}
currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable] ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable] ?? undefined;
currentData._unreadThreadNotifications = (_ref = (_data = data[_sync.UNREAD_THREAD_NOTIFICATIONS.stable]) !== null && _data !== void 0 ? _data : data[_sync.UNREAD_THREAD_NOTIFICATIONS.unstable]) !== null && _ref !== void 0 ? _ref : undefined;
if (data.summary) {
var _sum$HEROES_KEY, _sum$JOINED_COUNT_KEY, _sum$INVITED_COUNT_KE;
const HEROES_KEY = "m.heroes";
const INVITED_COUNT_KEY = "m.invited_member_count";
const JOINED_COUNT_KEY = "m.joined_member_count";
const acc = currentData._summary;
const sum = data.summary;
acc[HEROES_KEY] = sum[HEROES_KEY] ?? acc[HEROES_KEY];
acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] ?? acc[JOINED_COUNT_KEY];
acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] ?? acc[INVITED_COUNT_KEY];
acc[HEROES_KEY] = (_sum$HEROES_KEY = sum[HEROES_KEY]) !== null && _sum$HEROES_KEY !== void 0 ? _sum$HEROES_KEY : acc[HEROES_KEY];
acc[JOINED_COUNT_KEY] = (_sum$JOINED_COUNT_KEY = sum[JOINED_COUNT_KEY]) !== null && _sum$JOINED_COUNT_KEY !== void 0 ? _sum$JOINED_COUNT_KEY : acc[JOINED_COUNT_KEY];
acc[INVITED_COUNT_KEY] = (_sum$INVITED_COUNT_KE = sum[INVITED_COUNT_KEY]) !== null && _sum$INVITED_COUNT_KE !== void 0 ? _sum$INVITED_COUNT_KE : acc[INVITED_COUNT_KEY];
}
// We purposefully do not persist m.typing events.
@@ -322,7 +263,7 @@ export class SyncAccumulator {
// clobber the one we persisted).
// Persist the receipts
currentData._receipts.consumeEphemeralEvents(data.ephemeral?.events);
currentData._receipts.consumeEphemeralEvents((_data$ephemeral = data.ephemeral) === null || _data$ephemeral === void 0 ? void 0 : _data$ephemeral.events);
// if we got a limited sync, we need to remove all timeline entries or else
// we will have gaps in the timeline.
@@ -333,18 +274,14 @@ export class SyncAccumulator {
// Work out the current state. The deltas need to be applied in the order:
// - existing state which didn't come down /sync.
// - State events under the 'state' key.
// - State events under the 'state_after' key OR state events in the 'timeline' if 'state_after' is not present.
data.state?.events?.forEach(e => {
// - State events in the 'timeline'.
(_data$state = data.state) === null || _data$state === void 0 || (_data$state = _data$state.events) === null || _data$state === void 0 ? void 0 : _data$state.forEach(e => {
setState(currentData._currentState, e);
});
data["org.matrix.msc4222.state_after"]?.events?.forEach(e => {
(_data$timeline = data.timeline) === null || _data$timeline === void 0 || (_data$timeline = _data$timeline.events) === null || _data$timeline === void 0 ? void 0 : _data$timeline.forEach((e, index) => {
var _data$timeline$prev_b;
// this nops if 'e' isn't a state event
setState(currentData._currentState, e);
});
data.timeline?.events?.forEach((e, index) => {
if (!data["org.matrix.msc4222.state_after"]) {
// this nops if 'e' isn't a state event
setState(currentData._currentState, e);
}
// append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline
let transformedEvent;
@@ -353,38 +290,17 @@ export class SyncAccumulator {
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
const age = e.unsigned?.age;
const age = e.unsigned ? e.unsigned.age : e.age;
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
} else {
transformedEvent = e;
}
currentData._timeline.push({
event: transformedEvent,
token: index === 0 ? data.timeline.prev_batch ?? null : null
token: index === 0 ? (_data$timeline$prev_b = data.timeline.prev_batch) !== null && _data$timeline$prev_b !== void 0 ? _data$timeline$prev_b : null : null
});
});
// Prune out any events in our stores that have since expired, do this before we
// insert new events.
currentData._stickyEvents = currentData._stickyEvents.filter(({
expiresTs
}) => expiresTs > now);
// We want this to be fast, so don't worry about duplicate events here. The RoomStickyEventsStore will
// process these events into the correct mapped order.
if (data.msc4354_sticky?.events) {
currentData._stickyEvents = currentData._stickyEvents.concat(data.msc4354_sticky.events.map(event => {
// If `duration_ms` exceeds the spec limit of a hour, we cap it.
const cappedDuration = Math.min(event.msc4354_sticky.duration_ms, MAX_STICKY_DURATION_MS);
// If `origin_server_ts` claims to have been from the future, we still bound it to now.
const createdTs = Math.min(event.origin_server_ts, now);
return {
event,
expiresTs: cappedDuration + createdTs
};
}));
}
// attempt to prune the timeline by jumping between events which have
// pagination tokens.
if (currentData._timeline.length > this.opts.maxTimelineEntries) {
@@ -392,7 +308,7 @@ export class SyncAccumulator {
for (let i = startIndex; i < currentData._timeline.length; i++) {
if (currentData._timeline[i].token) {
// keep all events after this, including this one
currentData._timeline = currentData._timeline.slice(i);
currentData._timeline = currentData._timeline.slice(i, currentData._timeline.length);
break;
}
}
@@ -418,7 +334,6 @@ export class SyncAccumulator {
const data = {
join: {},
invite: {},
knock: {},
// always empty. This is set by /sync when a room was previously
// in 'invite' or 'join'. On fresh startup, the client won't know
// about any previous room being in 'invite' or 'join' so we can
@@ -435,34 +350,25 @@ export class SyncAccumulator {
Object.keys(this.inviteRooms).forEach(roomId => {
data.invite[roomId] = this.inviteRooms[roomId];
});
Object.keys(this.knockRooms).forEach(roomId => {
data.knock[roomId] = this.knockRooms[roomId];
});
Object.keys(this.joinRooms).forEach(roomId => {
const roomData = this.joinRooms[roomId];
const roomJson = {
"ephemeral": {
ephemeral: {
events: []
},
"account_data": {
account_data: {
events: []
},
"state": {
state: {
events: []
},
"org.matrix.msc4222.state_after": {
events: []
},
"timeline": {
timeline: {
events: [],
prev_batch: null
},
"unread_notifications": roomData._unreadNotifications,
"unread_thread_notifications": roomData._unreadThreadNotifications,
"summary": roomData._summary,
"msc4354_sticky": roomData._stickyEvents?.length ? {
events: roomData._stickyEvents.map(e => e.event)
} : undefined
unread_notifications: roomData._unreadNotifications,
unread_thread_notifications: roomData._unreadThreadNotifications,
summary: roomData._summary
};
// Add account data
Object.keys(roomData._accountData).forEach(evType => {
@@ -483,6 +389,7 @@ export class SyncAccumulator {
if (!msgData.token) {
return; // this shouldn't happen as we prune constantly.
}
roomJson.timeline.prev_batch = msgData.token;
}
let transformedEvent;
@@ -520,7 +427,7 @@ export class SyncAccumulator {
// since we're going back in time, we need to use the previous
// state value else we'll break causality. We don't have the
// complete previous state event, so we need to create one.
const prevStateEvent = deepCopy(timelineEvent);
const prevStateEvent = (0, _utils.deepCopy)(timelineEvent);
if (prevStateEvent.unsigned) {
if (prevStateEvent.unsigned.prev_content) {
prevStateEvent.content = prevStateEvent.unsigned.prev_content;
@@ -534,11 +441,8 @@ export class SyncAccumulator {
Object.keys(roomData._currentState).forEach(evType => {
Object.keys(roomData._currentState[evType]).forEach(stateKey => {
let ev = roomData._currentState[evType][stateKey];
// Push to both fields to provide downgrade compatibility in the sync accumulator db
// the code will prefer `state_after` if it is present
roomJson["org.matrix.msc4222.state_after"].events.push(ev);
// Roll the state back to the value at the start of the timeline if it was changed
if (rollBackState[evType] && rollBackState[evType][stateKey]) {
// use the reverse clobbered event instead.
ev = rollBackState[evType][stateKey];
}
roomJson.state.events.push(ev);
@@ -561,11 +465,8 @@ export class SyncAccumulator {
getNextBatchToken() {
return this.nextBatch;
}
removeEventsFromRoom(roomId, eventIds) {
this.joinRooms[roomId]._timeline = this.joinRooms[roomId]._timeline.filter(ev => !eventIds.includes(ev.event.event_id));
this.joinRooms[roomId]._stickyEvents = this.joinRooms[roomId]._stickyEvents.filter(ev => !eventIds.includes(ev.event.event_id));
}
}
exports.SyncAccumulator = SyncAccumulator;
function setState(eventMap, event) {
if (event.state_key === null || event.state_key === undefined || !event.type) {
return;