mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
AppService start
This commit is contained in:
parent
81cd91c250
commit
94040b00d6
@ -4,6 +4,7 @@ up:
|
|||||||
before:
|
before:
|
||||||
# Launch the reverse proxy, listening for connections *only* on the local host.
|
# 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 -p 127.0.0.1:8081:80 -v $MX_TEST_CWD/test/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx
|
||||||
|
- cp mjolnir-registration.yaml $MX_TEST_SYNAPSE_DIR/data/
|
||||||
after:
|
after:
|
||||||
# Wait until Synapse is ready
|
# Wait until Synapse is ready
|
||||||
- until curl localhost:9999 --stderr /dev/null > /dev/null; do echo "Waiting for Synapse..."; sleep 1s; done
|
- until curl localhost:9999 --stderr /dev/null > /dev/null; do echo "Waiting for Synapse..."; sleep 1s; done
|
||||||
@ -19,7 +20,7 @@ down:
|
|||||||
modules:
|
modules:
|
||||||
- name: mjolnir
|
- name: mjolnir
|
||||||
build:
|
build:
|
||||||
- cp -r synapse_antispam $MX_TEST_MODULE_DIR
|
- cp -r synapse_antispam $MX_TEST_MODULE_DIR/
|
||||||
config:
|
config:
|
||||||
module: mjolnir.Module
|
module: mjolnir.Module
|
||||||
config: {}
|
config: {}
|
||||||
@ -34,6 +35,9 @@ homeserver:
|
|||||||
enable_registration: true
|
enable_registration: true
|
||||||
enable_registration_without_verification: true
|
enable_registration_without_verification: true
|
||||||
|
|
||||||
|
app_service_config_files:
|
||||||
|
- "/data/mjolnir-registration.yaml"
|
||||||
|
|
||||||
# We remove rc_message so we can test rate limiting,
|
# We remove rc_message so we can test rate limiting,
|
||||||
# but we keep the others because of https://github.com/matrix-org/synapse/issues/11785
|
# but we keep the others because of https://github.com/matrix-org/synapse/issues/11785
|
||||||
# and we don't want to slow integration tests down.
|
# and we don't want to slow integration tests down.
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsdom": "^16.2.11",
|
"@types/jsdom": "^16.2.11",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
|
"@types/nedb": "^1.8.12",
|
||||||
"@types/node": "^16.7.10",
|
"@types/node": "^16.7.10",
|
||||||
"@types/shell-quote": "1.7.1",
|
"@types/shell-quote": "1.7.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
@ -43,7 +44,8 @@
|
|||||||
"humanize-duration-ts": "^2.1.1",
|
"humanize-duration-ts": "^2.1.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsdom": "^16.6.0",
|
"jsdom": "^16.6.0",
|
||||||
"matrix-bot-sdk": "^0.5.19",
|
"matrix-appservice-bridge": "^4.0.1",
|
||||||
|
"nedb": "^1.8.0",
|
||||||
"parse-duration": "^1.0.2",
|
"parse-duration": "^1.0.2",
|
||||||
"shell-quote": "^1.7.3",
|
"shell-quote": "^1.7.3",
|
||||||
"yaml": "^2.1.1"
|
"yaml": "^2.1.1"
|
||||||
|
101
src/AppService.ts
Normal file
101
src/AppService.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* What kind of vulnerabilities make process isolation count?
|
||||||
|
* Either way they have the token to the appservice even with process isolation
|
||||||
|
* the bounds are limitless.
|
||||||
|
*
|
||||||
|
* In both casees we have to migrate the configuration away from being static
|
||||||
|
* so that it can be built on the fly to start new processes.
|
||||||
|
* Yes we could also write to a file but that's disgusting and i refuse.
|
||||||
|
* The config is the biggest piece of static bullshit that makes things a pita,
|
||||||
|
* so this removes one of the arguments against in-process work.
|
||||||
|
*
|
||||||
|
* Ok so my idea is to just use fork and have a special Mjolnir instance
|
||||||
|
* that basically proxies the mjolnir in the forked processes.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { AppServiceRegistration, Bridge, Cli, Request, WeakEvent, BridgeContext, MatrixUser, UserBridgeStore, RemoteUser } from "matrix-appservice-bridge";
|
||||||
|
// needed by appservice irc, though it looks completely dead.
|
||||||
|
import * as Datastore from "nedb";
|
||||||
|
import { MjolnirManager } from "./appservice/MjolnirManager";
|
||||||
|
//import config from "./config";
|
||||||
|
// node index.js -r -u "http://localhost:9000" # remember to add the registration! you probably want host.docker.internal if using mx-tester
|
||||||
|
// node index.js -p 9000
|
||||||
|
|
||||||
|
class MjolnirAppService {
|
||||||
|
|
||||||
|
public readonly bridge: Bridge;
|
||||||
|
private readonly mjolnirManager: MjolnirManager = new MjolnirManager();
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.bridge = new Bridge({
|
||||||
|
homeserverUrl: "http://localhost:9999",
|
||||||
|
domain: "localhost",
|
||||||
|
registration: "mjolnir-registration.yaml",
|
||||||
|
controller: {
|
||||||
|
onUserQuery: this.onUserQuery.bind(this),
|
||||||
|
onEvent: this.onEvent.bind(this),
|
||||||
|
},
|
||||||
|
userStore: new UserBridgeStore(new Datastore()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async provisionNewMjolnir(requestingUserId: string) {
|
||||||
|
// FIXME: we need to restrict who can do it (special list? ban remote users?)
|
||||||
|
const issuedMjolnirs = await this.bridge.getUserStore()!.getRemoteUsersFromMatrixId(requestingUserId);
|
||||||
|
if (issuedMjolnirs.length === 0) {
|
||||||
|
// Now we need to make one of the transparent mjolnirs and add it to the monitor.
|
||||||
|
const mjIntent = await this.bridge.getIntentFromLocalpart(`mjolnir_${randomUUID()}`);
|
||||||
|
await mjIntent.ensureRegistered();
|
||||||
|
// we're only doing this because it's complaining about missing profiles.
|
||||||
|
// actually the user id wasn't even right, so this might not be necessary anymore.
|
||||||
|
await mjIntent.ensureProfile('Mjolnir');
|
||||||
|
this.mjolnirManager.createNew(requestingUserId, mjIntent);
|
||||||
|
// Technically the mjolnir is a remote user, but also not because it's matrix-matrix.
|
||||||
|
//const mjAsRemote = new RemoteUser(mjIntent.userId)
|
||||||
|
//const bridgeStore = this.bridge.getUserStore()!;
|
||||||
|
//bridgeStore.setRemoteUser()
|
||||||
|
await this.bridge.getUserStore()!.linkUsers(new MatrixUser(requestingUserId), new RemoteUser(mjIntent.userId));
|
||||||
|
} else {
|
||||||
|
throw new Error(`User: ${requestingUserId} has already provisioned ${issuedMjolnirs.length} mjolnirs.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUserQuery (queriedUser: MatrixUser) {
|
||||||
|
return {}; // auto-provision users with no additonal data
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it ok for this to be async? seems a bit dodge.
|
||||||
|
// it should be BridgeRequestEvent not whatever this is
|
||||||
|
public async onEvent(request: Request<WeakEvent>, context: BridgeContext) {
|
||||||
|
// https://github.com/matrix-org/matrix-appservice-irc/blob/develop/src/bridge/MatrixHandler.ts#L921
|
||||||
|
// ^ that's how matrix-appservice-irc maps from room to channel, we basically need to do the same but map
|
||||||
|
// from room to which mjolnir it's for, unless that information is present in BridgeContext, which it might be...
|
||||||
|
const mxEvent = request.getData();
|
||||||
|
if ('m.room.member' === mxEvent.type) {
|
||||||
|
if ('invite' === mxEvent.content['membership'] && mxEvent.state_key === this.bridge.botUserId) {
|
||||||
|
await this.provisionNewMjolnir(mxEvent.sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mjolnirManager.onEvent(request, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Cli({
|
||||||
|
registrationPath: "mjolnir-registration.yaml",
|
||||||
|
generateRegistration: function(reg, callback) {
|
||||||
|
reg.setId(AppServiceRegistration.generateToken());
|
||||||
|
reg.setHomeserverToken(AppServiceRegistration.generateToken());
|
||||||
|
reg.setAppServiceToken(AppServiceRegistration.generateToken());
|
||||||
|
reg.setSenderLocalpart("mjolnir");
|
||||||
|
reg.addRegexPattern("users", "@mjolnir_.*", true);
|
||||||
|
callback(reg);
|
||||||
|
},
|
||||||
|
run: function(port: number) {
|
||||||
|
const service = new MjolnirAppService();
|
||||||
|
console.log("Matrix-side listening on port %s", port);
|
||||||
|
service.bridge.run(port);
|
||||||
|
}
|
||||||
|
}).run();
|
101
src/appservice/MjolnirManager.ts
Normal file
101
src/appservice/MjolnirManager.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Mjolnir } from "../Mjolnir";
|
||||||
|
import { Intent, Request, WeakEvent, BridgeContext } from "matrix-appservice-bridge";
|
||||||
|
import { IConfig, setDefaults } from "../config";
|
||||||
|
import { SHORTCODE_EVENT_TYPE } from "../models/BanList";
|
||||||
|
import { Permalinks } from "matrix-bot-sdk";
|
||||||
|
|
||||||
|
export class MjolnirManager {
|
||||||
|
private readonly mjolnirs: Map</*the user id of the mjolnir*/string, ManagedMjolnir> = new Map();
|
||||||
|
|
||||||
|
public getDefaultMjolnirConfig(managementRoom: string): IConfig {
|
||||||
|
return setDefaults({managementRoom});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createNew(requestingUserId: string, intent: Intent) {
|
||||||
|
// FIXME: We should be creating the intent here and generating the id surely?
|
||||||
|
// rather than externally...
|
||||||
|
// FIXME: We need to verify that we haven't stored a mjolnir already if we aren't doing the above.
|
||||||
|
|
||||||
|
// get mjolnir list wroking by just avoiding it for now and see if protections work
|
||||||
|
// and bans.
|
||||||
|
// Find out trade offs of changing mjolnir to make it work vs making new subcomponent of mjolnir.
|
||||||
|
const managementRoomId = (await intent.createRoom({
|
||||||
|
createAsClient: true,
|
||||||
|
options: {
|
||||||
|
preset: 'private_chat',
|
||||||
|
invite: [requestingUserId],
|
||||||
|
name: `${requestingUserId}'s mjolnir`
|
||||||
|
}
|
||||||
|
})).room_id;
|
||||||
|
const managedMjolnir = new ManagedMjolnir(intent, await Mjolnir.setupMjolnirFromConfig(intent.matrixClient, this.getDefaultMjolnirConfig(managementRoomId)));
|
||||||
|
await managedMjolnir.moveMeSomewhereCommonAndStopImplementingFunctionalityOnACommandFirstBasis(requestingUserId, 'list')
|
||||||
|
this.mjolnirs.set(intent.userId, managedMjolnir);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public onEvent(request: Request<WeakEvent>, context: BridgeContext) {
|
||||||
|
// We honestly don't know how we're going to map from bridge to user
|
||||||
|
// https://github.com/matrix-org/matrix-appservice-bridge/blob/6046d31c54d461ad53e6d6e244ce2d944b62f890/src/components/room-bridge-store.ts
|
||||||
|
// looks like it might work, but we will ask, figure it out later.
|
||||||
|
[...this.mjolnirs.values()].forEach((mj: ManagedMjolnir) => mj.onEvent(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Isolating this mjolnir is going to require provisioning an access token just for this user to be useful.
|
||||||
|
// We can use fork and node's IPC to inform the process of matrix evnets.
|
||||||
|
export class ManagedMjolnir {
|
||||||
|
|
||||||
|
|
||||||
|
public constructor(private readonly intent: Intent, private readonly mjolnir: Mjolnir) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onEvent(request: Request<WeakEvent>) {
|
||||||
|
// phony sync.
|
||||||
|
const mxEvent = request.getData();
|
||||||
|
if (mxEvent['type'] !== undefined) {
|
||||||
|
this.mjolnir.client.emit('room.event', mxEvent.room_id, mxEvent);
|
||||||
|
if (mxEvent.type === 'm.room.message') {
|
||||||
|
this.mjolnir.client.emit('room.message', mxEvent.room_id, mxEvent);
|
||||||
|
}
|
||||||
|
// room.join requires us to know the joined rooms before so lol.
|
||||||
|
}
|
||||||
|
if (mxEvent['type'] === 'm.room.member') {
|
||||||
|
if (mxEvent['content']['membership'] === 'invite' && mxEvent.state_key === this.intent.userId) {
|
||||||
|
this.mjolnir.client.emit('room.invite', mxEvent.room_id, mxEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async moveMeSomewhereCommonAndStopImplementingFunctionalityOnACommandFirstBasis(mjolnirOwnerId: string, shortcode: string) {
|
||||||
|
const powerLevels: { [key: string]: any } = {
|
||||||
|
"ban": 50,
|
||||||
|
"events": {
|
||||||
|
"m.room.name": 100,
|
||||||
|
"m.room.power_levels": 100,
|
||||||
|
},
|
||||||
|
"events_default": 50, // non-default
|
||||||
|
"invite": 0,
|
||||||
|
"kick": 50,
|
||||||
|
"notifications": {
|
||||||
|
"room": 20,
|
||||||
|
},
|
||||||
|
"redact": 50,
|
||||||
|
"state_default": 50,
|
||||||
|
"users": {
|
||||||
|
[this.intent.userId]: 100,
|
||||||
|
[mjolnirOwnerId]: 50
|
||||||
|
},
|
||||||
|
"users_default": 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listRoomId = await this.mjolnir.client.createRoom({
|
||||||
|
preset: "public_chat",
|
||||||
|
invite: [mjolnirOwnerId],
|
||||||
|
initial_state: [{type: SHORTCODE_EVENT_TYPE, state_key: "", content: {shortcode: shortcode}}],
|
||||||
|
power_level_content_override: powerLevels,
|
||||||
|
});
|
||||||
|
|
||||||
|
const roomRef = Permalinks.forRoom(listRoomId);
|
||||||
|
await this.mjolnir.watchList(roomRef);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user