Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add getRoomMessages API #265

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/MatrixClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { RustSdkCryptoStorageProvider } from "./storage/RustSdkCryptoStorageProv
import { DMs } from "./DMs";
import { ServerVersions } from "./models/ServerVersions";
import { RoomCreateOptions } from "./models/CreateRoom";
import { RoomMessagesResponse } from "./models/RoomMessagesResponse";

const SYNC_BACKOFF_MIN_MS = 5000;
const SYNC_BACKOFF_MAX_MS = 15000;
Expand Down Expand Up @@ -897,6 +898,34 @@ export class MatrixClient extends EventEmitter {
}
}

/**
* This returns a list of message and state events for a room.
*
* It uses pagination query parameters to paginate history in the room.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's how pagination works :p

Possibly something like this for the overall description?

Retrieve a (paginated) chunk of history from the room. The direction in which the server is
asked to find events in can be changed, with b (backwards) being "find events older than
the provided token".

We really shouldn't be copy/pasting from the spec, particularly when it doesn't make sense.

*
* @param roomId the room ID to get the event in
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong docs?

* @param dir The direction to return events from.
* If this is set to `f`, events will be returned in chronological order starting at `from`.
* If it is set to `b`, events will be returned in reverse chronolgical order, again starting at `from`.
* @param from The token to start returning events from.
* This token can be obtained from a prev_batch or next_batch token returned by the /sync endpoint,
* or from an end token returned by a previous call to this function.
* @param filter A JSON RoomEventFilter to filter returned events with.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be forcing the caller to encode their filter for us

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, that would be silly.

* @param limit The maximum number of events to return. Default: 10.
* @param to The token to stop returning events at.
* This token can be obtained from a `prev_batch` or `next_batch` token returned by the /sync endpoint,
* or from an end token returned by a previous call to this function.
*/
public async getRoomMessages(roomId: string, dir: 'f'|'b', from: string, opts: {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably assume that b is going to be the default and set it accordingly.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name should also be getRoomHistory or getRoomEvents or similar - we don't have to match the endpoint name, particularly when it's confusing to consumers.

filter?: string;
limit?: number;
to?: string;
}): Promise<RoomMessagesResponse> {
return this.doRequest("GET", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages`, {
dir, from, ...opts,
});
}

/**
* Gets an event for a room. If the event is encrypted, and the client supports encryption,
* and the room is encrypted, then this will return a decrypted event.
Expand Down
12 changes: 12 additions & 0 deletions src/models/RoomMessagesResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RoomEventData, RoomStateEventData } from "./events/RoomEvent";

/**
* The options available when creating a room.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the right docs. However, given the translation we'll need to do on the returned events anyways this interface can be deleted.

* @category Models
*/
export interface RoomMessagesResponse {
start: string;
chunk: RoomEventData[];
end?: string;
state: RoomStateEventData[];
}
42 changes: 42 additions & 0 deletions src/models/events/RoomEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,48 @@ export interface TypicalUnsigned {
[prop: string]: any;
}

export interface RoomEventData<T extends (Object | unknown) = unknown> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK isn't really set up to have these interfaces exist: events are either any (deliberately) or an instance of MatrixEvent - neither of which rely on the interfaces existing. Eventually the SDK will publish a breaking change that actually uses MatrixEvent instances everywhere, however until then it's expected that new code does the conversion. See getEventContext for an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the code to use MatrixEvent for now then, to make that transition easier.

/**
* The fields in this object will vary depending on the type of event.
*/
content: T;
/**
* The globally unique event identifier.
*/
event_id: string;
/**
* Timestamp in milliseconds on originating homeserver when this event was sent.
*/
origin_server_ts: number;
/**
* The ID of the room associated with this event. Will not be present on events that arrive through /sync, despite being required everywhere else.
*/
room_id: string;
/**
* Contains the fully-qualified ID of the user who sent this event.
*/
sender: string;
/**
* The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. `com.example.subdomain.event.type`
*/
type: string;
/**
* Contains optional extra information about the event.
*/
unsinged: TypicalUnsigned;
}

export interface RoomStateEventData<T extends (Object | unknown) = unknown, P = T> extends RoomEventData<T> {
/**
* A unique key which defines the overwriting semantics for this piece of room state.
*/
state_key: string;
/**
* The previous content for this event. If there is no previous content, this key will be missing.
*/
prev_content?: P;
}

/**
* Empty room event content.
* @category Matrix event contents
Expand Down
42 changes: 42 additions & 0 deletions test/MatrixClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,48 @@ describe('MatrixClient', () => {
});
});

describe('getRoomMessages', () => {
it('should call the right endpoint', async () => {
const { client, http, hsUrl } = createTestClient();

const roomId = "!abc123:example.org";

const opts = {
filter: 'foo-bar',
limit: 5,
to: 'my-to-token',
};

const from = 'my-from-token';
const dir = 'f';

const response = {
chunk: [
{ a_chunk: "foo" },
],
start: 'new-from',
end: 'new-to',
state: [
{ state_chunk: 'bar', state_key: '' },
],
};

// noinspection TypeScriptValidateJSTypes
http.when("GET", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages`).respond(200, (path, _data, req) => {
expect(path).toEqual(`${hsUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages`);
expect(req.queryParams['dir']).toEqual(dir);
expect(req.queryParams['from']).toEqual(from);
expect(req.queryParams['filter']).toEqual(opts.filter);
expect(req.queryParams['limit']).toEqual(opts.limit);
expect(req.queryParams['to']).toEqual(opts.to);
return response;
});

const [result] = await Promise.all([client.getRoomMessages(roomId, dir, from, opts), http.flushAllExpected()]);
expect(result).toMatchObject(response);
});
});

describe('getEvent', () => {
it('should call the right endpoint', async () => {
const { client, http, hsUrl } = createTestClient();
Expand Down