Skip to content

Commit

Permalink
Support authenticated media endpoints. (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot authored Sep 19, 2024
1 parent b3d2d64 commit 6ef32f7
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 26 deletions.
26 changes: 18 additions & 8 deletions src/MatrixClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1593,14 +1593,22 @@ export class MatrixClient extends EventEmitter {
await this.sendStateEvent(roomId, "m.room.power_levels", "", currentLevels);
}

private async getMediaEndpointPrefix() {
if (await this.doesServerSupportVersion('v1.11')) {
return `${this.homeserverUrl}/_matrix/client/v1/media`;
}
return `${this.homeserverUrl}/_matrix/media/v3`;
}

/**
* Converts a MXC URI to an HTTP URL.
* @param {string} mxc The MXC URI to convert
* @returns {string} The HTTP URL for the content.
*/
public mxcToHttp(mxc: string): string {
public async mxcToHttp(mxc: string): Promise<string> {
const { domain, mediaId } = MXCUrl.parse(mxc);
return `${this.homeserverUrl}/_matrix/media/v3/download/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}`;
const endpoint = await this.getMediaEndpointPrefix();
return `${endpoint}/download/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}`;
}

/**
Expand All @@ -1611,9 +1619,9 @@ export class MatrixClient extends EventEmitter {
* @param {"crop"|"scale"} method Whether to crop or scale (preserve aspect ratio) the content.
* @returns {string} The HTTP URL for the downsized content.
*/
public mxcToHttpThumbnail(mxc: string, width: number, height: number, method: "crop" | "scale"): string {
const downloadUri = this.mxcToHttp(mxc);
return downloadUri.replace("/_matrix/media/v3/download", "/_matrix/media/v3/thumbnail")
public async mxcToHttpThumbnail(mxc: string, width: number, height: number, method: "crop" | "scale"): Promise<string> {
const downloadUri = await this.mxcToHttp(mxc);
return downloadUri.replace("/download", "/thumbnail")
+ `?width=${width}&height=${height}&method=${encodeURIComponent(method)}`;
}

Expand All @@ -1626,9 +1634,10 @@ export class MatrixClient extends EventEmitter {
* @returns {Promise<string>} resolves to the MXC URI of the content
*/
@timedMatrixClientFunctionCall()
public uploadContent(data: Buffer, contentType = "application/octet-stream", filename: string = null): Promise<string> {
public async uploadContent(data: Buffer, contentType = "application/octet-stream", filename: string = null): Promise<string> {
// TODO: Make doRequest take an object for options
return this.doRequest("POST", "/_matrix/media/v3/upload", { filename: filename }, data, 60000, false, contentType)
const endpoint = await this.getMediaEndpointPrefix();
return this.doRequest("POST", `${endpoint}/upload`, { filename: filename }, data, 60000, false, contentType)
.then(response => response["content_uri"]);
}

Expand All @@ -1646,8 +1655,9 @@ export class MatrixClient extends EventEmitter {
if (this.contentScannerInstance) {
return this.contentScannerInstance.downloadContent(mxcUrl);
}
const endpoint = await this.getMediaEndpointPrefix();
const { domain, mediaId } = MXCUrl.parse(mxcUrl);
const path = `/_matrix/media/v3/download/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}`;
const path = `${endpoint}/download/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}`;
const res = await this.doRequest("GET", path, { allow_remote: allowRemote }, null, null, true, null, true);
return {
data: res.body,
Expand Down
32 changes: 16 additions & 16 deletions test/MatrixClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ describe('MatrixClient', () => {

describe('getServerVersions', () => {
it('should call the right endpoint', async () => {
const { client, http } = createTestClient();
const { client, http } = createTestClient(undefined, undefined, undefined, { handleWhoAmI: true, precacheVersions: false });

const versionsResponse: ServerVersions = {
unstable_features: {
Expand All @@ -322,7 +322,7 @@ describe('MatrixClient', () => {
});

it('should cache the response', async () => {
const { client, http } = createTestClient();
const { client, http } = createTestClient(undefined, undefined, undefined, { handleWhoAmI: true, precacheVersions: false });

const versionsResponse: ServerVersions = {
unstable_features: {
Expand Down Expand Up @@ -358,7 +358,7 @@ describe('MatrixClient', () => {
[{ "org.example.wrong": true }, "org.example.feature", false],
[{ "org.example.wrong": false }, "org.example.feature", false],
])("should find that %p has %p as %p", async (versions, flag, target) => {
const { client, http } = createTestClient();
const { client, http } = createTestClient(undefined, undefined, undefined, { handleWhoAmI: true, precacheVersions: false });

const versionsResponse: ServerVersions = {
versions: ["v1.1"],
Expand All @@ -378,7 +378,7 @@ describe('MatrixClient', () => {
[["v1.2"], "v1.1", false],
[["v1.1", "v1.2", "v1.3"], "v1.2", true],
])("should find that %p has %p as %p", async (versions, version, target) => {
const { client, http } = createTestClient();
const { client, http } = createTestClient(undefined, undefined, undefined, { handleWhoAmI: true, precacheVersions: false });

const versionsResponse: ServerVersions = {
versions: versions,
Expand All @@ -397,7 +397,7 @@ describe('MatrixClient', () => {
[["v1.3"], ["v1.1", "v1.2"], false],
[["v1.1", "v1.2", "v1.3"], ["v1.2", "v1.3"], true],
])("should find that %p has %p as %p", async (versions, searchVersions, target) => {
const { client, http } = createTestClient();
const { client, http } = createTestClient(undefined, undefined, undefined, { handleWhoAmI: true, precacheVersions: false });

const versionsResponse: ServerVersions = {
versions: versions,
Expand Down Expand Up @@ -5646,8 +5646,8 @@ describe('MatrixClient', () => {
const mediaId = "testing/val";
const mxc = `mxc://${domain}/${mediaId}`;

const http = client.mxcToHttp(mxc);
expect(http).toBe(`${hsUrl}/_matrix/media/v3/download/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}`);
const http = await client.mxcToHttp(mxc);
expect(http).toBe(`${hsUrl}/_matrix/client/v1/media/download/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}`);
});

it('should error for non-MXC URIs', async () => {
Expand All @@ -5658,7 +5658,7 @@ describe('MatrixClient', () => {
const mxc = `https://${domain}/${mediaId}`;

try {
client.mxcToHttp(mxc);
await client.mxcToHttp(mxc);

// noinspection ExceptionCaughtLocallyJS
throw new Error("Expected an error and didn't get one");
Expand All @@ -5679,9 +5679,9 @@ describe('MatrixClient', () => {
const method = "scale";
const mxc = `mxc://${domain}/${mediaId}`;

const http = client.mxcToHttpThumbnail(mxc, width, height, method);
const http = await client.mxcToHttpThumbnail(mxc, width, height, method);
// eslint-disable-next-line max-len
expect(http).toBe(`${hsUrl}/_matrix/media/v3/thumbnail/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}?width=${width}&height=${height}&method=${encodeURIComponent(method)}`);
expect(http).toBe(`${hsUrl}/_matrix/client/v1/media/thumbnail/${encodeURIComponent(domain)}/${encodeURIComponent(mediaId)}?width=${width}&height=${height}&method=${encodeURIComponent(method)}`);
});

it('should error for non-MXC URIs', async () => {
Expand All @@ -5695,7 +5695,7 @@ describe('MatrixClient', () => {
const mxc = `https://${domain}/${mediaId}`;

try {
client.mxcToHttpThumbnail(mxc, width, height, method);
await client.mxcToHttpThumbnail(mxc, width, height, method);

// noinspection ExceptionCaughtLocallyJS
throw new Error("Expected an error and didn't get one");
Expand All @@ -5717,7 +5717,7 @@ describe('MatrixClient', () => {
Buffer.isBuffer = <any>(i => i === data);

// noinspection TypeScriptValidateJSTypes
http.when("POST", "/_matrix/media/v3/upload").respond(200, (path, content, req) => {
http.when("POST", "/_matrix/client/v1/media/upload").respond(200, (path, content, req) => {
expect(content).toBeDefined();
expect(req.queryParams.filename).toEqual(filename);
expect(req.headers["Content-Type"]).toEqual(contentType);
Expand All @@ -5740,7 +5740,7 @@ describe('MatrixClient', () => {
Buffer.isBuffer = <any>(i => i === data);

// noinspection TypeScriptValidateJSTypes
http.when("POST", "/_matrix/media/v3/upload").respond(200, (path, content, req) => {
http.when("POST", "/_matrix/client/v1/media/upload").respond(200, (path, content, req) => {
expect(content).toBeDefined();
expect(req.queryParams.filename).toEqual(filename);
expect(req.headers["Content-Type"]).toEqual(contentType);
Expand All @@ -5761,8 +5761,8 @@ describe('MatrixClient', () => {
// const fileContents = Buffer.from("12345");

// noinspection TypeScriptValidateJSTypes
http.when("GET", "/_matrix/media/v3/download/").respond(200, (path, _, req) => {
expect(path).toContain("/_matrix/media/v3/download/" + urlPart);
http.when("GET", "/_matrix/client/v1/media/download/").respond(200, (path, _, req) => {
expect(path).toContain("/_matrix/client/v1/media/download/" + urlPart);
expect((req as any).opts.encoding).toEqual(null);
// TODO: Honestly, I have no idea how to coerce the mock library to return headers or buffers,
// so this is left as a fun activity.
Expand Down Expand Up @@ -5798,7 +5798,7 @@ describe('MatrixClient', () => {
});

// noinspection TypeScriptValidateJSTypes
http.when("POST", "/_matrix/media/v3/upload").respond(200, (path, content, req) => {
http.when("POST", "/_matrix/client/v1/media/upload").respond(200, (path, content, req) => {
expect(content).toBeDefined();
// HACK: We know the mock library will return JSON
expect(req.headers["Content-Type"]).toEqual("application/json");
Expand Down
18 changes: 16 additions & 2 deletions test/TestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as tmp from "tmp";
import HttpBackend from "matrix-mock-request";
import { StoreType } from "@matrix-org/matrix-sdk-crypto-nodejs";

import { IStorageProvider, MatrixClient, OTKAlgorithm, RustSdkCryptoStorageProvider, UnpaddedBase64, setRequestFn } from "../src";
import { IStorageProvider, MatrixClient, OTKAlgorithm, RustSdkCryptoStorageProvider, ServerVersions, UnpaddedBase64, setRequestFn } from "../src";

export const TEST_DEVICE_ID = "TEST_DEVICE";

Expand Down Expand Up @@ -31,20 +31,34 @@ export function createTestClient(
storage: IStorageProvider = null,
userId: string = null,
cryptoStoreType?: StoreType,
opts = { handleWhoAmI: true },
opts?: Partial<{ handleWhoAmI: boolean, precacheVersions: boolean }>,
): {
client: MatrixClient;
http: HttpBackend;
hsUrl: string;
accessToken: string;
} {
opts = {
handleWhoAmI: true,
precacheVersions: true,
...opts,
};
const http = new HttpBackend();
const hsUrl = "https://localhost";
const accessToken = "s3cret";
const client = new MatrixClient(hsUrl, accessToken, storage, (cryptoStoreType !== undefined) ? new RustSdkCryptoStorageProvider(tmp.dirSync().name, cryptoStoreType) : null);
(<any>client).userId = userId; // private member access
setRequestFn(http.requestFn);

// Force versions
if (opts.precacheVersions) {
(<any>client).cachedVersions = {
unstable_features: { },
versions: ["v1.11"],
} as ServerVersions;
(<any>client).versionsLastFetched = Date.now();
}

if (opts.handleWhoAmI) {
// Ensure we always respond to a whoami
client.getWhoAmI = () => Promise.resolve({ user_id: userId, device_id: TEST_DEVICE_ID });
Expand Down

0 comments on commit 6ef32f7

Please sign in to comment.