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,32 +1,38 @@
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
"use strict";
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event.js";
import { logger } from "../logger.js";
import { averageBetweenStrings, DEFAULT_ALPHABET, lexicographicCompare, nextString, prevString, simpleRetryOperation } from "../utils.js";
import { MSC3089Branch } from "./MSC3089Branch.js";
import { MatrixError } from "../http-api/index.js";
import { KnownMembership } from "../@types/membership.js";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TreePermissions = exports.MSC3089TreeSpace = exports.DEFAULT_TREE_POWER_LEVELS_TEMPLATE = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _pRetry = _interopRequireDefault(require("p-retry"));
var _event = require("../@types/event");
var _logger = require("../logger");
var _utils = require("../utils");
var _MSC3089Branch = require("./MSC3089Branch");
var _megolm = require("../crypto/algorithms/megolm");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } /*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
/**
* The recommended defaults for a tree space's power levels. Note that this
* is UNSTABLE and subject to breaking changes without notice.
*/
export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
// Owner
invite: 100,
kick: 100,
@@ -39,14 +45,14 @@ export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
users_default: 0,
// Mixed
events: {
[EventType.RoomPowerLevels]: 100,
[EventType.RoomHistoryVisibility]: 100,
[EventType.RoomTombstone]: 100,
[EventType.RoomEncryption]: 100,
[EventType.RoomName]: 50,
[EventType.RoomMessage]: 50,
[EventType.RoomMessageEncrypted]: 50,
[EventType.Sticker]: 50
[_event.EventType.RoomPowerLevels]: 100,
[_event.EventType.RoomHistoryVisibility]: 100,
[_event.EventType.RoomTombstone]: 100,
[_event.EventType.RoomEncryption]: 100,
[_event.EventType.RoomName]: 50,
[_event.EventType.RoomMessage]: 50,
[_event.EventType.RoomMessageEncrypted]: 50,
[_event.EventType.Sticker]: 50
},
users: {} // defined by calling code
};
@@ -55,24 +61,24 @@ export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
* Ease-of-use representation for power levels represented as simple roles.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*/
export let TreePermissions = /*#__PURE__*/function (TreePermissions) {
exports.DEFAULT_TREE_POWER_LEVELS_TEMPLATE = DEFAULT_TREE_POWER_LEVELS_TEMPLATE;
let TreePermissions = /*#__PURE__*/function (TreePermissions) {
TreePermissions["Viewer"] = "viewer";
// Default
TreePermissions["Editor"] = "editor";
// "Moderator" or ~PL50
TreePermissions["Owner"] = "owner"; // "Admin" or PL100
TreePermissions["Owner"] = "owner";
return TreePermissions;
}({});
}({}); // "Admin" or PL100
/**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089)
* file tree Space. Note that this is UNSTABLE and subject to breaking changes
* without notice.
*/
export class MSC3089TreeSpace {
exports.TreePermissions = TreePermissions;
class MSC3089TreeSpace {
constructor(client, roomId) {
_defineProperty(this, "room", void 0);
this.client = client;
this.roomId = roomId;
(0, _defineProperty2.default)(this, "room", void 0);
this.room = this.client.getRoom(this.roomId);
if (!this.room) throw new Error("Unknown room");
}
@@ -90,9 +96,12 @@ export class MSC3089TreeSpace {
get isTopLevel() {
// XXX: This is absolutely not how you find out if the space is top level
// but is safe for a managed usecase like we offer in the SDK.
const parentEvents = this.room.currentState.getStateEvents(EventType.SpaceParent);
if (!parentEvents?.length) return true;
return parentEvents.every(e => !e.getContent()?.["via"]);
const parentEvents = this.room.currentState.getStateEvents(_event.EventType.SpaceParent);
if (!(parentEvents !== null && parentEvents !== void 0 && parentEvents.length)) return true;
return parentEvents.every(e => {
var _e$getContent;
return !((_e$getContent = e.getContent()) !== null && _e$getContent !== void 0 && _e$getContent["via"]);
});
}
/**
@@ -101,7 +110,7 @@ export class MSC3089TreeSpace {
* @returns Promise which resolves when complete.
*/
async setName(name) {
await this.client.sendStateEvent(this.roomId, EventType.RoomName, {
await this.client.sendStateEvent(this.roomId, _event.EventType.RoomName, {
name
}, "");
}
@@ -112,22 +121,38 @@ export class MSC3089TreeSpace {
* @param userId - The user ID to invite.
* @param andSubspaces - True (default) to invite the user to all
* directories/subspaces too, recursively.
* @param shareHistoryKeys - True (default) to share encryption keys
* with the invited user. This will allow them to decrypt the events (files)
* in the tree. Keys will not be shared if the room is lacking appropriate
* history visibility (by default, history visibility is "shared" in trees,
* which is an appropriate visibility for these purposes).
* @returns Promise which resolves when complete.
*/
async invite(userId, andSubspaces = true) {
async invite(userId, andSubspaces = true, shareHistoryKeys = true) {
const promises = [this.retryInvite(userId)];
if (andSubspaces) {
promises.push(...this.getDirectories().map(d => d.invite(userId, andSubspaces)));
promises.push(...this.getDirectories().map(d => d.invite(userId, andSubspaces, shareHistoryKeys)));
}
await Promise.all(promises);
}
async retryInvite(userId) {
await simpleRetryOperation(() => this.client.invite(this.roomId, userId), e => {
// We don't want to retry permission errors forever...
if (e instanceof MatrixError && e.errcode === "M_FORBIDDEN") {
return false;
return Promise.all(promises).then(() => {
// Note: key sharing is default on because for file trees it is relatively important that the invite
// target can actually decrypt the files. The implied use case is that by inviting a user to the tree
// it means the sender would like the receiver to view/download the files contained within, much like
// sharing a folder in other circles.
if (shareHistoryKeys && (0, _megolm.isRoomSharedHistory)(this.room)) {
// noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
this.client.sendSharedHistoryKeys(this.roomId, [userId]);
}
return true;
});
}
retryInvite(userId) {
return (0, _utils.simpleRetryOperation)(async () => {
await this.client.invite(this.roomId, userId).catch(e => {
// We don't want to retry permission errors forever...
if ((e === null || e === void 0 ? void 0 : e.errcode) === "M_FORBIDDEN") {
throw new _pRetry.default.AbortError(e);
}
throw e;
});
});
}
@@ -140,12 +165,13 @@ export class MSC3089TreeSpace {
* @returns Promise which resolves when complete.
*/
async setPermissions(userId, role) {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
var _pls$events;
const currentPls = this.room.currentState.getStateEvents(_event.EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls?.getContent() || {};
const pls = (currentPls === null || currentPls === void 0 ? void 0 : currentPls.getContent()) || {};
const viewLevel = pls["users_default"] || 0;
const editLevel = pls["events_default"] || 50;
const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100;
const adminLevel = ((_pls$events = pls["events"]) === null || _pls$events === void 0 ? void 0 : _pls$events[_event.EventType.RoomPowerLevels]) || 100;
const users = pls["users"] || {};
switch (role) {
case TreePermissions.Viewer:
@@ -161,7 +187,7 @@ export class MSC3089TreeSpace {
throw new Error("Invalid role: " + role);
}
pls["users"] = users;
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
await this.client.sendStateEvent(this.roomId, _event.EventType.RoomPowerLevels, pls, "");
}
/**
@@ -172,13 +198,14 @@ export class MSC3089TreeSpace {
* @returns The permissions for the user, defaulting to Viewer.
*/
getPermissions(userId) {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
var _pls$events2, _pls$users;
const currentPls = this.room.currentState.getStateEvents(_event.EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls?.getContent() || {};
const pls = (currentPls === null || currentPls === void 0 ? void 0 : currentPls.getContent()) || {};
const viewLevel = pls["users_default"] || 0;
const editLevel = pls["events_default"] || 50;
const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100;
const userLevel = pls["users"]?.[userId] || viewLevel;
const adminLevel = ((_pls$events2 = pls["events"]) === null || _pls$events2 === void 0 ? void 0 : _pls$events2[_event.EventType.RoomPowerLevels]) || 100;
const userLevel = ((_pls$users = pls["users"]) === null || _pls$users === void 0 ? void 0 : _pls$users[userId]) || viewLevel;
if (userLevel >= adminLevel) return TreePermissions.Owner;
if (userLevel >= editLevel) return TreePermissions.Editor;
return TreePermissions.Viewer;
@@ -191,10 +218,10 @@ export class MSC3089TreeSpace {
*/
async createDirectory(name) {
const directory = await this.client.unstableCreateFileTree(name);
await this.client.sendStateEvent(this.roomId, EventType.SpaceChild, {
await this.client.sendStateEvent(this.roomId, _event.EventType.SpaceChild, {
via: [this.client.getDomain()]
}, directory.roomId);
await this.client.sendStateEvent(directory.roomId, EventType.SpaceParent, {
await this.client.sendStateEvent(directory.roomId, _event.EventType.SpaceParent, {
via: [this.client.getDomain()]
}, this.roomId);
return directory;
@@ -206,7 +233,7 @@ export class MSC3089TreeSpace {
*/
getDirectories() {
const trees = [];
const children = this.room.currentState.getStateEvents(EventType.SpaceChild);
const children = this.room.currentState.getStateEvents(_event.EventType.SpaceChild);
for (const child of children) {
try {
const stateKey = child.getStateKey();
@@ -215,7 +242,7 @@ export class MSC3089TreeSpace {
if (tree) trees.push(tree);
}
} catch (e) {
logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
_logger.logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
}
}
return trees;
@@ -240,8 +267,8 @@ export class MSC3089TreeSpace {
for (const dir of subdirectories) {
await dir.delete();
}
const kickMemberships = [KnownMembership.Invite, KnownMembership.Knock, KnownMembership.Join];
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
const kickMemberships = ["invite", "knock", "join"];
const members = this.room.currentState.getStateEvents(_event.EventType.RoomMember);
for (const member of members) {
const isNotUs = member.getStateKey() !== this.client.getUserId();
if (isNotUs && kickMemberships.includes(member.getContent().membership)) {
@@ -265,27 +292,28 @@ export class MSC3089TreeSpace {
} else if (!a.order && b.order) {
return 1;
} else if (!a.order && !b.order) {
var _roomA$currentState$g, _roomA$currentState$g2, _roomB$currentState$g, _roomB$currentState$g2;
const roomA = this.client.getRoom(a.roomId);
const roomB = this.client.getRoom(b.roomId);
if (!roomA || !roomB) {
// just don't bother trying to do more partial sorting
return lexicographicCompare(a.roomId, b.roomId);
return (0, _utils.lexicographicCompare)(a.roomId, b.roomId);
}
const createTsA = roomA.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
const createTsB = roomB.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
const createTsA = (_roomA$currentState$g = (_roomA$currentState$g2 = roomA.currentState.getStateEvents(_event.EventType.RoomCreate, "")) === null || _roomA$currentState$g2 === void 0 ? void 0 : _roomA$currentState$g2.getTs()) !== null && _roomA$currentState$g !== void 0 ? _roomA$currentState$g : 0;
const createTsB = (_roomB$currentState$g = (_roomB$currentState$g2 = roomB.currentState.getStateEvents(_event.EventType.RoomCreate, "")) === null || _roomB$currentState$g2 === void 0 ? void 0 : _roomB$currentState$g2.getTs()) !== null && _roomB$currentState$g !== void 0 ? _roomB$currentState$g : 0;
if (createTsA === createTsB) {
return lexicographicCompare(a.roomId, b.roomId);
return (0, _utils.lexicographicCompare)(a.roomId, b.roomId);
}
return createTsA - createTsB;
} else {
// both not-null orders
return lexicographicCompare(a.order, b.order);
return (0, _utils.lexicographicCompare)(a.order, b.order);
}
});
return ordered;
}
getParentRoom() {
const parents = this.room.currentState.getStateEvents(EventType.SpaceParent);
const parents = this.room.currentState.getStateEvents(_event.EventType.SpaceParent);
const parent = parents[0]; // XXX: Wild assumption
if (!parent) throw new Error("Expected to have a parent in a non-top level space");
@@ -306,7 +334,7 @@ export class MSC3089TreeSpace {
getOrder() {
if (this.isTopLevel) return -1;
const parentRoom = this.getParentRoom();
const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild);
const children = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild);
const ordered = this.getOrderedChildren(children);
return ordered.findIndex(c => c.roomId === this.roomId);
}
@@ -320,9 +348,10 @@ export class MSC3089TreeSpace {
* @throws Throws if this is a top level space.
*/
async setOrder(index) {
var _currentChild$getCont2;
if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently");
const parentRoom = this.getParentRoom();
const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild);
const children = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild);
const ordered = this.getOrderedChildren(children);
index = Math.max(Math.min(index, ordered.length - 1), 0);
const currentIndex = this.getOrder();
@@ -334,36 +363,36 @@ export class MSC3089TreeSpace {
}
const prev = ordered[movingUp ? index : index - 1];
const next = ordered[movingUp ? index + 1 : index];
let newOrder = DEFAULT_ALPHABET[0];
let newOrder = _utils.DEFAULT_ALPHABET[0];
let ensureBeforeIsSane = false;
if (!prev) {
// Move to front
if (next?.order) {
newOrder = prevString(next.order);
if (next !== null && next !== void 0 && next.order) {
newOrder = (0, _utils.prevString)(next.order);
}
} else if (index === ordered.length - 1) {
// Move to back
if (next?.order) {
newOrder = nextString(next.order);
if (next !== null && next !== void 0 && next.order) {
newOrder = (0, _utils.nextString)(next.order);
}
} else {
// Move somewhere in the middle
const startOrder = prev?.order;
const endOrder = next?.order;
const startOrder = prev === null || prev === void 0 ? void 0 : prev.order;
const endOrder = next === null || next === void 0 ? void 0 : next.order;
if (startOrder && endOrder) {
if (startOrder === endOrder) {
// Error case: just move +1 to break out of awful math
newOrder = nextString(startOrder);
newOrder = (0, _utils.nextString)(startOrder);
} else {
newOrder = averageBetweenStrings(startOrder, endOrder);
newOrder = (0, _utils.averageBetweenStrings)(startOrder, endOrder);
}
} else {
if (startOrder) {
// We're at the end (endOrder is null, so no explicit order)
newOrder = nextString(startOrder);
newOrder = (0, _utils.nextString)(startOrder);
} else if (endOrder) {
// We're at the start (startOrder is null, so nothing before us)
newOrder = prevString(endOrder);
newOrder = (0, _utils.prevString)(endOrder);
} else {
// Both points are unknown. We're likely in a range where all the children
// don't have particular order values, so we may need to update them too.
@@ -384,13 +413,14 @@ export class MSC3089TreeSpace {
lastOrder = target.order;
}
if (!target.order) {
var _currentChild$getCont;
// XXX: We should be creating gaps to avoid conflicts
lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0];
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId);
const content = currentChild?.getContent() ?? {
lastOrder = lastOrder ? (0, _utils.nextString)(lastOrder) : _utils.DEFAULT_ALPHABET[0];
const currentChild = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild, target.roomId);
const content = (_currentChild$getCont = currentChild === null || currentChild === void 0 ? void 0 : currentChild.getContent()) !== null && _currentChild$getCont !== void 0 ? _currentChild$getCont : {
via: [this.client.getDomain()]
};
await this.client.sendStateEvent(parentRoom.roomId, EventType.SpaceChild, _objectSpread(_objectSpread({}, content), {}, {
await this.client.sendStateEvent(parentRoom.roomId, _event.EventType.SpaceChild, _objectSpread(_objectSpread({}, content), {}, {
order: lastOrder
}), target.roomId);
} else {
@@ -398,18 +428,18 @@ export class MSC3089TreeSpace {
}
}
if (lastOrder) {
newOrder = nextString(lastOrder);
newOrder = (0, _utils.nextString)(lastOrder);
}
}
// TODO: Deal with order conflicts by reordering
// Now we can finally update our own order state
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId);
const content = currentChild?.getContent() ?? {
const currentChild = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild, this.roomId);
const content = (_currentChild$getCont2 = currentChild === null || currentChild === void 0 ? void 0 : currentChild.getContent()) !== null && _currentChild$getCont2 !== void 0 ? _currentChild$getCont2 : {
via: [this.client.getDomain()]
};
await this.client.sendStateEvent(parentRoom.roomId, EventType.SpaceChild, _objectSpread(_objectSpread({}, content), {}, {
await this.client.sendStateEvent(parentRoom.roomId, _event.EventType.SpaceChild, _objectSpread(_objectSpread({}, content), {}, {
// TODO: Safely constrain to 50 character limit required by spaces.
order: newOrder
}), this.roomId);
@@ -425,6 +455,7 @@ export class MSC3089TreeSpace {
* @returns Promise which resolves to the file event's sent response.
*/
async createFile(name, encryptedContents, info, additionalContent) {
var _additionalContent;
const {
content_uri: mxc
} = await this.client.uploadContent(encryptedContents, {
@@ -432,12 +463,12 @@ export class MSC3089TreeSpace {
});
info.url = mxc;
const fileContent = {
msgtype: MsgType.File,
msgtype: _event.MsgType.File,
body: name,
url: mxc,
file: info
};
additionalContent = additionalContent ?? {};
additionalContent = (_additionalContent = additionalContent) !== null && _additionalContent !== void 0 ? _additionalContent : {};
if (additionalContent["m.new_content"]) {
// We do the right thing according to the spec, but due to how relations are
// handled we also end up duplicating this information to the regular `content`
@@ -445,9 +476,9 @@ export class MSC3089TreeSpace {
additionalContent["m.new_content"] = fileContent;
}
const res = await this.client.sendMessage(this.roomId, _objectSpread(_objectSpread(_objectSpread({}, additionalContent), fileContent), {}, {
[UNSTABLE_MSC3089_LEAF.name]: {}
[_event.UNSTABLE_MSC3089_LEAF.name]: {}
}));
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, {
active: true,
name: name
}, res["event_id"]);
@@ -460,8 +491,8 @@ export class MSC3089TreeSpace {
* @returns The file, or null if not found.
*/
getFile(fileEventId) {
const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId);
return branch ? new MSC3089Branch(this.client, branch, this) : null;
const branch = this.room.currentState.getStateEvents(_event.UNSTABLE_MSC3089_BRANCH.name, fileEventId);
return branch ? new _MSC3089Branch.MSC3089Branch(this.client, branch, this) : null;
}
/**
@@ -477,8 +508,10 @@ export class MSC3089TreeSpace {
* @returns The known files. May be empty, but not null.
*/
listAllFiles() {
const branches = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name) ?? [];
return branches.map(e => new MSC3089Branch(this.client, e, this));
var _this$room$currentSta;
const branches = (_this$room$currentSta = this.room.currentState.getStateEvents(_event.UNSTABLE_MSC3089_BRANCH.name)) !== null && _this$room$currentSta !== void 0 ? _this$room$currentSta : [];
return branches.map(e => new _MSC3089Branch.MSC3089Branch(this.client, e, this));
}
}
exports.MSC3089TreeSpace = MSC3089TreeSpace;
//# sourceMappingURL=MSC3089TreeSpace.js.map