Skip to content

Commit

Permalink
Merge branch 'main' into gnuxie/replace-groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Gnuxie committed Aug 9, 2022
2 parents 681106c + 21aabc8 commit 2042d9b
Show file tree
Hide file tree
Showing 35 changed files with 780 additions and 579 deletions.
148 changes: 78 additions & 70 deletions src/Mjolnir.ts

Large diffs are not rendered by default.

23 changes: 16 additions & 7 deletions src/actions/ApplyAcl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import BanList from "../models/BanList";
import PolicyList from "../models/PolicyList";
import { ServerAcl } from "../models/ServerAcl";
import { RoomUpdateError } from "../models/RoomUpdateError";
import { Mjolnir } from "../Mjolnir";
import config from "../config";
import { LogLevel, UserID } from "matrix-bot-sdk";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";

/**
* Applies the server ACLs represented by the ban lists to the provided rooms, returning the
* room IDs that could not be updated and their error.
* Does not update the banLists before taking their rules to build the server ACL.
* @param {BanList[]} lists The lists to construct ACLs from.
* @param {PolicyList[]} lists The lists to construct ACLs from.
* @param {string[]} roomIds The room IDs to apply the ACLs in.
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
*/
export async function applyServerAcls(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
export async function applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
// we need to provide mutual exclusion so that we do not have requests updating the m.room.server_acl event
// finish out of order and therefore leave the room out of sync with the policy lists.
return new Promise((resolve, reject) => {
mjolnir.aclChain = mjolnir.aclChain
.then(() => _applyServerAcls(lists, roomIds, mjolnir))
.then(resolve, reject);
});
}

async function _applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain;

// Construct a server ACL first
Expand All @@ -47,7 +56,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Mjölnir has detected and removed an ACL that would exclude itself. Please check the ACL lists.`);
}

if (config.verboseLogging) {
if (mjolnir.config.verboseLogging) {
// We specifically use sendNotice to avoid having to escape HTML
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`);
}
Expand All @@ -70,15 +79,15 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
// We specifically use sendNotice to avoid having to escape HTML
await mjolnir.logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, roomId);

if (!config.noop) {
if (!mjolnir.config.noop) {
await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl);
} else {
await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}
} catch (e) {
const message = e.message || (e.body ? e.body.error : '<no message>');
const kind = message && message.includes("You don't have permission to post that to the room") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL;
errors.push({roomId, errorMessage: message, errorKind: kind});
errors.push({ roomId, errorMessage: message, errorKind: kind });
}
}

Expand Down
15 changes: 7 additions & 8 deletions src/actions/ApplyBan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import BanList from "../models/BanList";
import PolicyList from "../models/PolicyList";
import { RoomUpdateError } from "../models/RoomUpdateError";
import { Mjolnir } from "../Mjolnir";
import config from "../config";
import { LogLevel } from "matrix-bot-sdk";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";

/**
* Applies the member bans represented by the ban lists to the provided rooms, returning the
* room IDs that could not be updated and their error.
* @param {BanList[]} lists The lists to determine bans from.
* @param {PolicyList[]} lists The lists to determine bans from.
* @param {string[]} roomIds The room IDs to apply the bans in.
* @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with.
*/
export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
export async function applyUserBans(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
// We can only ban people who are not already banned, and who match the rules.
const errors: RoomUpdateError[] = [];
for (const roomId of roomIds) {
Expand All @@ -38,15 +37,15 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir

let members: { userId: string, membership: string }[];

if (config.fasterMembershipChecks) {
if (mjolnir.config.fasterMembershipChecks) {
const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId);
members = memberIds.map(u => {
return {userId: u, membership: "join"};
return { userId: u, membership: "join" };
});
} else {
const state = await mjolnir.client.getRoomState(roomId);
members = state.filter(s => s['type'] === 'm.room.member' && !!s['state_key']).map(s => {
return {userId: s['state_key'], membership: s['content'] ? s['content']['membership'] : 'leave'};
return { userId: s['state_key'], membership: s['content'] ? s['content']['membership'] : 'leave' };
});
}

Expand All @@ -64,7 +63,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir
// We specifically use sendNotice to avoid having to escape HTML
await mjolnir.logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`, roomId);

if (!config.noop) {
if (!mjolnir.config.noop) {
await mjolnir.client.banUser(member.userId, roomId, userRule.reason);
if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) {
mjolnir.queueRedactUserMessagesIn(member.userId, roomId);
Expand Down
6 changes: 4 additions & 2 deletions src/commands/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import { execRedactCommand } from "./RedactCommand";
import { execImportCommand } from "./ImportCommand";
import { execSetDefaultListCommand } from "./SetDefaultBanListCommand";
import { execDeactivateCommand } from "./DeactivateCommand";
import { execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection,
execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection } from "./ProtectionsCommands";
import {
execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection,
execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection
} from "./ProtectionsCommands";
import { execListProtectedRooms } from "./ListProtectedRoomsCommand";
import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand";
import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand";
Expand Down
4 changes: 2 additions & 2 deletions src/commands/CreateBanListCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import { Mjolnir } from "../Mjolnir";
import { SHORTCODE_EVENT_TYPE } from "../models/BanList";
import { SHORTCODE_EVENT_TYPE } from "../models/PolicyList";
import { Permalinks, RichReply } from "matrix-bot-sdk";

// !mjolnir list create <shortcode> <alias localpart>
Expand Down Expand Up @@ -48,7 +48,7 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir:
preset: "public_chat",
room_alias_name: aliasLocalpart,
invite: [event['sender']],
initial_state: [{type: SHORTCODE_EVENT_TYPE, state_key: "", content: {shortcode: shortcode}}],
initial_state: [{ type: SHORTCODE_EVENT_TYPE, state_key: "", content: { shortcode: shortcode } }],
power_level_content_override: powerLevels,
});

Expand Down
20 changes: 12 additions & 8 deletions src/commands/DumpRulesCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

import { RichReply } from "matrix-bot-sdk";
import { Mjolnir } from "../Mjolnir";
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList";
import { EntityType } from "../models/ListRule";
import { htmlEscape } from "../utils";

/**
Expand All @@ -33,7 +33,7 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln
let html = "";
let text = "";
for (const list of mjolnir.lists) {
const matches = list.rulesMatchingEntity(entity)
const matches = list.rulesMatchingEntity(entity)

if (matches.length === 0) {
continue;
Expand All @@ -48,12 +48,16 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln
for (const rule of matches) {
// If we know the rule kind, we will give it a readable name, otherwise just use its name.
let ruleKind: string = rule.kind;
if (ruleKind === RULE_USER) {
ruleKind = 'user';
} else if (ruleKind === RULE_SERVER) {
ruleKind = 'server';
} else if (ruleKind === RULE_ROOM) {
ruleKind = 'room';
switch (ruleKind) {
case EntityType.RULE_USER:
ruleKind = 'user';
break;
case EntityType.RULE_SERVER:
ruleKind = 'server';
break;
case EntityType.RULE_ROOM:
ruleKind = 'room';
break;
}
html += `<li>${htmlEscape(ruleKind)} (<code>${htmlEscape(rule.recommendation ?? "")}</code>): <code>${htmlEscape(rule.entity)}</code> (${htmlEscape(rule.reason)})</li>`;
text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`;
Expand Down
13 changes: 5 additions & 8 deletions src/commands/ImportCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ limitations under the License.

import { Mjolnir } from "../Mjolnir";
import { RichReply } from "matrix-bot-sdk";
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
import { RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList";
import { EntityType, Recommendation } from "../models/ListRule";

// !mjolnir import <room ID> <shortcode>
export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
Expand Down Expand Up @@ -45,14 +44,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo

await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`);

const recommendation = recommendationToStable(RECOMMENDATION_BAN);
const ruleContent = {
entity: stateEvent['state_key'],
recommendation,
recommendation: Recommendation.Ban,
reason: reason,
};
const stateKey = `rule:${ruleContent.entity}`;
let stableRule = ruleTypeToStable(RULE_USER);
let stableRule = EntityType.RULE_USER;
if (stableRule) {
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
}
Expand All @@ -66,14 +64,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo

await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`);

const recommendation = recommendationToStable(RECOMMENDATION_BAN);
const ruleContent = {
entity: server,
recommendation,
recommendation: Recommendation.Ban,
reason: reason,
};
const stateKey = `rule:${ruleContent.entity}`;
let stableRule = ruleTypeToStable(RULE_SERVER);
let stableRule = EntityType.RULE_SERVER;
if (stableRule) {
await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent);
}
Expand Down
5 changes: 2 additions & 3 deletions src/commands/KickCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.

import { Mjolnir } from "../Mjolnir";
import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk";
import config from "../config";

// !mjolnir kick <user|filter> [room] [reason]
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
Expand All @@ -30,7 +29,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
parts.pop();
}

if (config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) {
if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) {
let replyMessage = "Wildcard bans require an addition `--force` argument to confirm";
const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage);
reply["msgtype"] = "m.notice";
Expand Down Expand Up @@ -60,7 +59,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
if (kickRule.test(victim)) {
await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);

if (!config.noop) {
if (!mjolnir.config.noop) {
try {
await mjolnir.taskQueue.push(async () => {
return mjolnir.client.kickUser(victim, protectedRoomId, reason);
Expand Down
3 changes: 1 addition & 2 deletions src/commands/MakeRoomAdminCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import config from "../config";
import { Mjolnir } from "../Mjolnir";
import { RichReply } from "matrix-bot-sdk";

// !mjolnir make admin <room> [<user ID>]
export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const isAdmin = await mjolnir.isSynapseAdmin();
if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) {
if (!mjolnir.config.admin?.enableMakeRoomAdminCommand || !isAdmin) {
const message = "Either the command is disabled or I am not running as homeserver administrator.";
const reply = RichReply.createFor(roomId, event, message, message);
reply['msgtype'] = "m.notice";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/SetDefaultBanListCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ export async function execSetDefaultListCommand(roomId: string, event: any, mjol
return;
}

await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, {shortcode});
await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, { shortcode });
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅');
}
20 changes: 9 additions & 11 deletions src/commands/UnbanBanCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,20 @@ limitations under the License.
*/

import { Mjolnir } from "../Mjolnir";
import BanList, { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/BanList";
import PolicyList from "../models/PolicyList";
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk";
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
import config from "../config";
import { Recommendation, RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule";
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";

interface Arguments {
list: BanList | null;
list: PolicyList | null;
entity: string;
ruleType: string | null;
reason: string;
}

// Exported for tests
export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise<Arguments|null> {
export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise<Arguments | null> {
let defaultShortcode: string | null = null;
try {
const data: { shortcode: string } = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE);
Expand All @@ -44,7 +43,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
let argumentIndex = 2;
let ruleType: string | null = null;
let entity: string | null = null;
let list: BanList | null = null;
let list: PolicyList | null = null;
let force = false;
while (argumentIndex < 7 && argumentIndex < parts.length) {
const arg = parts[argumentIndex++];
Expand Down Expand Up @@ -95,7 +94,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni
else if (!ruleType) replyMessage = "Please specify the type as either 'user', 'room', or 'server'";
else if (!entity) replyMessage = "No entity found";

if (config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) {
if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) {
replyMessage = "Wildcard bans require an additional `--force` argument to confirm";
}

Expand All @@ -119,10 +118,9 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni
const bits = await parseArguments(roomId, event, mjolnir, parts);
if (!bits) return; // error already handled

const recommendation = recommendationToStable(RECOMMENDATION_BAN);
const ruleContent = {
entity: bits.entity,
recommendation,
recommendation: Recommendation.Ban,
reason: bits.reason || '<no reason supplied>',
};
const stateKey = `rule:${bits.entity}`;
Expand Down Expand Up @@ -151,7 +149,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol
if (rule.test(victim)) {
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);

if (!config.noop) {
if (!mjolnir.config.noop) {
await mjolnir.client.unbanUser(victim, protectedRoomId);
} else {
await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId);
Expand All @@ -164,7 +162,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol

if (unbannedSomeone) {
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`);
await mjolnir.syncLists(config.verboseLogging);
await mjolnir.syncLists(mjolnir.config.verboseLogging);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { MatrixClient } from "matrix-bot-sdk";
// The object is magically generated by external lib `config`
// from the file specified by `NODE_ENV`, e.g. production.yaml
// or harness.yaml.
interface IConfig {
export interface IConfig {
homeserverUrl: string;
rawHomeserverUrl: string;
accessToken: string;
Expand Down
3 changes: 2 additions & 1 deletion src/health/healthz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import config from "../config";
import * as http from "http";
import { LogService } from "matrix-bot-sdk";
// allowed to use the global configuration since this is only intended to be used by `src/index.ts`.
import config from '../config';

export class Healthz {
private static healthCode: number;
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@ if (config.health.healthz.enabled) {
patchMatrixClient();
config.RUNTIME.client = client;

bot = await Mjolnir.setupMjolnirFromConfig(client);
bot = await Mjolnir.setupMjolnirFromConfig(client, config);
} catch (err) {
console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`);
throw err;
}
try {
await bot.start();
Healthz.isHealthy = true;
} catch (err) {
console.error(`Mjolnir failed to start: ${err}`);
throw err;
Expand Down
Loading

0 comments on commit 2042d9b

Please sign in to comment.