Threads, timelines, auth

This commit is contained in:
tezlm 2023-12-14 09:48:45 -08:00
parent 3a5c212f51
commit b0cf2ad840
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
38 changed files with 878 additions and 343 deletions

View file

@ -2,6 +2,7 @@
"imports": {
"nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts",
"events": "https://deno.land/x/events@v1.0.0/mod.ts",
"typed-emitter": "npm:typed-emitter"
"typed-emitter": "npm:typed-emitter",
"@matrix-org/matrix-sdk-crypto-wasm": "npm:@matrix-org/matrix-sdk-crypto-wasm"
}
}

21
dist/src/api.js vendored
View file

@ -1,3 +1,24 @@
// all the types for the api
export {};
// export interface WellKnownClient {
// baseUrl: string,
// }
// export interface ServerConfig {
// components: {
// api: string,
// sync: string,
// media: string,
// voip: string,
// },
// server: {
// name: string,
// version: string,
// url: string,
// },
// admins: Array<{
// user_id: string,
// email_address?: string,
// purposes: Array<"m.admin" | "m.moderation" | "m.security">,
// }>,
// }
//# sourceMappingURL=api.js.map

2
dist/src/api.js.map vendored
View file

@ -1 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"AAAA,4BAA4B"}
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"AAAA,4BAA4B;;AAwP5B,qCAAqC;AACrC,qBAAqB;AACrB,IAAI;AAEJ,kCAAkC;AAClC,kBAAkB;AAClB,mBAAmB;AACnB,oBAAoB;AACpB,qBAAqB;AACrB,oBAAoB;AACpB,OAAO;AACP,cAAc;AACd,oBAAoB;AACpB,uBAAuB;AACvB,mBAAmB;AACnB,OAAO;AACP,oBAAoB;AACpB,uBAAuB;AACvB,8BAA8B;AAC9B,kEAAkE;AAClE,QAAQ;AACR,IAAI"}

10
dist/src/client.d.ts vendored
View file

@ -3,13 +3,13 @@ import { Network } from "./net.js";
import { Room } from "./room.js";
import { Connection } from "./sync.js";
import TypedEmitter from "typed-emitter";
interface ClientConfig {
export interface ClientConfig {
baseUrl: string;
token: string;
userId: string;
deviceId: string;
}
type ClientState = {
export type ClientState = {
state: "stop";
} | {
state: "sync";
@ -21,6 +21,9 @@ type ClientState = {
} | {
state: "retry";
backoff: number;
} | {
state: "logout";
soft: boolean;
};
type ClientEvents = {
state: (state: ClientState) => void;
@ -30,7 +33,7 @@ type ClientEvents = {
accountData: (type: string, content: string) => void;
toDevice: (event: ApiDeviceEvent) => void;
};
type RoomList = {
export type RoomList = {
count: number;
rooms: Array<Room>;
};
@ -69,5 +72,6 @@ export declare class Client extends Client_base {
private setState;
start(): void;
stop(): void;
logout(): Promise<void>;
}
export {};

7
dist/src/client.js vendored
View file

@ -119,9 +119,14 @@ export class Client extends EventEmitter {
}
// Stop receiving events from /sync.
stop() {
this.conn.abort();
this.conn.abort("stop sync");
this.conn = new Connection(this);
this.setState({ state: "stop" });
}
async logout() {
this.stop();
await this.net.authLogout();
this.setState({ state: "logout", soft: false });
}
}
//# sourceMappingURL=client.js.map

View file

@ -1 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}

10
dist/src/index.d.ts vendored
View file

@ -1,6 +1,8 @@
export { Client } from "./client.js";
export { Setup } from "./setup.js";
export { ThreadPaginator } from "./room.js";
export type { Event, StateEvent } from "./event.js";
export type { Room } from "./room.js";
export type { Thread } from "./thread.js";
export type { Timeline } from "./timeline.js";
export { Event, StateEvent } from "./event.js";
export { Room } from "./room.js";
export { Thread } from "./thread.js";
export type { RoomList, ClientState } from "./client.js";
export type { UserId, RoomId, EventId } from "./api.js";

4
dist/src/index.js vendored
View file

@ -1,3 +1,7 @@
export { Client } from "./client.js";
export { Setup } from "./setup.js";
export { ThreadPaginator } from "./room.js";
export { Event, StateEvent } from "./event.js";
export { Room } from "./room.js";
export { Thread } from "./thread.js";
//# sourceMappingURL=index.js.map

View file

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}

3
dist/src/net.d.ts vendored
View file

@ -47,5 +47,8 @@ export declare class Network {
sendEvent(roomId: t.RoomId, type: string, txnId: string, content: any): Promise<t.SendResponse>;
sendState(roomId: t.RoomId, type: string, stateKey: string, content: any): Promise<t.SendResponse>;
ack(acks: t.AckRequest): Promise<any>;
authLogin(): Promise<void>;
authRegister(): Promise<void>;
authLogout(): Promise<any>;
}
export {};

46
dist/src/net.js vendored
View file

@ -76,8 +76,19 @@ export class Network {
body: (isJson ? JSON.stringify(options.body) : options.body),
...options.extra,
});
if (!req.ok)
throw new Error(`Request failed: ${await req.text()}`);
// FIXME: handle uiaa and make this less awful
if (!req.ok) {
// const response = await req.json();
// throw { response, error: new Error(`Request failed: ${await req.text()}`) };
if (req.status === 401) {
this.client.stop();
this.client.setState({ state: "logout" });
throw new Error(`Request failed: ${await req.text()}`);
}
else {
throw new Error(`Request failed: ${await req.text()}`);
}
}
if (options.raw)
return req.body;
return req.json();
@ -188,5 +199,36 @@ export class Network {
body: acks,
});
}
async authLogin() {
throw new Error("todo!");
// return this.fetch({
// method: "POST",
// path: `/_matrix/client/v3/login`,
// body: {},
// });
}
async authRegister() {
throw new Error("todo!");
// return this.fetch({
// method: "POST",
// path: `/_matrix/client/v3/login`,
// body: {},
// });
}
// public async authGuest() {
// throw new Error("todo!")
// // return this.fetch({
// // method: "POST",
// // path: `/_matrix/client/v3/login`,
// // body: {},
// // });
// }
async authLogout() {
return this.fetch({
method: "POST",
path: `/_matrix/client/v3/logout`,
body: {},
});
}
}
//# sourceMappingURL=net.js.map

2
dist/src/net.js.map vendored

File diff suppressed because one or more lines are too long

8
dist/src/room.d.ts vendored
View file

@ -1,12 +1,10 @@
import TypedEmitter from "typed-emitter";
import { ApiEphemeralEvent, EventId, IncludeThreads, SyncResponseRoom, Unreads } from "./api.js";
import { EventId, IncludeThreads, SyncResponseRoom, Unreads } from "./api.js";
import { Client } from "./client.js";
import { Event, StateEvent } from "./event.js";
import { TimelineSet } from "./timeline.js";
import { RoomTimelineSet } from "./timeline.js";
import { Thread } from "./thread.js";
type RoomEvents = {
timeline: (event: Event) => void;
ephemeral: (event: ApiEphemeralEvent) => void;
thread: (thread: Thread) => void;
state: (event: StateEvent) => void;
notifications: (notifs: Unreads) => void;
@ -21,7 +19,7 @@ export declare class Room extends Room_base {
client: Client;
id: string;
private state;
timelines: TimelineSet;
timelines: RoomTimelineSet;
events: Map<EventId, Event>;
threads: RoomThreads;
constructor(client: Client, id: string, data: SyncResponseRoom);

23
dist/src/room.js vendored
View file

@ -1,6 +1,6 @@
import EventEmitter from "events";
import { Event, StateEvent } from "./event.js";
import { TimelineSet } from "./timeline.js";
import { RoomTimelineSet } from "./timeline.js";
import { nanoid } from "nanoid";
import { Thread } from "./thread.js";
export class Room extends EventEmitter {
@ -43,7 +43,7 @@ export class Room extends EventEmitter {
enumerable: true,
configurable: true,
writable: true,
value: new TimelineSet(this)
value: new RoomTimelineSet(this)
});
Object.defineProperty(this, "events", {
enumerable: true,
@ -76,7 +76,20 @@ export class Room extends EventEmitter {
const events = data.timeline
.filter(raw => !this.events.has(raw.event_id))
.map(raw => new Event(this, raw));
this.timelines._appendEvents(events);
for (const event of events) {
this.events.set(event.id, event);
this.timelines.live._eventList.push(...events);
for (const event of events)
this.timelines.live.emit("timelineAppend", event);
const threadId = event.content["m.relations"]?.find((rel) => rel.rel_type === "m.thread")?.event_id;
const thread = this.threads.get(threadId);
if (thread) {
thread.messageCount++;
thread.latestEvent = event;
thread.timelines.live._eventList.push(event);
thread.timelines.live.emit("timelineAppend", event);
}
}
}
}
getState(type, stateKey = "") {
@ -97,11 +110,11 @@ export class Room extends EventEmitter {
return new Promise((res) => {
const sub = (event) => {
if (event.unsigned.transaction_id === txn) {
this.off("timeline", sub);
this.timelines.live.off("timelineAppend", sub);
res(event);
}
};
this.on("timeline", sub);
this.timelines.live.on("timelineAppend", sub);
});
}
async leave(reason) {

File diff suppressed because one or more lines are too long

32
dist/src/setup.d.ts vendored
View file

@ -1,2 +1,32 @@
declare class Setup {
import { Client } from "./client.js";
type Flows = {
"m.login.password": {
password: string;
};
"m.login.recaptcha": {
response: string;
};
};
type Identifier = {
type: "m.id.user";
user: string;
} | {
type: string;
[key: string]: any;
};
export declare class Setup {
target: "login" | "register" | "guest";
deviceName: string;
baseUrl: string | null;
endpoint: string | null;
session: string | null;
flows: Array<string> | null;
params: Record<string, string> | null;
identifier: Identifier | null;
constructor(target: "login" | "register" | "guest", deviceName?: string);
useDomain(_domain: string): Promise<void>;
useBaseUrl(baseUrl: string): Promise<void>;
useIdentifier(id: Identifier): void;
exec<K extends keyof Flows>(type: K, options: Flows[K]): Promise<Client | null>;
}
export {};

117
dist/src/setup.js vendored
View file

@ -1,5 +1,118 @@
"use strict";
// Used for initiating a client, handling well-known and authentication.
class Setup {
/*
I'd love to rip this out and replace this with oidc a la "matrix 2.0",
but I'm not sure if that's a good idea yet.
Benefits:
1. It will be more secure since users won't enter credentials into random clients.
2. It separates concerns.
2.1 client and server devs can focus on matrix instead of security best practices
2.2 server admins can add new auth mechanisms without needing spec changes
3. I can use existing sdks and libraries to do authentication - rolling your own auth is a bad idea.
Drawbacks:
1. The ux will be worse since there's now a separtate place to do auth (login, register, manage sessions).
2. It will be much more complex. Server admins now have extra stuff to setup, maintain, and debug. Arguably, it's more difficult for client devs to use as well.
3. It forces users to have some kind of web browser to login.
*/
// TODO: properly integrate with net.js
import { Client } from "./client.js";
export class Setup {
constructor(target, deviceName = "unnamed (sdk-ts)") {
Object.defineProperty(this, "target", {
enumerable: true,
configurable: true,
writable: true,
value: target
});
Object.defineProperty(this, "deviceName", {
enumerable: true,
configurable: true,
writable: true,
value: deviceName
});
Object.defineProperty(this, "baseUrl", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "endpoint", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "session", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "flows", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "params", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "identifier", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
}
async useDomain(_domain) {
throw new Error("todo!");
}
async useBaseUrl(baseUrl) {
this.baseUrl = baseUrl;
this.endpoint = `${baseUrl}/_matrix/client/v3/${this.target === "login" ? "login" : "register"}${this.target === "guest" ? "?kind=guest" : ""}`;
const res = await fetch(this.endpoint).then(res => res.json());
this.flows = res.flows;
this.params = res.params;
}
useIdentifier(id) {
this.identifier = id;
}
async exec(type, options) {
if (!this.endpoint)
throw new Error("you need to call useBaseUrl first");
const res = await fetch(this.endpoint, {
body: JSON.stringify({
identifier: this.identifier,
initial_device_display_name: this.deviceName,
session: this.session ?? undefined,
type: type,
...options,
}),
method: "POST",
}).then(res => res.json());
if (res.status === 400) {
this.session = res.session;
return null;
}
else {
return new Client({
baseUrl: this.baseUrl,
token: res.access_token,
deviceId: res.device_id,
userId: res.user_id,
});
}
}
}
// const setup = new Setup("login");
// // setup.useIdentifier({ type: "m.id.user", user: "@localpart:server.tld" });
// setup.useIdentifier({ type: "m.id.user", user: "localpart" });
// await setup.useBaseUrl("http://localhost:6167");
// await setup.exec("m.login.password", { password: "a" });
//# sourceMappingURL=setup.js.map

View file

@ -1 +1 @@
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/setup.ts"],"names":[],"mappings":";AAAA,wEAAwE;AAExE,MAAM,KAAK;CACV"}
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/setup.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE;;;;;;;;;;;;;;;;;EAiBE;AAEF,uCAAuC;AAEvC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAWrC,MAAM,OAAO,KAAK;IAQhB,YACS,MAAsC,EACtC,aAAqB,kBAAkB;QAD9C;;;;mBAAO,MAAM;WAAgC;QAC7C;;;;mBAAO,UAAU;WAA6B;QATzC;;;;mBAAyB,IAAI;WAAC;QAC9B;;;;mBAA0B,IAAI;WAAC;QAC/B;;;;mBAAyB,IAAI;WAAC;QAC9B;;;;mBAA8B,IAAI;WAAC;QACnC;;;;mBAAwC,IAAI;WAAC;QAC7C;;;;mBAAgC,IAAI;WAAC;IAKzC,CAAC;IAEJ,KAAK,CAAC,SAAS,CAAC,OAAe;QAC7B,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAe;QAC9B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,GAAG,OAAO,sBAAsB,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAChJ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,aAAa,CAAC,EAAc;QAC1B,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,IAAI,CAAwB,IAAO,EAAE,OAAiB;QAC1D,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACzE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE;YACrC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,2BAA2B,EAAE,IAAI,CAAC,UAAU;gBAC5C,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS;gBAClC,IAAI,EAAE,IAAI;gBACV,GAAG,OAAO;aACX,CAAC;YACF,MAAM,EAAE,MAAM;SACf,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3B,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;YAC3B,OAAO,IAAI,CAAC;QACd,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,MAAM,CAAC;gBAChB,OAAO,EAAE,IAAI,CAAC,OAAQ;gBACtB,KAAK,EAAE,GAAG,CAAC,YAAY;gBACvB,QAAQ,EAAE,GAAG,CAAC,SAAS;gBACvB,MAAM,EAAE,GAAG,CAAC,OAAO;aACpB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF;AAED,oCAAoC;AACpC,gFAAgF;AAChF,iEAAiE;AACjE,mDAAmD;AACnD,2DAA2D"}

2
dist/src/sync.js vendored
View file

@ -52,6 +52,8 @@ export class Connection {
}, this.controller.signal).catch((reason) => {
if (reason === "update query")
return null;
if (reason === "stop sync")
return null;
throw reason;
});
if (!json)

View file

@ -1 +1 @@
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/sync.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/sync.ts"],"names":[],"mappings":"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"}

View file

@ -2,6 +2,7 @@ import TypedEmitter from "typed-emitter";
import { EventId, Unreads } from "./api.js";
import { Room } from "./room.js";
import { Event } from "./event.js";
import { ThreadTimelineSet } from "./timeline.js";
type ThreadEvents = {
timeline: (event: Event) => void;
notifications: (notifs: Unreads) => void;
@ -11,6 +12,7 @@ export declare class Thread extends Thread_base {
baseEvent: Event;
room: Room;
id: string;
timelines: ThreadTimelineSet;
participation: string;
messageCount: number;
latestEvent: Event;

7
dist/src/thread.js vendored
View file

@ -1,5 +1,6 @@
import EventEmitter from "events";
import { Event } from "./event.js";
import { ThreadTimelineSet } from "./timeline.js";
export class Thread extends EventEmitter {
constructor(baseEvent) {
const threadRel = baseEvent.unsigned["m.relations"]?.["m.thread"];
@ -24,6 +25,12 @@ export class Thread extends EventEmitter {
writable: true,
value: this.baseEvent.id
});
Object.defineProperty(this, "timelines", {
enumerable: true,
configurable: true,
writable: true,
value: new ThreadTimelineSet(this)
});
Object.defineProperty(this, "participation", {
enumerable: true,
configurable: true,

View file

@ -1 +1 @@
{"version":3,"file":"thread.js","sourceRoot":"","sources":["../../src/thread.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,QAAQ,CAAC;AAIlC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAUnC,MAAM,OAAO,MAAO,SAAS,YAAgE;IAQ3F,YAAmB,SAAgB;QACjC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;QAClE,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QAErF,KAAK,EAAE,CAAC;QAJE;;;;mBAAO,SAAS;WAAO;QAP5B;;;;mBAAa,IAAI,CAAC,SAAS,CAAC,IAAI;WAAC;QACjC;;;;mBAAK,IAAI,CAAC,SAAS,CAAC,EAAE;WAAC;QAEvB;;;;mBAAgB,eAAe;WAAC;QAChC;;;;;WAAqB;QACrB;;;;;WAAmB;QAQxB,MAAM,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC;QACzC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9D,MAAM,WAAW,GAAG,YAAY,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACpE,IAAI,CAAC,YAAY;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QACrE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC;IACtC,CAAC;IAED,uCAAuC;IACvC,gDAAgD;IAChD,uEAAuE;IACvE,IAAI;IAEJ,sDAAsD;IACtD,sDAAsD;IACtD,IAAI;IAEJ,KAAK,CAAC,GAAG,CAAC,OAAiB;QACzB,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;YAC7B,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;SACnF,CAAC,CAAC;IACL,CAAC;CACF"}
{"version":3,"file":"thread.js","sourceRoot":"","sources":["../../src/thread.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,QAAQ,CAAC;AAIlC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAUlD,MAAM,OAAO,MAAO,SAAS,YAAgE;IAS3F,YAAmB,SAAgB;QACjC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;QAClE,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QAErF,KAAK,EAAE,CAAC;QAJE;;;;mBAAO,SAAS;WAAO;QAR5B;;;;mBAAa,IAAI,CAAC,SAAS,CAAC,IAAI;WAAC;QACjC;;;;mBAAK,IAAI,CAAC,SAAS,CAAC,EAAE;WAAC;QACvB;;;;mBAAY,IAAI,iBAAiB,CAAC,IAAI,CAAC;WAAC;QAExC;;;;mBAAgB,eAAe;WAAC;QAChC;;;;;WAAqB;QACrB;;;;;WAAmB;QAQxB,MAAM,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC;QACzC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9D,MAAM,WAAW,GAAG,YAAY,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACpE,IAAI,CAAC,YAAY;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QACrE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC;IACtC,CAAC;IAED,uCAAuC;IACvC,gDAAgD;IAChD,uEAAuE;IACvE,IAAI;IAEJ,sDAAsD;IACtD,sDAAsD;IACtD,IAAI;IAEJ,KAAK,CAAC,GAAG,CAAC,OAAiB;QACzB,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;YAC7B,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;SACnF,CAAC,CAAC;IACL,CAAC;CACF"}

View file

@ -3,49 +3,55 @@ import { Room } from "./room.js";
import { Event } from "./event.js";
import { Thread } from "./thread.js";
import TypedEmitter from "typed-emitter";
type TimelineEvents = {
type TimelineEvents<T> = {
timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void;
timelineAppend: (event: Event) => void;
timelineReplace: (timeine: Timeline) => void;
timelineReplace: (timeine: T) => void;
ephemeral: (event: ApiEphemeralEvent) => void;
};
export interface Timeline extends TypedEmitter<TimelineEvents> {
isAtBeginning: boolean;
isAtEnd: boolean;
getEvents(): Array<Event>;
paginate(dir: "f" | "b", limit: number): Promise<boolean>;
}
declare const RoomTimeline_base: new () => TypedEmitter<TimelineEvents>;
export declare class RoomTimeline extends RoomTimeline_base implements Timeline {
room: Room;
declare const Timeline_base: new () => TypedEmitter<TimelineEvents<any>>;
declare abstract class Timeline extends Timeline_base {
isAtBeginning: boolean;
isAtEnd: boolean;
_eventList: Array<Event>;
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(room: Room);
getEvents(): Array<Event>;
abstract paginate(dir: "f" | "b", limit: number): Promise<boolean>;
}
export declare class RoomTimeline extends Timeline implements TypedEmitter<TimelineEvents<RoomTimeline>> {
private timelineSet;
room: Room;
constructor(timelineSet: RoomTimelineSet, room: Room);
paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
}
declare const ThreadTimeline_base: new () => TypedEmitter<TimelineEvents>;
export declare class ThreadTimeline extends ThreadTimeline_base implements Timeline {
export declare class ThreadTimeline extends Timeline implements TypedEmitter<TimelineEvents<ThreadTimeline>> {
private timelineSet;
thread: Thread;
isAtBeginning: boolean;
isAtEnd: boolean;
_eventList: Array<Event>;
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(thread: Thread);
getEvents(): Array<Event>;
constructor(timelineSet: ThreadTimelineSet, thread: Thread);
paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
}
export declare class TimelineSet {
declare abstract class TimelineSet {
abstract timelines: Set<Timeline>;
abstract timelineMap: Map<EventId, Timeline>;
merge(events: Array<Event>): Timeline | null;
}
export declare class ThreadTimelineSet extends TimelineSet {
thread: Thread;
client: import("./client.js").Client;
live: ThreadTimeline;
timelines: Set<ThreadTimeline>;
timelineMap: Map<EventId, ThreadTimeline>;
constructor(thread: Thread);
fetch(at: EventId | "start" | "end", limit?: number): Promise<ThreadTimeline>;
}
export declare class RoomTimelineSet extends TimelineSet {
room: Room;
client: import("./client.js").Client;
live: RoomTimeline;
timelines: Set<Timeline>;
timelines: Set<RoomTimeline>;
timelineMap: Map<EventId, RoomTimeline>;
constructor(room: Room);
forThread(thread: Thread, at: EventId | "start" | "end"): Promise<ThreadTimeline>;
forEvent(eventId: EventId): Promise<RoomTimeline>;
_appendEvents(events: Array<Event>): void;
fetch(at: EventId | "start" | "end", limit?: number): Promise<RoomTimeline>;
}
export {};

332
dist/src/timeline.js vendored
View file

@ -9,15 +9,9 @@ const intoEvent = (room) => (raw) => {
room.events.set(raw.event_id, event);
return event;
};
export class RoomTimeline extends EventEmitter {
constructor(room) {
super();
Object.defineProperty(this, "room", {
enumerable: true,
configurable: true,
writable: true,
value: room
});
class Timeline extends EventEmitter {
constructor() {
super(...arguments);
Object.defineProperty(this, "isAtBeginning", {
enumerable: true,
configurable: true,
@ -53,6 +47,23 @@ export class RoomTimeline extends EventEmitter {
getEvents() {
return this._eventList;
}
}
export class RoomTimeline extends Timeline {
constructor(timelineSet, room) {
super();
Object.defineProperty(this, "timelineSet", {
enumerable: true,
configurable: true,
writable: true,
value: timelineSet
});
Object.defineProperty(this, "room", {
enumerable: true,
configurable: true,
writable: true,
value: room
});
}
async paginate(dir, limit = 50) {
if (dir === "b" && this.isAtBeginning)
return false;
@ -87,57 +98,38 @@ export class RoomTimeline extends EventEmitter {
this.isAtBeginning = true;
}
this._eventList.unshift(...events);
for (const event of events)
this.room.events.set(event.id, event);
for (const event of events) {
const existing = this.timelineSet.timelineMap.get(event.id);
if (existing) {
const otherIdx = existing.getEvents().indexOf(event);
if (otherIdx === -1)
continue;
}
else {
this.timelineSet.timelineMap.set(event.id, this);
this.room.events.set(event.id, event);
}
}
this.emit("timelineUpdate", events, true);
}
return true;
}
}
export class ThreadTimeline extends EventEmitter {
constructor(thread) {
export class ThreadTimeline extends Timeline {
constructor(timelineSet, thread) {
super();
Object.defineProperty(this, "timelineSet", {
enumerable: true,
configurable: true,
writable: true,
value: timelineSet
});
Object.defineProperty(this, "thread", {
enumerable: true,
configurable: true,
writable: true,
value: thread
});
Object.defineProperty(this, "isAtBeginning", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "isAtEnd", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
// private eventIdToTimeline: Map<EventId, Timeline> = new Map();
// These should be private, but typescript doesn't have "private to module"
Object.defineProperty(this, "_eventList", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "prevBatch", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "nextBatch", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
getEvents() {
return this._eventList;
}
async paginate(dir, limit = 50) {
if (dir === "b" && this.isAtBeginning)
@ -175,36 +167,50 @@ export class ThreadTimeline extends EventEmitter {
this.isAtBeginning = true;
}
this._eventList.unshift(...events);
for (const event of events)
room.events.set(event.id, event);
// TODO: make more performant
// const timelines = [...this.thread.room.timelines.timelines].filter(i => i instanceof ThreadTimeline && i.thread === this.thread) as Array<ThreadTimeline>;
// triple loops!
// this only definitely merges once backwards and once forwards, but that's fine since it would've already been merged anyways if there's multiple overlaps
// for (const timeline of timelines) {
// for (const event of events) {
// // this: [1, 2, 3, 4, 5]
// // other: [3, 4, 5, 6, 7, 8]
// const otherIdx = timeline.getEvents().indexOf(event);
// if (otherIdx === -1) continue;
// // const thisIdx = this._eventList.indexOf(event);
// // this._eventList
// }
// }
for (const event of events) {
const existing = this.timelineSet.timelineMap.get(event.id);
if (existing) {
const otherIdx = existing.getEvents().indexOf(event);
if (otherIdx === -1)
continue;
}
else {
this.timelineSet.timelineMap.set(event.id, this);
room.events.set(event.id, event);
}
}
this.emit("timelineUpdate", events, true);
return !!data.prev_batch;
}
}
}
export class TimelineSet {
constructor(room) {
Object.defineProperty(this, "room", {
class TimelineSet {
merge(events) {
for (const event of events) {
const tl = this.timelineMap.get(event.id);
if (!tl)
continue;
return tl;
}
return null;
}
}
export class ThreadTimelineSet extends TimelineSet {
constructor(thread) {
super();
Object.defineProperty(this, "thread", {
enumerable: true,
configurable: true,
writable: true,
value: room
value: thread
});
// This is the one live timeline
Object.defineProperty(this, "client", {
enumerable: true,
configurable: true,
writable: true,
value: this.thread.room.client
});
// Other timelines *may* be live, but this one is guaranteed to be live
Object.defineProperty(this, "live", {
enumerable: true,
configurable: true,
@ -217,79 +223,147 @@ export class TimelineSet {
writable: true,
value: new Set()
});
this.live = new RoomTimeline(room);
Object.defineProperty(this, "timelineMap", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
this.live = new ThreadTimeline(this, thread);
this.live.isAtEnd = true;
this.timelines.add(this.live);
}
// Get a timeline for a thread. Becomes a live timeline if `atEnd = true`.
async forThread(thread, at) {
async fetch(at, limit = 50) {
if (at === "end") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtEnd);
if (existing)
return existing;
const tl = new ThreadTimeline(thread);
await tl.paginate("b");
tl.isAtEnd = true;
return tl;
const fetchCount = limit - this.live.getEvents().length;
if (fetchCount > 0)
await this.live.paginate("b", fetchCount);
return this.live;
}
else if (at === "start") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtBeginning);
if (existing)
const existing = [...this.timelines].find(i => i.isAtBeginning);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0)
await existing.paginate("f", fetchCount);
return existing;
const tl = new ThreadTimeline(thread);
await tl.paginate("b");
this.timelines.add(tl);
return tl;
}
else {
const tl = new ThreadTimeline(this, this.thread);
await tl.paginate("b", limit);
this.timelines.add(tl);
return tl;
}
}
else {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.getEvents().some(ev => ev.id === at));
if (existing)
// TODO: respect limit?
const existing = this.timelineMap.get(at);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0)
await existing.paginate("f", fetchCount);
return existing;
const tl = new ThreadTimeline(thread);
const context = await this.room.client.net.fetchContext(this.room.id, at, 0);
tl._eventList = [intoEvent(this.room)(context.event)];
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelines.add(tl);
return tl;
}
else {
const tl = new ThreadTimeline(this, this.thread);
const context = await this.client.net.fetchContext(this.thread.room.id, at, 0);
const event = intoEvent(this.thread.room)(context.event);
tl._eventList = [event];
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelineMap.set(event.id, tl);
this.timelines.add(tl);
return tl;
}
}
}
// Get a timeline for an event (context).
async forEvent(eventId) {
const existing = [...this.timelines].find(tl => tl instanceof RoomTimeline && tl.getEvents().some(ev => ev.id === eventId));
if (existing)
return existing;
const context = await this.room.client.net.fetchContext(this.room.id, eventId);
const tl = new RoomTimeline(this.room);
const events = context.events_before
.reverse()
.concat([context.event])
.concat(context.events_after)
.map(intoEvent(this.room));
tl._eventList = events;
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelines.add(tl);
return tl;
}
export class RoomTimelineSet extends TimelineSet {
constructor(room) {
super();
Object.defineProperty(this, "room", {
enumerable: true,
configurable: true,
writable: true,
value: room
});
// Other timelines *may* be live, but this one is guaranteed to be live
Object.defineProperty(this, "client", {
enumerable: true,
configurable: true,
writable: true,
value: this.room.client
});
Object.defineProperty(this, "live", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "timelines", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(this, "timelineMap", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
this.live = new RoomTimeline(this, room);
this.live.isAtEnd = true;
this.timelines.add(this.live);
}
_appendEvents(events) {
// FIXME: there should only be one live timeline for each room and
// thread, they need to be merged together
for (const event of events) {
this.room.events.set(event.id, event);
this.live._eventList.push(...events);
for (const event of events)
this.live.emit("timelineAppend", event);
const threadId = event.content["m.relations"]?.find((rel) => rel.rel_type === "m.thread")?.event_id;
const thread = this.room.threads.get(threadId);
if (thread) {
thread.messageCount++;
thread.latestEvent = event;
const tl = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtEnd);
if (tl) {
tl._eventList.push(event);
tl.emit("timelineAppend", event);
}
// Get a timeline for an event (context).
async fetch(at, limit = 50) {
if (at === "end") {
const fetchCount = limit - this.live.getEvents().length;
if (fetchCount > 0)
await this.live.paginate("b", fetchCount);
return this.live;
}
else if (at === "start") {
const existing = [...this.timelines].find(i => i.isAtBeginning);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0)
await existing.paginate("f", fetchCount);
return existing;
}
else {
const tl = new RoomTimeline(this, this.room);
await tl.paginate("b", limit);
this.timelines.add(tl);
return tl;
}
}
else {
// TODO: respect limit?
const existing = this.timelineMap.get(at);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0)
await existing.paginate("f", fetchCount);
return existing;
}
else {
const context = await this.client.net.fetchContext(this.room.id, at);
const tl = new RoomTimeline(this, this.room);
const events = context.events_before
.reverse()
.concat([context.event])
.concat(context.events_after)
.map(intoEvent(this.room));
tl._eventList = events;
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelines.add(tl);
// FIXME: merge timelines
for (const event of events)
this.timelineMap.set(event.id, tl);
return tl;
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -15,6 +15,7 @@
"typescript": "^5.3.2"
},
"dependencies": {
"@matrix-org/matrix-sdk-crypto-wasm": "^3.4.0",
"events": "^3.3.0",
"nanoid": "^5.0.4"
}

View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@matrix-org/matrix-sdk-crypto-wasm':
specifier: ^3.4.0
version: 3.4.0
events:
specifier: ^3.3.0
version: 3.3.0
@ -25,6 +28,11 @@ devDependencies:
packages:
/@matrix-org/matrix-sdk-crypto-wasm@3.4.0:
resolution: {integrity: sha512-noO6QnH+ypT//CxewoQdlK/z2iuyQo1Ecp1PDaYyr/NV5yXkWvGfGIIcShXqrQJfL5kuWxg/14edNplXsaXoDQ==}
engines: {node: '>= 10'}
dev: false
/@types/events@3.0.3:
resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
dev: true

View file

@ -245,3 +245,26 @@ export interface AckRequest {
thread_id?: EventId,
}>
}
// export interface WellKnownClient {
// baseUrl: string,
// }
// export interface ServerConfig {
// components: {
// api: string,
// sync: string,
// media: string,
// voip: string,
// },
// server: {
// name: string,
// version: string,
// url: string,
// },
// admins: Array<{
// user_id: string,
// email_address?: string,
// purposes: Array<"m.admin" | "m.moderation" | "m.security">,
// }>,
// }

View file

@ -7,18 +7,19 @@ import { Connection } from "./sync.js";
import EventEmitter from "events";
import TypedEmitter from "typed-emitter";
interface ClientConfig {
export interface ClientConfig {
baseUrl: string,
token: string,
userId: string,
deviceId: string,
}
type ClientState = { state: "stop" } // The client is stopped and inactive
export type ClientState = { state: "stop" } // The client is stopped and inactive
| { state: "sync" } // The client is active and syncing
| { state: "catchup" } // The client is catching up after a `retry`
| { state: "error", reason: any } // The client failed and will not retry
| { state: "retry", backoff: number } // The client failed and is retrying
| { state: "logout", soft: boolean } // The client isn't logged in
type ClientEvents = {
// The client's state changed.
@ -43,7 +44,7 @@ type ClientEvents = {
toDevice: (event: ApiDeviceEvent) => void,
};
type RoomList = {
export type RoomList = {
// The total number of rooms in this list
count: number,
@ -144,8 +145,14 @@ export class Client extends (EventEmitter as unknown as new () => TypedEmitter<C
// Stop receiving events from /sync.
stop() {
this.conn.abort();
this.conn.abort("stop sync");
this.conn = new Connection(this);
this.setState({ state: "stop" });
}
async logout() {
this.stop();
await this.net.authLogout();
this.setState({ state: "logout", soft: false });
}
}

View file

@ -1,6 +1,8 @@
export { Client } from "./client.js";
export { Setup } from "./setup.js";
export { ThreadPaginator } from "./room.js";
export type { Event, StateEvent } from "./event.js";
export type { Room } from "./room.js";
export type { Thread } from "./thread.js";
export type { Timeline } from "./timeline.js";
export { Event, StateEvent } from "./event.js";
export { Room } from "./room.js";
export { Thread } from "./thread.js";
export type { RoomList, ClientState } from "./client.js";
export type { UserId, RoomId, EventId } from "./api.js";

View file

@ -91,7 +91,18 @@ export class Network {
body: (isJson ? JSON.stringify(options.body) : options.body) as BodyInit | undefined,
...options.extra,
});
if (!req.ok) throw new Error(`Request failed: ${await req.text()}`);
// FIXME: handle uiaa and make this less awful
if (!req.ok) {
// const response = await req.json();
// throw { response, error: new Error(`Request failed: ${await req.text()}`) };
if (req.status === 401) {
this.client.stop();
(this.client as any).setState({ state: "logout" });
throw new Error(`Request failed: ${await req.text()}`);
} else {
throw new Error(`Request failed: ${await req.text()}`);
}
}
if (options.raw) return req.body;
return req.json();
}
@ -217,4 +228,39 @@ export class Network {
body: acks,
});
}
public async authLogin() {
throw new Error("todo!")
// return this.fetch({
// method: "POST",
// path: `/_matrix/client/v3/login`,
// body: {},
// });
}
public async authRegister() {
throw new Error("todo!")
// return this.fetch({
// method: "POST",
// path: `/_matrix/client/v3/login`,
// body: {},
// });
}
// public async authGuest() {
// throw new Error("todo!")
// // return this.fetch({
// // method: "POST",
// // path: `/_matrix/client/v3/login`,
// // body: {},
// // });
// }
public async authLogout() {
return this.fetch({
method: "POST",
path: `/_matrix/client/v3/logout`,
body: {},
});
}
}

View file

@ -1,21 +1,13 @@
import EventEmitter from "events";
import TypedEmitter from "typed-emitter";
import { ApiEphemeralEvent, EventId, IncludeThreads, RoomId, SyncResponseRoom, Unreads } from "./api.js";
import { EventId, IncludeThreads, RoomId, SyncResponseRoom, Unreads } from "./api.js";
import { Client } from "./client.js";
import { Event, StateEvent } from "./event.js";
import { TimelineSet } from "./timeline.js";
import { RoomTimelineSet } from "./timeline.js";
import { nanoid } from "nanoid";
import { Thread } from "./thread.js";
type RoomEvents = {
// an event is appended to this room's live timeline
// @deprecated: use timelines.live
timeline: (event: Event) => void,
// an ephemeral event was received
// @deprecated: use timelines.live
ephemeral: (event: ApiEphemeralEvent) => void,
// A thread was created
thread: (thread: Thread) => void,
@ -36,7 +28,7 @@ export class Room extends (EventEmitter as unknown as new () => TypedEmitter<Roo
// The (possibly incomplete) state of this room
private state: Map<string, Map<string, StateEvent>> = new Map();
public timelines = new TimelineSet(this);
public timelines = new RoomTimelineSet(this);
public events: Map<EventId, Event> = new Map();
public threads = new RoomThreads(this);
// public members: Members;
@ -80,7 +72,21 @@ room.unban(userid)
const events = data.timeline
.filter(raw => !this.events.has(raw.event_id))
.map(raw => new Event(this, raw));
this.timelines._appendEvents(events);
for (const event of events) {
this.events.set(event.id, event);
this.timelines.live._eventList.push(...events);
for (const event of events) this.timelines.live.emit("timelineAppend", event);
const threadId = event.content["m.relations"]?.find((rel: any) => rel.rel_type === "m.thread")?.event_id;
const thread = this.threads.get(threadId);
if (thread) {
thread.messageCount++;
thread.latestEvent = event;
thread.timelines.live._eventList.push(event);
thread.timelines.live.emit("timelineAppend", event);
}
}
}
}
@ -105,12 +111,12 @@ room.unban(userid)
return new Promise((res) => {
const sub = (event: Event) => {
if (event.unsigned.transaction_id === txn) {
this.off("timeline", sub);
this.timelines.live.off("timelineAppend", sub);
res(event);
}
};
this.on("timeline", sub);
this.timelines.live.on("timelineAppend", sub);
})
}

View file

@ -1,4 +1,94 @@
// Used for initiating a client, handling well-known and authentication.
class Setup {
/*
I'd love to rip this out and replace this with oidc a la "matrix 2.0",
but I'm not sure if that's a good idea yet.
Benefits:
1. It will be more secure since users won't enter credentials into random clients.
2. It separates concerns.
2.1 client and server devs can focus on matrix instead of security best practices
2.2 server admins can add new auth mechanisms without needing spec changes
3. I can use existing sdks and libraries to do authentication - rolling your own auth is a bad idea.
Drawbacks:
1. The ux will be worse since there's now a separtate place to do auth (login, register, manage sessions).
2. It will be much more complex. Server admins now have extra stuff to setup, maintain, and debug. Arguably, it's more difficult for client devs to use as well.
3. It forces users to have some kind of web browser to login.
*/
// TODO: properly integrate with net.js
import { Client } from "./client.js";
type Flows = {
"m.login.password": { password: string, },
"m.login.recaptcha": { response: string },
}
type Identifier =
{ type: "m.id.user", user: string } |
{ type: string, [key: string]: any }
export class Setup {
public baseUrl: string | null = null;
public endpoint: string | null = null;
public session: string | null = null;
public flows: Array<string> | null = null;
public params: Record<string, string> | null = null;
public identifier: Identifier | null = null;
constructor(
public target: "login" | "register" | "guest",
public deviceName: string = "unnamed (sdk-ts)",
) {}
async useDomain(_domain: string) {
throw new Error("todo!");
}
async useBaseUrl(baseUrl: string) {
this.baseUrl = baseUrl;
this.endpoint = `${baseUrl}/_matrix/client/v3/${this.target === "login" ? "login" : "register"}${this.target === "guest" ? "?kind=guest" : ""}`;
const res = await fetch(this.endpoint).then(res => res.json());
this.flows = res.flows;
this.params = res.params;
}
useIdentifier(id: Identifier) {
this.identifier = id;
}
async exec<K extends keyof Flows>(type: K, options: Flows[K]): Promise<Client | null> {
if (!this.endpoint) throw new Error("you need to call useBaseUrl first");
const res = await fetch(this.endpoint, {
body: JSON.stringify({
identifier: this.identifier,
initial_device_display_name: this.deviceName,
session: this.session ?? undefined,
type: type,
...options,
}),
method: "POST",
}).then(res => res.json());
if (res.status === 400) {
this.session = res.session;
return null;
} else {
return new Client({
baseUrl: this.baseUrl!,
token: res.access_token,
deviceId: res.device_id,
userId: res.user_id,
});
}
}
}
// const setup = new Setup("login");
// // setup.useIdentifier({ type: "m.id.user", user: "@localpart:server.tld" });
// setup.useIdentifier({ type: "m.id.user", user: "localpart" });
// await setup.useBaseUrl("http://localhost:6167");
// await setup.exec("m.login.password", { password: "a" });

View file

@ -25,6 +25,7 @@ export class Connection {
...this.query,
}, this.controller.signal).catch((reason) => {
if (reason === "update query") return null;
if (reason === "stop sync") return null;
throw reason;
});
if (!json) return;

View file

@ -3,6 +3,7 @@ import TypedEmitter from "typed-emitter";
import { EventId, Unreads } from "./api.js";
import { Room } from "./room.js";
import { Event } from "./event.js";
import { ThreadTimelineSet } from "./timeline.js";
type ThreadEvents = {
// an event is appended to this thread's live timeline
@ -15,6 +16,7 @@ type ThreadEvents = {
export class Thread extends (EventEmitter as unknown as new () => TypedEmitter<ThreadEvents>) {
public room: Room = this.baseEvent.room;
public id = this.baseEvent.id;
public timelines = new ThreadTimelineSet(this);
public participation = "participating";
public messageCount: number;

View file

@ -8,7 +8,7 @@ import { Thread } from "./thread.js";
import TypedEmitter from "typed-emitter";
import EventEmitter from "events";
type TimelineEvents = {
type TimelineEvents<T> = {
// This room's live timeline is updated, usually via pagination.
timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void,
@ -17,31 +17,12 @@ type TimelineEvents = {
timelineAppend: (event: Event) => void,
// This timeline has been merged with another timeline.
timelineReplace: (timeine: Timeline) => void,
timelineReplace: (timeine: T) => void,
// An ephemeral event was received
ephemeral: (event: ApiEphemeralEvent) => void,
}
export interface Timeline extends TypedEmitter<TimelineEvents> {
// if this timeline has reached the beginning (earliest event)
isAtBeginning: boolean,
// if this timeline has reached the end (latest event)
// this timeline will also be live and receive new events
isAtEnd: boolean,
// isPaginating: boolean,
// Get the events in this timeline
getEvents(): Array<Event>,
// Paginate a timeline for more events
// TODO: fuse two neighboring timelines together
// TODO: don't bother paginating if a timeline is at the end
paginate(dir: "f" | "b", limit: number): Promise<boolean>;
}
const intoEvent = (room: Room) => (raw: ApiEvent): Event => {
const existing = room.events.get(raw.event_id);
if (existing) return existing;
@ -50,7 +31,7 @@ const intoEvent = (room: Room) => (raw: ApiEvent): Event => {
return event;
};
export class RoomTimeline extends (EventEmitter as unknown as new () => TypedEmitter<TimelineEvents>) implements Timeline {
abstract class Timeline extends (EventEmitter as unknown as new () => TypedEmitter<TimelineEvents<any>>) {
public isAtBeginning: boolean = false;
public isAtEnd: boolean = false;
@ -59,14 +40,21 @@ export class RoomTimeline extends (EventEmitter as unknown as new () => TypedEmi
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(public room: Room) {
super();
}
public getEvents(): Array<Event> {
return this._eventList;
}
public abstract paginate(dir: "f" | "b", limit: number): Promise<boolean>;
}
export class RoomTimeline extends Timeline implements TypedEmitter<TimelineEvents<RoomTimeline>> {
constructor(
private timelineSet: RoomTimelineSet,
public room: Room,
) {
super();
}
public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
if (dir === "b" && this.isAtBeginning) return false;
if (dir === "f" && this.isAtEnd) return false;
@ -96,31 +84,29 @@ export class RoomTimeline extends (EventEmitter as unknown as new () => TypedEmi
this.isAtBeginning = true;
}
this._eventList.unshift(...events);
for (const event of events) this.room.events.set(event.id, event);
for (const event of events) {
const existing = this.timelineSet.timelineMap.get(event.id);
if (existing) {
const otherIdx = existing.getEvents().indexOf(event);
if (otherIdx === -1) continue;
} else {
this.timelineSet.timelineMap.set(event.id, this);
this.room.events.set(event.id, event);
}
}
this.emit("timelineUpdate", events, true);
}
return true;
}
}
export class ThreadTimeline extends (EventEmitter as unknown as new () => TypedEmitter<TimelineEvents>) implements Timeline {
public isAtBeginning: boolean = false;
public isAtEnd: boolean = false;
// private eventIdToTimeline: Map<EventId, Timeline> = new Map();
// These should be private, but typescript doesn't have "private to module"
_eventList: Array<Event> = [];
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(public thread: Thread) {
export class ThreadTimeline extends Timeline implements TypedEmitter<TimelineEvents<ThreadTimeline>> {
constructor(
private timelineSet: ThreadTimelineSet,
public thread: Thread,
) {
super();
}
public getEvents(): Array<Event> {
return this._eventList;
}
public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
if (dir === "b" && this.isAtBeginning) return false;
@ -153,107 +139,143 @@ export class ThreadTimeline extends (EventEmitter as unknown as new () => TypedE
this.isAtBeginning = true;
}
this._eventList.unshift(...events);
for (const event of events) room.events.set(event.id, event);
// TODO: make more performant
// const timelines = [...this.thread.room.timelines.timelines].filter(i => i instanceof ThreadTimeline && i.thread === this.thread) as Array<ThreadTimeline>;
// triple loops!
// this only definitely merges once backwards and once forwards, but that's fine since it would've already been merged anyways if there's multiple overlaps
// for (const timeline of timelines) {
// for (const event of events) {
// // this: [1, 2, 3, 4, 5]
// // other: [3, 4, 5, 6, 7, 8]
// const otherIdx = timeline.getEvents().indexOf(event);
// if (otherIdx === -1) continue;
// // const thisIdx = this._eventList.indexOf(event);
// // this._eventList
// }
// }
for (const event of events) {
const existing = this.timelineSet.timelineMap.get(event.id);
if (existing) {
const otherIdx = existing.getEvents().indexOf(event);
if (otherIdx === -1) continue;
} else {
this.timelineSet.timelineMap.set(event.id, this);
room.events.set(event.id, event);
}
}
this.emit("timelineUpdate", events, true);
return !!data.prev_batch;
}
}
}
export class TimelineSet {
// Other timelines *may* be live, but this one is guaranteed to be live
public live: RoomTimeline;
timelines: Set<Timeline> = new Set();
abstract class TimelineSet {
abstract timelines: Set<Timeline>;
abstract timelineMap: Map<EventId, Timeline>;
constructor(public room: Room) {
this.live = new RoomTimeline(room);
merge(events: Array<Event>): Timeline | null {
for (const event of events) {
const tl = this.timelineMap.get(event.id);
if (!tl) continue;
return tl;
}
return null;
}
}
export class ThreadTimelineSet extends TimelineSet {
// This is the one live timeline
public client = this.thread.room.client;
public live: ThreadTimeline;
timelines: Set<ThreadTimeline> = new Set();
timelineMap: Map<EventId, ThreadTimeline> = new Map();
constructor(public thread: Thread) {
super();
this.live = new ThreadTimeline(this, thread);
this.live.isAtEnd = true;
this.timelines.add(this.live);
}
// Get a timeline for a thread. Becomes a live timeline if `atEnd = true`.
public async forThread(thread: Thread, at: EventId | "start" | "end"): Promise<ThreadTimeline> {
public async fetch(at: EventId | "start" | "end", limit = 50): Promise<ThreadTimeline> {
if (at === "end") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtEnd) as ThreadTimeline | undefined;
if (existing) return existing;
const tl = new ThreadTimeline(thread);
await tl.paginate("b");
tl.isAtEnd = true;
return tl;
const fetchCount = limit - this.live.getEvents().length;
if (fetchCount > 0) await this.live.paginate("b", fetchCount);
return this.live;
} else if (at === "start") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtBeginning) as ThreadTimeline | undefined;
if (existing) return existing;
const tl = new ThreadTimeline(thread);
await tl.paginate("b");
this.timelines.add(tl);
return tl;
const existing = [...this.timelines].find(i => i.isAtBeginning);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0) await existing.paginate("f", fetchCount);
return existing;
} else {
const tl = new ThreadTimeline(this, this.thread);
await tl.paginate("b", limit);
this.timelines.add(tl);
return tl;
}
} else {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.getEvents().some(ev => ev.id === at)) as ThreadTimeline | undefined;
if (existing) return existing;
const tl = new ThreadTimeline(thread);
const context = await this.room.client.net.fetchContext(this.room.id, at, 0);
tl._eventList = [intoEvent(this.room)(context.event)];
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelines.add(tl);
return tl;
}
}
// Get a timeline for an event (context).
public async forEvent(eventId: EventId): Promise<RoomTimeline> {
const existing = [...this.timelines].find(tl => tl instanceof RoomTimeline && tl.getEvents().some(ev => ev.id === eventId)) as RoomTimeline | undefined;
if (existing) return existing;
const context = await this.room.client.net.fetchContext(this.room.id, eventId);
const tl = new RoomTimeline(this.room);
const events = context.events_before
.reverse()
.concat([context.event])
.concat(context.events_after)
.map(intoEvent(this.room));
tl._eventList = events;
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelines.add(tl);
return tl;
}
_appendEvents(events: Array<Event>) {
// FIXME: there should only be one live timeline for each room and
// thread, they need to be merged together
for (const event of events) {
this.room.events.set(event.id, event);
this.live._eventList.push(...events);
for (const event of events) this.live.emit("timelineAppend", event);
const threadId = event.content["m.relations"]?.find((rel: any) => rel.rel_type === "m.thread")?.event_id;
const thread = this.room.threads.get(threadId);
if (thread) {
thread.messageCount++;
thread.latestEvent = event;
const tl = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtEnd) as ThreadTimeline | undefined;
if (tl) {
tl._eventList.push(event);
tl.emit("timelineAppend", event);
}
// TODO: respect limit?
const existing = this.timelineMap.get(at);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0) await existing.paginate("f", fetchCount);
return existing;
} else {
const tl = new ThreadTimeline(this, this.thread);
const context = await this.client.net.fetchContext(this.thread.room.id, at, 0);
const event = intoEvent(this.thread.room)(context.event);
tl._eventList = [event];
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelineMap.set(event.id, tl);
this.timelines.add(tl);
return tl;
}
}
}
}
export class RoomTimelineSet extends TimelineSet {
// Other timelines *may* be live, but this one is guaranteed to be live
public client = this.room.client;
public live: RoomTimeline;
timelines: Set<RoomTimeline> = new Set();
timelineMap: Map<EventId, RoomTimeline> = new Map();
constructor(public room: Room) {
super();
this.live = new RoomTimeline(this, room);
this.live.isAtEnd = true;
this.timelines.add(this.live);
}
// Get a timeline for an event (context).
public async fetch(at: EventId | "start" | "end", limit = 50): Promise<RoomTimeline> {
if (at === "end") {
const fetchCount = limit - this.live.getEvents().length;
if (fetchCount > 0) await this.live.paginate("b", fetchCount);
return this.live;
} else if (at === "start") {
const existing = [...this.timelines].find(i => i.isAtBeginning);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0) await existing.paginate("f", fetchCount);
return existing;
} else {
const tl = new RoomTimeline(this, this.room);
await tl.paginate("b", limit);
this.timelines.add(tl);
return tl;
}
} else {
// TODO: respect limit?
const existing = this.timelineMap.get(at);
if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0) await existing.paginate("f", fetchCount);
return existing;
} else {
const context = await this.client.net.fetchContext(this.room.id, at);
const tl = new RoomTimeline(this, this.room);
const events = context.events_before
.reverse()
.concat([context.event])
.concat(context.events_after)
.map(intoEvent(this.room));
tl._eventList = events;
tl.prevBatch = context.start;
tl.nextBatch = context.end;
this.timelines.add(tl);
// FIXME: merge timelines
for (const event of events) this.timelineMap.set(event.id, tl);
return tl;
}
}
}