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": { "imports": {
"nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts", "nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts",
"events": "https://deno.land/x/events@v1.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 // all the types for the api
export {}; 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 //# 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 { Room } from "./room.js";
import { Connection } from "./sync.js"; import { Connection } from "./sync.js";
import TypedEmitter from "typed-emitter"; import TypedEmitter from "typed-emitter";
interface ClientConfig { export interface ClientConfig {
baseUrl: string; baseUrl: string;
token: string; token: string;
userId: string; userId: string;
deviceId: string; deviceId: string;
} }
type ClientState = { export type ClientState = {
state: "stop"; state: "stop";
} | { } | {
state: "sync"; state: "sync";
@ -21,6 +21,9 @@ type ClientState = {
} | { } | {
state: "retry"; state: "retry";
backoff: number; backoff: number;
} | {
state: "logout";
soft: boolean;
}; };
type ClientEvents = { type ClientEvents = {
state: (state: ClientState) => void; state: (state: ClientState) => void;
@ -30,7 +33,7 @@ type ClientEvents = {
accountData: (type: string, content: string) => void; accountData: (type: string, content: string) => void;
toDevice: (event: ApiDeviceEvent) => void; toDevice: (event: ApiDeviceEvent) => void;
}; };
type RoomList = { export type RoomList = {
count: number; count: number;
rooms: Array<Room>; rooms: Array<Room>;
}; };
@ -69,5 +72,6 @@ export declare class Client extends Client_base {
private setState; private setState;
start(): void; start(): void;
stop(): void; stop(): void;
logout(): Promise<void>;
} }
export {}; export {};

7
dist/src/client.js vendored
View file

@ -119,9 +119,14 @@ export class Client extends EventEmitter {
} }
// Stop receiving events from /sync. // Stop receiving events from /sync.
stop() { stop() {
this.conn.abort(); this.conn.abort("stop sync");
this.conn = new Connection(this); this.conn = new Connection(this);
this.setState({ state: "stop" }); this.setState({ state: "stop" });
} }
async logout() {
this.stop();
await this.net.authLogout();
this.setState({ state: "logout", soft: false });
}
} }
//# sourceMappingURL=client.js.map //# 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 { Client } from "./client.js";
export { Setup } from "./setup.js";
export { ThreadPaginator } from "./room.js"; export { ThreadPaginator } from "./room.js";
export type { Event, StateEvent } from "./event.js"; export { Event, StateEvent } from "./event.js";
export type { Room } from "./room.js"; export { Room } from "./room.js";
export type { Thread } from "./thread.js"; export { Thread } from "./thread.js";
export type { Timeline } from "./timeline.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 { Client } from "./client.js";
export { Setup } from "./setup.js";
export { ThreadPaginator } from "./room.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 //# 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>; 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>; sendState(roomId: t.RoomId, type: string, stateKey: string, content: any): Promise<t.SendResponse>;
ack(acks: t.AckRequest): Promise<any>; ack(acks: t.AckRequest): Promise<any>;
authLogin(): Promise<void>;
authRegister(): Promise<void>;
authLogout(): Promise<any>;
} }
export {}; export {};

44
dist/src/net.js vendored
View file

@ -76,8 +76,19 @@ export class Network {
body: (isJson ? JSON.stringify(options.body) : options.body), body: (isJson ? JSON.stringify(options.body) : options.body),
...options.extra, ...options.extra,
}); });
if (!req.ok) // 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()}`); throw new Error(`Request failed: ${await req.text()}`);
}
else {
throw new Error(`Request failed: ${await req.text()}`);
}
}
if (options.raw) if (options.raw)
return req.body; return req.body;
return req.json(); return req.json();
@ -188,5 +199,36 @@ export class Network {
body: acks, 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 //# 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 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 { Client } from "./client.js";
import { Event, StateEvent } from "./event.js"; import { Event, StateEvent } from "./event.js";
import { TimelineSet } from "./timeline.js"; import { RoomTimelineSet } from "./timeline.js";
import { Thread } from "./thread.js"; import { Thread } from "./thread.js";
type RoomEvents = { type RoomEvents = {
timeline: (event: Event) => void;
ephemeral: (event: ApiEphemeralEvent) => void;
thread: (thread: Thread) => void; thread: (thread: Thread) => void;
state: (event: StateEvent) => void; state: (event: StateEvent) => void;
notifications: (notifs: Unreads) => void; notifications: (notifs: Unreads) => void;
@ -21,7 +19,7 @@ export declare class Room extends Room_base {
client: Client; client: Client;
id: string; id: string;
private state; private state;
timelines: TimelineSet; timelines: RoomTimelineSet;
events: Map<EventId, Event>; events: Map<EventId, Event>;
threads: RoomThreads; threads: RoomThreads;
constructor(client: Client, id: string, data: SyncResponseRoom); constructor(client: Client, id: string, data: SyncResponseRoom);

23
dist/src/room.js vendored
View file

@ -1,6 +1,6 @@
import EventEmitter from "events"; import EventEmitter from "events";
import { Event, StateEvent } from "./event.js"; import { Event, StateEvent } from "./event.js";
import { TimelineSet } from "./timeline.js"; import { RoomTimelineSet } from "./timeline.js";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { Thread } from "./thread.js"; import { Thread } from "./thread.js";
export class Room extends EventEmitter { export class Room extends EventEmitter {
@ -43,7 +43,7 @@ export class Room extends EventEmitter {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
writable: true, writable: true,
value: new TimelineSet(this) value: new RoomTimelineSet(this)
}); });
Object.defineProperty(this, "events", { Object.defineProperty(this, "events", {
enumerable: true, enumerable: true,
@ -76,7 +76,20 @@ export class Room extends EventEmitter {
const events = data.timeline const events = data.timeline
.filter(raw => !this.events.has(raw.event_id)) .filter(raw => !this.events.has(raw.event_id))
.map(raw => new Event(this, raw)); .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 = "") { getState(type, stateKey = "") {
@ -97,11 +110,11 @@ export class Room extends EventEmitter {
return new Promise((res) => { return new Promise((res) => {
const sub = (event) => { const sub = (event) => {
if (event.unsigned.transaction_id === txn) { if (event.unsigned.transaction_id === txn) {
this.off("timeline", sub); this.timelines.live.off("timelineAppend", sub);
res(event); res(event);
} }
}; };
this.on("timeline", sub); this.timelines.live.on("timelineAppend", sub);
}); });
} }
async leave(reason) { 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. // 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 //# 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) => { }, this.controller.signal).catch((reason) => {
if (reason === "update query") if (reason === "update query")
return null; return null;
if (reason === "stop sync")
return null;
throw reason; throw reason;
}); });
if (!json) 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 { EventId, Unreads } from "./api.js";
import { Room } from "./room.js"; import { Room } from "./room.js";
import { Event } from "./event.js"; import { Event } from "./event.js";
import { ThreadTimelineSet } from "./timeline.js";
type ThreadEvents = { type ThreadEvents = {
timeline: (event: Event) => void; timeline: (event: Event) => void;
notifications: (notifs: Unreads) => void; notifications: (notifs: Unreads) => void;
@ -11,6 +12,7 @@ export declare class Thread extends Thread_base {
baseEvent: Event; baseEvent: Event;
room: Room; room: Room;
id: string; id: string;
timelines: ThreadTimelineSet;
participation: string; participation: string;
messageCount: number; messageCount: number;
latestEvent: Event; latestEvent: Event;

7
dist/src/thread.js vendored
View file

@ -1,5 +1,6 @@
import EventEmitter from "events"; import EventEmitter from "events";
import { Event } from "./event.js"; import { Event } from "./event.js";
import { ThreadTimelineSet } from "./timeline.js";
export class Thread extends EventEmitter { export class Thread extends EventEmitter {
constructor(baseEvent) { constructor(baseEvent) {
const threadRel = baseEvent.unsigned["m.relations"]?.["m.thread"]; const threadRel = baseEvent.unsigned["m.relations"]?.["m.thread"];
@ -24,6 +25,12 @@ export class Thread extends EventEmitter {
writable: true, writable: true,
value: this.baseEvent.id value: this.baseEvent.id
}); });
Object.defineProperty(this, "timelines", {
enumerable: true,
configurable: true,
writable: true,
value: new ThreadTimelineSet(this)
});
Object.defineProperty(this, "participation", { Object.defineProperty(this, "participation", {
enumerable: true, enumerable: true,
configurable: 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 { Event } from "./event.js";
import { Thread } from "./thread.js"; import { Thread } from "./thread.js";
import TypedEmitter from "typed-emitter"; import TypedEmitter from "typed-emitter";
type TimelineEvents = { type TimelineEvents<T> = {
timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void; timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void;
timelineAppend: (event: Event) => void; timelineAppend: (event: Event) => void;
timelineReplace: (timeine: Timeline) => void; timelineReplace: (timeine: T) => void;
ephemeral: (event: ApiEphemeralEvent) => void; ephemeral: (event: ApiEphemeralEvent) => void;
}; };
export interface Timeline extends TypedEmitter<TimelineEvents> { declare const Timeline_base: new () => TypedEmitter<TimelineEvents<any>>;
isAtBeginning: boolean; declare abstract class Timeline extends Timeline_base {
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;
isAtBeginning: boolean; isAtBeginning: boolean;
isAtEnd: boolean; isAtEnd: boolean;
_eventList: Array<Event>; _eventList: Array<Event>;
prevBatch: string | undefined; prevBatch: string | undefined;
nextBatch: string | undefined; nextBatch: string | undefined;
constructor(room: Room);
getEvents(): Array<Event>; 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>; paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
} }
declare const ThreadTimeline_base: new () => TypedEmitter<TimelineEvents>; export declare class ThreadTimeline extends Timeline implements TypedEmitter<TimelineEvents<ThreadTimeline>> {
export declare class ThreadTimeline extends ThreadTimeline_base implements Timeline { private timelineSet;
thread: Thread; thread: Thread;
isAtBeginning: boolean; constructor(timelineSet: ThreadTimelineSet, thread: Thread);
isAtEnd: boolean;
_eventList: Array<Event>;
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(thread: Thread);
getEvents(): Array<Event>;
paginate(dir: "f" | "b", limit?: number): Promise<boolean>; 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; room: Room;
client: import("./client.js").Client;
live: RoomTimeline; live: RoomTimeline;
timelines: Set<Timeline>; timelines: Set<RoomTimeline>;
timelineMap: Map<EventId, RoomTimeline>;
constructor(room: Room); constructor(room: Room);
forThread(thread: Thread, at: EventId | "start" | "end"): Promise<ThreadTimeline>; fetch(at: EventId | "start" | "end", limit?: number): Promise<RoomTimeline>;
forEvent(eventId: EventId): Promise<RoomTimeline>;
_appendEvents(events: Array<Event>): void;
} }
export {}; export {};

306
dist/src/timeline.js vendored
View file

@ -9,15 +9,9 @@ const intoEvent = (room) => (raw) => {
room.events.set(raw.event_id, event); room.events.set(raw.event_id, event);
return event; return event;
}; };
export class RoomTimeline extends EventEmitter { class Timeline extends EventEmitter {
constructor(room) { constructor() {
super(); super(...arguments);
Object.defineProperty(this, "room", {
enumerable: true,
configurable: true,
writable: true,
value: room
});
Object.defineProperty(this, "isAtBeginning", { Object.defineProperty(this, "isAtBeginning", {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
@ -53,6 +47,23 @@ export class RoomTimeline extends EventEmitter {
getEvents() { getEvents() {
return this._eventList; 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) { async paginate(dir, limit = 50) {
if (dir === "b" && this.isAtBeginning) if (dir === "b" && this.isAtBeginning)
return false; return false;
@ -87,57 +98,38 @@ export class RoomTimeline extends EventEmitter {
this.isAtBeginning = true; this.isAtBeginning = true;
} }
this._eventList.unshift(...events); this._eventList.unshift(...events);
for (const event of events) 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.room.events.set(event.id, event);
}
}
this.emit("timelineUpdate", events, true); this.emit("timelineUpdate", events, true);
} }
return true; return true;
} }
} }
export class ThreadTimeline extends EventEmitter { export class ThreadTimeline extends Timeline {
constructor(thread) { constructor(timelineSet, thread) {
super(); super();
Object.defineProperty(this, "timelineSet", {
enumerable: true,
configurable: true,
writable: true,
value: timelineSet
});
Object.defineProperty(this, "thread", { Object.defineProperty(this, "thread", {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
writable: true, writable: true,
value: thread 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) { async paginate(dir, limit = 50) {
if (dir === "b" && this.isAtBeginning) if (dir === "b" && this.isAtBeginning)
@ -175,36 +167,50 @@ export class ThreadTimeline extends EventEmitter {
this.isAtBeginning = true; this.isAtBeginning = true;
} }
this._eventList.unshift(...events); this._eventList.unshift(...events);
for (const event of events) 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); 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
// }
// }
this.emit("timelineUpdate", events, true); this.emit("timelineUpdate", events, true);
return !!data.prev_batch; return !!data.prev_batch;
} }
} }
} }
export class TimelineSet { class TimelineSet {
constructor(room) { merge(events) {
Object.defineProperty(this, "room", { 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, enumerable: true,
configurable: true, configurable: true,
writable: 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", { Object.defineProperty(this, "live", {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
@ -217,50 +223,134 @@ export class TimelineSet {
writable: true, writable: true,
value: new Set() 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.live.isAtEnd = true;
this.timelines.add(this.live); this.timelines.add(this.live);
} }
// Get a timeline for a thread. Becomes a live timeline if `atEnd = true`. async fetch(at, limit = 50) {
async forThread(thread, at) {
if (at === "end") { if (at === "end") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtEnd); const fetchCount = limit - this.live.getEvents().length;
if (existing) if (fetchCount > 0)
return existing; await this.live.paginate("b", fetchCount);
const tl = new ThreadTimeline(thread); return this.live;
await tl.paginate("b");
tl.isAtEnd = true;
return tl;
} }
else if (at === "start") { else if (at === "start") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtBeginning); const existing = [...this.timelines].find(i => i.isAtBeginning);
if (existing) if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0)
await existing.paginate("f", fetchCount);
return existing; return existing;
const tl = new ThreadTimeline(thread);
await tl.paginate("b");
this.timelines.add(tl);
return tl;
} }
else { else {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.getEvents().some(ev => ev.id === at)); const tl = new ThreadTimeline(this, this.thread);
if (existing) await tl.paginate("b", limit);
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); this.timelines.add(tl);
return tl; return tl;
} }
} }
// Get a timeline for an event (context). else {
async forEvent(eventId) { // TODO: respect limit?
const existing = [...this.timelines].find(tl => tl instanceof RoomTimeline && tl.getEvents().some(ev => ev.id === eventId)); const existing = this.timelineMap.get(at);
if (existing) if (existing) {
const fetchCount = limit - existing.getEvents().length;
if (fetchCount > 0)
await existing.paginate("f", fetchCount);
return existing; return existing;
const context = await this.room.client.net.fetchContext(this.room.id, eventId); }
const tl = new RoomTimeline(this.room); 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 {
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);
}
// 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 const events = context.events_before
.reverse() .reverse()
.concat([context.event]) .concat([context.event])
@ -270,26 +360,10 @@ export class TimelineSet {
tl.prevBatch = context.start; tl.prevBatch = context.start;
tl.nextBatch = context.end; tl.nextBatch = context.end;
this.timelines.add(tl); this.timelines.add(tl);
return tl; // FIXME: merge timelines
}
_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) for (const event of events)
this.live.emit("timelineAppend", event); this.timelineMap.set(event.id, tl);
const threadId = event.content["m.relations"]?.find((rel) => rel.rel_type === "m.thread")?.event_id; return tl;
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);
}
} }
} }
} }

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" "typescript": "^5.3.2"
}, },
"dependencies": { "dependencies": {
"@matrix-org/matrix-sdk-crypto-wasm": "^3.4.0",
"events": "^3.3.0", "events": "^3.3.0",
"nanoid": "^5.0.4" "nanoid": "^5.0.4"
} }

View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@matrix-org/matrix-sdk-crypto-wasm':
specifier: ^3.4.0
version: 3.4.0
events: events:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.0 version: 3.3.0
@ -25,6 +28,11 @@ devDependencies:
packages: 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: /@types/events@3.0.3:
resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
dev: true dev: true

View file

@ -245,3 +245,26 @@ export interface AckRequest {
thread_id?: EventId, 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 EventEmitter from "events";
import TypedEmitter from "typed-emitter"; import TypedEmitter from "typed-emitter";
interface ClientConfig { export interface ClientConfig {
baseUrl: string, baseUrl: string,
token: string, token: string,
userId: string, userId: string,
deviceId: 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: "sync" } // The client is active and syncing
| { state: "catchup" } // The client is catching up after a `retry` | { state: "catchup" } // The client is catching up after a `retry`
| { state: "error", reason: any } // The client failed and will not retry | { state: "error", reason: any } // The client failed and will not retry
| { state: "retry", backoff: number } // The client failed and is retrying | { state: "retry", backoff: number } // The client failed and is retrying
| { state: "logout", soft: boolean } // The client isn't logged in
type ClientEvents = { type ClientEvents = {
// The client's state changed. // The client's state changed.
@ -43,7 +44,7 @@ type ClientEvents = {
toDevice: (event: ApiDeviceEvent) => void, toDevice: (event: ApiDeviceEvent) => void,
}; };
type RoomList = { export type RoomList = {
// The total number of rooms in this list // The total number of rooms in this list
count: number, count: number,
@ -144,8 +145,14 @@ export class Client extends (EventEmitter as unknown as new () => TypedEmitter<C
// Stop receiving events from /sync. // Stop receiving events from /sync.
stop() { stop() {
this.conn.abort(); this.conn.abort("stop sync");
this.conn = new Connection(this); this.conn = new Connection(this);
this.setState({ state: "stop" }); 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 { Client } from "./client.js";
export { Setup } from "./setup.js";
export { ThreadPaginator } from "./room.js"; export { ThreadPaginator } from "./room.js";
export type { Event, StateEvent } from "./event.js"; export { Event, StateEvent } from "./event.js";
export type { Room } from "./room.js"; export { Room } from "./room.js";
export type { Thread } from "./thread.js"; export { Thread } from "./thread.js";
export type { Timeline } from "./timeline.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, body: (isJson ? JSON.stringify(options.body) : options.body) as BodyInit | undefined,
...options.extra, ...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; if (options.raw) return req.body;
return req.json(); return req.json();
} }
@ -217,4 +228,39 @@ export class Network {
body: acks, 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 EventEmitter from "events";
import TypedEmitter from "typed-emitter"; 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 { Client } from "./client.js";
import { Event, StateEvent } from "./event.js"; import { Event, StateEvent } from "./event.js";
import { TimelineSet } from "./timeline.js"; import { RoomTimelineSet } from "./timeline.js";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { Thread } from "./thread.js"; import { Thread } from "./thread.js";
type RoomEvents = { 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 // A thread was created
thread: (thread: Thread) => void, 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 // The (possibly incomplete) state of this room
private state: Map<string, Map<string, StateEvent>> = new Map(); 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 events: Map<EventId, Event> = new Map();
public threads = new RoomThreads(this); public threads = new RoomThreads(this);
// public members: Members; // public members: Members;
@ -80,7 +72,21 @@ room.unban(userid)
const events = data.timeline const events = data.timeline
.filter(raw => !this.events.has(raw.event_id)) .filter(raw => !this.events.has(raw.event_id))
.map(raw => new Event(this, raw)); .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) => { return new Promise((res) => {
const sub = (event: Event) => { const sub = (event: Event) => {
if (event.unsigned.transaction_id === txn) { if (event.unsigned.transaction_id === txn) {
this.off("timeline", sub); this.timelines.live.off("timelineAppend", sub);
res(event); 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. // 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.query,
}, this.controller.signal).catch((reason) => { }, this.controller.signal).catch((reason) => {
if (reason === "update query") return null; if (reason === "update query") return null;
if (reason === "stop sync") return null;
throw reason; throw reason;
}); });
if (!json) return; if (!json) return;

View file

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

View file

@ -8,7 +8,7 @@ import { Thread } from "./thread.js";
import TypedEmitter from "typed-emitter"; import TypedEmitter from "typed-emitter";
import EventEmitter from "events"; import EventEmitter from "events";
type TimelineEvents = { type TimelineEvents<T> = {
// This room's live timeline is updated, usually via pagination. // This room's live timeline is updated, usually via pagination.
timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void, timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void,
@ -17,31 +17,12 @@ type TimelineEvents = {
timelineAppend: (event: Event) => void, timelineAppend: (event: Event) => void,
// This timeline has been merged with another timeline. // This timeline has been merged with another timeline.
timelineReplace: (timeine: Timeline) => void, timelineReplace: (timeine: T) => void,
// An ephemeral event was received // An ephemeral event was received
ephemeral: (event: ApiEphemeralEvent) => void, 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 intoEvent = (room: Room) => (raw: ApiEvent): Event => {
const existing = room.events.get(raw.event_id); const existing = room.events.get(raw.event_id);
if (existing) return existing; if (existing) return existing;
@ -50,7 +31,7 @@ const intoEvent = (room: Room) => (raw: ApiEvent): Event => {
return 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 isAtBeginning: boolean = false;
public isAtEnd: boolean = false; public isAtEnd: boolean = false;
@ -59,14 +40,21 @@ export class RoomTimeline extends (EventEmitter as unknown as new () => TypedEmi
prevBatch: string | undefined; prevBatch: string | undefined;
nextBatch: string | undefined; nextBatch: string | undefined;
constructor(public room: Room) {
super();
}
public getEvents(): Array<Event> { public getEvents(): Array<Event> {
return this._eventList; 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> { public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
if (dir === "b" && this.isAtBeginning) return false; if (dir === "b" && this.isAtBeginning) return false;
if (dir === "f" && this.isAtEnd) return false; if (dir === "f" && this.isAtEnd) return false;
@ -96,32 +84,30 @@ export class RoomTimeline extends (EventEmitter as unknown as new () => TypedEmi
this.isAtBeginning = true; this.isAtBeginning = true;
} }
this._eventList.unshift(...events); 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); this.emit("timelineUpdate", events, true);
} }
return true; return true;
} }
} }
export class ThreadTimeline extends (EventEmitter as unknown as new () => TypedEmitter<TimelineEvents>) implements Timeline { export class ThreadTimeline extends Timeline implements TypedEmitter<TimelineEvents<ThreadTimeline>> {
public isAtBeginning: boolean = false; constructor(
public isAtEnd: boolean = false; private timelineSet: ThreadTimelineSet,
public thread: Thread,
// 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) {
super(); super();
} }
public getEvents(): Array<Event> {
return this._eventList;
}
public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> { public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
if (dir === "b" && this.isAtBeginning) return false; if (dir === "b" && this.isAtBeginning) return false;
if (dir === "f" && this.isAtEnd) return false; if (dir === "f" && this.isAtEnd) return false;
@ -153,75 +139,131 @@ export class ThreadTimeline extends (EventEmitter as unknown as new () => TypedE
this.isAtBeginning = true; this.isAtBeginning = true;
} }
this._eventList.unshift(...events); this._eventList.unshift(...events);
for (const event of events) room.events.set(event.id, event); for (const event of events) {
const existing = this.timelineSet.timelineMap.get(event.id);
// TODO: make more performant if (existing) {
// const timelines = [...this.thread.room.timelines.timelines].filter(i => i instanceof ThreadTimeline && i.thread === this.thread) as Array<ThreadTimeline>; const otherIdx = existing.getEvents().indexOf(event);
// triple loops! if (otherIdx === -1) continue;
// 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 } else {
// for (const timeline of timelines) { this.timelineSet.timelineMap.set(event.id, this);
// for (const event of events) { room.events.set(event.id, event);
// // 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
// }
// }
this.emit("timelineUpdate", events, true); this.emit("timelineUpdate", events, true);
return !!data.prev_batch; return !!data.prev_batch;
} }
} }
} }
export class TimelineSet { abstract class TimelineSet {
// Other timelines *may* be live, but this one is guaranteed to be live abstract timelines: Set<Timeline>;
public live: RoomTimeline; abstract timelineMap: Map<EventId, Timeline>;
timelines: Set<Timeline> = new Set();
constructor(public room: Room) { merge(events: Array<Event>): Timeline | null {
this.live = new RoomTimeline(room); 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.live.isAtEnd = true;
this.timelines.add(this.live); this.timelines.add(this.live);
} }
// Get a timeline for a thread. Becomes a live timeline if `atEnd = true`. public async fetch(at: EventId | "start" | "end", limit = 50): Promise<ThreadTimeline> {
public async forThread(thread: Thread, at: EventId | "start" | "end"): Promise<ThreadTimeline> {
if (at === "end") { if (at === "end") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtEnd) as ThreadTimeline | undefined; const fetchCount = limit - this.live.getEvents().length;
if (existing) return existing; if (fetchCount > 0) await this.live.paginate("b", fetchCount);
const tl = new ThreadTimeline(thread); return this.live;
await tl.paginate("b");
tl.isAtEnd = true;
return tl;
} else if (at === "start") { } else if (at === "start") {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.isAtBeginning) as ThreadTimeline | undefined; const existing = [...this.timelines].find(i => i.isAtBeginning);
if (existing) return existing; if (existing) {
const tl = new ThreadTimeline(thread); const fetchCount = limit - existing.getEvents().length;
await tl.paginate("b"); 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); this.timelines.add(tl);
return tl; return tl;
}
} else { } else {
const existing = [...this.timelines].find(i => i instanceof ThreadTimeline && i.thread === thread && i.getEvents().some(ev => ev.id === at)) as ThreadTimeline | undefined; // TODO: respect limit?
if (existing) return existing; const existing = this.timelineMap.get(at);
const tl = new ThreadTimeline(thread); if (existing) {
const context = await this.room.client.net.fetchContext(this.room.id, at, 0); const fetchCount = limit - existing.getEvents().length;
tl._eventList = [intoEvent(this.room)(context.event)]; 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.prevBatch = context.start;
tl.nextBatch = context.end; tl.nextBatch = context.end;
this.timelineMap.set(event.id, tl);
this.timelines.add(tl); this.timelines.add(tl);
return 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). // Get a timeline for an event (context).
public async forEvent(eventId: EventId): Promise<RoomTimeline> { public async fetch(at: EventId | "start" | "end", limit = 50): Promise<RoomTimeline> {
const existing = [...this.timelines].find(tl => tl instanceof RoomTimeline && tl.getEvents().some(ev => ev.id === eventId)) as RoomTimeline | undefined; if (at === "end") {
if (existing) return existing; const fetchCount = limit - this.live.getEvents().length;
const context = await this.room.client.net.fetchContext(this.room.id, eventId); if (fetchCount > 0) await this.live.paginate("b", fetchCount);
const tl = new RoomTimeline(this.room); 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 const events = context.events_before
.reverse() .reverse()
.concat([context.event]) .concat([context.event])
@ -231,30 +273,10 @@ export class TimelineSet {
tl.prevBatch = context.start; tl.prevBatch = context.start;
tl.nextBatch = context.end; tl.nextBatch = context.end;
this.timelines.add(tl); this.timelines.add(tl);
// FIXME: merge timelines
for (const event of events) this.timelineMap.set(event.id, tl);
return 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);
}
}
} }
} }
} }