first commit

This commit is contained in:
Myk
2025-07-31 23:47:20 +03:00
commit 2186b278a0
5149 changed files with 537218 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
/// <reference types="node" />
import { EventEmitter } from "events";
import { Intent } from "./Intent";
import { IAppserviceCryptoStorageProvider, IAppserviceStorageProvider, IJoinRoomStrategy, IPreprocessor, MatrixClient, Metrics } from "..";
import { MatrixBridge } from "./MatrixBridge";
/**
* Represents an application service's registration file. This is expected to be
* loaded from another source, such as a YAML file.
* @category Application services
*/
export interface IAppserviceRegistration {
/**
* Optional ID for the appplication service. Used by homeservers to track which application
* service registers what.
*/
id?: string;
/**
* Optional URL at which the application service can be contacted.
*/
url?: string;
/**
* The token the application service uses to communicate with the homeserver.
*/
as_token: string;
/**
* The token the homeserver uses to communicate with the application service.
*/
hs_token: string;
/**
* The application service's own localpart (eg: "_irc_bot" in the user ID "@_irc_bot:domain.com")
*/
sender_localpart: string;
/**
* The various namespaces the application service can support.
*/
namespaces: {
/**
* The user namespaces the application service is requesting.
*/
users: {
/**
* Whether or not the application service holds an exclusive lock on the namespace. This
* means that no other user on the homeserver may register users that match this namespace.
*/
exclusive: boolean;
/**
* The regular expression that the homeserver uses to determine if a user is in this namespace.
*/
regex: string;
}[];
/**
* The room namespaces the application service is requesting. This is not for alises.
*/
rooms: {
/**
* Whether or not the application service holds an exclusive lock on the namespace.
*/
exclusive: boolean;
/**
* The regular expression that the homeserver uses to determine if a user is in this namespace.
*/
regex: string;
}[];
/**
* The room alias namespaces the application service is requesting.
*/
aliases: {
/**
* Whether or not the application service holds an exclusive lock on the namespace. This means
* that no other user on the homeserver may register aliases that match this namespace.
*/
exclusive: boolean;
/**
* The regular expression that the homeserver uses to determine if an alias is in this namespace.
*/
regex: string;
}[];
};
/**
* The protocols the application service supports. Optional.
*/
protocols?: string[];
/**
* If the application service is rate limited by the homeserver. Optional.
*/
rate_limited?: boolean;
/**
* **Experimental**
*
* Should the application service receive ephemeral events from the homeserver. Optional.
* @see https://github.com/matrix-org/matrix-doc/pull/2409
*/
"de.sorunome.msc2409.push_ephemeral"?: boolean;
}
/**
* General options for the application service
* @category Application services
*/
export interface IAppserviceOptions {
/**
* The port to listen for requests from the homeserver on.
*/
port: number;
/**
* The bind address to listen for requests on.
*/
bindAddress: string;
/**
* The name of the homeserver, as presented over federation (eg: "matrix.org")
*/
homeserverName: string;
/**
* The URL to the homeserver's client server API (eg: "https://matrix.org")
*/
homeserverUrl: string;
/**
* The storage provider to use for this application service.
*/
storage?: IAppserviceStorageProvider;
/**
* The storage provider to use for setting up encryption. Encryption will be
* disabled for all intents and the appservice if not configured.
*/
cryptoStorage?: IAppserviceCryptoStorageProvider;
/**
* The registration for this application service.
*/
registration: IAppserviceRegistration;
/**
* The join strategy to use for all intents, if any.
*/
joinStrategy?: IJoinRoomStrategy;
/**
* Options for how Intents are handled.
*/
intentOptions?: {
/**
* The maximum number of intents to keep cached. Defaults to 10 thousand.
*/
maxCached?: number;
/**
* The maximum age in milliseconds to keep an Intent around for, provided
* the maximum number of intents has been reached. Defaults to 60 minutes.
*/
maxAgeMs?: number;
/**
* If false (default), crypto will not be automatically set up for all intent
* instances - it will need to be manually enabled with
* `await intent.enableEncryption()`.
*
* If true, crypto will be automatically set up.
*
* Note that the appservice bot account is considered an intent.
*/
encryption?: boolean;
};
}
/**
* Represents an application service. This provides helper utilities such as tracking
* of user intents (clients that are aware of their membership in rooms).
* @category Application services
*/
export declare class Appservice extends EventEmitter {
private options;
/**
* The metrics instance for this appservice. This will raise all metrics
* from this appservice instance as well as any intents/MatrixClients created
* by the appservice.
*/
readonly metrics: Metrics;
private readonly userPrefix;
private readonly aliasPrefix;
private readonly registration;
private readonly storage;
private readonly cryptoStorage;
private readonly bridgeInstance;
private app;
private appServer;
private intentsCache;
private eventProcessors;
private pendingTransactions;
/**
* Creates a new application service.
* @param {IAppserviceOptions} options The options for the application service.
*/
constructor(options: IAppserviceOptions);
/**
* Gets the express app instance which is serving requests. Not recommended for
* general usage, but may be used to append routes to the web server.
*/
get expressAppInstance(): import("express-serve-static-core").Express;
/**
* Gets the bridge-specific APIs for this application service.
*/
get bridge(): MatrixBridge;
/**
* Get the application service's "bot" user ID (the sender_localpart).
*/
get botUserId(): string;
/**
* Get the application service's "bot" Intent (the sender_localpart).
* @returns {Intent} The intent for the application service itself.
*/
get botIntent(): Intent;
/**
* Get the application service's "bot" MatrixClient (the sender_localpart).
* Normally the botIntent should be used to ensure that the bot user is safely
* handled.
* @returns {MatrixClient} The client for the application service itself.
*/
get botClient(): MatrixClient;
/**
* Starts the application service, opening the bind address to begin processing requests.
* @returns {Promise<void>} resolves when started
*/
begin(): Promise<void>;
/**
* Stops the application service, freeing the web server.
*/
stop(): void;
/**
* Gets an intent for a given localpart. The user ID will be formed with the domain name given
* in the constructor.
* @param localpart The localpart to get an Intent for.
* @returns {Intent} An Intent for the user.
*/
getIntent(localpart: string): Intent;
/**
* Gets a full user ID for a given localpart. The user ID will be formed with the domain name given
* in the constructor.
* @param localpart The localpart to get a user ID for.
* @returns {string} The user's ID.
*/
getUserId(localpart: string): string;
/**
* Gets an Intent for a given user suffix. The prefix is automatically detected from the registration
* options.
* @param suffix The user's suffix
* @returns {Intent} An Intent for the user.
*/
getIntentForSuffix(suffix: string): Intent;
/**
* Gets a full user ID for a given suffix. The prefix is automatically detected from the registration
* options.
* @param suffix The user's suffix
* @returns {string} The user's ID.
*/
getUserIdForSuffix(suffix: string): string;
/**
* Gets an Intent for a given user ID.
* @param {string} userId The user ID to get an Intent for.
* @returns {Intent} An Intent for the user.
*/
getIntentForUserId(userId: string): Intent;
/**
* Gets the suffix for the provided user ID. If the user ID is not a namespaced
* user, this will return a falsey value.
* @param {string} userId The user ID to parse
* @returns {string} The suffix from the user ID.
*/
getSuffixForUserId(userId: string): string;
/**
* Determines if a given user ID is namespaced by this application service.
* @param {string} userId The user ID to check
* @returns {boolean} true if the user is namespaced, false otherwise
*/
isNamespacedUser(userId: string): boolean;
/**
* Gets a full alias for a given localpart. The alias will be formed with the domain name given
* in the constructor.
* @param localpart The localpart to get an alias for.
* @returns {string} The alias.
*/
getAlias(localpart: string): string;
/**
* Gets a full alias for a given suffix. The prefix is automatically detected from the registration
* options.
* @param suffix The alias's suffix
* @returns {string} The alias.
*/
getAliasForSuffix(suffix: string): string;
/**
* Gets the localpart of an alias for a given suffix. The prefix is automatically detected from the registration
* options. Useful for the createRoom endpoint.
* @param suffix The alias's suffix
* @returns {string} The alias localpart.
*/
getAliasLocalpartForSuffix(suffix: string): string;
/**
* Gets the suffix for the provided alias. If the alias is not a namespaced
* alias, this will return a falsey value.
* @param {string} alias The alias to parse
* @returns {string} The suffix from the alias.
*/
getSuffixForAlias(alias: string): string;
/**
* Determines if a given alias is namespaced by this application service.
* @param {string} alias The alias to check
* @returns {boolean} true if the alias is namespaced, false otherwise
*/
isNamespacedAlias(alias: string): boolean;
/**
* Adds a preprocessor to the event pipeline. When this appservice encounters an event, it
* will try to run it through the preprocessors it can in the order they were added.
* @param {IPreprocessor} preprocessor the preprocessor to add
*/
addPreprocessor(preprocessor: IPreprocessor): void;
/**
* Sets the visibility of a room in the appservice's room directory.
* @param {string} networkId The network ID to group the room under.
* @param {string} roomId The room ID to manipulate the visibility of.
* @param {"public" | "private"} visibility The visibility to set for the room.
* @return {Promise<any>} resolves when the visibility has been updated.
*/
setRoomDirectoryVisibility(networkId: string, roomId: string, visibility: "public" | "private"): Promise<any>;
private processEphemeralEvent;
private processEvent;
private processMembershipEvent;
private isAuthed;
private onTransaction;
private onUser;
private onRoomAlias;
private onKeysClaim;
private onKeysQuery;
private onThirdpartyProtocol;
private handleThirdpartyObject;
private onThirdpartyUser;
private onThirdpartyLocation;
}

View File

@@ -0,0 +1,803 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Appservice = void 0;
const express = require("express");
const events_1 = require("events");
const morgan = require("morgan");
const LRU = require("lru-cache");
const querystring_1 = require("querystring");
const Intent_1 = require("./Intent");
const __1 = require("..");
const MatrixBridge_1 = require("./MatrixBridge");
const EDU_ANNOTATION_KEY = "io.t2bot.sdk.bot.type";
var EduAnnotation;
(function (EduAnnotation) {
EduAnnotation["ToDevice"] = "to_device";
EduAnnotation["Ephemeral"] = "ephemeral";
})(EduAnnotation || (EduAnnotation = {}));
/**
* Represents an application service. This provides helper utilities such as tracking
* of user intents (clients that are aware of their membership in rooms).
* @category Application services
*/
class Appservice extends events_1.EventEmitter {
/**
* Creates a new application service.
* @param {IAppserviceOptions} options The options for the application service.
*/
constructor(options) {
super();
this.options = options;
/**
* The metrics instance for this appservice. This will raise all metrics
* from this appservice instance as well as any intents/MatrixClients created
* by the appservice.
*/
this.metrics = new __1.Metrics();
this.bridgeInstance = new MatrixBridge_1.MatrixBridge(this);
this.app = express();
this.eventProcessors = {};
this.pendingTransactions = {};
options.joinStrategy = new __1.AppserviceJoinRoomStrategy(options.joinStrategy, this);
if (!options.intentOptions)
options.intentOptions = {};
if (!options.intentOptions.maxAgeMs)
options.intentOptions.maxAgeMs = 60 * 60 * 1000;
if (!options.intentOptions.maxCached)
options.intentOptions.maxCached = 10000;
this.intentsCache = new LRU.LRUCache({
max: options.intentOptions.maxCached,
ttl: options.intentOptions.maxAgeMs,
});
this.registration = options.registration;
// If protocol is not defined, define an empty array.
if (!this.registration.protocols) {
this.registration.protocols = [];
}
this.storage = options.storage || new __1.MemoryStorageProvider();
options.storage = this.storage;
this.cryptoStorage = options.cryptoStorage;
this.app.use(express.json({ limit: Number.MAX_SAFE_INTEGER })); // disable limits, use a reverse proxy
morgan.token('url-safe', (req) => `${req.path}?${(0, querystring_1.stringify)((0, __1.redactObjectForLogging)(req.query ?? {}))}`);
this.app.use(morgan(
// Same as "combined", but with sensitive values removed from requests.
':remote-addr - :remote-user [:date[clf]] ":method :url-safe HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"', { stream: { write: __1.LogService.info.bind(__1.LogService, 'Appservice') } }));
// ETag headers break the tests sometimes, and we don't actually need them anyways for
// appservices - none of this should be cached.
this.app.set('etag', false);
this.app.get("/users/:userId", this.onUser.bind(this));
this.app.get("/rooms/:roomAlias", this.onRoomAlias.bind(this));
this.app.put("/transactions/:txnId", this.onTransaction.bind(this));
this.app.get("/_matrix/app/v1/users/:userId", this.onUser.bind(this));
this.app.get("/_matrix/app/v1/rooms/:roomAlias", this.onRoomAlias.bind(this));
this.app.put("/_matrix/app/v1/transactions/:txnId", this.onTransaction.bind(this));
this.app.get("/_matrix/app/v1/thirdparty/protocol/:protocol", this.onThirdpartyProtocol.bind(this));
this.app.get("/_matrix/app/v1/thirdparty/user/:protocol", this.onThirdpartyUser.bind(this));
this.app.get("/_matrix/app/v1/thirdparty/user", this.onThirdpartyUser.bind(this));
this.app.get("/_matrix/app/v1/thirdparty/location/:protocol", this.onThirdpartyLocation.bind(this));
this.app.get("/_matrix/app/v1/thirdparty/location", this.onThirdpartyLocation.bind(this));
this.app.post("/_matrix/app/unstable/org.matrix.msc3983/keys/claim", this.onKeysClaim.bind(this));
this.app.post("/_matrix/app/unstable/org.matrix.msc3984/keys/query", this.onKeysQuery.bind(this));
// Workaround for https://github.com/matrix-org/synapse/issues/3780
this.app.post("/_matrix/app/v1/unstable/org.matrix.msc3983/keys/claim", this.onKeysClaim.bind(this));
this.app.post("/unstable/org.matrix.msc3983/keys/claim", this.onKeysClaim.bind(this));
this.app.post("/_matrix/app/v1/unstable/org.matrix.msc3984/keys/query", this.onKeysQuery.bind(this));
this.app.post("/unstable/org.matrix.msc3984/keys/query", this.onKeysQuery.bind(this));
// We register the 404 handler in the `begin()` function to allow consumers to add their own endpoints.
if (!this.registration.namespaces || !this.registration.namespaces.users || this.registration.namespaces.users.length === 0) {
throw new Error("No user namespaces in registration");
}
if (this.registration.namespaces.users.length !== 1) {
throw new Error("Too many user namespaces registered: expecting exactly one");
}
const userPrefix = (this.registration.namespaces.users[0].regex || "").split(":")[0];
if (!userPrefix.endsWith(".*") && !userPrefix.endsWith(".+")) {
this.userPrefix = null;
}
else {
this.userPrefix = userPrefix.substring(0, userPrefix.length - 2); // trim off the .* part
}
if (!this.registration.namespaces?.aliases || this.registration.namespaces.aliases.length !== 1) {
this.aliasPrefix = null;
}
else {
this.aliasPrefix = (this.registration.namespaces.aliases[0].regex || "").split(":")[0];
if (!this.aliasPrefix.endsWith(".*") && !this.aliasPrefix.endsWith(".+")) {
this.aliasPrefix = null;
}
else {
this.aliasPrefix = this.aliasPrefix.substring(0, this.aliasPrefix.length - 2); // trim off the .* part
}
}
}
/**
* Gets the express app instance which is serving requests. Not recommended for
* general usage, but may be used to append routes to the web server.
*/
get expressAppInstance() {
return this.app;
}
/**
* Gets the bridge-specific APIs for this application service.
*/
get bridge() {
return this.bridgeInstance;
}
/**
* Get the application service's "bot" user ID (the sender_localpart).
*/
get botUserId() {
return this.getUserId(this.registration.sender_localpart);
}
/**
* Get the application service's "bot" Intent (the sender_localpart).
* @returns {Intent} The intent for the application service itself.
*/
get botIntent() {
return this.getIntentForUserId(this.botUserId);
}
/**
* Get the application service's "bot" MatrixClient (the sender_localpart).
* Normally the botIntent should be used to ensure that the bot user is safely
* handled.
* @returns {MatrixClient} The client for the application service itself.
*/
get botClient() {
return this.botIntent.underlyingClient;
}
/**
* Starts the application service, opening the bind address to begin processing requests.
* @returns {Promise<void>} resolves when started
*/
begin() {
return new Promise((resolve, reject) => {
// Per constructor, all other endpoints should 404.
// Technically, according to https://spec.matrix.org/v1.6/application-service-api/#unknown-routes we should
// be returning 405 for *known* endpoints with the wrong method.
this.app.all("*", (req, res) => {
res.status(404).json({ errcode: "M_UNRECOGNIZED", error: "Endpoint not implemented" });
});
this.appServer = this.app.listen(this.options.port, this.options.bindAddress, () => resolve());
}).then(async () => {
if (this.options.intentOptions?.encryption) {
await this.botIntent.enableEncryption();
}
else {
await this.botIntent.ensureRegistered();
}
});
}
/**
* Stops the application service, freeing the web server.
*/
stop() {
if (!this.appServer)
return;
this.appServer.close();
}
/**
* Gets an intent for a given localpart. The user ID will be formed with the domain name given
* in the constructor.
* @param localpart The localpart to get an Intent for.
* @returns {Intent} An Intent for the user.
*/
getIntent(localpart) {
return this.getIntentForUserId(this.getUserId(localpart));
}
/**
* Gets a full user ID for a given localpart. The user ID will be formed with the domain name given
* in the constructor.
* @param localpart The localpart to get a user ID for.
* @returns {string} The user's ID.
*/
getUserId(localpart) {
return `@${localpart}:${this.options.homeserverName}`;
}
/**
* Gets an Intent for a given user suffix. The prefix is automatically detected from the registration
* options.
* @param suffix The user's suffix
* @returns {Intent} An Intent for the user.
*/
getIntentForSuffix(suffix) {
return this.getIntentForUserId(this.getUserIdForSuffix(suffix));
}
/**
* Gets a full user ID for a given suffix. The prefix is automatically detected from the registration
* options.
* @param suffix The user's suffix
* @returns {string} The user's ID.
*/
getUserIdForSuffix(suffix) {
if (!this.userPrefix) {
throw new Error(`Cannot use getUserIdForSuffix, provided namespace did not include a valid suffix`);
}
return `${this.userPrefix}${suffix}:${this.options.homeserverName}`;
}
/**
* Gets an Intent for a given user ID.
* @param {string} userId The user ID to get an Intent for.
* @returns {Intent} An Intent for the user.
*/
getIntentForUserId(userId) {
let intent = this.intentsCache.get(userId);
if (!intent) {
intent = new Intent_1.Intent(this.options, userId, this);
this.intentsCache.set(userId, intent);
if (this.options.intentOptions.encryption) {
intent.enableEncryption().catch(e => {
__1.LogService.error("Appservice", `Failed to set up crypto on intent ${userId}`, e);
throw e; // re-throw to cause unhandled exception
});
}
}
return intent;
}
/**
* Gets the suffix for the provided user ID. If the user ID is not a namespaced
* user, this will return a falsey value.
* @param {string} userId The user ID to parse
* @returns {string} The suffix from the user ID.
*/
getSuffixForUserId(userId) {
if (!this.userPrefix) {
throw new Error(`Cannot use getUserIdForSuffix, provided namespace did not include a valid suffix`);
}
if (!userId || !userId.startsWith(this.userPrefix) || !userId.endsWith(`:${this.options.homeserverName}`)) {
// Invalid ID
return null;
}
return userId
.split('')
.slice(this.userPrefix.length)
.reverse()
.slice(this.options.homeserverName.length + 1)
.reverse()
.join('');
}
/**
* Determines if a given user ID is namespaced by this application service.
* @param {string} userId The user ID to check
* @returns {boolean} true if the user is namespaced, false otherwise
*/
isNamespacedUser(userId) {
return userId === this.botUserId ||
!!this.registration.namespaces?.users.find(({ regex }) => new RegExp(regex).test(userId));
}
/**
* Gets a full alias for a given localpart. The alias will be formed with the domain name given
* in the constructor.
* @param localpart The localpart to get an alias for.
* @returns {string} The alias.
*/
getAlias(localpart) {
return `#${localpart}:${this.options.homeserverName}`;
}
/**
* Gets a full alias for a given suffix. The prefix is automatically detected from the registration
* options.
* @param suffix The alias's suffix
* @returns {string} The alias.
*/
getAliasForSuffix(suffix) {
if (!this.aliasPrefix) {
throw new Error("Invalid configured alias prefix");
}
return `${this.aliasPrefix}${suffix}:${this.options.homeserverName}`;
}
/**
* Gets the localpart of an alias for a given suffix. The prefix is automatically detected from the registration
* options. Useful for the createRoom endpoint.
* @param suffix The alias's suffix
* @returns {string} The alias localpart.
*/
getAliasLocalpartForSuffix(suffix) {
if (!this.aliasPrefix) {
throw new Error("Invalid configured alias prefix");
}
return `${this.aliasPrefix.substr(1)}${suffix}`;
}
/**
* Gets the suffix for the provided alias. If the alias is not a namespaced
* alias, this will return a falsey value.
* @param {string} alias The alias to parse
* @returns {string} The suffix from the alias.
*/
getSuffixForAlias(alias) {
if (!this.aliasPrefix) {
throw new Error("Invalid configured alias prefix");
}
if (!alias || !this.isNamespacedAlias(alias)) {
// Invalid ID
return null;
}
return alias
.split('')
.slice(this.aliasPrefix.length)
.reverse()
.slice(this.options.homeserverName.length + 1)
.reverse()
.join('');
}
/**
* Determines if a given alias is namespaced by this application service.
* @param {string} alias The alias to check
* @returns {boolean} true if the alias is namespaced, false otherwise
*/
isNamespacedAlias(alias) {
if (!this.aliasPrefix) {
throw new Error("Invalid configured alias prefix");
}
return alias.startsWith(this.aliasPrefix) && alias.endsWith(":" + this.options.homeserverName);
}
/**
* Adds a preprocessor to the event pipeline. When this appservice encounters an event, it
* will try to run it through the preprocessors it can in the order they were added.
* @param {IPreprocessor} preprocessor the preprocessor to add
*/
addPreprocessor(preprocessor) {
if (!preprocessor)
throw new Error("Preprocessor cannot be null");
const eventTypes = preprocessor.getSupportedEventTypes();
if (!eventTypes)
return; // Nothing to do
for (const eventType of eventTypes) {
if (!this.eventProcessors[eventType])
this.eventProcessors[eventType] = [];
this.eventProcessors[eventType].push(preprocessor);
}
}
/**
* Sets the visibility of a room in the appservice's room directory.
* @param {string} networkId The network ID to group the room under.
* @param {string} roomId The room ID to manipulate the visibility of.
* @param {"public" | "private"} visibility The visibility to set for the room.
* @return {Promise<any>} resolves when the visibility has been updated.
*/
setRoomDirectoryVisibility(networkId, roomId, visibility) {
roomId = encodeURIComponent(roomId);
networkId = encodeURIComponent(networkId);
return this.botClient.doRequest("PUT", `/_matrix/client/v3/directory/list/appservice/${networkId}/${roomId}`, null, {
visibility,
});
}
async processEphemeralEvent(event) {
if (!event)
return event;
if (!this.eventProcessors[event["type"]])
return event;
for (const processor of this.eventProcessors[event["type"]]) {
await processor.processEvent(event, this.botIntent.underlyingClient, __1.EventKind.EphemeralEvent);
}
return event;
}
async processEvent(event) {
if (!event)
return event;
if (!this.eventProcessors[event["type"]])
return event;
for (const processor of this.eventProcessors[event["type"]]) {
await processor.processEvent(event, this.botIntent.underlyingClient, __1.EventKind.RoomEvent);
}
return event;
}
async processMembershipEvent(event) {
if (!event["content"])
return;
const domain = new __1.UserID(event['state_key']).domain;
const botDomain = new __1.UserID(this.botUserId).domain;
if (domain !== botDomain)
return; // can't be impersonated, so don't try
// Update the target intent's joined rooms (fixes transition errors with the cache, like join->kick->join)
const intent = this.getIntentForUserId(event['state_key']);
await intent.refreshJoinedRooms();
const targetMembership = event["content"]["membership"];
if (targetMembership === "join") {
this.emit("room.join", event["room_id"], event);
await intent.underlyingClient.crypto?.onRoomJoin(event["room_id"]);
}
else if (targetMembership === "ban" || targetMembership === "leave") {
this.emit("room.leave", event["room_id"], event);
}
else if (targetMembership === "invite") {
this.emit("room.invite", event["room_id"], event);
}
}
isAuthed(req) {
let providedToken = req.query ? req.query["access_token"] : null;
if (req.headers && req.headers["authorization"]) {
const authHeader = req.headers["authorization"];
if (!authHeader.startsWith("Bearer "))
providedToken = null;
else
providedToken = authHeader.substring("Bearer ".length);
}
return providedToken === this.registration.hs_token;
}
async onTransaction(req, res) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
if (typeof (req.body) !== "object") {
res.status(400).json({ errcode: "BAD_REQUEST", error: "Expected JSON" });
return;
}
if (!req.body["events"] || !Array.isArray(req.body["events"])) {
res.status(400).json({ errcode: "BAD_REQUEST", error: "Invalid JSON: expected events" });
return;
}
const txnId = req.params["txnId"];
if (await Promise.resolve(this.storage.isTransactionCompleted(txnId))) {
res.status(200).json({});
return;
}
if (this.pendingTransactions[txnId]) {
try {
await this.pendingTransactions[txnId];
res.status(200).json({});
}
catch (e) {
__1.LogService.error("Appservice", e);
res.status(500).json({});
}
return;
}
__1.LogService.info("Appservice", "Processing transaction " + txnId);
// eslint-disable-next-line no-async-promise-executor
this.pendingTransactions[txnId] = new Promise(async (resolve) => {
// Process all the crypto stuff first to ensure that future transactions (if not this one)
// will decrypt successfully. We start with EDUs because we need structures to put counts
// and such into in a later stage, and EDUs are independent of crypto.
const byUserId = {};
const orderedEdus = [];
if (Array.isArray(req.body["de.sorunome.msc2409.to_device"])) {
orderedEdus.push(...req.body["de.sorunome.msc2409.to_device"].map(e => ({
...e,
unsigned: {
...e['unsigned'],
[EDU_ANNOTATION_KEY]: EduAnnotation.ToDevice,
},
})));
}
if (Array.isArray(req.body["de.sorunome.msc2409.ephemeral"])) {
orderedEdus.push(...req.body["de.sorunome.msc2409.ephemeral"].map(e => ({
...e,
unsigned: {
...e['unsigned'],
[EDU_ANNOTATION_KEY]: EduAnnotation.Ephemeral,
},
})));
}
for (let event of orderedEdus) {
if (event['edu_type'])
event['type'] = event['edu_type']; // handle property change during MSC2409's course
__1.LogService.info("Appservice", `Processing ${event['unsigned'][EDU_ANNOTATION_KEY]} event of type ${event["type"]}`);
event = await this.processEphemeralEvent(event);
// These events aren't tied to rooms, so just emit them generically
this.emit("ephemeral.event", event);
if (this.cryptoStorage && (event["type"] === "m.room.encrypted" || event.unsigned?.[EDU_ANNOTATION_KEY] === EduAnnotation.ToDevice)) {
const toUser = event["to_user_id"];
const intent = this.getIntentForUserId(toUser);
await intent.enableEncryption();
if (!byUserId[toUser])
byUserId[toUser] = { counts: null, toDevice: null, unusedFallbacks: null };
if (!byUserId[toUser].toDevice)
byUserId[toUser].toDevice = [];
byUserId[toUser].toDevice.push(event);
}
}
const deviceLists = req.body["org.matrix.msc3202.device_lists"] ?? {
changed: [],
removed: [],
};
if (!deviceLists.changed)
deviceLists.changed = [];
if (!deviceLists.removed)
deviceLists.removed = [];
if (deviceLists.changed.length || deviceLists.removed.length) {
this.emit("device_lists", deviceLists);
}
let otks = req.body["org.matrix.msc3202.device_one_time_keys_count"];
const otks2 = req.body["org.matrix.msc3202.device_one_time_key_counts"];
if (otks2 && !otks) {
__1.LogService.warn("Appservice", "Your homeserver is using an outdated field (device_one_time_key_counts) to talk to this appservice. " +
"If you're using Synapse, please upgrade to 1.73.0 or higher.");
otks = otks2;
}
if (otks) {
this.emit("otk.counts", otks);
}
if (otks && this.cryptoStorage) {
for (const userId of Object.keys(otks)) {
const intent = this.getIntentForUserId(userId);
await intent.enableEncryption();
const otksForUser = otks[userId][intent.underlyingClient.crypto.clientDeviceId];
if (otksForUser) {
if (!byUserId[userId]) {
byUserId[userId] = {
counts: null,
toDevice: null,
unusedFallbacks: null,
};
}
byUserId[userId].counts = otksForUser;
}
}
}
const fallbacks = req.body["org.matrix.msc3202.device_unused_fallback_key_types"];
if (fallbacks) {
this.emit("otk.unused_fallback_keys", fallbacks);
}
if (fallbacks && this.cryptoStorage) {
for (const userId of Object.keys(fallbacks)) {
const intent = this.getIntentForUserId(userId);
await intent.enableEncryption();
const fallbacksForUser = fallbacks[userId][intent.underlyingClient.crypto.clientDeviceId];
if (Array.isArray(fallbacksForUser) && !fallbacksForUser.includes(__1.OTKAlgorithm.Signed)) {
if (!byUserId[userId]) {
byUserId[userId] = {
counts: null,
toDevice: null,
unusedFallbacks: null,
};
}
byUserId[userId].unusedFallbacks = fallbacksForUser;
}
}
}
if (this.cryptoStorage) {
for (const userId of Object.keys(byUserId)) {
const intent = this.getIntentForUserId(userId);
await intent.enableEncryption();
const info = byUserId[userId];
const userStorage = this.storage.storageForUser(userId);
if (!info.toDevice)
info.toDevice = [];
if (!info.unusedFallbacks)
info.unusedFallbacks = JSON.parse(await userStorage.readValue("last_unused_fallbacks") || "[]");
if (!info.counts)
info.counts = JSON.parse(await userStorage.readValue("last_counts") || "{}");
__1.LogService.info("Appservice", `Updating crypto state for ${userId}`);
await intent.underlyingClient.crypto.updateSyncData(info.toDevice, info.counts, info.unusedFallbacks, deviceLists.changed, deviceLists.removed);
}
}
for (let event of req.body["events"]) {
__1.LogService.info("Appservice", `Processing event of type ${event["type"]}`);
event = await this.processEvent(event);
if (event['type'] === 'm.room.encrypted') {
this.emit("room.encrypted_event", event["room_id"], event);
if (this.cryptoStorage) {
try {
const encrypted = new __1.EncryptedRoomEvent(event);
const roomId = event['room_id'];
try {
event = (await this.botClient.crypto.decryptRoomEvent(encrypted, roomId)).raw;
event = await this.processEvent(event);
this.emit("room.decrypted_event", roomId, event);
// For logging purposes: show that the event was decrypted
__1.LogService.info("Appservice", `Processing decrypted event of type ${event["type"]}`);
}
catch (e1) {
__1.LogService.warn("Appservice", `Bot client was not able to decrypt ${roomId} ${event['event_id']} - trying other intents`);
let tryUserId;
try {
// TODO: This could be more efficient
const userIdsInRoom = await this.botClient.getJoinedRoomMembers(roomId);
tryUserId = userIdsInRoom.find(u => this.isNamespacedUser(u));
}
catch (e) {
__1.LogService.error("Appservice", "Failed to get members of room - cannot decrypt message");
}
if (tryUserId) {
const intent = this.getIntentForUserId(tryUserId);
event = (await intent.underlyingClient.crypto.decryptRoomEvent(encrypted, roomId)).raw;
event = await this.processEvent(event);
this.emit("room.decrypted_event", roomId, event);
// For logging purposes: show that the event was decrypted
__1.LogService.info("Appservice", `Processing decrypted event of type ${event["type"]}`);
}
else {
// noinspection ExceptionCaughtLocallyJS
throw e1;
}
}
}
catch (e) {
__1.LogService.error("Appservice", `Decryption error on ${event['room_id']} ${event['event_id']}`, e);
this.emit("room.failed_decryption", event['room_id'], event, e);
}
}
}
this.emit("room.event", event["room_id"], event);
if (event['type'] === 'm.room.message') {
this.emit("room.message", event["room_id"], event);
}
if (event['type'] === 'm.room.member' && this.isNamespacedUser(event['state_key'])) {
await this.processMembershipEvent(event);
}
if (event['type'] === 'm.room.tombstone' && event['state_key'] === '') {
this.emit("room.archived", event['room_id'], event);
}
if (event['type'] === 'm.room.create' && event['state_key'] === '' && event['content'] && event['content']['predecessor']) {
this.emit("room.upgraded", event['room_id'], event);
}
}
resolve();
});
try {
await this.pendingTransactions[txnId];
await Promise.resolve(this.storage.setTransactionCompleted(txnId));
res.status(200).json({});
}
catch (e) {
__1.LogService.error("Appservice", e);
res.status(500).json({});
}
}
async onUser(req, res) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
const userId = req.params["userId"];
this.emit("query.user", userId, async (result) => {
if (result.then)
result = await result;
if (result === false) {
res.status(404).json({ errcode: "USER_DOES_NOT_EXIST", error: "User not created" });
}
else {
const intent = this.getIntentForUserId(userId);
await intent.ensureRegistered();
if (result.display_name)
await intent.underlyingClient.setDisplayName(result.display_name);
if (result.avatar_mxc)
await intent.underlyingClient.setAvatarUrl(result.avatar_mxc);
res.status(200).json(result); // return result for debugging + testing
}
});
}
async onRoomAlias(req, res) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
const roomAlias = req.params["roomAlias"];
this.emit("query.room", roomAlias, async (result) => {
if (result.then)
result = await result;
if (result === false) {
res.status(404).json({ errcode: "ROOM_DOES_NOT_EXIST", error: "Room not created" });
}
else {
const intent = this.botIntent;
await intent.ensureRegistered();
result["room_alias_name"] = roomAlias.substring(1).split(':')[0];
result["__roomId"] = await intent.underlyingClient.createRoom(result);
res.status(200).json(result); // return result for debugging + testing
}
});
}
async onKeysClaim(req, res) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
if (typeof (req.body) !== "object") {
res.status(400).json({ errcode: "BAD_REQUEST", error: "Expected JSON" });
return;
}
let responded = false;
this.emit("query.key_claim", req.body, (result) => {
responded = true;
const handleResult = (result2) => {
if (!result2) {
res.status(404).json({ errcode: "M_UNRECOGNIZED", error: "Endpoint not implemented" });
return;
}
res.status(200).json(result2);
};
Promise.resolve(result).then(r => handleResult(r)).catch(e => {
__1.LogService.error("Appservice", "Error handling key claim API", e);
res.status(500).json({ errcode: "M_UNKNOWN", error: "Error handling key claim API" });
});
});
if (!responded) {
res.status(404).json({ errcode: "M_UNRECOGNIZED", error: "Endpoint not implemented" });
}
}
async onKeysQuery(req, res) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
if (typeof (req.body) !== "object") {
res.status(400).json({ errcode: "BAD_REQUEST", error: "Expected JSON" });
return;
}
let responded = false;
this.emit("query.key", req.body, (result) => {
responded = true;
const handleResult = (result2) => {
if (!result2) {
res.status(404).json({ errcode: "M_UNRECOGNIZED", error: "Endpoint not implemented" });
return;
}
// Implementation note: we could probably query the device keys from our storage if we wanted to.
res.status(200).json(result2);
};
Promise.resolve(result).then(r => handleResult(r)).catch(e => {
__1.LogService.error("Appservice", "Error handling key query API", e);
res.status(500).json({ errcode: "M_UNKNOWN", error: "Error handling key query API" });
});
});
if (!responded) {
res.status(404).json({ errcode: "M_UNRECOGNIZED", error: "Endpoint not implemented" });
}
}
onThirdpartyProtocol(req, res) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
const protocol = req.params["protocol"];
if (!this.registration.protocols.includes(protocol)) {
res.status(404).json({
errcode: "PROTOCOL_NOT_HANDLED",
error: "Protocol is not handled by this appservice",
});
return;
}
this.emit("thirdparty.protocol", protocol, (protocolResponse) => {
res.status(200).json(protocolResponse);
});
}
handleThirdpartyObject(req, res, objType, matrixId) {
if (!this.isAuthed(req)) {
res.status(401).json({ errcode: "AUTH_FAILED", error: "Authentication failed" });
return;
}
const protocol = req.params["protocol"];
const responseFunc = (items) => {
if (items && items.length > 0) {
res.status(200).json(items);
return;
}
res.status(404).json({
errcode: "NO_MAPPING_FOUND",
error: "No mappings found",
});
};
// Lookup remote objects(s)
if (protocol) { // If protocol is given, we are looking up a objects based on fields
if (!this.registration.protocols.includes(protocol)) {
res.status(404).json({
errcode: "PROTOCOL_NOT_HANDLED",
error: "Protocol is not handled by this appservice",
});
return;
}
// Remove the access_token
delete req.query.access_token;
this.emit(`thirdparty.${objType}.remote`, protocol, req.query, responseFunc);
return;
}
else if (matrixId) { // If a user ID is given, we are looking up a remote objects based on a id
this.emit(`thirdparty.${objType}.matrix`, matrixId, responseFunc);
return;
}
res.status(400).json({
errcode: "INVALID_PARAMETERS",
error: "Invalid parameters given",
});
}
onThirdpartyUser(req, res) {
return this.handleThirdpartyObject(req, res, "user", req.query["userid"]);
}
onThirdpartyLocation(req, res) {
return this.handleThirdpartyObject(req, res, "location", req.query["alias"]);
}
}
exports.Appservice = Appservice;
//# sourceMappingURL=Appservice.js.map

File diff suppressed because one or more lines are too long

113
node_modules/matrix-bot-sdk/lib/appservice/Intent.d.ts generated vendored Normal file
View File

@@ -0,0 +1,113 @@
import { MatrixClient, Metrics } from "..";
import { Appservice, IAppserviceOptions } from "./Appservice";
import { UnstableAppserviceApis } from "./UnstableAppserviceApis";
/**
* An Intent is an intelligent client that tracks things like the user's membership
* in rooms to ensure the action being performed is possible. This is very similar
* to how Intents work in the matrix-js-sdk in that the Intent will ensure that the
* user is joined to the room before posting a message, for example.
* @category Application services
*/
export declare class Intent {
private options;
private impersonateUserId;
private appservice;
/**
* The metrics instance for this intent. Note that this will not raise metrics
* for the underlying client - those will be available through this instance's
* parent (the appservice).
*/
readonly metrics: Metrics;
private readonly storage;
private readonly cryptoStorage;
private client;
private unstableApisInstance;
private knownJoinedRooms;
private cryptoSetupPromise;
/**
* Creates a new intent. Intended to be created by application services.
* @param {IAppserviceOptions} options The options for the application service.
* @param {string} impersonateUserId The user ID to impersonate.
* @param {Appservice} appservice The application service itself.
*/
constructor(options: IAppserviceOptions, impersonateUserId: string, appservice: Appservice);
private makeClient;
/**
* Gets the user ID this intent is for.
*/
get userId(): string;
/**
* Gets the underlying MatrixClient that powers this Intent.
*/
get underlyingClient(): MatrixClient;
/**
* Gets the unstable API access class. This is generally not recommended to be
* used by appservices.
* @return {UnstableAppserviceApis} The unstable API access class.
*/
get unstableApis(): UnstableAppserviceApis;
/**
* Sets up crypto on the client if it hasn't already been set up.
* @returns {Promise<void>} Resolves when complete.
*/
enableEncryption(): Promise<void>;
/**
* Gets the joined rooms for the intent. Note that by working around
* the intent to join rooms may yield inaccurate results.
* @returns {Promise<string[]>} Resolves to an array of room IDs where
* the intent is joined.
*/
getJoinedRooms(): Promise<string[]>;
/**
* Leaves the given room.
* @param {string} roomId The room ID to leave
* @param {string=} reason Optional reason to be included as the reason for leaving the room.
* @returns {Promise<any>} Resolves when the room has been left.
*/
leaveRoom(roomId: string, reason?: string): Promise<any>;
/**
* Joins the given room
* @param {string} roomIdOrAlias the room ID or alias to join
* @returns {Promise<string>} resolves to the joined room ID
*/
joinRoom(roomIdOrAlias: string): Promise<string>;
/**
* Sends a text message to a room.
* @param {string} roomId The room ID to send text to.
* @param {string} body The message body to send.
* @param {"m.text" | "m.emote" | "m.notice"} msgtype The message type to send.
* @returns {Promise<string>} Resolves to the event ID of the sent message.
*/
sendText(roomId: string, body: string, msgtype?: "m.text" | "m.emote" | "m.notice"): Promise<string>;
/**
* Sends an event to a room.
* @param {string} roomId The room ID to send the event to.
* @param {any} content The content of the event.
* @returns {Promise<string>} Resolves to the event ID of the sent event.
*/
sendEvent(roomId: string, content: any): Promise<string>;
/**
* Ensures the user is registered and joined to the given room.
* @param {string} roomId The room ID to join
* @returns {Promise<any>} Resolves when complete
*/
ensureRegisteredAndJoined(roomId: string): Promise<void>;
/**
* Ensures the user is joined to the given room
* @param {string} roomId The room ID to join
* @returns {Promise<any>} Resolves when complete
*/
ensureJoined(roomId: string): Promise<string>;
/**
* Refreshes which rooms the user is joined to, potentially saving time on
* calls like ensureJoined()
* @returns {Promise<string[]>} Resolves to the joined room IDs for the user.
*/
refreshJoinedRooms(): Promise<string[]>;
/**
* Ensures the user is registered
* @param deviceId An optional device ID to register with.
* @returns {Promise<any>} Resolves when complete
*/
ensureRegistered(deviceId?: string): Promise<any>;
}

363
node_modules/matrix-bot-sdk/lib/appservice/Intent.js generated vendored Normal file
View File

@@ -0,0 +1,363 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Intent = void 0;
const __1 = require("..");
// noinspection TypeScriptPreferShortImport
const decorators_1 = require("../metrics/decorators");
const UnstableAppserviceApis_1 = require("./UnstableAppserviceApis");
const MatrixError_1 = require("../models/MatrixError");
/**
* An Intent is an intelligent client that tracks things like the user's membership
* in rooms to ensure the action being performed is possible. This is very similar
* to how Intents work in the matrix-js-sdk in that the Intent will ensure that the
* user is joined to the room before posting a message, for example.
* @category Application services
*/
class Intent {
/**
* Creates a new intent. Intended to be created by application services.
* @param {IAppserviceOptions} options The options for the application service.
* @param {string} impersonateUserId The user ID to impersonate.
* @param {Appservice} appservice The application service itself.
*/
constructor(options, impersonateUserId, appservice) {
this.options = options;
this.impersonateUserId = impersonateUserId;
this.appservice = appservice;
this.knownJoinedRooms = [];
this.metrics = new __1.Metrics(appservice.metrics);
this.storage = options.storage;
this.cryptoStorage = options.cryptoStorage;
this.makeClient(false);
}
makeClient(withCrypto, accessToken) {
let cryptoStore;
const storage = this.storage?.storageForUser?.(this.userId);
if (withCrypto) {
cryptoStore = this.cryptoStorage?.storageForUser(this.userId);
if (!cryptoStore) {
throw new Error("Tried to set up client with crypto when not available");
}
if (!storage) {
throw new Error("Tried to set up client with crypto, but no persistent storage");
}
}
this.client = new __1.MatrixClient(this.options.homeserverUrl, accessToken ?? this.options.registration.as_token, storage, cryptoStore);
this.client.metrics = new __1.Metrics(this.appservice.metrics); // Metrics only go up by one parent
this.unstableApisInstance = new UnstableAppserviceApis_1.UnstableAppserviceApis(this.client);
if (this.impersonateUserId !== this.appservice.botUserId) {
this.client.impersonateUserId(this.impersonateUserId);
}
if (this.options.joinStrategy) {
this.client.setJoinStrategy(this.options.joinStrategy);
}
}
/**
* Gets the user ID this intent is for.
*/
get userId() {
return this.impersonateUserId;
}
/**
* Gets the underlying MatrixClient that powers this Intent.
*/
get underlyingClient() {
return this.client;
}
/**
* Gets the unstable API access class. This is generally not recommended to be
* used by appservices.
* @return {UnstableAppserviceApis} The unstable API access class.
*/
get unstableApis() {
return this.unstableApisInstance;
}
/**
* Sets up crypto on the client if it hasn't already been set up.
* @returns {Promise<void>} Resolves when complete.
*/
async enableEncryption() {
if (!this.cryptoSetupPromise) {
// eslint-disable-next-line no-async-promise-executor
this.cryptoSetupPromise = new Promise(async (resolve, reject) => {
try {
// Prepare a client first
await this.ensureRegistered();
const storage = this.storage?.storageForUser?.(this.userId);
this.client.impersonateUserId(this.userId); // make sure the devices call works
const cryptoStore = this.cryptoStorage?.storageForUser(this.userId);
if (!cryptoStore) {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to create crypto store");
}
// Try to impersonate a device ID
const ownDevices = await this.client.getOwnDevices();
let deviceId = await cryptoStore.getDeviceId();
if (!deviceId || !ownDevices.some(d => d.device_id === deviceId)) {
const deviceKeys = await this.client.getUserDevices([this.userId]);
const userDeviceKeys = deviceKeys.device_keys[this.userId];
if (userDeviceKeys) {
// We really should be validating signatures here, but we're actively looking
// for devices without keys to impersonate, so it should be fine. In theory,
// those devices won't even be present but we're cautious.
const devicesWithKeys = Array.from(Object.entries(userDeviceKeys))
.filter(d => d[0] === d[1].device_id && !!d[1].keys?.[`${__1.DeviceKeyAlgorithm.Curve25519}:${d[1].device_id}`])
.map(t => t[0]); // grab device ID from tuple
deviceId = ownDevices.find(d => !devicesWithKeys.includes(d.device_id))?.device_id;
}
}
let prepared = false;
if (deviceId) {
this.makeClient(true);
this.client.impersonateUserId(this.userId, deviceId);
// verify that the server supports impersonating the device
const respDeviceId = (await this.client.getWhoAmI()).device_id;
prepared = (respDeviceId === deviceId);
}
if (!prepared) {
// XXX: We work around servers that don't support device_id impersonation
const accessToken = await Promise.resolve(storage?.readValue("accessToken"));
if (!accessToken) {
const loginBody = {
type: "m.login.application_service",
identifier: {
type: "m.id.user",
user: this.userId,
},
};
const res = await this.client.doRequest("POST", "/_matrix/client/v3/login", {}, loginBody);
this.makeClient(true, res['access_token']);
storage.storeValue("accessToken", this.client.accessToken);
prepared = true;
}
else {
this.makeClient(true, accessToken);
prepared = true;
}
}
if (!prepared) { // noinspection ExceptionCaughtLocallyJS
throw new Error("Unable to establish a device ID");
}
// Now set up crypto
await this.client.crypto.prepare(await this.refreshJoinedRooms());
this.appservice.on("room.event", (roomId, event) => {
if (!this.knownJoinedRooms.includes(roomId))
return;
this.client.crypto.onRoomEvent(roomId, event);
});
resolve();
}
catch (e) {
reject(e);
}
});
}
return this.cryptoSetupPromise;
}
/**
* Gets the joined rooms for the intent. Note that by working around
* the intent to join rooms may yield inaccurate results.
* @returns {Promise<string[]>} Resolves to an array of room IDs where
* the intent is joined.
*/
async getJoinedRooms() {
await this.ensureRegistered();
if (this.knownJoinedRooms.length === 0)
await this.refreshJoinedRooms();
return this.knownJoinedRooms.map(r => r); // clone
}
/**
* Leaves the given room.
* @param {string} roomId The room ID to leave
* @param {string=} reason Optional reason to be included as the reason for leaving the room.
* @returns {Promise<any>} Resolves when the room has been left.
*/
async leaveRoom(roomId, reason) {
await this.ensureRegistered();
return this.client.leaveRoom(roomId, reason).then(async () => {
// Recalculate joined rooms now that we've left a room
await this.refreshJoinedRooms();
});
}
/**
* Joins the given room
* @param {string} roomIdOrAlias the room ID or alias to join
* @returns {Promise<string>} resolves to the joined room ID
*/
async joinRoom(roomIdOrAlias) {
await this.ensureRegistered();
return this.client.joinRoom(roomIdOrAlias).then(async (roomId) => {
// Recalculate joined rooms now that we've joined a room
await this.refreshJoinedRooms();
return roomId;
});
}
/**
* Sends a text message to a room.
* @param {string} roomId The room ID to send text to.
* @param {string} body The message body to send.
* @param {"m.text" | "m.emote" | "m.notice"} msgtype The message type to send.
* @returns {Promise<string>} Resolves to the event ID of the sent message.
*/
async sendText(roomId, body, msgtype = "m.text") {
return this.sendEvent(roomId, { body: body, msgtype: msgtype });
}
/**
* Sends an event to a room.
* @param {string} roomId The room ID to send the event to.
* @param {any} content The content of the event.
* @returns {Promise<string>} Resolves to the event ID of the sent event.
*/
async sendEvent(roomId, content) {
await this.ensureRegisteredAndJoined(roomId);
return this.client.sendMessage(roomId, content);
}
/**
* Ensures the user is registered and joined to the given room.
* @param {string} roomId The room ID to join
* @returns {Promise<any>} Resolves when complete
*/
async ensureRegisteredAndJoined(roomId) {
await this.ensureRegistered();
await this.ensureJoined(roomId);
}
/**
* Ensures the user is joined to the given room
* @param {string} roomId The room ID to join
* @returns {Promise<any>} Resolves when complete
*/
async ensureJoined(roomId) {
if (this.knownJoinedRooms.indexOf(roomId) !== -1) {
return;
}
await this.refreshJoinedRooms();
if (this.knownJoinedRooms.indexOf(roomId) !== -1) {
return;
}
const returnedRoomId = await this.client.joinRoom(roomId);
if (!this.knownJoinedRooms.includes(returnedRoomId)) {
this.knownJoinedRooms.push(returnedRoomId);
}
return returnedRoomId;
}
/**
* Refreshes which rooms the user is joined to, potentially saving time on
* calls like ensureJoined()
* @returns {Promise<string[]>} Resolves to the joined room IDs for the user.
*/
async refreshJoinedRooms() {
this.knownJoinedRooms = await this.client.getJoinedRooms();
return this.knownJoinedRooms.map(r => r); // clone
}
/**
* Ensures the user is registered
* @param deviceId An optional device ID to register with.
* @returns {Promise<any>} Resolves when complete
*/
async ensureRegistered(deviceId) {
if (!(await Promise.resolve(this.storage.isUserRegistered(this.userId)))) {
try {
const result = await this.client.doRequest("POST", "/_matrix/client/v3/register", null, {
type: "m.login.application_service",
username: this.userId.substring(1).split(":")[0],
device_id: deviceId,
});
// HACK: Workaround for unit tests
if (result['errcode']) {
// noinspection ExceptionCaughtLocallyJS
throw { body: result }; // eslint-disable-line no-throw-literal
}
this.client.impersonateUserId(this.userId, result["device_id"]);
}
catch (err) {
if (err instanceof MatrixError_1.MatrixError && err.errcode === "M_USER_IN_USE") {
await Promise.resolve(this.storage.addRegisteredUser(this.userId));
if (this.userId === this.appservice.botUserId) {
return null;
}
else {
__1.LogService.error("Appservice", "Error registering user: User ID is in use");
return null;
}
}
else {
__1.LogService.error("Appservice", "Encountered error registering user: ");
__1.LogService.error("Appservice", (0, __1.extractRequestError)(err));
}
throw err;
}
await Promise.resolve(this.storage.addRegisteredUser(this.userId));
}
}
}
exports.Intent = Intent;
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], Intent.prototype, "enableEncryption", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], Intent.prototype, "getJoinedRooms", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", Promise)
], Intent.prototype, "leaveRoom", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], Intent.prototype, "joinRoom", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, String]),
__metadata("design:returntype", Promise)
], Intent.prototype, "sendText", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], Intent.prototype, "sendEvent", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], Intent.prototype, "ensureRegisteredAndJoined", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], Intent.prototype, "ensureJoined", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], Intent.prototype, "refreshJoinedRooms", null);
__decorate([
(0, decorators_1.timedIntentFunctionCall)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], Intent.prototype, "ensureRegistered", null);
//# sourceMappingURL=Intent.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,85 @@
import { Appservice } from "./Appservice";
import { Intent } from "./Intent";
export declare const REMOTE_USER_INFO_ACCOUNT_DATA_EVENT_TYPE = "io.t2bot.sdk.bot.remote_user_info";
export declare const REMOTE_ROOM_INFO_ACCOUNT_DATA_EVENT_TYPE = "io.t2bot.sdk.bot.remote_room_info";
export declare const REMOTE_USER_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX = "io.t2bot.sdk.bot.remote_user_map";
export declare const REMOTE_ROOM_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX = "io.t2bot.sdk.bot.remote_room_map";
/**
* @see MatrixBridge
* @category Application services
*/
export interface IRemoteRoomInfo {
/**
* A unique identifier for the remote user.
*/
id: string;
}
/**
* @see MatrixBridge
* @category Application services
*/
export interface IRemoteUserInfo {
/**
* A unique identifier for the remote room (or room equivalent).
*/
id: string;
}
/**
* Utility class for common operations performed by bridges (represented
* as appservices).
*
* The storage utilities are not intended for bridges which allow 1:many
* relationships with the remote network.
*
* Bridges are generally expected to create their own classes which extend
* the IRemoteRoomInfo and IRemoteUserInfo interfaces and serialize to JSON
* cleanly. The serialized version of these classes is persisted in various
* account data locations for future lookups.
* @category Application services
*/
export declare class MatrixBridge {
private appservice;
constructor(appservice: Appservice);
/**
* Gets information about a remote user.
* @param {Intent} userIntent The Matrix user intent to get information on.
* @returns {Promise<IRemoteUserInfo>} Resolves to the remote user information.
*/
getRemoteUserInfo<T extends IRemoteUserInfo>(userIntent: Intent): Promise<T>;
/**
* Sets information about a remote user. Calling this function will map the
* provided remote user ID to the intent's owner.
* @param {Intent} userIntent The Matrix user intent to store information on.
* @param {IRemoteUserInfo} remoteInfo The remote user information to store
* @returns {Promise<any>} Resolves when the information has been updated.
*/
setRemoteUserInfo<T extends IRemoteUserInfo>(userIntent: Intent, remoteInfo: T): Promise<any>;
/**
* Gets information about a remote room.
* @param {string} matrixRoomId The Matrix room ID to get information on.
* @returns {Promise<IRemoteRoomInfo>} Resolves to the remote room information.
*/
getRemoteRoomInfo<T extends IRemoteRoomInfo>(matrixRoomId: string): Promise<T>;
/**
* Sets information about a remote room. Calling this function will map the
* provided remote room ID to the matrix room ID.
* @param {string} matrixRoomId The Matrix room ID to store information on.
* @param {IRemoteRoomInfo} remoteInfo The remote room information to store
* @returns {Promise<any>} Resolves when the information has been updated.
*/
setRemoteRoomInfo<T extends IRemoteRoomInfo>(matrixRoomId: string, remoteInfo: T): Promise<any>;
/**
* Gets the Matrix room ID for the provided remote room ID.
* @param {string} remoteRoomId The remote room ID to look up.
* @returns {Promise<string>} Resolves to the Matrix room ID.
*/
getMatrixRoomIdForRemote(remoteRoomId: string): Promise<string>;
/**
* Gets a Matrix user intent for the provided remote user ID.
* @param {string} remoteUserId The remote user ID to look up.
* @returns {Promise<Intent>} Resolves to the Matrix user intent.
*/
getIntentForRemote(remoteUserId: string): Promise<Intent>;
private updateRemoteUserMapping;
private updateRemoteRoomMapping;
}

View File

@@ -0,0 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MatrixBridge = exports.REMOTE_ROOM_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX = exports.REMOTE_USER_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX = exports.REMOTE_ROOM_INFO_ACCOUNT_DATA_EVENT_TYPE = exports.REMOTE_USER_INFO_ACCOUNT_DATA_EVENT_TYPE = void 0;
exports.REMOTE_USER_INFO_ACCOUNT_DATA_EVENT_TYPE = "io.t2bot.sdk.bot.remote_user_info";
exports.REMOTE_ROOM_INFO_ACCOUNT_DATA_EVENT_TYPE = "io.t2bot.sdk.bot.remote_room_info";
exports.REMOTE_USER_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX = "io.t2bot.sdk.bot.remote_user_map";
exports.REMOTE_ROOM_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX = "io.t2bot.sdk.bot.remote_room_map";
/**
* Utility class for common operations performed by bridges (represented
* as appservices).
*
* The storage utilities are not intended for bridges which allow 1:many
* relationships with the remote network.
*
* Bridges are generally expected to create their own classes which extend
* the IRemoteRoomInfo and IRemoteUserInfo interfaces and serialize to JSON
* cleanly. The serialized version of these classes is persisted in various
* account data locations for future lookups.
* @category Application services
*/
class MatrixBridge {
constructor(appservice) {
this.appservice = appservice;
}
/**
* Gets information about a remote user.
* @param {Intent} userIntent The Matrix user intent to get information on.
* @returns {Promise<IRemoteUserInfo>} Resolves to the remote user information.
*/
async getRemoteUserInfo(userIntent) {
await userIntent.ensureRegistered();
return userIntent.underlyingClient.getAccountData(exports.REMOTE_USER_INFO_ACCOUNT_DATA_EVENT_TYPE);
}
/**
* Sets information about a remote user. Calling this function will map the
* provided remote user ID to the intent's owner.
* @param {Intent} userIntent The Matrix user intent to store information on.
* @param {IRemoteUserInfo} remoteInfo The remote user information to store
* @returns {Promise<any>} Resolves when the information has been updated.
*/
async setRemoteUserInfo(userIntent, remoteInfo) {
await userIntent.ensureRegistered();
await userIntent.underlyingClient.setAccountData(exports.REMOTE_USER_INFO_ACCOUNT_DATA_EVENT_TYPE, remoteInfo);
await this.updateRemoteUserMapping(userIntent.userId, remoteInfo.id);
}
/**
* Gets information about a remote room.
* @param {string} matrixRoomId The Matrix room ID to get information on.
* @returns {Promise<IRemoteRoomInfo>} Resolves to the remote room information.
*/
async getRemoteRoomInfo(matrixRoomId) {
const bridgeBot = this.appservice.botIntent;
await bridgeBot.ensureRegistered();
// We do not need to ensure the user is joined to the room because we can associate
// room account data with any arbitrary room.
return bridgeBot.underlyingClient.getRoomAccountData(exports.REMOTE_ROOM_INFO_ACCOUNT_DATA_EVENT_TYPE, matrixRoomId);
}
/**
* Sets information about a remote room. Calling this function will map the
* provided remote room ID to the matrix room ID.
* @param {string} matrixRoomId The Matrix room ID to store information on.
* @param {IRemoteRoomInfo} remoteInfo The remote room information to store
* @returns {Promise<any>} Resolves when the information has been updated.
*/
async setRemoteRoomInfo(matrixRoomId, remoteInfo) {
const bridgeBot = this.appservice.botIntent;
await bridgeBot.ensureRegistered();
// We do not need to ensure the user is joined to the room because we can associate
// room account data with any arbitrary room.
await bridgeBot.underlyingClient.setRoomAccountData(exports.REMOTE_ROOM_INFO_ACCOUNT_DATA_EVENT_TYPE, matrixRoomId, remoteInfo);
await this.updateRemoteRoomMapping(matrixRoomId, remoteInfo.id);
}
/**
* Gets the Matrix room ID for the provided remote room ID.
* @param {string} remoteRoomId The remote room ID to look up.
* @returns {Promise<string>} Resolves to the Matrix room ID.
*/
async getMatrixRoomIdForRemote(remoteRoomId) {
const eventType = `${exports.REMOTE_ROOM_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX}.${remoteRoomId}`;
const bridgeBot = this.appservice.botIntent;
await bridgeBot.ensureRegistered();
const result = await bridgeBot.underlyingClient.getAccountData(eventType);
return result['id'];
}
/**
* Gets a Matrix user intent for the provided remote user ID.
* @param {string} remoteUserId The remote user ID to look up.
* @returns {Promise<Intent>} Resolves to the Matrix user intent.
*/
async getIntentForRemote(remoteUserId) {
const eventType = `${exports.REMOTE_USER_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX}.${remoteUserId}`;
const bridgeBot = this.appservice.botIntent;
await bridgeBot.ensureRegistered();
const result = await bridgeBot.underlyingClient.getAccountData(eventType);
return this.appservice.getIntentForUserId(result['id']);
}
async updateRemoteUserMapping(matrixUserId, remoteUserId) {
const eventType = `${exports.REMOTE_USER_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX}.${remoteUserId}`;
const bridgeBot = this.appservice.botIntent;
await bridgeBot.ensureRegistered();
await bridgeBot.underlyingClient.setAccountData(eventType, { id: matrixUserId });
}
async updateRemoteRoomMapping(matrixRoomId, remoteRoomId) {
const eventType = `${exports.REMOTE_ROOM_MAP_ACCOUNT_DATA_EVENT_TYPE_PREFIX}.${remoteRoomId}`;
const bridgeBot = this.appservice.botIntent;
await bridgeBot.ensureRegistered();
await bridgeBot.underlyingClient.setAccountData(eventType, { id: matrixRoomId });
}
}
exports.MatrixBridge = MatrixBridge;
//# sourceMappingURL=MatrixBridge.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"MatrixBridge.js","sourceRoot":"","sources":["../../src/appservice/MatrixBridge.ts"],"names":[],"mappings":";;;AAGa,QAAA,wCAAwC,GAAG,mCAAmC,CAAC;AAC/E,QAAA,wCAAwC,GAAG,mCAAmC,CAAC;AAC/E,QAAA,8CAA8C,GAAG,kCAAkC,CAAC;AACpF,QAAA,8CAA8C,GAAG,kCAAkC,CAAC;AAwBjG;;;;;;;;;;;;GAYG;AACH,MAAa,YAAY;IACrB,YAAoB,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAC1C,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,iBAAiB,CAA4B,UAAkB;QACxE,MAAM,UAAU,CAAC,gBAAgB,EAAE,CAAC;QACpC,OAAmB,UAAU,CAAC,gBAAgB,CAAC,cAAc,CAAC,gDAAwC,CAAC,CAAC;IAC5G,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,iBAAiB,CAA4B,UAAkB,EAAE,UAAa;QACvF,MAAM,UAAU,CAAC,gBAAgB,EAAE,CAAC;QACpC,MAAM,UAAU,CAAC,gBAAgB,CAAC,cAAc,CAAC,gDAAwC,EAAE,UAAU,CAAC,CAAC;QACvG,MAAM,IAAI,CAAC,uBAAuB,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,iBAAiB,CAA4B,YAAoB;QAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC5C,MAAM,SAAS,CAAC,gBAAgB,EAAE,CAAC;QACnC,mFAAmF;QACnF,6CAA6C;QAC7C,OAAmB,SAAS,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,gDAAwC,EAAE,YAAY,CAAC,CAAC;IAC7H,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,iBAAiB,CAA4B,YAAoB,EAAE,UAAa;QACzF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC5C,MAAM,SAAS,CAAC,gBAAgB,EAAE,CAAC;QACnC,mFAAmF;QACnF,6CAA6C;QAC7C,MAAM,SAAS,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,gDAAwC,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;QACxH,MAAM,IAAI,CAAC,uBAAuB,CAAC,YAAY,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,wBAAwB,CAAC,YAAoB;QACtD,MAAM,SAAS,GAAG,GAAG,sDAA8C,IAAI,YAAY,EAAE,CAAC;QACtF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC5C,MAAM,SAAS,CAAC,gBAAgB,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QAC1E,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,kBAAkB,CAAC,YAAoB;QAChD,MAAM,SAAS,GAAG,GAAG,sDAA8C,IAAI,YAAY,EAAE,CAAC;QACtF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC5C,MAAM,SAAS,CAAC,gBAAgB,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,CAAC;IAEO,KAAK,CAAC,uBAAuB,CAAC,YAAoB,EAAE,YAAoB;QAC5E,MAAM,SAAS,GAAG,GAAG,sDAA8C,IAAI,YAAY,EAAE,CAAC;QACtF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC5C,MAAM,SAAS,CAAC,gBAAgB,EAAE,CAAC;QACnC,MAAM,SAAS,CAAC,gBAAgB,CAAC,cAAc,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;IACrF,CAAC;IAEO,KAAK,CAAC,uBAAuB,CAAC,YAAoB,EAAE,YAAoB;QAC5E,MAAM,SAAS,GAAG,GAAG,sDAA8C,IAAI,YAAY,EAAE,CAAC;QACtF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAC5C,MAAM,SAAS,CAAC,gBAAgB,EAAE,CAAC;QACnC,MAAM,SAAS,CAAC,gBAAgB,CAAC,cAAc,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;IACrF,CAAC;CACJ;AA/FD,oCA+FC"}

View File

@@ -0,0 +1,41 @@
import { MatrixClient } from "../MatrixClient";
import { MSC2716BatchSendResponse } from "../models/MSC2176";
/**
* Unstable APIs that shouldn't be used in most circumstances for appservices.
* @category Unstable APIs
*/
export declare class UnstableAppserviceApis {
private client;
private requestId;
constructor(client: MatrixClient);
/**
* Send several historical events into a room.
* @see https://github.com/matrix-org/matrix-doc/pull/2716
* @param {string} roomId The roomID to send to.
* @param {string} prevEventId The event ID where this batch will be inserted
* @param {string} chunkId The chunk ID returned from a previous call. Set falsy to start at the beginning.
* @param {any[]} events A set of event contents for events to be inserted into the room.
* @param {any[]} stateEventsAtStart A set of state events to be inserted into the room. Defaults to empty.
* @returns A set of eventIds and the next chunk ID
*/
sendHistoricalEventBatch(roomId: string, prevEventId: string, events: any[], stateEventsAtStart?: any[], chunkId?: string): Promise<MSC2716BatchSendResponse>;
/**
* Sends an event to the given room with a given timestamp.
* @param {string} roomId the room ID to send the event to
* @param {string} eventType the type of event to send
* @param {string} content the event body to send
* @param {number} ts The origin_server_ts of the new event
* @returns {Promise<string>} resolves to the event ID that represents the event
*/
sendEventWithTimestamp(roomId: string, eventType: string, content: any, ts: number): Promise<any>;
/**
* Sends a state event to the given room with a given timestamp.
* @param {string} roomId the room ID to send the event to
* @param {string} type the event type to send
* @param {string} stateKey the state key to send, should not be null
* @param {string} content the event body to send
* @param {number} ts The origin_server_ts of the new event
* @returns {Promise<string>} resolves to the event ID that represents the message
*/
sendStateEventWithTimestamp(roomId: string, type: string, stateKey: string, content: any, ts: number): Promise<string>;
}

View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnstableAppserviceApis = void 0;
/**
* Unstable APIs that shouldn't be used in most circumstances for appservices.
* @category Unstable APIs
*/
class UnstableAppserviceApis {
constructor(client) {
this.client = client;
this.requestId = 0;
}
/**
* Send several historical events into a room.
* @see https://github.com/matrix-org/matrix-doc/pull/2716
* @param {string} roomId The roomID to send to.
* @param {string} prevEventId The event ID where this batch will be inserted
* @param {string} chunkId The chunk ID returned from a previous call. Set falsy to start at the beginning.
* @param {any[]} events A set of event contents for events to be inserted into the room.
* @param {any[]} stateEventsAtStart A set of state events to be inserted into the room. Defaults to empty.
* @returns A set of eventIds and the next chunk ID
*/
async sendHistoricalEventBatch(roomId, prevEventId, events, stateEventsAtStart = [], chunkId) {
return this.client.doRequest("POST", `/_matrix/client/unstable/org.matrix.msc2716/rooms/${encodeURIComponent(roomId)}/batch_send`, {
prev_event: prevEventId,
chunk_id: chunkId,
}, {
events,
state_events_at_start: stateEventsAtStart,
});
}
/**
* Sends an event to the given room with a given timestamp.
* @param {string} roomId the room ID to send the event to
* @param {string} eventType the type of event to send
* @param {string} content the event body to send
* @param {number} ts The origin_server_ts of the new event
* @returns {Promise<string>} resolves to the event ID that represents the event
*/
async sendEventWithTimestamp(roomId, eventType, content, ts) {
const txnId = `${(new Date().getTime())}__inc_appts${++this.requestId}`;
const path = `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`;
const response = await this.client.doRequest("PUT", path, { ts }, content);
return response.event_id;
}
/**
* Sends a state event to the given room with a given timestamp.
* @param {string} roomId the room ID to send the event to
* @param {string} type the event type to send
* @param {string} stateKey the state key to send, should not be null
* @param {string} content the event body to send
* @param {number} ts The origin_server_ts of the new event
* @returns {Promise<string>} resolves to the event ID that represents the message
*/
async sendStateEventWithTimestamp(roomId, type, stateKey, content, ts) {
const path = `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(type)}/${encodeURIComponent(stateKey)}`;
const response = await this.client.doRequest("PUT", path, { ts }, content);
return response.event_id;
}
}
exports.UnstableAppserviceApis = UnstableAppserviceApis;
//# sourceMappingURL=UnstableAppserviceApis.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"UnstableAppserviceApis.js","sourceRoot":"","sources":["../../src/appservice/UnstableAppserviceApis.ts"],"names":[],"mappings":";;;AAGA;;;GAGG;AACH,MAAa,sBAAsB;IAG/B,YAAoB,MAAoB;QAApB,WAAM,GAAN,MAAM,CAAc;QAFhC,cAAS,GAAG,CAAC,CAAC;IAGtB,CAAC;IAED;;;;;;;;;OASG;IACI,KAAK,CAAC,wBAAwB,CAAC,MAAc,EAAE,WAAmB,EAAE,MAAa,EAAE,qBAA4B,EAAE,EAAE,OAAgB;QACtI,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,qDAAqD,kBAAkB,CAAC,MAAM,CAAC,aAAa,EAAE;YAC/H,UAAU,EAAE,WAAW;YACvB,QAAQ,EAAE,OAAO;SACpB,EAAE;YACC,MAAM;YACN,qBAAqB,EAAE,kBAAkB;SAC5C,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,sBAAsB,CAAC,MAAc,EAAE,SAAiB,EAAE,OAAY,EAAE,EAAU;QAC3F,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QACxE,MAAM,IAAI,GAAG,4BAA4B,kBAAkB,CAAC,MAAM,CAAC,SAAS,kBAAkB,CAAC,SAAS,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;QACzI,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;QAC3E,OAAO,QAAQ,CAAC,QAAQ,CAAC;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACI,KAAK,CAAC,2BAA2B,CAAC,MAAc,EAAE,IAAY,EAAE,QAAgB,EAAE,OAAY,EAAE,EAAU;QAC7G,MAAM,IAAI,GAAG,4BAA4B,kBAAkB,CAAC,MAAM,CAAC,UAAU,kBAAkB,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxI,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;QAC3E,OAAO,QAAQ,CAAC,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAvDD,wDAuDC"}

View File

@@ -0,0 +1,77 @@
/**
* This is the response format documented in
* https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-thirdparty-protocol-protocol
* @category Application services
*/
export interface IApplicationServiceProtocol {
user_fields: string[];
location_fields: string[];
icon: string;
field_types: {
[field: string]: IFieldType;
};
instances: {
[name: string]: IProtocolInstance;
};
}
interface IFieldType {
regexp: string;
placeholder: string;
}
interface IProtocolInstance {
desc: string;
icon: string;
fields: {
[field: string]: string;
};
network_id: string;
}
/**
* This is the response format for an MSC3983 `/keys/claim` request.
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3983
* @deprecated This can be removed at any time without notice as it is unstable functionality.
* @category Application services
*/
export interface MSC3983KeyClaimResponse {
[userId: string]: {
[deviceId: string]: {
[keyId: string]: {
key: string;
signatures: {
[userId: string]: {
[keyId: string]: string;
};
};
};
};
};
}
/**
* This is the response format for an MSC3984 `/keys/query` request.
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3984
* @deprecated This can be removed at any time without notice as it is unstable functionality.
* @category Application services
*/
export interface MSC3984KeyQueryResponse {
device_keys: {
[userId: string]: {
[deviceId: string]: {
algorithms: string[];
device_id: string;
user_id: string;
keys: {
[keyId: string]: string;
};
signatures: {
[userId: string]: {
[keyId: string]: string;
};
};
unsigned?: {
[key: string]: any;
};
};
};
};
}
export {};

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=http_responses.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"http_responses.js","sourceRoot":"","sources":["../../src/appservice/http_responses.ts"],"names":[],"mappings":""}