├── .dockerignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── docker.yml │ ├── newsfile.yml │ ├── sign-off.yml │ └── tests.yml ├── .gitignore ├── .mocharc.yml ├── .npmrc ├── .nycrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── changelog.d └── git.keep ├── config ├── config.sample.yaml └── config.schema.yaml ├── docs ├── bridge-migrations.md ├── howto.md └── puppeting.md ├── package.json ├── pyproject.toml ├── screenshot.png ├── scripts ├── changelog.sh └── check-newsfragment ├── secstart.sh ├── src ├── bot.ts ├── channelsyncroniser.ts ├── clientfactory.ts ├── config.ts ├── db │ ├── connector.ts │ ├── dbdataemoji.ts │ ├── dbdataevent.ts │ ├── dbdatainterface.ts │ ├── postgres.ts │ ├── roomstore.ts │ ├── schema │ │ ├── dbschema.ts │ │ ├── v1.ts │ │ ├── v10.ts │ │ ├── v11.ts │ │ ├── v12.ts │ │ ├── v2.ts │ │ ├── v3.ts │ │ ├── v4.ts │ │ ├── v5.ts │ │ ├── v6.ts │ │ ├── v7.ts │ │ ├── v8.ts │ │ └── v9.ts │ ├── sqlite3.ts │ └── userstore.ts ├── discordas.ts ├── discordcommandhandler.ts ├── discordmessageprocessor.ts ├── log.ts ├── matrixcommandhandler.ts ├── matrixeventprocessor.ts ├── matrixmessageprocessor.ts ├── matrixroomhandler.ts ├── matrixtypes.ts ├── metrics.ts ├── presencehandler.ts ├── provisioner.ts ├── store.ts ├── structures │ ├── lock.ts │ └── timedcache.ts ├── usersyncroniser.ts └── util.ts ├── test ├── config.ts ├── db │ └── test_roomstore.ts ├── mocks │ ├── appservicemock.ts │ ├── channel.ts │ ├── collection.ts │ ├── discordclient.ts │ ├── discordclientfactory.ts │ ├── emoji.ts │ ├── guild.ts │ ├── member.ts │ ├── message.ts │ ├── presence.ts │ ├── role.ts │ └── user.ts ├── structures │ ├── test_lock.ts │ └── test_timedcache.ts ├── test_channelsyncroniser.ts ├── test_clientfactory.ts ├── test_config.ts ├── test_discordbot.ts ├── test_discordcommandhandler.ts ├── test_discordmessageprocessor.ts ├── test_log.ts ├── test_matrixcommandhandler.ts ├── test_matrixeventprocessor.ts ├── test_matrixmessageprocessor.ts ├── test_matrixroomhandler.ts ├── test_presencehandler.ts ├── test_provisioner.ts ├── test_store.ts ├── test_usersyncroniser.ts └── test_util.ts ├── tools ├── addRoomsToDirectory.ts ├── addbot.ts ├── adminme.ts ├── chanfix.ts ├── ghostfix.ts ├── toolshelper.ts └── userClientTools.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | 4 | # This is just stuff we don't need 5 | .travis.yml 6 | .gitignore 7 | build 8 | *.db 9 | discord-registration.yaml 10 | *.png 11 | README.md 12 | test 13 | docs 14 | config.yaml 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "parserOptions": { 5 | "ecmaVersion": 9, 6 | "ecmaFeatures": { 7 | "jsx": false 8 | }, 9 | "project": "tsconfig.json" 10 | }, 11 | "env": { 12 | "node": true, 13 | "jasmine": true 14 | }, 15 | "extends": ["plugin:@typescript-eslint/recommended"], 16 | "rules": { 17 | "ordered-imports": "off", 18 | "no-trailing-spaces": "error", 19 | "max-classes-per-file": ["warn", 1], 20 | "object-literal-sort-keys": "off", 21 | "@typescript-eslint/naming-convention": "warn", 22 | "@typescript-eslint/no-explicit-any": "error", 23 | "@typescript-eslint/prefer-for-of": "error", 24 | "@typescript-eslint/typedef": "warn", 25 | "@typescript-eslint/no-floating-promises": "error", 26 | "curly": "error", 27 | "no-empty": "off", 28 | "no-invalid-this": "error", 29 | "@typescript-eslint/no-throw-literal": "warn", 30 | "prefer-const": "error", 31 | "indent": ["error", 4], 32 | "max-lines": ["warn", 500], 33 | "no-duplicate-imports": "error", 34 | "@typescript-eslint/array-type": "error", 35 | "@typescript-eslint/promise-function-async": "error", 36 | "no-bitwise": "error", 37 | "no-console": "error", 38 | "no-debugger": "error", 39 | "prefer-template": "error", 40 | // Disable these as they were introduced by @typescript-eslint/recommended 41 | "@typescript-eslint/no-use-before-define": "off", 42 | "@typescript-eslint/no-inferrable-types": "off", 43 | "@typescript-eslint/member-delimiter-style": "off", 44 | "@typescript-eslint/no-unused-expressions": "off", 45 | "@typescript-eslint/interface-name-prefix": "off" 46 | }, 47 | "overrides": [ 48 | { 49 | "files": [ 50 | "test/**/*" 51 | ], 52 | "rules": { 53 | "@typescript-eslint/no-empty-function": "off", 54 | "@typescript-eslint/no-explicit-any": "off" 55 | } 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @matrix-org/bridges -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Container Image 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ develop ] 9 | 10 | env: 11 | IMAGE_NAME: ${{ github.repository }} 12 | REGISTRY: ghcr.io 13 | 14 | jobs: 15 | push: 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | contents: read 20 | packages: write 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Log into registry ${{ env.REGISTRY }} 27 | uses: docker/login-action@v1 28 | with: 29 | registry: ${{ env.REGISTRY }} 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Extract Docker metadata 34 | id: meta 35 | uses: docker/metadata-action@v3 36 | with: 37 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 38 | 39 | - name: Build and push Docker image 40 | uses: docker/build-push-action@v3 41 | with: 42 | push: ${{ github.event_name != 'pull_request' }} 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | -------------------------------------------------------------------------------- /.github/workflows/newsfile.yml: -------------------------------------------------------------------------------- 1 | name: Newsfile 2 | 3 | on: 4 | push: 5 | branches: ["develop", "release-*"] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | changelog: 11 | if: ${{ github.base_ref == 'develop' || contains(github.base_ref, 'release-') }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | ref: ${{ github.event.pull_request.head.sha }} 17 | fetch-depth: 0 18 | - uses: actions/setup-python@v2 19 | - run: pip install towncrier==21.9.0 20 | - run: scripts/check-newsfragment 21 | env: 22 | PULL_REQUEST_NUMBER: ${{ github.event.number }} 23 | -------------------------------------------------------------------------------- /.github/workflows/sign-off.yml: -------------------------------------------------------------------------------- 1 | name: Contribution requirements 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize] 6 | 7 | jobs: 8 | signoff: 9 | uses: matrix-org/backend-meta/.github/workflows/sign-off.yml@v2 10 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 18 14 | - run: yarn 15 | - run: yarn lint 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node_version: [18, 20] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node_version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node_version }} 27 | - run: yarn 28 | - run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | 4 | # Logs 5 | logs 6 | *.log 7 | *.log.* 8 | npm-debug.log* 9 | .audit.json 10 | *-audit.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules 37 | jspm_packages 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional local Yarn settings 43 | .yarn 44 | .yarnrc 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | config.yaml 50 | discord-registration.yaml 51 | 52 | # Ignore TS output 53 | build 54 | 55 | *.db 56 | *.db.backup 57 | 58 | .vscode/ 59 | *.code-workspace 60 | 61 | # Local History for Visual Studio Code 62 | .history/ 63 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: list 2 | ui: bdd 3 | require: 4 | - "ts-node/register" 5 | - "source-map-support/register" 6 | recursive: true -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | @mx-puppet:registry="https://gitlab.com/api/v4/packages/npm/" 3 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "reporter": ["text", "text-summary", "lcov"], 4 | "all": true 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 4.0.0 (2023-09-15) 2 | ================== 3 | 4 | Bugfixes 5 | -------- 6 | 7 | - Prefer server-level display names when available. ([\#888](https://github.com/matrix-org/matrix-appservice-discord/issues/888)) 8 | - Update matrix-appservice-bridge to use a non-deprecated method of authenticating to a homeserver. Fixes #896. ([\#897](https://github.com/matrix-org/matrix-appservice-discord/issues/897)) 9 | - Let file logs correctly ignore modules matching `"logging.files[*].disabled"` in the configuration file. ([\#902](https://github.com/matrix-org/matrix-appservice-discord/issues/902)) 10 | 11 | 12 | Deprecations and Removals 13 | ------------------------- 14 | 15 | - Node.JS 16 is now unsupported, please upgrade to Node.JS 18 or later. Node.JS 18 is now used for Docker. ([\#897](https://github.com/matrix-org/matrix-appservice-discord/issues/897)) 16 | 17 | 18 | Internal Changes 19 | ---------------- 20 | 21 | - Update the package registry for better-discord.js, and use its latest release. ([\#898](https://github.com/matrix-org/matrix-appservice-discord/issues/898)) 22 | - Modify the "start" and "debug" package scripts to not trigger a TypeScript build, and to accept a user-supplied command line option for the path to the bridge configuration file. ([\#900](https://github.com/matrix-org/matrix-appservice-discord/issues/900)) 23 | - Update the GitHub action used for checking pull requests for sign-off status. ([\#901](https://github.com/matrix-org/matrix-appservice-discord/issues/901)) 24 | 25 | 26 | 3.1.1 (2022-11-10) 27 | ================== 28 | 29 | Bugfixes 30 | -------- 31 | 32 | - Fix a crash caused by processing metrics for Matrix events. ([\#869](https://github.com/matrix-org/matrix-appservice-discord/issues/869)) 33 | 34 | 35 | 3.1.0 (2022-11-03) 36 | ================== 37 | 38 | Features 39 | -------- 40 | 41 | - Adds a config value, in order to disable forwarding room topic changes from Matrix to Discord (`disableRoomTopicNotifications`, false by default). ([\#836](https://github.com/matrix-org/matrix-appservice-discord/issues/836)) 42 | 43 | 44 | Bugfixes 45 | -------- 46 | 47 | - Include the domain name in the regular expression. ([\#834](https://github.com/matrix-org/matrix-appservice-discord/issues/834)) 48 | - Remove usage of unreliable field `age` on events, allowing the bridge to work with non-Synapse homeserver implementations. ([\#842](https://github.com/matrix-org/matrix-appservice-discord/issues/842)) 49 | - Prevent crashes when handling messages sent to voice channels. ([\#858](https://github.com/matrix-org/matrix-appservice-discord/issues/858)) 50 | 51 | 52 | 3.0.0 (2022-08-12) 53 | ================== 54 | 55 | Bugfixes 56 | -------- 57 | 58 | - Make sure we don't lose errors thrown when checking usage limits. ([\#823](https://github.com/matrix-org/matrix-appservice-discord/issues/823)) 59 | - Fix Docker instances not starting due to being unable to load a dynamic library in the latest unstable image. ([\#828](https://github.com/matrix-org/matrix-appservice-discord/issues/828)) 60 | - Remove matrix.to hyperlinks when relaying non-Discord user mentions to Discord. 61 | Fix mentioning Matrix users in Discord. ([\#829](https://github.com/matrix-org/matrix-appservice-discord/issues/829)) 62 | 63 | 64 | Deprecations and Removals 65 | ------------------------- 66 | 67 | - Minimum required Node.js version is now 16. ([\#825](https://github.com/matrix-org/matrix-appservice-discord/issues/825)) 68 | 69 | 70 | Internal Changes 71 | ---------------- 72 | 73 | - Remove unused variables. ([\#657](https://github.com/matrix-org/matrix-appservice-discord/issues/657)) 74 | - Add workflow for building docker images, and push new docker images to ghcr.io. ([\#826](https://github.com/matrix-org/matrix-appservice-discord/issues/826)) 75 | - Remove `git config` workaround to pull a dependency from github.com. ([\#830](https://github.com/matrix-org/matrix-appservice-discord/issues/830)) 76 | 77 | 78 | 2.0.0 (2022-08-05) 79 | ================== 80 | 81 | Improved Documentation 82 | ---------------------- 83 | 84 | - Update `CONTRIBUTING.md` guide to reference the newly-updated guide for all of the matrix.org bridge repos. ([\#794](https://github.com/matrix-org/matrix-appservice-discord/issues/794)) 85 | 86 | 87 | Deprecations and Removals 88 | ------------------------- 89 | 90 | - Node.JS 12 is now unsupported, please upgrade to Node.JS 14 or later. Node.JS 16 becomes the new default version. ([\#811](https://github.com/matrix-org/matrix-appservice-discord/issues/811)) 91 | 92 | 93 | Internal Changes 94 | ---------------- 95 | 96 | - Add automatic changelog generation via [Towncrier](https://github.com/twisted/towncrier). ([\#787](https://github.com/matrix-org/matrix-appservice-discord/issues/787)) 97 | - Use `yarn` instead of `npm` for package management and scripts. ([\#796](https://github.com/matrix-org/matrix-appservice-discord/issues/796)) 98 | - Add new CI workflow to check for signoffs. ([\#818](https://github.com/matrix-org/matrix-appservice-discord/issues/818)) 99 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hi there! Please read the [CONTRIBUTING.md](https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md) guide for all matrix.org bridge 2 | projects. 3 | 4 | ## matrix-appservice-discord Guidelines 5 | 6 | * Discussion of ideas for the bridge and work items should be in [#discord:half-shot.uk](https://matrix.to/#/#discord:half-shot.uk). 7 | * Everything submitted as a PR should have at least one test, the only exception being non-code items. 8 | 9 | ## Overview of the Bridge 10 | 11 | The bridge runs as a standalone server that connects to both the Discord API 12 | network and a local Matrix homeserver over the [application service 13 | protocol](https://matrix.org/docs/spec/application_service/unstable.html). 14 | Primarily it syncs events and users from Matrix to Discord and vice versa. 15 | 16 | While the bridge is constantly evolving and we can't keep this section updated 17 | with each component, we follow the principle of handler and processor classes 18 | and each part of the functionality of the bridge will be in a seperate class. 19 | For example, the processing of Matrix events destined for Discord are handled 20 | inside the `MatrixEventProcessor` class. 21 | 22 | ## Setting up 23 | 24 | * You will need to [setup the bridge](https://github.com/Half-Shot/matrix-appservice-discord/tree/develop#setup-the-bridge) similarly to how we describe, 25 | but you should setup a homeserver locally on your development machine. We would recommend [Synapse](https://github.com/matrix-org/synapse). 26 | * The bridge uses `yarn` for dependency management and package scripts instead of `npm`. 27 | For details, view the full setup instructions in the [README](README.md#set-up-the-bridge). 28 | 29 | ## Testing 30 | 31 | CI will lint and test your code automatically, 32 | but you can save yourself some time by checking locally before submitting code. 33 | Refer to the main matrix.org bridge contributing guide for instructions on how to 34 | [lint](https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md#%EF%B8%8F-code-style) and 35 | [test](https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md#-tests--ci). 36 | 37 | Please bear in mind that you will need to cover the whole, or a reasonable 38 | degree of your code. You can check to see if you have with `yarn coverage`. 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim AS BUILD 2 | COPY . /tmp/src 3 | # install some dependencies needed for the build process 4 | RUN apt update && apt install -y build-essential make gcc g++ python3 ca-certificates libc-dev wget git 5 | 6 | RUN cd /tmp/src \ 7 | && yarn 8 | 9 | FROM node:18-slim 10 | ENV NODE_ENV=production 11 | COPY --from=BUILD /tmp/src/build /build 12 | COPY --from=BUILD /tmp/src/config /config 13 | COPY --from=BUILD /tmp/src/node_modules /node_modules 14 | COPY ./secstart.sh /secstart.sh 15 | RUN sh -c 'cd /build/tools; for TOOL in *.js; do LINK="/usr/bin/$(basename $TOOL .js)"; echo -e "#!/bin/sh\ncd /data;\nnode /build/tools/$TOOL \$@" > $LINK; chmod +x $LINK; done' 16 | RUN apt update && apt install patch dos2unix 17 | RUN dos2unix ./secstart.sh 18 | CMD /secstart.sh 19 | EXPOSE 9005 20 | VOLUME ["/data"] 21 | -------------------------------------------------------------------------------- /changelog.d/git.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t2bot/matrix-appservice-discord/82fcf2580b98971e7be29fa21cb268148977ae93/changelog.d/git.keep -------------------------------------------------------------------------------- /config/config.sample.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample of the config file showing all available options. 2 | # Where possible we have documented what they do, and all values are the 3 | # default values. 4 | 5 | bridge: 6 | # Domain part of the bridge, e.g. matrix.org 7 | domain: "localhost" 8 | # This should be your publicly-facing URL because Discord may use it to 9 | # fetch media from the media store. 10 | homeserverUrl: "http://localhost:8008" 11 | # The TCP port on which the appservice runs on. 12 | port: 9005 13 | # Interval at which to process users in the 'presence queue'. If you have 14 | # 5 users, one user will be processed every 500 milliseconds according to the 15 | # value below. This has a minimum value of 250. 16 | # WARNING: This has a high chance of spamming the homeserver with presence 17 | # updates since it will send one each time somebody changes state or is online. 18 | presenceInterval: 500 19 | # Disable setting presence for 'ghost users' which means Discord users on Matrix 20 | # will not be shown as away or online. 21 | disablePresence: false 22 | # Disable sending typing notifications when somebody on Discord types. 23 | disableTypingNotifications: false 24 | # Disable deleting messages on Discord if a message is redacted on Matrix. 25 | disableDeletionForwarding: false 26 | # Disable portal bridging, where Matrix users can search for unbridged Discord 27 | # rooms on their Matrix server. 28 | disablePortalBridging: false 29 | # Enable users to bridge rooms using !discord commands. See 30 | # https://t2bot.io/discord for instructions. 31 | enableSelfServiceBridging: false 32 | # Disable sending of read receipts for Matrix events which have been 33 | # successfully bridged to Discord. 34 | disableReadReceipts: false 35 | # Disable Join Leave echos from matrix 36 | disableJoinLeaveNotifications: false 37 | # Disable Invite echos from matrix 38 | disableInviteNotifications: false 39 | # Disable Room Topic echos from matrix 40 | disableRoomTopicNotifications: false 41 | # Auto-determine the language of code blocks (this can be CPU-intensive) 42 | determineCodeLanguage: false 43 | # MXID of an admin user that will be PMd if the bridge experiences problems. Optional 44 | adminMxid: '@admin:localhost' 45 | # The message to send to the bridge admin if the Discord token is not valid 46 | invalidTokenMessage: 'Your Discord bot token seems to be invalid, and the bridge cannot function. Please update it in your bridge settings and restart the bridge' 47 | # Authentication configuration for the discord bot. 48 | auth: 49 | # This MUST be a string (wrapped in quotes) 50 | clientID: "12345" 51 | botToken: "foobar" 52 | # You must enable "Privileged Gateway Intents" in your bot settings on discord.com (e.g. https://discord.com/developers/applications/12345/bot) 53 | # for this to work 54 | usePrivilegedIntents: false 55 | shards: 1 56 | logging: 57 | # What level should the logger output to the console at. 58 | console: "warn" #silly, verbose, info, http, warn, error, silent 59 | lineDateFormat: "MMM-D HH:mm:ss.SSS" # This is in moment.js format 60 | files: 61 | - file: "debug.log" 62 | disabled: 63 | - "PresenceHandler" # Will not capture presence logging 64 | - file: "warn.log" # Will capture warnings 65 | level: "warn" 66 | - file: "botlogs.log" # Will capture logs from DiscordBot 67 | level: "info" 68 | enabled: 69 | - "DiscordBot" 70 | database: 71 | # You may either use SQLite or Postgresql for the bridge database, which contains 72 | # important mappings for events and user puppeting configurations. 73 | # Use the filename option for SQLite, or connString for Postgresql. 74 | # If you are migrating, see https://github.com/Half-Shot/matrix-appservice-discord/blob/master/docs/howto.md#migrate-to-postgres-from-sqlite 75 | # WARNING: You will almost certainly be fine with sqlite unless your bridge 76 | # is in heavy demand and you suffer from IO slowness. 77 | filename: "discord.db" 78 | # connString: "postgresql://user:password@localhost/database_name" 79 | room: 80 | # Set the default visibility of alias rooms, defaults to "public". 81 | # One of: "public", "private" 82 | defaultVisibility: "public" 83 | channel: 84 | # Pattern of the name given to bridged rooms. 85 | # Can use :guild for the guild name and :name for the channel name. 86 | namePattern: "[Discord] :guild :name" 87 | # Changes made to rooms when a channel is deleted. 88 | deleteOptions: 89 | # Prefix the room name with a string. 90 | #namePrefix: "[Deleted]" 91 | # Prefix the room topic with a string. 92 | #topicPrefix: "This room has been deleted" 93 | # Disable people from talking in the room by raising the event PL to 50 94 | disableMessaging: false 95 | # Remove the discord alias from the room. 96 | unsetRoomAlias: true 97 | # Remove the room from the directory. 98 | unlistFromDirectory: true 99 | # Set the room to be unavailable for joining without an invite. 100 | setInviteOnly: true 101 | # Make all the discord users leave the room. 102 | ghostsLeave: true 103 | limits: 104 | # Delay in milliseconds between discord users joining a room. 105 | roomGhostJoinDelay: 6000 106 | # Lock timeout in milliseconds before sending messages to discord to avoid 107 | # echos. Default is rather high as the lock will most likely time out 108 | # before anyways. 109 | # echos = (Copies of a sent message may arrive from discord before we've 110 | # fininished handling it, causing us to echo it back to the room) 111 | discordSendDelay: 1500 112 | # Set a maximum of rooms to be bridged. 113 | # roomCount: 20 114 | ghosts: 115 | # Pattern for the ghosts nick, available is :nick, :username, :tag and :id 116 | nickPattern: ":nick" 117 | # Pattern for the ghosts username, available is :username, :tag and :id 118 | usernamePattern: ":username#:tag" 119 | # Prometheus-compatible metrics endpoint 120 | metrics: 121 | enable: false 122 | port: 9001 123 | host: "127.0.0.1" 124 | -------------------------------------------------------------------------------- /config/config.schema.yaml: -------------------------------------------------------------------------------- 1 | "$schema": "http://json-schema.org/draft-04/schema#" 2 | type: "object" 3 | required: ["bridge", "auth"] 4 | properties: 5 | bridge: 6 | type: "object" 7 | required: ["domain", "homeserverUrl"] 8 | properties: 9 | domain: 10 | type: "string" 11 | homeserverUrl: 12 | type: "string" 13 | port: 14 | type: "number" 15 | presenceInterval: 16 | type: "number" 17 | disablePresence: 18 | type: "boolean" 19 | disableTypingNotifications: 20 | type: "boolean" 21 | disableDeletionForwarding: 22 | type: "boolean" 23 | disablePortalBridging: 24 | type: "boolean" 25 | enableSelfServiceBridging: 26 | type: "boolean" 27 | disableReadReceipts: 28 | type: "boolean" 29 | disableJoinLeaveNotifications: 30 | type: "boolean" 31 | disableInviteNotifications: 32 | type: "boolean" 33 | disableRoomTopicNotifications: 34 | type: "boolean" 35 | userActivity: 36 | type: "object" 37 | required: ["minUserActiveDays", "inactiveAfterDays"] 38 | properties: 39 | minUserActiveDays: 40 | type: "number" 41 | inactiveAfterDays: 42 | type: "number" 43 | userLimit: 44 | type: "number" 45 | auth: 46 | type: "object" 47 | required: ["botToken", "clientID"] 48 | properties: 49 | clientID: 50 | type: "string" 51 | botToken: 52 | type: "string" 53 | usePrivilegedIntents: 54 | type: "boolean" 55 | shards: 56 | type: "number" 57 | logging: 58 | type: "object" 59 | properties: 60 | console: 61 | type: "string" 62 | enum: ["error", "warn", "info", "verbose", "silly", "silent"] 63 | lineDateFormat: 64 | type: "string" 65 | files: 66 | type: "array" 67 | items: 68 | type: "object" 69 | required: ["file"] 70 | properties: 71 | file: 72 | type: "string" 73 | level: 74 | type: "string" 75 | enum: ["error", "warn", "info", "verbose", "silly"] 76 | maxFiles: 77 | type: "string" 78 | maxSize: 79 | type: ["number", "string"] 80 | datePattern: 81 | type: "string" 82 | enabled: 83 | type: "array" 84 | items: 85 | type: "string" 86 | disabled: 87 | type: "array" 88 | items: 89 | type: "string" 90 | database: 91 | type: "object" 92 | properties: 93 | connString: 94 | type: "string" 95 | filename: 96 | type: "string" 97 | userStorePath: 98 | type: "string" 99 | roomStorePath: 100 | type: "string" 101 | room: 102 | type: "object" 103 | properties: 104 | defaultVisibility: 105 | type: "string" 106 | enum: ["public", "private"] 107 | limits: 108 | type: "object" 109 | properties: 110 | roomGhostJoinDelay: 111 | type: "number" 112 | discordSendDelay: 113 | type: "number" 114 | roomCount: 115 | type: "number" 116 | channel: 117 | type: "object" 118 | properties: 119 | namePattern: 120 | type: "string" 121 | deleteOptions: 122 | type: "object" 123 | properties: 124 | namePrefix: 125 | type: "string" 126 | topicPrefix: 127 | type: "string" 128 | disableMessaging: 129 | type: "boolean" 130 | unsetRoomAlias: 131 | type: "boolean" 132 | unlistFromDirectory: 133 | type: "boolean" 134 | setInviteOnly: 135 | type: "boolean" 136 | ghostsLeave: 137 | type: "boolean" 138 | ghosts: 139 | type: "object" 140 | properties: 141 | nickPattern: 142 | type: "string" 143 | usernamePattern: 144 | type: "string" 145 | metrics: 146 | type: "object" 147 | properties: 148 | enable: 149 | type: "boolean" 150 | port: 151 | type: "number" 152 | host: 153 | type: "string" 154 | -------------------------------------------------------------------------------- /docs/bridge-migrations.md: -------------------------------------------------------------------------------- 1 | # 1.0 Migration (from 0.5.1 or lower) 2 | 3 | If you have been linked here, there is an issue with your config on your bridge. 4 | 5 | Please follow the following steps: 6 | 7 | 1. If you have just created a new install OR were previously running 0.5.X, 8 | please remove `roomDataStore` and `userDataStore` from your config file. 9 | 2. If this is a existing install but you have not run 0.5.X (0.4.X or lower), 10 | please downgrade to 0.5.X to migrate your database across and then run 11 | this version of the bridge again. 12 | -------------------------------------------------------------------------------- /docs/howto.md: -------------------------------------------------------------------------------- 1 | 2 | ### Join a room 3 | 4 | The default format for room aliases (which are automatically resolved, whether the room exists on Matrix or not) is: 5 | 6 | ``#_discord_guildid_channelid`` 7 | 8 | You can find these on discord in the browser where: 9 | 10 | ``https://discord.com/channels/282616294245662720/282616372591329281`` 11 | 12 | is formatted as https://discord.com/channels/``guildid``/``channelid`` 13 | 14 | ### Set privileges on bridge managed rooms 15 | 16 | * The ``adminme`` script is provided to set Admin/Moderator or any other custom power level to a specific user. 17 | * e.g. To set Alice to Admin on her ``example.com`` HS on default config. (``config.yaml``) 18 | * ``yarn adminme -r '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'`` 19 | * Run ``yarn adminme -h`` for usage. 20 | 21 | Please note that `!AbcdefghijklmnopqR:example.com` is the internal room id and will always begin with `!`. 22 | You can find this internal id in the room settings in Element. 23 | 24 | ### Migrate to postgres from sqlite 25 | * Stop the bridge. 26 | * Create a new database on postgres and create a user for it with a password. 27 | * We will call the database `discord_bridge` and the the user `discord`. 28 | * Install `pgloader` if you do not have it. 29 | * Run `pgloader ./discord.db postgresql://discord:password@localhost/discord_bridge` 30 | * Change the config so that the config contains: 31 | 32 | ```yaml 33 | database: 34 | connString: "postgresql://discord:password@localhost/discord_bridge" 35 | ``` 36 | * All done! 37 | -------------------------------------------------------------------------------- /docs/puppeting.md: -------------------------------------------------------------------------------- 1 | # Puppeting 2 | 3 | This docs describes the method to puppet yourself with the bridge, so you can 4 | interact with the bridge as if you were using the real Discord client. This 5 | has the benefits of (not all of these may be implemented): 6 | * Talking as yourself, rather than as the bot. 7 | * DM channels 8 | * Able to use your Discord permissions, as well as joining rooms limited to 9 | your roles as on Discord. 10 | 11 | ## Caveats & Disclaimer 12 | 13 | Discord is currently __not__ offering any way to authenticate on behalf 14 | of a user _and_ interact on their behalf. The OAuth system does not allow 15 | remote access beyond reading information about the users. While [developers have 16 | expressed a wish for this](https://feedback.discordapp.com/forums/326712-discord-dream-land/suggestions/16753837-support-custom-clients), 17 | it is my opinion that Discord are unlikely to support this any time soon. With 18 | all this said, Discord will not be banning users or the bridge itself for acting 19 | on the behalf of the user. 20 | 21 | Therefore while I loathe to do it, we have to store login tokens for *full 22 | permissions* on the user's account (excluding things such as changing passwords 23 | and e-mail which require re-authenication, thankfully). 24 | 25 | The tokens will be stored by the bridge and are valid until the user 26 | changes their password, so please be careful not to give the token to anything 27 | that you wouldn't trust with your password. 28 | 29 | I accept no responsibility if Discord ban your IP, Account or even your details on 30 | their system. They have never given official support on custom clients (and 31 | by extension, puppeting bridges). If you are in any doubt, stick to the 32 | bot which is within the rules. 33 | 34 | ## How to Puppet an Account 35 | ~~*2FA does not work with bridging, please do not try it.*~~ 36 | You should be able to puppet with 2FA enabled on your account 37 | 38 | *You must also be a bridge admin to add or remove puppets at the moment* 39 | 40 | * Follow https://discordhelp.net/discord-token to find your discord token. 41 | * Stop the bridge, if it is running. 42 | * Run `yarn usertool --add` and follow the instructions. 43 | * If all is well, you can start the bridge. 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrix-appservice-discord", 3 | "version": "4.0.0", 4 | "description": "A bridge between Matrix and Discord", 5 | "main": "discordas.js", 6 | "engines": { 7 | "npm": "please-use-yarn", 8 | "node": ">=18 <=20" 9 | }, 10 | "scripts": { 11 | "test": "mocha -r ts-node/register test/config.ts test/test_*.ts test/**/test_*.ts", 12 | "lint": "eslint -c .eslintrc --max-warnings 200 src/**/*.ts test/**/*.ts", 13 | "coverage": "tsc && nyc mocha build/test/config.js build/test", 14 | "build": "tsc", 15 | "postinstall": "yarn build", 16 | "start": "node ./build/src/discordas.js", 17 | "debug": "node --inspect ./build/src/discordas.js", 18 | "addbot": "node ./build/tools/addbot.js", 19 | "adminme": "node ./build/tools/adminme.js", 20 | "usertool": "node ./build/tools/userClientTools.js", 21 | "directoryfix": "node ./build/tools/addRoomsToDirectory.js", 22 | "ghostfix": "node ./build/tools/ghostfix.js", 23 | "chanfix": "node ./build/tools/chanfix.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/Half-Shot/matrix-appservice-discord.git" 28 | }, 29 | "keywords": [ 30 | "matrix", 31 | "discord", 32 | "bridge", 33 | "application-service", 34 | "as" 35 | ], 36 | "author": "Half-Shot", 37 | "license": "Apache-2.0", 38 | "bugs": { 39 | "url": "https://github.com/Half-Shot/matrix-appservice-discord/issues" 40 | }, 41 | "homepage": "https://github.com/Half-Shot/matrix-appservice-discord#readme", 42 | "dependencies": { 43 | "@mx-puppet/better-discord.js": "git+https://gitlab.com/MRAAGH/better-discord.js.git#a2d4ab4a5a4d40b8c053f64c6104ecd2f93a8a5a", 44 | "@mx-puppet/matrix-discord-parser": "^0.1.10", 45 | "better-sqlite3": "^8.6.0", 46 | "command-line-args": "^5.1.1", 47 | "command-line-usage": "^6.1.0", 48 | "escape-html": "^1.0.3", 49 | "escape-string-regexp": "^4.0.0", 50 | "js-yaml": "^3.14.0", 51 | "lru-cache": "^10.0.1", 52 | "marked": "^1.2.2", 53 | "matrix-appservice-bridge": "^9.0.1", 54 | "mime": "^2.4.6", 55 | "p-queue": "^6.4.0", 56 | "pg-promise": "^10.5.6", 57 | "prom-client": "^12.0.0", 58 | "uuid": "^8.3.1", 59 | "winston": "^3.2.1", 60 | "winston-daily-rotate-file": "^4.5.0" 61 | }, 62 | "devDependencies": { 63 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 64 | "@types/better-sqlite3": "^5.4.1", 65 | "@types/chai": "^4.2.11", 66 | "@types/command-line-args": "^5.0.0", 67 | "@types/express": "^4.17.9", 68 | "@types/js-yaml": "^3.12.4", 69 | "@types/marked": "^1.1.0", 70 | "@types/mime": "^2.0.2", 71 | "@types/mocha": "^7.0.2", 72 | "@types/node": "^14", 73 | "@typescript-eslint/eslint-plugin": "^5.4.0", 74 | "@typescript-eslint/parser": "^5.4.0", 75 | "chai": "^4.2.0", 76 | "eslint": "^7.4.0", 77 | "mocha": "^8.0.1", 78 | "nyc": "^15.1.0", 79 | "proxyquire": "^1.7.11", 80 | "source-map-support": "^0.5.19", 81 | "ts-node": "^8.10.2", 82 | "typescript": "^4.2.3", 83 | "why-is-node-running": "^2.2.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | # The name of your Python package 3 | filename = "CHANGELOG.md" 4 | directory = "changelog.d" 5 | issue_format = "[\\#{issue}](https://github.com/matrix-org/matrix-appservice-discord/issues/{issue})" 6 | 7 | [[tool.towncrier.type]] 8 | directory = "feature" 9 | name = "Features" 10 | showcontent = true 11 | 12 | [[tool.towncrier.type]] 13 | directory = "bugfix" 14 | name = "Bugfixes" 15 | showcontent = true 16 | 17 | [[tool.towncrier.type]] 18 | directory = "doc" 19 | name = "Improved Documentation" 20 | showcontent = true 21 | 22 | [[tool.towncrier.type]] 23 | directory = "removal" 24 | name = "Deprecations and Removals" 25 | showcontent = true 26 | 27 | [[tool.towncrier.type]] 28 | directory = "misc" 29 | name = "Internal Changes" 30 | showcontent = true 31 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t2bot/matrix-appservice-discord/82fcf2580b98971e7be29fa21cb268148977ae93/screenshot.png -------------------------------------------------------------------------------- /scripts/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=`python3 -c "import json; f = open('./package.json', 'r'); v = json.loads(f.read())['version']; f.close(); print(v)"` 3 | towncrier build --version $VERSION $1 4 | -------------------------------------------------------------------------------- /scripts/check-newsfragment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A script which checks that an appropriate news file has been added on this 4 | # branch. 5 | 6 | 7 | echo -e "+++ \033[32mChecking newsfragment\033[m" 8 | 9 | set -e 10 | 11 | # make sure that origin/develop is up to date 12 | git remote set-branches --add origin develop 13 | git fetch -q origin develop 14 | 15 | pr="$PULL_REQUEST_NUMBER" 16 | 17 | # Print a link to the contributing guide if the user makes a mistake 18 | CONTRIBUTING_GUIDE_TEXT="!! Please see the contributing guide for help writing your changelog entry: 19 | https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md#%EF%B8%8F-pull-requests" 20 | 21 | # If check-newsfragment returns a non-zero exit code, print the contributing guide and exit 22 | python3 -m towncrier.check --compare-with=origin/develop || (echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 && exit 1) 23 | 24 | echo 25 | echo "--------------------------" 26 | echo 27 | 28 | matched=0 29 | for f in $(git diff --diff-filter=d --name-only FETCH_HEAD... -- changelog.d | grep -xv changelog.d/git.keep); do 30 | # check that any added newsfiles on this branch end with a full stop. 31 | lastchar=$(tr -d '\n' < "$f" | tail -c 1) 32 | if [ "$lastchar" != '.' ] && [ "$lastchar" != '!' ]; then 33 | echo -e "\e[31mERROR: newsfragment $f does not end with a '.' or '!'\e[39m" >&2 34 | echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 35 | exit 1 36 | fi 37 | 38 | # see if this newsfile corresponds to the right PR 39 | [[ -n "$pr" && "$f" == changelog.d/"$pr".* ]] && matched=1 40 | done 41 | 42 | if [[ -n "$pr" && "$matched" -eq 0 ]]; then 43 | echo -e "\e[31mERROR: Did not find a news fragment with the right number: expected changelog.d/$pr.*.\e[39m" >&2 44 | echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 45 | exit 1 46 | fi 47 | -------------------------------------------------------------------------------- /secstart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sh /data/apply-patches.sh 3 | node --max-old-space-size=4096 /build/src/discordas.js -p 9005 -c /data/config.yaml -f /data/discord-registration.yaml 4 | -------------------------------------------------------------------------------- /src/clientfactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 - 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { Client as DiscordClient, Intents, TextChannel } from "@mx-puppet/better-discord.js"; 18 | import { DiscordBridgeConfigAuth } from "./config"; 19 | import { DiscordStore } from "./store"; 20 | import { Log } from "./log"; 21 | import { MetricPeg } from "./metrics"; 22 | 23 | const log = new Log("ClientFactory"); 24 | 25 | export class DiscordClientFactory { 26 | private config: DiscordBridgeConfigAuth; 27 | private store: DiscordStore; 28 | private botClient: DiscordClient; 29 | private clients: Map; 30 | constructor(store: DiscordStore, config?: DiscordBridgeConfigAuth) { 31 | this.config = config!; 32 | this.clients = new Map(); 33 | this.store = store; 34 | } 35 | 36 | public async init(): Promise { 37 | if (this.config === undefined) { 38 | return Promise.reject("Client config not supplied."); 39 | } 40 | // We just need to make sure we have a bearer token. 41 | // Create a new Bot client. 42 | this.botClient = new DiscordClient({ 43 | fetchAllMembers: this.config.usePrivilegedIntents, 44 | messageCacheLifetime: 5, 45 | ws: { 46 | intents: this.config.usePrivilegedIntents ? Intents.ALL : Intents.NON_PRIVILEGED, 47 | }, 48 | shardCount: this.config.shards, 49 | }); 50 | 51 | const waitPromise = new Promise((resolve, reject) => { 52 | this.botClient.once("shardReady", resolve); 53 | this.botClient.once("shardError", reject); 54 | }); 55 | 56 | try { 57 | await this.botClient.login(this.config.botToken, true); 58 | log.info("Waiting for shardReady signal"); 59 | await waitPromise; 60 | log.info("Got shardReady signal"); 61 | } catch (err) { 62 | log.error("Could not login as the bot user. This is bad!", err); 63 | throw err; 64 | } 65 | 66 | } 67 | 68 | public async getDiscordId(token: string): Promise { 69 | const client = new DiscordClient({ 70 | fetchAllMembers: false, 71 | messageCacheLifetime: 5, 72 | ws: { 73 | intents: Intents.NON_PRIVILEGED, 74 | }, 75 | }); 76 | 77 | await client.login(token, false); 78 | const id = client.user?.id; 79 | client.destroy(); 80 | if (!id) { 81 | throw Error("Client did not have a user object, cannot determine ID"); 82 | } 83 | return id; 84 | } 85 | 86 | public async getClient(userId: string | null = null): Promise { 87 | if (userId === null) { 88 | return this.botClient; 89 | } 90 | 91 | if (this.clients.has(userId)) { 92 | log.verbose("Returning cached user client for", userId); 93 | return this.clients.get(userId) as DiscordClient; 94 | } 95 | 96 | const discordIds = await this.store.getUserDiscordIds(userId); 97 | if (discordIds.length === 0) { 98 | return this.botClient; 99 | } 100 | // TODO: Select a profile based on preference, not the first one. 101 | const token = await this.store.getToken(discordIds[0]); 102 | const client = new DiscordClient({ 103 | fetchAllMembers: false, 104 | messageCacheLifetime: 5, 105 | ws: { 106 | intents: Intents.NON_PRIVILEGED, 107 | }, 108 | }); 109 | 110 | const jsLog = new Log("discord.js-ppt"); 111 | client.on("debug", (msg) => { jsLog.verbose(msg); }); 112 | client.on("error", (msg) => { jsLog.error(msg); }); 113 | client.on("warn", (msg) => { jsLog.warn(msg); }); 114 | 115 | try { 116 | await client.login(token, false); 117 | log.verbose("Logged in. Storing ", userId); 118 | this.clients.set(userId, client); 119 | return client; 120 | } catch (err) { 121 | log.warn(`Could not log ${userId} in. Returning bot user for now.`, err); 122 | return this.botClient; 123 | } 124 | } 125 | 126 | public bindMetricsToChannel(channel: TextChannel) { 127 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 128 | const flexChan = channel as any; 129 | if (flexChan._xmet_send !== undefined) { 130 | return; 131 | } 132 | // Prefix the real functions with _xmet_ 133 | // eslint-disable-next-line @typescript-eslint/naming-convention 134 | flexChan._xmet_send = channel.send; 135 | channel.send = (...rest) => { 136 | MetricPeg.get.remoteCall("channel.send"); 137 | return flexChan._xmet_send.apply(channel, rest); 138 | }; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 - 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const ENV_PREFIX = "APPSERVICE_DISCORD"; 18 | const ENV_KEY_SEPARATOR = "_"; 19 | const ENV_VAL_SEPARATOR = ","; 20 | 21 | import { UserActivityTrackerConfig } from 'matrix-appservice-bridge'; 22 | 23 | /** Type annotations for config/config.schema.yaml */ 24 | export class DiscordBridgeConfig { 25 | public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge(); 26 | public auth: DiscordBridgeConfigAuth = new DiscordBridgeConfigAuth(); 27 | public logging: DiscordBridgeConfigLogging = new DiscordBridgeConfigLogging(); 28 | public database: DiscordBridgeConfigDatabase = new DiscordBridgeConfigDatabase(); 29 | public room: DiscordBridgeConfigRoom = new DiscordBridgeConfigRoom(); 30 | public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel(); 31 | public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits(); 32 | public ghosts: DiscordBridgeConfigGhosts = new DiscordBridgeConfigGhosts(); 33 | public metrics: DiscordBridgeConfigMetrics = new DiscordBridgeConfigMetrics(); 34 | 35 | /** 36 | * Apply a set of keys and values over the default config. 37 | * @param newConfig Config keys 38 | * @param configLayer Private parameter 39 | */ 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | public applyConfig(newConfig: {[key: string]: any}, configLayer: {[key: string]: any} = this) { 42 | Object.keys(newConfig).forEach((key) => { 43 | if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) { 44 | this.applyConfig(newConfig[key], configLayer[key]); 45 | } else { 46 | configLayer[key] = newConfig[key]; 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * Override configuration keys defined in the supplied environment dictionary. 53 | * @param environment environment variable dictionary 54 | * @param path private parameter: config layer path determining the environment key prefix 55 | * @param configLayer private parameter: current layer of configuration to alter recursively 56 | */ 57 | public applyEnvironmentOverrides( 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 | environment: {[key: string]: any}, 60 | path: string[] = [ENV_PREFIX], 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | configLayer: {[key: string]: any} = this, 63 | ) { 64 | Object.keys(configLayer).forEach((key) => { 65 | // camelCase to THICK_SNAKE 66 | const attributeKey = key.replace(/[A-Z]/g, (prefix) => `${ENV_KEY_SEPARATOR}${prefix}`).toUpperCase(); 67 | const attributePath = path.concat([attributeKey]); 68 | 69 | if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) { 70 | this.applyEnvironmentOverrides(environment, attributePath, configLayer[key]); 71 | } else { 72 | const lookupKey = attributePath.join(ENV_KEY_SEPARATOR); 73 | if (lookupKey in environment) { 74 | configLayer[key] = (configLayer[key] instanceof Array) 75 | ? environment[lookupKey].split(ENV_VAL_SEPARATOR) 76 | : environment[lookupKey]; 77 | } 78 | } 79 | }); 80 | } 81 | } 82 | 83 | export class DiscordBridgeConfigBridge { 84 | public domain: string; 85 | public homeserverUrl: string; 86 | public port: number; 87 | public bindAddress: string; 88 | public presenceInterval: number = 500; 89 | public disablePresence: boolean; 90 | public disableTypingNotifications: boolean; 91 | public disableDiscordMentions: boolean; 92 | public disableDeletionForwarding: boolean; 93 | public enableSelfServiceBridging: boolean; 94 | public disablePortalBridging: boolean; 95 | public disableReadReceipts: boolean; 96 | public disableEveryoneMention: boolean = false; 97 | public disableHereMention: boolean = false; 98 | public disableJoinLeaveNotifications: boolean = false; 99 | public disableInviteNotifications: boolean = false; 100 | public disableRoomTopicNotifications: boolean = false; 101 | public determineCodeLanguage: boolean = false; 102 | public activityTracker: UserActivityTrackerConfig = UserActivityTrackerConfig.DEFAULT; 103 | public userLimit: number|null = null; 104 | public adminMxid: string|null = null; 105 | public invalidTokenMessage: string = 'Your Discord token is invalid'; 106 | } 107 | 108 | export class DiscordBridgeConfigDatabase { 109 | public connString: string; 110 | public filename: string; 111 | // These parameters are legacy, and will stop the bridge if defined. 112 | public userStorePath: string; 113 | public roomStorePath: string; 114 | } 115 | 116 | export class DiscordBridgeConfigAuth { 117 | public clientID: string; 118 | public botToken: string; 119 | public usePrivilegedIntents: boolean; 120 | public shards: number; 121 | } 122 | 123 | export class DiscordBridgeConfigLogging { 124 | public console: string = "info"; 125 | public lineDateFormat: string = "MMM-D HH:mm:ss.SSS"; 126 | public files: LoggingFile[] = []; 127 | } 128 | 129 | class DiscordBridgeConfigRoom { 130 | public defaultVisibility: string; 131 | public kickFor: number = 30000; 132 | } 133 | 134 | class DiscordBridgeConfigChannel { 135 | public namePattern: string = "[Discord] :guild :name"; 136 | public deleteOptions = new DiscordBridgeConfigChannelDeleteOptions(); 137 | } 138 | 139 | export class DiscordBridgeConfigChannelDeleteOptions { 140 | public namePrefix: string | null = null; 141 | public topicPrefix: string | null = null; 142 | public disableMessaging: boolean = false; 143 | public unsetRoomAlias: boolean = true; 144 | public unlistFromDirectory: boolean = true; 145 | public setInviteOnly: boolean = true; 146 | public ghostsLeave: boolean = true; 147 | } 148 | 149 | class DiscordBridgeConfigLimits { 150 | public roomGhostJoinDelay: number = 6000; 151 | public discordSendDelay: number = 1500; 152 | public roomCount: number = -1; 153 | } 154 | 155 | export class LoggingFile { 156 | public file: string; 157 | public level: string = "info"; 158 | public maxFiles: string = "14d"; 159 | public maxSize: string|number = "50m"; 160 | public datePattern: string = "YYYY-MM-DD"; 161 | public enabled: string[] = []; 162 | public disabled: string[] = []; 163 | } 164 | 165 | class DiscordBridgeConfigGhosts { 166 | public nickPattern: string = ":nick"; 167 | public usernamePattern: string = ":username#:tag"; 168 | } 169 | 170 | export class DiscordBridgeConfigMetrics { 171 | public enable: boolean = false; 172 | public port: number = 9001; 173 | public host: string = "127.0.0.1"; 174 | } 175 | -------------------------------------------------------------------------------- /src/db/connector.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018, 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | type SQLTYPES = number | boolean | string | null; 18 | 19 | export interface ISqlCommandParameters { 20 | [paramKey: string]: SQLTYPES | Promise; 21 | } 22 | 23 | export interface ISqlRow { 24 | [key: string]: SQLTYPES; 25 | } 26 | 27 | export interface IDatabaseConnector { 28 | Open(): void; 29 | Get(sql: string, parameters?: ISqlCommandParameters): Promise; 30 | All(sql: string, parameters?: ISqlCommandParameters): Promise; 31 | Run(sql: string, parameters?: ISqlCommandParameters): Promise; 32 | Close(): Promise; 33 | Exec(sql: string): Promise; 34 | } 35 | -------------------------------------------------------------------------------- /src/db/dbdataemoji.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 - 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { DiscordStore } from "../store"; 18 | import { IDbData } from "./dbdatainterface"; 19 | import { ISqlCommandParameters } from "./connector"; 20 | 21 | export class DbEmoji implements IDbData { 22 | public EmojiId: string; 23 | public Name: string; 24 | public Animated: boolean; 25 | public MxcUrl: string; 26 | public CreatedAt: number; 27 | public UpdatedAt: number; 28 | public Result: boolean; 29 | 30 | public async RunQuery(store: DiscordStore, params: ISqlCommandParameters): Promise { 31 | let query = ` 32 | SELECT * 33 | FROM emoji 34 | WHERE emoji_id = $id`; 35 | if (params.mxc_url) { 36 | query = ` 37 | SELECT * 38 | FROM emoji 39 | WHERE mxc_url = $mxc`; 40 | } 41 | const row = await store.db.Get(query, { 42 | id: params.emoji_id, 43 | mxc: params.mxc_url, 44 | }); 45 | this.Result = Boolean(row); // check if row exists 46 | if (this.Result && row) { 47 | this.EmojiId = row.emoji_id as string; 48 | this.Name = row.name as string; 49 | this.Animated = Boolean(row.animated); 50 | this.MxcUrl = row.mxc_url as string; 51 | this.CreatedAt = row.created_at as number; 52 | this.UpdatedAt = row.updated_at as number; 53 | } 54 | } 55 | 56 | public async Insert(store: DiscordStore): Promise { 57 | this.CreatedAt = new Date().getTime(); 58 | this.UpdatedAt = this.CreatedAt; 59 | await store.db.Run(` 60 | INSERT INTO emoji 61 | (emoji_id,name,animated,mxc_url,created_at,updated_at) 62 | VALUES ($emoji_id,$name,$animated,$mxc_url,$created_at,$updated_at);`, { 63 | /* eslint-disable @typescript-eslint/naming-convention */ 64 | animated: Number(this.Animated), 65 | created_at: this.CreatedAt, 66 | emoji_id: this.EmojiId, 67 | mxc_url: this.MxcUrl, 68 | name: this.Name, 69 | updated_at: this.UpdatedAt, 70 | /* eslint-enable @typescript-eslint/naming-convention */ 71 | }); 72 | } 73 | 74 | public async Update(store: DiscordStore): Promise { 75 | // Ensure this has incremented by 1 for Insert+Update operations. 76 | this.UpdatedAt = new Date().getTime() + 1; 77 | await store.db.Run(` 78 | UPDATE emoji 79 | SET name = $name, 80 | animated = $animated, 81 | mxc_url = $mxc_url, 82 | updated_at = $updated_at 83 | WHERE 84 | emoji_id = $emoji_id`, { 85 | /* eslint-disable @typescript-eslint/naming-convention */ 86 | animated: Number(this.Animated), 87 | emoji_id: this.EmojiId, 88 | mxc_url: this.MxcUrl, 89 | name: this.Name, 90 | updated_at: this.UpdatedAt, 91 | /* eslint-enable @typescript-eslint/naming-convention */ 92 | }); 93 | } 94 | 95 | public async Delete(store: DiscordStore): Promise { 96 | throw new Error("Delete is not implemented"); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/db/dbdataevent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { DiscordStore } from "../store"; 18 | import { IDbDataMany } from "./dbdatainterface"; 19 | import { ISqlCommandParameters } from "./connector"; 20 | 21 | export class DbEvent implements IDbDataMany { 22 | public MatrixId: string; 23 | public DiscordId: string; 24 | public GuildId: string; 25 | public ChannelId: string; 26 | public Result: boolean; 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | private rows: any[]; 29 | 30 | get ResultCount(): number { 31 | return this.rows.length; 32 | } 33 | 34 | public async RunQuery(store: DiscordStore, params: ISqlCommandParameters): Promise { 35 | this.rows = []; 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | let rowsM: any[] | null = null; 38 | if (params.matrix_id) { 39 | rowsM = await store.db.All(` 40 | SELECT * 41 | FROM event_store 42 | WHERE matrix_id = $id`, { 43 | id: params.matrix_id, 44 | }); 45 | } else if (params.discord_id) { 46 | rowsM = await store.db.All(` 47 | SELECT * 48 | FROM event_store 49 | WHERE discord_id = $id`, { 50 | id: params.discord_id, 51 | }); 52 | } else { 53 | throw new Error("Unknown/incorrect id given as a param"); 54 | } 55 | 56 | for (const rowM of rowsM) { 57 | const row = { 58 | /* eslint-disable @typescript-eslint/naming-convention */ 59 | discord_id: rowM.discord_id, 60 | matrix_id: rowM.matrix_id, 61 | /* eslint-enable @typescript-eslint/naming-convention */ 62 | }; 63 | for (const rowD of await store.db.All(` 64 | SELECT * 65 | FROM discord_msg_store 66 | WHERE msg_id = $id`, { 67 | id: rowM.discord_id, 68 | })) { 69 | this.rows.push({ 70 | /* eslint-disable @typescript-eslint/naming-convention */ 71 | ...row, 72 | guild_id: rowD.guild_id, 73 | channel_id: rowD.channel_id, 74 | /* eslint-enable @typescript-eslint/naming-convention */ 75 | }); 76 | } 77 | } 78 | this.Result = this.rows.length !== 0; 79 | } 80 | 81 | public Next(): boolean { 82 | if (!this.Result || this.ResultCount === 0) { 83 | return false; 84 | } 85 | const item = this.rows.shift(); 86 | this.MatrixId = item.matrix_id; 87 | this.DiscordId = item.discord_id; 88 | this.GuildId = item.guild_id; 89 | this.ChannelId = item.channel_id; 90 | return true; 91 | } 92 | 93 | public async Insert(store: DiscordStore): Promise { 94 | await store.db.Run(` 95 | INSERT INTO event_store 96 | (matrix_id,discord_id) 97 | VALUES ($matrix_id,$discord_id);`, { 98 | /* eslint-disable @typescript-eslint/naming-convention */ 99 | discord_id: this.DiscordId, 100 | matrix_id: this.MatrixId, 101 | /* eslint-enable @typescript-eslint/naming-convention */ 102 | }); 103 | // Check if the discord item exists? 104 | const msgExists = await store.db.Get(` 105 | SELECT * 106 | FROM discord_msg_store 107 | WHERE msg_id = $id`, { 108 | id: this.DiscordId, 109 | }) != null; 110 | if (msgExists) { 111 | return; 112 | } 113 | return store.db.Run(` 114 | INSERT INTO discord_msg_store 115 | (msg_id, guild_id, channel_id) 116 | VALUES ($msg_id, $guild_id, $channel_id);`, { 117 | /* eslint-disable @typescript-eslint/naming-convention */ 118 | channel_id: this.ChannelId, 119 | guild_id: this.GuildId, 120 | msg_id: this.DiscordId, 121 | /* eslint-enable @typescript-eslint/naming-convention */ 122 | }); 123 | } 124 | 125 | public async Update(store: DiscordStore): Promise { 126 | throw new Error("Update is not implemented"); 127 | } 128 | 129 | public async Delete(store: DiscordStore): Promise { 130 | await store.db.Run(` 131 | DELETE FROM event_store 132 | WHERE matrix_id = $matrix_id 133 | AND discord_id = $discord_id;`, { 134 | /* eslint-disable @typescript-eslint/naming-convention */ 135 | discord_id: this.DiscordId, 136 | matrix_id: this.MatrixId, 137 | /* eslint-enable @typescript-eslint/naming-convention */ 138 | }); 139 | return store.db.Run(` 140 | DELETE FROM discord_msg_store 141 | WHERE msg_id = $discord_id;`, { 142 | /* eslint-disable @typescript-eslint/naming-convention */ 143 | discord_id: this.DiscordId, 144 | /* eslint-enable @typescript-eslint/naming-convention */ 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/db/dbdatainterface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { DiscordStore } from "../store"; 18 | 19 | export interface IDbData { 20 | Result: boolean; 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | RunQuery(store: DiscordStore, params: any): Promise; 23 | Insert(store: DiscordStore): Promise; 24 | Update(store: DiscordStore): Promise; 25 | Delete(store: DiscordStore): Promise; 26 | } 27 | 28 | export interface IDbDataMany extends IDbData { 29 | ResultCount: number; 30 | Next(): boolean; 31 | } 32 | -------------------------------------------------------------------------------- /src/db/postgres.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018, 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as pgPromise from "pg-promise"; 18 | import { Log } from "../log"; 19 | import { IDatabaseConnector, ISqlCommandParameters, ISqlRow } from "./connector"; 20 | const log = new Log("Postgres"); 21 | 22 | const pgp: pgPromise.IMain = pgPromise({ 23 | // Initialization Options 24 | }); 25 | 26 | export class Postgres implements IDatabaseConnector { 27 | public static ParameterizeSql(sql: string): string { 28 | return sql.replace(/\$((\w|\d|_)+)+/g, (k: string) => { 29 | return `\${${k.substring("$".length)}}`; 30 | }); 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 | private db: pgPromise.IDatabase; 35 | constructor(private connectionString: string) { 36 | 37 | } 38 | 39 | public Open(): void { 40 | // Hide username:password 41 | const logConnString = this.connectionString.substring( 42 | this.connectionString.indexOf("@") || 0, 43 | ); 44 | log.info(`Opening ${logConnString}`); 45 | this.db = pgp(this.connectionString); 46 | } 47 | 48 | public async Get(sql: string, parameters?: ISqlCommandParameters): Promise { 49 | log.silly("Get:", sql); 50 | return this.db.oneOrNone(Postgres.ParameterizeSql(sql), parameters); 51 | } 52 | 53 | public async All(sql: string, parameters?: ISqlCommandParameters): Promise { 54 | log.silly("All:", sql); 55 | try { 56 | return await this.db.many(Postgres.ParameterizeSql(sql), parameters); 57 | } catch (ex) { 58 | if (ex.code === pgPromise.errors.queryResultErrorCode.noData ) { 59 | return []; 60 | } 61 | throw ex; 62 | } 63 | } 64 | 65 | public async Run(sql: string, parameters?: ISqlCommandParameters): Promise { 66 | log.silly("Run:", sql); 67 | await this.db.oneOrNone(Postgres.ParameterizeSql(sql), parameters); 68 | } 69 | 70 | public async Close(): Promise { 71 | // Postgres doesn't support disconnecting. 72 | } 73 | 74 | public async Exec(sql: string): Promise { 75 | log.silly("Exec:", sql); 76 | await this.db.none(sql); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/db/schema/dbschema.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { DiscordStore } from "../../store"; 18 | export interface IDbSchema { 19 | description: string; 20 | run(store: DiscordStore): Promise; 21 | rollBack(store: DiscordStore): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /src/db/schema/v1.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | export class Schema implements IDbSchema { 20 | public description = "Schema, Client Auth Table"; 21 | public async run(store: DiscordStore): Promise { 22 | await store.createTable(` 23 | CREATE TABLE schema ( 24 | version INTEGER UNIQUE NOT NULL 25 | );`, "schema"); 26 | await store.db.Exec("INSERT INTO schema VALUES (0);"); 27 | await store.createTable(` 28 | CREATE TABLE user_tokens ( 29 | userId TEXT UNIQUE NOT NULL, 30 | token TEXT UNIQUE NOT NULL 31 | );`, "user_tokens"); 32 | } 33 | public async rollBack(store: DiscordStore): Promise { 34 | await store.db.Exec( 35 | `DROP TABLE IF EXISTS schema; 36 | DROP TABLE IF EXISTS user_tokens`, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/db/schema/v10.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | import { Log } from "../../log"; 20 | 21 | const log = new Log("SchemaV10"); 22 | 23 | export class Schema implements IDbSchema { 24 | public description = "create indexes on tables"; 25 | private readonly INDEXES = { 26 | idx_discord_msg_store_msgid: ["discord_msg_store", "msg_id"], 27 | idx_emoji_id: ["emoji", "emoji_id"], 28 | idx_emoji_mxc_url: ["emoji", "mxc_url"], 29 | idx_event_store_discord_id: ["event_store", "discord_id"], 30 | idx_event_store_matrix_id: ["event_store", "matrix_id"], 31 | idx_remote_room_data_room_id: ["remote_room_data", "room_id"], 32 | idx_room_entries_id: ["room_entries", "id"], 33 | idx_room_entries_matrix_id: ["room_entries", "matrix_id"], 34 | idx_room_entries_remote_id: ["room_entries", "remote_id"], 35 | }; 36 | 37 | public async run(store: DiscordStore): Promise { 38 | try { 39 | await Promise.all(Object.keys(this.INDEXES).map(async (indexId: string) => { 40 | const ids = this.INDEXES[indexId]; 41 | return store.db.Exec(`CREATE INDEX ${indexId} ON ${ids[0]}(${ids[1]})`); 42 | })); 43 | } catch (ex) { 44 | log.error("Failed to apply indexes:", ex); 45 | } 46 | 47 | } 48 | 49 | public async rollBack(store: DiscordStore): Promise { 50 | await Promise.all(Object.keys(this.INDEXES).map(async (indexId: string) => { 51 | return store.db.Exec(`DROP INDEX ${indexId}`); 52 | })); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/db/schema/v11.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | import { Log } from "../../log"; 20 | 21 | const log = new Log("SchemaV11"); 22 | 23 | export class Schema implements IDbSchema { 24 | public description = "create stores for bot sdk"; 25 | 26 | public async run(store: DiscordStore): Promise { 27 | try { 28 | await store.createTable( 29 | "CREATE TABLE registered_users (user_id TEXT UNIQUE NOT NULL);", 30 | "registered_users", 31 | ); 32 | await store.createTable( 33 | "CREATE TABLE as_txns (txn_id TEXT UNIQUE NOT NULL);", 34 | "as_txns", 35 | ); 36 | } catch (ex) { 37 | log.error("Failed to apply indexes:", ex); 38 | } 39 | 40 | } 41 | 42 | public async rollBack(store: DiscordStore): Promise { 43 | await store.db.Exec( 44 | `DROP TABLE IF EXISTS registered_users; 45 | DROP TABLE IF EXISTS as_txns;`, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/db/schema/v12.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | import { Log } from "../../log"; 20 | 21 | const log = new Log("SchemaV12"); 22 | 23 | export class Schema implements IDbSchema { 24 | public description = "create stores for user activity tracking"; 25 | 26 | public async run(store: DiscordStore): Promise { 27 | try { 28 | await store.createTable( 29 | "CREATE TABLE user_activity (user_id TEXT UNIQUE, data JSON);", 30 | "user_activity", 31 | ); 32 | } catch (ex) { 33 | log.error("Failed to create table:", ex); 34 | } 35 | 36 | } 37 | 38 | public async rollBack(store: DiscordStore): Promise { 39 | await store.db.Exec( 40 | "DROP TABLE IF EXISTS user_activity;" 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/db/schema/v2.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | export class Schema implements IDbSchema { 20 | public description = "Create DM Table, User Options"; 21 | public async run(store: DiscordStore): Promise { 22 | await Promise.all([ 23 | store.createTable(` 24 | CREATE TABLE dm_rooms ( 25 | discord_id TEXT NOT NULL, 26 | channel_id TEXT NOT NULL, 27 | room_id TEXT UNIQUE NOT NULL 28 | );`, "dm_rooms"), 29 | store.createTable(` 30 | CREATE TABLE client_options ( 31 | discord_id TEXT UNIQUE NOT NULL, 32 | options INTEGER NOT NULL 33 | );`, "client_options", 34 | )]); 35 | } 36 | public async rollBack(store: DiscordStore): Promise { 37 | await store.db.Exec( 38 | `DROP TABLE IF EXISTS dm_rooms; 39 | DROP TABLE IF EXISTS client_options;`, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/db/schema/v3.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | import {DiscordClientFactory} from "../../clientfactory"; 20 | import { Log } from "../../log"; 21 | 22 | const log = new Log("SchemaV3"); 23 | 24 | export class Schema implements IDbSchema { 25 | public description = "user_tokens split into user_id_discord_id"; 26 | public async run(store: DiscordStore): Promise { 27 | await Promise.all([store.createTable(` 28 | CREATE TABLE user_id_discord_id ( 29 | discord_id TEXT NOT NULL, 30 | user_id TEXT NOT NULL, 31 | PRIMARY KEY(discord_id, user_id) 32 | );`, "user_id_discord_id"), 33 | store.createTable(` 34 | CREATE TABLE discord_id_token ( 35 | discord_id TEXT UNIQUE NOT NULL, 36 | token TEXT NOT NULL, 37 | PRIMARY KEY(discord_id) 38 | );`, "discord_id_token", 39 | )]); 40 | 41 | // Backup before moving data. 42 | await store.backupDatabase(); 43 | 44 | // Move old data to new tables. 45 | await this.moveUserIds(store); 46 | 47 | // Drop old table. 48 | await store.db.Run( 49 | `DROP TABLE IF EXISTS user_tokens;`, 50 | ); 51 | } 52 | 53 | public async rollBack(store: DiscordStore): Promise { 54 | await Promise.all([store.db.Run( 55 | `DROP TABLE IF EXISTS user_id_discord_id;`, 56 | ), store.db.Run( 57 | `DROP TABLE IF EXISTS discord_id_token;`, 58 | )]); 59 | } 60 | 61 | private async moveUserIds(store: DiscordStore): Promise { 62 | log.info("Performing one time moving of tokens to new table. Please wait."); 63 | let rows; 64 | try { 65 | rows = await store.db.All(`SELECT * FROM user_tokens`); 66 | } catch (err) { 67 | log.error(` 68 | Could not select users from 'user_tokens'.It is possible that the table does 69 | not exist on your database in which case you can proceed safely. Otherwise 70 | a copy of the database before the schema update has been placed in the root 71 | directory.`); 72 | log.error(err); 73 | return; 74 | } 75 | const promises = []; 76 | const clientFactory = new DiscordClientFactory(store); 77 | for (const row of rows) { 78 | log.info("Moving ", row.userId); 79 | try { 80 | const client = await clientFactory.getClient(row.token); 81 | const dId = client.user?.id; 82 | if (!dId) { 83 | continue; 84 | } 85 | log.verbose("INSERT INTO discord_id_token."); 86 | await store.db.Run( 87 | ` 88 | INSERT INTO discord_id_token (discord_id,token) 89 | VALUES ($discordId,$token); 90 | ` 91 | , { 92 | $discordId: dId, 93 | $token: row.token, 94 | }); 95 | log.verbose("INSERT INTO user_id_discord_id."); 96 | await store.db.Run( 97 | ` 98 | INSERT INTO user_id_discord_id (discord_id,user_id) 99 | VALUES ($discordId,$userId); 100 | ` 101 | , { 102 | $discordId: dId, 103 | $userId: row.userId, 104 | }); 105 | } catch (err) { 106 | log.error(`Couldn't move ${row.userId}'s token into new table.`); 107 | log.error(err); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/db/schema/v4.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | 20 | export class Schema implements IDbSchema { 21 | public description = "create guild emoji table"; 22 | public async run(store: DiscordStore): Promise { 23 | await store.createTable(` 24 | CREATE TABLE guild_emoji ( 25 | emoji_id TEXT NOT NULL, 26 | guild_id TEXT NOT NULL, 27 | name TEXT NOT NULL, 28 | mxc_url TEXT NOT NULL, 29 | created_at INTEGER NOT NULL, 30 | updated_at INTEGER NOT NULL, 31 | PRIMARY KEY(emoji_id, guild_id) 32 | );`, "guild_emoji"); 33 | } 34 | 35 | public async rollBack(store: DiscordStore): Promise { 36 | await store.db.Run( 37 | `DROP TABLE IF EXISTS guild_emoji;`, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/db/schema/v5.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | 20 | export class Schema implements IDbSchema { 21 | public description = "create event_store table"; 22 | public async run(store: DiscordStore): Promise { 23 | await store.createTable(` 24 | CREATE TABLE event_store ( 25 | matrix_id TEXT NOT NULL, 26 | discord_id TEXT NOT NULL, 27 | PRIMARY KEY(matrix_id, discord_id) 28 | );`, "event_store"); 29 | } 30 | 31 | public async rollBack(store: DiscordStore): Promise { 32 | await store.db.Run( 33 | `DROP TABLE IF EXISTS event_store;`, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/db/schema/v6.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | 20 | export class Schema implements IDbSchema { 21 | public description = "create event_store and discord_msg_store tables"; 22 | public async run(store: DiscordStore): Promise { 23 | await store.db.Run( 24 | `DROP TABLE IF EXISTS event_store;`, 25 | ); 26 | await store.createTable(` 27 | CREATE TABLE event_store ( 28 | matrix_id TEXT NOT NULL, 29 | discord_id TEXT NOT NULL, 30 | PRIMARY KEY(matrix_id, discord_id) 31 | );`, "event_store"); 32 | await store.createTable(` 33 | CREATE TABLE discord_msg_store ( 34 | msg_id TEXT NOT NULL, 35 | guild_id TEXT NOT NULL, 36 | channel_id TEXT NOT NULL, 37 | PRIMARY KEY(msg_id) 38 | );`, "discord_msg_store"); 39 | } 40 | 41 | public async rollBack(store: DiscordStore): Promise { 42 | await store.db.Exec( 43 | `DROP TABLE IF EXISTS event_store;` + 44 | `DROP TABLE IF EXISTS discord_msg_store;`, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/db/schema/v7.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | import { Log } from "../../log"; 20 | 21 | const log = new Log("SchemaV7"); 22 | 23 | export class Schema implements IDbSchema { 24 | public description = "create guild emoji table"; 25 | public async run(store: DiscordStore): Promise { 26 | await store.createTable(` 27 | CREATE TABLE emoji ( 28 | emoji_id TEXT NOT NULL, 29 | name TEXT NOT NULL, 30 | animated INTEGER NOT NULL, 31 | mxc_url TEXT NOT NULL, 32 | created_at BIGINT NOT NULL, 33 | updated_at BIGINT NOT NULL, 34 | PRIMARY KEY(emoji_id) 35 | );`, "emoji"); 36 | 37 | // migrate existing emoji 38 | try { 39 | await store.db.Run(` 40 | INSERT INTO emoji 41 | (emoji_id, name, animated, mxc_url, created_at, updated_at) 42 | SELECT emoji_id, name, 0 AS animated, mxc_url, created_at, updated_at FROM guild_emoji; 43 | `); 44 | } catch (e) { 45 | // ignore errors 46 | log.warning("Failed to migrate old data to new table"); 47 | } 48 | } 49 | 50 | public async rollBack(store: DiscordStore): Promise { 51 | await store.db.Run( 52 | `DROP TABLE IF EXISTS emoji;`, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/db/schema/v8.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {IDbSchema} from "./dbschema"; 18 | import {DiscordStore} from "../../store"; 19 | 20 | export class Schema implements IDbSchema { 21 | public description = "create room store tables"; 22 | 23 | constructor() { 24 | 25 | } 26 | 27 | public async run(store: DiscordStore): Promise { 28 | await store.createTable(` 29 | CREATE TABLE remote_room_data ( 30 | room_id TEXT NOT NULL, 31 | discord_guild TEXT NOT NULL, 32 | discord_channel TEXT NOT NULL, 33 | discord_name TEXT DEFAULT NULL, 34 | discord_topic TEXT DEFAULT NULL, 35 | discord_type TEXT DEFAULT NULL, 36 | discord_iconurl TEXT DEFAULT NULL, 37 | discord_iconurl_mxc TEXT DEFAULT NULL, 38 | update_name NUMERIC DEFAULT 0, 39 | update_topic NUMERIC DEFAULT 0, 40 | update_icon NUMERIC DEFAULT 0, 41 | plumbed NUMERIC DEFAULT 0, 42 | PRIMARY KEY(room_id) 43 | );`, "remote_room_data"); 44 | 45 | await store.createTable(` 46 | CREATE TABLE room_entries ( 47 | id TEXT NOT NULL, 48 | matrix_id TEXT, 49 | remote_id TEXT, 50 | PRIMARY KEY(id) 51 | );`, "room_entries"); 52 | 53 | // XXX: This used to migrate rooms across from the old room store format but 54 | // since we moved to the matrix-js-bot-sdk, we can no longer do this. Please 55 | // use a 0.X release for this. 56 | } 57 | 58 | public async rollBack(store: DiscordStore): Promise { 59 | await store.db.Run( 60 | `DROP TABLE IF EXISTS remote_room_data;`, 61 | ); 62 | await store.db.Run( 63 | `DROP TABLE IF EXISTS room_entries;`, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/db/schema/v9.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { IDbSchema } from "./dbschema"; 18 | import { DiscordStore } from "../../store"; 19 | 20 | export class Schema implements IDbSchema { 21 | public description = "create user store tables"; 22 | 23 | constructor() { 24 | 25 | } 26 | 27 | public async run(store: DiscordStore): Promise { 28 | await store.createTable(` 29 | CREATE TABLE remote_user_guild_nicks ( 30 | remote_id TEXT NOT NULL, 31 | guild_id TEXT NOT NULL, 32 | nick TEXT NOT NULL, 33 | PRIMARY KEY(remote_id, guild_id) 34 | );`, "remote_user_guild_nicks"); 35 | 36 | await store.createTable(` 37 | CREATE TABLE remote_user_data ( 38 | remote_id TEXT NOT NULL, 39 | displayname TEXT, 40 | avatarurl TEXT, 41 | avatarurl_mxc TEXT, 42 | PRIMARY KEY(remote_id) 43 | );`, "remote_user_data"); 44 | 45 | await store.createTable(` 46 | CREATE TABLE user_entries ( 47 | matrix_id TEXT, 48 | remote_id TEXT, 49 | PRIMARY KEY(matrix_id, remote_id) 50 | );`, "user_entries"); 51 | 52 | // XXX: This used to migrate rooms across from the old room store format but 53 | // since we moved to the matrix-js-bot-sdk, we can no longer do this. Please 54 | // use a 0.X release for this. 55 | } 56 | 57 | public async rollBack(store: DiscordStore): Promise { 58 | await store.db.Run( 59 | `DROP TABLE IF EXISTS remote_user_guild_nicks;`, 60 | ); 61 | await store.db.Run( 62 | `DROP TABLE IF EXISTS remote_user_data;`, 63 | ); 64 | await store.db.Run( 65 | `DROP TABLE IF EXISTS user_entries;`, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/db/sqlite3.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as BetterSQLite3 from "better-sqlite3"; 18 | import { Log } from "../log"; 19 | import { IDatabaseConnector, ISqlCommandParameters, ISqlRow } from "./connector"; 20 | const log = new Log("SQLite3"); 21 | 22 | export class SQLite3 implements IDatabaseConnector { 23 | private db: BetterSQLite3.Database; 24 | constructor(private filename: string) { 25 | 26 | } 27 | 28 | public async Open() { 29 | log.info(`Opening ${this.filename}`); 30 | this.db = new BetterSQLite3(this.filename); 31 | } 32 | 33 | public async Get(sql: string, parameters?: ISqlCommandParameters): Promise { 34 | log.silly("Get:", sql); 35 | return this.db.prepare(sql).get(parameters || []); 36 | } 37 | 38 | public async All(sql: string, parameters?: ISqlCommandParameters): Promise { 39 | log.silly("All:", sql); 40 | return this.db.prepare(sql).all(parameters || []); 41 | } 42 | 43 | public async Run(sql: string, parameters?: ISqlCommandParameters): Promise { 44 | log.silly("Run:", sql); 45 | this.db.prepare(sql).run(parameters || []); 46 | } 47 | 48 | public async Close(): Promise { 49 | this.db.close(); 50 | } 51 | 52 | public async Exec(sql: string): Promise { 53 | log.silly("Exec:", sql); 54 | this.db.exec(sql); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/db/userstore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | /* eslint-disable max-classes-per-file */ 17 | 18 | import { IDatabaseConnector } from "./connector"; 19 | import { Log } from "../log"; 20 | import { MetricPeg } from "../metrics"; 21 | import { TimedCache } from "../structures/timedcache"; 22 | 23 | /** 24 | * A UserStore compatible with 25 | * https://github.com/matrix-org/matrix-appservice-bridge/blob/master/lib/components/user-bridge-store.js 26 | * that accesses the database instead. 27 | */ 28 | 29 | const ENTRY_CACHE_LIMETIME = 30000; 30 | 31 | export class RemoteUser { 32 | public displayname: string|null = null; 33 | public avatarurl: string|null = null; 34 | public avatarurlMxc: string|null = null; 35 | public guildNicks: Map = new Map(); 36 | constructor(public readonly id: string) { 37 | 38 | } 39 | } 40 | 41 | const log = new Log("DbUserStore"); 42 | 43 | export interface IUserStoreEntry { 44 | id: string; 45 | matrix: string|null; 46 | remote: RemoteUser|null; 47 | } 48 | 49 | export class DbUserStore { 50 | private remoteUserCache: TimedCache; 51 | 52 | constructor(private db: IDatabaseConnector) { 53 | this.remoteUserCache = new TimedCache(ENTRY_CACHE_LIMETIME); 54 | } 55 | 56 | public async getRemoteUser(remoteId: string): Promise { 57 | const cached = this.remoteUserCache.get(remoteId); 58 | if (cached) { 59 | MetricPeg.get.storeCall("UserStore.getRemoteUser", true); 60 | return cached; 61 | } 62 | MetricPeg.get.storeCall("UserStore.getRemoteUser", false); 63 | 64 | const row = await this.db.Get( 65 | "SELECT * FROM user_entries WHERE remote_id = $id", {id: remoteId}, 66 | ); 67 | if (!row) { 68 | return null; 69 | } 70 | const remoteUser = new RemoteUser(remoteId); 71 | const data = await this.db.Get( 72 | "SELECT * FROM remote_user_data WHERE remote_id = $remoteId", 73 | {remoteId}, 74 | ); 75 | if (data) { 76 | remoteUser.avatarurl = data.avatarurl as string|null; 77 | remoteUser.displayname = data.displayname as string|null; 78 | remoteUser.avatarurlMxc = data.avatarurl_mxc as string|null; 79 | } 80 | const nicks = await this.db.All( 81 | "SELECT guild_id, nick FROM remote_user_guild_nicks WHERE remote_id = $remoteId", 82 | {remoteId}, 83 | ); 84 | if (nicks) { 85 | /* eslint-disable @typescript-eslint/naming-convention */ 86 | nicks.forEach(({nick, guild_id}) => { 87 | remoteUser.guildNicks.set(guild_id as string, nick as string); 88 | }); 89 | /* eslint-enable @typescript-eslint/naming-convention */ 90 | } 91 | this.remoteUserCache.set(remoteId, remoteUser); 92 | return remoteUser; 93 | } 94 | 95 | public async setRemoteUser(user: RemoteUser) { 96 | MetricPeg.get.storeCall("UserStore.setRemoteUser", false); 97 | this.remoteUserCache.delete(user.id); 98 | const existingData = await this.db.Get( 99 | "SELECT * FROM remote_user_data WHERE remote_id = $remoteId", 100 | {remoteId: user.id}, 101 | ); 102 | if (!existingData) { 103 | await this.db.Run( 104 | `INSERT INTO remote_user_data VALUES ( 105 | $remote_id, 106 | $displayname, 107 | $avatarurl, 108 | $avatarurl_mxc 109 | )`, 110 | { 111 | /* eslint-disable @typescript-eslint/naming-convention */ 112 | avatarurl: user.avatarurl, 113 | avatarurl_mxc: user.avatarurlMxc, 114 | displayname: user.displayname, 115 | remote_id: user.id, 116 | /* eslint-enable @typescript-eslint/naming-convention */ 117 | }); 118 | } else { 119 | await this.db.Run( 120 | `UPDATE remote_user_data SET displayname = $displayname, 121 | avatarurl = $avatarurl, 122 | avatarurl_mxc = $avatarurl_mxc WHERE remote_id = $remote_id`, 123 | { 124 | /* eslint-disable @typescript-eslint/naming-convention */ 125 | avatarurl: user.avatarurl, 126 | avatarurl_mxc: user.avatarurlMxc, 127 | displayname: user.displayname, 128 | remote_id: user.id, 129 | /* eslint-enable @typescript-eslint/naming-convention */ 130 | }); 131 | } 132 | const existingNicks = {}; 133 | (await this.db.All( 134 | "SELECT guild_id, nick FROM remote_user_guild_nicks WHERE remote_id = $remoteId", 135 | {remoteId: user.id}, 136 | )).forEach(({guild_id, nick}) => existingNicks[guild_id as string] = nick); // eslint-disable-line @typescript-eslint/naming-convention 137 | for (const guildId of user.guildNicks.keys()) { 138 | const nick = user.guildNicks.get(guildId) || null; 139 | if (existingData) { 140 | if (existingNicks[guildId] === nick) { 141 | return; 142 | } else if (existingNicks[guildId]) { 143 | await this.db.Run( 144 | `UPDATE remote_user_guild_nicks SET nick = $nick 145 | WHERE remote_id = $remote_id 146 | AND guild_id = $guild_id`, 147 | { 148 | /* eslint-disable @typescript-eslint/naming-convention */ 149 | guild_id: guildId, 150 | nick, 151 | remote_id: user.id, 152 | /* eslint-enable @typescript-eslint/naming-convention */ 153 | }); 154 | return; 155 | } 156 | } 157 | await this.db.Run( 158 | `INSERT INTO remote_user_guild_nicks VALUES ( 159 | $remote_id, 160 | $guild_id, 161 | $nick 162 | )`, 163 | { 164 | /* eslint-disable @typescript-eslint/naming-convention */ 165 | guild_id: guildId, 166 | nick, 167 | remote_id: user.id, 168 | /* eslint-enable @typescript-eslint/naming-convention */ 169 | }); 170 | } 171 | 172 | } 173 | 174 | public async linkUsers(matrixId: string, remoteId: string) { 175 | MetricPeg.get.storeCall("UserStore.linkUsers", false); 176 | // This is used ONCE in the bridge to link two IDs, so do not UPSURT data. 177 | try { 178 | await this.db.Run(`INSERT INTO user_entries VALUES ($matrixId, $remoteId)`, { 179 | matrixId, 180 | remoteId, 181 | }); 182 | } catch (ex) { 183 | log.verbose("Failed to insert into user_entries, entry probably exists:", ex); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/discordcommandhandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as Discord from "@mx-puppet/better-discord.js"; 18 | import { DiscordBot } from "./bot"; 19 | import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util"; 20 | import { Log } from "./log"; 21 | import { Appservice } from "matrix-bot-sdk"; 22 | 23 | const log = new Log("DiscordCommandHandler"); 24 | 25 | export class DiscordCommandHandler { 26 | constructor( 27 | private bridge: Appservice, 28 | private discord: DiscordBot, 29 | ) { } 30 | 31 | public async Process(msg: Discord.Message) { 32 | const chan = msg.channel as Discord.TextChannel; 33 | if (!chan.guild) { 34 | await msg.channel.send("**ERROR:** only available for guild channels"); 35 | return; 36 | } 37 | if (!msg.member) { 38 | await msg.channel.send("**ERROR:** could not determine message member"); 39 | return; 40 | } 41 | 42 | const discordMember = msg.member; 43 | 44 | const intent = this.bridge.botIntent; 45 | 46 | const actions: ICommandActions = { 47 | approve: { 48 | description: "Approve a pending bridge request", 49 | params: [], 50 | permission: "MANAGE_WEBHOOKS", 51 | run: async () => { 52 | if (await this.discord.Provisioner.MarkApproved(chan, discordMember, true)) { 53 | return "Thanks for your response! The matrix bridge has been approved."; 54 | } else { 55 | return "Thanks for your response, however" + 56 | " it has arrived after the deadline - sorry!"; 57 | } 58 | }, 59 | }, 60 | ban: { 61 | description: "Bans a user on the matrix side", 62 | params: ["name"], 63 | permission: "BAN_MEMBERS", 64 | run: this.ModerationActionGenerator(chan, "ban"), 65 | }, 66 | deny: { 67 | description: "Deny a pending bridge request", 68 | params: [], 69 | permission: "MANAGE_WEBHOOKS", 70 | run: async () => { 71 | if (await this.discord.Provisioner.MarkApproved(chan, discordMember, false)) { 72 | return "Thanks for your response! The matrix bridge has been declined."; 73 | } else { 74 | return "Thanks for your response, however" + 75 | " it has arrived after the deadline - sorry!"; 76 | } 77 | }, 78 | }, 79 | kick: { 80 | description: "Kicks a user on the matrix side", 81 | params: ["name"], 82 | permission: "KICK_MEMBERS", 83 | run: this.ModerationActionGenerator(chan, "kick"), 84 | }, 85 | unban: { 86 | description: "Unbans a user on the matrix side", 87 | params: ["name"], 88 | permission: "BAN_MEMBERS", 89 | run: this.ModerationActionGenerator(chan, "unban"), 90 | }, 91 | unbridge: { 92 | description: "Unbridge matrix rooms from this channel", 93 | params: [], 94 | permission: ["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"], 95 | run: async () => this.UnbridgeChannel(chan), 96 | }, 97 | }; 98 | 99 | const parameters: ICommandParameters = { 100 | name: { 101 | description: "The display name or mxid of a matrix user", 102 | get: async (name) => { 103 | const channelMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(msg.channel); 104 | const mxUserId = await Util.GetMxidFromName(intent, name, channelMxids); 105 | return mxUserId; 106 | }, 107 | }, 108 | }; 109 | 110 | const permissionCheck: CommandPermissonCheck = async (permission: string|string[]) => { 111 | if (!Array.isArray(permission)) { 112 | permission = [permission]; 113 | } 114 | return permission.every((p) => discordMember.hasPermission(p as Discord.PermissionResolvable)); 115 | }; 116 | 117 | const reply = await Util.ParseCommand("!matrix", msg.content, actions, parameters, permissionCheck); 118 | await msg.channel.send(reply); 119 | } 120 | 121 | private ModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: "kick"|"ban"|"unban") { 122 | return async ({name}) => { 123 | let allChannelMxids: string[] = []; 124 | await Promise.all(discordChannel.guild.channels.cache.map(async (chan) => { 125 | try { 126 | const chanMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan); 127 | allChannelMxids = allChannelMxids.concat(chanMxids); 128 | } catch (e) { 129 | // pass, non-text-channel 130 | } 131 | })); 132 | let errorMsg = ""; 133 | await Promise.all(allChannelMxids.map(async (chanMxid) => { 134 | try { 135 | await this.bridge.botIntent.underlyingClient[funcKey + "User"](chanMxid, name); 136 | } catch (e) { 137 | // maybe we don't have permission to kick/ban/unban...? 138 | errorMsg += `\nCouldn't ${funcKey} ${name} from ${chanMxid}`; 139 | } 140 | })); 141 | if (errorMsg) { 142 | throw Error(errorMsg); 143 | } 144 | const action = { 145 | ban: "Banned", 146 | kick: "Kicked", 147 | unban: "Unbanned", 148 | }[funcKey]; 149 | return `${action} ${name}`; 150 | }; 151 | } 152 | 153 | private async UnbridgeChannel(channel: Discord.TextChannel): Promise { 154 | try { 155 | await this.discord.Provisioner.UnbridgeChannel(channel); 156 | return "This channel has been unbridged"; 157 | } catch (err) { 158 | if (err.message === "Channel is not bridged") { 159 | return "This channel is not bridged to a plumbed matrix room"; 160 | } 161 | log.error("Error while unbridging room " + channel.id); 162 | log.error(err); 163 | return "There was an error unbridging this room. " + 164 | "Please try again later or contact the bridge operator."; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/discordmessageprocessor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 - 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as Discord from "@mx-puppet/better-discord.js"; 18 | import { DiscordBot } from "./bot"; 19 | import { Log } from "./log"; 20 | import { 21 | DiscordMessageParser, 22 | IDiscordMessageParserOpts, 23 | IDiscordMessageParserCallbacks, 24 | IDiscordMessageParserResult, 25 | } from "@mx-puppet/matrix-discord-parser"; 26 | 27 | const log = new Log("DiscordMessageProcessor"); 28 | 29 | export class DiscordMessageProcessor { 30 | private parser: DiscordMessageParser; 31 | constructor(private domain: string, private bot: DiscordBot) { 32 | this.parser = new DiscordMessageParser(); 33 | } 34 | 35 | public async FormatMessage(msg: Discord.Message): Promise { 36 | const opts = { 37 | callbacks: this.getParserCallbacks(msg), 38 | } as IDiscordMessageParserOpts; 39 | return await this.parser.FormatMessage(opts, msg); 40 | } 41 | 42 | public async FormatEdit( 43 | msg1: Discord.Message, 44 | msg2: Discord.Message, 45 | link: string, 46 | ): Promise { 47 | // obsolete once edit PR is merged 48 | const opts = { 49 | callbacks: this.getParserCallbacks(msg2), 50 | } as IDiscordMessageParserOpts; 51 | return await this.parser.FormatEdit(opts, msg1, msg2, link); 52 | } 53 | 54 | private getParserCallbacks(msg: Discord.Message): IDiscordMessageParserCallbacks { 55 | return { 56 | getChannel: async (id: string) => { 57 | const channel = msg.guild?.channels.resolve(id); 58 | if (!channel) { 59 | return null; 60 | } 61 | const alias = await this.bot.ChannelSyncroniser.GetAliasFromChannel(channel); 62 | if (!alias) { 63 | return null; 64 | } 65 | return { 66 | mxid: alias, 67 | name: channel.name, 68 | }; 69 | }, 70 | getEmoji: async (name: string, animated: boolean, id: string) => { 71 | try { 72 | const mxcUrl = await this.bot.GetEmoji(name, animated, id); 73 | return mxcUrl; 74 | } catch (ex) { 75 | log.warn(`Could not get emoji ${id} with name ${name}`, ex); 76 | } 77 | return null; 78 | }, 79 | getUser: async (id: string) => { 80 | const member = msg.guild?.members.resolve(id); 81 | const mxid = `@_discord_${id}:${this.domain}`; 82 | const name = member ? member.displayName : mxid; 83 | return { 84 | mxid, 85 | name, 86 | }; 87 | }, 88 | }; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { createLogger, Logger, format, transports } from "winston"; 18 | import { DiscordBridgeConfigLogging, LoggingFile} from "./config"; 19 | import { inspect } from "util"; 20 | import "winston-daily-rotate-file"; 21 | 22 | const FORMAT_FUNC = format.printf((info) => { 23 | return `${info.timestamp} [${info.module}] ${info.level}: ${info.message}`; 24 | }); 25 | 26 | export class Log { 27 | public static get level(): string { 28 | return this.logger.level; 29 | } 30 | 31 | public static set level(level: string) { 32 | this.logger.level = level; 33 | } 34 | 35 | public static Configure(config: DiscordBridgeConfigLogging): void { 36 | // Merge defaults. 37 | Log.config = Object.assign(new DiscordBridgeConfigLogging(), config); 38 | Log.setupLogger(); 39 | } 40 | 41 | public static ForceSilent(): void { 42 | new Log("Log").warn("Log set to silent"); 43 | Log.logger.silent = true; 44 | } 45 | 46 | private static config: DiscordBridgeConfigLogging; 47 | private static logger: Logger; 48 | 49 | private static isValidLevel(level: string) { 50 | return ["silly", "verbose", "info", "http", "warn", "error", "silent"].includes(level); 51 | } 52 | 53 | private static setupLogger(): void { 54 | if (Log.logger) { 55 | Log.logger.close(); 56 | } 57 | const tsports: transports.StreamTransportInstance[] = Log.config.files.map((file) => 58 | Log.setupFileTransport(file), 59 | ); 60 | if (Log.config.console && !Log.isValidLevel(Log.config.console)) { 61 | new Log("Log").warn("Console log level is invalid. Please pick one of the case-sensitive levels provided in the sample config."); 62 | } 63 | tsports.push(new transports.Console({ 64 | level: Log.config.console, 65 | })); 66 | Log.logger = createLogger({ 67 | format: format.combine( 68 | format.timestamp({ 69 | format: Log.config.lineDateFormat, 70 | }), 71 | format.colorize(), 72 | FORMAT_FUNC, 73 | ), 74 | transports: tsports, 75 | }); 76 | } 77 | 78 | private static setupFileTransport(config: LoggingFile): transports.FileTransportInstance { 79 | config = Object.assign(new LoggingFile(), config); 80 | const filterOutMods = format((info, _) => { 81 | if (config.disabled.includes(info.module) || 82 | config.enabled.length > 0 && 83 | !config.enabled.includes(info.module) 84 | ) { 85 | return false; 86 | } 87 | return info; 88 | }); 89 | 90 | if (config.level && !Log.isValidLevel(config.level)) { 91 | new Log("Log").warn(`Log level of ${config.file} is invalid. Please pick one of the case-sensitive levels provided in the sample config.`); 92 | } 93 | 94 | const opts = { 95 | datePattern: config.datePattern, 96 | filename: config.file, 97 | format: format.combine( 98 | filterOutMods(), 99 | FORMAT_FUNC, 100 | ), 101 | level: config.level, 102 | maxFiles: config.maxFiles, 103 | maxSize: config.maxSize, 104 | }; 105 | 106 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 107 | return new (transports as any).DailyRotateFile(opts); 108 | } 109 | 110 | constructor(private module: string) { } 111 | 112 | public error(...msg: unknown[]): void { 113 | this.log("error", msg); 114 | } 115 | 116 | public warn(...msg: unknown[]): void { 117 | this.log("warn", msg); 118 | } 119 | 120 | public warning(...msg: unknown[]): void { 121 | this.warn(...msg); 122 | } 123 | 124 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 125 | public info(...msg: unknown[]): void { 126 | this.log("info", msg); 127 | } 128 | 129 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 130 | public verbose(...msg: unknown[]): void { 131 | this.log("verbose", msg); 132 | } 133 | 134 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 135 | public silly(...msg: unknown[]): void { 136 | this.log("silly", msg); 137 | } 138 | 139 | private log(level: string, msg: unknown[]): void { 140 | if (!Log.logger) { 141 | // We've not configured the logger yet, so create a basic one. 142 | Log.config = new DiscordBridgeConfigLogging(); 143 | Log.setupLogger(); 144 | } 145 | const msgStr = msg.map((item) => { 146 | return typeof(item) === "string" ? item : inspect(item); 147 | }).join(" "); 148 | 149 | Log.logger.log(level, msgStr, {module: this.module}); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/matrixmessageprocessor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018, 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as Discord from "@mx-puppet/better-discord.js"; 18 | import { IMatrixMessage } from "./matrixtypes"; 19 | import { Util } from "./util"; 20 | import { DiscordBot } from "./bot"; 21 | import { MatrixClient } from "matrix-bot-sdk"; 22 | import { DiscordBridgeConfig } from "./config"; 23 | import { 24 | IMatrixMessageParserCallbacks, 25 | IMatrixMessageParserOpts, 26 | MatrixMessageParser, 27 | } from "@mx-puppet/matrix-discord-parser"; 28 | 29 | const DEFAULT_ROOM_NOTIFY_POWER_LEVEL = 50; 30 | 31 | export interface IMatrixMessageProcessorParams { 32 | displayname?: string; 33 | mxClient?: MatrixClient; 34 | roomId?: string; 35 | userId?: string; 36 | } 37 | 38 | export class MatrixMessageProcessor { 39 | private parser: MatrixMessageParser; 40 | constructor(public bot: DiscordBot, private config: DiscordBridgeConfig) { 41 | this.parser = new MatrixMessageParser(); 42 | } 43 | 44 | public async FormatMessage( 45 | msg: IMatrixMessage, 46 | guild: Discord.Guild, 47 | params?: IMatrixMessageProcessorParams, 48 | ): Promise { 49 | const opts = this.getParserOpts(msg, guild, params); 50 | return this.parser.FormatMessage(opts, msg); 51 | } 52 | 53 | private getParserOpts( 54 | msg: IMatrixMessage, 55 | guild: Discord.Guild, 56 | params?: IMatrixMessageProcessorParams, 57 | ): IMatrixMessageParserOpts { 58 | return { 59 | callbacks: this.getParserCallbacks(msg, guild, params), 60 | determineCodeLanguage: this.config.bridge.determineCodeLanguage, 61 | displayname: params ? params.displayname || "" : "", 62 | }; 63 | } 64 | 65 | private getParserCallbacks( 66 | msg: IMatrixMessage, 67 | guild: Discord.Guild, 68 | params?: IMatrixMessageProcessorParams, 69 | ): IMatrixMessageParserCallbacks { 70 | return { 71 | canNotifyRoom: async () => { 72 | if (!params || !params.mxClient || !params.roomId || !params.userId) { 73 | return false; 74 | } 75 | return await Util.CheckMatrixPermission( 76 | params.mxClient, 77 | params.userId, 78 | params.roomId, 79 | DEFAULT_ROOM_NOTIFY_POWER_LEVEL, 80 | "notifications", 81 | "room", 82 | ); 83 | }, 84 | getChannelId: async (mxid: string) => { 85 | const CHANNEL_REGEX = /^#_discord_[0-9]*_([0-9]*):/; 86 | const match = mxid.match(CHANNEL_REGEX); 87 | const channel = match && guild.channels.resolve(match[1]); 88 | if (!channel) { 89 | /* 90 | This isn't formatted in #_discord_, so let's fetch the internal room ID 91 | and see if it is still a bridged room! 92 | */ 93 | if (params && params.mxClient) { 94 | try { 95 | const resp = await params.mxClient.lookupRoomAlias(mxid); 96 | if (resp && resp.roomId) { 97 | const roomId = resp.roomId; 98 | const ch = await this.bot.GetChannelFromRoomId(roomId); 99 | return ch.id; 100 | } 101 | } catch (err) { } // ignore, room ID wasn't found 102 | } 103 | return null; 104 | } 105 | return match && match[1] || null; 106 | }, 107 | getEmoji: async (mxc: string, name: string) => { 108 | let emoji: {id: string, animated: boolean, name: string} | null = null; 109 | try { 110 | const emojiDb = await this.bot.GetEmojiByMxc(mxc); 111 | const id = emojiDb.EmojiId; 112 | emoji = guild.emojis.resolve(id); 113 | } catch (e) { 114 | emoji = null; 115 | } 116 | if (!emoji) { 117 | emoji = guild.emojis.resolve(name); 118 | } 119 | return emoji; 120 | }, 121 | getUserId: async (mxid: string) => { 122 | const USER_REGEX = /^@_discord_([0-9]*)/; 123 | const match = mxid.match(USER_REGEX); 124 | const member = match && await guild.members.fetch(match[1]); 125 | if (!match || !member) { 126 | return null; 127 | } 128 | return match[1]; 129 | }, 130 | mxcUrlToHttp: (mxc: string) => { 131 | if (params && params.mxClient) { 132 | return params.mxClient.mxcToHttp(mxc); 133 | } 134 | return mxc; 135 | }, 136 | }; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/matrixtypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018, 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export interface IMatrixEventContent { 18 | body?: string; 19 | info?: any; // tslint:disable-line no-any 20 | name?: string; 21 | topic?: string; 22 | membership?: string; 23 | msgtype?: string; 24 | url?: string; 25 | displayname?: string; 26 | avatar_url?: string; 27 | reason?: string; 28 | "m.relates_to"?: any; // tslint:disable-line no-any 29 | } 30 | 31 | export interface IMatrixEvent { 32 | event_id: string; 33 | state_key: string; 34 | type: string; 35 | sender: string; 36 | room_id: string; 37 | membership?: string; 38 | avatar_url?: string; 39 | displayname?: string; 40 | redacts?: string; 41 | replaces_state?: string; 42 | content?: IMatrixEventContent; 43 | origin_server_ts: number; 44 | users?: any; // tslint:disable-line no-any 45 | users_default?: any; // tslint:disable-line no-any 46 | notifications?: any; // tslint:disable-line no-any 47 | unsigned?: { 48 | replaces_state: any; // tslint:disable-line no-any 49 | } 50 | } 51 | 52 | export interface IMatrixMessage { 53 | body: string; 54 | msgtype: string; 55 | formatted_body?: string; 56 | format?: string; 57 | "m.new_content"?: any; // tslint:disable-line no-any 58 | "m.relates_to"?: any; // tslint:disable-line no-any 59 | } 60 | 61 | export interface IMatrixMediaInfo { 62 | w?: number; 63 | h?: number; 64 | mimetype: string; 65 | size: number; 66 | duration?: number; 67 | } 68 | -------------------------------------------------------------------------------- /src/presencehandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 - 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { User, Presence } from "@mx-puppet/better-discord.js"; 18 | import { DiscordBot } from "./bot"; 19 | import { Log } from "./log"; 20 | import { MetricPeg } from "./metrics"; 21 | const log = new Log("PresenceHandler"); 22 | 23 | export class PresenceHandlerStatus { 24 | public Presence: "online"|"offline"|"unavailable"; 25 | public StatusMsg: string; 26 | public ShouldDrop: boolean = false; 27 | } 28 | 29 | interface IMatrixPresence { 30 | presence?: "online"|"offline"|"unavailable"; 31 | status_msg?: string; 32 | } 33 | 34 | export class PresenceHandler { 35 | private presenceQueue: Presence[]; 36 | private interval: NodeJS.Timeout | null; 37 | constructor(private bot: DiscordBot) { 38 | this.presenceQueue = []; 39 | } 40 | 41 | get QueueCount(): number { 42 | return this.presenceQueue.length; 43 | } 44 | 45 | public async Start(intervalTime: number) { 46 | if (this.interval) { 47 | log.info("Restarting presence handler..."); 48 | this.Stop(); 49 | } 50 | log.info(`Starting presence handler with new interval ${intervalTime}ms`); 51 | this.interval = setInterval(await this.processIntervalThread.bind(this), 52 | intervalTime); 53 | } 54 | 55 | public Stop() { 56 | if (!this.interval) { 57 | log.info("Can not stop interval, not running."); 58 | return; 59 | } 60 | log.info("Stopping presence handler"); 61 | clearInterval(this.interval); 62 | this.interval = null; 63 | } 64 | 65 | public EnqueueUser(presence: Presence) { 66 | if (presence.userID === this.bot.GetBotId()) { 67 | return; 68 | } 69 | 70 | // Delete stale presence 71 | const indexOfPresence = this.presenceQueue.findIndex((u) => u.userID === presence.userID); 72 | if (indexOfPresence !== -1) { 73 | this.presenceQueue.splice(indexOfPresence, 1); 74 | } 75 | log.verbose(`Adding ${presence.userID} (${presence.user?.username}) to the presence queue`); 76 | this.presenceQueue.push(presence); 77 | MetricPeg.get.setPresenceCount(this.presenceQueue.length); 78 | } 79 | 80 | public DequeueUser(user: User) { 81 | const index = this.presenceQueue.findIndex((item) => { 82 | return user.id === item.userID; 83 | }); 84 | if (index !== -1) { 85 | this.presenceQueue.splice(index, 1); 86 | MetricPeg.get.setPresenceCount(this.presenceQueue.length); 87 | } else { 88 | log.warn( 89 | `Tried to remove ${user.id} from the presence queue but it could not be found`, 90 | ); 91 | } 92 | } 93 | 94 | public async ProcessUser(presence: Presence): Promise { 95 | if (!presence.user) { 96 | return true; 97 | } 98 | const status = this.getUserPresence(presence); 99 | await this.setMatrixPresence(presence.user, status); 100 | return status.ShouldDrop; 101 | } 102 | 103 | private async processIntervalThread() { 104 | const presence = this.presenceQueue.shift(); 105 | if (presence) { 106 | const proccessed = await this.ProcessUser(presence); 107 | if (!proccessed) { 108 | this.presenceQueue.push(presence); 109 | } else { 110 | log.verbose(`Dropping ${presence.userID} from the presence queue.`); 111 | MetricPeg.get.setPresenceCount(this.presenceQueue.length); 112 | } 113 | } 114 | } 115 | 116 | private getUserPresence(presence: Presence): PresenceHandlerStatus { 117 | const status = new PresenceHandlerStatus(); 118 | 119 | // How do we show multiple activities? 120 | const activity = presence.activities[0]; 121 | if (activity) { 122 | const type = activity.type[0] + activity.type.substring(1).toLowerCase(); // STREAMING -> Streaming; 123 | status.StatusMsg = `${type} ${activity.name}`; 124 | if (activity.url) { 125 | status.StatusMsg += ` | ${activity.url}`; 126 | } 127 | } 128 | 129 | if (presence.status === "online") { 130 | status.Presence = "online"; 131 | } else if (presence.status === "dnd") { 132 | status.Presence = "online"; 133 | status.StatusMsg = status.StatusMsg ? `Do not disturb | ${status.StatusMsg}` : "Do not disturb"; 134 | } else if (presence.status === "offline") { 135 | status.Presence = "offline"; 136 | status.ShouldDrop = true; // Drop until we recieve an update. 137 | } else { // idle 138 | status.Presence = "unavailable"; 139 | } 140 | return status; 141 | } 142 | 143 | private async setMatrixPresence(user: User, status: PresenceHandlerStatus) { 144 | const intent = this.bot.GetIntentFromDiscordMember(user); 145 | try { 146 | await intent.ensureRegistered(); 147 | await intent.underlyingClient.setPresenceStatus(status.Presence, status.StatusMsg || ""); 148 | } catch (ex) { 149 | if (ex.errcode !== "M_FORBIDDEN") { 150 | log.warn(`Could not update Matrix presence for ${user.id}`); 151 | return; 152 | } 153 | try { 154 | await this.bot.UserSyncroniser.OnUpdateUser(user); 155 | } catch (err) { 156 | log.warn(`Could not register new Matrix user for ${user.id}`); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/provisioner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018, 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as Discord from "@mx-puppet/better-discord.js"; 18 | import { DbRoomStore, RemoteStoreRoom, MatrixStoreRoom } from "./db/roomstore"; 19 | import { ChannelSyncroniser } from "./channelsyncroniser"; 20 | import { Log } from "./log"; 21 | 22 | const PERMISSION_REQUEST_TIMEOUT = 300000; // 5 minutes 23 | 24 | const log = new Log("Provisioner"); 25 | 26 | export class Provisioner { 27 | 28 | private pendingRequests: Map void> = new Map(); // [channelId]: resolver fn 29 | 30 | constructor(private roomStore: DbRoomStore, private channelSync: ChannelSyncroniser) { } 31 | 32 | public async BridgeMatrixRoom(channel: Discord.TextChannel, roomId: string) { 33 | const remote = new RemoteStoreRoom(`discord_${channel.guild.id}_${channel.id}_bridged`, { 34 | discord_channel: channel.id, 35 | discord_guild: channel.guild.id, 36 | discord_type: "text", 37 | plumbed: true, 38 | }); 39 | const local = new MatrixStoreRoom(roomId); 40 | return this.roomStore.linkRooms(local, remote); 41 | } 42 | 43 | /** 44 | * Returns if the room count limit has been reached. 45 | * This can be set by the bridge admin and prevents new rooms from being bridged. 46 | * @returns Has the limit been reached? 47 | */ 48 | public async RoomCountLimitReached(limit: number): Promise { 49 | return limit >= 0 && await this.roomStore.countEntries() >= limit; 50 | } 51 | 52 | public async UnbridgeChannel(channel: Discord.TextChannel, rId?: string) { 53 | const roomsRes = await this.roomStore.getEntriesByRemoteRoomData({ 54 | discord_channel: channel.id, 55 | discord_guild: channel.guild.id, 56 | plumbed: true, 57 | }); 58 | if (roomsRes.length === 0) { 59 | throw Error("Channel is not bridged"); 60 | } 61 | const remoteRoom = roomsRes[0].remote as RemoteStoreRoom; 62 | let roomsToUnbridge: string[] = []; 63 | if (rId) { 64 | roomsToUnbridge = [rId]; 65 | } else { 66 | // Kill em all. 67 | roomsToUnbridge = roomsRes.map((entry) => entry.matrix!.roomId); 68 | } 69 | await Promise.all(roomsToUnbridge.map( async (roomId) => { 70 | try { 71 | await this.channelSync.OnUnbridge(channel, roomId); 72 | } catch (ex) { 73 | log.error(`Failed to cleanly unbridge ${channel.id} ${channel.guild} from ${roomId}`, ex); 74 | } 75 | })); 76 | await this.roomStore.removeEntriesByRemoteRoomId(remoteRoom.getId()); 77 | } 78 | 79 | public async AskBridgePermission( 80 | channel: Discord.TextChannel, 81 | requestor: string, 82 | timeout: number = PERMISSION_REQUEST_TIMEOUT): Promise { 83 | const channelId = `${channel.guild.id}/${channel.id}`; 84 | 85 | let responded = false; 86 | let resolve: (msg: string) => void; 87 | let reject: (err: Error) => void; 88 | const deferP: Promise = new Promise((res, rej) => {resolve = res; reject = rej; }); 89 | 90 | const approveFn = (approved: boolean, expired = false) => { 91 | if (responded) { 92 | return; 93 | } 94 | 95 | responded = true; 96 | this.pendingRequests.delete(channelId); 97 | if (approved) { 98 | resolve("Approved"); 99 | } else { 100 | if (expired) { 101 | reject(Error("Timed out waiting for a response from the Discord owners.")); 102 | } else { 103 | reject(Error("The bridge has been declined by the Discord guild.")); 104 | } 105 | } 106 | }; 107 | 108 | this.pendingRequests.set(channelId, approveFn); 109 | setTimeout(() => approveFn(false, true), timeout); 110 | 111 | await channel.send(`${requestor} on matrix would like to bridge this channel. Someone with permission` + 112 | " to manage webhooks please reply with `!matrix approve` or `!matrix deny` in the next 5 minutes."); 113 | return await deferP; 114 | 115 | } 116 | 117 | public HasPendingRequest(channel: Discord.TextChannel): boolean { 118 | const channelId = `${channel.guild.id}/${channel.id}`; 119 | return this.pendingRequests.has(channelId); 120 | } 121 | 122 | public async MarkApproved( 123 | channel: Discord.TextChannel, 124 | member: Discord.GuildMember, 125 | allow: boolean, 126 | ): Promise { 127 | const channelId = `${channel.guild.id}/${channel.id}`; 128 | if (!this.pendingRequests.has(channelId)) { 129 | return false; // no change, so false 130 | } 131 | 132 | const perms = channel.permissionsFor(member); 133 | if (!perms || !perms.has(Discord.Permissions.FLAGS.MANAGE_WEBHOOKS as Discord.PermissionResolvable)) { 134 | // Missing permissions, so just reject it 135 | throw new Error("You do not have permission to manage webhooks in this channel"); 136 | } 137 | 138 | this.pendingRequests.get(channelId)!(allow); 139 | return true; // replied, so true 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/structures/lock.ts: -------------------------------------------------------------------------------- 1 | export class Lock { 2 | private locks: Map void)|null}>; 3 | private lockPromises: Map>; 4 | constructor( 5 | private timeout: number, 6 | ) { 7 | this.locks = new Map(); 8 | this.lockPromises = new Map(); 9 | } 10 | 11 | public set(key: T) { 12 | // if there is a lock set.....we don't set a second one ontop 13 | if (this.locks.has(key)) { 14 | return; 15 | } 16 | 17 | // set a dummy lock so that if we re-set again before releasing it won't do anthing 18 | this.locks.set(key, {i: null, r: null}); 19 | 20 | const p = new Promise((resolve) => { 21 | // first we check if the lock has the key....if not, e.g. if it 22 | // got released too quickly, we still want to resolve our promise 23 | if (!this.locks.has(key)) { 24 | resolve(); 25 | return; 26 | } 27 | // create the interval that will release our promise after the timeout 28 | const i = setTimeout(() => { 29 | this.release(key); 30 | }, this.timeout); 31 | // aaand store to our lock 32 | this.locks.set(key, {r: resolve, i}); 33 | }); 34 | this.lockPromises.set(key, p); 35 | } 36 | 37 | public release(key: T) { 38 | // if there is nothing to release then there is nothing to release 39 | if (!this.locks.has(key)) { 40 | return; 41 | } 42 | const lock = this.locks.get(key)!; 43 | if (lock.r !== null) { 44 | lock.r(); 45 | } 46 | if (lock.i !== null) { 47 | clearTimeout(lock.i); 48 | } 49 | this.locks.delete(key); 50 | this.lockPromises.delete(key); 51 | } 52 | 53 | public async wait(key: T) { 54 | // we wait for a lock release only if a promise is present 55 | const promise = this.lockPromises.get(key); 56 | if (promise) { 57 | await promise; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/structures/timedcache.ts: -------------------------------------------------------------------------------- 1 | import { Log } from "../log"; 2 | const log = new Log("Timedcache"); 3 | 4 | interface ITimedValue { 5 | value: V; 6 | ts: number; 7 | } 8 | 9 | export class TimedCache implements Map { 10 | private readonly map: Map>; 11 | 12 | public constructor(private readonly liveFor: number) { 13 | this.map = new Map(); 14 | } 15 | 16 | public clear(): void { 17 | this.map.clear(); 18 | } 19 | 20 | public delete(key: K): boolean { 21 | return this.map.delete(key); 22 | } 23 | 24 | public forEach(callbackfn: (value: V, key: K, map: Map) => void|Promise): void { 25 | for (const item of this) { 26 | const potentialPromise = callbackfn(item[1], item[0], this); 27 | if (potentialPromise) { 28 | potentialPromise.catch(log.warn); 29 | } 30 | } 31 | } 32 | 33 | public get(key: K): V | undefined { 34 | const v = this.map.get(key); 35 | if (v === undefined) { 36 | return; 37 | } 38 | const val = this.filterV(v); 39 | if (val !== undefined) { 40 | return val; 41 | } 42 | // Cleanup expired key 43 | this.map.delete(key); 44 | } 45 | 46 | public has(key: K): boolean { 47 | return this.get(key) !== undefined; 48 | } 49 | 50 | public set(key: K, value: V): this { 51 | this.map.set(key, { 52 | ts: Date.now(), 53 | value, 54 | }); 55 | return this; 56 | } 57 | 58 | public get size(): number { 59 | return this.map.size; 60 | } 61 | 62 | public [Symbol.iterator](): IterableIterator<[K, V]> { 63 | let iterator: IterableIterator<[K, ITimedValue]>; 64 | return { 65 | next: () => { 66 | if (!iterator) { 67 | iterator = this.map.entries(); 68 | } 69 | let item: IteratorResult<[K, ITimedValue]>|undefined; 70 | let filteredValue: V|undefined; 71 | // Loop if we have no item, or the item has expired. 72 | while (!item || filteredValue === undefined) { 73 | item = iterator.next(); 74 | // No more items in map. Bye bye. 75 | if (item.done) { 76 | break; 77 | } 78 | filteredValue = this.filterV(item.value[1]); 79 | } 80 | if (item.done) { 81 | // Typscript doesn't like us returning undefined for value, which is dumb. 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | return {done: true, value: undefined} as any as IteratorResult<[K, V]>; 84 | } 85 | return {done: false, value: [item.value[0], filteredValue]} as IteratorResult<[K, V]>; 86 | }, 87 | [Symbol.iterator]: () => this[Symbol.iterator](), 88 | }; 89 | } 90 | 91 | public entries(): IterableIterator<[K, V]> { 92 | return this[Symbol.iterator](); 93 | } 94 | 95 | public keys(): IterableIterator { 96 | throw new Error("Method not implemented."); 97 | } 98 | 99 | public values(): IterableIterator { 100 | throw new Error("Method not implemented."); 101 | } 102 | 103 | get [Symbol.toStringTag](): "Map" { 104 | return "Map"; 105 | } 106 | 107 | private filterV(v: ITimedValue): V|undefined { 108 | if (Date.now() - v.ts < this.liveFor) { 109 | return v.value; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { argv } from "process"; 18 | import { Log } from "../src/log"; 19 | import * as WhyRunning from "why-is-node-running"; 20 | 21 | if (!argv.includes("--noisy")) { 22 | Log.ForceSilent(); 23 | } 24 | 25 | after(() => { 26 | WhyRunning(); 27 | }); 28 | -------------------------------------------------------------------------------- /test/mocks/channel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {Permissions, PermissionResolvable, TextChannel} from "@mx-puppet/better-discord.js"; 18 | import {MockMember} from "./member"; 19 | import {MockCollection} from "./collection"; 20 | import { MockGuild } from "./guild"; 21 | 22 | // we are a test file and thus need those 23 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 24 | 25 | // Mocking TextChannel 26 | export class MockChannel { 27 | public members = new MockCollection(); 28 | constructor( 29 | public id: string = "", 30 | public guild: any = null, 31 | public type: string = "text", 32 | public name: string = "", 33 | public topic: string = "", 34 | ) { } 35 | 36 | public async send(data: any): Promise { 37 | return data; 38 | } 39 | 40 | public permissionsFor(member: MockMember) { 41 | return new Permissions(Permissions.FLAGS.MANAGE_WEBHOOKS as PermissionResolvable); 42 | } 43 | } 44 | 45 | export class MockTextChannel extends TextChannel { 46 | constructor(guild?: MockGuild, channelData: any = {}) { 47 | // Mock the nessacery 48 | super(guild || { 49 | client: { 50 | options: { 51 | messageCacheMaxSize: -1, 52 | }, 53 | }, 54 | } as any, channelData); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/mocks/collection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { Collection } from "@mx-puppet/better-discord.js"; 18 | 19 | export class MockCollection extends Collection { 20 | public array(): T2[] { 21 | return [...this.values()]; 22 | } 23 | 24 | public keyArray(): T1[] { 25 | return [...this.keys()]; 26 | } 27 | } 28 | 29 | export class MockCollectionManager { 30 | private innerCache = new MockCollection(); 31 | public get cache() { 32 | return this.innerCache; 33 | } 34 | 35 | public updateCache(c: MockCollection) { 36 | this.innerCache = c; 37 | } 38 | 39 | public resolve(id: T1) { 40 | return this.innerCache.get(id); 41 | } 42 | 43 | public async fetch(id: T1) { 44 | return this.innerCache.get(id); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/mocks/discordclient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {MockCollectionManager} from "./collection"; 18 | import {MockGuild} from "./guild"; 19 | import {MockUser} from "./user"; 20 | 21 | // we are a test file and thus need those 22 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 23 | 24 | export class MockDiscordClient { 25 | public guilds = new MockCollectionManager(); 26 | public user: MockUser; 27 | private testCallbacks: Map void> = new Map(); 28 | 29 | constructor() { 30 | const channels = [ 31 | { 32 | id: "321", 33 | name: "achannel", 34 | type: "text", 35 | }, 36 | { 37 | id: "654", 38 | name: "a-channel", 39 | type: "text", 40 | }, 41 | { 42 | id: "987", 43 | name: "a channel", 44 | type: "text", 45 | }, 46 | ]; 47 | this.guilds.cache.set("123", new MockGuild("MyGuild", channels)); 48 | this.guilds.cache.set("456", new MockGuild("My Spaces Gui", channels)); 49 | this.guilds.cache.set("789", new MockGuild("My Dash-Guild", channels)); 50 | this.user = new MockUser("12345"); 51 | } 52 | 53 | public on(event: string, callback: (...data: any[]) => void) { 54 | this.testCallbacks.set(event, callback); 55 | } 56 | 57 | public once(event: string, callback: (...data: any[]) => void) { 58 | this.testCallbacks.set(event, () => { 59 | this.testCallbacks.delete(event); 60 | callback(); 61 | }); 62 | } 63 | 64 | public async emit(event: string, ...data: any[]) { 65 | return await this.testCallbacks.get(event)!.apply(this, data); 66 | } 67 | 68 | public async login(token: string): Promise { 69 | if (token !== "passme") { 70 | throw new Error("Mock Discord Client only logins with the token 'passme'"); 71 | } 72 | if (this.testCallbacks.has("ready")) { 73 | this.testCallbacks.get("ready")!(); 74 | } 75 | if (this.testCallbacks.has("shardReady")) { 76 | this.testCallbacks.get("shardReady")!(); 77 | } 78 | return; 79 | } 80 | 81 | public async destroy() { } // no-op 82 | } 83 | -------------------------------------------------------------------------------- /test/mocks/discordclientfactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {MockDiscordClient} from "./discordclient"; 18 | 19 | // we are a test file and thus need those 20 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 21 | 22 | export class DiscordClientFactory { 23 | private botClient: MockDiscordClient; 24 | constructor(config: any, store: any) { } 25 | 26 | public async init(): Promise { } 27 | 28 | public async getClient(userId?: string): Promise { 29 | if (!userId && !this.botClient) { 30 | this.botClient = new MockDiscordClient(); 31 | } 32 | return this.botClient; 33 | } 34 | 35 | public bindMetricsToChannel() {} 36 | } 37 | -------------------------------------------------------------------------------- /test/mocks/emoji.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // we are a test file and thus need those 18 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 19 | 20 | export class MockEmoji { 21 | constructor(public id: string = "", public name = "", public animated = false) { } 22 | } 23 | -------------------------------------------------------------------------------- /test/mocks/guild.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import {Channel} from "@mx-puppet/better-discord.js"; 18 | import {MockCollectionManager} from "./collection"; 19 | import {MockMember} from "./member"; 20 | import {MockEmoji} from "./emoji"; 21 | import {MockRole} from "./role"; 22 | 23 | // we are a test file and thus need those 24 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 25 | 26 | export class MockGuild { 27 | public channels = new MockCollectionManager(); 28 | public members = new MockCollectionManager(); 29 | public emojis = new MockCollectionManager(); 30 | public roles = new MockCollectionManager(); 31 | public id: string; 32 | public name: string; 33 | public icon: string; 34 | constructor(id: string, channels: any[] = [], name: string = "") { 35 | this.id = id; 36 | this.name = name || id; 37 | channels.forEach((item) => { 38 | this.channels.cache.set(item.id, item); 39 | }); 40 | } 41 | 42 | public get client() { 43 | return { 44 | options: { 45 | messageCacheMaxSize: -1, 46 | }, 47 | }; 48 | } 49 | 50 | public async fetchMember(id: string): Promise { 51 | if (this.members.cache.has(id)) { 52 | return this.members.cache.get(id)!; 53 | } 54 | throw new Error("Member not in this guild"); 55 | } 56 | 57 | public _mockAddMember(member: MockMember) { 58 | this.members.cache.set(member.id, member); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/mocks/member.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as Discord from "@mx-puppet/better-discord.js"; 18 | import {MockCollectionManager} from "./collection"; 19 | import {MockUser} from "./user"; 20 | import {MockRole} from "./role"; 21 | 22 | // we are a test file and thus need those 23 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 24 | 25 | export class MockMember { 26 | public id = ""; 27 | public presence: Discord.Presence; 28 | public user: MockUser; 29 | public nickname: string; 30 | public roles = new MockCollectionManager(); 31 | constructor(id: string, username: string, public guild: any = null, public displayName: string = username) { 32 | this.id = id; 33 | this.presence = new Discord.Presence({} as any, { 34 | user: { 35 | id: this.id, 36 | }, 37 | }); 38 | this.user = new MockUser(this.id, username); 39 | this.nickname = displayName; 40 | } 41 | 42 | public MockSetPresence(presence: Discord.Presence) { 43 | this.presence = presence; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/mocks/message.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as Discord from "@mx-puppet/better-discord.js"; 18 | import { MockUser } from "./user"; 19 | import { MockCollection } from "./collection"; 20 | 21 | // we are a test file and thus need those 22 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 23 | 24 | export class MockMessage { 25 | public attachments = new MockCollection(); 26 | public embeds: any[] = []; 27 | public content = ""; 28 | public channel: Discord.TextChannel | undefined; 29 | public guild: Discord.Guild | undefined; 30 | public author: MockUser; 31 | public mentions: any = {}; 32 | constructor(channel?: Discord.TextChannel) { 33 | this.mentions.everyone = false; 34 | this.channel = channel; 35 | if (channel && channel.guild) { 36 | this.guild = channel.guild; 37 | } 38 | this.author = new MockUser("123456"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/mocks/presence.ts: -------------------------------------------------------------------------------- 1 | import { MockUser } from "./user"; 2 | 3 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 4 | export class MockPresence { 5 | constructor(public internalUser: MockUser, guild: string, public status?: string, public activities: any = []) { 6 | 7 | } 8 | 9 | public get user() { 10 | return this.internalUser; 11 | } 12 | 13 | public get userID() { 14 | return this.internalUser.id; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/role.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // we are a test file and thus need those 18 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 19 | export class MockRole { 20 | constructor(public id: string = "", public name = "", public color = 0, public position = 0) { } 21 | } 22 | -------------------------------------------------------------------------------- /test/mocks/user.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { Presence } from "@mx-puppet/better-discord.js"; 18 | 19 | // we are a test file and thus need those 20 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 21 | 22 | export class MockUser { 23 | public presence: Presence; 24 | constructor( 25 | public id: string, 26 | public username: string = "", 27 | public discriminator: string = "", 28 | public avatarUrl: string | null = "", 29 | public avatar: string | null = "", 30 | public bot: boolean = false, 31 | ) { } 32 | 33 | public avatarURL() { 34 | return this.avatarUrl; 35 | } 36 | 37 | public MockSetPresence(presence: Presence) { 38 | this.presence = presence; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/structures/test_lock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | import { Lock } from "../../src/structures/lock"; 19 | import { Util } from "../../src/util"; 20 | 21 | const LOCKTIMEOUT = 300; 22 | 23 | describe("Lock", () => { 24 | it("should lock and unlock", async () => { 25 | const lock = new Lock(LOCKTIMEOUT); 26 | const t = Date.now(); 27 | lock.set("bunny"); 28 | await lock.wait("bunny"); 29 | const diff = Date.now() - t; 30 | expect(diff).to.be.greaterThan(LOCKTIMEOUT - 1); 31 | }); 32 | it("should lock and unlock early, if unlocked", async () => { 33 | const SHORTDELAY = 100; 34 | const DELAY_ACCURACY = 5; 35 | const lock = new Lock(LOCKTIMEOUT); 36 | setTimeout(() => lock.release("fox"), SHORTDELAY); 37 | const t = Date.now(); 38 | lock.set("fox"); 39 | await lock.wait("fox"); 40 | const diff = Date.now() - t; 41 | // accuracy can be off by a few ms soemtimes 42 | expect(diff).to.be.greaterThan(SHORTDELAY - DELAY_ACCURACY); 43 | expect(diff).to.be.lessThan(SHORTDELAY + DELAY_ACCURACY); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/structures/test_timedcache.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | import { TimedCache } from "../../src/structures/timedcache"; 19 | import { Util } from "../../src/util"; 20 | 21 | // we are a test file and thus need those 22 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 23 | 24 | describe("TimedCache", () => { 25 | it("should construct", () => { 26 | const timedCache = new TimedCache(1000); 27 | expect(timedCache.size).to.equal(0); 28 | }); 29 | 30 | it("should add and get values", () => { 31 | const timedCache = new TimedCache(1000); 32 | timedCache.set("foo", 1); 33 | timedCache.set("bar", -1); 34 | timedCache.set("baz", 0); 35 | expect(timedCache.get("foo")).to.equal(1); 36 | expect(timedCache.get("bar")).to.equal(-1); 37 | expect(timedCache.get("baz")).to.equal(0); 38 | }); 39 | 40 | it("should be able to overwrite values", () => { 41 | const timedCache = new TimedCache(1000); 42 | timedCache.set("foo", 1); 43 | expect(timedCache.get("foo")).to.equal(1); 44 | timedCache.set("bar", 0); 45 | timedCache.set("foo", -1); 46 | expect(timedCache.get("bar")).to.equal(0); 47 | expect(timedCache.get("foo")).to.equal(-1); 48 | }); 49 | 50 | it("should be able to check if a value exists", () => { 51 | const timedCache = new TimedCache(1000); 52 | expect(timedCache.has("foo")).to.be.false; 53 | timedCache.set("foo", 1); 54 | expect(timedCache.has("foo")).to.be.true; 55 | timedCache.set("bar", 1); 56 | expect(timedCache.has("bar")).to.be.true; 57 | }); 58 | 59 | it("should be able to delete a value", () => { 60 | const timedCache = new TimedCache(1000); 61 | timedCache.set("foo", 1); 62 | expect(timedCache.has("foo")).to.be.true; 63 | timedCache.delete("foo"); 64 | expect(timedCache.has("foo")).to.be.false; 65 | expect(timedCache.get("foo")).to.be.undefined; 66 | }); 67 | 68 | it("should expire a value", async () => { 69 | const LIVE_FOR = 50; 70 | const timedCache = new TimedCache(LIVE_FOR); 71 | timedCache.set("foo", 1); 72 | expect(timedCache.has("foo")).to.be.true; 73 | expect(timedCache.get("foo")).to.equal(1); 74 | await Util.DelayedPromise(LIVE_FOR); 75 | expect(timedCache.has("foo")).to.be.false; 76 | expect(timedCache.get("foo")).to.be.undefined; 77 | }); 78 | 79 | it("should be able to iterate around a long-lasting collection", () => { 80 | const timedCache = new TimedCache(1000); 81 | timedCache.set("foo", 1); 82 | timedCache.set("bar", -1); 83 | timedCache.set("baz", 0); 84 | let i = 0; 85 | for (const iterator of timedCache) { 86 | if (i === 0) { 87 | expect(iterator[0]).to.equal("foo"); 88 | expect(iterator[1]).to.equal(1); 89 | } else if (i === 1) { 90 | expect(iterator[0]).to.equal("bar"); 91 | expect(iterator[1]).to.equal(-1); 92 | } else { 93 | expect(iterator[0]).to.equal("baz"); 94 | expect(iterator[1]).to.equal(0); 95 | } 96 | i++; 97 | } 98 | }); 99 | 100 | it("should be able to iterate around a short-term collection", async () => { 101 | const LIVE_FOR = 100; 102 | const timedCache = new TimedCache(LIVE_FOR); 103 | timedCache.set("foo", 1); 104 | timedCache.set("bar", -1); 105 | timedCache.set("baz", 0); 106 | let i = 0; 107 | for (const iterator of timedCache) { 108 | if (i === 0) { 109 | expect(iterator[0]).to.equal("foo"); 110 | expect(iterator[1]).to.equal(1); 111 | } else if (i === 1) { 112 | expect(iterator[0]).to.equal("bar"); 113 | expect(iterator[1]).to.equal(-1); 114 | } else { 115 | expect(iterator[0]).to.equal("baz"); 116 | expect(iterator[1]).to.equal(0); 117 | } 118 | i++; 119 | } 120 | await Util.DelayedPromise(LIVE_FOR * 5); 121 | const vals = [...timedCache.entries()]; 122 | expect(vals).to.be.empty; 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/test_clientfactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | import * as Proxyquire from "proxyquire"; 19 | import { DiscordBridgeConfigAuth } from "../src/config"; 20 | 21 | // we are a test file and thus need those 22 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 23 | 24 | const DiscordClientFactory = Proxyquire("../src/clientfactory", { 25 | "@mx-puppet/better-discord.js": { Client: require("./mocks/discordclient").MockDiscordClient }, 26 | }).DiscordClientFactory; 27 | 28 | const STORE = { 29 | getToken: async (discordid: string) => { 30 | if (discordid === "12345") { 31 | return "passme"; 32 | } else if (discordid === "1234555") { 33 | return "failme"; 34 | } 35 | throw new Error("Token not found"); 36 | }, 37 | getUserDiscordIds: async (userid: string) => { 38 | if (userid === "@valid:localhost") { 39 | return ["12345"]; 40 | } else if (userid === "@invalid:localhost") { 41 | return ["1234555"]; 42 | } 43 | return []; 44 | }, 45 | }; 46 | 47 | describe("ClientFactory", () => { 48 | describe("init", () => { 49 | it ("should start successfully", async () => { 50 | const config = new DiscordBridgeConfigAuth(); 51 | config.botToken = "passme"; 52 | const cf = new DiscordClientFactory(null, config); 53 | await cf.init(); 54 | }); 55 | it ("should fail if a config is not supplied", async () => { 56 | const cf = new DiscordClientFactory(null); 57 | try { 58 | await cf.init(); 59 | throw new Error("didn't fail"); 60 | } catch (e) { 61 | expect(e.message).to.not.equal("didn't fail"); 62 | } 63 | }); 64 | it ("should fail if the bot fails to connect", async () => { 65 | const config = new DiscordBridgeConfigAuth(); 66 | config.botToken = "failme"; 67 | const cf = new DiscordClientFactory(null, config); 68 | try { 69 | await cf.init(); 70 | throw new Error("didn't fail"); 71 | } catch (e) { 72 | expect(e.message).to.not.equal("didn't fail"); 73 | } 74 | }); 75 | }); 76 | describe("getDiscordId", () => { 77 | it("should fetch id successfully", async () => { 78 | const config = new DiscordBridgeConfigAuth(); 79 | const cf = new DiscordClientFactory(null, config); 80 | const discordId = await cf.getDiscordId("passme"); 81 | expect(discordId).equals("12345"); 82 | }); 83 | it("should fail if the token is not recognised", async () => { 84 | const config = new DiscordBridgeConfigAuth(); 85 | const cf = new DiscordClientFactory(null, config); 86 | try { 87 | await cf.getDiscordId("failme"); 88 | throw new Error("didn't fail"); 89 | } catch (e) { 90 | expect(e.message).to.not.equal("didn't fail"); 91 | } 92 | }); 93 | }); 94 | describe("getClient", () => { 95 | it("should fetch bot client successfully", async () => { 96 | const config = new DiscordBridgeConfigAuth(); 97 | config.botToken = "passme"; 98 | const cf = new DiscordClientFactory(null, config); 99 | cf.botClient = 1; 100 | const client = await cf.getClient(); 101 | expect(client).equals(cf.botClient); 102 | }); 103 | it("should return cached client", async () => { 104 | const config = new DiscordBridgeConfigAuth(); 105 | const cf = new DiscordClientFactory(null, config); 106 | cf.clients.set("@user:localhost", "testclient"); 107 | const client = await cf.getClient("@user:localhost"); 108 | expect(client).equals("testclient"); 109 | }); 110 | it("should fetch bot client if userid doesn't match", async () => { 111 | const config = new DiscordBridgeConfigAuth(); 112 | const cf = new DiscordClientFactory(STORE); 113 | cf.botClient = 1; 114 | const client = await cf.getClient("@user:localhost"); 115 | expect(client).equals(cf.botClient); 116 | }); 117 | it("should fetch user client if userid matches", async () => { 118 | const config = new DiscordBridgeConfigAuth(); 119 | const cf = new DiscordClientFactory(STORE, config); 120 | const client = await cf.getClient("@valid:localhost"); 121 | expect(client).is.not.null; 122 | expect(cf.clients.has("@valid:localhost")).to.be.true; 123 | }); 124 | it("should fail if the user client cannot log in", async () => { 125 | const config = new DiscordBridgeConfigAuth(); 126 | const cf = new DiscordClientFactory(STORE, config); 127 | cf.botClient = 1; 128 | const client = await cf.getClient("@invalid:localhost"); 129 | expect(client).to.equal(cf.botClient); 130 | expect(cf.clients.has("@invalid:localhost")).to.be.false; 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/test_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | import { DiscordBridgeConfig } from "../src/config"; 19 | 20 | // we are a test file and thus need those 21 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 22 | 23 | describe("DiscordBridgeConfig.applyConfig", () => { 24 | it("should merge configs correctly", () => { 25 | const config = new DiscordBridgeConfig(); 26 | config.applyConfig({ 27 | bridge: { 28 | disableDeletionForwarding: true, 29 | disableDiscordMentions: false, 30 | disableInviteNotifications: true, 31 | disableJoinLeaveNotifications: true, 32 | disableTypingNotifications: true, 33 | enableSelfServiceBridging: false, 34 | homeserverUrl: "blah", 35 | }, 36 | logging: { 37 | console: "warn", 38 | }, 39 | }); 40 | expect(config.bridge.homeserverUrl).to.equal("blah"); 41 | expect(config.bridge.disableTypingNotifications).to.be.true; 42 | expect(config.bridge.disableDiscordMentions).to.be.false; 43 | expect(config.bridge.disableDeletionForwarding).to.be.true; 44 | expect(config.bridge.enableSelfServiceBridging).to.be.false; 45 | expect(config.bridge.disableJoinLeaveNotifications).to.be.true; 46 | expect(config.bridge.disableInviteNotifications).to.be.true; 47 | expect(config.logging.console).to.equal("warn"); 48 | }); 49 | it("should merge environment overrides correctly", () => { 50 | const config = new DiscordBridgeConfig(); 51 | config.applyConfig({ 52 | bridge: { 53 | disableDeletionForwarding: true, 54 | disableDiscordMentions: false, 55 | homeserverUrl: "blah", 56 | }, 57 | logging: { 58 | console: "warn", 59 | }, 60 | }); 61 | config.applyEnvironmentOverrides({ 62 | APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false, 63 | APPSERVICE_DISCORD_BRIDGE_DISABLE_INVITE_NOTIFICATIONS: true, 64 | APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true, 65 | APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug", 66 | }); 67 | expect(config.bridge.disableJoinLeaveNotifications).to.be.true; 68 | expect(config.bridge.disableInviteNotifications).to.be.true; 69 | expect(config.bridge.disableDeletionForwarding).to.be.false; 70 | expect(config.bridge.disableDiscordMentions).to.be.false; 71 | expect(config.bridge.homeserverUrl).to.equal("blah"); 72 | expect(config.logging.console).to.equal("debug"); 73 | }); 74 | it("should merge logging.files correctly", () => { 75 | const config = new DiscordBridgeConfig(); 76 | config.applyConfig({ 77 | logging: { 78 | console: "silent", 79 | files: [ 80 | { 81 | file: "./bacon.log", 82 | }, 83 | ], 84 | }, 85 | }); 86 | expect(config.logging.files[0].file).to.equal("./bacon.log"); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/test_log.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | import * as Proxyquire from "proxyquire"; 19 | 20 | // we are a test file and thus need those 21 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 22 | 23 | let createdLogger: any = null; 24 | let loggedMessages: any[] = []; 25 | 26 | const WinstonMock = { 27 | createLogger: (format, transports) => { 28 | return createdLogger = { 29 | close: (): void => { }, 30 | format, 31 | log: (type, ...msg) => { 32 | loggedMessages = loggedMessages.concat(msg); 33 | }, 34 | silent: false, 35 | transports, 36 | }; 37 | }, 38 | }; 39 | 40 | const Log = (Proxyquire("../src/log", { 41 | winston: WinstonMock, 42 | }).Log); 43 | 44 | describe("Log", () => { 45 | 46 | beforeEach(() => { 47 | loggedMessages = []; 48 | }); 49 | 50 | describe("Configure", () => { 51 | it("should pass if config is empty", () => { 52 | Log.Configure({}); 53 | }); 54 | it("should set basic log options", () => { 55 | Log.Configure({ 56 | console: "warn", 57 | lineDateFormat: "HH:mm:ss", 58 | }); 59 | expect(Log.config.console).to.equal("warn"); 60 | expect(Log.config.lineDateFormat).to.equal("HH:mm:ss"); 61 | expect(Log.config.files).to.be.empty; 62 | }); 63 | it("should setup file logging", () => { 64 | Log.Configure({ 65 | files: [ 66 | { 67 | file: "./logfile.log", 68 | }, 69 | ], 70 | }); 71 | expect(Log.config.files).to.not.be.empty; 72 | expect(Log.config.files[0].file).to.equal("./logfile.log"); 73 | }); 74 | it("should warn if log level got misspelled", () => { 75 | Log.Configure({ 76 | console: "WARNING", 77 | lineDateFormat: "HH:mm:ss", 78 | }); 79 | expect(loggedMessages).to.contain("Console log level is invalid. Please pick one of the case-sensitive levels provided in the sample config."); 80 | }); 81 | it("should warn if log level for a file got misspelled", () => { 82 | Log.Configure({ 83 | files: [ 84 | { 85 | file: "./logfile.log", 86 | level: "WARNING", 87 | }, 88 | ], 89 | }); 90 | expect(loggedMessages).to.contain("Log level of ./logfile.log is invalid. Please pick one of the case-sensitive levels provided in the sample config."); 91 | }); 92 | }); 93 | describe("ForceSilent", () => { 94 | it("should be silent", () => { 95 | Log.ForceSilent(); 96 | expect(createdLogger.silent).to.be.true; 97 | expect(loggedMessages).to.contain("Log set to silent"); 98 | }); 99 | }); 100 | describe("instance", () => { 101 | it("should log without configuring", () => { 102 | new Log("test").info("hi"); 103 | expect(loggedMessages).to.contain("hi"); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/test_presencehandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | 19 | import { PresenceHandler } from "../src/presencehandler"; 20 | import { DiscordBot } from "../src/bot"; 21 | import { MockUser } from "./mocks/user"; 22 | import { AppserviceMock } from "./mocks/appservicemock"; 23 | import { MockPresence } from "./mocks/presence"; 24 | 25 | // we are a test file and thus need those 26 | /* tslint:disable:no-unused-expression max-file-line-count no-any */ 27 | 28 | const INTERVAL = 250; 29 | let lastStatus = null; 30 | const appservice = new AppserviceMock(); 31 | const bot: any = { 32 | GetBotId: () => { 33 | return "1234"; 34 | }, 35 | GetIntentFromDiscordMember: (member: MockUser) => { 36 | return appservice.getIntentForSuffix(member.id); 37 | }, 38 | }; 39 | 40 | describe("PresenceHandler", () => { 41 | describe("init", () => { 42 | it("constructor", () => { 43 | new PresenceHandler(bot as DiscordBot); 44 | }); 45 | }); 46 | describe("Stop", () => { 47 | it("should start and stop without errors", async () => { 48 | const handler = new PresenceHandler(bot as DiscordBot); 49 | await handler.Start(INTERVAL); 50 | handler.Stop(); 51 | }); 52 | }); 53 | describe("EnqueueUser", () => { 54 | it("adds a user properly", () => { 55 | const handler = new PresenceHandler(bot as DiscordBot); 56 | const COUNT = 2; 57 | handler.EnqueueUser(new MockPresence(new MockUser("abc", "alice"), "def") as any); 58 | handler.EnqueueUser(new MockPresence(new MockUser("123", "bob"), "ghi") as any); 59 | expect(handler.QueueCount).to.be.equal(COUNT); 60 | }); 61 | it("does not add duplicate users", () => { 62 | const handler = new PresenceHandler(bot as DiscordBot); 63 | handler.EnqueueUser(new MockPresence(new MockUser("123", "alice"), "def") as any); 64 | handler.EnqueueUser(new MockPresence(new MockUser("123", "alice"), "def") as any); 65 | expect(handler.QueueCount).to.be.equal(1); 66 | }); 67 | it("does not add the bot user", () => { 68 | const handler = new PresenceHandler(bot as DiscordBot); 69 | handler.EnqueueUser(new MockPresence(new MockUser("1234", "bob"), "ghi") as any); 70 | expect(handler.QueueCount).to.be.equal(0); 71 | }); 72 | }); 73 | describe("DequeueUser", () => { 74 | it("removes users properly", () => { 75 | const handler = new PresenceHandler(bot as DiscordBot); 76 | const members = [ 77 | new MockPresence(new MockUser("abc", "alice"), "def") as any, 78 | new MockPresence(new MockUser("def", "bob"), "ghi") as any, 79 | new MockPresence(new MockUser("ghi", "foo"), "wew") as any, 80 | ]; 81 | handler.EnqueueUser(members[0]); 82 | handler.EnqueueUser(members[1]); 83 | handler.EnqueueUser(members[2]); 84 | 85 | handler.DequeueUser(members[2].user); 86 | expect(handler.QueueCount).to.be.equal(members.length - 1); 87 | handler.DequeueUser(members[1].user); 88 | expect(handler.QueueCount).to.be.equal(1); 89 | handler.DequeueUser(members[0].user); 90 | expect(handler.QueueCount).to.be.equal(0); 91 | }); 92 | }); 93 | describe("ProcessUser", () => { 94 | it("processes an online user", async () => { 95 | lastStatus = null; 96 | const handler = new PresenceHandler(bot as DiscordBot); 97 | const member = new MockPresence(new MockUser("ghi", "alice"), "def", "online"); 98 | await handler.ProcessUser(member as any); 99 | appservice.getIntentForSuffix(member.userID) 100 | .underlyingClient.wasCalled("setPresenceStatus", true, "online", ""); 101 | }); 102 | it("processes an offline user", async () => { 103 | lastStatus = null; 104 | const handler = new PresenceHandler(bot as DiscordBot); 105 | const member = new MockPresence(new MockUser("abc", "alice"), "def", "offline"); 106 | await handler.ProcessUser(member as any); 107 | appservice.getIntentForSuffix(member.userID) 108 | .underlyingClient.wasCalled("setPresenceStatus", true, "offline", ""); 109 | }); 110 | it("processes an idle user", async () => { 111 | lastStatus = null; 112 | const handler = new PresenceHandler(bot as DiscordBot); 113 | const member = new MockPresence(new MockUser("abc", "alice"), "def", "idle"); 114 | await handler.ProcessUser(member as any); 115 | appservice.getIntentForSuffix(member.userID) 116 | .underlyingClient.wasCalled("setPresenceStatus", true, "unavailable", ""); 117 | }); 118 | it("processes an dnd user", async () => { 119 | lastStatus = null; 120 | const handler = new PresenceHandler(bot as DiscordBot); 121 | const member = new MockPresence(new MockUser("abc", "alice"), "def", "dnd"); 122 | await handler.ProcessUser(member as any); 123 | appservice.getIntentForSuffix(member.userID) 124 | .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Do not disturb"); 125 | const member2 = new MockPresence(new MockUser("abc", "alice"), "def", "dnd", [{name: "Test Game", type: "PLAYING"}]); 126 | await handler.ProcessUser(member2 as any); 127 | appservice.getIntentForSuffix(member.userID) 128 | .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Do not disturb | Playing Test Game"); 129 | }); 130 | it("processes a user playing games", async () => { 131 | lastStatus = null; 132 | const handler = new PresenceHandler(bot as DiscordBot); 133 | const member = new MockPresence(new MockUser("abc", "alice"), "def", "online", [{name: "Test Game", type: "PLAYING"}]); 134 | await handler.ProcessUser(member as any); 135 | appservice.getIntentForSuffix(member.userID) 136 | .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Playing Test Game"); 137 | const member2 = new MockPresence(new MockUser("abc", "alice"), "def", "online", [{name: "Test Game", type: "STREAMING"}]); 138 | await handler.ProcessUser(member2 as any); 139 | appservice.getIntentForSuffix(member.userID) 140 | .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Streaming Test Game"); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/test_provisioner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { expect } from "chai"; 18 | import { Provisioner } from "../src/provisioner"; 19 | import { MockChannel } from "./mocks/channel"; 20 | import { MockMember } from "./mocks/member"; 21 | 22 | // we are a test file and thus need those 23 | /* tslint:disable:no-any */ 24 | 25 | const TIMEOUT_MS = 1000; 26 | 27 | describe("Provisioner", () => { 28 | describe("AskBridgePermission", () => { 29 | it("should fail to bridge a room that timed out", async () => { 30 | const p = new Provisioner({} as any, {} as any); 31 | const startAt = Date.now(); 32 | try { 33 | await p.AskBridgePermission( 34 | new MockChannel("foo", "bar") as any, 35 | "Mark", 36 | TIMEOUT_MS, 37 | ); 38 | throw Error("Should have thrown an error"); 39 | } catch (err) { 40 | expect(err.message).to.eq("Timed out waiting for a response from the Discord owners."); 41 | const delay = Date.now() - startAt; 42 | if (delay < TIMEOUT_MS) { 43 | throw Error(`Should have waited for timeout before resolving, waited: ${delay}ms`); 44 | } 45 | } 46 | }); 47 | it("should fail to bridge a room that was declined", async () => { 48 | const p = new Provisioner({} as any, {} as any); 49 | const promise = p.AskBridgePermission( 50 | new MockChannel("foo", "bar") as any, 51 | "Mark", 52 | TIMEOUT_MS, 53 | ); 54 | await p.MarkApproved(new MockChannel("foo", "bar") as any, new MockMember("abc", "Mark") as any, false); 55 | try { 56 | await promise; 57 | throw Error("Should have thrown an error"); 58 | } catch (err) { 59 | expect(err.message).to.eq("The bridge has been declined by the Discord guild."); 60 | } 61 | 62 | }); 63 | it("should bridge a room that was approved", async () => { 64 | const p = new Provisioner({} as any, {} as any); 65 | const promise = p.AskBridgePermission( 66 | new MockChannel("foo", "bar") as any, 67 | "Mark", 68 | TIMEOUT_MS, 69 | ); 70 | await p.MarkApproved(new MockChannel("foo", "bar") as any, new MockMember("abc", "Mark") as any, true); 71 | expect(await promise).to.eq("Approved"); 72 | }); 73 | }); 74 | describe("RoomCountLimitReached", () => { 75 | it("should return false if no limit is defined", async () => { 76 | const p = new Provisioner({ 77 | countEntries: async () => 7, 78 | } as any, {} as any); 79 | expect(await p.RoomCountLimitReached(-1)).to.equal(false); 80 | }); 81 | it("should return false if less rooms exist than the limit", async () => { 82 | const p = new Provisioner({ 83 | countEntries: async () => 7, 84 | } as any, {} as any); 85 | expect(await p.RoomCountLimitReached(10)).to.equal(false); 86 | }); 87 | it("should return true if more rooms exist than the limit", async () => { 88 | const p = new Provisioner({ 89 | countEntries: async () => 7, 90 | } as any, {} as any); 91 | expect(await p.RoomCountLimitReached(5)).to.equal(true); 92 | }); 93 | it("should return true if there are as many rooms as the limit allows", async () => { 94 | const p = new Provisioner({ 95 | countEntries: async () => 7, 96 | } as any, {} as any); 97 | expect(await p.RoomCountLimitReached(7)).to.equal(true); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /tools/addRoomsToDirectory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-console */ 18 | /** 19 | * Allows you to become an admin for a room the bot is in control of. 20 | */ 21 | 22 | import * as args from "command-line-args"; 23 | import * as usage from "command-line-usage"; 24 | import { Log } from "../src/log"; 25 | import { Util } from "../src/util"; 26 | import { ToolsHelper } from "./toolshelper"; 27 | const log = new Log("AddRoomsToDirectory"); 28 | const optionDefinitions = [ 29 | { 30 | alias: "h", 31 | description: "Display this usage guide.", 32 | name: "help", 33 | type: Boolean, 34 | }, 35 | { 36 | alias: "c", 37 | defaultValue: "config.yaml", 38 | description: "The AS config file.", 39 | name: "config", 40 | type: String, 41 | typeLabel: "", 42 | }, 43 | { 44 | alias: "r", 45 | defaultValue: "discord-registration.yaml", 46 | description: "The AS registration file.", 47 | name: "registration", 48 | type: String, 49 | typeLabel: "", 50 | }, 51 | ]; 52 | 53 | const options = args(optionDefinitions); 54 | 55 | if (options.help) { 56 | /* eslint-disable no-console */ 57 | console.log(usage([ 58 | { 59 | content: "A tool to set all the bridged rooms to visible in the directory.", 60 | header: "Add rooms to directory", 61 | }, 62 | { 63 | header: "Options", 64 | optionList: optionDefinitions, 65 | }, 66 | ])); 67 | process.exit(0); 68 | } 69 | 70 | const {store, appservice} = ToolsHelper.getToolDependencies(options.config, options.registration, true); 71 | 72 | async function run(): Promise { 73 | try { 74 | await store!.init(); 75 | } catch (e) { 76 | log.error(`Failed to load database`, e); 77 | } 78 | let rooms = await store!.roomStore.getEntriesByRemoteRoomData({ 79 | /* eslint-disable @typescript-eslint/naming-convention */ 80 | discord_type: "text", 81 | /* eslint-disable @typescript-eslint/naming-convention */ 82 | }); 83 | rooms = rooms.filter((r) => r.remote && r.remote.get("plumbed") !== true ); 84 | log.info(`Got ${rooms.length} rooms to set`); 85 | try { 86 | await Util.AsyncForEach(rooms, async (room) => { 87 | const guild = room.remote!.get("discord_guild"); 88 | const roomId = room.matrix!.getId(); 89 | try { 90 | await appservice.botIntent.underlyingClient.setDirectoryVisibility( 91 | roomId, 92 | "public", 93 | ); 94 | log.info(`Set ${roomId} to visible in ${guild}'s directory`); 95 | } catch (e) { 96 | log.error(`Failed to set ${roomId} to visible in ${guild}'s directory`, e); 97 | } 98 | }); 99 | } catch (e) { 100 | log.error(`Failed to run script`, e); 101 | } 102 | } 103 | 104 | run(); // tslint:disable-line no-floating-promises 105 | -------------------------------------------------------------------------------- /tools/addbot.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-bitwise, no-console */ 18 | /** 19 | * Generates a URL you can use to authorize a bot with a guild. 20 | */ 21 | import * as yaml from "js-yaml"; 22 | import * as fs from "fs"; 23 | import * as args from "command-line-args"; 24 | import * as usage from "command-line-usage"; 25 | import { Util } from "../src/util"; 26 | import { DiscordBridgeConfig } from "../src/config"; 27 | 28 | const optionDefinitions = [ 29 | { 30 | alias: "h", 31 | description: "Display this usage guide.", 32 | name: "help", 33 | type: Boolean, 34 | }, 35 | { 36 | alias: "c", 37 | defaultValue: "config.yaml", 38 | description: "The AS config file.", 39 | name: "config", 40 | type: String, 41 | typeLabel: "", 42 | }, 43 | ]; 44 | 45 | const options = args(optionDefinitions); 46 | 47 | if (options.help) { 48 | // eslint-disable-next-line no-console 49 | console.log(usage([ 50 | { 51 | content: "A tool to obtain the Discord bot invitation URL.", 52 | header: "Add bot", 53 | }, 54 | { 55 | header: "Options", 56 | optionList: optionDefinitions, 57 | }, 58 | ])); 59 | process.exit(0); 60 | } 61 | 62 | const yamlConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")); 63 | if (yamlConfig === null || typeof yamlConfig !== "object") { 64 | throw Error("You have an error in your discord config."); 65 | } 66 | const url = Util.GetBotLink(yamlConfig as DiscordBridgeConfig); 67 | console.log(`Go to ${url} to invite the bot into a guild.`); 68 | -------------------------------------------------------------------------------- /tools/adminme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-console */ 18 | /** 19 | * Allows you to become an admin for a room that the bot is in control of. 20 | */ 21 | 22 | import * as args from "command-line-args"; 23 | import * as usage from "command-line-usage"; 24 | import { ToolsHelper } from "./toolshelper"; 25 | 26 | const optionDefinitions = [ 27 | { 28 | alias: "h", 29 | description: "Display this usage guide.", 30 | name: "help", 31 | type: Boolean, 32 | }, 33 | { 34 | alias: "c", 35 | defaultValue: "config.yaml", 36 | description: "The AS config file.", 37 | name: "config", 38 | type: String, 39 | typeLabel: "", 40 | }, 41 | { 42 | alias: "r", 43 | defaultValue: "discord-registration.yaml", 44 | description: "The AS registration file.", 45 | name: "registration", 46 | type: String, 47 | typeLabel: "", 48 | }, 49 | { 50 | alias: "m", 51 | description: "The roomid to modify", 52 | name: "roomid", 53 | type: String, 54 | }, 55 | { 56 | alias: "u", 57 | description: "The userid to give powers", 58 | name: "userid", 59 | type: String, 60 | }, 61 | { 62 | alias: "p", 63 | defaultValue: 100, 64 | description: "The power to set", 65 | name: "power", 66 | type: Number, 67 | typeLabel: "<0-100>", 68 | }, 69 | ]; 70 | 71 | const options = args(optionDefinitions); 72 | 73 | if (options.help) { 74 | /* eslint-disable no-console */ 75 | console.log(usage([ 76 | { 77 | content: "A tool to give a user a power level in a bot user controlled room.", 78 | header: "Admin Me", 79 | }, 80 | { 81 | header: "Options", 82 | optionList: optionDefinitions, 83 | }, 84 | ])); 85 | process.exit(0); 86 | } 87 | 88 | if (!options.roomid) { 89 | console.error("Missing roomid parameter. Check -h"); 90 | process.exit(1); 91 | } 92 | 93 | if (!options.userid) { 94 | console.error("Missing userid parameter. Check -h"); 95 | process.exit(1); 96 | } 97 | 98 | const {appservice} = ToolsHelper.getToolDependencies(options.config, options.registration, false); 99 | 100 | async function run() { 101 | try { 102 | const powerLevels = (await appservice.botIntent.underlyingClient.getRoomStateEvent( 103 | options.roomid, "m.room.power_levels", "", 104 | )); 105 | powerLevels.users[options.userid] = options.power; 106 | 107 | await appservice.botIntent.underlyingClient.sendStateEvent( 108 | options.roomid, "m.room.power_levels", "", powerLevels, 109 | ); 110 | console.log("Power levels set"); 111 | process.exit(0); 112 | } catch (err) { 113 | console.error("Could not apply power levels to room:", err); 114 | process.exit(1); 115 | } 116 | } 117 | 118 | run(); // tslint:disable-line no-floating-promises 119 | -------------------------------------------------------------------------------- /tools/chanfix.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as args from "command-line-args"; 18 | import * as usage from "command-line-usage"; 19 | import { DiscordBot } from "../src/bot"; 20 | import { Log } from "../src/log"; 21 | import { Util } from "../src/util"; 22 | import { ToolsHelper } from "./toolshelper"; 23 | 24 | const log = new Log("ChanFix"); 25 | 26 | const optionDefinitions = [ 27 | { 28 | alias: "h", 29 | description: "Display this usage guide.", 30 | name: "help", 31 | type: Boolean, 32 | }, 33 | { 34 | alias: "c", 35 | defaultValue: "config.yaml", 36 | description: "The AS config file.", 37 | name: "config", 38 | type: String, 39 | typeLabel: "", 40 | }, 41 | { 42 | alias: "r", 43 | defaultValue: "discord-registration.yaml", 44 | description: "The AS registration file.", 45 | name: "registration", 46 | type: String, 47 | typeLabel: "", 48 | }, 49 | ]; 50 | 51 | const options = args(optionDefinitions); 52 | 53 | if (options.help) { 54 | /* eslint-disable no-console */ 55 | console.log(usage([ 56 | { 57 | content: "A tool to fix channels of rooms already bridged " + 58 | "to matrix, to make sure their names, icons etc. are correctly.", 59 | header: "Fix bridged channels", 60 | }, 61 | { 62 | header: "Options", 63 | optionList: optionDefinitions, 64 | }, 65 | ])); 66 | process.exit(0); 67 | } 68 | 69 | async function run() { 70 | const {store, appservice, config} = ToolsHelper.getToolDependencies(options.config, options.registration); 71 | await store!.init(); 72 | const discordbot = new DiscordBot(config, appservice, store!); 73 | await discordbot.init(); 74 | await discordbot.ClientFactory.init(); 75 | const client = await discordbot.ClientFactory.getClient(); 76 | 77 | // first set update_icon to true if needed 78 | const mxRoomEntries = await store!.roomStore.getEntriesByRemoteRoomData({ 79 | update_name: true, 80 | update_topic: true, 81 | }); 82 | 83 | const promiseList: Promise[] = []; 84 | mxRoomEntries.forEach((entry) => { 85 | if (entry.remote!.get("plumbed")) { 86 | return; // skipping plumbed rooms 87 | } 88 | const updateIcon = entry.remote!.get("update_icon"); 89 | if (updateIcon !== undefined && updateIcon !== null) { 90 | return; // skipping because something was set manually 91 | } 92 | entry.remote!.set("update_icon", true); 93 | promiseList.push(store!.roomStore.upsertEntry(entry)); 94 | }); 95 | await Promise.all(promiseList); 96 | 97 | // now it is time to actually run the updates 98 | const promiseList2: Promise[] = []; 99 | 100 | let curDelay = config.limits.roomGhostJoinDelay; // we'll just re-use this 101 | client.guilds.cache.forEach((guild) => { 102 | promiseList2.push((async () => { 103 | await Util.DelayedPromise(curDelay); 104 | try { 105 | await discordbot.ChannelSyncroniser.OnGuildUpdate(guild, true); 106 | } catch (err) { 107 | log.warn(`Couldn't update rooms of guild ${guild.id}`, err); 108 | } 109 | })()); 110 | curDelay += config.limits.roomGhostJoinDelay; 111 | }); 112 | try { 113 | await Promise.all(promiseList2); 114 | } catch (err) { 115 | log.error(err); 116 | } 117 | process.exit(0); 118 | } 119 | 120 | run(); // tslint:disable-line no-floating-promises 121 | -------------------------------------------------------------------------------- /tools/ghostfix.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as args from "command-line-args"; 18 | import * as usage from "command-line-usage"; 19 | import { Log } from "../src/log"; 20 | import { Util } from "../src/util"; 21 | import { DiscordBot } from "../src/bot"; 22 | import { ToolsHelper } from "./toolshelper"; 23 | 24 | const log = new Log("GhostFix"); 25 | 26 | // Note: The schedule must not have duplicate values to avoid problems in positioning. 27 | const JOIN_ROOM_SCHEDULE = [ 28 | 0, // Right away 29 | 1000, // 1 second 30 | 30000, // 30 seconds 31 | 300000, // 5 minutes 32 | 900000, // 15 minutes 33 | ]; 34 | 35 | const optionDefinitions = [ 36 | { 37 | alias: "h", 38 | description: "Display this usage guide.", 39 | name: "help", 40 | type: Boolean, 41 | }, 42 | { 43 | alias: "c", 44 | defaultValue: "config.yaml", 45 | description: "The AS config file.", 46 | name: "config", 47 | type: String, 48 | typeLabel: "", 49 | }, 50 | { 51 | alias: "r", 52 | defaultValue: "discord-registration.yaml", 53 | description: "The AS registration file.", 54 | name: "registration", 55 | type: String, 56 | typeLabel: "", 57 | }, 58 | ]; 59 | 60 | const options = args(optionDefinitions); 61 | 62 | if (options.help) { 63 | /* eslint-disable no-console */ 64 | console.log(usage([ 65 | { 66 | content: "A tool to fix usernames of ghosts already in " + 67 | "matrix rooms, to make sure they represent the correct discord usernames.", 68 | header: "Fix usernames of joined ghosts", 69 | }, 70 | { 71 | header: "Options", 72 | optionList: optionDefinitions, 73 | }, 74 | ])); 75 | process.exit(0); 76 | } 77 | 78 | async function run() { 79 | const {appservice, config, store} = ToolsHelper.getToolDependencies(options.config, options.registration); 80 | await store!.init(); 81 | const discordbot = new DiscordBot(config, appservice, store!); 82 | await discordbot.init(); 83 | const client = await discordbot.ClientFactory.getClient(); 84 | 85 | const promiseList: Promise[] = []; 86 | let curDelay = config.limits.roomGhostJoinDelay; 87 | try { 88 | client.guilds.cache.forEach((guild) => { 89 | guild.members.cache.forEach((member) => { 90 | if (member.id === client.user?.id) { 91 | return; 92 | } 93 | promiseList.push((async () => { 94 | await Util.DelayedPromise(curDelay); 95 | let currentSchedule = JOIN_ROOM_SCHEDULE[0]; 96 | const doJoin = async () => { 97 | await Util.DelayedPromise(currentSchedule); 98 | await discordbot.UserSyncroniser.OnUpdateGuildMember(member, true, false); 99 | }; 100 | const errorHandler = async (err) => { 101 | log.error(`Error joining rooms for ${member.id}`); 102 | log.error(err); 103 | const idx = JOIN_ROOM_SCHEDULE.indexOf(currentSchedule); 104 | if (idx === JOIN_ROOM_SCHEDULE.length - 1) { 105 | log.warn(`Cannot join rooms for ${member.id}`); 106 | throw new Error(err); 107 | } else { 108 | currentSchedule = JOIN_ROOM_SCHEDULE[idx + 1]; 109 | try { 110 | await doJoin(); 111 | } catch (e) { 112 | await errorHandler(e); 113 | } 114 | } 115 | }; 116 | try { 117 | await doJoin(); 118 | } catch (e) { 119 | await errorHandler(e); 120 | } 121 | })()); 122 | curDelay += config.limits.roomGhostJoinDelay; 123 | }); 124 | }); 125 | 126 | await Promise.all(promiseList); 127 | } catch (err) { 128 | log.error(err); 129 | } 130 | process.exit(0); 131 | } 132 | 133 | run(); // eslint-disable no-floating-promises 134 | -------------------------------------------------------------------------------- /tools/toolshelper.ts: -------------------------------------------------------------------------------- 1 | import { DiscordBridgeConfig } from "../src/config"; 2 | import { Appservice, IAppserviceRegistration } from "matrix-bot-sdk"; 3 | import { DiscordStore } from "../src/store"; 4 | import * as yaml from "js-yaml"; 5 | import * as fs from "fs"; 6 | 7 | export class ToolsHelper { 8 | public static getToolDependencies( 9 | configFile: string, regFile: string = "./discord-registration.yaml", needsStore: boolean = true): { 10 | store: DiscordStore|null, 11 | appservice: Appservice, 12 | config: DiscordBridgeConfig, 13 | } { 14 | const registration = yaml.safeLoad(fs.readFileSync(regFile, "utf8")); 15 | const config: DiscordBridgeConfig = Object.assign( 16 | new DiscordBridgeConfig(), yaml.safeLoad(fs.readFileSync(configFile, "utf8"))); 17 | config.applyEnvironmentOverrides(process.env); 18 | if (registration === null || typeof registration !== "object") { 19 | throw Error("Failed to parse registration file"); 20 | } 21 | 22 | const appservice = new Appservice({ 23 | bindAddress: "notathing", 24 | homeserverName: config.bridge.domain, 25 | homeserverUrl: config.bridge.homeserverUrl, 26 | port: 0, 27 | // We assume the registration is well formed 28 | registration: registration as IAppserviceRegistration, 29 | }); 30 | 31 | const store = needsStore ? new DiscordStore(config.database) : null; 32 | return { 33 | appservice, 34 | config, 35 | store, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tools/userClientTools.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017, 2018 matrix-appservice-discord 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as yaml from "js-yaml"; 18 | import * as fs from "fs"; 19 | import * as args from "command-line-args"; 20 | import * as usage from "command-line-usage"; 21 | import * as readline from "readline"; 22 | import * as process from "process"; 23 | 24 | import { DiscordClientFactory } from "../src/clientfactory"; 25 | import { DiscordBridgeConfig } from "../src/config"; 26 | import { DiscordStore } from "../src/store"; 27 | import { Log } from "../src/log"; 28 | const log = new Log("UserClientTools"); 29 | const PUPPETING_DOC_URL = "https://github.com/Half-Shot/matrix-appservice-discord/blob/develop/docs/puppeting.md"; 30 | 31 | const optionDefinitions = [ 32 | { 33 | alias: "h", 34 | description: "Display this usage guide.", 35 | name: "help", 36 | type: Boolean, 37 | }, 38 | { 39 | alias: "c", 40 | defaultValue: "config.yaml", 41 | description: "The AS config file.", 42 | name: "config", 43 | type: String, 44 | typeLabel: "", 45 | }, 46 | { 47 | description: "Add the user to the database.", 48 | name: "add", 49 | type: Boolean, 50 | }, 51 | { 52 | description: "Remove the user from the database.", 53 | name: "remove", 54 | type: Boolean, 55 | }, 56 | ]; 57 | 58 | const options = args(optionDefinitions); 59 | if (options.help || (options.add && options.remove) || !(options.add || options.remove)) { 60 | /* eslint-disable no-console */ 61 | console.log(usage([ 62 | { 63 | content: "A tool to give a user a power level in a bot user controlled room.", 64 | header: "User Client Tools", 65 | }, 66 | { 67 | header: "Options", 68 | optionList: optionDefinitions, 69 | }, 70 | ])); 71 | process.exit(0); 72 | } 73 | 74 | const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig; 75 | const discordstore = new DiscordStore(config.database ? config.database : "discord.db"); 76 | discordstore.init().then(() => { 77 | log.info("Loaded database."); 78 | handleUI(); 79 | }).catch((err) => { 80 | log.info("Couldn't load database. Cannot continue.", err); 81 | log.info("Ensure the bridge is not running while using this command."); 82 | process.exit(1); 83 | }); 84 | 85 | function handleUI() { 86 | const rl = readline.createInterface({ 87 | input: process.stdin, 88 | output: process.stdout, 89 | }); 90 | let userid = ""; 91 | let token = ""; 92 | 93 | rl.question("Please enter your UserID ( ex @Half-Shot:half-shot.uk, @username:matrix.org)", (answeru) => { 94 | userid = answeru; 95 | if (options.add) { 96 | rl.question(` 97 | Please enter your Discord Token 98 | (Instructions for this are on ${PUPPETING_DOC_URL})`, (answert) => { 99 | token = answert; 100 | rl.close(); 101 | addUserToken(userid, token).then(() => { 102 | log.info("Completed successfully"); 103 | process.exit(0); 104 | }).catch((err) => { 105 | log.info("Failed to add, $s", err); 106 | process.exit(1); 107 | }); 108 | }); 109 | } else if (options.remove) { 110 | rl.close(); 111 | discordstore.deleteUserToken(userid).then(() => { 112 | log.info("Completed successfully"); 113 | process.exit(0); 114 | }).catch((err) => { 115 | log.info("Failed to delete, $s", err); 116 | process.exit(1); 117 | }); 118 | } 119 | }); 120 | } 121 | 122 | async function addUserToken(userid: string, token: string): Promise { 123 | const clientFactory = new DiscordClientFactory(discordstore); 124 | const discordid = await clientFactory.getDiscordId(token); 125 | await discordstore.addUserToken(userid, discordid, token); 126 | } 127 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2016", 6 | "noImplicitAny": false, 7 | "inlineSourceMap": true, 8 | "outDir": "./build", 9 | "types": ["mocha", "node"], 10 | "strictNullChecks": true, 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, /* matrix-bot-sdk throws up errors */ 13 | }, 14 | "compileOnSave": true, 15 | "include": [ 16 | "src/**/*", 17 | "test/**/*", 18 | "tools/**/*", 19 | ] 20 | } 21 | --------------------------------------------------------------------------------