Commit 3ff043aa authored by Grant's avatar Grant
Browse files

refactor webserver & add initial admin endpoints

parent 0d74e824
Loading
Loading
Loading
Loading
Loading

src/bridge.ts

deleted100644 → 0
+0 −3
Original line number Diff line number Diff line
class Bridge_ {}

export const Bridge = new Bridge_();
+35 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ import {
  PartialMessageReaction,
  PartialUser,
  Partials,
  PermissionResolvable,
  ReactionEmoji,
  Typing,
  User,
@@ -119,6 +120,32 @@ class Discord_ {
    throw new Error("Unknown proxy type");
  }

  async checkPermissions(guildId: string, channelId: string): Promise<true> {
    const guild = await this.client.guilds.fetch(guildId);
    if (!guild.members.me) throw new Error("Not member of guild");

    const channel = await guild.channels.fetch(channelId);
    if (!channel) throw new Error("Channel not found");

    const REQUIRED_PERMISSIONS: PermissionResolvable[] = [
      "ManageWebhooks",
      "ManageMessages",
      "ManageThreads",
      "UseExternalSounds",
    ];

    const perms = channel.permissionsFor(guild.members.me);
    const hasAllPerms = REQUIRED_PERMISSIONS.map((perm) =>
      perms.has(perm)
    ).every((e) => Boolean(e));

    if (!hasAllPerms) {
      throw new Error("Does not have all permissions");
    }

    return true;
  }

  async getAttachmentURL(
    guild_id: string,
    channel_id: string,
@@ -1214,6 +1241,14 @@ class Discord_ {
    }
  }

  async sendNotice(guild_id: string, channel_id: string, content: string) {
    const guild = await this.client.guilds.fetch(guild_id);
    const channel = await guild.channels.fetch(channel_id);
    if (!channel || !channel.isSendable()) return;

    return channel.send(content);
  }

  async sendMessage(
    guild_id: string,
    channel_id: string,
+3 −2
Original line number Diff line number Diff line
import "./lib/sentry";
import "./types/env";

import { Secrets } from "./lib/secrets";
import "./webserver";
import { Discord } from "./discord";
import { Matrix } from "./lib/matrix";
import { WebServer } from "./routes";

new WebServer().listen();

Discord.login(Secrets.get("DISCORD_TOKEN"));

+65 −3
Original line number Diff line number Diff line
@@ -6,6 +6,8 @@ import {
  MatrixReactionEvent,
  MatrixRoomMembership,
  MatrixRoomMessage,
  MatrixRoomPowerLevels,
  MatrixRoomPowerLevelsDefaults,
  SentClientEvent,
} from "../types/matrix";
import { FriendlyError } from "./utils";
@@ -169,10 +171,22 @@ export class Matrix {
    );
  }

  static async isUserInRoom(roomId: string, user: IUsername): Promise<boolean> {
    const req = await this.fetch<MatrixRoomMembership["content"]>(
      `/_matrix/client/v3/rooms/${roomId}/state/m.room.member/@${user}:${MATRIX_HOMESERVER}`
  static async getState<T>(roomId: string, state: string, key: string) {
    return await this.fetch<T>(
      `/_matrix/client/v3/rooms/${roomId}/state/${state}/${key}`
    );
  }

  static async getMemberState(roomId: string, user: IUsername) {
    return await this.getState<MatrixRoomMembership["content"]>(
      roomId,
      `m.room.member`,
      `@${user}:${MATRIX_HOMESERVER}`
    );
  }

  static async isUserInRoom(roomId: string, user: IUsername): Promise<boolean> {
    const req = await this.getMemberState(roomId, user);

    if ("errcode" in req.data) {
      // if(req.data.errcode === "M_NOT_FOUND")
@@ -569,6 +583,54 @@ export class Matrix {
      "?via=" +
      MATRIX_HOMESERVER) as any;
  }

  static hasPermission(
    powerLevels: MatrixRoomPowerLevels["content"],
    user_id: IUsername,
    event_type: "invite" | "kick" | "redact" | "ban" | (string & {})
  ): boolean {
    const effectiveLevel =
      powerLevels.users[`@${user_id}:${MATRIX_HOMESERVER}`] ??
      powerLevels.users_default;

    let requiredLevel =
      powerLevels.events[event_type] ?? powerLevels.events_default;

    if (["invite", "kick", "redact", "ban"].indexOf(event_type) > -1) {
      requiredLevel =
        powerLevels[event_type as Exclude<typeof event_type, string & {}>] ??
        MatrixRoomPowerLevelsDefaults[
          event_type as Exclude<typeof event_type, string & {}>
        ];
    }

    return effectiveLevel >= requiredLevel;
  }

  static async checkPermissions(room_id: string): Promise<true> {
    const member = await this.getMemberState(room_id, `_discord_bot`);
    if ("errcode" in member.data) throw new Error("Unable to get member");
    if (member.data.membership !== "join") throw new Error("Not in room");

    const powerLevels = await this.getState<MatrixRoomPowerLevels["content"]>(
      room_id,
      "m.room.power_levels",
      ""
    );
    if ("errcode" in powerLevels.data)
      throw new Error("Unable to get power levels");

    const powerLevelsD: MatrixRoomPowerLevels["content"] = powerLevels.data;

    const REQUIRED_PERMS = ["invite", "kick", "ban", "redact"];

    const hasAll = REQUIRED_PERMS.map((p) =>
      this.hasPermission(powerLevelsD, `_discord_bot`, p)
    ).every((a) => Boolean(a));
    if (!hasAll) throw new Error("Does not have all required permissions");

    return true;
  }
}

type IUsername = `_discord_${string}`;
+4 −1
Original line number Diff line number Diff line
@@ -4,7 +4,8 @@ type SecretName =
  | "MATRIX_AS_TOKEN"
  | "MATRIX_HS_TOKEN"
  | "DISCORD_TOKEN"
  | "SIGNED_SECRET";
  | "SIGNED_SECRET"
  | "ADMIN_TOKEN";

export class Secrets {
  private static instance: Secrets;
@@ -13,6 +14,7 @@ export class Secrets {
    MATRIX_HS_TOKEN: "",
    DISCORD_TOKEN: "",
    SIGNED_SECRET: "",
    ADMIN_TOKEN: "",
  };

  constructor() {
@@ -21,6 +23,7 @@ export class Secrets {
    this.load("MATRIX_AS_TOKEN");
    this.load("MATRIX_HS_TOKEN");
    this.load("SIGNED_SECRET");
    this.load("ADMIN_TOKEN");
  }

  private load(secret: SecretName) {
Loading