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

Initial MSC1929 Implementation #150

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 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
2 changes: 1 addition & 1 deletion mx-tester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ up:
# Wait until postgresql is ready
- until psql postgres://mjolnir-tester:mjolnir-test@127.0.0.1:8083/mjolnir-test-db -c ""; do echo "Waiting for psql..."; sleep 1s; done
# Launch the reverse proxy, listening for connections *only* on the local host.
- docker run --rm --network host --name mjolnir-test-reverse-proxy -p 127.0.0.1:8081:80 -v $MX_TEST_CWD/test/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx
- docker run --rm --network host --name mjolnir-test-reverse-proxy -v $MX_TEST_CWD/test/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx
- yarn install
- npx ts-node src/appservice/cli.ts -r -u "http://host.docker.internal:9000"
- cp mjolnir-registration.yaml $MX_TEST_SYNAPSE_DIR/data/
Expand Down
1 change: 1 addition & 0 deletions src/commands/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import "./Rules";
import "./WatchUnwatchCommand";
import "./Help";
import "./SetDisplayNameCommand";
import "./QueryAdminDetails";

export const COMMAND_PREFIX = "!mjolnir";

Expand Down
123 changes: 123 additions & 0 deletions src/commands/QueryAdminDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Permalinks, UserID, getRequestFn } from "matrix-bot-sdk";
import { MjolnirContext } from "./CommandHandler";
import { CommandError, CommandResult } from "./interface-manager/Validation";
import { defineInterfaceCommand, findTableCommand } from "./interface-manager/InterfaceCommand";
import { findPresentationType, makePresentationType, parameters, ParsedKeywords, simpleTypeValidator, union } from "./interface-manager/ParameterParsing";
import "./interface-manager/MatrixPresentations";
import { JSXFactory } from "./interface-manager/JSXFactory";
import { defineMatrixInterfaceAdaptor } from "./interface-manager/MatrixInterfaceAdaptor";
import { renderMatrixAndSend } from "./interface-manager/DeadDocumentMatrix";
import { DocumentNode } from "./interface-manager/DeadDocument";
import { ReadItem } from "./interface-manager/CommandReader";

export type MatrixHomeserver = string;

makePresentationType({
name: "MatrixHomeserver",
// This is a very very crude way to detect a url.
validator: simpleTypeValidator("MatrixHomeserver", (readItem: ReadItem) => (readItem instanceof String) && (!readItem.includes('#') || !readItem.includes('!')))
})

interface SupportJson {
contacts?: {
matrix_id?: string,
email_address?: string;
role: "admin" | "security";
}[],
support_page?: string;
}

async function queryAdminDetails(
this: MjolnirContext,
_keywords: ParsedKeywords,
entity: UserID | MatrixHomeserver | string
): Promise<CommandResult<[UserID | MatrixHomeserver | string, SupportJson], CommandError>> {
let domain: string;
if (entity instanceof UserID) {
domain = `https://${entity.domain}`;
} else {
// Doing some cleanup on the url
if (!entity.startsWith("https://") && !entity.startsWith("http://")) {
domain = `https://${entity}`;
} else {
domain = entity;
}
}


try {
const resp: SupportJson = await new Promise((resolve, reject) => {
getRequestFn()(`${domain}/.well-known/matrix/support`, (error: any, response: any, resBody: unknown) => {
Copy link
Member

Choose a reason for hiding this comment

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

image

what did i do wrong here? do they just not support the MSC?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah Hm ok yeah it prints the error but seems like in this case it just printed the whole page oO. Yeah not supported and it went wrong.

if (error) {
reject(new CommandError(`The request failed with an error: ${error}.`));
} else if (response.statusCode !== 200) {
reject(new CommandError(`The server didn't reply with a valid response code: ${response.statusCode}.`));
} else if (typeof resBody === 'object' && resBody !== null && ('contacts' in resBody || 'support_page' in resBody)) {
resolve(resBody as SupportJson)
} else if (resBody === null) {
reject(new CommandError(`The response was empty.`));
} else {
reject(new CommandError(`Don't know what to do with response body ${resBody}. Assuming its not a json`));
}
});
});
return CommandResult.Ok([entity, resp]);
} catch (error: any) {
return CommandResult.Err(error);
}
}

defineInterfaceCommand({
designator: ["queryAdmin"],
table: "mjolnir",
parameters: parameters([
{
name: "entity",
acceptor: union(
findPresentationType("UserID"),
findPresentationType("MatrixHomeserver"),
findPresentationType("string")
)
}
]),
command: queryAdminDetails,
summary: "Queries the admin of the Homeserver or user using MSC1929 if available",
})

function renderSupportJson([entity, support_json]: [UserID | MatrixHomeserver | string, SupportJson],): DocumentNode {
if (!support_json.support_page) {
return <root>
<b>Support infos for ({entity}):</b><br />
<ul>
{support_json.contacts!.map(r => <li><b>{r.role}</b> - {r.matrix_id ? <a href={Permalinks.forUser(r.matrix_id)}>{r.matrix_id}</a> : <a href="mailto:{r.email_address}">{r.email_address}</a>}<br /></li>)}
</ul>
</root>
} else if (!support_json.contacts) {
return <root>
<b>Support Page for ({entity}):</b><br />
<p>
Support Page: {support_json.support_page}
</p>
</root>
} else {
return <root>
<b>Support info for ({entity}):</b><br />
<p>
Support Page: {support_json.support_page}
</p><br />
<ul>
{support_json.contacts!.map(r => <li><b>{r.role}</b> - {r.matrix_id ? <a href={Permalinks.forUser(r.matrix_id)}>{r.matrix_id}</a> : <a href="mailto:{r.email_address}">{r.email_address}</a>}<br /></li>)}
</ul>
</root>
}
}

defineMatrixInterfaceAdaptor({
interfaceCommand: findTableCommand("mjolnir", "queryAdmin"),
renderer: async function (client, commandRoomId, event, result) {
await renderMatrixAndSend(
renderSupportJson(result.ok),
commandRoomId, event, client
);
}
})
53 changes: 53 additions & 0 deletions test/integration/commands/queryAdminDetailsTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { strict as assert } from "assert";

import { newTestUser } from "../clientHelper";
import { getFirstReply } from "./commandUtils";

describe("Test: The queryAdmin command", function () {
// If a test has a timeout while awaitng on a promise then we never get given control back.
afterEach(function () { this.moderator?.stop(); });

it('Mjölnir can query and display the query results for a complete json.', async function () {
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
this.moderator = moderator;
await moderator.joinRoom(this.config.managementRoom);

const reply_event = await getFirstReply(this.mjolnir.matrixEmitter, this.mjolnir.managementRoomId, async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir queryAdmin http://localhost:8081` });
});
assert.equal(reply_event.content.body, "**Support info for (http://localhost:8081):**\nSupport Page: http://localhost\n\n\n\n * **admin** - [@admin:localhost](https://matrix.to/#/@admin:localhost)\n\n", `Draupnir did not parse the json as expected.`);
})

it('Mjölnir can query and display the query results for a partial contacts-only json.', async function () {
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
this.moderator = moderator;
await moderator.joinRoom(this.config.managementRoom);

const reply_event = await getFirstReply(this.mjolnir.matrixEmitter, this.mjolnir.managementRoomId, async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir queryAdmin http://localhost:7072` });
});
assert.equal(reply_event.content.body, "**Support infos for (http://localhost:7072):**\n\n * **admin** - [@admin:localhost](https://matrix.to/#/@admin:localhost)\n\n", `Draupnir did not parse the json as expected.`);
})

it('Mjölnir can query and display the query results for a partial support_page-only json.', async function () {
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
this.moderator = moderator;
await moderator.joinRoom(this.config.managementRoom);

const reply_event = await getFirstReply(this.mjolnir.matrixEmitter, this.mjolnir.managementRoomId, async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir queryAdmin http://localhost:7071` });
});
assert.equal(reply_event.content.body, "**Support Page for (http://localhost:7071):**\nSupport Page: http://localhost\n\n", `Draupnir did not parse the json as expected.`);
})

it('Mjölnir can query and display an error for a non well-formed json.', async function () {
const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
this.moderator = moderator;
await moderator.joinRoom(this.config.managementRoom);

const reply_event = await getFirstReply(this.mjolnir.matrixEmitter, this.mjolnir.managementRoomId, async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir queryAdmin http://localhost:7070` });
});
assert.equal(reply_event.content.body.includes("The request failed with an error: Error: Error during MatrixClient request GET /.well-known/matrix/support:"), true, `Draupnir did not print an error as.`);
})
});
89 changes: 67 additions & 22 deletions test/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,74 @@ events {

http {
server {
listen [::]:8081 ipv6only=off;

location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ {
# Abuse reports should be sent to Mjölnir.
# The r0 endpoint is deprecated but still used by many clients.
# As of this writing, the v3 endpoint is the up-to-date version.

# Add CORS, otherwise a browser will refuse this request.
add_header 'Access-Control-Allow-Origin' '*' always; # Note: '*' is for testing purposes. For your own server, you probably want to tighten this.
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days

# Alias the regexps, to ensure that they're not rewritten.
set $room_id $2;
set $event_id $3;
proxy_pass http://127.0.0.1:8082/api/1/report/$room_id/$event_id;
listen [::]:8081 ipv6only=off;

location ~ ^/_matrix/client/(r0|v3)/rooms/([^/\s]*)/report/(.*)$ {
# Abuse reports should be sent to Mjölnir.
# The r0 endpoint is deprecated but still used by many clients.
# As of this writing, the v3 endpoint is the up-to-date version.

# Add CORS, otherwise a browser will refuse this request.
add_header 'Access-Control-Allow-Origin' '*' always; # Note: '*' is for testing purposes. For your own server, you probably want to tighten this.
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days

# Alias the regexps, to ensure that they're not rewritten.
set $room_id $2;
set $event_id $3;
proxy_pass http://127.0.0.1:8082/api/1/report/$room_id/$event_id;
}
# MSC1929 Test in best-case
location /.well-known/matrix/support {
default_type application/json;
return 200 '{
"contacts": [{
"matrix_id": "@admin:localhost",
"role": "admin"
}],
"support_page": "http://localhost"
}';
}
location / {
# Everything else should be sent to Synapse.
proxy_pass http://127.0.0.1:9999;
}
}
server {
listen [::]:7070 ipv6only=off;

# MSC1929 Test in worst-case
location /.well-known/matrix/support {
default_type application/json;
return 404 '{}';
}
}
server {
listen [::]:7071 ipv6only=off;

# MSC1929 Test in supportpage_only-case
location /.well-known/matrix/support {
default_type application/json;
return 200 '{
"support_page": "http://localhost"
}';
}
}
location / {
# Everything else should be sent to Synapse.
proxy_pass http://127.0.0.1:9999;
server {
listen [::]:7072 ipv6only=off;

# MSC1929 Test in contacts-case
location /.well-known/matrix/support {
default_type application/json;
return 200 '{
"contacts": [{
"matrix_id": "@admin:localhost",
"role": "admin"
}]
}';
}
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
"./test/integration/protectionSettingsTest.ts",
"./test/integration/banPropagationTest.ts",
"./test/integration/protectedRoomsConfigTest.ts",
"./test/integration/commands/queryAdminDetailsTest.ts",
]
}