Merge branch 'release/1.0.0'
This commit is contained in:
commit
20a7ed9702
77 changed files with 9926 additions and 6691 deletions
6
.mocharc.yml
Normal file
6
.mocharc.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
reporter: list
|
||||
ui: bdd
|
||||
require:
|
||||
- "ts-node/register"
|
||||
- "source-map-support/register"
|
||||
recursive: true
|
19
.travis.yml
19
.travis.yml
|
@ -1,6 +1,6 @@
|
|||
dist: xenial
|
||||
language: node_js
|
||||
install: npm install
|
||||
install: yarn
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
@ -8,19 +8,16 @@ cache:
|
|||
|
||||
jobs:
|
||||
include:
|
||||
# Lint doesn't need to build
|
||||
- stage: lint
|
||||
script: npm run lint
|
||||
node_js: "12"
|
||||
script: yarn lint
|
||||
node_js: "14"
|
||||
- stage: unit tests
|
||||
# Test already builds
|
||||
script: npm run test
|
||||
node_js: "10"
|
||||
- node_js: "12"
|
||||
- stage: coverage
|
||||
# Coverage does NOT build
|
||||
script: npm run build && npm run coverage
|
||||
script: yarn test
|
||||
node_js: "12"
|
||||
- node_js: "14"
|
||||
- stage: coverage
|
||||
script: yarn coverage
|
||||
node_js: "14"
|
||||
|
||||
# NOTE: This is unused atm
|
||||
# notifications:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:alpine AS BUILD
|
||||
FROM node:14-alpine AS BUILD
|
||||
COPY . /tmp/src
|
||||
# install some dependencies needed for the build process
|
||||
RUN apk add --no-cache -t build-deps make gcc g++ python ca-certificates libc-dev wget git
|
||||
|
@ -6,7 +6,7 @@ RUN cd /tmp/src \
|
|||
&& npm install \
|
||||
&& npm run build
|
||||
|
||||
FROM node:alpine
|
||||
FROM node:14-alpine
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=BUILD /tmp/src/build /build
|
||||
COPY --from=BUILD /tmp/src/config /config
|
||||
|
|
|
@ -27,7 +27,7 @@ Please also be aware that this is an unoffical project worked on in our spare ti
|
|||
The bridge has been tested against the [Synapse](https://github.com/matrix-org/synapse) homeserver, although any homeserver
|
||||
that implements the [AS API](https://matrix.org/docs/spec/application_service/r0.1.0.html) should work with this bridge.
|
||||
|
||||
The bridge supports any version of Node.js >= v10.X, including all [current releases](https://nodejs.org/en/about/releases/).
|
||||
The bridge supports any version of Node.js >= v12.X, including all [current releases](https://nodejs.org/en/about/releases/).
|
||||
|
||||
### Setup the bridge
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ bridge:
|
|||
# This should be your publically facing URL because Discord may use it to
|
||||
# fetch media from the media store.
|
||||
homeserverUrl: "http://localhost:8008"
|
||||
# The TCP port on which the appservice runs on.
|
||||
port: 9005
|
||||
# Interval at which to process users in the 'presence queue'. If you have
|
||||
# 5 users, one user will be processed every 500 milliseconds according to the
|
||||
# value below. This has a minimum value of 250.
|
||||
|
@ -29,10 +31,18 @@ bridge:
|
|||
disableReadReceipts: false
|
||||
# Disable Join Leave echos from matrix
|
||||
disableJoinLeaveNotifications: false
|
||||
# Disable Invite echos from matrix
|
||||
disableInviteNotifications: false
|
||||
# Auto-determine the language of code blocks (this can be CPU-intensive)
|
||||
determineCodeLanguage: false
|
||||
# Authentication configuration for the discord bot.
|
||||
auth:
|
||||
# This MUST be a string (wrapped in quotes)
|
||||
clientID: "12345"
|
||||
botToken: "foobar"
|
||||
# You must enable "Privileged Gateway Intents" in your bot settings on discord.com (e.g.g https://discord.com/developers/applications/12345/bot)
|
||||
# for this to work
|
||||
usePrivilegedIntents: false
|
||||
logging:
|
||||
# What level should the logger output to the console at.
|
||||
console: "warn" #silly, verbose, info, http, warn, error, silent
|
||||
|
@ -48,8 +58,6 @@ logging:
|
|||
enable:
|
||||
- "DiscordBot"
|
||||
database:
|
||||
userStorePath: "user-store.db"
|
||||
roomStorePath: "room-store.db"
|
||||
# You may either use SQLite or Postgresql for the bridge database, which contains
|
||||
# important mappings for events and user puppeting configurations.
|
||||
# Use the filename option for SQLite, or connString for Postgresql.
|
||||
|
@ -85,10 +93,12 @@ channel:
|
|||
limits:
|
||||
# Delay in milliseconds between discord users joining a room.
|
||||
roomGhostJoinDelay: 6000
|
||||
# Delay in milliseconds before sending messages to discord to avoid echos.
|
||||
# (Copies of a sent message may arrive from discord before we've
|
||||
# Lock timeout in milliseconds before seinding messages to discord to avoid
|
||||
# echos. Default is rather high as the lock will most likely time out
|
||||
# before anyways.
|
||||
# echos = (Copies of a sent message may arrive from discord before we've
|
||||
# fininished handling it, causing us to echo it back to the room)
|
||||
discordSendDelay: 750
|
||||
discordSendDelay: 1500
|
||||
ghosts:
|
||||
# Pattern for the ghosts nick, available is :nick, :username, :tag and :id
|
||||
nickPattern: ":nick"
|
||||
|
|
|
@ -10,6 +10,8 @@ properties:
|
|||
type: "string"
|
||||
homeserverUrl:
|
||||
type: "string"
|
||||
port:
|
||||
type: "number"
|
||||
presenceInterval:
|
||||
type: "number"
|
||||
disablePresence:
|
||||
|
@ -22,6 +24,10 @@ properties:
|
|||
type: "boolean"
|
||||
disableReadReceipts:
|
||||
type: "boolean"
|
||||
disableJoinLeaveNotifications:
|
||||
type: "boolean"
|
||||
disableInviteNotifications:
|
||||
type: "boolean"
|
||||
auth:
|
||||
type: "object"
|
||||
required: ["botToken", "clientID"]
|
||||
|
@ -30,6 +36,8 @@ properties:
|
|||
type: "string"
|
||||
botToken:
|
||||
type: "string"
|
||||
usePrivilegedIntents:
|
||||
type: "boolean"
|
||||
logging:
|
||||
type: "object"
|
||||
properties:
|
||||
|
|
11
docs/bridge-migrations.md
Normal file
11
docs/bridge-migrations.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# 1.0 Migration (from 0.5.1 or lower)
|
||||
|
||||
If you have been linked here, there is an issue with your config on your bridge.
|
||||
|
||||
Please follow the following steps:
|
||||
|
||||
1. If you have just created a new install OR were previously running 0.5.X,
|
||||
please remove `roomDataStore` and `userDataStore` from your config file.
|
||||
2. If this is a existing install but you have not run 0.5.X (0.4.X or lower),
|
||||
please downgrade to 0.5.X to migrate your database across and then run
|
||||
this version of the bridge again.
|
|
@ -15,7 +15,7 @@ is formatted as https://discordapp.com/channels/``guildid``/``channelid``
|
|||
|
||||
* The ``adminme`` script is provided to set Admin/Moderator or any other custom power level to a specific user.
|
||||
* e.g. To set Alice to Admin on her ``example.com`` HS on default config. (``config.yaml``)
|
||||
* ``npm run adminme -- -r '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'``
|
||||
* ``npm run adminme -- -m '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'``
|
||||
* Run ``npm run adminme -- -h`` for usage.
|
||||
|
||||
Please note that `!AbcdefghijklmnopqR:example.com` is the internal room id and will always begin with `!`.
|
||||
|
|
|
@ -13,8 +13,8 @@ has the benefits of (not all of these may be implemented):
|
|||
Discord is currently __not__ offering any way to authenticate on behalf
|
||||
of a user _and_ interact on their behalf. The OAuth system does not allow
|
||||
remote access beyond reading information about the users. While [developers have
|
||||
expressed a wish for this](https://feedback.discordapp.com/forums/326712-discord-dream-land/suggestions/16753837-support-custom-clients)
|
||||
,it is my opinion that Discord are unlikely to support this any time soon. With
|
||||
expressed a wish for this](https://feedback.discordapp.com/forums/326712-discord-dream-land/suggestions/16753837-support-custom-clients),
|
||||
it is my opinion that Discord are unlikely to support this any time soon. With
|
||||
all this said, Discord will not be banning users or the bridge itself for acting
|
||||
on the behalf of the user.
|
||||
|
||||
|
|
5249
package-lock.json
generated
5249
package-lock.json
generated
File diff suppressed because it is too large
Load diff
73
package.json
73
package.json
|
@ -1,14 +1,16 @@
|
|||
{
|
||||
"name": "matrix-appservice-discord",
|
||||
"version": "0.5.2",
|
||||
"version": "1.0.0",
|
||||
"description": "A bridge between Matrix and Discord",
|
||||
"main": "discordas.js",
|
||||
"scripts": {
|
||||
"test": "npm run-script build && mocha --opts test/mocha.opts",
|
||||
"test": "mocha -r ts-node/register test/config.ts test/test_*.ts test/**/test_*.ts",
|
||||
"lint": "tslint --project ./tsconfig.json -t stylish",
|
||||
"coverage": "tsc && nyc mocha",
|
||||
"coverage": "nyc mocha -r ts-node/register test/config.ts test/test_*.ts test/**/test_*.ts",
|
||||
"build": "tsc",
|
||||
"start": "npm run-script build && node ./build/src/discordas.js -p 9005 -c config.yaml",
|
||||
"postinstall": "npm run build",
|
||||
"start": "npm run-script build && node ./build/src/discordas.js -c config.yaml",
|
||||
"debug": "npm run-script build && node --inspect ./build/src/discordas.js -c config.yaml",
|
||||
"addbot": "node ./build/tools/addbot.js",
|
||||
"adminme": "node ./build/tools/adminme.js",
|
||||
"usertool": "node ./build/tools/userClientTools.js",
|
||||
|
@ -34,39 +36,42 @@
|
|||
},
|
||||
"homepage": "https://github.com/Half-Shot/matrix-appservice-discord#readme",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^5.4.3",
|
||||
"command-line-args": "^4.0.1",
|
||||
"command-line-usage": "^4.1.0",
|
||||
"discord-markdown": "^2.2.0",
|
||||
"discord.js": "^11.5.1",
|
||||
"better-discord.js": "git://github.com/Sorunome/better-discord.js.git#b5a28499899fe2d9e6aa1aa3b3c5d693ae672117",
|
||||
"better-sqlite3": "^7.1.0",
|
||||
"command-line-args": "^5.1.1",
|
||||
"command-line-usage": "^6.1.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"js-yaml": "^3.13.1",
|
||||
"matrix-appservice-bridge": "matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76",
|
||||
"mime": "^1.6.0",
|
||||
"node-html-parser": "^1.1.11",
|
||||
"p-queue": "^6.0.1",
|
||||
"pg-promise": "^8.5.1",
|
||||
"prom-client": "^11.3.0",
|
||||
"tslint": "^5.11.0",
|
||||
"typescript": "^3.1.3",
|
||||
"winston": "^3.0.0",
|
||||
"winston-daily-rotate-file": "^3.3.0"
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"marked": "^1.2.2",
|
||||
"matrix-bot-sdk": "0.5.4",
|
||||
"matrix-discord-parser": "0.1.5",
|
||||
"mime": "^2.4.6",
|
||||
"node-html-parser": "^1.2.19",
|
||||
"p-queue": "^6.4.0",
|
||||
"pg-promise": "^10.5.6",
|
||||
"prom-client": "^12.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@istanbuljs/nyc-config-typescript": "^0.1.3",
|
||||
"@types/chai": "^3.4.35",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^10.12.0",
|
||||
"@types/sqlite3": "^3.1.3",
|
||||
"chai": "^3.5.0",
|
||||
"eslint": "^3.8.1",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^5.2.0",
|
||||
"nyc": "^14.1.1",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@types/chai": "^4.2.11",
|
||||
"@types/command-line-args": "^5.0.0",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/marked": "^1.1.0",
|
||||
"@types/mime": "^2.0.2",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^12",
|
||||
"@types/better-sqlite3": "^5.4.1",
|
||||
"chai": "^4.2.0",
|
||||
"mocha": "^8.0.1",
|
||||
"nyc": "^15.1.0",
|
||||
"proxyquire": "^1.7.11",
|
||||
"source-map-support": "^0.5.12",
|
||||
"ts-node": "^8.1.0",
|
||||
"why-is-node-running": "^2.0.3"
|
||||
"source-map-support": "^0.5.19",
|
||||
"ts-node": "^8.10.2",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.9.5",
|
||||
"why-is-node-running": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
|
616
src/bot.ts
616
src/bot.ts
File diff suppressed because it is too large
Load diff
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { DiscordBot } from "./bot";
|
||||
import { Util } from "./util";
|
||||
import { DiscordBridgeConfig, DiscordBridgeConfigChannelDeleteOptions } from "./config";
|
||||
import { Bridge } from "matrix-appservice-bridge";
|
||||
import { Log } from "./log";
|
||||
import { DbRoomStore, IRoomStoreEntry } from "./db/roomstore";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
|
||||
const log = new Log("ChannelSync");
|
||||
|
||||
|
@ -58,7 +58,7 @@ export interface IChannelState {
|
|||
|
||||
export class ChannelSyncroniser {
|
||||
constructor(
|
||||
private bridge: Bridge,
|
||||
private bridge: Appservice,
|
||||
private config: DiscordBridgeConfig,
|
||||
private bot: DiscordBot,
|
||||
private roomStore: DbRoomStore,
|
||||
|
@ -81,7 +81,7 @@ export class ChannelSyncroniser {
|
|||
public async OnGuildUpdate(guild: Discord.Guild, force = false) {
|
||||
log.verbose(`Got guild update for guild ${guild.id}`);
|
||||
const channelStates: IChannelState[] = [];
|
||||
for (const [_, channel] of guild.channels) {
|
||||
for (const [_, channel] of guild.channels.cache) {
|
||||
if (channel.type !== "text") {
|
||||
continue; // not supported for now
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ export class ChannelSyncroniser {
|
|||
}
|
||||
|
||||
public async OnGuildDelete(guild: Discord.Guild) {
|
||||
for (const [_, channel] of guild.channels) {
|
||||
for (const [_, channel] of guild.channels.cache) {
|
||||
try {
|
||||
await this.OnDelete(channel);
|
||||
} catch (e) {
|
||||
|
@ -169,21 +169,32 @@ export class ChannelSyncroniser {
|
|||
try {
|
||||
rooms = await this.GetRoomIdsFromChannel(channel);
|
||||
} catch (err) { } // do nothing, our rooms array will just be empty
|
||||
let fallbackAlias = "";
|
||||
for (const room of rooms) {
|
||||
try {
|
||||
const al = (await this.bridge.getIntent().getClient()
|
||||
.getStateEvent(room, "m.room.canonical_alias")).alias;
|
||||
const al = (await this.bridge.botIntent.underlyingClient.getRoomStateEvent(
|
||||
room,
|
||||
"m.room.canonical_alias",
|
||||
"")
|
||||
).alias;
|
||||
if (al) {
|
||||
return al; // we are done, we found an alias
|
||||
if (this.bridge.isNamespacedAlias(al)) {
|
||||
fallbackAlias = al;
|
||||
} else {
|
||||
return al; // we are done, we found an alias
|
||||
}
|
||||
}
|
||||
} catch (err) { } // do nothing, as if we error we just roll over to the next entry
|
||||
}
|
||||
if (fallbackAlias) {
|
||||
return fallbackAlias;
|
||||
}
|
||||
const guildChannel = channel as Discord.TextChannel;
|
||||
if (!guildChannel.guild) {
|
||||
return null; // we didn't pass a guild, so we have no way of bridging this room, thus no alias
|
||||
}
|
||||
// at last, no known canonical aliases and we are ag uild....so we know an alias!
|
||||
return `#_discord_${guildChannel.guild.id}_${channel.id}:${this.config.bridge.domain}`;
|
||||
// at last, no known canonical aliases and we are a guild....so we know an alias!
|
||||
return this.bridge.getAliasForSuffix(`${guildChannel.guild.id}_${channel.id}`);
|
||||
}
|
||||
|
||||
public async GetChannelUpdateState(channel: Discord.TextChannel, forceUpdate = false): Promise<IChannelState> {
|
||||
|
@ -252,7 +263,7 @@ export class ChannelSyncroniser {
|
|||
}
|
||||
|
||||
private async ApplyStateToChannel(channelsState: IChannelState) {
|
||||
const intent = this.bridge.getIntent();
|
||||
const intent = this.bridge.botIntent;
|
||||
for (const channelState of channelsState.mxChannels) {
|
||||
let roomUpdated = false;
|
||||
const remoteRoom = (await this.roomStore.getEntriesByMatrixId(channelState.mxid))[0];
|
||||
|
@ -262,14 +273,24 @@ export class ChannelSyncroniser {
|
|||
}
|
||||
if (channelState.name !== null) {
|
||||
log.verbose(`Updating channelname for ${channelState.mxid} to "${channelState.name}"`);
|
||||
await intent.setRoomName(channelState.mxid, channelState.name);
|
||||
await intent.underlyingClient.sendStateEvent(
|
||||
channelState.mxid,
|
||||
"m.room.name",
|
||||
"",
|
||||
{ name: channelState.name },
|
||||
);
|
||||
remoteRoom.remote.set("discord_name", channelState.name);
|
||||
roomUpdated = true;
|
||||
}
|
||||
|
||||
if (channelState.topic !== null) {
|
||||
log.verbose(`Updating channeltopic for ${channelState.mxid} to "${channelState.topic}"`);
|
||||
await intent.setRoomTopic(channelState.mxid, channelState.topic);
|
||||
await intent.underlyingClient.sendStateEvent(
|
||||
channelState.mxid,
|
||||
"m.room.topic",
|
||||
"",
|
||||
{ topic: channelState.topic },
|
||||
);
|
||||
remoteRoom.remote.set("discord_topic", channelState.topic);
|
||||
roomUpdated = true;
|
||||
}
|
||||
|
@ -277,14 +298,21 @@ export class ChannelSyncroniser {
|
|||
if (channelState.iconUrl !== null && channelState.iconId !== null) {
|
||||
log.verbose(`Updating icon_url for ${channelState.mxid} to "${channelState.iconUrl}"`);
|
||||
if (channelsState.iconMxcUrl === null) {
|
||||
const iconMxc = await Util.UploadContentFromUrl(
|
||||
channelState.iconUrl,
|
||||
intent,
|
||||
const file = await Util.DownloadFile(channelState.iconUrl);
|
||||
const iconMxc = await this.bridge.botIntent.underlyingClient.uploadContent(
|
||||
file.buffer,
|
||||
file.mimeType,
|
||||
channelState.iconId,
|
||||
);
|
||||
channelsState.iconMxcUrl = iconMxc.mxcUrl;
|
||||
channelsState.iconMxcUrl = iconMxc;
|
||||
}
|
||||
await intent.setRoomAvatar(channelState.mxid, channelsState.iconMxcUrl);
|
||||
await intent.underlyingClient.sendStateEvent(
|
||||
channelState.mxid,
|
||||
"m.room.avatar",
|
||||
"",
|
||||
// TODO: "info" object for avatar
|
||||
{ url: channelsState.iconMxcUrl },
|
||||
);
|
||||
remoteRoom.remote.set("discord_iconurl", channelState.iconUrl);
|
||||
remoteRoom.remote.set("discord_iconurl_mxc", channelsState.iconMxcUrl);
|
||||
roomUpdated = true;
|
||||
|
@ -292,7 +320,12 @@ export class ChannelSyncroniser {
|
|||
|
||||
if (channelState.removeIcon) {
|
||||
log.verbose(`Clearing icon_url for ${channelState.mxid}`);
|
||||
await intent.setRoomAvatar(channelState.mxid, null);
|
||||
await intent.underlyingClient.sendStateEvent(
|
||||
channelState.mxid,
|
||||
"m.room.avatar",
|
||||
"",
|
||||
{ },
|
||||
);
|
||||
remoteRoom.remote.set("discord_iconurl", null);
|
||||
remoteRoom.remote.set("discord_iconurl_mxc", null);
|
||||
roomUpdated = true;
|
||||
|
@ -310,36 +343,47 @@ export class ChannelSyncroniser {
|
|||
entry: IRoomStoreEntry,
|
||||
overrideOptions?: DiscordBridgeConfigChannelDeleteOptions): Promise<void> {
|
||||
log.info(`Deleting ${channel.id} from ${roomId}.`);
|
||||
const intent = await this.bridge.getIntent();
|
||||
const intent = this.bridge.botIntent;
|
||||
const client = this.bridge.botClient;
|
||||
const options = overrideOptions || this.config.channel.deleteOptions;
|
||||
const plumbed = entry.remote!.get("plumbed");
|
||||
|
||||
await this.roomStore.upsertEntry(entry);
|
||||
if (options.ghostsLeave) {
|
||||
for (const member of channel.members.array()) {
|
||||
const mIntent = await this.bot.GetIntentFromDiscordMember(member);
|
||||
// Not awaiting this because we want to do this in the background.
|
||||
mIntent.leave(roomId).then(() => {
|
||||
try {
|
||||
const mIntent = this.bot.GetIntentFromDiscordMember(member);
|
||||
await client.leaveRoom(roomId);
|
||||
log.verbose(`${member.id} left ${roomId}.`);
|
||||
}).catch(() => {
|
||||
log.warn(`Failed to make ${member.id} leave.`);
|
||||
});
|
||||
} catch (e) {
|
||||
log.warn(`Failed to make ${member.id} leave `);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.namePrefix) {
|
||||
try {
|
||||
const name = await intent.getClient().getStateEvent(roomId, "m.room.name");
|
||||
const name = await client.getRoomStateEvent(roomId, "m.room.name", "");
|
||||
name.name = options.namePrefix + name.name;
|
||||
await intent.getClient().setRoomName(roomId, name.name);
|
||||
await client.sendStateEvent(
|
||||
roomId,
|
||||
"m.room.name",
|
||||
"",
|
||||
name,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(`Failed to set name of room ${roomId} ${e}`);
|
||||
}
|
||||
}
|
||||
if (options.topicPrefix) {
|
||||
try {
|
||||
const topic = await intent.getClient().getStateEvent(roomId, "m.room.topic");
|
||||
const topic = await client.getRoomStateEvent(roomId, "m.room.topic", "");
|
||||
topic.topic = options.topicPrefix + topic.topic;
|
||||
await intent.getClient().setRoomTopic(roomId, topic.topic);
|
||||
await client.sendStateEvent(
|
||||
roomId,
|
||||
"m.room.topic",
|
||||
"",
|
||||
topic,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(`Failed to set topic of room ${roomId} ${e}`);
|
||||
}
|
||||
|
@ -349,11 +393,15 @@ export class ChannelSyncroniser {
|
|||
if (options.unsetRoomAlias) {
|
||||
try {
|
||||
const alias = `#_${entry.remote!.roomId}:${this.config.bridge.domain}`;
|
||||
const canonicalAlias = await intent.getClient().getStateEvent(roomId, "m.room.canonical_alias");
|
||||
const canonicalAlias = await client.getRoomStateEvent(
|
||||
roomId,
|
||||
"m.room.canonical_alias",
|
||||
"",
|
||||
);
|
||||
if (canonicalAlias.alias === alias) {
|
||||
await intent.getClient().sendStateEvent(roomId, "m.room.canonical_alias", {});
|
||||
await client.sendStateEvent(roomId, "m.room.canonical_alias", "", {});
|
||||
}
|
||||
await intent.getClient().deleteAlias(alias);
|
||||
await client.deleteRoomAlias(alias);
|
||||
} catch (e) {
|
||||
log.error(`Couldn't remove alias of ${roomId} ${e}`);
|
||||
}
|
||||
|
@ -361,7 +409,7 @@ export class ChannelSyncroniser {
|
|||
|
||||
if (options.unlistFromDirectory) {
|
||||
try {
|
||||
await intent.getClient().setRoomDirectoryVisibility(roomId, "private");
|
||||
await client.setDirectoryVisibility(roomId, "private");
|
||||
} catch (e) {
|
||||
log.error(`Couldn't remove ${roomId} from room directory ${e}`);
|
||||
}
|
||||
|
@ -370,7 +418,12 @@ export class ChannelSyncroniser {
|
|||
|
||||
if (options.setInviteOnly) {
|
||||
try {
|
||||
await intent.getClient().sendStateEvent(roomId, "m.room.join_rules", {join_role: "invite"});
|
||||
await client.sendStateEvent(
|
||||
roomId,
|
||||
"m.room.join_rules",
|
||||
"",
|
||||
{join_role: "invite"},
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(`Couldn't set ${roomId} to private ${e}`);
|
||||
}
|
||||
|
@ -378,9 +431,9 @@ export class ChannelSyncroniser {
|
|||
|
||||
if (options.disableMessaging) {
|
||||
try {
|
||||
const state = await intent.getClient().getStateEvent(roomId, "m.room.power_levels");
|
||||
const state = await client.getRoomStateEvent(roomId, "m.room.power_levels", "");
|
||||
state.events_default = POWER_LEVEL_MESSAGE_TALK;
|
||||
await intent.getClient().sendStateEvent(roomId, "m.room.power_levels", state);
|
||||
await client.sendStateEvent(roomId, "m.room.power_levels", "", state);
|
||||
} catch (e) {
|
||||
log.error(`Couldn't disable messaging for ${roomId} ${e}`);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import { DiscordBridgeConfigAuth } from "./config";
|
||||
import { DiscordStore } from "./store";
|
||||
import { Client as DiscordClient, TextChannel } from "discord.js";
|
||||
import { Client as DiscordClient, Intents, TextChannel } from "better-discord.js";
|
||||
import { Log } from "./log";
|
||||
import { MetricPeg } from "./metrics";
|
||||
|
||||
|
@ -40,13 +40,23 @@ export class DiscordClientFactory {
|
|||
// We just need to make sure we have a bearer token.
|
||||
// Create a new Bot client.
|
||||
this.botClient = new DiscordClient({
|
||||
fetchAllMembers: true,
|
||||
fetchAllMembers: this.config.usePrivilegedIntents,
|
||||
messageCacheLifetime: 5,
|
||||
sync: true,
|
||||
ws: {
|
||||
intents: this.config.usePrivilegedIntents ? Intents.PRIVILEGED : Intents.NON_PRIVILEGED,
|
||||
},
|
||||
});
|
||||
|
||||
const waitPromise = new Promise((resolve, reject) => {
|
||||
this.botClient.once("shardReady", resolve);
|
||||
this.botClient.once("shardError", reject);
|
||||
});
|
||||
|
||||
try {
|
||||
await this.botClient.login(this.config.botToken);
|
||||
await this.botClient.login(this.config.botToken, true);
|
||||
log.info("Waiting for shardReady signal");
|
||||
await waitPromise;
|
||||
log.info("Got shardReady signal");
|
||||
} catch (err) {
|
||||
log.error("Could not login as the bot user. This is bad!", err);
|
||||
throw err;
|
||||
|
@ -58,16 +68,17 @@ export class DiscordClientFactory {
|
|||
const client = new DiscordClient({
|
||||
fetchAllMembers: false,
|
||||
messageCacheLifetime: 5,
|
||||
sync: false,
|
||||
ws: {
|
||||
intents: Intents.NON_PRIVILEGED,
|
||||
},
|
||||
});
|
||||
|
||||
await client.login(token);
|
||||
const id = client.user.id;
|
||||
|
||||
// This can be done asynchronously, because we don't need to block to return the id.
|
||||
client.destroy().catch((err) => {
|
||||
log.warn("Failed to destroy client ", id);
|
||||
});
|
||||
await client.login(token, false);
|
||||
const id = client.user?.id;
|
||||
client.destroy();
|
||||
if (!id) {
|
||||
throw Error("Client did not have a user object, cannot determine ID");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
@ -88,9 +99,11 @@ export class DiscordClientFactory {
|
|||
// TODO: Select a profile based on preference, not the first one.
|
||||
const token = await this.store.getToken(discordIds[0]);
|
||||
const client = new DiscordClient({
|
||||
fetchAllMembers: true,
|
||||
fetchAllMembers: false,
|
||||
messageCacheLifetime: 5,
|
||||
sync: true,
|
||||
ws: {
|
||||
intents: Intents.NON_PRIVILEGED,
|
||||
},
|
||||
});
|
||||
|
||||
const jsLog = new Log("discord.js-ppt");
|
||||
|
@ -99,7 +112,7 @@ export class DiscordClientFactory {
|
|||
client.on("warn", (msg) => { jsLog.warn(msg); });
|
||||
|
||||
try {
|
||||
await client.login(token);
|
||||
await client.login(token, false);
|
||||
log.verbose("Logged in. Storing ", userId);
|
||||
this.clients.set(userId, client);
|
||||
return client;
|
||||
|
|
|
@ -28,6 +28,7 @@ export class DiscordBridgeConfig {
|
|||
public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel();
|
||||
public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits();
|
||||
public ghosts: DiscordBridgeConfigGhosts = new DiscordBridgeConfigGhosts();
|
||||
public metrics: DiscordBridgeConfigMetrics = new DiscordBridgeConfigMetrics();
|
||||
|
||||
/**
|
||||
* Apply a set of keys and values over the default config.
|
||||
|
@ -80,6 +81,8 @@ export class DiscordBridgeConfig {
|
|||
class DiscordBridgeConfigBridge {
|
||||
public domain: string;
|
||||
public homeserverUrl: string;
|
||||
public port: number;
|
||||
public bindAddress: string;
|
||||
public presenceInterval: number = 500;
|
||||
public disablePresence: boolean;
|
||||
public disableTypingNotifications: boolean;
|
||||
|
@ -90,12 +93,14 @@ class DiscordBridgeConfigBridge {
|
|||
public disableEveryoneMention: boolean = false;
|
||||
public disableHereMention: boolean = false;
|
||||
public disableJoinLeaveNotifications: boolean = false;
|
||||
public enableMetrics: boolean = false;
|
||||
public disableInviteNotifications: boolean = false;
|
||||
public determineCodeLanguage: boolean = false;
|
||||
}
|
||||
|
||||
export class DiscordBridgeConfigDatabase {
|
||||
public connString: string;
|
||||
public filename: string;
|
||||
// These parameters are legacy, and will stop the bridge if defined.
|
||||
public userStorePath: string;
|
||||
public roomStorePath: string;
|
||||
}
|
||||
|
@ -103,6 +108,7 @@ export class DiscordBridgeConfigDatabase {
|
|||
export class DiscordBridgeConfigAuth {
|
||||
public clientID: string;
|
||||
public botToken: string;
|
||||
public usePrivilegedIntents: boolean;
|
||||
}
|
||||
|
||||
export class DiscordBridgeConfigLogging {
|
||||
|
@ -133,7 +139,7 @@ export class DiscordBridgeConfigChannelDeleteOptions {
|
|||
|
||||
class DiscordBridgeConfigLimits {
|
||||
public roomGhostJoinDelay: number = 6000;
|
||||
public discordSendDelay: number = 750;
|
||||
public discordSendDelay: number = 1500;
|
||||
}
|
||||
|
||||
export class LoggingFile {
|
||||
|
@ -150,3 +156,9 @@ class DiscordBridgeConfigGhosts {
|
|||
public nickPattern: string = ":nick";
|
||||
public usernamePattern: string = ":username#:tag";
|
||||
}
|
||||
|
||||
export class DiscordBridgeConfigMetrics {
|
||||
public enable: boolean;
|
||||
public port: number = 9001;
|
||||
public host: string = "127.0.0.1";
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { DiscordStore } from "../../store";
|
||||
import { DiscordBridgeConfigDatabase } from "../../config";
|
||||
export interface IDbSchema {
|
||||
description: string;
|
||||
run(store: DiscordStore): Promise<null|void|Error|Error[]>;
|
||||
|
|
48
src/db/schema/v11.ts
Normal file
48
src/db/schema/v11.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2019 matrix-appservice-discord
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {IDbSchema} from "./dbschema";
|
||||
import {DiscordStore} from "../../store";
|
||||
import { Log } from "../../log";
|
||||
|
||||
const log = new Log("SchemaV11");
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "create stores for bot sdk";
|
||||
|
||||
public async run(store: DiscordStore): Promise<void> {
|
||||
try {
|
||||
await store.createTable(
|
||||
"CREATE TABLE registered_users (user_id TEXT UNIQUE NOT NULL);",
|
||||
"registered_users",
|
||||
);
|
||||
await store.createTable(
|
||||
"CREATE TABLE as_txns (txn_id TEXT UNIQUE NOT NULL);",
|
||||
"as_txns",
|
||||
);
|
||||
} catch (ex) {
|
||||
log.error("Failed to apply indexes:", ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async rollBack(store: DiscordStore): Promise<void> {
|
||||
await store.db.Exec(
|
||||
`DROP TABLE IF EXISTS registered_users;
|
||||
DROP TABLE IF EXISTS as_txns;`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -78,8 +78,8 @@ directory.`);
|
|||
log.info("Moving ", row.userId);
|
||||
try {
|
||||
const client = await clientFactory.getClient(row.token);
|
||||
const dId = client.user.id;
|
||||
if (dId === null) {
|
||||
const dId = client.user?.id;
|
||||
if (!dId) {
|
||||
continue;
|
||||
}
|
||||
log.verbose("INSERT INTO discord_id_token.");
|
||||
|
|
|
@ -17,16 +17,11 @@ limitations under the License.
|
|||
import {IDbSchema} from "./dbschema";
|
||||
import {DiscordStore} from "../../store";
|
||||
import { Log } from "../../log";
|
||||
import {
|
||||
RoomStore,
|
||||
} from "matrix-appservice-bridge";
|
||||
import { RemoteStoreRoom, MatrixStoreRoom } from "../roomstore";
|
||||
const log = new Log("SchemaV8");
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "create room store tables";
|
||||
|
||||
constructor(private roomStore: RoomStore|null) {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
|
@ -56,33 +51,9 @@ export class Schema implements IDbSchema {
|
|||
PRIMARY KEY(id)
|
||||
);`, "room_entries");
|
||||
|
||||
if (this.roomStore === null) {
|
||||
log.warn("Not migrating rooms from room store, room store is null");
|
||||
return;
|
||||
}
|
||||
log.warn("Migrating rooms from roomstore, this may take a while...");
|
||||
const rooms = await this.roomStore.select({});
|
||||
log.info(`Found ${rooms.length} rooms in the DB`);
|
||||
// Matrix room only entrys are useless.
|
||||
const entrys = rooms.filter((r) => r.remote);
|
||||
log.info(`Filtered out rooms without remotes. Have ${entrys.length} entries`);
|
||||
let migrated = 0;
|
||||
for (const e of entrys) {
|
||||
const matrix = new MatrixStoreRoom(e.matrix_id);
|
||||
try {
|
||||
const remote = new RemoteStoreRoom(e.remote_id, e.remote);
|
||||
await store.roomStore.linkRooms(matrix, remote);
|
||||
log.info(`Migrated ${matrix.roomId}`);
|
||||
migrated++;
|
||||
} catch (ex) {
|
||||
log.error(`Failed to link ${matrix.roomId}: `, ex);
|
||||
}
|
||||
}
|
||||
if (migrated !== entrys.length) {
|
||||
log.error(`Didn't migrate all rooms, ${entrys.length - migrated} failed to be migrated.`);
|
||||
} else {
|
||||
log.info("Migrated all rooms successfully");
|
||||
}
|
||||
// XXX: This used to migrate rooms across from the old room store format but
|
||||
// since we moved to the matrix-js-bot-sdk, we can no longer do this. Please
|
||||
// use a 0.X release for this.
|
||||
}
|
||||
|
||||
public async rollBack(store: DiscordStore): Promise<void> {
|
||||
|
|
|
@ -16,18 +16,11 @@ limitations under the License.
|
|||
|
||||
import { IDbSchema } from "./dbschema";
|
||||
import { DiscordStore } from "../../store";
|
||||
import { Log } from "../../log";
|
||||
import {
|
||||
UserStore,
|
||||
} from "matrix-appservice-bridge";
|
||||
import { RemoteUser } from "../userstore";
|
||||
import PQueue from "p-queue";
|
||||
const log = new Log("SchemaV9");
|
||||
|
||||
export class Schema implements IDbSchema {
|
||||
public description = "create user store tables";
|
||||
|
||||
constructor(private userStore: UserStore|null) {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
|
@ -56,54 +49,9 @@ export class Schema implements IDbSchema {
|
|||
PRIMARY KEY(matrix_id, remote_id)
|
||||
);`, "user_entries");
|
||||
|
||||
if (this.userStore === null) {
|
||||
log.warn("Not migrating users from users store, users store is null");
|
||||
return;
|
||||
}
|
||||
log.warn("Migrating users from userstore, this may take a while...");
|
||||
const remoteUsers = await this.userStore.select({type: "remote"});
|
||||
log.info(`Found ${remoteUsers.length} remote users in the DB`);
|
||||
let migrated = 0;
|
||||
const processQueue = new PQueue({
|
||||
autoStart: true,
|
||||
concurrency: 100,
|
||||
});
|
||||
for (const user of remoteUsers) {
|
||||
const matrixIds = await this.userStore.getMatrixLinks(user.id);
|
||||
if (!matrixIds || matrixIds.length === 0) {
|
||||
log.warn(`Not migrating ${user.id}, has no linked matrix user`);
|
||||
continue;
|
||||
} else if (matrixIds.length > 1) {
|
||||
log.warn(`Multiple matrix ids for ${user.id}, using first`);
|
||||
}
|
||||
const matrixId = matrixIds[0];
|
||||
try {
|
||||
const remote = new RemoteUser(user.id);
|
||||
remote.avatarurl = user.data.avatarurl;
|
||||
remote.avatarurlMxc = user.data.avatarurl_mxc;
|
||||
remote.displayname = user.data.displayname;
|
||||
Object.keys(user.data).filter((k) => k.startsWith("nick_")).forEach((k) => {
|
||||
remote.guildNicks.set(k.substr("nick_".length), user.data[k]);
|
||||
});
|
||||
processQueue.add(async () => {
|
||||
await store.userStore.linkUsers(matrixId, remote.id);
|
||||
return store.userStore.setRemoteUser(remote);
|
||||
}).then(() => {
|
||||
log.info(`Migrated ${matrixId}, ${processQueue.pending} to go.`);
|
||||
migrated++;
|
||||
}).catch((err) => {
|
||||
log.error(`Failed to migrate ${matrixId} ${err}`);
|
||||
});
|
||||
} catch (ex) {
|
||||
log.error(`Failed to link ${matrixId}: `, ex);
|
||||
}
|
||||
}
|
||||
await processQueue.onIdle();
|
||||
if (migrated !== remoteUsers.length) {
|
||||
log.error(`Didn't migrate all users, ${remoteUsers.length - migrated} failed to be migrated.`);
|
||||
} else {
|
||||
log.info("Migrated all users successfully");
|
||||
}
|
||||
// XXX: This used to migrate rooms across from the old room store format but
|
||||
// since we moved to the matrix-js-bot-sdk, we can no longer do this. Please
|
||||
// use a 0.X release for this.
|
||||
}
|
||||
|
||||
public async rollBack(store: DiscordStore): Promise<void> {
|
||||
|
|
|
@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Database from "better-sqlite3";
|
||||
import * as BetterSQLite3 from "better-sqlite3";
|
||||
import { Log } from "../log";
|
||||
import { IDatabaseConnector, ISqlCommandParameters, ISqlRow } from "./connector";
|
||||
const log = new Log("SQLite3");
|
||||
|
||||
export class SQLite3 implements IDatabaseConnector {
|
||||
private db: Database;
|
||||
private db: BetterSQLite3.Database;
|
||||
constructor(private filename: string) {
|
||||
|
||||
}
|
||||
|
||||
public async Open() {
|
||||
log.info(`Opening ${this.filename}`);
|
||||
this.db = new Database(this.filename);
|
||||
this.db = new BetterSQLite3(this.filename);
|
||||
}
|
||||
|
||||
public async Get(sql: string, parameters?: ISqlCommandParameters): Promise<ISqlRow|null> {
|
||||
|
@ -42,7 +42,7 @@ export class SQLite3 implements IDatabaseConnector {
|
|||
|
||||
public async Run(sql: string, parameters?: ISqlCommandParameters): Promise<void> {
|
||||
log.silly("Run:", sql);
|
||||
return this.db.prepare(sql).run(parameters || []);
|
||||
this.db.prepare(sql).run(parameters || []);
|
||||
}
|
||||
|
||||
public async Close(): Promise<void> {
|
||||
|
@ -51,6 +51,6 @@ export class SQLite3 implements IDatabaseConnector {
|
|||
|
||||
public async Exec(sql: string): Promise<void> {
|
||||
log.silly("Exec:", sql);
|
||||
return this.db.exec(sql);
|
||||
this.db.exec(sql);
|
||||
}
|
||||
}
|
||||
|
|
295
src/discordas.ts
295
src/discordas.ts
|
@ -13,8 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Cli, Bridge, AppServiceRegistration, ClientFactory, BridgeContext } from "matrix-appservice-bridge";
|
||||
import { Appservice, IAppserviceRegistration, LogService } from "matrix-bot-sdk";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
|
@ -22,185 +21,188 @@ import { DiscordBot } from "./bot";
|
|||
import { DiscordStore } from "./store";
|
||||
import { Log } from "./log";
|
||||
import "source-map-support/register";
|
||||
import * as cliArgs from "command-line-args";
|
||||
import * as usage from "command-line-usage";
|
||||
import * as uuid from "uuid/v4";
|
||||
import { IMatrixEvent } from "./matrixtypes";
|
||||
import { MetricPeg, PrometheusBridgeMetrics } from "./metrics";
|
||||
|
||||
const log = new Log("DiscordAS");
|
||||
|
||||
const cli = new Cli({
|
||||
bridgeConfig: {
|
||||
affectsRegistration: true,
|
||||
schema: "./config/config.schema.yaml",
|
||||
},
|
||||
generateRegistration,
|
||||
registrationPath: "discord-registration.yaml",
|
||||
run,
|
||||
});
|
||||
const commandOptions = [
|
||||
{ name: "config", alias: "c", type: String },
|
||||
{ name: "url", alias: "u", type: String },
|
||||
{ name: "port", alias: "p", type: Number },
|
||||
{ name: "file", alias: "f", type: String },
|
||||
{ name: "generate-registration", alias: "r", type: Boolean },
|
||||
{ name: "help", alias: "h", type: Boolean },
|
||||
];
|
||||
|
||||
try {
|
||||
cli.run();
|
||||
} catch (err) {
|
||||
log.error("Failed to start bridge.");
|
||||
log.error(err);
|
||||
function generateRegistration(opts, registrationPath) {
|
||||
if (!opts.url) {
|
||||
throw Error("'url' not given in command line opts, cannot generate registration file");
|
||||
}
|
||||
const reg = {
|
||||
as_token: uuid(),
|
||||
hs_token: uuid(),
|
||||
id: "discord-bridge",
|
||||
namespaces: {
|
||||
aliases: [
|
||||
{
|
||||
exclusive: true,
|
||||
regex: "#_discord_.*",
|
||||
},
|
||||
],
|
||||
rooms: [ ],
|
||||
users: [
|
||||
{
|
||||
exclusive: true,
|
||||
regex: "@_discord_.*",
|
||||
},
|
||||
],
|
||||
},
|
||||
protocols: ["discord"],
|
||||
rate_limited: false,
|
||||
sender_localpart: "_discord_bot",
|
||||
url: opts.url,
|
||||
} as IAppserviceRegistration;
|
||||
fs.writeFileSync(registrationPath, yaml.safeDump(reg));
|
||||
}
|
||||
|
||||
function generateRegistration(reg, callback) {
|
||||
reg.setId(AppServiceRegistration.generateToken());
|
||||
reg.setHomeserverToken(AppServiceRegistration.generateToken());
|
||||
reg.setAppServiceToken(AppServiceRegistration.generateToken());
|
||||
reg.setSenderLocalpart("_discord_bot");
|
||||
reg.addRegexPattern("users", "@_discord_.*", true);
|
||||
reg.addRegexPattern("aliases", "#_discord_.*", true);
|
||||
reg.setRateLimited(false);
|
||||
reg.setProtocols(["discord"]);
|
||||
callback(reg);
|
||||
function setupLogging() {
|
||||
const logMap = new Map<string, Log>();
|
||||
// tslint:disable-next-line:no-any
|
||||
const logFunc = (level: string, module: string, args: any[]) => {
|
||||
if (!Array.isArray(args)) {
|
||||
args = [args];
|
||||
}
|
||||
if (args.find((s) => s.includes && s.includes("M_USER_IN_USE"))) {
|
||||
// Spammy logs begon
|
||||
return;
|
||||
}
|
||||
const mod = "bot-sdk" + module;
|
||||
let logger = logMap.get(mod);
|
||||
if (!logger) {
|
||||
logger = new Log(mod);
|
||||
logMap.set(mod, logger);
|
||||
}
|
||||
logger[level](args);
|
||||
};
|
||||
|
||||
LogService.setLogger({
|
||||
// tslint:disable-next-line:no-any
|
||||
debug: (mod: string, args: any[]) => logFunc("silly", mod, args),
|
||||
// tslint:disable-next-line:no-any
|
||||
error: (mod: string, args: any[]) => logFunc("error", mod, args),
|
||||
// tslint:disable-next-line:no-any
|
||||
info: (mod: string, args: any[]) => logFunc("info", mod, args),
|
||||
// tslint:disable-next-line:no-any
|
||||
warn: (mod: string, args: any[]) => logFunc("warn", mod, args),
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
type callbackFn = (...args: any[]) => Promise<any>;
|
||||
async function run() {
|
||||
const opts = cliArgs(commandOptions);
|
||||
if (opts.help) {
|
||||
/* tslint:disable:no-console */
|
||||
console.log(usage([
|
||||
{
|
||||
content: "The matrix appservice for discord",
|
||||
header: "Matrix Discord Bridge",
|
||||
},
|
||||
{
|
||||
header: "Options",
|
||||
optionList: commandOptions,
|
||||
},
|
||||
]));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const configPath = opts.config || "config.yaml";
|
||||
const registrationPath = opts.file || "discord-registration.yaml";
|
||||
if (opts["generate-registration"]) {
|
||||
if (fs.existsSync(registrationPath)) {
|
||||
throw Error("Not writing new registration file, file already exists");
|
||||
}
|
||||
generateRegistration(opts, registrationPath);
|
||||
return;
|
||||
}
|
||||
|
||||
async function run(port: number, fileConfig: DiscordBridgeConfig) {
|
||||
const config = new DiscordBridgeConfig();
|
||||
config.applyConfig(fileConfig);
|
||||
const readConfig = yaml.safeLoad(fs.readFileSync(configPath, "utf8"));
|
||||
if (typeof readConfig !== "object") {
|
||||
throw Error("Config is not of type object");
|
||||
}
|
||||
config.applyConfig(readConfig);
|
||||
config.applyEnvironmentOverrides(process.env);
|
||||
Log.Configure(config.logging);
|
||||
log.info("Starting Discord AS");
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync(cli.opts.registrationPath, "utf8"));
|
||||
const registration = AppServiceRegistration.fromObject(yamlConfig);
|
||||
if (registration === null) {
|
||||
throw new Error("Failed to parse registration file");
|
||||
const port = opts.port || config.bridge.port;
|
||||
if (!port) {
|
||||
throw Error("Port not given in command line or config file");
|
||||
}
|
||||
if (config.database.roomStorePath || config.database.userStorePath) {
|
||||
log.error("The keys 'roomStorePath' and/or 'userStorePath' is still defined in the config. " +
|
||||
"Please see docs/bridge-migrations.md on " +
|
||||
"https://github.com/Half-Shot/matrix-appservice-discord/");
|
||||
throw Error("Bridge has legacy configuration options and is unable to start");
|
||||
}
|
||||
const registration = yaml.safeLoad(fs.readFileSync(registrationPath, "utf8")) as IAppserviceRegistration;
|
||||
setupLogging();
|
||||
|
||||
const botUserId = `@${registration.sender_localpart}:${config.bridge.domain}`;
|
||||
const clientFactory = new ClientFactory({
|
||||
appServiceUserId: botUserId,
|
||||
token: registration.as_token,
|
||||
url: config.bridge.homeserverUrl,
|
||||
});
|
||||
const store = new DiscordStore(config.database);
|
||||
|
||||
const callbacks: { [id: string]: callbackFn; } = {};
|
||||
|
||||
const bridge = new Bridge({
|
||||
clientFactory,
|
||||
controller: {
|
||||
// onUserQuery: userQuery,
|
||||
onAliasQueried: async (alias: string, roomId: string) => {
|
||||
try {
|
||||
return await callbacks.onAliasQueried(alias, roomId);
|
||||
} catch (err) { log.error("Exception thrown while handling \"onAliasQueried\" event", err); }
|
||||
},
|
||||
onAliasQuery: async (alias: string, aliasLocalpart: string) => {
|
||||
try {
|
||||
return await callbacks.onAliasQuery(alias, aliasLocalpart);
|
||||
} catch (err) { log.error("Exception thrown while handling \"onAliasQuery\" event", err); }
|
||||
},
|
||||
onEvent: async (request) => {
|
||||
const data = request.getData();
|
||||
try {
|
||||
MetricPeg.get.registerRequest(data.event_id);
|
||||
// Build our own context.
|
||||
if (!store.roomStore) {
|
||||
log.warn("Discord store not ready yet, dropping message");
|
||||
MetricPeg.get.requestOutcome(data.event_id, false, "dropped");
|
||||
return;
|
||||
}
|
||||
const roomId = data.room_id;
|
||||
|
||||
const context: BridgeContext = {
|
||||
rooms: {},
|
||||
};
|
||||
|
||||
if (roomId) {
|
||||
const entries = await store.roomStore.getEntriesByMatrixId(roomId);
|
||||
context.rooms = entries[0] || {};
|
||||
}
|
||||
|
||||
await request.outcomeFrom(callbacks.onEvent(request, context));
|
||||
MetricPeg.get.requestOutcome(data.event_id, false, "success");
|
||||
} catch (err) {
|
||||
MetricPeg.get.requestOutcome(data.event_id, false, "fail");
|
||||
log.error("Exception thrown while handling \"onEvent\" event", err);
|
||||
await request.outcomeFrom(Promise.reject("Failed to handle"));
|
||||
}
|
||||
},
|
||||
onLog: (line, isError) => {
|
||||
log.verbose("matrix-appservice-bridge", line);
|
||||
},
|
||||
thirdPartyLookup: async () => {
|
||||
try {
|
||||
return await callbacks.thirdPartyLookup();
|
||||
} catch (err) {
|
||||
log.error("Exception thrown while handling \"thirdPartyLookup\" event", err);
|
||||
}
|
||||
},
|
||||
},
|
||||
disableContext: true,
|
||||
domain: config.bridge.domain,
|
||||
const appservice = new Appservice({
|
||||
bindAddress: config.bridge.bindAddress || "0.0.0.0",
|
||||
homeserverName: config.bridge.domain,
|
||||
homeserverUrl: config.bridge.homeserverUrl,
|
||||
intentOptions: {
|
||||
clients: {
|
||||
dontJoin: true, // handled manually
|
||||
},
|
||||
},
|
||||
// To avoid out of order message sending.
|
||||
queue: {
|
||||
perRequest: true,
|
||||
type: "per_room",
|
||||
},
|
||||
port,
|
||||
registration,
|
||||
// These must be kept for a while yet since we use them for migrations.
|
||||
roomStore: config.database.roomStorePath,
|
||||
userStore: config.database.userStorePath,
|
||||
storage: store,
|
||||
});
|
||||
|
||||
if (config.database.roomStorePath) {
|
||||
log.warn("[DEPRECATED] The room store is now part of the SQL database."
|
||||
+ "The config option roomStorePath no longer has any use.");
|
||||
}
|
||||
|
||||
if (config.database.userStorePath) {
|
||||
log.warn("[DEPRECATED] The user store is now part of the SQL database."
|
||||
+ "The config option userStorePath no longer has any use.");
|
||||
}
|
||||
|
||||
await bridge.run(port, config);
|
||||
log.info(`Started listening on port ${port}`);
|
||||
|
||||
if (config.bridge.enableMetrics) {
|
||||
if (config.metrics.enable) {
|
||||
log.info("Enabled metrics");
|
||||
MetricPeg.set(new PrometheusBridgeMetrics().init(bridge));
|
||||
MetricPeg.set(new PrometheusBridgeMetrics().init(appservice, config.metrics));
|
||||
}
|
||||
|
||||
try {
|
||||
await store.init(undefined, bridge.getRoomStore(), bridge.getUserStore());
|
||||
await store.init();
|
||||
} catch (ex) {
|
||||
log.error("Failed to init database. Exiting.", ex);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const discordbot = new DiscordBot(botUserId, config, bridge, store);
|
||||
const discordbot = new DiscordBot(config, appservice, store);
|
||||
const roomhandler = discordbot.RoomHandler;
|
||||
const eventProcessor = discordbot.MxEventProcessor;
|
||||
|
||||
try {
|
||||
callbacks.onAliasQueried = roomhandler.OnAliasQueried.bind(roomhandler);
|
||||
callbacks.onAliasQuery = roomhandler.OnAliasQuery.bind(roomhandler);
|
||||
callbacks.onEvent = eventProcessor.OnEvent.bind(eventProcessor);
|
||||
callbacks.thirdPartyLookup = async () => {
|
||||
return roomhandler.ThirdPartyLookup;
|
||||
};
|
||||
} catch (err) {
|
||||
log.error("Failed to register callbacks. Exiting.", err);
|
||||
process.exit(1);
|
||||
}
|
||||
// tslint:disable-next-line:no-any
|
||||
appservice.on("query.room", async (roomAlias: string, createRoom: (opts: any) => Promise<void>) => {
|
||||
try {
|
||||
const createRoomOpts = await roomhandler.OnAliasQuery(roomAlias);
|
||||
await createRoom(createRoomOpts);
|
||||
await roomhandler.OnAliasQueried(roomAlias, createRoomOpts.__roomId);
|
||||
} catch (err) {
|
||||
log.error("Exception thrown while handling \"query.room\" event", err);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("Initing bridge");
|
||||
appservice.on("room.event", async (roomId: string, event: IMatrixEvent) => {
|
||||
try {
|
||||
const entries = await store.roomStore.getEntriesByMatrixId(roomId);
|
||||
await eventProcessor.OnEvent(event, entries);
|
||||
} catch (err) {
|
||||
log.error("Exception thrown while handling \"room.event\" event", err);
|
||||
}
|
||||
});
|
||||
|
||||
roomhandler.bindThirdparty();
|
||||
|
||||
await appservice.begin();
|
||||
log.info(`Started listening on port ${port}`);
|
||||
|
||||
try {
|
||||
log.info("Initing store.");
|
||||
await discordbot.init();
|
||||
log.info(`Started listening on port ${port}.`);
|
||||
log.info("Initing bot.");
|
||||
await discordbot.run();
|
||||
log.info("Discordbot started successfully");
|
||||
} catch (err) {
|
||||
|
@ -209,3 +211,8 @@ async function run(port: number, fileConfig: DiscordBridgeConfig) {
|
|||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
log.error("A fatal error occurred during startup:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -15,16 +15,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { DiscordBot } from "./bot";
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
|
||||
import { Bridge } from "matrix-appservice-bridge";
|
||||
import { Log } from "./log";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
|
||||
const log = new Log("DiscordCommandHandler");
|
||||
|
||||
export class DiscordCommandHandler {
|
||||
constructor(
|
||||
private bridge: Bridge,
|
||||
private bridge: Appservice,
|
||||
private discord: DiscordBot,
|
||||
) { }
|
||||
|
||||
|
@ -34,8 +34,14 @@ export class DiscordCommandHandler {
|
|||
await msg.channel.send("**ERROR:** only available for guild channels");
|
||||
return;
|
||||
}
|
||||
if (!msg.member) {
|
||||
await msg.channel.send("**ERROR:** could not determine message member");
|
||||
return;
|
||||
}
|
||||
|
||||
const intent = this.bridge.getIntent();
|
||||
const discordMember = msg.member;
|
||||
|
||||
const intent = this.bridge.botIntent;
|
||||
|
||||
const actions: ICommandActions = {
|
||||
approve: {
|
||||
|
@ -43,7 +49,7 @@ export class DiscordCommandHandler {
|
|||
params: [],
|
||||
permission: "MANAGE_WEBHOOKS",
|
||||
run: async () => {
|
||||
if (await this.discord.Provisioner.MarkApproved(chan, msg.member, true)) {
|
||||
if (await this.discord.Provisioner.MarkApproved(chan, discordMember, true)) {
|
||||
return "Thanks for your response! The matrix bridge has been approved";
|
||||
} else {
|
||||
return "Thanks for your response, however" +
|
||||
|
@ -62,7 +68,7 @@ export class DiscordCommandHandler {
|
|||
params: [],
|
||||
permission: "MANAGE_WEBHOOKS",
|
||||
run: async () => {
|
||||
if (await this.discord.Provisioner.MarkApproved(chan, msg.member, false)) {
|
||||
if (await this.discord.Provisioner.MarkApproved(chan, discordMember, false)) {
|
||||
return "Thanks for your response! The matrix bridge has been declined";
|
||||
} else {
|
||||
return "Thanks for your response, however" +
|
||||
|
@ -105,7 +111,7 @@ export class DiscordCommandHandler {
|
|||
if (!Array.isArray(permission)) {
|
||||
permission = [permission];
|
||||
}
|
||||
return permission.every((p) => msg.member.hasPermission(p as Discord.PermissionResolvable));
|
||||
return permission.every((p) => discordMember.hasPermission(p as Discord.PermissionResolvable));
|
||||
};
|
||||
|
||||
const reply = await Util.ParseCommand("!matrix", msg.content, actions, parameters, permissionCheck);
|
||||
|
@ -115,7 +121,7 @@ export class DiscordCommandHandler {
|
|||
private ModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: "kick"|"ban"|"unban") {
|
||||
return async ({name}) => {
|
||||
let allChannelMxids: string[] = [];
|
||||
await Promise.all(discordChannel.guild.channels.map(async (chan) => {
|
||||
await Promise.all(discordChannel.guild.channels.cache.map(async (chan) => {
|
||||
try {
|
||||
const chanMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan);
|
||||
allChannelMxids = allChannelMxids.concat(chanMxids);
|
||||
|
@ -125,9 +131,8 @@ export class DiscordCommandHandler {
|
|||
}));
|
||||
let errorMsg = "";
|
||||
await Promise.all(allChannelMxids.map(async (chanMxid) => {
|
||||
const intent = this.bridge.getIntent();
|
||||
try {
|
||||
await intent[funcKey](chanMxid, name);
|
||||
await this.bridge.botIntent.underlyingClient[funcKey + "User"](chanMxid, name);
|
||||
} catch (e) {
|
||||
// maybe we don't have permission to kick/ban/unban...?
|
||||
errorMsg += `\nCouldn't ${funcKey} ${name} from ${chanMxid}`;
|
||||
|
|
|
@ -14,337 +14,78 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import * as markdown from "discord-markdown";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { DiscordBot } from "./bot";
|
||||
import * as escapeHtml from "escape-html";
|
||||
import { Util } from "./util";
|
||||
import { Bridge } from "matrix-appservice-bridge";
|
||||
|
||||
import { Log } from "./log";
|
||||
import {
|
||||
DiscordMessageParser,
|
||||
IDiscordMessageParserOpts,
|
||||
IDiscordMessageParserCallbacks,
|
||||
IDiscordMessageParserResult,
|
||||
} from "matrix-discord-parser";
|
||||
|
||||
const log = new Log("DiscordMessageProcessor");
|
||||
|
||||
const MATRIX_TO_LINK = "https://matrix.to/#/";
|
||||
// somehow the regex works properly if it isn't global
|
||||
// as we replace the match fully anyways this shouldn't be an issue
|
||||
const MXC_INSERT_REGEX = /\x01emoji\x01(\w+)\x01([01])\x01([0-9]*)\x01/;
|
||||
const NAME_MXC_INSERT_REGEX_GROUP = 1;
|
||||
const ANIMATED_MXC_INSERT_REGEX_GROUP = 2;
|
||||
const ID_MXC_INSERT_REGEX_GROUP = 3;
|
||||
const EMOJI_SIZE = 32;
|
||||
const MAX_EDIT_MSG_LENGTH = 50;
|
||||
|
||||
// same as above, no global flag here, too
|
||||
const CHANNEL_INSERT_REGEX = /\x01chan\x01([0-9]*)\x01/;
|
||||
const ID_CHANNEL_INSERT_REGEX = 1;
|
||||
|
||||
export class DiscordMessageProcessorOpts {
|
||||
constructor(readonly domain: string, readonly bot?: DiscordBot) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscordMessageProcessorResult {
|
||||
public formattedBody: string;
|
||||
public body: string;
|
||||
public msgtype: string;
|
||||
}
|
||||
|
||||
interface IDiscordNode {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IEmojiNode extends IDiscordNode {
|
||||
animated: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class DiscordMessageProcessor {
|
||||
private readonly opts: DiscordMessageProcessorOpts;
|
||||
constructor(opts: DiscordMessageProcessorOpts, bot: DiscordBot | null = null) {
|
||||
// Backwards compat
|
||||
if (bot !== null) {
|
||||
this.opts = new DiscordMessageProcessorOpts(opts.domain, bot);
|
||||
} else {
|
||||
this.opts = opts;
|
||||
}
|
||||
private parser: DiscordMessageParser;
|
||||
constructor(private domain: string, private bot: DiscordBot) {
|
||||
this.parser = new DiscordMessageParser();
|
||||
}
|
||||
|
||||
public async FormatMessage(msg: Discord.Message): Promise<DiscordMessageProcessorResult> {
|
||||
const result = new DiscordMessageProcessorResult();
|
||||
|
||||
let content = msg.content;
|
||||
|
||||
// for the formatted body we need to parse markdown first
|
||||
// as else it'll HTML escape the result of the discord syntax
|
||||
let contentPostmark = markdown.toHTML(content, {
|
||||
discordCallback: this.getDiscordParseCallbacksHTML(msg),
|
||||
});
|
||||
|
||||
// parse the plain text stuff
|
||||
content = markdown.toHTML(content, {
|
||||
discordCallback: this.getDiscordParseCallbacks(msg),
|
||||
discordOnly: true,
|
||||
escapeHTML: false,
|
||||
});
|
||||
content = this.InsertEmbeds(content, msg);
|
||||
content = await this.InsertMxcImages(content, msg);
|
||||
content = await this.InsertChannelPills(content, msg);
|
||||
|
||||
// parse postmark stuff
|
||||
contentPostmark = this.InsertEmbedsPostmark(contentPostmark, msg);
|
||||
contentPostmark = await this.InsertMxcImages(contentPostmark, msg, true);
|
||||
contentPostmark = await this.InsertChannelPills(contentPostmark, msg, true);
|
||||
|
||||
result.body = content;
|
||||
result.formattedBody = contentPostmark;
|
||||
result.msgtype = msg.author.bot ? "m.notice" : "m.text";
|
||||
return result;
|
||||
public async FormatMessage(msg: Discord.Message): Promise<IDiscordMessageParserResult> {
|
||||
const opts = {
|
||||
callbacks: this.getParserCallbacks(msg),
|
||||
} as IDiscordMessageParserOpts;
|
||||
return await this.parser.FormatMessage(opts, msg);
|
||||
}
|
||||
|
||||
public async FormatEdit(
|
||||
oldMsg: Discord.Message,
|
||||
newMsg: Discord.Message,
|
||||
link?: string,
|
||||
): Promise<DiscordMessageProcessorResult> {
|
||||
oldMsg.embeds = []; // we don't want embeds on old msg
|
||||
const oldMsgParsed = await this.FormatMessage(oldMsg);
|
||||
const newMsgParsed = await this.FormatMessage(newMsg);
|
||||
const result = new DiscordMessageProcessorResult();
|
||||
result.body = `*edit:* ~~${oldMsgParsed.body}~~ -> ${newMsgParsed.body}`;
|
||||
result.msgtype = newMsgParsed.msgtype;
|
||||
oldMsg.content = `*edit:* ~~${oldMsg.content}~~ -> ${newMsg.content}`;
|
||||
const linkStart = link ? `<a href="${escapeHtml(link)}">` : "";
|
||||
const linkEnd = link ? "</a>" : "";
|
||||
if (oldMsg.content.includes("\n") || newMsg.content.includes("\n")
|
||||
|| newMsg.content.length > MAX_EDIT_MSG_LENGTH) {
|
||||
result.formattedBody = `<p>${linkStart}<em>edit:</em>${linkEnd}</p><p><del>${oldMsgParsed.formattedBody}` +
|
||||
`</del></p><hr><p>${newMsgParsed.formattedBody}</p>`;
|
||||
} else {
|
||||
result.formattedBody = `${linkStart}<em>edit:</em>${linkEnd} <del>${oldMsgParsed.formattedBody}</del>` +
|
||||
` -> ${newMsgParsed.formattedBody}`;
|
||||
}
|
||||
return result;
|
||||
msg1: Discord.Message,
|
||||
msg2: Discord.Message,
|
||||
link: string,
|
||||
): Promise<IDiscordMessageParserResult> {
|
||||
// obsolete once edit PR is merged
|
||||
const opts = {
|
||||
callbacks: this.getParserCallbacks(msg2),
|
||||
} as IDiscordMessageParserOpts;
|
||||
return await this.parser.FormatEdit(opts, msg1, msg2, link);
|
||||
}
|
||||
|
||||
public InsertEmbeds(content: string, msg: Discord.Message): string {
|
||||
for (const embed of msg.embeds) {
|
||||
if (embed.title === undefined && embed.description === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (this.isEmbedInBody(msg, embed)) {
|
||||
continue;
|
||||
}
|
||||
let embedContent = "\n\n----"; // Horizontal rule. Two to make sure the content doesn't become a title.
|
||||
const embedTitle = embed.url ? `[${embed.title}](${embed.url})` : embed.title;
|
||||
if (embedTitle) {
|
||||
embedContent += "\n##### " + embedTitle; // h5 is probably best.
|
||||
}
|
||||
if (embed.description) {
|
||||
embedContent += "\n" + markdown.toHTML(embed.description, {
|
||||
discordCallback: this.getDiscordParseCallbacks(msg),
|
||||
discordOnly: true,
|
||||
escapeHTML: false,
|
||||
});
|
||||
}
|
||||
if (embed.fields) {
|
||||
for (const field of embed.fields) {
|
||||
embedContent += `\n**${field.name}**\n`;
|
||||
embedContent += markdown.toHTML(field.value, {
|
||||
discordCallback: this.getDiscordParseCallbacks(msg),
|
||||
discordOnly: true,
|
||||
escapeHTML: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (embed.image) {
|
||||
embedContent += "\nImage: " + embed.image.url;
|
||||
}
|
||||
if (embed.footer) {
|
||||
embedContent += "\n" + markdown.toHTML(embed.footer.text, {
|
||||
discordCallback: this.getDiscordParseCallbacks(msg),
|
||||
discordOnly: true,
|
||||
escapeHTML: false,
|
||||
});
|
||||
}
|
||||
content += embedContent;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public InsertEmbedsPostmark(content: string, msg: Discord.Message): string {
|
||||
for (const embed of msg.embeds) {
|
||||
if (embed.title === undefined && embed.description === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (this.isEmbedInBody(msg, embed)) {
|
||||
continue;
|
||||
}
|
||||
let embedContent = "<hr>"; // Horizontal rule. Two to make sure the content doesn't become a title.
|
||||
const embedTitle = embed.url ?
|
||||
`<a href="${escapeHtml(embed.url)}">${escapeHtml(embed.title)}</a>`
|
||||
: (embed.title ? escapeHtml(embed.title) : undefined);
|
||||
if (embedTitle) {
|
||||
embedContent += `<h5>${embedTitle}</h5>`; // h5 is probably best.
|
||||
}
|
||||
if (embed.description) {
|
||||
embedContent += "<p>";
|
||||
embedContent += markdown.toHTML(embed.description, {
|
||||
discordCallback: this.getDiscordParseCallbacksHTML(msg),
|
||||
embed: true,
|
||||
}) + "</p>";
|
||||
}
|
||||
if (embed.fields) {
|
||||
for (const field of embed.fields) {
|
||||
embedContent += `<p><strong>${escapeHtml(field.name)}</strong><br>`;
|
||||
embedContent += markdown.toHTML(field.value, {
|
||||
discordCallback: this.getDiscordParseCallbacks(msg),
|
||||
embed: true,
|
||||
}) + "</p>";
|
||||
}
|
||||
}
|
||||
if (embed.image) {
|
||||
const imgUrl = escapeHtml(embed.image.url);
|
||||
embedContent += `<p>Image: <a href="${imgUrl}">${imgUrl}</a></p>`;
|
||||
}
|
||||
if (embed.footer) {
|
||||
embedContent += "<p>";
|
||||
embedContent += markdown.toHTML(embed.footer.text, {
|
||||
discordCallback: this.getDiscordParseCallbacksHTML(msg),
|
||||
embed: true,
|
||||
}) + "</p>";
|
||||
}
|
||||
content += embedContent;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public InsertUser(node: IDiscordNode, msg: Discord.Message, html: boolean = false): string {
|
||||
const id = node.id;
|
||||
const member = msg.guild.members.get(id);
|
||||
const memberId = `@_discord_${id}:${this.opts.domain}`;
|
||||
const memberName = member ? member.displayName : memberId;
|
||||
if (!html) {
|
||||
return memberName;
|
||||
}
|
||||
return `<a href="${MATRIX_TO_LINK}${escapeHtml(memberId)}">${escapeHtml(memberName)}</a>`;
|
||||
}
|
||||
|
||||
public InsertChannel(node: IDiscordNode): string {
|
||||
// unfortunately these callbacks are sync, so we flag our channel with some special stuff
|
||||
// and later on grab the real channel pill async
|
||||
const FLAG = "\x01";
|
||||
return `${FLAG}chan${FLAG}${node.id}${FLAG}`;
|
||||
}
|
||||
|
||||
public InsertRole(node: IDiscordNode, msg: Discord.Message, html: boolean = false): string {
|
||||
const id = node.id;
|
||||
const role = msg.guild.roles.get(id);
|
||||
if (!role) {
|
||||
return html ? `<@&${id}>` : `<@&${id}>`;
|
||||
}
|
||||
if (!html) {
|
||||
return `@${role.name}`;
|
||||
}
|
||||
const color = Util.NumberToHTMLColor(role.color);
|
||||
return `<span data-mx-color="${color}"><strong>@${escapeHtml(role.name)}</strong></span>`;
|
||||
}
|
||||
|
||||
public InsertEmoji(node: IEmojiNode): string {
|
||||
// unfortunately these callbacks are sync, so we flag our url with some special stuff
|
||||
// and later on grab the real url async
|
||||
const FLAG = "\x01";
|
||||
return `${FLAG}emoji${FLAG}${node.name}${FLAG}${node.animated ? 1 : 0}${FLAG}${node.id}${FLAG}`;
|
||||
}
|
||||
|
||||
public InsertRoom(msg: Discord.Message, def: string): string {
|
||||
return msg.mentions.everyone ? "@room" : def;
|
||||
}
|
||||
|
||||
public async InsertMxcImages(content: string, msg: Discord.Message, html: boolean = false): Promise<string> {
|
||||
let results = MXC_INSERT_REGEX.exec(content);
|
||||
while (results !== null) {
|
||||
const name = results[NAME_MXC_INSERT_REGEX_GROUP];
|
||||
const animated = results[ANIMATED_MXC_INSERT_REGEX_GROUP] === "1";
|
||||
const id = results[ID_MXC_INSERT_REGEX_GROUP];
|
||||
let replace = "";
|
||||
const nameHtml = escapeHtml(name);
|
||||
try {
|
||||
const mxcUrl = await this.opts.bot!.GetEmoji(name, animated, id);
|
||||
if (html) {
|
||||
replace = `<img alt="${nameHtml}" title="${nameHtml}" ` +
|
||||
`height="${EMOJI_SIZE}" src="${mxcUrl}" />`;
|
||||
} else {
|
||||
replace = `:${name}:`;
|
||||
}
|
||||
} catch (ex) {
|
||||
log.warn(
|
||||
`Could not insert emoji ${id} for msg ${msg.id} in guild ${msg.guild.id}: ${ex}`,
|
||||
);
|
||||
if (html) {
|
||||
replace = `<${animated ? "a" : ""}:${nameHtml}:${id}>`;
|
||||
} else {
|
||||
replace = `<${animated ? "a" : ""}:${name}:${id}>`;
|
||||
}
|
||||
}
|
||||
content = content.replace(results[0], replace);
|
||||
results = MXC_INSERT_REGEX.exec(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public async InsertChannelPills(content: string, msg: Discord.Message, html: boolean = false): Promise<string> {
|
||||
let results = CHANNEL_INSERT_REGEX.exec(content);
|
||||
while (results !== null) {
|
||||
const id = results[ID_CHANNEL_INSERT_REGEX];
|
||||
let replace = "";
|
||||
const channel = msg.guild.channels.get(id);
|
||||
if (channel) {
|
||||
const alias = await this.opts.bot!.ChannelSyncroniser.GetAliasFromChannel(channel);
|
||||
if (alias) {
|
||||
const name = "#" + channel.name;
|
||||
replace = html ? `<a href="${MATRIX_TO_LINK}${escapeHtml(alias)}">${escapeHtml(name)}</a>` : name;
|
||||
}
|
||||
}
|
||||
if (!replace) {
|
||||
replace = html ? `<#${escapeHtml(id)}>` : `<#${id}>`;
|
||||
}
|
||||
content = content.replace(results[0], replace);
|
||||
results = CHANNEL_INSERT_REGEX.exec(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private isEmbedInBody(msg: Discord.Message, embed: Discord.MessageEmbed): boolean {
|
||||
if (!embed.url) {
|
||||
return false;
|
||||
}
|
||||
let url = embed.url;
|
||||
if (url.substr(url.length - 1) === "/") {
|
||||
url = url.substr(0, url.length - 1);
|
||||
}
|
||||
return msg.content.includes(url);
|
||||
}
|
||||
|
||||
private getDiscordParseCallbacks(msg: Discord.Message) {
|
||||
private getParserCallbacks(msg: Discord.Message): IDiscordMessageParserCallbacks {
|
||||
return {
|
||||
channel: (node) => this.InsertChannel(node), // are post-inserted
|
||||
emoji: (node) => this.InsertEmoji(node), // are post-inserted
|
||||
everyone: (_) => this.InsertRoom(msg, "@everyone"),
|
||||
here: (_) => this.InsertRoom(msg, "@here"),
|
||||
role: (node) => this.InsertRole(node, msg),
|
||||
user: (node) => this.InsertUser(node, msg),
|
||||
getChannel: async (id: string) => {
|
||||
const channel = msg.guild?.channels.resolve(id);
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
const alias = await this.bot.ChannelSyncroniser.GetAliasFromChannel(channel);
|
||||
if (!alias) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
mxid: alias,
|
||||
name: channel.name,
|
||||
};
|
||||
},
|
||||
getEmoji: async (name: string, animated: boolean, id: string) => {
|
||||
try {
|
||||
const mxcUrl = await this.bot.GetEmoji(name, animated, id);
|
||||
return mxcUrl;
|
||||
} catch (ex) {
|
||||
log.warn(`Could not get emoji ${id} with name ${name}`, ex);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getUser: async (id: string) => {
|
||||
const member = msg.guild?.members.resolve(id);
|
||||
const mxid = `@_discord_${id}:${this.domain}`;
|
||||
const name = member ? member.displayName : mxid;
|
||||
return {
|
||||
mxid,
|
||||
name,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getDiscordParseCallbacksHTML(msg: Discord.Message) {
|
||||
return {
|
||||
channel: (node) => this.InsertChannel(node), // are post-inserted
|
||||
emoji: (node) => this.InsertEmoji(node), // are post-inserted
|
||||
everyone: (_) => this.InsertRoom(msg, "@everyone"),
|
||||
here: (_) => this.InsertRoom(msg, "@here"),
|
||||
role: (node) => this.InsertRole(node, msg, true),
|
||||
user: (node) => this.InsertUser(node, msg, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
import { DiscordBot } from "./bot";
|
||||
import { Log } from "./log";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
import { Bridge, BridgeContext } from "matrix-appservice-bridge";
|
||||
import { IMatrixEvent } from "./matrixtypes";
|
||||
import { Provisioner } from "./provisioner";
|
||||
import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
|
||||
import * as Discord from "discord.js";
|
||||
import * as markdown from "discord-markdown";
|
||||
import { RemoteStoreRoom } from "./db/roomstore";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { IRoomStoreEntry } from "./db/roomstore";
|
||||
import * as markdown from "marked";
|
||||
const log = new Log("MatrixCommandHandler");
|
||||
|
||||
/* tslint:disable:no-magic-numbers */
|
||||
|
@ -38,7 +38,7 @@ export class MatrixCommandHandler {
|
|||
private provisioner: Provisioner;
|
||||
constructor(
|
||||
private discord: DiscordBot,
|
||||
private bridge: Bridge,
|
||||
private bridge: Appservice,
|
||||
private config: DiscordBridgeConfig,
|
||||
) {
|
||||
this.provisioner = this.discord.Provisioner;
|
||||
|
@ -46,11 +46,11 @@ export class MatrixCommandHandler {
|
|||
|
||||
public async HandleInvite(event: IMatrixEvent) {
|
||||
log.info(`Received invite for ${event.state_key} in room ${event.room_id}`);
|
||||
await this.bridge.getIntent().join(event.room_id);
|
||||
await this.bridge.botIntent.joinRoom(event.room_id);
|
||||
this.botJoinedRooms.add(event.room_id);
|
||||
}
|
||||
|
||||
public async Process(event: IMatrixEvent, context: BridgeContext) {
|
||||
public async Process(event: IMatrixEvent, roomEntry: IRoomStoreEntry|null) {
|
||||
if (!(await this.isBotInRoom(event.room_id))) {
|
||||
log.warn(`Bot is not in ${event.room_id}. Ignoring command`);
|
||||
return;
|
||||
|
@ -78,7 +78,7 @@ export class MatrixCommandHandler {
|
|||
subcat: "m.room.power_levels",
|
||||
},
|
||||
run: async ({guildId, channelId}) => {
|
||||
if (context.rooms.remote) {
|
||||
if (roomEntry && roomEntry.remote) {
|
||||
return "This room is already bridged to a Discord guild.";
|
||||
}
|
||||
if (!guildId || !channelId) {
|
||||
|
@ -89,10 +89,11 @@ export class MatrixCommandHandler {
|
|||
const channel = discordResult.channel as Discord.TextChannel;
|
||||
|
||||
log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`);
|
||||
this.bridge.getIntent().sendMessage(event.room_id, {
|
||||
body: "I'm asking permission from the guild administrators to make this bridge.",
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
await this.bridge.botIntent.sendText(
|
||||
event.room_id,
|
||||
"I'm asking permission from the guild administrators to make this bridge.",
|
||||
"m.notice",
|
||||
);
|
||||
|
||||
await this.provisioner.AskBridgePermission(channel, event.sender);
|
||||
await this.provisioner.BridgeMatrixRoom(channel, event.room_id);
|
||||
|
@ -119,16 +120,15 @@ export class MatrixCommandHandler {
|
|||
subcat: "m.room.power_levels",
|
||||
},
|
||||
run: async () => {
|
||||
const remoteRoom = context.rooms.remote as RemoteStoreRoom;
|
||||
if (!remoteRoom) {
|
||||
if (!roomEntry || !roomEntry.remote) {
|
||||
return "This room is not bridged.";
|
||||
}
|
||||
if (!remoteRoom.data.plumbed) {
|
||||
if (!roomEntry.remote.data.plumbed) {
|
||||
return "This room cannot be unbridged.";
|
||||
}
|
||||
const res = await this.discord.LookupRoom(
|
||||
remoteRoom.data.discord_guild!,
|
||||
remoteRoom.data.discord_channel!,
|
||||
roomEntry.remote.data.discord_guild!,
|
||||
roomEntry.remote.data.discord_channel!,
|
||||
);
|
||||
try {
|
||||
await this.provisioner.UnbridgeChannel(res.channel, event.room_id);
|
||||
|
@ -177,7 +177,7 @@ export class MatrixCommandHandler {
|
|||
return "The owner of this bridge does not permit self-service bridging.";
|
||||
}
|
||||
return await Util.CheckMatrixPermission(
|
||||
this.bridge.getIntent().getClient(),
|
||||
this.bridge.botClient,
|
||||
event.sender,
|
||||
event.room_id,
|
||||
permission.level,
|
||||
|
@ -187,9 +187,8 @@ export class MatrixCommandHandler {
|
|||
};
|
||||
|
||||
const reply = await Util.ParseCommand("!discord", event.content!.body!, actions, parameters, permissionCheck);
|
||||
const formattedReply = markdown.toHTML(reply);
|
||||
|
||||
await this.bridge.getIntent().sendMessage(event.room_id, {
|
||||
const formattedReply = markdown(reply);
|
||||
await this.bridge.botClient.sendMessage(event.room_id, {
|
||||
body: reply,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: formattedReply,
|
||||
|
@ -204,8 +203,7 @@ export class MatrixCommandHandler {
|
|||
try {
|
||||
log.verbose("Got new room cache for bot");
|
||||
this.botJoinedRoomsCacheUpdatedAt = Date.now();
|
||||
const rooms = (await this.bridge.getBot().getJoinedRooms()) as string[];
|
||||
this.botJoinedRooms = new Set(rooms);
|
||||
this.botJoinedRooms = new Set(await this.bridge.botIntent.underlyingClient.getJoinedRooms());
|
||||
} catch (e) {
|
||||
log.error("Failed to get room cache for bot, ", e);
|
||||
return false;
|
||||
|
|
|
@ -14,22 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { DiscordBot } from "./bot";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
import * as escapeStringRegexp from "escape-string-regexp";
|
||||
import { Util } from "./util";
|
||||
import { Util, wrapError } from "./util";
|
||||
import * as path from "path";
|
||||
import * as mime from "mime";
|
||||
import { MatrixUser, Bridge, BridgeContext } from "matrix-appservice-bridge";
|
||||
import { Client as MatrixClient } from "matrix-js-sdk";
|
||||
import { IMatrixEvent, IMatrixEventContent, IMatrixMessage } from "./matrixtypes";
|
||||
import { MatrixMessageProcessor, IMatrixMessageProcessorParams } from "./matrixmessageprocessor";
|
||||
import { MatrixCommandHandler } from "./matrixcommandhandler";
|
||||
import { DbEvent } from "./db/dbdataevent";
|
||||
|
||||
import { Log } from "./log";
|
||||
import { IRoomStoreEntry, RemoteStoreRoom } from "./db/roomstore";
|
||||
import { Appservice, MatrixClient } from "matrix-bot-sdk";
|
||||
import { DiscordStore } from "./store";
|
||||
import { TimedCache } from "./structures/timedcache";
|
||||
import { MetricPeg } from "./metrics";
|
||||
|
||||
const log = new Log("MatrixEventProcessor");
|
||||
|
||||
const MaxFileSize = 8000000;
|
||||
|
@ -44,22 +45,25 @@ const PROFILE_CACHE_LIFETIME = 900000;
|
|||
export class MatrixEventProcessorOpts {
|
||||
constructor(
|
||||
readonly config: DiscordBridgeConfig,
|
||||
readonly bridge: Bridge,
|
||||
readonly bridge: Appservice,
|
||||
readonly discord: DiscordBot,
|
||||
readonly store: DiscordStore,
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface IMatrixEventProcessorResult {
|
||||
messageEmbed: Discord.RichEmbed;
|
||||
replyEmbed?: Discord.RichEmbed;
|
||||
messageEmbed: Discord.MessageEmbed;
|
||||
replyEmbed?: Discord.MessageEmbed;
|
||||
imageEmbed?: Discord.MessageEmbed;
|
||||
}
|
||||
|
||||
export class MatrixEventProcessor {
|
||||
private config: DiscordBridgeConfig;
|
||||
private bridge: Bridge;
|
||||
private bridge: Appservice;
|
||||
private discord: DiscordBot;
|
||||
private store: DiscordStore;
|
||||
private matrixMsgProcessor: MatrixMessageProcessor;
|
||||
private mxCommandHandler: MatrixCommandHandler;
|
||||
private mxUserProfileCache: TimedCache<string, {displayname: string, avatar_url: string|undefined}>;
|
||||
|
@ -67,8 +71,10 @@ export class MatrixEventProcessor {
|
|||
constructor(opts: MatrixEventProcessorOpts, cm?: MatrixCommandHandler) {
|
||||
this.config = opts.config;
|
||||
this.bridge = opts.bridge;
|
||||
this.store = opts.store;
|
||||
this.discord = opts.discord;
|
||||
this.matrixMsgProcessor = new MatrixMessageProcessor(this.discord);
|
||||
this.store = opts.store;
|
||||
this.matrixMsgProcessor = new MatrixMessageProcessor(this.discord, this.config);
|
||||
this.mxUserProfileCache = new TimedCache(PROFILE_CACHE_LIFETIME);
|
||||
if (cm) {
|
||||
this.mxCommandHandler = cm;
|
||||
|
@ -77,27 +83,39 @@ export class MatrixEventProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
public async OnEvent(request, context: BridgeContext): Promise<void> {
|
||||
const event = request.getData() as IMatrixEvent;
|
||||
/**
|
||||
* Callback which is called when the HS notifies the bridge of a new event.
|
||||
*
|
||||
* @param request Request object containing the event for which this callback is called.
|
||||
* @param context The current context of the bridge.
|
||||
* @throws {Unstable.EventNotHandledError} When the event can finally not be handled.
|
||||
*/
|
||||
public async OnEvent(event: IMatrixEvent, rooms: IRoomStoreEntry[]): Promise<void> {
|
||||
const remoteRoom = rooms[0];
|
||||
if (event.unsigned.age > AGE_LIMIT) {
|
||||
log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`);
|
||||
MetricPeg.get.requestOutcome(event.event_id, false, "dropped");
|
||||
log.info(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`);
|
||||
// throw new Unstable.EventTooOldError(
|
||||
// `Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`,
|
||||
// );
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.type === "m.room.member" &&
|
||||
event.content!.membership === "invite" &&
|
||||
event.state_key === this.bridge.getClientFactory()._botUserId
|
||||
event.state_key === this.bridge.botUserId
|
||||
) {
|
||||
await this.mxCommandHandler.HandleInvite(event);
|
||||
return;
|
||||
} else if (event.type === "m.room.member" && this.bridge.getBot().isRemoteUser(event.state_key)) {
|
||||
} else if (event.type === "m.room.member" && this.bridge.isNamespacedUser(event.state_key)) {
|
||||
if (["leave", "ban"].includes(event.content!.membership!) && event.sender !== event.state_key) {
|
||||
// Kick/Ban handling
|
||||
let prevMembership = "";
|
||||
if (event.content!.membership === "leave") {
|
||||
const intent = this.bridge.getIntent();
|
||||
prevMembership = (await intent.getEvent(event.room_id, event.replaces_state)).content.membership;
|
||||
if (event.content!.membership === "leave" && event.replaces_state) {
|
||||
const intent = this.bridge.botIntent;
|
||||
prevMembership = (await intent.underlyingClient.getEvent(
|
||||
event.room_id,
|
||||
event.replaces_state,
|
||||
)).content.membership;
|
||||
}
|
||||
await this.discord.HandleMatrixKickBan(
|
||||
event.room_id,
|
||||
|
@ -109,46 +127,40 @@ export class MatrixEventProcessor {
|
|||
);
|
||||
}
|
||||
return;
|
||||
} else if (this.bridge.isNamespacedUser(event.sender)) {
|
||||
// Ignore echo
|
||||
return;
|
||||
} else if (["m.room.member", "m.room.name", "m.room.topic"].includes(event.type)) {
|
||||
await this.ProcessStateEvent(event);
|
||||
return;
|
||||
} else if (event.type === "m.room.redaction" && context.rooms.remote) {
|
||||
} else if (event.type === "m.room.redaction" && remoteRoom) {
|
||||
await this.discord.ProcessMatrixRedact(event);
|
||||
return;
|
||||
} else if (event.type === "m.room.message" || event.type === "m.sticker") {
|
||||
log.verbose(`Got ${event.type} event`);
|
||||
const isBotCommand = event.type === "m.room.message" &&
|
||||
event.content!.body &&
|
||||
event.content!.body!.startsWith("!discord");
|
||||
if (isBotCommand) {
|
||||
await this.mxCommandHandler.Process(event, context);
|
||||
} else if (context.rooms.remote) {
|
||||
const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
|
||||
if (isBotCommand(event)) {
|
||||
await this.mxCommandHandler.Process(event, remoteRoom);
|
||||
} else if (remoteRoom) {
|
||||
try {
|
||||
await this.ProcessMsgEvent(event, srvChanPair[0], srvChanPair[1]);
|
||||
await this.ProcessMsgEvent(event, remoteRoom.remote!);
|
||||
} catch (err) {
|
||||
log.warn("There was an error sending a matrix event", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if (event.type === "m.room.encryption" && context.rooms.remote) {
|
||||
try {
|
||||
await this.HandleEncryptionWarning(event.room_id);
|
||||
return;
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to handle encrypted room, ${err}`);
|
||||
}
|
||||
} else if (event.type === "m.room.encryption" && remoteRoom) {
|
||||
await this.HandleEncryptionWarning(event.room_id);
|
||||
return;
|
||||
}
|
||||
log.verbose("Event not processed by bridge");
|
||||
MetricPeg.get.requestOutcome(event.event_id, false, "dropped");
|
||||
// throw new Unstable.EventUnknownError(`${event.event_id} not processed by bridge`);
|
||||
log.verbose(`${event.event_id} not processed by bridge`);
|
||||
}
|
||||
|
||||
public async HandleEncryptionWarning(roomId: string): Promise<void> {
|
||||
const intent = this.bridge.getIntent();
|
||||
log.info(`User has turned on encryption in ${roomId}, so leaving.`);
|
||||
/* N.B 'status' is not specced but https://github.com/matrix-org/matrix-doc/pull/828
|
||||
has been open for over a year with no resolution. */
|
||||
const sendPromise = intent.sendMessage(roomId, {
|
||||
const sendPromise = this.bridge.botIntent.sendEvent(roomId, {
|
||||
body: "You have turned on encryption in this room, so the service will not bridge any new messages.",
|
||||
msgtype: "m.notice",
|
||||
status: "critical",
|
||||
|
@ -158,26 +170,51 @@ export class MatrixEventProcessor {
|
|||
"Someone on Matrix has turned on encryption in this room, so the service will not bridge any new messages",
|
||||
);
|
||||
await sendPromise;
|
||||
await intent.leave(roomId);
|
||||
await this.bridge.getRoomStore().removeEntriesByMatrixRoomId(roomId);
|
||||
await this.bridge.botIntent.underlyingClient.leaveRoom(roomId);
|
||||
await this.store.roomStore.removeEntriesByMatrixRoomId(roomId);
|
||||
}
|
||||
|
||||
public async ProcessMsgEvent(event: IMatrixEvent, guildId: string, channelId: string) {
|
||||
const mxClient = this.bridge.getClientFactory().getClientAs();
|
||||
/**
|
||||
* Processes a matrix event by sending it to Discord and marking the event as read.
|
||||
*
|
||||
* @param event The matrix m.room.message event to process.
|
||||
* @param context Context of the bridge.
|
||||
* @throws {Unstable.ForeignNetworkError}
|
||||
*/
|
||||
public async ProcessMsgEvent(event: IMatrixEvent, room: RemoteStoreRoom): Promise<void> {
|
||||
const guildId = room.data.discord_guild!;
|
||||
const channelId = room.data.discord_channel!;
|
||||
const mxClient = this.bridge.botClient;
|
||||
log.verbose(`Looking up ${guildId}_${channelId}`);
|
||||
const roomLookup = await this.discord.LookupRoom(guildId, channelId, event.sender);
|
||||
const chan = roomLookup.channel;
|
||||
|
||||
const embedSet = await this.EventToEmbed(event, chan);
|
||||
const opts: Discord.MessageOptions = {};
|
||||
const file = await this.HandleAttachment(event, mxClient);
|
||||
if (typeof(file) === "string") {
|
||||
embedSet.messageEmbed.description += " " + file;
|
||||
} else {
|
||||
opts.file = file;
|
||||
let editEventId = "";
|
||||
if (event.content && event.content["m.relates_to"] && event.content["m.relates_to"].rel_type === "m.replace") {
|
||||
const editMatrixId = `${event.content["m.relates_to"].event_id};${event.room_id}`;
|
||||
const storeEvent = await this.store.Get(DbEvent, {matrix_id: editMatrixId});
|
||||
if (storeEvent && storeEvent.Result && storeEvent.Next()) {
|
||||
editEventId = storeEvent.DiscordId;
|
||||
}
|
||||
}
|
||||
|
||||
await this.discord.send(embedSet, opts, roomLookup, event);
|
||||
const embedSet = await this.EventToEmbed(event, chan);
|
||||
const opts: Discord.MessageOptions = {};
|
||||
const file = await this.HandleAttachment(event, mxClient, roomLookup.canSendEmbeds);
|
||||
if (typeof(file) === "string") {
|
||||
embedSet.messageEmbed.description += " " + file;
|
||||
} else if ((file as Discord.FileOptions).name && (file as Discord.FileOptions).attachment) {
|
||||
opts.files = [file as Discord.FileOptions];
|
||||
} else {
|
||||
embedSet.imageEmbed = file as Discord.MessageEmbed;
|
||||
}
|
||||
|
||||
// Throws an `Unstable.ForeignNetworkError` when sending the message fails.
|
||||
if (editEventId) {
|
||||
await this.discord.edit(embedSet, opts, roomLookup, event, editEventId);
|
||||
} else {
|
||||
await this.discord.send(embedSet, opts, roomLookup, event);
|
||||
}
|
||||
// Don't await this.
|
||||
this.sendReadReceipt(event).catch((ex) => {
|
||||
log.verbose("Failed to send read reciept for ", event.event_id, ex);
|
||||
|
@ -186,7 +223,6 @@ export class MatrixEventProcessor {
|
|||
|
||||
public async ProcessStateEvent(event: IMatrixEvent) {
|
||||
log.verbose(`Got state event from ${event.room_id} ${event.type}`);
|
||||
const channel = await this.discord.GetChannelFromRoomId(event.room_id) as Discord.TextChannel;
|
||||
|
||||
const SUPPORTED_EVENTS = ["m.room.member", "m.room.name", "m.room.topic"];
|
||||
if (!SUPPORTED_EVENTS.includes(event.type)) {
|
||||
|
@ -194,7 +230,7 @@ export class MatrixEventProcessor {
|
|||
return;
|
||||
}
|
||||
|
||||
if (event.sender === this.bridge.getIntent().getClient().getUserId()) {
|
||||
if (event.sender === this.bridge.botUserId) {
|
||||
log.verbose(`${event.event_id} ${event.type} is by our bot user, ignoring.`);
|
||||
return;
|
||||
}
|
||||
|
@ -202,6 +238,7 @@ export class MatrixEventProcessor {
|
|||
let msg = `\`${event.sender}\` `;
|
||||
|
||||
const allowJoinLeave = !this.config.bridge.disableJoinLeaveNotifications;
|
||||
const allowInvite = !this.config.bridge.disableInviteNotifications;
|
||||
|
||||
if (event.type === "m.room.name") {
|
||||
msg += `set the name to \`${event.content!.name}\``;
|
||||
|
@ -209,9 +246,9 @@ export class MatrixEventProcessor {
|
|||
msg += `set the topic to \`${event.content!.topic}\``;
|
||||
} else if (event.type === "m.room.member") {
|
||||
const membership = event.content!.membership;
|
||||
const intent = this.bridge.getIntent();
|
||||
const client = this.bridge.botIntent.underlyingClient;
|
||||
const isNewJoin = event.unsigned.replaces_state === undefined ? true : (
|
||||
await intent.getEvent(event.room_id, event.unsigned.replaces_state)).content.membership !== "join";
|
||||
await client.getEvent(event.room_id, event.unsigned.replaces_state)).content.membership !== "join";
|
||||
if (membership === "join") {
|
||||
this.mxUserProfileCache.delete(`${event.room_id}:${event.sender}`);
|
||||
this.mxUserProfileCache.delete(event.sender);
|
||||
|
@ -226,7 +263,7 @@ export class MatrixEventProcessor {
|
|||
}
|
||||
if (membership === "join" && isNewJoin && allowJoinLeave) {
|
||||
msg += "joined the room";
|
||||
} else if (membership === "invite") {
|
||||
} else if (membership === "invite" && allowInvite) {
|
||||
msg += `invited \`${event.state_key}\` to the room`;
|
||||
} else if (membership === "leave" && event.state_key !== event.sender) {
|
||||
msg += `kicked \`${event.state_key}\` from the room`;
|
||||
|
@ -241,6 +278,7 @@ export class MatrixEventProcessor {
|
|||
}
|
||||
|
||||
msg += " on Matrix.";
|
||||
const channel = await this.discord.GetChannelFromRoomId(event.room_id) as Discord.TextChannel;
|
||||
await this.discord.sendAsBot(msg, channel, event);
|
||||
await this.sendReadReceipt(event);
|
||||
}
|
||||
|
@ -248,7 +286,7 @@ export class MatrixEventProcessor {
|
|||
public async EventToEmbed(
|
||||
event: IMatrixEvent, channel: Discord.TextChannel, getReply: boolean = true,
|
||||
): Promise<IMatrixEventProcessorResult> {
|
||||
const mxClient = this.bridge.getClientFactory().getClientAs();
|
||||
const mxClient = this.bridge.botIntent.underlyingClient;
|
||||
const profile = await this.GetUserProfileForRoom(event.room_id, event.sender);
|
||||
const params = {
|
||||
mxClient,
|
||||
|
@ -261,10 +299,11 @@ export class MatrixEventProcessor {
|
|||
|
||||
let body: string = "";
|
||||
if (event.type !== "m.sticker") {
|
||||
body = await this.matrixMsgProcessor.FormatMessage(event.content as IMatrixMessage, channel.guild, params);
|
||||
const content = event.content!["m.new_content"] ? event.content!["m.new_content"] : event.content;
|
||||
body = await this.matrixMsgProcessor.FormatMessage(content as IMatrixMessage, channel.guild, params);
|
||||
}
|
||||
|
||||
const messageEmbed = new Discord.RichEmbed();
|
||||
const messageEmbed = new Discord.MessageEmbed();
|
||||
messageEmbed.setDescription(body);
|
||||
await this.SetEmbedAuthor(messageEmbed, event.sender, profile);
|
||||
const replyEmbed = getReply ? (await this.GetEmbedForReply(event, channel)) : undefined;
|
||||
|
@ -284,7 +323,11 @@ export class MatrixEventProcessor {
|
|||
};
|
||||
}
|
||||
|
||||
public async HandleAttachment(event: IMatrixEvent, mxClient: MatrixClient): Promise<string|Discord.FileOptions> {
|
||||
public async HandleAttachment(
|
||||
event: IMatrixEvent,
|
||||
mxClient: MatrixClient,
|
||||
sendEmbeds: boolean = false,
|
||||
): Promise<string|Discord.FileOptions|Discord.MessageEmbed> {
|
||||
if (!this.HasAttachment(event)) {
|
||||
return "";
|
||||
}
|
||||
|
@ -296,7 +339,7 @@ export class MatrixEventProcessor {
|
|||
if (!event.content.info) {
|
||||
// Fractal sends images without an info, which is technically allowed
|
||||
// but super unhelpful: https://gitlab.gnome.org/World/fractal/issues/206
|
||||
event.content.info = {size: 0};
|
||||
event.content.info = {mimetype: "", size: 0};
|
||||
}
|
||||
|
||||
if (!event.content.url) {
|
||||
|
@ -305,10 +348,10 @@ export class MatrixEventProcessor {
|
|||
}
|
||||
|
||||
let size = event.content.info.size || 0;
|
||||
const url = mxClient.mxcUrlToHttp(event.content.url);
|
||||
const name = this.GetFilenameForMediaEvent(event.content);
|
||||
const url = this.bridge.botClient.mxcToHttp(event.content.url);
|
||||
if (size < MaxFileSize) {
|
||||
const attachment = await Util.DownloadFile(url);
|
||||
const attachment = (await Util.DownloadFile(url)).buffer;
|
||||
size = attachment.byteLength;
|
||||
if (size < MaxFileSize) {
|
||||
return {
|
||||
|
@ -317,13 +360,17 @@ export class MatrixEventProcessor {
|
|||
} as Discord.FileOptions;
|
||||
}
|
||||
}
|
||||
if (sendEmbeds && event.content.info.mimetype.split("/")[0] === "image") {
|
||||
return new Discord.MessageEmbed()
|
||||
.setImage(url);
|
||||
}
|
||||
return `[${name}](${url})`;
|
||||
}
|
||||
|
||||
public async GetEmbedForReply(
|
||||
event: IMatrixEvent,
|
||||
channel: Discord.TextChannel,
|
||||
): Promise<Discord.RichEmbed|undefined> {
|
||||
): Promise<Discord.MessageEmbed|undefined> {
|
||||
if (!event.content) {
|
||||
event.content = {};
|
||||
}
|
||||
|
@ -336,30 +383,31 @@ export class MatrixEventProcessor {
|
|||
return;
|
||||
}
|
||||
|
||||
const intent = this.bridge.getIntent();
|
||||
const intent = this.bridge.botIntent;
|
||||
// Try to get the event.
|
||||
try {
|
||||
const sourceEvent = await intent.getEvent(event.room_id, eventId);
|
||||
sourceEvent.content.body = sourceEvent.content.body || "Reply with unknown content";
|
||||
const replyEmbed = (await this.EventToEmbed(sourceEvent, channel, false)).messageEmbed;
|
||||
const sourceEvent = (await intent.underlyingClient.getEvent(event.room_id, eventId)) as IMatrixEvent;
|
||||
if (!sourceEvent || !sourceEvent.content || !sourceEvent.content.body) {
|
||||
throw Error("No content could be found");
|
||||
}
|
||||
const replyEmbed = (await this.EventToEmbed(sourceEvent, channel, true)).messageEmbed;
|
||||
|
||||
// if we reply to a discord member, ping them!
|
||||
if (this.bridge.getBot().isRemoteUser(sourceEvent.sender)) {
|
||||
const uid = new MatrixUser(sourceEvent.sender.replace("@", "")).localpart.substring("_discord".length);
|
||||
if (this.bridge.isNamespacedUser(sourceEvent.sender)) {
|
||||
const uid = this.bridge.getSuffixForUserId(sourceEvent.sender);
|
||||
replyEmbed.addField("ping", `<@${uid}>`);
|
||||
}
|
||||
|
||||
replyEmbed.setTimestamp(new Date(sourceEvent.origin_server_ts));
|
||||
replyEmbed.setTimestamp(new Date(sourceEvent.origin_server_ts!));
|
||||
|
||||
if (this.HasAttachment(sourceEvent)) {
|
||||
const mxClient = this.bridge.getClientFactory().getClientAs();
|
||||
const url = mxClient.mxcUrlToHttp(sourceEvent.content.url);
|
||||
if (["m.image", "m.sticker"].includes(sourceEvent.content.msgtype as string)
|
||||
const url = this.bridge.botClient.mxcToHttp(sourceEvent.content!.url!);
|
||||
if (["m.image", "m.sticker"].includes(sourceEvent.content!.msgtype as string)
|
||||
|| sourceEvent.type === "m.sticker") {
|
||||
// we have an image reply
|
||||
replyEmbed.setImage(url);
|
||||
} else {
|
||||
const name = this.GetFilenameForMediaEvent(sourceEvent.content);
|
||||
const name = this.GetFilenameForMediaEvent(sourceEvent.content!);
|
||||
replyEmbed.description = `[${name}](${url})`;
|
||||
}
|
||||
}
|
||||
|
@ -368,15 +416,14 @@ export class MatrixEventProcessor {
|
|||
log.warn("Failed to handle reply, showing a unknown embed:", ex);
|
||||
}
|
||||
// For some reason we failed to get the event, so using fallback.
|
||||
const embed = new Discord.RichEmbed();
|
||||
const embed = new Discord.MessageEmbed();
|
||||
embed.setDescription("Reply with unknown content");
|
||||
embed.setAuthor("Unknown");
|
||||
return embed;
|
||||
}
|
||||
|
||||
private async GetUserProfileForRoom(roomId: string, userId: string) {
|
||||
const mxClient = this.bridge.getClientFactory().getClientAs();
|
||||
const intent = this.bridge.getIntent();
|
||||
const mxClient = this.bridge.botIntent.underlyingClient;
|
||||
let profile: {displayname: string, avatar_url: string|undefined} | undefined;
|
||||
try {
|
||||
// First try to pull out the room-specific profile from the cache.
|
||||
|
@ -387,7 +434,7 @@ export class MatrixEventProcessor {
|
|||
log.verbose(`Profile ${userId}:${roomId} not cached`);
|
||||
|
||||
// Failing that, try fetching the state.
|
||||
profile = await mxClient.getStateEvent(roomId, "m.room.member", userId);
|
||||
profile = await mxClient.getRoomStateEvent(roomId, "m.room.member", userId);
|
||||
if (profile) {
|
||||
this.mxUserProfileCache.set(`${roomId}:${userId}`, profile);
|
||||
return profile;
|
||||
|
@ -401,7 +448,7 @@ export class MatrixEventProcessor {
|
|||
|
||||
// Failing that, try fetching the profile.
|
||||
log.verbose(`Profile ${userId} not cached`);
|
||||
profile = await intent.getProfileInfo(userId);
|
||||
profile = await mxClient.getUserProfile(userId);
|
||||
if (profile) {
|
||||
this.mxUserProfileCache.set(userId, profile);
|
||||
return profile;
|
||||
|
@ -416,7 +463,7 @@ export class MatrixEventProcessor {
|
|||
private async sendReadReceipt(event: IMatrixEvent) {
|
||||
if (!this.config.bridge.disableReadReceipts) {
|
||||
try {
|
||||
await this.bridge.getIntent().sendReadReceipt(event.room_id, event.event_id);
|
||||
await this.bridge.botIntent.underlyingClient.sendReadReceipt(event.room_id, event.event_id);
|
||||
} catch (err) {
|
||||
log.error(`Failed to send read receipt for ${event}. `, err);
|
||||
}
|
||||
|
@ -440,26 +487,26 @@ export class MatrixEventProcessor {
|
|||
return hasAttachment;
|
||||
}
|
||||
|
||||
private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: {
|
||||
private async SetEmbedAuthor(embed: Discord.MessageEmbed, sender: string, profile?: {
|
||||
displayname: string,
|
||||
avatar_url: string|undefined }) {
|
||||
let displayName = sender;
|
||||
let avatarUrl;
|
||||
|
||||
// Are they a discord user.
|
||||
if (this.bridge.getBot().isRemoteUser(sender)) {
|
||||
const localpart = new MatrixUser(sender.replace("@", "")).localpart;
|
||||
if (this.bridge.isNamespacedUser(sender)) {
|
||||
const localpart = Util.ParseMxid(sender).localpart;
|
||||
const userOrMember = await this.discord.GetDiscordUserOrMember(localpart.substring("_discord".length));
|
||||
if (userOrMember instanceof Discord.User) {
|
||||
embed.setAuthor(
|
||||
userOrMember.username,
|
||||
userOrMember.avatarURL,
|
||||
userOrMember.avatarURL() || undefined,
|
||||
);
|
||||
return;
|
||||
} else if (userOrMember instanceof Discord.GuildMember) {
|
||||
embed.setAuthor(
|
||||
userOrMember.displayName,
|
||||
userOrMember.user.avatarURL,
|
||||
userOrMember.user.avatarURL() || undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -474,8 +521,12 @@ export class MatrixEventProcessor {
|
|||
}
|
||||
|
||||
if (profile.avatar_url) {
|
||||
const mxClient = this.bridge.getClientFactory().getClientAs();
|
||||
avatarUrl = mxClient.mxcUrlToHttp(profile.avatar_url, DISCORD_AVATAR_WIDTH, DISCORD_AVATAR_HEIGHT);
|
||||
avatarUrl = this.bridge.botClient.mxcToHttpThumbnail(
|
||||
profile.avatar_url,
|
||||
DISCORD_AVATAR_WIDTH,
|
||||
DISCORD_AVATAR_HEIGHT,
|
||||
"scale",
|
||||
);
|
||||
}
|
||||
}
|
||||
embed.setAuthor(
|
||||
|
@ -486,12 +537,27 @@ export class MatrixEventProcessor {
|
|||
}
|
||||
|
||||
private GetFilenameForMediaEvent(content: IMatrixEventContent): string {
|
||||
let ext = "";
|
||||
try {
|
||||
ext = "." + mime.getExtension(content.info.mimetype);
|
||||
} catch (err) { } // pass, we don't have an extension
|
||||
if (content.body) {
|
||||
if (path.extname(content.body) !== "") {
|
||||
return content.body;
|
||||
}
|
||||
return `${path.basename(content.body)}.${mime.extension(content.info.mimetype)}`;
|
||||
return path.basename(content.body) + ext;
|
||||
}
|
||||
return "matrix-media." + mime.extension(content.info.mimetype);
|
||||
return "matrix-media" + ext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given event is a bot command.
|
||||
*/
|
||||
function isBotCommand(event: IMatrixEvent): boolean {
|
||||
return !!(
|
||||
event.type === "m.room.message" &&
|
||||
event.content!.body &&
|
||||
event.content!.body!.startsWith("!discord")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,16 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import { IMatrixMessage, IMatrixEvent } from "./matrixtypes";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { IMatrixMessage } from "./matrixtypes";
|
||||
import * as Parser from "node-html-parser";
|
||||
import { Util } from "./util";
|
||||
import { DiscordBot } from "./bot";
|
||||
import { Client as MatrixClient } from "matrix-js-sdk";
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
import {
|
||||
IMatrixMessageParserCallbacks,
|
||||
IMatrixMessageParserOpts,
|
||||
MatrixMessageParser,
|
||||
} from "matrix-discord-parser";
|
||||
|
||||
const MIN_NAME_LENGTH = 2;
|
||||
const MAX_NAME_LENGTH = 32;
|
||||
const MATRIX_TO_LINK = "https://matrix.to/#/";
|
||||
const DEFAULT_ROOM_NOTIFY_POWER_LEVEL = 50;
|
||||
|
||||
export interface IMatrixMessageProcessorParams {
|
||||
|
@ -34,326 +37,104 @@ export interface IMatrixMessageProcessorParams {
|
|||
}
|
||||
|
||||
export class MatrixMessageProcessor {
|
||||
private guild: Discord.Guild;
|
||||
private listDepth: number = 0;
|
||||
private listBulletPoints: string[] = ["●", "○", "■", "‣"];
|
||||
private params?: IMatrixMessageProcessorParams;
|
||||
constructor(public bot: DiscordBot) { }
|
||||
private parser: MatrixMessageParser;
|
||||
constructor(public bot: DiscordBot, private config: DiscordBridgeConfig) {
|
||||
this.parser = new MatrixMessageParser();
|
||||
}
|
||||
|
||||
public async FormatMessage(
|
||||
msg: IMatrixMessage,
|
||||
guild: Discord.Guild,
|
||||
params?: IMatrixMessageProcessorParams,
|
||||
): Promise<string> {
|
||||
this.guild = guild;
|
||||
this.listDepth = 0;
|
||||
this.params = params;
|
||||
let reply = "";
|
||||
if (msg.formatted_body) {
|
||||
// parser needs everything wrapped in html elements
|
||||
// so we wrap everything in <div> just to be sure stuff is wrapped
|
||||
// as <div> will be un-touched anyways
|
||||
const parsed = Parser.parse(`<div>${msg.formatted_body}</div>`, {
|
||||
lowerCaseTagName: true,
|
||||
pre: true,
|
||||
// tslint:disable-next-line no-any
|
||||
} as any);
|
||||
reply = await this.walkNode(parsed);
|
||||
reply = reply.replace(/\s*$/, ""); // trim off whitespace at end
|
||||
} else {
|
||||
reply = await this.escapeDiscord(msg.body);
|
||||
}
|
||||
|
||||
if (msg.msgtype === "m.emote") {
|
||||
if (params &&
|
||||
params.displayname &&
|
||||
params.displayname.length >= MIN_NAME_LENGTH &&
|
||||
params.displayname.length <= MAX_NAME_LENGTH) {
|
||||
reply = `_${await this.escapeDiscord(params.displayname)} ${reply}_`;
|
||||
} else {
|
||||
reply = `_${reply}_`;
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
const opts = this.getParserOpts(msg, guild, params);
|
||||
return this.parser.FormatMessage(opts, msg);
|
||||
}
|
||||
|
||||
private async canNotifyRoom() {
|
||||
if (!this.params || !this.params.mxClient || !this.params.roomId || !this.params.userId) {
|
||||
return false;
|
||||
}
|
||||
return await Util.CheckMatrixPermission(
|
||||
this.params.mxClient,
|
||||
this.params.userId,
|
||||
this.params.roomId,
|
||||
DEFAULT_ROOM_NOTIFY_POWER_LEVEL,
|
||||
"notifications",
|
||||
"room",
|
||||
);
|
||||
private getParserOpts(
|
||||
msg: IMatrixMessage,
|
||||
guild: Discord.Guild,
|
||||
params?: IMatrixMessageProcessorParams,
|
||||
): IMatrixMessageParserOpts {
|
||||
return {
|
||||
callbacks: this.getParserCallbacks(msg, guild, params),
|
||||
determineCodeLanguage: this.config.bridge.determineCodeLanguage,
|
||||
displayname: params ? params.displayname || "" : "",
|
||||
};
|
||||
}
|
||||
|
||||
private async escapeDiscord(msg: string): Promise<string> {
|
||||
// \u200B is the zero-width space --> they still look the same but don't mention
|
||||
msg = msg.replace(/@everyone/g, "@\u200Beveryone");
|
||||
msg = msg.replace(/@here/g, "@\u200Bhere");
|
||||
|
||||
// Check the Matrix permissions to see if this user has the required
|
||||
// power level to notify with @room; if so, replace it with @here.
|
||||
if (msg.includes("@room") && await this.canNotifyRoom()) {
|
||||
msg = msg.replace(/@room/g, "@here");
|
||||
}
|
||||
const escapeChars = ["\\", "*", "_", "~", "`", "|"];
|
||||
msg = msg.split(" ").map((s) => {
|
||||
if (s.match(/^https?:\/\//)) {
|
||||
return s;
|
||||
}
|
||||
escapeChars.forEach((char) => {
|
||||
s = s.replace(new RegExp("\\" + char, "g"), "\\" + char);
|
||||
});
|
||||
return s;
|
||||
}).join(" ");
|
||||
return msg;
|
||||
}
|
||||
|
||||
private parsePreContent(node: Parser.HTMLElement): string {
|
||||
let text = node.text;
|
||||
const match = text.match(/^<code([^>]*)>/i);
|
||||
if (!match) {
|
||||
if (text[0] !== "\n") {
|
||||
text = "\n" + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
// remove <code> opening-tag
|
||||
text = text.substr(match[0].length);
|
||||
// remove </code> closing tag
|
||||
text = text.replace(/<\/code>$/i, "");
|
||||
if (text[0] !== "\n") {
|
||||
text = "\n" + text;
|
||||
}
|
||||
const language = match[1].match(/language-(\w*)/i);
|
||||
if (language) {
|
||||
text = language[1] + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private parseUser(id: string): string {
|
||||
const USER_REGEX = /^@_discord_([0-9]*)/;
|
||||
const match = id.match(USER_REGEX);
|
||||
if (!match || !this.guild.members.get(match[1])) {
|
||||
return "";
|
||||
}
|
||||
return `<@${match[1]}>`;
|
||||
}
|
||||
|
||||
private async parseChannel(id: string): Promise<string> {
|
||||
const CHANNEL_REGEX = /^#_discord_[0-9]*_([0-9]*):/;
|
||||
const match = id.match(CHANNEL_REGEX);
|
||||
if (!match || !this.guild.channels.get(match[1])) {
|
||||
/*
|
||||
This isn't formatted in #_discord_, so let's fetch the internal room ID
|
||||
and see if it is still a bridged room!
|
||||
*/
|
||||
if (this.params && this.params.mxClient) {
|
||||
try {
|
||||
const resp = await this.params.mxClient.getRoomIdForAlias(id);
|
||||
if (resp && resp.room_id) {
|
||||
const roomId = resp.room_id;
|
||||
const channel = await this.bot.GetChannelFromRoomId(roomId);
|
||||
return `<#${channel.id}>`;
|
||||
private getParserCallbacks(
|
||||
msg: IMatrixMessage,
|
||||
guild: Discord.Guild,
|
||||
params?: IMatrixMessageProcessorParams,
|
||||
): IMatrixMessageParserCallbacks {
|
||||
return {
|
||||
canNotifyRoom: async () => {
|
||||
if (!params || !params.mxClient || !params.roomId || !params.userId) {
|
||||
return false;
|
||||
}
|
||||
return await Util.CheckMatrixPermission(
|
||||
params.mxClient,
|
||||
params.userId,
|
||||
params.roomId,
|
||||
DEFAULT_ROOM_NOTIFY_POWER_LEVEL,
|
||||
"notifications",
|
||||
"room",
|
||||
);
|
||||
},
|
||||
getChannelId: async (mxid: string) => {
|
||||
const CHANNEL_REGEX = /^#_discord_[0-9]*_([0-9]*):/;
|
||||
const match = mxid.match(CHANNEL_REGEX);
|
||||
const channel = match && guild.channels.resolve(match[1]);
|
||||
if (!channel) {
|
||||
/*
|
||||
This isn't formatted in #_discord_, so let's fetch the internal room ID
|
||||
and see if it is still a bridged room!
|
||||
*/
|
||||
if (params && params.mxClient) {
|
||||
try {
|
||||
const resp = await params.mxClient.lookupRoomAlias(mxid);
|
||||
if (resp && resp.roomId) {
|
||||
const roomId = resp.roomId;
|
||||
const ch = await this.bot.GetChannelFromRoomId(roomId);
|
||||
return ch.id;
|
||||
}
|
||||
} catch (err) { } // ignore, room ID wasn't found
|
||||
}
|
||||
} catch (err) { } // ignore, room ID wasn't found
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return `<#${match[1]}>`;
|
||||
}
|
||||
|
||||
private async parseLinkContent(node: Parser.HTMLElement): Promise<string> {
|
||||
const attrs = node.attributes;
|
||||
const content = await this.walkChildNodes(node);
|
||||
if (!attrs.href || content === attrs.href) {
|
||||
return content;
|
||||
}
|
||||
return `[${content}](${attrs.href})`;
|
||||
}
|
||||
|
||||
private async parsePillContent(node: Parser.HTMLElement): Promise<string> {
|
||||
const attrs = node.attributes;
|
||||
if (!attrs.href || !attrs.href.startsWith(MATRIX_TO_LINK)) {
|
||||
return await this.parseLinkContent(node);
|
||||
}
|
||||
const id = attrs.href.replace(MATRIX_TO_LINK, "");
|
||||
let reply = "";
|
||||
switch (id[0]) {
|
||||
case "@":
|
||||
// user pill
|
||||
reply = this.parseUser(id);
|
||||
break;
|
||||
case "#":
|
||||
reply = await this.parseChannel(id);
|
||||
break;
|
||||
}
|
||||
if (!reply) {
|
||||
return await this.parseLinkContent(node);
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
private async parseImageContent(node: Parser.HTMLElement): Promise<string> {
|
||||
const EMOTE_NAME_REGEX = /^:?(\w+):?/;
|
||||
const attrs = node.attributes;
|
||||
const name = attrs.alt || attrs.title || "";
|
||||
let emoji: Discord.Emoji | null = null;
|
||||
// first check for matching mxc url
|
||||
if (attrs.src) {
|
||||
let id = "";
|
||||
try {
|
||||
const emojiDb = await this.bot.GetEmojiByMxc(attrs.src);
|
||||
id = emojiDb.EmojiId;
|
||||
emoji = this.guild.emojis.find((e) => e.id === id);
|
||||
} catch (e) {
|
||||
emoji = null;
|
||||
}
|
||||
}
|
||||
// nexc check for matching alt text / title
|
||||
if (!emoji) {
|
||||
const match = name.match(EMOTE_NAME_REGEX);
|
||||
let emojiName = "";
|
||||
if (match) {
|
||||
emojiName = match[1];
|
||||
emoji = this.guild.emojis.find((e) => e.name === emojiName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!emoji) {
|
||||
const content = await this.escapeDiscord(name);
|
||||
const url = this.params && this.params.mxClient ? this.params.mxClient.mxcUrlToHttp(attrs.src) : attrs.src;
|
||||
return attrs.src ? `[${content}](${url})` : content;
|
||||
}
|
||||
return `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`;
|
||||
}
|
||||
|
||||
private async parseBlockquoteContent(node: Parser.HTMLElement): Promise<string> {
|
||||
let msg = await this.walkChildNodes(node);
|
||||
|
||||
msg = msg.split("\n").map((s) => {
|
||||
return "> " + s;
|
||||
}).join("\n");
|
||||
msg = msg + "\n\n";
|
||||
return msg;
|
||||
}
|
||||
|
||||
private async parseUlContent(node: Parser.HTMLElement): Promise<string> {
|
||||
this.listDepth++;
|
||||
const entries = await this.arrayChildNodes(node, ["li"]);
|
||||
this.listDepth--;
|
||||
const bulletPoint = this.listBulletPoints[this.listDepth % this.listBulletPoints.length];
|
||||
|
||||
let msg = entries.map((s) => {
|
||||
return `${" ".repeat(this.listDepth)}${bulletPoint} ${s}`;
|
||||
}).join("\n");
|
||||
|
||||
if (this.listDepth === 0) {
|
||||
msg = `\n${msg}\n\n`;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
private async parseOlContent(node: Parser.HTMLElement): Promise<string> {
|
||||
this.listDepth++;
|
||||
const entries = await this.arrayChildNodes(node, ["li"]);
|
||||
this.listDepth--;
|
||||
let entry = 0;
|
||||
const attrs = node.attributes;
|
||||
if (attrs.start && attrs.start.match(/^[0-9]+$/)) {
|
||||
entry = parseInt(attrs.start, 10) - 1;
|
||||
}
|
||||
|
||||
let msg = entries.map((s) => {
|
||||
entry++;
|
||||
return `${" ".repeat(this.listDepth)}${entry}. ${s}`;
|
||||
}).join("\n");
|
||||
|
||||
if (this.listDepth === 0) {
|
||||
msg = `\n${msg}\n\n`;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
private async arrayChildNodes(node: Parser.Node, types: string[] = []): Promise<string[]> {
|
||||
const replies: string[] = [];
|
||||
await Util.AsyncForEach(node.childNodes, async (child) => {
|
||||
if (types.length && (
|
||||
child.nodeType === Parser.NodeType.TEXT_NODE
|
||||
|| !types.includes((child as Parser.HTMLElement).tagName)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
replies.push(await this.walkNode(child));
|
||||
});
|
||||
return replies;
|
||||
}
|
||||
|
||||
private async walkChildNodes(node: Parser.Node): Promise<string> {
|
||||
let reply = "";
|
||||
await Util.AsyncForEach(node.childNodes, async (child) => {
|
||||
reply += await this.walkNode(child);
|
||||
});
|
||||
return reply;
|
||||
}
|
||||
|
||||
private async walkNode(node: Parser.Node): Promise<string> {
|
||||
if (node.nodeType === Parser.NodeType.TEXT_NODE) {
|
||||
// ignore \n between single nodes
|
||||
if ((node as Parser.TextNode).text === "\n") {
|
||||
return "";
|
||||
}
|
||||
return await this.escapeDiscord((node as Parser.TextNode).text);
|
||||
} else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) {
|
||||
const nodeHtml = node as Parser.HTMLElement;
|
||||
switch (nodeHtml.tagName) {
|
||||
case "em":
|
||||
case "i":
|
||||
return `*${await this.walkChildNodes(nodeHtml)}*`;
|
||||
case "strong":
|
||||
case "b":
|
||||
return `**${await this.walkChildNodes(nodeHtml)}**`;
|
||||
case "u":
|
||||
return `__${await this.walkChildNodes(nodeHtml)}__`;
|
||||
case "del":
|
||||
return `~~${await this.walkChildNodes(nodeHtml)}~~`;
|
||||
case "code":
|
||||
return `\`${nodeHtml.text}\``;
|
||||
case "pre":
|
||||
return `\`\`\`${this.parsePreContent(nodeHtml)}\`\`\``;
|
||||
case "a":
|
||||
return await this.parsePillContent(nodeHtml);
|
||||
case "img":
|
||||
return await this.parseImageContent(nodeHtml);
|
||||
case "br":
|
||||
return "\n";
|
||||
case "blockquote":
|
||||
return await this.parseBlockquoteContent(nodeHtml);
|
||||
case "ul":
|
||||
return await this.parseUlContent(nodeHtml);
|
||||
case "ol":
|
||||
return await this.parseOlContent(nodeHtml);
|
||||
case "mx-reply":
|
||||
return "";
|
||||
case "hr":
|
||||
return "\n----------\n";
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
const level = parseInt(nodeHtml.tagName[1], 10);
|
||||
return `**${"#".repeat(level)} ${await this.walkChildNodes(nodeHtml)}**\n`;
|
||||
default:
|
||||
return await this.walkChildNodes(nodeHtml);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
return null;
|
||||
}
|
||||
return match && match[1] || null;
|
||||
},
|
||||
getEmoji: async (mxc: string, name: string) => {
|
||||
let emoji: {id: string, animated: boolean, name: string} | null = null;
|
||||
try {
|
||||
const emojiDb = await this.bot.GetEmojiByMxc(mxc);
|
||||
const id = emojiDb.EmojiId;
|
||||
emoji = guild.emojis.resolve(id);
|
||||
} catch (e) {
|
||||
emoji = null;
|
||||
}
|
||||
if (!emoji) {
|
||||
emoji = guild.emojis.resolve(name);
|
||||
}
|
||||
return emoji;
|
||||
},
|
||||
getUserId: async (mxid: string) => {
|
||||
const USER_REGEX = /^@_discord_([0-9]*)/;
|
||||
const match = mxid.match(USER_REGEX);
|
||||
const member = match && await guild.members.fetch(match[1]);
|
||||
if (!match || !member) {
|
||||
return null;
|
||||
}
|
||||
return match[1];
|
||||
},
|
||||
mxcUrlToHttp: (mxc: string) => {
|
||||
if (params && params.mxClient) {
|
||||
return params.mxClient.mxcToHttp(mxc);
|
||||
}
|
||||
return mxc;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,26 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DiscordBot } from "./bot";
|
||||
import {
|
||||
Bridge,
|
||||
RemoteRoom,
|
||||
thirdPartyLookup,
|
||||
thirdPartyProtocolResult,
|
||||
thirdPartyUserResult,
|
||||
thirdPartyLocationResult,
|
||||
ProvisionedRoom,
|
||||
Intent,
|
||||
} from "matrix-appservice-bridge";
|
||||
import { DiscordBot, IThirdPartyLookup } from "./bot";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { Util } from "./util";
|
||||
import { Provisioner } from "./provisioner";
|
||||
import { Log } from "./log";
|
||||
const log = new Log("MatrixRoomHandler");
|
||||
import { IMatrixEvent } from "./matrixtypes";
|
||||
import { DbRoomStore, MatrixStoreRoom, RemoteStoreRoom } from "./db/roomstore";
|
||||
import { Appservice, Intent, IApplicationServiceProtocol } from "matrix-bot-sdk";
|
||||
|
||||
const ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org/mlxoESwIsTbJrfXyAAogrNxA";
|
||||
/* tslint:disable:no-magic-numbers */
|
||||
|
@ -63,21 +53,37 @@ export class MatrixRoomHandler {
|
|||
private discord: DiscordBot,
|
||||
private config: DiscordBridgeConfig,
|
||||
private provisioner: Provisioner,
|
||||
private bridge: Bridge,
|
||||
private bridge: Appservice,
|
||||
private roomStore: DbRoomStore) {
|
||||
this.botUserId = this.discord.BotUserId;
|
||||
this.botJoinedRooms = new Set();
|
||||
}
|
||||
|
||||
public get ThirdPartyLookup(): thirdPartyLookup {
|
||||
return {
|
||||
getLocation: this.tpGetLocation.bind(this),
|
||||
getProtocol: this.tpGetProtocol.bind(this),
|
||||
getUser: this.tpGetUser.bind(this),
|
||||
parseLocation: this.tpParseLocation.bind(this),
|
||||
parseUser: this.tpParseUser.bind(this),
|
||||
protocols: ["discord"],
|
||||
};
|
||||
public bindThirdparty() {
|
||||
this.bridge.on("thirdparty.protocol",
|
||||
(protocol: string, cb: (protocolResponse: IApplicationServiceProtocol) => void) => {
|
||||
this.tpGetProtocol(protocol)
|
||||
.then(cb)
|
||||
.catch((err) => log.warn("Failed to get protocol", err));
|
||||
});
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
this.bridge.on("thirdparty.location.remote", (protocol: string, fields: any, cb: (response: any) => void) => {
|
||||
this.tpGetLocation(protocol, fields)
|
||||
.then(cb)
|
||||
.catch((err) => log.warn("Failed to get remote locations", err));
|
||||
});
|
||||
|
||||
// These are not supported.
|
||||
this.bridge.on("thirdparty.location.matrix", (matrixId: string, cb: (response: null) => void) => {
|
||||
cb(null);
|
||||
});
|
||||
this.bridge.on("thirdparty.user.remote", (matrixId: string, fields: unknown, cb: (response: null) => void) => {
|
||||
cb(null);
|
||||
});
|
||||
this.bridge.on("thirdparty.user.matrix", (matrixId: string, cb: (response: null) => void) => {
|
||||
cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
public async OnAliasQueried(alias: string, roomId: string) {
|
||||
|
@ -104,11 +110,13 @@ export class MatrixRoomHandler {
|
|||
}
|
||||
|
||||
// Fire and forget RoomDirectory mapping
|
||||
this.bridge.getIntent().getClient().setRoomDirectoryVisibilityAppService(
|
||||
this.bridge.setRoomDirectoryVisibility(
|
||||
channel.guild.id,
|
||||
roomId,
|
||||
"public",
|
||||
);
|
||||
).catch((err) => {
|
||||
log.warn("Failed to set room directory visibility for new room:", err);
|
||||
});
|
||||
await this.discord.ChannelSyncroniser.OnUpdate(channel);
|
||||
const promiseList: Promise<void>[] = [];
|
||||
// Join a whole bunch of users.
|
||||
|
@ -136,7 +144,9 @@ export class MatrixRoomHandler {
|
|||
await Promise.all(promiseList);
|
||||
}
|
||||
|
||||
public async OnAliasQuery(alias: string, aliasLocalpart: string): Promise<ProvisionedRoom> {
|
||||
// tslint:disable-next-line no-any
|
||||
public async OnAliasQuery(alias: string): Promise<any> {
|
||||
const aliasLocalpart = alias.substr("#".length, alias.indexOf(":") - 1);
|
||||
log.info("Got request for #", aliasLocalpart);
|
||||
const srvChanPair = aliasLocalpart.substr("_discord_".length).split("_", ROOM_NAME_PARTS);
|
||||
if (srvChanPair.length < ROOM_NAME_PARTS || srvChanPair[0] === "" || srvChanPair[1] === "") {
|
||||
|
@ -150,10 +160,21 @@ export class MatrixRoomHandler {
|
|||
} catch (err) {
|
||||
log.error(`Couldn't find discord room '${aliasLocalpart}'.`, err);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async tpGetProtocol(protocol: string): Promise<thirdPartyProtocolResult> {
|
||||
public async tpGetProtocol(protocol: string): Promise<IApplicationServiceProtocol> {
|
||||
const instances = {};
|
||||
for (const guild of this.discord.GetGuilds()) {
|
||||
instances[guild.name] = {
|
||||
bot_user_id: this.botUserId,
|
||||
desc: guild.name,
|
||||
fields: {
|
||||
guild_id: guild.id,
|
||||
},
|
||||
icon: guild.iconURL || ICON_URL,
|
||||
network_id: guild.id,
|
||||
};
|
||||
}
|
||||
return {
|
||||
field_types: {
|
||||
// guild_name: {
|
||||
|
@ -181,43 +202,20 @@ export class MatrixRoomHandler {
|
|||
regexp: "[A-Za-z0-9_\-]{2,100}",
|
||||
},
|
||||
},
|
||||
instances: this.discord.GetGuilds().map((guild) => {
|
||||
return {
|
||||
bot_user_id: this.botUserId,
|
||||
desc: guild.name,
|
||||
fields: {
|
||||
guild_id: guild.id,
|
||||
},
|
||||
icon: guild.iconURL || ICON_URL, // TODO: Use icons from our content repo. Potential security risk.
|
||||
network_id: guild.id,
|
||||
};
|
||||
}),
|
||||
icon: "", // TODO: Add this.
|
||||
instances,
|
||||
location_fields: ["guild_id", "channel_name"],
|
||||
user_fields: ["username", "discriminator"],
|
||||
};
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async tpGetLocation(protocol: string, fields: any): Promise<thirdPartyLocationResult[]> {
|
||||
public async tpGetLocation(protocol: string, fields: any): Promise<IThirdPartyLookup[]> {
|
||||
log.info("Got location request ", protocol, fields);
|
||||
const chans = this.discord.ThirdpartySearchForChannels(fields.guild_id, fields.channel_name);
|
||||
return chans;
|
||||
}
|
||||
|
||||
public async tpParseLocation(alias: string): Promise<thirdPartyLocationResult[]> {
|
||||
throw {err: "Unsupported", code: HTTP_UNSUPPORTED};
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async tpGetUser(protocol: string, fields: any): Promise<thirdPartyUserResult[]> {
|
||||
log.info("Got user request ", protocol, fields);
|
||||
throw {err: "Unsupported", code: HTTP_UNSUPPORTED};
|
||||
}
|
||||
|
||||
public async tpParseUser(userid: string): Promise<thirdPartyUserResult[]> {
|
||||
throw {err: "Unsupported", code: HTTP_UNSUPPORTED};
|
||||
}
|
||||
|
||||
private async joinRoom(intent: Intent, roomIdOrAlias: string, member?: Discord.GuildMember): Promise<void> {
|
||||
let currentSchedule = JOIN_ROOM_SCHEDULE[0];
|
||||
const doJoin = async () => {
|
||||
|
@ -225,15 +223,15 @@ export class MatrixRoomHandler {
|
|||
if (member) {
|
||||
await this.discord.UserSyncroniser.JoinRoom(member, roomIdOrAlias);
|
||||
} else {
|
||||
await intent.getClient().joinRoom(roomIdOrAlias);
|
||||
await intent.joinRoom(roomIdOrAlias);
|
||||
}
|
||||
};
|
||||
const errorHandler = async (err) => {
|
||||
log.error(`Error joining room ${roomIdOrAlias} as ${intent.getClient().getUserId()}`);
|
||||
log.error(`Error joining room ${roomIdOrAlias} as ${intent.userId}`);
|
||||
log.error(err);
|
||||
const idx = JOIN_ROOM_SCHEDULE.indexOf(currentSchedule);
|
||||
if (idx === JOIN_ROOM_SCHEDULE.length - 1) {
|
||||
log.warn(`Cannot join ${roomIdOrAlias} as ${intent.getClient().getUserId()}`);
|
||||
log.warn(`Cannot join ${roomIdOrAlias} as ${intent.userId}`);
|
||||
throw new Error(err);
|
||||
} else {
|
||||
currentSchedule = JOIN_ROOM_SCHEDULE[idx + 1];
|
||||
|
@ -253,7 +251,7 @@ export class MatrixRoomHandler {
|
|||
}
|
||||
|
||||
private async createMatrixRoom(channel: Discord.TextChannel,
|
||||
alias: string, aliasLocalpart: string): ProvisionedRoom {
|
||||
alias: string, aliasLocalpart: string) {
|
||||
const remote = new RemoteStoreRoom(`discord_${channel.guild.id}_${channel.id}`, {
|
||||
discord_channel: channel.id,
|
||||
discord_guild: channel.guild.id,
|
||||
|
@ -280,8 +278,6 @@ export class MatrixRoomHandler {
|
|||
new MatrixStoreRoom(alias),
|
||||
remote,
|
||||
);
|
||||
return {
|
||||
creationOpts,
|
||||
} as ProvisionedRoom;
|
||||
return creationOpts;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ export interface IMatrixMessage {
|
|||
msgtype: string;
|
||||
formatted_body?: string;
|
||||
format?: string;
|
||||
"m.new_content"?: any; // tslint:disable-line no-any
|
||||
"m.relates_to"?: any; // tslint:disable-line no-any
|
||||
}
|
||||
|
||||
export interface IMatrixMediaInfo {
|
||||
|
|
151
src/metrics.ts
151
src/metrics.ts
|
@ -14,30 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { PrometheusMetrics, Bridge } from "matrix-appservice-bridge";
|
||||
import { Gauge, Counter, Histogram } from "prom-client";
|
||||
import { Gauge, Counter, Histogram, default as promClient } from "prom-client";
|
||||
import { Log } from "./log";
|
||||
import { Appservice,
|
||||
IMetricContext,
|
||||
METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL,
|
||||
METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL,
|
||||
FunctionCallContext,
|
||||
METRIC_MATRIX_CLIENT_FUNCTION_CALL} from "matrix-bot-sdk";
|
||||
import { DiscordBridgeConfigMetrics } from "./config";
|
||||
import * as http from "http";
|
||||
|
||||
const AgeCounters = PrometheusMetrics.AgeCounters;
|
||||
const log = new Log("BridgeMetrics");
|
||||
const REQUEST_EXPIRE_TIME_MS = 30000;
|
||||
|
||||
interface IAgeCounter {
|
||||
setGauge(gauge: Gauge, morelabels: string[]);
|
||||
bump(age: number);
|
||||
}
|
||||
|
||||
interface IBridgeGauges {
|
||||
matrixRoomConfigs: number;
|
||||
remoteRoomConfigs: number;
|
||||
matrixGhosts: number;
|
||||
remoteGhosts: number;
|
||||
matrixRoomsByAge: IAgeCounter;
|
||||
remoteRoomsByAge: IAgeCounter;
|
||||
matrixUsersByAge: IAgeCounter;
|
||||
remoteUsersByAge: IAgeCounter;
|
||||
}
|
||||
|
||||
export interface IBridgeMetrics {
|
||||
registerRequest(id: string);
|
||||
requestOutcome(id: string, isRemote: boolean, outcome: string);
|
||||
|
@ -67,55 +57,71 @@ export class MetricPeg {
|
|||
}
|
||||
|
||||
export class PrometheusBridgeMetrics implements IBridgeMetrics {
|
||||
private metrics;
|
||||
private remoteCallCounter: Counter;
|
||||
private storeCallCounter: Counter;
|
||||
private presenceGauge: Gauge;
|
||||
private remoteRequest: Histogram;
|
||||
private matrixRequest: Histogram;
|
||||
private matrixCallCounter: Counter<string>;
|
||||
private remoteCallCounter: Counter<string>;
|
||||
private storeCallCounter: Counter<string>;
|
||||
private presenceGauge: Gauge<string>;
|
||||
private remoteRequest: Histogram<string>;
|
||||
private matrixRequest: Histogram<string>;
|
||||
private requestsInFlight: Map<string, number>;
|
||||
private bridgeGauges: IBridgeGauges = {
|
||||
matrixGhosts: 0,
|
||||
matrixRoomConfigs: 0,
|
||||
matrixRoomsByAge: new AgeCounters(),
|
||||
matrixUsersByAge: new AgeCounters(),
|
||||
remoteGhosts: 0,
|
||||
remoteRoomConfigs: 0,
|
||||
remoteRoomsByAge: new AgeCounters(),
|
||||
remoteUsersByAge: new AgeCounters(),
|
||||
};
|
||||
private matrixRequestStatus: Map<string, "success"|"failed">;
|
||||
private httpServer: http.Server;
|
||||
|
||||
public init(bridge: Bridge) {
|
||||
this.metrics = new PrometheusMetrics();
|
||||
this.metrics.registerMatrixSdkMetrics();
|
||||
this.metrics.registerBridgeGauges(() => this.bridgeGauges);
|
||||
this.metrics.addAppServicePath(bridge);
|
||||
this.remoteCallCounter = this.metrics.addCounter({
|
||||
public init(as: Appservice, config: DiscordBridgeConfigMetrics) {
|
||||
promClient.collectDefaultMetrics();
|
||||
// TODO: Bind this for every user.
|
||||
this.httpServer = http.createServer((req, res) => {
|
||||
if (req.method !== "GET" || req.url !== "/metrics") {
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
res.writeHead(404, "Not found");
|
||||
res.end();
|
||||
}
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
res.writeHead(200, "OK", {"Content-Type": promClient.register.contentType});
|
||||
res.write(promClient.register.metrics());
|
||||
res.end();
|
||||
});
|
||||
this.matrixCallCounter = new Counter({
|
||||
help: "Count of matrix API calls made",
|
||||
labelNames: ["method", "result"],
|
||||
name: "matrix_api_calls",
|
||||
});
|
||||
promClient.register.registerMetric(this.matrixCallCounter);
|
||||
|
||||
this.remoteCallCounter = new Counter({
|
||||
help: "Count of remote API calls made",
|
||||
labels: ["method"],
|
||||
labelNames: ["method"],
|
||||
name: "remote_api_calls",
|
||||
});
|
||||
this.storeCallCounter = this.metrics.addCounter({
|
||||
promClient.register.registerMetric(this.remoteCallCounter);
|
||||
|
||||
this.storeCallCounter = new Counter({
|
||||
help: "Count of store function calls made",
|
||||
labels: ["method", "cached"],
|
||||
labelNames: ["method", "cached"],
|
||||
name: "store_calls",
|
||||
});
|
||||
this.presenceGauge = this.metrics.addGauge({
|
||||
help: "Count of users in the presence queue",
|
||||
labels: [],
|
||||
promClient.register.registerMetric(this.storeCallCounter);
|
||||
|
||||
this.presenceGauge = new Gauge({
|
||||
help: "Count of users in the presence queue",
|
||||
name: "active_presence_users",
|
||||
});
|
||||
this.matrixRequest = this.metrics.addTimer({
|
||||
promClient.register.registerMetric(this.presenceGauge);
|
||||
|
||||
this.matrixRequest = new Histogram({
|
||||
help: "Histogram of processing durations of received Matrix messages",
|
||||
labels: ["outcome"],
|
||||
labelNames: ["outcome"],
|
||||
name: "matrix_request_seconds",
|
||||
});
|
||||
this.remoteRequest = this.metrics.addTimer({
|
||||
promClient.register.registerMetric(this.matrixRequest);
|
||||
|
||||
this.remoteRequest = new Histogram({
|
||||
help: "Histogram of processing durations of received remote messages",
|
||||
labels: ["outcome"],
|
||||
labelNames: ["outcome"],
|
||||
name: "remote_request_seconds",
|
||||
});
|
||||
promClient.register.registerMetric(this.remoteRequest);
|
||||
|
||||
this.requestsInFlight = new Map();
|
||||
setInterval(() => {
|
||||
this.requestsInFlight.forEach((time, id) => {
|
||||
|
@ -124,6 +130,17 @@ export class PrometheusBridgeMetrics implements IBridgeMetrics {
|
|||
}
|
||||
});
|
||||
}, REQUEST_EXPIRE_TIME_MS);
|
||||
this.httpServer.listen(config.port, config.host);
|
||||
|
||||
// Bind bot-sdk metrics
|
||||
as.botClient.metrics.registerListener({
|
||||
onDecrement: this.sdkDecrementMetric.bind(this),
|
||||
onEndMetric: this.sdkEndMetric.bind(this),
|
||||
onIncrement: this.sdkIncrementMetric.bind(this),
|
||||
onReset: this.sdkResetMetric.bind(this),
|
||||
onStartMetric: this.sdkStartMetric.bind(this),
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -152,4 +169,36 @@ export class PrometheusBridgeMetrics implements IBridgeMetrics {
|
|||
public storeCall(method: string, cached: boolean) {
|
||||
this.storeCallCounter.inc({method, cached: cached ? "yes" : "no"});
|
||||
}
|
||||
|
||||
private sdkStartMetric(metricName: string, context: IMetricContext) {
|
||||
// We don't use this yet.
|
||||
}
|
||||
|
||||
private sdkEndMetric(metricName: string, context: FunctionCallContext, timeMs: number) {
|
||||
if (metricName !== METRIC_MATRIX_CLIENT_FUNCTION_CALL) {
|
||||
return; // We don't handle any other type yet.
|
||||
}
|
||||
const successFail = this.matrixRequestStatus.get(context.uniqueId)!;
|
||||
this.matrixRequestStatus.delete(context.uniqueId);
|
||||
this.matrixRequest.observe({
|
||||
method: context.functionName,
|
||||
result: successFail,
|
||||
}, timeMs);
|
||||
}
|
||||
|
||||
private sdkResetMetric(metricName: string, context: IMetricContext) {
|
||||
// We don't use this yet.
|
||||
}
|
||||
|
||||
private sdkIncrementMetric(metricName: string, context: IMetricContext, amount: number) {
|
||||
if (metricName === METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL) {
|
||||
this.matrixRequestStatus.set(context.uniqueId, "success");
|
||||
} else if (metricName === METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL) {
|
||||
this.matrixRequestStatus.set(context.uniqueId, "failed");
|
||||
}
|
||||
}
|
||||
|
||||
private sdkDecrementMetric(metricName: string, context: IMetricContext, amount: number) {
|
||||
// We don't use this yet.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,26 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { User, Presence } from "discord.js";
|
||||
import { User, Presence } from "better-discord.js";
|
||||
import { DiscordBot } from "./bot";
|
||||
import { Log } from "./log";
|
||||
import { MetricPeg } from "./metrics";
|
||||
const log = new Log("PresenceHandler");
|
||||
|
||||
export class PresenceHandlerStatus {
|
||||
/* One of: ["online", "offline", "unavailable"] */
|
||||
public Presence: string;
|
||||
public Presence: "online"|"offline"|"unavailable";
|
||||
public StatusMsg: string;
|
||||
public ShouldDrop: boolean = false;
|
||||
}
|
||||
|
||||
interface IMatrixPresence {
|
||||
presence?: string;
|
||||
presence?: "online"|"offline"|"unavailable";
|
||||
status_msg?: string;
|
||||
}
|
||||
|
||||
export class PresenceHandler {
|
||||
private presenceQueue: User[];
|
||||
private presenceQueue: Presence[];
|
||||
private interval: NodeJS.Timeout | null;
|
||||
constructor(private bot: DiscordBot) {
|
||||
this.presenceQueue = [];
|
||||
|
@ -63,17 +62,24 @@ export class PresenceHandler {
|
|||
this.interval = null;
|
||||
}
|
||||
|
||||
public EnqueueUser(user: User) {
|
||||
if (user.id !== this.bot.GetBotId() && this.presenceQueue.find((u) => u.id === user.id) === undefined) {
|
||||
log.info(`Adding ${user.id} (${user.username}) to the presence queue`);
|
||||
this.presenceQueue.push(user);
|
||||
MetricPeg.get.setPresenceCount(this.presenceQueue.length);
|
||||
public EnqueueUser(presence: Presence) {
|
||||
if (presence.userID === this.bot.GetBotId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete stale presence
|
||||
const indexOfPresence = this.presenceQueue.findIndex((u) => u.userID === presence.userID);
|
||||
if (indexOfPresence !== -1) {
|
||||
this.presenceQueue.splice(indexOfPresence, 1);
|
||||
}
|
||||
log.verbose(`Adding ${presence.userID} (${presence.user?.username}) to the presence queue`);
|
||||
this.presenceQueue.push(presence);
|
||||
MetricPeg.get.setPresenceCount(this.presenceQueue.length);
|
||||
}
|
||||
|
||||
public DequeueUser(user: User) {
|
||||
const index = this.presenceQueue.findIndex((item) => {
|
||||
return user.id === item.id;
|
||||
return user.id === item.userID;
|
||||
});
|
||||
if (index !== -1) {
|
||||
this.presenceQueue.splice(index, 1);
|
||||
|
@ -85,20 +91,23 @@ export class PresenceHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public async ProcessUser(user: User): Promise<boolean> {
|
||||
const status = this.getUserPresence(user.presence);
|
||||
await this.setMatrixPresence(user, status);
|
||||
public async ProcessUser(presence: Presence): Promise<boolean> {
|
||||
if (!presence.user) {
|
||||
return true;
|
||||
}
|
||||
const status = this.getUserPresence(presence);
|
||||
await this.setMatrixPresence(presence.user, status);
|
||||
return status.ShouldDrop;
|
||||
}
|
||||
|
||||
private async processIntervalThread() {
|
||||
const user = this.presenceQueue.shift();
|
||||
if (user) {
|
||||
const proccessed = await this.ProcessUser(user);
|
||||
const presence = this.presenceQueue.shift();
|
||||
if (presence) {
|
||||
const proccessed = await this.ProcessUser(presence);
|
||||
if (!proccessed) {
|
||||
this.presenceQueue.push(user);
|
||||
this.presenceQueue.push(presence);
|
||||
} else {
|
||||
log.info(`Dropping ${user.id} from the presence queue.`);
|
||||
log.verbose(`Dropping ${presence.userID} from the presence queue.`);
|
||||
MetricPeg.get.setPresenceCount(this.presenceQueue.length);
|
||||
}
|
||||
}
|
||||
|
@ -107,10 +116,13 @@ export class PresenceHandler {
|
|||
private getUserPresence(presence: Presence): PresenceHandlerStatus {
|
||||
const status = new PresenceHandlerStatus();
|
||||
|
||||
if (presence.game) {
|
||||
status.StatusMsg = `${presence.game.streaming ? "Streaming" : "Playing"} ${presence.game.name}`;
|
||||
if (presence.game.url) {
|
||||
status.StatusMsg += ` | ${presence.game.url}`;
|
||||
// How do we show multiple activities?
|
||||
const activity = presence.activities[0];
|
||||
if (activity) {
|
||||
const type = activity.type[0] + activity.type.substring(1).toLowerCase(); // STREAMING -> Streaming;
|
||||
status.StatusMsg = `${type} ${activity.name}`;
|
||||
if (activity.url) {
|
||||
status.StatusMsg += ` | ${activity.url}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,7 +130,7 @@ export class PresenceHandler {
|
|||
status.Presence = "online";
|
||||
} else if (presence.status === "dnd") {
|
||||
status.Presence = "online";
|
||||
status.StatusMsg = status.StatusMsg ? "Do not disturb | " + status.StatusMsg : "Do not disturb";
|
||||
status.StatusMsg = status.StatusMsg ? `Do not disturb | ${status.StatusMsg}` : "Do not disturb";
|
||||
} else if (presence.status === "offline") {
|
||||
status.Presence = "offline";
|
||||
status.ShouldDrop = true; // Drop until we recieve an update.
|
||||
|
@ -130,12 +142,9 @@ export class PresenceHandler {
|
|||
|
||||
private async setMatrixPresence(user: User, status: PresenceHandlerStatus) {
|
||||
const intent = this.bot.GetIntentFromDiscordMember(user);
|
||||
const statusObj: IMatrixPresence = {presence: status.Presence};
|
||||
if (status.StatusMsg) {
|
||||
statusObj.status_msg = status.StatusMsg;
|
||||
}
|
||||
try {
|
||||
await intent.getClient().setPresence(statusObj);
|
||||
await intent.ensureRegistered();
|
||||
await intent.underlyingClient.setPresenceStatus(status.Presence, status.StatusMsg);
|
||||
} catch (ex) {
|
||||
if (ex.errcode !== "M_FORBIDDEN") {
|
||||
log.warn(`Could not update Matrix presence for ${user.id}`);
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { DbRoomStore, RemoteStoreRoom, MatrixStoreRoom } from "./db/roomstore";
|
||||
import { ChannelSyncroniser } from "./channelsyncroniser";
|
||||
import { Log } from "./log";
|
||||
|
@ -36,7 +36,6 @@ export class Provisioner {
|
|||
discord_type: "text",
|
||||
plumbed: true,
|
||||
});
|
||||
|
||||
const local = new MatrixStoreRoom(roomId);
|
||||
return this.roomStore.linkRooms(local, remote);
|
||||
}
|
||||
|
|
79
src/store.ts
79
src/store.ts
|
@ -24,20 +24,21 @@ import { Postgres } from "./db/postgres";
|
|||
import { IDatabaseConnector } from "./db/connector";
|
||||
import { DbRoomStore } from "./db/roomstore";
|
||||
import { DbUserStore } from "./db/userstore";
|
||||
import {
|
||||
RoomStore, UserStore,
|
||||
} from "matrix-appservice-bridge";
|
||||
|
||||
import { IAppserviceStorageProvider } from "matrix-bot-sdk";
|
||||
const log = new Log("DiscordStore");
|
||||
export const CURRENT_SCHEMA = 10;
|
||||
export const CURRENT_SCHEMA = 11;
|
||||
/**
|
||||
* Stores data for specific users and data not specific to rooms.
|
||||
*/
|
||||
export class DiscordStore {
|
||||
export class DiscordStore implements IAppserviceStorageProvider {
|
||||
public db: IDatabaseConnector;
|
||||
private config: DiscordBridgeConfigDatabase;
|
||||
private pRoomStore: DbRoomStore;
|
||||
private pUserStore: DbUserStore;
|
||||
|
||||
private registeredUsers: Set<string>;
|
||||
private asTxns: Set<string>;
|
||||
|
||||
constructor(configOrFile: DiscordBridgeConfigDatabase|string) {
|
||||
if (typeof(configOrFile) === "string") {
|
||||
this.config = new DiscordBridgeConfigDatabase();
|
||||
|
@ -45,6 +46,8 @@ export class DiscordStore {
|
|||
} else {
|
||||
this.config = configOrFile;
|
||||
}
|
||||
this.registeredUsers = new Set();
|
||||
this.asTxns = new Set();
|
||||
}
|
||||
|
||||
get roomStore() {
|
||||
|
@ -66,7 +69,7 @@ export class DiscordStore {
|
|||
}
|
||||
const BACKUP_NAME = this.config.filename + ".backup";
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
// Check to see if a backup file already exists.
|
||||
fs.access(BACKUP_NAME, (err) => {
|
||||
return resolve(err === null);
|
||||
|
@ -91,10 +94,8 @@ export class DiscordStore {
|
|||
* Checks the database has all the tables needed.
|
||||
*/
|
||||
public async init(
|
||||
overrideSchema: number = 0, roomStore: RoomStore = null, userStore: UserStore = null,
|
||||
overrideSchema: number = 0,
|
||||
): Promise<void> {
|
||||
const SCHEMA_ROOM_STORE_REQUIRED = 8;
|
||||
const SCHEMA_USER_STORE_REQUIRED = 9;
|
||||
log.info("Starting DB Init");
|
||||
await this.openDatabase();
|
||||
let version = await this.getSchemaVersion();
|
||||
|
@ -102,15 +103,9 @@ export class DiscordStore {
|
|||
log.info(`Database schema version is ${version}, latest version is ${targetSchema}`);
|
||||
while (version < targetSchema) {
|
||||
version++;
|
||||
const schemaClass = require(`./db/schema/v${version}.js`).Schema;
|
||||
const schemaClass = require(`./db/schema/v${version}`).Schema;
|
||||
let schema: IDbSchema;
|
||||
if (version === SCHEMA_ROOM_STORE_REQUIRED) { // 8 requires access to the roomstore.
|
||||
schema = (new schemaClass(roomStore) as IDbSchema);
|
||||
} else if (version === SCHEMA_USER_STORE_REQUIRED) {
|
||||
schema = (new schemaClass(userStore) as IDbSchema);
|
||||
} else {
|
||||
schema = (new schemaClass() as IDbSchema);
|
||||
}
|
||||
schema = (new schemaClass() as IDbSchema);
|
||||
log.info(`Updating database to v${version}, "${schema.description}"`);
|
||||
try {
|
||||
await schema.run(this);
|
||||
|
@ -130,6 +125,14 @@ export class DiscordStore {
|
|||
await this.setSchemaVersion(version);
|
||||
}
|
||||
log.info("Updated database to the latest schema");
|
||||
// We need to prepopulate some sets
|
||||
for (const row of await this.db.All("SELECT * FROM registered_users")) {
|
||||
this.registeredUsers.add(row.user_id as string);
|
||||
}
|
||||
|
||||
for (const row of await this.db.All("SELECT * FROM as_txns")) {
|
||||
this.asTxns.add(row.txn_id as string);
|
||||
}
|
||||
}
|
||||
|
||||
public async close() {
|
||||
|
@ -172,23 +175,30 @@ export class DiscordStore {
|
|||
}
|
||||
}
|
||||
|
||||
public async deleteUserToken(discordId: string): Promise<void> {
|
||||
public async deleteUserToken(mxid: string): Promise<void> {
|
||||
const res = await this.db.Get("SELECT * from user_id_discord_id WHERE user_id = $id", {
|
||||
id: mxid,
|
||||
});
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
const discordId = res.discord_id;
|
||||
log.silly("SQL", "deleteUserToken => ", discordId);
|
||||
try {
|
||||
await Promise.all([
|
||||
this.db.Run(
|
||||
`
|
||||
DELETE FROM user_id_discord_id WHERE discord_id = $id;
|
||||
DELETE FROM user_id_discord_id WHERE discord_id = $id
|
||||
`
|
||||
, {
|
||||
$id: discordId,
|
||||
id: discordId,
|
||||
}),
|
||||
this.db.Run(
|
||||
`
|
||||
DELETE FROM discord_id_token WHERE discord_id = $id;
|
||||
DELETE FROM discord_id_token WHERE discord_id = $id
|
||||
`
|
||||
, {
|
||||
$id: discordId,
|
||||
id: discordId,
|
||||
}),
|
||||
]);
|
||||
} catch (err) {
|
||||
|
@ -238,7 +248,8 @@ export class DiscordStore {
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
// tslint:disable-next-line no-any
|
||||
|
||||
// tslint:disable-next-line no-any callable-types
|
||||
public async Get<T extends IDbData>(dbType: {new(): T; }, params: any): Promise<T|null> {
|
||||
const dType = new dbType();
|
||||
log.silly(`get <${dType.constructor.name} with params ${params}>`);
|
||||
|
@ -267,6 +278,26 @@ export class DiscordStore {
|
|||
await data.Delete(this);
|
||||
}
|
||||
|
||||
public addRegisteredUser(userId: string) {
|
||||
this.registeredUsers.add(userId);
|
||||
this.db.Run("INSERT INTO registered_users VALUES ($userId)", {userId}).catch((err) => {
|
||||
log.warn("Failed to insert registered user", err);
|
||||
});
|
||||
}
|
||||
public isUserRegistered(userId: string): boolean {
|
||||
return this.registeredUsers.has(userId);
|
||||
}
|
||||
|
||||
public setTransactionCompleted(transactionId: string) {
|
||||
this.asTxns.add(transactionId);
|
||||
this.db.Run("INSERT INTO as_txns (txn_id) VALUES ($transactionId)", {transactionId}).catch((err) => {
|
||||
log.warn("Failed to insert txn", err);
|
||||
});
|
||||
}
|
||||
public isTransactionCompleted(transactionId: string): boolean {
|
||||
return this.asTxns.has(transactionId);
|
||||
}
|
||||
|
||||
private async getSchemaVersion( ): Promise<number> {
|
||||
log.silly("_get_schema_version");
|
||||
let version = 0;
|
||||
|
|
60
src/structures/lock.ts
Normal file
60
src/structures/lock.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
export class Lock<T> {
|
||||
private locks: Map<T, {i: NodeJS.Timeout|null, r: (() => void)|null}>;
|
||||
private lockPromises: Map<T, Promise<{}>>;
|
||||
constructor(
|
||||
private timeout: number,
|
||||
) {
|
||||
this.locks = new Map();
|
||||
this.lockPromises = new Map();
|
||||
}
|
||||
|
||||
public set(key: T) {
|
||||
// if there is a lock set.....we don't set a second one ontop
|
||||
if (this.locks.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set a dummy lock so that if we re-set again before releasing it won't do anthing
|
||||
this.locks.set(key, {i: null, r: null});
|
||||
|
||||
const p = new Promise<{}>((resolve) => {
|
||||
// first we check if the lock has the key....if not, e.g. if it
|
||||
// got released too quickly, we still want to resolve our promise
|
||||
if (!this.locks.has(key)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
// create the interval that will release our promise after the timeout
|
||||
const i = setTimeout(() => {
|
||||
this.release(key);
|
||||
}, this.timeout);
|
||||
// aaand store to our lock
|
||||
this.locks.set(key, {r: resolve, i});
|
||||
});
|
||||
this.lockPromises.set(key, p);
|
||||
}
|
||||
|
||||
public release(key: T) {
|
||||
// if there is nothing to release then there is nothing to release
|
||||
if (!this.locks.has(key)) {
|
||||
return;
|
||||
}
|
||||
const lock = this.locks.get(key)!;
|
||||
if (lock.r !== null) {
|
||||
lock.r();
|
||||
}
|
||||
if (lock.i !== null) {
|
||||
clearTimeout(lock.i);
|
||||
}
|
||||
this.locks.delete(key);
|
||||
this.lockPromises.delete(key);
|
||||
}
|
||||
|
||||
public async wait(key: T) {
|
||||
// we wait for a lock release only if a promise is present
|
||||
const promise = this.lockPromises.get(key);
|
||||
if (promise) {
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { User, GuildMember } from "discord.js";
|
||||
import { User, GuildMember } from "better-discord.js";
|
||||
import { DiscordBot } from "./bot";
|
||||
import { Util } from "./util";
|
||||
import { Bridge, Intent, MatrixUser } from "matrix-appservice-bridge";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
import { Log } from "./log";
|
||||
import { IMatrixEvent } from "./matrixtypes";
|
||||
import { DbUserStore, RemoteUser } from "./db/userstore";
|
||||
import { Appservice, Intent } from "matrix-bot-sdk";
|
||||
|
||||
const log = new Log("UserSync");
|
||||
|
||||
|
@ -82,7 +82,7 @@ export class UserSyncroniser {
|
|||
// roomId+userId => ev
|
||||
public userStateHold: Map<string, IMatrixEvent>;
|
||||
constructor(
|
||||
private bridge: Bridge,
|
||||
private bridge: Appservice,
|
||||
private config: DiscordBridgeConfig,
|
||||
private discord: DiscordBot,
|
||||
private userStore: DbUserStore) {
|
||||
|
@ -96,8 +96,8 @@ export class UserSyncroniser {
|
|||
* @returns {Promise<void>}
|
||||
* @constructor
|
||||
*/
|
||||
public async OnUpdateUser(discordUser: User, webhookID?: string) {
|
||||
const userState = await this.GetUserUpdateState(discordUser, webhookID);
|
||||
public async OnUpdateUser(discordUser: User, isWebhook: boolean = false) {
|
||||
const userState = await this.GetUserUpdateState(discordUser, isWebhook);
|
||||
try {
|
||||
await this.ApplyStateToProfile(userState);
|
||||
} catch (e) {
|
||||
|
@ -106,7 +106,7 @@ export class UserSyncroniser {
|
|||
}
|
||||
|
||||
public async ApplyStateToProfile(userState: IUserState) {
|
||||
const intent = this.bridge.getIntent(userState.mxUserId);
|
||||
const intent = this.bridge.getIntentForUserId(userState.mxUserId);
|
||||
let userUpdated = false;
|
||||
let remoteUser: RemoteUser;
|
||||
if (userState.createUser) {
|
||||
|
@ -122,30 +122,32 @@ export class UserSyncroniser {
|
|||
const rUser = await this.userStore.getRemoteUser(userState.id);
|
||||
remoteUser = rUser ? rUser : new RemoteUser(userState.id);
|
||||
}
|
||||
await intent.ensureRegistered();
|
||||
|
||||
if (userState.displayName !== null) {
|
||||
log.verbose(`Updating displayname for ${userState.mxUserId} to "${userState.displayName}"`);
|
||||
await intent.setDisplayName(userState.displayName);
|
||||
await intent.underlyingClient.setDisplayName(userState.displayName);
|
||||
remoteUser.displayname = userState.displayName;
|
||||
userUpdated = true;
|
||||
}
|
||||
|
||||
if (userState.avatarUrl !== null) {
|
||||
log.verbose(`Updating avatar_url for ${userState.mxUserId} to "${userState.avatarUrl}"`);
|
||||
const avatarMxc = await Util.UploadContentFromUrl(
|
||||
userState.avatarUrl,
|
||||
intent,
|
||||
const data = await Util.DownloadFile(userState.avatarUrl);
|
||||
const avatarMxc = await intent.underlyingClient.uploadContent(
|
||||
data.buffer,
|
||||
data.mimeType,
|
||||
userState.avatarId,
|
||||
);
|
||||
await intent.setAvatarUrl(avatarMxc.mxcUrl);
|
||||
await intent.underlyingClient.setAvatarUrl(avatarMxc);
|
||||
remoteUser.avatarurl = userState.avatarUrl;
|
||||
remoteUser.avatarurlMxc = avatarMxc.mxcUrl;
|
||||
remoteUser.avatarurlMxc = avatarMxc;
|
||||
userUpdated = true;
|
||||
}
|
||||
|
||||
if (userState.removeAvatar) {
|
||||
log.verbose(`Clearing avatar_url for ${userState.mxUserId} to "${userState.avatarUrl}"`);
|
||||
await intent.setAvatarUrl(null);
|
||||
await intent.underlyingClient.setAvatarUrl("");
|
||||
remoteUser.avatarurl = null;
|
||||
remoteUser.avatarurlMxc = null;
|
||||
userUpdated = true;
|
||||
|
@ -157,10 +159,10 @@ export class UserSyncroniser {
|
|||
}
|
||||
}
|
||||
|
||||
public async JoinRoom(member: GuildMember | User, roomId: string, webhookID?: string) {
|
||||
public async JoinRoom(member: GuildMember | User, roomId: string, isWebhook: boolean = false) {
|
||||
let state: IGuildMemberState;
|
||||
if (member instanceof User) {
|
||||
state = await this.GetUserStateForDiscordUser(member, webhookID);
|
||||
state = await this.GetUserStateForDiscordUser(member, isWebhook);
|
||||
} else {
|
||||
state = await this.GetUserStateForGuildMember(member);
|
||||
}
|
||||
|
@ -175,7 +177,7 @@ export class UserSyncroniser {
|
|||
} else {
|
||||
log.info(`User not in room ${roomId}, inviting`);
|
||||
try {
|
||||
await this.bridge.getIntent().invite(roomId, state.mxUserId);
|
||||
await this.bridge.botIntent.underlyingClient.inviteUser(state.mxUserId, roomId);
|
||||
await this.ApplyStateToRoom(state, roomId, guildId);
|
||||
} catch (e) {
|
||||
log.error(`Failed to join ${state.id} to ${roomId}`, e);
|
||||
|
@ -204,10 +206,10 @@ export class UserSyncroniser {
|
|||
} else {
|
||||
log.warn("Remote user wasn't found, using blank avatar");
|
||||
}
|
||||
const intent = this.bridge.getIntent(memberState.mxUserId);
|
||||
const intent = this.bridge.getIntentForUserId(memberState.mxUserId);
|
||||
/* The intent class tries to be smart and deny a state update for <PL50 users.
|
||||
Obviously a user can change their own state so we use the client instead. */
|
||||
await intent.getClient().sendStateEvent(roomId, "m.room.member", {
|
||||
await intent.underlyingClient.sendStateEvent(roomId, "m.room.member", memberState.mxUserId, {
|
||||
"avatar_url": avatar,
|
||||
"displayname": memberState.displayName,
|
||||
"membership": "join",
|
||||
|
@ -218,7 +220,7 @@ export class UserSyncroniser {
|
|||
roles: memberState.roles,
|
||||
username: memberState.username,
|
||||
},
|
||||
}, memberState.mxUserId);
|
||||
});
|
||||
|
||||
if (remoteUser) {
|
||||
if (guildId) {
|
||||
|
@ -228,15 +230,18 @@ export class UserSyncroniser {
|
|||
}
|
||||
}
|
||||
|
||||
public async GetUserUpdateState(discordUser: User, webhookID?: string): Promise<IUserState> {
|
||||
public async GetUserUpdateState(discordUser: User, isWebhook: boolean = false): Promise<IUserState> {
|
||||
log.verbose(`State update requested for ${discordUser.id}`);
|
||||
let mxidExtra = "";
|
||||
if (webhookID) {
|
||||
// no need to escape as this mxid is only used to create an intent
|
||||
mxidExtra = `_${new MatrixUser(`@${webhookID}`).localpart}`;
|
||||
if (isWebhook) {
|
||||
// for webhooks we append the username to the mxid, as webhooks with the same
|
||||
// id can have multiple different usernames set. This way we don't spam
|
||||
// userstate changes
|
||||
|
||||
mxidExtra = `_${Util.ParseMxid(`@${discordUser.username}`).localpart}`;
|
||||
}
|
||||
const userState: IUserState = Object.assign({}, DEFAULT_USER_STATE, {
|
||||
id: discordUser.id,
|
||||
id: discordUser.id + mxidExtra,
|
||||
mxUserId: `@_discord_${discordUser.id}${mxidExtra}:${this.config.bridge.domain}`,
|
||||
});
|
||||
const displayName = Util.ApplyPatternString(this.config.ghosts.usernamePattern, {
|
||||
|
@ -251,8 +256,10 @@ export class UserSyncroniser {
|
|||
log.verbose(`Could not find user in remote user store.`);
|
||||
userState.createUser = true;
|
||||
userState.displayName = displayName;
|
||||
userState.avatarUrl = discordUser.avatarURL;
|
||||
userState.avatarId = discordUser.avatar;
|
||||
if (discordUser.avatar) {
|
||||
userState.avatarUrl = discordUser.avatarURL();
|
||||
userState.avatarId = discordUser.avatar;
|
||||
}
|
||||
return userState;
|
||||
}
|
||||
|
||||
|
@ -263,13 +270,13 @@ export class UserSyncroniser {
|
|||
}
|
||||
|
||||
const oldAvatarUrl = remoteUser.avatarurl;
|
||||
if (oldAvatarUrl !== discordUser.avatarURL) {
|
||||
if (oldAvatarUrl !== discordUser.avatarURL()) {
|
||||
log.verbose(`User ${discordUser.id} avatarurl should be updated`);
|
||||
if (discordUser.avatarURL !== null) {
|
||||
userState.avatarUrl = discordUser.avatarURL;
|
||||
if (discordUser.avatar) {
|
||||
userState.avatarUrl = discordUser.avatarURL();
|
||||
userState.avatarId = discordUser.avatar;
|
||||
} else {
|
||||
userState.removeAvatar = oldAvatarUrl !== null;
|
||||
userState.removeAvatar = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,7 +298,7 @@ export class UserSyncroniser {
|
|||
displayName: name,
|
||||
id: newMember.id,
|
||||
mxUserId: `@_discord_${newMember.id}:${this.config.bridge.domain}`,
|
||||
roles: newMember.roles.map((role) => { return {
|
||||
roles: newMember.roles.cache.map((role) => { return {
|
||||
color: role.color,
|
||||
name: role.name,
|
||||
position: role.position,
|
||||
|
@ -303,17 +310,19 @@ export class UserSyncroniser {
|
|||
|
||||
public async GetUserStateForDiscordUser(
|
||||
user: User,
|
||||
webhookID?: string,
|
||||
isWebhook: boolean = false,
|
||||
): Promise<IGuildMemberState> {
|
||||
let mxidExtra = "";
|
||||
if (webhookID) {
|
||||
// no need to escape as this mxid is only used to create an Intent
|
||||
mxidExtra = `_${new MatrixUser(`@${user.username}`).localpart}`;
|
||||
if (isWebhook) {
|
||||
// for webhooks we append the username to the mxid, as webhooks with the same
|
||||
// id can have multiple different usernames set. This way we don't spam
|
||||
// userstate changes
|
||||
mxidExtra = "_" + Util.ParseMxid(`@${user.username}`, false).localpart;
|
||||
}
|
||||
const guildState: IGuildMemberState = Object.assign({}, DEFAULT_GUILD_STATE, {
|
||||
bot: user.bot,
|
||||
displayName: user.username,
|
||||
id: user.id,
|
||||
id: user.id + mxidExtra,
|
||||
mxUserId: `@_discord_${user.id}${mxidExtra}:${this.config.bridge.domain}`,
|
||||
roles: [],
|
||||
username: user.tag,
|
||||
|
@ -333,7 +342,7 @@ export class UserSyncroniser {
|
|||
const intent = this.discord.GetIntentFromDiscordMember(member);
|
||||
return Promise.all(
|
||||
rooms.map(
|
||||
async (roomId) => this.leave(intent, roomId, false),
|
||||
async (roomId) => this.leave(intent, roomId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -374,12 +383,12 @@ export class UserSyncroniser {
|
|||
),
|
||||
);
|
||||
const userId = state.mxUserId;
|
||||
const intent = this.bridge.getIntent(userId);
|
||||
const intent = this.bridge.getIntentForUserId(userId);
|
||||
await Promise.all(
|
||||
leaveRooms.map(
|
||||
async (roomId) => {
|
||||
try {
|
||||
await this.leave(intent, roomId, true);
|
||||
await this.leave(intent, roomId);
|
||||
} catch (e) { } // not in room
|
||||
},
|
||||
),
|
||||
|
@ -391,9 +400,9 @@ export class UserSyncroniser {
|
|||
log.info(`Got update for ${id}.`);
|
||||
|
||||
await Util.AsyncForEach(this.discord.GetGuilds(), async (guild) => {
|
||||
if (guild.members.has(id)) {
|
||||
if (guild.members.cache.has(id)) {
|
||||
log.info(`Updating user ${id} in guild ${guild.id}.`);
|
||||
const member = guild.members.get(id);
|
||||
const member = guild.members.resolve(id);
|
||||
try {
|
||||
const state = await this.GetUserStateForGuildMember(member!);
|
||||
const rooms = await this.discord.GetRoomIdsFromGuild(guild, member!);
|
||||
|
@ -409,13 +418,7 @@ export class UserSyncroniser {
|
|||
});
|
||||
}
|
||||
|
||||
private async leave(intent: Intent, roomId: string, checkCache: boolean = true) {
|
||||
const userId = intent.getClient().getUserId();
|
||||
if (checkCache && ![null, "join", "invite"]
|
||||
.includes(intent.opts.backingStore.getMembership(roomId, userId))) {
|
||||
return;
|
||||
}
|
||||
await intent.leave(roomId);
|
||||
intent.opts.backingStore.setMembership(roomId, userId, "leave");
|
||||
private async leave(intent: Intent, roomId: string) {
|
||||
await intent.underlyingClient.leaveRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
|
238
src/util.ts
238
src/util.ts
|
@ -16,17 +16,15 @@ limitations under the License.
|
|||
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import { Intent } from "matrix-appservice-bridge";
|
||||
import { Buffer } from "buffer";
|
||||
import * as mime from "mime";
|
||||
import { Permissions } from "discord.js";
|
||||
import { Permissions } from "better-discord.js";
|
||||
import { DiscordBridgeConfig } from "./config";
|
||||
import { Client as MatrixClient } from "matrix-js-sdk";
|
||||
import { IMatrixEvent } from "./matrixtypes";
|
||||
|
||||
const HTTP_OK = 200;
|
||||
|
||||
import { Log } from "./log";
|
||||
import { Intent, MatrixClient } from "matrix-bot-sdk";
|
||||
const log = new Log("Util");
|
||||
|
||||
type PERMISSIONTYPES = any | any[]; // tslint:disable-line no-any
|
||||
|
@ -54,6 +52,11 @@ export interface ICommandParameters {
|
|||
|
||||
export type CommandPermissonCheck = (permission: PERMISSIONTYPES) => Promise<boolean | string>;
|
||||
|
||||
export interface IDownloadedFile {
|
||||
buffer: Buffer;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface IPatternMap {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
@ -63,15 +66,13 @@ export class Util {
|
|||
* downloadFile - This function will take a URL and store the resulting data into
|
||||
* a buffer.
|
||||
*/
|
||||
public static async DownloadFile(url: string): Promise<Buffer> {
|
||||
public static async DownloadFile(url: string): Promise<IDownloadedFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let ht;
|
||||
let get = http.get;
|
||||
if (url.startsWith("https")) {
|
||||
ht = https;
|
||||
} else {
|
||||
ht = http;
|
||||
get = https.get;
|
||||
}
|
||||
const req = ht.get((url), (res) => {
|
||||
const req = get((url), (res) => {
|
||||
let buffer = Buffer.alloc(0);
|
||||
if (res.statusCode !== HTTP_OK) {
|
||||
reject(`Non 200 status code (${res.statusCode})`);
|
||||
|
@ -82,72 +83,16 @@ export class Util {
|
|||
});
|
||||
|
||||
res.on("end", () => {
|
||||
resolve(buffer);
|
||||
resolve({
|
||||
buffer,
|
||||
mimeType: res.headers["content-type"],
|
||||
});
|
||||
});
|
||||
});
|
||||
req.on("error", (err) => {
|
||||
reject(`Failed to download. ${err.code}`);
|
||||
reject(`Failed to download. ${err}`);
|
||||
});
|
||||
}) as Promise<Buffer>;
|
||||
}
|
||||
/**
|
||||
* uploadContentFromUrl - Upload content from a given URL to the homeserver
|
||||
* and return a MXC URL.
|
||||
*/
|
||||
public static async UploadContentFromUrl(url: string, intent: Intent, name: string | null): Promise<IUploadResult> {
|
||||
let contenttype;
|
||||
name = name || null;
|
||||
try {
|
||||
const bufferRet = (await (new Promise((resolve, reject) => {
|
||||
let ht;
|
||||
if (url.startsWith("https")) {
|
||||
ht = https;
|
||||
} else {
|
||||
ht = http;
|
||||
}
|
||||
const req = ht.get( url, (res) => {
|
||||
let buffer = Buffer.alloc(0);
|
||||
|
||||
if (res.headers.hasOwnProperty("content-type")) {
|
||||
contenttype = res.headers["content-type"];
|
||||
} else {
|
||||
log.verbose("No content-type given by server, guessing based on file name.");
|
||||
contenttype = mime.lookup(url);
|
||||
}
|
||||
|
||||
if (name === null) {
|
||||
const names = url.split("/");
|
||||
name = names[names.length - 1];
|
||||
}
|
||||
|
||||
res.on("data", (d) => {
|
||||
buffer = Buffer.concat([buffer, d]);
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
resolve(buffer);
|
||||
});
|
||||
});
|
||||
req.on("error", (err) => {
|
||||
reject(`Failed to download. ${err.code}`);
|
||||
});
|
||||
}))) as Buffer;
|
||||
const size = bufferRet.length;
|
||||
const contentUri = await intent.getClient().uploadContent(bufferRet, {
|
||||
name,
|
||||
onlyContentUri: true,
|
||||
rawResponse: false,
|
||||
type: contenttype,
|
||||
});
|
||||
log.verbose("Media uploaded to ", contentUri);
|
||||
return {
|
||||
mxcUrl: contentUri,
|
||||
size,
|
||||
};
|
||||
} catch (reason) {
|
||||
log.error("Failed to upload content:\n", reason);
|
||||
throw reason;
|
||||
}
|
||||
}) as Promise<IDownloadedFile>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,8 +108,7 @@ export class Util {
|
|||
|
||||
public static GetBotLink(config: DiscordBridgeConfig): string {
|
||||
/* tslint:disable:no-bitwise */
|
||||
const perms = Permissions.FLAGS.READ_MESSAGES! |
|
||||
Permissions.FLAGS.SEND_MESSAGES! |
|
||||
const perms = Permissions.FLAGS.SEND_MESSAGES! |
|
||||
Permissions.FLAGS.CHANGE_NICKNAME! |
|
||||
Permissions.FLAGS.CONNECT! |
|
||||
Permissions.FLAGS.SPEAK! |
|
||||
|
@ -184,42 +128,30 @@ export class Util {
|
|||
if (name[0] === "@" && name.includes(":")) {
|
||||
return name;
|
||||
}
|
||||
const client = intent.getClient();
|
||||
const client = intent.underlyingClient;
|
||||
await intent.ensureRegistered();
|
||||
const matrixUsers = {};
|
||||
let matches = 0;
|
||||
await Promise.all(channelMxids.map((chan) => {
|
||||
// we would use this.bridge.getBot().getJoinedMembers()
|
||||
// but we also want to be able to search through banned members
|
||||
// so we gotta roll our own thing
|
||||
return client._http.authedRequestWithPrefix(
|
||||
undefined,
|
||||
"GET",
|
||||
`/rooms/${encodeURIComponent(chan)}/members`,
|
||||
undefined,
|
||||
undefined,
|
||||
"/_matrix/client/r0",
|
||||
).then((res) => {
|
||||
res.chunk.forEach((member) => {
|
||||
if (member.membership !== "join" && member.membership !== "ban") {
|
||||
return;
|
||||
}
|
||||
const mxid = member.state_key;
|
||||
if (mxid.startsWith("@_discord_")) {
|
||||
return;
|
||||
}
|
||||
let displayName = member.content.displayname;
|
||||
if (!displayName && member.unsigned && member.unsigned.prev_content &&
|
||||
member.unsigned.prev_content.displayname) {
|
||||
displayName = member.unsigned.prev_content.displayname;
|
||||
}
|
||||
if (!displayName) {
|
||||
displayName = mxid.substring(1, mxid.indexOf(":"));
|
||||
}
|
||||
if (name.toLowerCase() === displayName.toLowerCase() || name === mxid) {
|
||||
matrixUsers[mxid] = displayName;
|
||||
matches++;
|
||||
}
|
||||
});
|
||||
await Promise.all(channelMxids.map( async (chan) => {
|
||||
(await client.getRoomMembers(chan, undefined, ["leave"])).forEach((member) => {
|
||||
if (member.membership === "invite") {
|
||||
return;
|
||||
}
|
||||
const mxid = member.stateKey;
|
||||
if (mxid.startsWith("@_discord_")) {
|
||||
return;
|
||||
}
|
||||
let displayName = member.content.displayname;
|
||||
if (!displayName && member.previousContent.displayname) {
|
||||
displayName = member.previousContent.displayname;
|
||||
}
|
||||
if (!displayName) {
|
||||
displayName = mxid.substring(1, mxid.indexOf(":"));
|
||||
}
|
||||
if (name.toLowerCase() === displayName.toLowerCase() || name === mxid) {
|
||||
matrixUsers[mxid] = displayName;
|
||||
matches++;
|
||||
}
|
||||
});
|
||||
}));
|
||||
if (matches === 0) {
|
||||
|
@ -268,7 +200,11 @@ export class Util {
|
|||
}
|
||||
return reply;
|
||||
}
|
||||
if (Object.keys(actions).length === 0) {
|
||||
return "No commands found";
|
||||
}
|
||||
reply += "Available Commands:\n";
|
||||
let commandsHavePermission = 0;
|
||||
for (const actionKey of Object.keys(actions)) {
|
||||
const action = actions[actionKey];
|
||||
if (action.permission !== undefined && permissionCheck) {
|
||||
|
@ -277,12 +213,16 @@ export class Util {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
commandsHavePermission++;
|
||||
reply += ` - \`${prefix} ${actionKey}`;
|
||||
for (const param of action.params) {
|
||||
reply += ` <${param}>`;
|
||||
}
|
||||
reply += `\`: ${action.description}\n`;
|
||||
}
|
||||
if (!commandsHavePermission) {
|
||||
return "No commands found";
|
||||
}
|
||||
reply += "\nParameters:\n";
|
||||
for (const parameterKey of Object.keys(parameters)) {
|
||||
const parameter = parameters[parameterKey];
|
||||
|
@ -313,7 +253,7 @@ export class Util {
|
|||
return `**ERROR:** ${permCheck}`;
|
||||
}
|
||||
if (!permCheck) {
|
||||
return `**ERROR:** insufficiant permissions to use this command! ` +
|
||||
return `**ERROR:** insufficient permissions to use this command! ` +
|
||||
`Try \`${prefix} help\` to see all available commands`;
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +296,7 @@ export class Util {
|
|||
return {command, args};
|
||||
}
|
||||
|
||||
public static async AsyncForEach(arr, callback) {
|
||||
public static async AsyncForEach<T>(arr: T[], callback: (item: T, i: number, a: T[]) => Promise<void>) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
await callback(arr[i], i, arr);
|
||||
}
|
||||
|
@ -392,7 +332,7 @@ export class Util {
|
|||
cat: string,
|
||||
subcat?: string,
|
||||
) {
|
||||
const res: IMatrixEvent = await mxClient.getStateEvent(roomId, "m.room.power_levels");
|
||||
const res: IMatrixEvent = await mxClient.getRoomStateEvent(roomId, "m.room.power_levels", "");
|
||||
let requiredLevel = defaultLevel;
|
||||
if (res && (res[cat] || !subcat)) {
|
||||
if (subcat) {
|
||||
|
@ -415,9 +355,79 @@ export class Util {
|
|||
}
|
||||
return haveLevel >= requiredLevel;
|
||||
}
|
||||
|
||||
public static ParseMxid(unescapedMxid: string, escape: boolean = true) {
|
||||
const RADIX = 16;
|
||||
const parts = unescapedMxid.substr(1).split(":");
|
||||
const domain = parts[1];
|
||||
let localpart = parts[0];
|
||||
if (escape) {
|
||||
const badChars = new Set(localpart.replace(/([a-z0-9]|-|\.|=|_)+/g, ""));
|
||||
badChars.forEach((c) => {
|
||||
const hex = c.charCodeAt(0).toString(RADIX).toLowerCase();
|
||||
localpart = localpart.replace(
|
||||
new RegExp(`\\${c}`, "g"),
|
||||
`=${hex}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
return {
|
||||
domain,
|
||||
localpart,
|
||||
mxid: `@${localpart}:${domain}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Taken from https://github.com/matrix-org/matrix-appservice-bridge/blob/master/lib/models/users/matrix.js
|
||||
public static EscapeStringForUserId(localpart: string) {
|
||||
// NOTE: Currently Matrix accepts / in the userId, although going forward it will be removed.
|
||||
const badChars = new Set(localpart.replace(/([a-z]|[0-9]|-|\.|=|_)+/g, ""));
|
||||
let res = localpart;
|
||||
badChars.forEach((c) => {
|
||||
const hex = c.charCodeAt(0).toString(16).toLowerCase();
|
||||
res = res.replace(
|
||||
new RegExp(`\\x${hex}`, "g"),
|
||||
`=${hex}`,
|
||||
);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
interface IUploadResult {
|
||||
mxcUrl: string;
|
||||
size: number;
|
||||
// Type type
|
||||
type Type = Function; // tslint:disable-line ban-types
|
||||
|
||||
/**
|
||||
* Returns true if `obj` is subtype of at least one of the given types.
|
||||
*/
|
||||
export function isInstanceOfTypes(obj: object, types: Type[]): boolean {
|
||||
return types.some((type) => obj instanceof type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the old error message to the new one and keep its stack trace.
|
||||
*
|
||||
* @example
|
||||
* throw wrapError(e, HighLevelError, "This error is more specific");
|
||||
*
|
||||
* @param oldError The original error to wrap.
|
||||
* @param newErrorType Type of the error returned by this function.
|
||||
* @returns A new error of type `newErrorType` containing the information of
|
||||
* the original error (stacktrace and error message).
|
||||
*/
|
||||
export function wrapError<T extends Error>(
|
||||
oldError: object|Error,
|
||||
newErrorType: new (...args: any[]) => T, // tslint:disable-line no-any
|
||||
...args: any[] // tslint:disable-line no-any trailing-comma
|
||||
): T {
|
||||
const newError = new newErrorType(...args);
|
||||
let appendMsg;
|
||||
if (oldError instanceof Error) {
|
||||
appendMsg = oldError.message;
|
||||
newError.stack = oldError.stack;
|
||||
} else {
|
||||
appendMsg = oldError.toString();
|
||||
}
|
||||
newError.message += ":\n" + appendMsg;
|
||||
return newError;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
// import * as Proxyquire from "proxyquire";
|
||||
import { DiscordStore, CURRENT_SCHEMA } from "../../src/store";
|
||||
import { RemoteStoreRoom, MatrixStoreRoom } from "../../src/db/roomstore";
|
||||
|
||||
|
@ -193,75 +192,3 @@ describe("RoomStore", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe("RoomStore.schema.v8", () => {
|
||||
it("will successfully migrate rooms", async () => {
|
||||
const SCHEMA_VERSION = 8;
|
||||
store = new DiscordStore(":memory:");
|
||||
const roomStore = {
|
||||
select: () => {
|
||||
return [
|
||||
{
|
||||
_id: "DGFUYs4hlXNDmmw0",
|
||||
id: "123",
|
||||
matrix: {extras: {}},
|
||||
matrix_id: "!badroom:localhost",
|
||||
},
|
||||
{
|
||||
_id: "Dd37MWDw57dAQz5p",
|
||||
data: {},
|
||||
id: "!xdnLTCNErGnwsGnmnm:localhost discord_282616294245662720_514843269599985674_bridged",
|
||||
matrix: {
|
||||
extras: {},
|
||||
},
|
||||
matrix_id: "!bridged1:localhost",
|
||||
remote: {
|
||||
discord_channel: "514843269599985674",
|
||||
discord_guild: "282616294245662720",
|
||||
discord_type: "text",
|
||||
plumbed: false,
|
||||
},
|
||||
remote_id: "discord_282616294245662720_514843269599985674_bridged",
|
||||
},
|
||||
{
|
||||
_id: "H3XEftQWj8BZYuCe",
|
||||
data: {},
|
||||
id: "!oGkfjmeNEkJdFasVRF:localhost discord_282616294245662720_520332167952334849",
|
||||
matrix: {
|
||||
extras: {},
|
||||
},
|
||||
matrix_id: "!bridged2:localhost",
|
||||
remote: {
|
||||
discord_channel: "514843269599985674",
|
||||
discord_guild: "282616294245662720",
|
||||
discord_type: "text",
|
||||
plumbed: true,
|
||||
update_icon: true,
|
||||
update_name: false,
|
||||
update_topic: true,
|
||||
},
|
||||
remote_id: "discord_282616294245662720_520332167952334849",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
await store.init(SCHEMA_VERSION, roomStore);
|
||||
expect(await store.roomStore.getEntriesByMatrixId("!badroom:localhost")).to.be.empty;
|
||||
const bridge1 = (await store.roomStore.getEntriesByMatrixId("!bridged1:localhost"))[0];
|
||||
expect(bridge1).to.exist;
|
||||
expect(bridge1.remote).to.not.be.null;
|
||||
expect(bridge1.remote!.data.discord_channel).to.be.equal("514843269599985674");
|
||||
expect(bridge1.remote!.data.discord_guild).to.be.equal("282616294245662720");
|
||||
expect(bridge1.remote!.data.discord_type).to.be.equal("text");
|
||||
expect(!!bridge1.remote!.data.plumbed).to.be.false;
|
||||
const bridge2 = (await store.roomStore.getEntriesByMatrixId("!bridged2:localhost"))[0];
|
||||
expect(bridge2).to.exist;
|
||||
expect(bridge2.remote).to.not.be.null;
|
||||
expect(bridge2.remote!.data.discord_channel).to.be.equal("514843269599985674");
|
||||
expect(bridge2.remote!.data.discord_guild).to.be.equal("282616294245662720");
|
||||
expect(bridge2.remote!.data.discord_type).to.be.equal("text");
|
||||
expect(!!bridge2.remote!.data.plumbed).to.be.true;
|
||||
expect(!!bridge2.remote!.data.update_icon).to.be.true;
|
||||
expect(!!bridge2.remote!.data.update_name).to.be.false;
|
||||
expect(!!bridge2.remote!.data.update_topic).to.be.true;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
--reporter list
|
||||
--ui bdd
|
||||
--require ts-node/register
|
||||
--require source-map-support/register
|
||||
--recursive
|
||||
build/test/config.js
|
||||
build/test
|
279
test/mocks/appservicemock.ts
Normal file
279
test/mocks/appservicemock.ts
Normal file
|
@ -0,0 +1,279 @@
|
|||
import { IMatrixEvent } from "../../src/matrixtypes";
|
||||
import { expect } from "chai";
|
||||
|
||||
/* tslint:disable:no-unused-expression no-any */
|
||||
|
||||
interface IAppserviceMockOpts {
|
||||
roommembers?: IMatrixEvent[];
|
||||
stateEventFetcher?: (roomId, stateType, stateKey) => Promise<any>;
|
||||
eventFetcher?: (roomId, eventId) => Promise<any>;
|
||||
profileFetcher?: (userId) => Promise<any>;
|
||||
botUserId?: string;
|
||||
userIdPrefix?: string;
|
||||
aliasPrefix?: string;
|
||||
joinedrooms?: string[];
|
||||
homeserverName?: string;
|
||||
}
|
||||
|
||||
class AppserviceMockBase {
|
||||
private calls: {[key: string]: [any[]]} = {};
|
||||
|
||||
public wasCalled(funcName: string, throwOnMissing: boolean = true, ...args: any[]): number {
|
||||
const called = this.calls[funcName];
|
||||
if (!called && throwOnMissing) {
|
||||
throw Error(`${funcName} was not called`);
|
||||
} else if (!called) {
|
||||
return 0;
|
||||
} else if (args.length === 0) {
|
||||
return called.length;
|
||||
}
|
||||
const calls = called.filter((callArgs) => {
|
||||
try {
|
||||
expect(callArgs).to.deep.equal(args);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}).length;
|
||||
if (calls === 0 && throwOnMissing) {
|
||||
let msg = `${funcName} was not called with the correct parameters`;
|
||||
if (called.length) {
|
||||
msg += `. Calls that were made:\n${JSON.stringify(called, undefined, 2)}`;
|
||||
}
|
||||
throw Error(msg);
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
public wasNotCalled(funcName: string, throwOnFound: boolean = true, ...args: any[]): boolean {
|
||||
if (this.wasCalled(funcName, false, args)) {
|
||||
if (throwOnFound) {
|
||||
throw Error(`${funcName} was called`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected funcCalled(funcName: string, ...args: any[]) {
|
||||
this.calls[funcName] = this.calls[funcName] || [];
|
||||
this.calls[funcName].push(args);
|
||||
}
|
||||
}
|
||||
|
||||
export class AppserviceMock extends AppserviceMockBase {
|
||||
public botIntent: IntentMock;
|
||||
public botClient: MatrixClientMock;
|
||||
public intents: {[id: string]: IntentMock};
|
||||
|
||||
public get botUserId(): string {
|
||||
return this.opts.botUserId || "@bot:localhost";
|
||||
}
|
||||
|
||||
constructor(private opts: IAppserviceMockOpts = {}) {
|
||||
super();
|
||||
opts.roommembers = opts.roommembers || [];
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
this.intents = {};
|
||||
this.botIntent = new IntentMock(this.opts, "BOT");
|
||||
this.botClient = this.botIntent.underlyingClient;
|
||||
}
|
||||
|
||||
public isNamespacedUser(userId: string) {
|
||||
this.funcCalled("isNamespacedUser", userId);
|
||||
if (this.opts.userIdPrefix) {
|
||||
return userId.startsWith(this.opts.userIdPrefix);
|
||||
}
|
||||
throw Error("No prefix defined");
|
||||
}
|
||||
|
||||
public isNamespacedAlias(alias: string) {
|
||||
this.funcCalled("isNamespacedAlias", alias);
|
||||
if (this.opts.aliasPrefix) {
|
||||
return alias.startsWith(this.opts.aliasPrefix);
|
||||
}
|
||||
throw Error("No prefix defined");
|
||||
}
|
||||
|
||||
public getIntent(userId: string) {
|
||||
this.funcCalled("getIntent", userId);
|
||||
if (!this.intents[userId]) {
|
||||
this.intents[userId] = new IntentMock(this.opts, userId);
|
||||
}
|
||||
return this.intents[userId];
|
||||
}
|
||||
|
||||
public getIntentForSuffix(suffix: string) {
|
||||
this.funcCalled("getIntentForSuffix", suffix);
|
||||
if (!this.intents[suffix]) {
|
||||
this.intents[suffix] = new IntentMock(this.opts, suffix);
|
||||
}
|
||||
return this.intents[suffix];
|
||||
}
|
||||
|
||||
public getAliasForSuffix(suffix: string) {
|
||||
this.funcCalled("getAliasForSuffix", suffix);
|
||||
if (this.opts.aliasPrefix) {
|
||||
return `${this.opts.aliasPrefix}${suffix}:${this.opts.homeserverName}`;
|
||||
}
|
||||
throw Error("No prefix defined");
|
||||
}
|
||||
|
||||
public getIntentForUserId(userId: string) {
|
||||
this.funcCalled("getIntentForUserId", userId);
|
||||
if (!this.intents[userId]) {
|
||||
this.intents[userId] = new IntentMock(this.opts, userId);
|
||||
}
|
||||
return this.intents[userId];
|
||||
}
|
||||
|
||||
public getSuffixForUserId(userId: string) {
|
||||
this.funcCalled("getSuffixForUserId", userId);
|
||||
const localpart = userId.split(":")[0];
|
||||
if (this.opts.userIdPrefix) {
|
||||
return localpart.replace(this.opts.userIdPrefix!, "");
|
||||
}
|
||||
throw Error("No prefix defined");
|
||||
}
|
||||
|
||||
public async setRoomDirectoryVisibility(roomId: string, vis: string) {
|
||||
this.funcCalled("setRoomDirectoryVisibility", roomId, vis);
|
||||
}
|
||||
}
|
||||
|
||||
class IntentMock extends AppserviceMockBase {
|
||||
public readonly underlyingClient: MatrixClientMock;
|
||||
constructor(private opts: IAppserviceMockOpts = {}, private id: string) {
|
||||
super();
|
||||
this.underlyingClient = new MatrixClientMock(opts);
|
||||
}
|
||||
|
||||
public join() {
|
||||
this.funcCalled("join");
|
||||
}
|
||||
|
||||
public joinRoom(roomIdOrAlias: string) {
|
||||
this.funcCalled("joinRoom", roomIdOrAlias);
|
||||
}
|
||||
|
||||
public leave(roomIdOrAlias: string) {
|
||||
this.funcCalled("leave", roomIdOrAlias);
|
||||
}
|
||||
|
||||
public sendText(roomId: string, body: string) {
|
||||
this.funcCalled("sendText", roomId, body);
|
||||
}
|
||||
|
||||
public sendEvent(roomId: string, content: any) {
|
||||
this.funcCalled("sendEvent", roomId, content);
|
||||
}
|
||||
|
||||
public async ensureRegistered(): Promise<void> {
|
||||
this.funcCalled("ensureRegistered");
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixClientMock extends AppserviceMockBase {
|
||||
|
||||
constructor(private opts: IAppserviceMockOpts = {}) {
|
||||
super();
|
||||
}
|
||||
|
||||
public banUser(roomId: string, userId: string) {
|
||||
this.funcCalled("banUser", roomId, userId);
|
||||
}
|
||||
|
||||
public sendMessage(roomId: string, eventContent: IMatrixEvent) {
|
||||
this.funcCalled("sendMessage", roomId, eventContent);
|
||||
}
|
||||
|
||||
public sendEvent(roomId: string, body: string, msgtype: string) {
|
||||
this.funcCalled("sendEvent", roomId, body, msgtype);
|
||||
}
|
||||
|
||||
public getRoomMembers(roomId: string) {
|
||||
this.funcCalled("getRoomMembers", roomId);
|
||||
if (!this.opts.roommembers) {
|
||||
throw Error("No roommembers defined");
|
||||
}
|
||||
return this.opts.roommembers;
|
||||
}
|
||||
|
||||
public getJoinedRooms() {
|
||||
this.funcCalled("getJoinedRooms");
|
||||
if (!this.opts.joinedrooms) {
|
||||
throw Error("No joinedrooms defined");
|
||||
}
|
||||
return this.opts.joinedrooms;
|
||||
}
|
||||
|
||||
public leaveRoom(roomId: string) {
|
||||
this.funcCalled("leaveRoom", roomId);
|
||||
}
|
||||
|
||||
public kickUser(roomId: string, userId: string) {
|
||||
this.funcCalled("kickUser", roomId, userId);
|
||||
}
|
||||
|
||||
public sendStateEvent(roomId: string, type: string, stateKey: string, content: {}) {
|
||||
this.funcCalled("sendStateEvent", roomId, type, stateKey, content);
|
||||
}
|
||||
|
||||
public setAvatarUrl(avatarUrl: string) {
|
||||
this.funcCalled("setAvatarUrl", avatarUrl);
|
||||
}
|
||||
|
||||
public setDisplayName(displayName: string) {
|
||||
this.funcCalled("setDisplayName", displayName);
|
||||
}
|
||||
|
||||
public async getUserProfile(userId: string) {
|
||||
this.funcCalled("getUserProfile", userId);
|
||||
if (this.opts.profileFetcher) {
|
||||
return await this.opts.profileFetcher(userId);
|
||||
}
|
||||
throw Error("No stateEventFetcher defined");
|
||||
}
|
||||
|
||||
public async uploadContent(data: Buffer, contentType: string, filename: string = "noname") {
|
||||
this.funcCalled("uploadContent", data, contentType, filename);
|
||||
return "mxc://" + filename;
|
||||
}
|
||||
|
||||
public mxcToHttp(mxcUrl: string) {
|
||||
this.funcCalled("mxcToHttp", mxcUrl);
|
||||
return mxcUrl.replace("mxc://", "https://");
|
||||
}
|
||||
|
||||
public mxcToHttpThumbnail(mxcUrl: string) {
|
||||
this.funcCalled("mxcToHttpThumbnail", mxcUrl);
|
||||
return mxcUrl.replace("mxc://", "https://");
|
||||
}
|
||||
|
||||
public async getRoomStateEvent(roomId: string, type: string, stateKey: string): Promise<any> {
|
||||
this.funcCalled("getRoomStateEvent", roomId, type, stateKey);
|
||||
if (this.opts.stateEventFetcher) {
|
||||
return await this.opts.stateEventFetcher(roomId, type, stateKey);
|
||||
}
|
||||
throw Error("No stateEventFetcher defined");
|
||||
}
|
||||
|
||||
public async getEvent(roomId: string, eventId: string): Promise<any> {
|
||||
this.funcCalled("getEvent", roomId, eventId);
|
||||
if (this.opts.eventFetcher) {
|
||||
return await this.opts.eventFetcher(roomId, eventId);
|
||||
}
|
||||
throw Error("No getEvent defined");
|
||||
}
|
||||
|
||||
public unbanUser(roomId: string, userId: string) {
|
||||
this.funcCalled("unbanUser", roomId, userId);
|
||||
}
|
||||
|
||||
public async setPresenceStatus(presence: string, status: string) {
|
||||
this.funcCalled("setPresenceStatus", presence, status);
|
||||
}
|
||||
}
|
|
@ -15,8 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {MockMember} from "./member";
|
||||
import {MockCollection} from "./collection";
|
||||
import {Permissions, PermissionResolvable} from "discord.js";
|
||||
import {MockCollection, MockCollectionManager} from "./collection";
|
||||
import {Permissions, PermissionResolvable, TextChannel} from "better-discord.js";
|
||||
import { MockGuild } from "./guild";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
@ -40,3 +41,16 @@ export class MockChannel {
|
|||
return new Permissions(Permissions.FLAGS.MANAGE_WEBHOOKS as PermissionResolvable);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockTextChannel extends TextChannel {
|
||||
constructor(guild?: MockGuild, channelData: any = {}) {
|
||||
// Mock the nessacery
|
||||
super(guild || {
|
||||
client: {
|
||||
options: {
|
||||
messageCacheMaxSize: -1,
|
||||
},
|
||||
},
|
||||
} as any, channelData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Collection } from "discord.js";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
import { Collection } from "better-discord.js";
|
||||
|
||||
export class MockCollection<T1, T2> extends Collection<T1, T2> {
|
||||
public array(): T2[] {
|
||||
|
@ -28,3 +25,22 @@ export class MockCollection<T1, T2> extends Collection<T1, T2> {
|
|||
return [...this.keys()];
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCollectionManager<T1, T2> {
|
||||
private innerCache = new MockCollection<T1, T2>();
|
||||
public get cache() {
|
||||
return this.innerCache;
|
||||
}
|
||||
|
||||
public updateCache(c: MockCollection<T1, T2>) {
|
||||
this.innerCache = c;
|
||||
}
|
||||
|
||||
public resolve(id: T1) {
|
||||
return this.innerCache.get(id);
|
||||
}
|
||||
|
||||
public async fetch(id: T1) {
|
||||
return this.innerCache.get(id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,16 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MockCollection} from "./collection";
|
||||
import {MockCollectionManager} from "./collection";
|
||||
import {MockGuild} from "./guild";
|
||||
import {MockUser} from "./user";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
export class MockDiscordClient {
|
||||
public guilds = new MockCollection<string, MockGuild>();
|
||||
public guilds = new MockCollectionManager<string, MockGuild>();
|
||||
public user: MockUser;
|
||||
private testLoggedIn: boolean = false;
|
||||
private testCallbacks: Map<string, (...data: any[]) => void> = new Map();
|
||||
|
@ -46,9 +45,9 @@ export class MockDiscordClient {
|
|||
type: "text",
|
||||
},
|
||||
];
|
||||
this.guilds.set("123", new MockGuild("MyGuild", channels));
|
||||
this.guilds.set("456", new MockGuild("My Spaces Gui", channels));
|
||||
this.guilds.set("789", new MockGuild("My Dash-Guild", channels));
|
||||
this.guilds.cache.set("123", new MockGuild("MyGuild", channels));
|
||||
this.guilds.cache.set("456", new MockGuild("My Spaces Gui", channels));
|
||||
this.guilds.cache.set("789", new MockGuild("My Dash-Guild", channels));
|
||||
this.user = new MockUser("12345");
|
||||
}
|
||||
|
||||
|
@ -56,6 +55,14 @@ export class MockDiscordClient {
|
|||
this.testCallbacks.set(event, callback);
|
||||
}
|
||||
|
||||
public once(event: string, callback: (...data: any[]) => void) {
|
||||
this.testCallbacks.set(event, () => {
|
||||
this.testCallbacks.delete(event);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async emit(event: string, ...data: any[]) {
|
||||
return await this.testCallbacks.get(event)!.apply(this, data);
|
||||
}
|
||||
|
@ -68,6 +75,9 @@ export class MockDiscordClient {
|
|||
if (this.testCallbacks.has("ready")) {
|
||||
this.testCallbacks.get("ready")!();
|
||||
}
|
||||
if (this.testCallbacks.has("shardReady")) {
|
||||
this.testCallbacks.get("shardReady")!();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MockCollection} from "./collection";
|
||||
import {MockCollectionManager} from "./collection";
|
||||
import {MockMember} from "./member";
|
||||
import {MockEmoji} from "./emoji";
|
||||
import {Channel} from "discord.js";
|
||||
import {Channel} from "better-discord.js";
|
||||
import {MockRole} from "./role";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
export class MockGuild {
|
||||
public channels = new MockCollection<string, Channel>();
|
||||
public members = new MockCollection<string, MockMember>();
|
||||
public emojis = new MockCollection<string, MockEmoji>();
|
||||
public roles = new MockCollection<string, MockRole>();
|
||||
public channels = new MockCollectionManager<string, Channel>();
|
||||
public members = new MockCollectionManager<string, MockMember>();
|
||||
public emojis = new MockCollectionManager<string, MockEmoji>();
|
||||
public roles = new MockCollectionManager<string, MockRole>();
|
||||
public id: string;
|
||||
public name: string;
|
||||
public icon: string;
|
||||
|
@ -35,18 +35,26 @@ export class MockGuild {
|
|||
this.id = id;
|
||||
this.name = name || id;
|
||||
channels.forEach((item) => {
|
||||
this.channels.set(item.id, item);
|
||||
this.channels.cache.set(item.id, item);
|
||||
});
|
||||
}
|
||||
|
||||
public get client() {
|
||||
return {
|
||||
options: {
|
||||
messageCacheMaxSize: -1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async fetchMember(id: string): Promise<MockMember|Error> {
|
||||
if (this.members.has(id)) {
|
||||
return this.members.get(id)!;
|
||||
if (this.members.cache.has(id)) {
|
||||
return this.members.cache.get(id)!;
|
||||
}
|
||||
throw new Error("Member not in this guild");
|
||||
}
|
||||
|
||||
public _mockAddMember(member: MockMember) {
|
||||
this.members.set(member.id, member);
|
||||
this.members.cache.set(member.id, member);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MockCollection} from "./collection";
|
||||
import {MockCollectionManager} from "./collection";
|
||||
import {MockUser} from "./user";
|
||||
import {MockRole} from "./role";
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
@ -27,10 +27,14 @@ export class MockMember {
|
|||
public presence: Discord.Presence;
|
||||
public user: MockUser;
|
||||
public nickname: string;
|
||||
public roles = new MockCollection<string, MockRole>();
|
||||
public roles = new MockCollectionManager<string, MockRole>();
|
||||
constructor(id: string, username: string, public guild: any = null, public displayName: string = username) {
|
||||
this.id = id;
|
||||
this.presence = new Discord.Presence({}, {} as any);
|
||||
this.presence = new Discord.Presence({} as any, {
|
||||
user: {
|
||||
id: this.id,
|
||||
},
|
||||
});
|
||||
this.user = new MockUser(this.id, username);
|
||||
this.nickname = displayName;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Discord from "discord.js";
|
||||
import * as Discord from "better-discord.js";
|
||||
import { MockUser } from "./user";
|
||||
import { MockCollection } from "./collection";
|
||||
|
||||
|
|
16
test/mocks/presence.ts
Normal file
16
test/mocks/presence.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { MockUser } from "./user";
|
||||
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
export class MockPresence {
|
||||
constructor(public internalUser: MockUser, guild: string, public status?: string, public activities: any = []) {
|
||||
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this.internalUser;
|
||||
}
|
||||
|
||||
public get userID() {
|
||||
return this.internalUser.id;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 matrix-appservice-discord
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Presence } from "discord.js";
|
||||
import { Presence } from "better-discord.js";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
@ -25,11 +25,15 @@ export class MockUser {
|
|||
public id: string,
|
||||
public username: string = "",
|
||||
public discriminator: string = "",
|
||||
public avatarURL: string | null = "",
|
||||
public avatarUrl: string | null = "",
|
||||
public avatar: string | null = "",
|
||||
public bot: boolean = false,
|
||||
) { }
|
||||
|
||||
public avatarURL() {
|
||||
return this.avatarUrl;
|
||||
}
|
||||
|
||||
public MockSetPresence(presence: Presence) {
|
||||
this.presence = presence;
|
||||
}
|
||||
|
|
45
test/structures/test_lock.ts
Normal file
45
test/structures/test_lock.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2019 matrix-appservice-discord
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from "chai";
|
||||
import { Lock } from "../../src/structures/lock";
|
||||
import { Util } from "../../src/util";
|
||||
|
||||
const LOCKTIMEOUT = 300;
|
||||
|
||||
describe("Lock", () => {
|
||||
it("should lock and unlock", async () => {
|
||||
const lock = new Lock<string>(LOCKTIMEOUT);
|
||||
const t = Date.now();
|
||||
lock.set("bunny");
|
||||
await lock.wait("bunny");
|
||||
const diff = Date.now() - t;
|
||||
expect(diff).to.be.greaterThan(LOCKTIMEOUT - 1);
|
||||
});
|
||||
it("should lock and unlock early, if unlocked", async () => {
|
||||
const SHORTDELAY = 100;
|
||||
const DELAY_ACCURACY = 5;
|
||||
const lock = new Lock<string>(LOCKTIMEOUT);
|
||||
setTimeout(() => lock.release("fox"), SHORTDELAY);
|
||||
const t = Date.now();
|
||||
lock.set("fox");
|
||||
await lock.wait("fox");
|
||||
const diff = Date.now() - t;
|
||||
// accuracy can be off by a few ms soemtimes
|
||||
expect(diff).to.be.greaterThan(SHORTDELAY - DELAY_ACCURACY);
|
||||
expect(diff).to.be.lessThan(SHORTDELAY + DELAY_ACCURACY);
|
||||
});
|
||||
});
|
|
@ -117,7 +117,7 @@ describe("TimedCache", () => {
|
|||
}
|
||||
i++;
|
||||
}
|
||||
await Util.DelayedPromise(LIVE_FOR);
|
||||
await Util.DelayedPromise(LIVE_FOR * 5);
|
||||
const vals = [...timedCache.entries()];
|
||||
expect(vals).to.be.empty;
|
||||
});
|
||||
|
|
|
@ -14,112 +14,56 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import * as Discord from "discord.js";
|
||||
import { expect } from "chai";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
|
||||
import { ISingleChannelState, IChannelState, ChannelSyncroniser } from "../src/channelsyncroniser";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { ChannelSyncroniser } from "../src/channelsyncroniser";
|
||||
import { MockGuild } from "./mocks/guild";
|
||||
import { MockMember } from "./mocks/member";
|
||||
import { MatrixEventProcessor, MatrixEventProcessorOpts } from "../src/matrixeventprocessor";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { Util } from "../src/util";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { Bridge, MatrixRoom, RemoteRoom } from "matrix-appservice-bridge";
|
||||
import { MatrixStoreRoom, RemoteStoreRoom, IRoomStoreEntry } from "../src/db/roomstore";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
let UTIL_UPLOADED_AVATAR: any = null;
|
||||
let REMOTECHANNEL_SET: any = false;
|
||||
let REMOTECHANNEL_REMOVED: any = false;
|
||||
let ROOM_NAME_SET: any = null;
|
||||
let ROOM_TOPIC_SET: any = null;
|
||||
let ROOM_AVATAR_SET: any = null;
|
||||
let STATE_EVENT_SENT: any = false;
|
||||
let ALIAS_DELETED: any = false;
|
||||
let ROOM_DIRECTORY_VISIBILITY: any = null;
|
||||
|
||||
const ChannelSync = (Proxyquire("../src/channelsyncroniser", {
|
||||
"./util": {
|
||||
Util: {
|
||||
ApplyPatternString: Util.ApplyPatternString,
|
||||
UploadContentFromUrl: async () => {
|
||||
UTIL_UPLOADED_AVATAR = true;
|
||||
return {mxcUrl: "avatarset"};
|
||||
},
|
||||
DownloadFile: () => ({buffer: "afile"}),
|
||||
},
|
||||
},
|
||||
})).ChannelSyncroniser;
|
||||
|
||||
class Entry {
|
||||
class Entry implements IRoomStoreEntry {
|
||||
public id: any;
|
||||
public matrix: MatrixRoom;
|
||||
public remote: RemoteRoom;
|
||||
public matrix: MatrixStoreRoom|null;
|
||||
public remote: RemoteStoreRoom|null;
|
||||
public data: any;
|
||||
constructor(doc: any = {}) {
|
||||
this.matrix = doc.matrix_id ? new MatrixRoom(doc.matrix_id, doc.matrix) : undefined;
|
||||
this.remote = doc.remote_id ? new RemoteRoom(doc.remote_id, doc.remote) : undefined;
|
||||
this.matrix = doc.matrix_id ? new MatrixStoreRoom(doc.matrix_id) : null;
|
||||
this.remote = doc.remote_id ? new RemoteStoreRoom(doc.remote_id, doc.remote) : null;
|
||||
this.data = doc.data;
|
||||
}
|
||||
}
|
||||
|
||||
function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser {
|
||||
UTIL_UPLOADED_AVATAR = false;
|
||||
const bridge: any = {
|
||||
getIntent: (id) => {
|
||||
ROOM_NAME_SET = null;
|
||||
ROOM_TOPIC_SET = null;
|
||||
ROOM_AVATAR_SET = null;
|
||||
STATE_EVENT_SENT = false;
|
||||
ALIAS_DELETED = false;
|
||||
ROOM_DIRECTORY_VISIBILITY = null;
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
deleteAlias: async (alias) => {
|
||||
ALIAS_DELETED = true;
|
||||
},
|
||||
getStateEvent: async (mxid, event) => {
|
||||
if (event === "m.room.canonical_alias") {
|
||||
if (mxid === "!valid:localhost") {
|
||||
return {
|
||||
alias: "#alias:localhost",
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return event;
|
||||
},
|
||||
sendStateEvent: async (mxid, event, data) => {
|
||||
STATE_EVENT_SENT = true;
|
||||
},
|
||||
setRoomDirectoryVisibility: async (mxid, visibility) => {
|
||||
ROOM_DIRECTORY_VISIBILITY = visibility;
|
||||
},
|
||||
setRoomName: async (mxid, name) => {
|
||||
ROOM_NAME_SET = name;
|
||||
},
|
||||
setRoomTopic: async (mxid, topic) => {
|
||||
ROOM_TOPIC_SET = topic;
|
||||
},
|
||||
};
|
||||
},
|
||||
setRoomAvatar: async (mxid, mxc) => {
|
||||
ROOM_AVATAR_SET = mxc;
|
||||
},
|
||||
setRoomName: async (mxid, name) => {
|
||||
ROOM_NAME_SET = name;
|
||||
},
|
||||
setRoomTopic: async (mxid, topic) => {
|
||||
ROOM_TOPIC_SET = topic;
|
||||
},
|
||||
};
|
||||
function CreateChannelSync(remoteChannels: any[] = []) {
|
||||
const bridge = new AppserviceMock({
|
||||
aliasPrefix: "#_discord_",
|
||||
homeserverName: "localhost",
|
||||
stateEventFetcher: async (roomId: string, type: string, key: string) => {
|
||||
if (roomId === "!valid:localhost" && type === "m.room.canonical_alias" && key === "") {
|
||||
return { alias: "#alias:localhost"};
|
||||
}
|
||||
throw Error("Event not found");
|
||||
},
|
||||
};
|
||||
});
|
||||
REMOTECHANNEL_REMOVED = false;
|
||||
REMOTECHANNEL_SET = false;
|
||||
const roomStore = {
|
||||
|
@ -156,10 +100,10 @@ function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser {
|
|||
return true;
|
||||
});
|
||||
},
|
||||
removeEntriesByMatrixRoomId: (room) => {
|
||||
removeEntriesByMatrixRoomId: () => {
|
||||
REMOTECHANNEL_REMOVED = true;
|
||||
},
|
||||
upsertEntry: (room) => {
|
||||
upsertEntry: () => {
|
||||
REMOTECHANNEL_SET = true;
|
||||
},
|
||||
};
|
||||
|
@ -169,8 +113,9 @@ function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser {
|
|||
const config = new DiscordBridgeConfig();
|
||||
config.bridge.domain = "localhost";
|
||||
config.channel.namePattern = "[Discord] :guild :name";
|
||||
const cs = new ChannelSync(bridge as Bridge, config, discordbot, roomStore) as ChannelSyncroniser;
|
||||
return cs;
|
||||
const fakedBridge = bridge as any;
|
||||
const channelSync = new ChannelSync(fakedBridge as Appservice, config, discordbot, roomStore) as ChannelSyncroniser;
|
||||
return {channelSync, bridge};
|
||||
}
|
||||
|
||||
describe("ChannelSyncroniser", () => {
|
||||
|
@ -190,7 +135,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
await channelSync.OnDelete(chan as any);
|
||||
|
||||
expect(REMOTECHANNEL_REMOVED).is.false;
|
||||
|
@ -210,7 +155,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
await channelSync.OnDelete(chan as any);
|
||||
|
||||
expect(REMOTECHANNEL_REMOVED).is.true;
|
||||
|
@ -231,7 +176,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const chans = await channelSync.GetRoomIdsFromChannel(chan as any);
|
||||
|
||||
expect(chans.length).equals(1);
|
||||
|
@ -267,7 +212,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const chans = await channelSync.GetRoomIdsFromChannel(chan as any);
|
||||
/* tslint:disable:no-magic-numbers */
|
||||
expect(chans.length).equals(2);
|
||||
|
@ -278,7 +223,7 @@ describe("ChannelSyncroniser", () => {
|
|||
it("should reject on no rooms", async () => {
|
||||
const chan = new MockChannel();
|
||||
chan.id = "blah";
|
||||
const channelSync = CreateChannelSync();
|
||||
const {channelSync} = CreateChannelSync();
|
||||
try {
|
||||
await channelSync.GetRoomIdsFromChannel(chan as any);
|
||||
throw new Error("didn't fail");
|
||||
|
@ -297,19 +242,38 @@ describe("ChannelSyncroniser", () => {
|
|||
it("Should get one canonical alias for a room", async () => {
|
||||
const chan = new MockChannel();
|
||||
chan.id = "678";
|
||||
const channelSync = CreateChannelSync();
|
||||
const {channelSync} = CreateChannelSync();
|
||||
channelSync.GetRoomIdsFromChannel = getIds;
|
||||
const alias = await channelSync.GetAliasFromChannel(chan as any);
|
||||
expect(alias).to.equal("#alias:localhost");
|
||||
});
|
||||
it("Should prefer non-discord canonical aliases", async () => {
|
||||
const {channelSync} = CreateChannelSync();
|
||||
channelSync.GetRoomIdsFromChannel = async (_) => {
|
||||
return ["!discord:localhost", "!valid:localhost"];
|
||||
};
|
||||
const alias = await channelSync.GetAliasFromChannel({} as any);
|
||||
|
||||
expect(alias).to.equal("#alias:localhost");
|
||||
});
|
||||
it("Should use discord canonical alias if none other present", async () => {
|
||||
const {channelSync} = CreateChannelSync();
|
||||
channelSync.GetRoomIdsFromChannel = async (_) => {
|
||||
return ["!discord:localhost"];
|
||||
};
|
||||
const alias = await channelSync.GetAliasFromChannel({
|
||||
guild: { id: "123" },
|
||||
id: "123",
|
||||
} as any);
|
||||
|
||||
expect(alias).to.equal("#_discord_123_123:localhost");
|
||||
});
|
||||
it("Should return null if no alias found and no guild present", async () => {
|
||||
const chan = new MockChannel();
|
||||
chan.id = "123";
|
||||
const channelSync = CreateChannelSync();
|
||||
const {channelSync} = CreateChannelSync();
|
||||
channelSync.GetRoomIdsFromChannel = getIds;
|
||||
const alias = await channelSync.GetAliasFromChannel(chan as any);
|
||||
|
||||
expect(alias).to.equal(null);
|
||||
});
|
||||
it("Should return a #_discord_ alias if a guild is present", async () => {
|
||||
|
@ -317,10 +281,9 @@ describe("ChannelSyncroniser", () => {
|
|||
const guild = new MockGuild("123");
|
||||
chan.id = "123";
|
||||
chan.guild = guild;
|
||||
const channelSync = CreateChannelSync();
|
||||
const {channelSync} = CreateChannelSync();
|
||||
channelSync.GetRoomIdsFromChannel = getIds;
|
||||
const alias = await channelSync.GetAliasFromChannel(chan as any);
|
||||
|
||||
expect(alias).to.equal("#_discord_123_123:localhost");
|
||||
});
|
||||
});
|
||||
|
@ -330,7 +293,7 @@ describe("ChannelSyncroniser", () => {
|
|||
chan.type = "text";
|
||||
chan.id = "blah";
|
||||
|
||||
const channelSync = CreateChannelSync();
|
||||
const {channelSync} = CreateChannelSync();
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.id).equals(chan.id);
|
||||
expect(state.mxChannels.length).equals(0);
|
||||
|
@ -359,7 +322,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].name).equals("[Discord] newGuild #newName");
|
||||
|
@ -387,7 +350,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].name).is.null;
|
||||
|
@ -417,7 +380,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].name).is.null;
|
||||
|
@ -444,7 +407,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].iconUrl).equals("https://cdn.discordapp.com/icons/654321/new_icon.png");
|
||||
|
@ -471,7 +434,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const { channelSync } = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].iconUrl).equals("https://cdn.discordapp.com/icons/654321/a_new_icon.gif");
|
||||
|
@ -498,7 +461,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].iconUrl).is.null;
|
||||
|
@ -525,7 +488,7 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const {channelSync} = CreateChannelSync(testStore);
|
||||
const state = await channelSync.GetChannelUpdateState(chan as any);
|
||||
expect(state.mxChannels.length).equals(1);
|
||||
expect(state.mxChannels[0].removeIcon).is.true;
|
||||
|
@ -559,13 +522,19 @@ describe("ChannelSyncroniser", () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const channelSync = CreateChannelSync(testStore);
|
||||
const state = await channelSync.OnUpdate(chan as any);
|
||||
expect(ROOM_NAME_SET).equals("[Discord] newGuild #newName");
|
||||
expect(ROOM_TOPIC_SET).equals("newTopic");
|
||||
expect(ROOM_AVATAR_SET).equals("avatarset");
|
||||
const {channelSync, bridge} = CreateChannelSync(testStore);
|
||||
await channelSync.OnUpdate(chan as any);
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendStateEvent", true, "!1:localhost", "m.room.name", "", {
|
||||
name: "[Discord] newGuild #newName",
|
||||
});
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendStateEvent", true, "!1:localhost", "m.room.topic", "", {
|
||||
topic: "newTopic",
|
||||
});
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendStateEvent", true, "!1:localhost", "m.room.avatar", "",
|
||||
{
|
||||
url: "mxc://new_icon",
|
||||
});
|
||||
expect(REMOTECHANNEL_SET).is.true;
|
||||
expect(UTIL_UPLOADED_AVATAR).is.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
import {DiscordBridgeConfigAuth} from "../src/config";
|
||||
import {MockDiscordClient} from "./mocks/discordclient";
|
||||
import { DiscordBridgeConfigAuth } from "../src/config";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
const DiscordClientFactory = Proxyquire("../src/clientfactory", {
|
||||
"discord.js": { Client: require("./mocks/discordclient").MockDiscordClient },
|
||||
"better-discord.js": { Client: require("./mocks/discordclient").MockDiscordClient },
|
||||
}).DiscordClientFactory;
|
||||
|
||||
const STORE = {
|
||||
|
|
|
@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
describe("DiscordBridgeConfig.applyConfig", () => {
|
||||
it("should merge configs correctly", () => {
|
||||
const config = new DiscordBridgeConfig();
|
||||
|
@ -29,6 +27,7 @@ describe("DiscordBridgeConfig.applyConfig", () => {
|
|||
bridge: {
|
||||
disableDeletionForwarding: true,
|
||||
disableDiscordMentions: false,
|
||||
disableInviteNotifications: true,
|
||||
disableJoinLeaveNotifications: true,
|
||||
disableTypingNotifications: true,
|
||||
enableSelfServiceBridging: false,
|
||||
|
@ -44,6 +43,7 @@ describe("DiscordBridgeConfig.applyConfig", () => {
|
|||
expect(config.bridge.disableDeletionForwarding).to.be.true;
|
||||
expect(config.bridge.enableSelfServiceBridging).to.be.false;
|
||||
expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
|
||||
expect(config.bridge.disableInviteNotifications).to.be.true;
|
||||
expect(config.logging.console).to.equal("warn");
|
||||
});
|
||||
it("should merge environment overrides correctly", () => {
|
||||
|
@ -60,10 +60,12 @@ describe("DiscordBridgeConfig.applyConfig", () => {
|
|||
});
|
||||
config.applyEnvironmentOverrides({
|
||||
APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false,
|
||||
APPSERVICE_DISCORD_BRIDGE_DISABLE_INVITE_NOTIFICATIONS: true,
|
||||
APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
|
||||
APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug",
|
||||
});
|
||||
expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
|
||||
expect(config.bridge.disableInviteNotifications).to.be.true;
|
||||
expect(config.bridge.disableDeletionForwarding).to.be.false;
|
||||
expect(config.bridge.disableDiscordMentions).to.be.false;
|
||||
expect(config.bridge.homeserverUrl).to.equal("blah");
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 matrix-appservice-discord
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yaml from "js-yaml";
|
||||
import * as Chai from "chai";
|
||||
import { ConfigValidator } from "matrix-appservice-bridge";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
describe("ConfigSchema", () => {
|
||||
const validator = new ConfigValidator("./config/config.schema.yaml");
|
||||
it("should successfully validate a minimal config", () => {
|
||||
const yamlConfig = yaml.safeLoad(`
|
||||
bridge:
|
||||
domain: localhost
|
||||
homeserverUrl: "http://localhost:8008"
|
||||
auth:
|
||||
clientID: foo
|
||||
botToken: foobar`);
|
||||
validator.validate(yamlConfig);
|
||||
});
|
||||
it("should successfully validate the sample config", () => {
|
||||
validator.validate("./config/config.sample.yaml");
|
||||
});
|
||||
});
|
|
@ -14,53 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
import * as Discord from "discord.js";
|
||||
import { Log } from "../src/log";
|
||||
|
||||
import { MockGuild } from "./mocks/guild";
|
||||
import { MockMember } from "./mocks/member";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { MockDiscordClient } from "./mocks/discordclient";
|
||||
import { MockMessage } from "./mocks/message";
|
||||
import { Util } from "../src/util";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
import { MockUser } from "./mocks/user";
|
||||
import { MockTextChannel } from "./mocks/channel";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
const assert = Chai.assert;
|
||||
// const should = Chai.should as any;
|
||||
|
||||
const mockBridge = {
|
||||
getIntentFromLocalpart: (localpart: string) => {
|
||||
return {
|
||||
sendTyping: (room: string, isTyping: boolean) => {
|
||||
return;
|
||||
},
|
||||
};
|
||||
},
|
||||
getRoomStore: () => {
|
||||
return {
|
||||
getEntriesByRemoteRoomData: async (data) => {
|
||||
if (data.discord_channel === "321") {
|
||||
return [{
|
||||
matrix: {
|
||||
getId: () => "foobar:example.com",
|
||||
},
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserStore: () => {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
const mockBridge = new AppserviceMock({});
|
||||
|
||||
const modDiscordBot = Proxyquire("../src/bot", {
|
||||
"./clientfactory": require("./mocks/discordclientfactory"),
|
||||
|
@ -68,6 +37,9 @@ const modDiscordBot = Proxyquire("../src/bot", {
|
|||
Util: {
|
||||
AsyncForEach: Util.AsyncForEach,
|
||||
DelayedPromise: Util.DelayedPromise,
|
||||
DownloadFile: async () => {
|
||||
return {buffer: Buffer.alloc(1000)};
|
||||
},
|
||||
UploadContentFromUrl: async () => {
|
||||
return {mxcUrl: "uploaded"};
|
||||
},
|
||||
|
@ -91,7 +63,6 @@ describe("DiscordBot", () => {
|
|||
describe("run()", () => {
|
||||
it("should resolve when ready.", async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
|
@ -103,7 +74,6 @@ describe("DiscordBot", () => {
|
|||
describe("LookupRoom()", () => {
|
||||
beforeEach( async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
|
@ -133,42 +103,27 @@ describe("DiscordBot", () => {
|
|||
});
|
||||
});
|
||||
describe("OnMessage()", () => {
|
||||
let SENT_MESSAGE = false;
|
||||
const channel = new MockTextChannel();
|
||||
const msg = new MockMessage(channel);
|
||||
const author = new MockUser("11111");
|
||||
let HANDLE_COMMAND = false;
|
||||
let ATTACHMENT = {} as any;
|
||||
let MSGTYPE = "";
|
||||
function getDiscordBot() {
|
||||
SENT_MESSAGE = false;
|
||||
HANDLE_COMMAND = false;
|
||||
ATTACHMENT = {};
|
||||
MSGTYPE = "";
|
||||
mockBridge.cleanup();
|
||||
const discord = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
);
|
||||
discord.bot = { user: { id: "654" } };
|
||||
discord.GetIntentFromDiscordMember = (_) => {return {
|
||||
sendMessage: async (room, msg) => {
|
||||
SENT_MESSAGE = true;
|
||||
if (msg.info) {
|
||||
ATTACHMENT = msg.info;
|
||||
}
|
||||
MSGTYPE = msg.msgtype;
|
||||
return {
|
||||
event_id: "$fox:localhost",
|
||||
};
|
||||
},
|
||||
}; };
|
||||
discord.userSync = {
|
||||
OnUpdateUser: async (user) => { },
|
||||
OnUpdateUser: async () => { },
|
||||
};
|
||||
discord.channelSync = {
|
||||
GetRoomIdsFromChannel: async (chan) => ["!asdf:localhost"],
|
||||
GetRoomIdsFromChannel: async () => ["!asdf:localhost"],
|
||||
};
|
||||
discord.discordCommandHandler = {
|
||||
Process: async (msg) => { HANDLE_COMMAND = true; },
|
||||
Process: async () => { HANDLE_COMMAND = true; },
|
||||
};
|
||||
discord.store = {
|
||||
Insert: async (_) => { },
|
||||
|
@ -178,104 +133,157 @@ describe("DiscordBot", () => {
|
|||
it("ignores own messages", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const author = new MockMember("654", "TestUsername");
|
||||
const ownAuthor = new MockUser("654", "TestUsername");
|
||||
guild._mockAddMember(author);
|
||||
const channel = new Discord.TextChannel(guild, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.author = ownAuthor;
|
||||
msg.content = "Hi!";
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(SENT_MESSAGE, false);
|
||||
expect(mockBridge.getIntent(author.id).wasCalled("sendEvent", false)).to.equal(0);
|
||||
});
|
||||
it("Passes on !matrix commands", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.content = "!matrix test";
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(HANDLE_COMMAND, true);
|
||||
expect(HANDLE_COMMAND).to.be.true;
|
||||
});
|
||||
it("skips empty messages", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.content = "";
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(SENT_MESSAGE, false);
|
||||
msg.author = author;
|
||||
await discordBot.OnMessage(msg as any);
|
||||
expect(mockBridge.getIntent(author.id).wasCalled("sendEvent", false)).to.equal(0);
|
||||
});
|
||||
it("sends normal messages", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.content = "Foxies are amazing!";
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(SENT_MESSAGE, true);
|
||||
await discordBot.OnMessage(msg as any);
|
||||
mockBridge.getIntent(author.id).wasCalled("sendEvent");
|
||||
});
|
||||
it("sends edit messages", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
msg.author = author;
|
||||
msg.content = "Foxies are super amazing!";
|
||||
await discordBot.OnMessage(msg, "editevent");
|
||||
mockBridge.getIntent(author.id).wasCalled("sendEvent", true, "!asdf:localhost", {
|
||||
"body": "* Foxies are super amazing!",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "* Foxies are super amazing!",
|
||||
"m.new_content": {
|
||||
body: "Foxies are super amazing!",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "Foxies are super amazing!",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
"m.relates_to": { event_id: "editevent", rel_type: "m.replace" },
|
||||
"msgtype": "m.text",
|
||||
});
|
||||
});
|
||||
it("uploads images", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.attachments.set("1234", {
|
||||
filename: "someimage.png",
|
||||
filesize: 42,
|
||||
name: "someimage.png",
|
||||
size: 42,
|
||||
height: 0,
|
||||
url: "asdf",
|
||||
width: 0,
|
||||
});
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(MSGTYPE, "m.image");
|
||||
Chai.assert.equal(ATTACHMENT.mimetype, "image/png");
|
||||
const intent = mockBridge.getIntent(author.id);
|
||||
intent.underlyingClient.wasCalled("uploadContent");
|
||||
intent.wasCalled("sendEvent", true, "!asdf:localhost", {
|
||||
body: "someimage.png",
|
||||
external_url: "asdf",
|
||||
info: {
|
||||
h: 0,
|
||||
mimetype: "image/png",
|
||||
size: 42,
|
||||
w: 0,
|
||||
},
|
||||
msgtype: "m.image",
|
||||
url: "mxc://someimage.png",
|
||||
});
|
||||
});
|
||||
it("uploads videos", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.attachments.set("1234", {
|
||||
filename: "foxes.mov",
|
||||
filesize: 42,
|
||||
name: "foxes.mov",
|
||||
size: 42,
|
||||
height: 0,
|
||||
url: "asdf",
|
||||
width: 0,
|
||||
});
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(MSGTYPE, "m.video");
|
||||
Chai.assert.equal(ATTACHMENT.mimetype, "video/quicktime");
|
||||
const intent = mockBridge.getIntent(author.id);
|
||||
intent.underlyingClient.wasCalled("uploadContent");
|
||||
intent.wasCalled("sendEvent", true, "!asdf:localhost", {
|
||||
body: "foxes.mov",
|
||||
external_url: "asdf",
|
||||
info: {
|
||||
h: 0,
|
||||
mimetype: "video/quicktime",
|
||||
size: 42,
|
||||
w: 0,
|
||||
},
|
||||
msgtype: "m.video",
|
||||
url: "mxc://foxes.mov",
|
||||
});
|
||||
});
|
||||
it("uploads audio", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.attachments.set("1234", {
|
||||
filename: "meow.mp3",
|
||||
filesize: 42,
|
||||
name: "meow.mp3",
|
||||
size: 42,
|
||||
height: 0,
|
||||
url: "asdf",
|
||||
width: 0,
|
||||
});
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(MSGTYPE, "m.audio");
|
||||
Chai.assert.equal(ATTACHMENT.mimetype, "audio/mpeg");
|
||||
const intent = mockBridge.getIntent(author.id);
|
||||
intent.underlyingClient.wasCalled("uploadContent");
|
||||
intent.wasCalled("sendEvent", true, "!asdf:localhost", {
|
||||
body: "meow.mp3",
|
||||
external_url: "asdf",
|
||||
info: {
|
||||
mimetype: "audio/mpeg",
|
||||
size: 42,
|
||||
},
|
||||
msgtype: "m.audio",
|
||||
url: "mxc://meow.mp3",
|
||||
});
|
||||
});
|
||||
it("uploads other files", async () => {
|
||||
discordBot = getDiscordBot();
|
||||
const channel = new Discord.TextChannel({} as any, {} as any);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
msg.author = author;
|
||||
msg.attachments.set("1234", {
|
||||
filename: "meow.zip",
|
||||
filesize: 42,
|
||||
name: "meow.zip",
|
||||
size: 42,
|
||||
height: 0,
|
||||
url: "asdf",
|
||||
width: 0,
|
||||
});
|
||||
await discordBot.OnMessage(msg);
|
||||
Chai.assert.equal(MSGTYPE, "m.file");
|
||||
Chai.assert.equal(ATTACHMENT.mimetype, "application/zip");
|
||||
const intent = mockBridge.getIntent(author.id);
|
||||
intent.underlyingClient.wasCalled("uploadContent");
|
||||
intent.wasCalled("sendEvent", true, "!asdf:localhost", {
|
||||
body: "meow.zip",
|
||||
external_url: "asdf",
|
||||
info: {
|
||||
mimetype: "application/zip",
|
||||
size: 42,
|
||||
},
|
||||
msgtype: "m.file",
|
||||
url: "mxc://meow.zip",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("OnMessageUpdate()", () => {
|
||||
it("should return on an unchanged message", async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
|
@ -283,7 +291,7 @@ describe("DiscordBot", () => {
|
|||
|
||||
const guild: any = new MockGuild("123", []);
|
||||
guild._mockAddMember(new MockMember("12345", "TestUsername"));
|
||||
const channel = new Discord.TextChannel(guild, {} as any);
|
||||
const channel = new MockTextChannel(guild);
|
||||
const oldMsg = new MockMessage(channel) as any;
|
||||
const newMsg = new MockMessage(channel) as any;
|
||||
oldMsg.embeds = [];
|
||||
|
@ -298,11 +306,10 @@ describe("DiscordBot", () => {
|
|||
discordBot.SendMatrixMessage = (...args) => checkMsgSent = true;
|
||||
|
||||
await discordBot.OnMessageUpdate(oldMsg, newMsg);
|
||||
Chai.assert.equal(checkMsgSent, false);
|
||||
expect(checkMsgSent).to.be.false;
|
||||
});
|
||||
it("should send a matrix message on an edited discord message", async () => {
|
||||
it("should send a matrix edit on an edited discord message", async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
|
@ -311,7 +318,7 @@ describe("DiscordBot", () => {
|
|||
|
||||
const guild: any = new MockGuild("123", []);
|
||||
guild._mockAddMember(new MockMember("12345", "TestUsername"));
|
||||
const channel = new Discord.TextChannel(guild, {} as any);
|
||||
const channel = new MockTextChannel(guild);
|
||||
const oldMsg = new MockMessage(channel) as any;
|
||||
const newMsg = new MockMessage(channel) as any;
|
||||
oldMsg.embeds = [];
|
||||
|
@ -321,16 +328,27 @@ describe("DiscordBot", () => {
|
|||
oldMsg.content = "a";
|
||||
newMsg.content = "b";
|
||||
|
||||
// Mock the SendMatrixMessage method to check if it is called
|
||||
let checkMsgSent = false;
|
||||
discordBot.SendMatrixMessage = (...args) => checkMsgSent = true;
|
||||
let storeMockResults = 1;
|
||||
discordBot.store = {
|
||||
Get: (a, b) => {
|
||||
return {
|
||||
MatrixId: "editedid",
|
||||
Next: () => storeMockResults--,
|
||||
Result: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
let checkEditEventSent = "";
|
||||
discordBot.OnMessage = (str, event) => {
|
||||
checkEditEventSent = event;
|
||||
};
|
||||
|
||||
await discordBot.OnMessageUpdate(oldMsg, newMsg);
|
||||
Chai.assert.equal(checkMsgSent, true);
|
||||
expect(checkEditEventSent).to.equal("editedid");
|
||||
});
|
||||
it("should delete and re-send if it is the newest message", async () => {
|
||||
it("should send a new message if no store event found", async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
|
@ -344,7 +362,7 @@ describe("DiscordBot", () => {
|
|||
|
||||
const guild: any = new MockGuild("123", []);
|
||||
guild._mockAddMember(new MockMember("12345", "TestUsername"));
|
||||
const channel = new Discord.TextChannel(guild, {} as any);
|
||||
const channel = new MockTextChannel(guild, {} as any);
|
||||
const oldMsg = new MockMessage(channel) as any;
|
||||
const newMsg = new MockMessage(channel) as any;
|
||||
oldMsg.embeds = [];
|
||||
|
@ -354,27 +372,36 @@ describe("DiscordBot", () => {
|
|||
oldMsg.content = "a";
|
||||
newMsg.content = "b";
|
||||
|
||||
let deletedMessage = false;
|
||||
discordBot.DeleteDiscordMessage = async (_) => { deletedMessage = true; };
|
||||
let sentMessage = false;
|
||||
discordBot.OnMessage = async (_) => { sentMessage = true; };
|
||||
let storeMockResults = 0;
|
||||
discordBot.store = {
|
||||
Get: (a, b) => {
|
||||
return {
|
||||
MatrixId: "editedid",
|
||||
Next: () => storeMockResults--,
|
||||
Result: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
let checkEditEventSent = "wrong";
|
||||
discordBot.OnMessage = (str, event) => {
|
||||
checkEditEventSent = event;
|
||||
};
|
||||
|
||||
await discordBot.OnMessageUpdate(oldMsg, newMsg);
|
||||
Chai.assert.equal(deletedMessage, true);
|
||||
Chai.assert.equal(sentMessage, true);
|
||||
expect(checkEditEventSent).to.be.undefined;
|
||||
});
|
||||
});
|
||||
describe("event:message", () => {
|
||||
it("should delay messages so they arrive in order", async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
);
|
||||
let expected = 0;
|
||||
discordBot.OnMessage = async (msg: any) => {
|
||||
assert.equal(msg.n, expected);
|
||||
expect(msg.n).to.eq(expected);
|
||||
expected++;
|
||||
};
|
||||
const client: MockDiscordClient = (await discordBot.ClientFactory.getClient()) as MockDiscordClient;
|
||||
|
@ -389,7 +416,6 @@ describe("DiscordBot", () => {
|
|||
});
|
||||
it("should handle messages that reject in the queue", async () => {
|
||||
discordBot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
|
@ -397,7 +423,7 @@ describe("DiscordBot", () => {
|
|||
let expected = 0;
|
||||
const THROW_EVERY = 5;
|
||||
discordBot.OnMessage = async (msg: any) => {
|
||||
assert.equal(msg.n, expected);
|
||||
expect(msg.n).to.eq(expected);
|
||||
expected++;
|
||||
if (expected % THROW_EVERY === 0) {
|
||||
return Promise.reject("Deliberate throw in test");
|
||||
|
@ -413,74 +439,7 @@ describe("DiscordBot", () => {
|
|||
await client.emit("message", { n, channel: { guild: { id: CHANID }, id: CHANID} });
|
||||
}
|
||||
await discordBot.discordMessageQueue[CHANID];
|
||||
assert.equal(expected, ITERATIONS);
|
||||
expect(expected).to.eq(ITERATIONS);
|
||||
});
|
||||
});
|
||||
describe("locks", () => {
|
||||
it("should lock and unlock a channel", async () => {
|
||||
const bot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
config,
|
||||
mockBridge,
|
||||
{},
|
||||
) as DiscordBot;
|
||||
const chan = new MockChannel("123") as any;
|
||||
const t = Date.now();
|
||||
bot.lockChannel(chan);
|
||||
await bot.waitUnlock(chan);
|
||||
const diff = Date.now() - t;
|
||||
expect(diff).to.be.greaterThan(config.limits.discordSendDelay - 1);
|
||||
});
|
||||
it("should lock and unlock a channel early, if unlocked", async () => {
|
||||
const discordSendDelay = 500;
|
||||
const SHORTDELAY = 100;
|
||||
const MINEXPECTEDDELAY = 95;
|
||||
const bot = new modDiscordBot.DiscordBot(
|
||||
"",
|
||||
{
|
||||
bridge: {
|
||||
domain: "localhost",
|
||||
},
|
||||
limits: {
|
||||
discordSendDelay,
|
||||
},
|
||||
},
|
||||
mockBridge,
|
||||
{},
|
||||
) as DiscordBot;
|
||||
const chan = new MockChannel("123") as any;
|
||||
setTimeout(() => bot.unlockChannel(chan), SHORTDELAY);
|
||||
const t = Date.now();
|
||||
bot.lockChannel(chan);
|
||||
await bot.waitUnlock(chan);
|
||||
const diff = Date.now() - t;
|
||||
// Date accuracy can be off by a few ms sometimes.
|
||||
expect(diff).to.be.greaterThan(MINEXPECTEDDELAY);
|
||||
});
|
||||
});
|
||||
// });
|
||||
// describe("ProcessMatrixMsgEvent()", () => {
|
||||
//
|
||||
// });
|
||||
// describe("UpdateRoom()", () => {
|
||||
//
|
||||
// });
|
||||
// describe("UpdateUser()", () => {
|
||||
//
|
||||
// });
|
||||
// describe("UpdatePresence()", () => {
|
||||
//
|
||||
// });
|
||||
// describe("OnTyping()", () => {
|
||||
// const discordBot = new modDiscordBot.DiscordBot(
|
||||
// config,
|
||||
// );
|
||||
// discordBot.run();
|
||||
// it("should reject an unknown room.", () => {
|
||||
// return assert.isRejected(discordBot.OnTyping( {id: "512"}, {id: "12345"}, true));
|
||||
// });
|
||||
// it("should resolve a known room.", () => {
|
||||
// return assert.isFulfilled(discordBot.OnTyping( {id: "321"}, {id: "12345"}, true));
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
|
|
@ -16,45 +16,23 @@ limitations under the License.
|
|||
import * as Chai from "chai";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
|
||||
import { DiscordCommandHandler } from "../src/discordcommandhandler";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { MockMember } from "./mocks/member";
|
||||
import { MockGuild } from "./mocks/guild";
|
||||
import { Util } from "../src/util";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
let USERSJOINED = 0;
|
||||
let USERSKICKED = 0;
|
||||
let USERSBANNED = 0;
|
||||
let USERSUNBANNED = 0;
|
||||
let ROOMSUNBRIDGED = 0;
|
||||
let MESSAGESENT: any = {};
|
||||
let MARKED = -1;
|
||||
function createCH(opts: any = {}) {
|
||||
USERSJOINED = 0;
|
||||
USERSKICKED = 0;
|
||||
USERSBANNED = 0;
|
||||
USERSUNBANNED = 0;
|
||||
ROOMSUNBRIDGED = 0;
|
||||
MESSAGESENT = {};
|
||||
MARKED = -1;
|
||||
const bridge = {
|
||||
getIntent: () => {
|
||||
return {
|
||||
ban: async () => { USERSBANNED++; },
|
||||
getEvent: () => ({ content: { } }),
|
||||
join: () => { USERSJOINED++; },
|
||||
kick: async () => { USERSKICKED++; },
|
||||
leave: () => { },
|
||||
sendMessage: async (roomId, content) => { MESSAGESENT = content; return content; },
|
||||
unban: async () => { USERSUNBANNED++; },
|
||||
};
|
||||
},
|
||||
};
|
||||
const bridge = new AppserviceMock();
|
||||
const cs = {
|
||||
GetRoomIdsFromChannel: async (chan) => {
|
||||
return [`#${chan.id}:localhost`];
|
||||
|
@ -83,12 +61,12 @@ function createCH(opts: any = {}) {
|
|||
},
|
||||
},
|
||||
})).DiscordCommandHandler;
|
||||
return new discordCommandHndlr(bridge as any, discord as any);
|
||||
return {handler: new discordCommandHndlr(bridge as any, discord as any), bridge};
|
||||
}
|
||||
|
||||
describe("DiscordCommandHandler", () => {
|
||||
it("will kick a member", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
@ -102,10 +80,10 @@ describe("DiscordCommandHandler", () => {
|
|||
member,
|
||||
};
|
||||
await handler.Process(message);
|
||||
expect(USERSKICKED).equals(1);
|
||||
bridge.botIntent.underlyingClient.wasCalled("kickUser", true, "#123:localhost", "@123456:localhost");
|
||||
});
|
||||
it("will kick a member in all guild rooms", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel, (new MockChannel("456"))]);
|
||||
channel.guild = guild;
|
||||
|
@ -120,10 +98,10 @@ describe("DiscordCommandHandler", () => {
|
|||
};
|
||||
await handler.Process(message);
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
expect(USERSKICKED).equals(2);
|
||||
expect(bridge.botIntent.underlyingClient.wasCalled("kickUser")).to.equal(2);
|
||||
});
|
||||
it("will deny permission", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
@ -137,10 +115,10 @@ describe("DiscordCommandHandler", () => {
|
|||
member,
|
||||
};
|
||||
await handler.Process(message);
|
||||
expect(USERSKICKED).equals(0);
|
||||
expect(bridge.botIntent.underlyingClient.wasCalled("kickUser", false)).to.equal(0);
|
||||
});
|
||||
it("will ban a member", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
@ -154,10 +132,10 @@ describe("DiscordCommandHandler", () => {
|
|||
member,
|
||||
};
|
||||
await handler.Process(message);
|
||||
expect(USERSBANNED).equals(1);
|
||||
expect(bridge.botIntent.underlyingClient.wasCalled("banUser")).to.equal(1);
|
||||
});
|
||||
it("will unban a member", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
@ -171,10 +149,10 @@ describe("DiscordCommandHandler", () => {
|
|||
member,
|
||||
};
|
||||
await handler.Process(message);
|
||||
expect(USERSUNBANNED).equals(1);
|
||||
expect(bridge.botIntent.underlyingClient.wasCalled("unbanUser")).to.equal(1);
|
||||
});
|
||||
it("handles !matrix approve", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
@ -191,7 +169,7 @@ describe("DiscordCommandHandler", () => {
|
|||
expect(MARKED).equals(1);
|
||||
});
|
||||
it("handles !matrix deny", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
@ -208,7 +186,7 @@ describe("DiscordCommandHandler", () => {
|
|||
expect(MARKED).equals(0);
|
||||
});
|
||||
it("handles !matrix unbridge", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler} = createCH();
|
||||
const channel = new MockChannel("123");
|
||||
const guild = new MockGuild("456", [channel]);
|
||||
channel.guild = guild;
|
||||
|
|
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import * as Discord from "discord.js";
|
||||
import { DiscordMessageProcessor, DiscordMessageProcessorOpts } from "../src/discordmessageprocessor";
|
||||
import * as Chai from "chai"; // TODO: Use expect
|
||||
import * as Discord from "better-discord.js";
|
||||
import { DiscordMessageProcessor } from "../src/discordmessageprocessor";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { MockGuild } from "./mocks/guild";
|
||||
import { MockMember } from "./mocks/member";
|
||||
import { MockMessage } from "./mocks/message";
|
||||
import { MockRole } from "./mocks/role";
|
||||
import { MockTextChannel } from "./mocks/channel";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
@ -45,15 +45,10 @@ const bot = {
|
|||
};
|
||||
|
||||
describe("DiscordMessageProcessor", () => {
|
||||
describe("init", () => {
|
||||
it("constructor", () => {
|
||||
const mp = new DiscordMessageProcessor(new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
});
|
||||
});
|
||||
describe("FormatMessage", () => {
|
||||
describe("FormatMessage", async () => {
|
||||
it("processes plain text messages correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "Hello World!";
|
||||
|
@ -63,7 +58,7 @@ describe("DiscordMessageProcessor", () => {
|
|||
});
|
||||
it("processes markdown messages correctly.", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "Hello *World*!";
|
||||
|
@ -73,13 +68,13 @@ describe("DiscordMessageProcessor", () => {
|
|||
});
|
||||
it("processes non-discord markdown correctly.", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "> inb4 tests";
|
||||
msg.content = ">inb4 tests";
|
||||
let result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "> inb4 tests");
|
||||
Chai.assert.equal(result.formattedBody, "> inb4 tests");
|
||||
Chai.assert.equal(result.body, ">inb4 tests");
|
||||
Chai.assert.equal(result.formattedBody, ">inb4 tests");
|
||||
|
||||
msg.embeds = [];
|
||||
msg.content = "[test](http://example.com)";
|
||||
|
@ -90,7 +85,7 @@ describe("DiscordMessageProcessor", () => {
|
|||
});
|
||||
it("processes discord-specific markdown correctly.", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "_ italic _";
|
||||
|
@ -100,7 +95,7 @@ describe("DiscordMessageProcessor", () => {
|
|||
});
|
||||
it("replaces @everyone correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "hey @everyone!";
|
||||
|
@ -115,7 +110,7 @@ describe("DiscordMessageProcessor", () => {
|
|||
});
|
||||
it("replaces @here correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "hey @here!";
|
||||
|
@ -129,610 +124,117 @@ describe("DiscordMessageProcessor", () => {
|
|||
Chai.assert.equal(result.formattedBody, "hey @room!");
|
||||
});
|
||||
});
|
||||
describe("FormatEmbeds", () => {
|
||||
it("should format embeds correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
{
|
||||
author: {} as any,
|
||||
client: {} as any,
|
||||
color: {} as any,
|
||||
createdAt: {} as any,
|
||||
createdTimestamp: {} as any,
|
||||
description: "Description",
|
||||
fields: [] as any,
|
||||
footer: undefined as any,
|
||||
hexColor: {} as any,
|
||||
image: undefined as any,
|
||||
message: {} as any,
|
||||
provider: {} as any,
|
||||
thumbnail: {} as any,
|
||||
title: "Title",
|
||||
type: {} as any,
|
||||
url: "http://example.com",
|
||||
video: {} as any,
|
||||
},
|
||||
];
|
||||
msg.content = "message";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "message\n\n----\n##### [Title](http://example.com)\nDescription");
|
||||
Chai.assert.equal(result.formattedBody, "message<hr><h5><a href=\"http://example.com\">Title</a>" +
|
||||
"</h5><p>Description</p>");
|
||||
});
|
||||
it("should ignore same-url embeds", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
{
|
||||
author: {} as any,
|
||||
client: {} as any,
|
||||
color: {} as any,
|
||||
createdAt: {} as any,
|
||||
createdTimestamp: {} as any,
|
||||
description: "Description",
|
||||
fields: [] as any,
|
||||
footer: {} as any,
|
||||
hexColor: {} as any,
|
||||
image: {} as any,
|
||||
message: {} as any,
|
||||
provider: {} as any,
|
||||
thumbnail: {} as any,
|
||||
title: "Title",
|
||||
type: {} as any,
|
||||
url: "http://example.com",
|
||||
video: {} as any,
|
||||
},
|
||||
];
|
||||
msg.content = "message http://example.com";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "message http://example.com");
|
||||
Chai.assert.equal(result.formattedBody, "message <a href=\"http://example.com\">" +
|
||||
"http://example.com</a>");
|
||||
});
|
||||
it("should ignore same-url embeds with trailing slash", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
{
|
||||
author: {} as any,
|
||||
client: {} as any,
|
||||
color: {} as any,
|
||||
createdAt: {} as any,
|
||||
createdTimestamp: {} as any,
|
||||
description: "Description",
|
||||
fields: [] as any,
|
||||
footer: {} as any,
|
||||
hexColor: {} as any,
|
||||
image: {} as any,
|
||||
message: {} as any,
|
||||
provider: {} as any,
|
||||
thumbnail: {} as any,
|
||||
title: "Title",
|
||||
type: {} as any,
|
||||
url: "http://example.com/",
|
||||
video: {} as any,
|
||||
},
|
||||
];
|
||||
msg.content = "message http://example.com";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "message http://example.com");
|
||||
Chai.assert.equal(result.formattedBody, "message <a href=\"http://example.com\">" +
|
||||
"http://example.com</a>");
|
||||
});
|
||||
});
|
||||
describe("FormatEdit", () => {
|
||||
it("should format basic edits appropriately", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const oldMsg = new MockMessage() as any;
|
||||
const newMsg = new MockMessage() as any;
|
||||
oldMsg.embeds = [];
|
||||
newMsg.embeds = [];
|
||||
|
||||
// Content updated but not changed
|
||||
oldMsg.content = "a";
|
||||
newMsg.content = "b";
|
||||
|
||||
const result = await processor.FormatEdit(oldMsg, newMsg);
|
||||
Chai.assert.equal(result.body, "*edit:* ~~a~~ -> b");
|
||||
Chai.assert.equal(result.formattedBody, "<em>edit:</em> <del>a</del> -> b");
|
||||
});
|
||||
it("should format markdown heavy edits apropriately", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const oldMsg = new MockMessage() as any;
|
||||
const newMsg = new MockMessage() as any;
|
||||
oldMsg.embeds = [];
|
||||
newMsg.embeds = [];
|
||||
|
||||
// Content updated but not changed
|
||||
oldMsg.content = "a slice of **cake**";
|
||||
newMsg.content = "*a* slice of cake";
|
||||
|
||||
const result = await processor.FormatEdit(oldMsg, newMsg);
|
||||
Chai.assert.equal(result.body, "*edit:* ~~a slice of **cake**~~ -> *a* slice of cake");
|
||||
Chai.assert.equal(result.formattedBody, "<em>edit:</em> <del>a slice of <strong>" +
|
||||
"cake</strong></del> -> <em>a</em> slice of cake");
|
||||
});
|
||||
it("should format discord fail edits correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const oldMsg = new MockMessage() as any;
|
||||
const newMsg = new MockMessage() as any;
|
||||
oldMsg.embeds = [];
|
||||
newMsg.embeds = [];
|
||||
|
||||
// Content updated but not changed
|
||||
oldMsg.content = "~~fail~";
|
||||
newMsg.content = "~~fail~~";
|
||||
|
||||
const result = await processor.FormatEdit(oldMsg, newMsg);
|
||||
Chai.assert.equal(result.body, "*edit:* ~~~~fail~~~ -> ~~fail~~");
|
||||
Chai.assert.equal(result.formattedBody, "<em>edit:</em> <del>~~fail~</del> -> <del>fail</del>");
|
||||
});
|
||||
it("should format multiline edits correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const oldMsg = new MockMessage() as any;
|
||||
const newMsg = new MockMessage() as any;
|
||||
oldMsg.embeds = [];
|
||||
newMsg.embeds = [];
|
||||
|
||||
// Content updated but not changed
|
||||
oldMsg.content = "multi\nline";
|
||||
newMsg.content = "multi\nline\nfoxies";
|
||||
|
||||
const result = await processor.FormatEdit(oldMsg, newMsg);
|
||||
Chai.assert.equal(result.body, "*edit:* ~~multi\nline~~ -> multi\nline\nfoxies");
|
||||
Chai.assert.equal(result.formattedBody, "<p><em>edit:</em></p><p><del>multi<br>line</del></p><hr>" +
|
||||
"<p>multi<br>line<br>foxies</p>");
|
||||
});
|
||||
it("should add old message link", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const oldMsg = new MockMessage() as any;
|
||||
const newMsg = new MockMessage() as any;
|
||||
oldMsg.embeds = [];
|
||||
newMsg.embeds = [];
|
||||
|
||||
// Content updated but not changed
|
||||
oldMsg.content = "fox";
|
||||
newMsg.content = "foxies";
|
||||
|
||||
const result = await processor.FormatEdit(oldMsg, newMsg, "https://matrix.to/#/old");
|
||||
Chai.assert.equal(result.body, "*edit:* ~~fox~~ -> foxies");
|
||||
Chai.assert.equal(result.formattedBody, "<a href=\"https://matrix.to/#/old\"><em>edit:</em></a>" +
|
||||
" <del>fox</del> -> foxies");
|
||||
});
|
||||
});
|
||||
|
||||
describe("InsertUser / HTML", () => {
|
||||
it("processes members missing from the guild correctly", () => {
|
||||
it("processes members missing from the guild correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {});
|
||||
const channel = new MockTextChannel(guild);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = { id: "12345" };
|
||||
let reply = processor.InsertUser(content, msg);
|
||||
Chai.assert.equal(reply, "@_discord_12345:localhost");
|
||||
|
||||
reply = processor.InsertUser(content, msg, true);
|
||||
Chai.assert.equal(reply,
|
||||
"<a href=\"https://matrix.to/#/@_discord_12345:localhost\">@_discord_12345:localhost</a>");
|
||||
msg.embeds = [];
|
||||
msg.content = "<@12345>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "@_discord_12345:localhost");
|
||||
Chai.assert.equal(result.formattedBody, "<a href=\"https://matrix.to/#/@_discord_12345:l" +
|
||||
"ocalhost\">@_discord_12345:localhost</a>");
|
||||
});
|
||||
it("processes members with usernames correctly", () => {
|
||||
it("processes members with usernames correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
guild._mockAddMember(new MockMember("12345", "TestUsername"));
|
||||
const channel = new Discord.TextChannel(guild, {});
|
||||
const channel = new MockTextChannel(guild);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = { id: "12345" };
|
||||
let reply = processor.InsertUser(content, msg);
|
||||
Chai.assert.equal(reply, "TestUsername");
|
||||
|
||||
reply = processor.InsertUser(content, msg, true);
|
||||
Chai.assert.equal(reply,
|
||||
"<a href=\"https://matrix.to/#/@_discord_12345:localhost\">TestUsername</a>");
|
||||
msg.embeds = [];
|
||||
msg.content = "<@12345>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "TestUsername");
|
||||
Chai.assert.equal(result.formattedBody, "<a href=\"https://matrix.to/#/@_discord_123" +
|
||||
"45:localhost\">TestUsername</a>");
|
||||
});
|
||||
it("processes members with nickname correctly", () => {
|
||||
it("processes members with nickname correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
guild._mockAddMember(new MockMember("12345", "TestUsername", null, "TestNickname"));
|
||||
const channel = new Discord.TextChannel(guild, {});
|
||||
const channel = new MockTextChannel(guild);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = { id: "12345" };
|
||||
let reply = processor.InsertUser(content, msg);
|
||||
Chai.assert.equal(reply, "TestNickname");
|
||||
|
||||
reply = processor.InsertUser(content, msg, true);
|
||||
Chai.assert.equal(reply,
|
||||
"<a href=\"https://matrix.to/#/@_discord_12345:localhost\">TestNickname</a>");
|
||||
});
|
||||
});
|
||||
describe("InsertRole / HTML", () => {
|
||||
it("ignores unknown roles", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const role = new MockRole("5678", "role");
|
||||
guild.roles.set("5678", role);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = { id: "1234" };
|
||||
let reply = processor.InsertRole(content, msg);
|
||||
Chai.assert.equal(reply, "<@&1234>");
|
||||
|
||||
reply = processor.InsertRole(content, msg, true);
|
||||
Chai.assert.equal(reply, "<@&1234>");
|
||||
});
|
||||
it("parses known roles", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const ROLE_COLOR = 0xDEAD88;
|
||||
const role = new MockRole("1234", "role", ROLE_COLOR);
|
||||
guild.roles.set("1234", role);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = { id: "1234" };
|
||||
let reply = processor.InsertRole(content, msg);
|
||||
Chai.assert.equal(reply, "@role");
|
||||
|
||||
reply = processor.InsertRole(content, msg, true);
|
||||
Chai.assert.equal(reply, "<span data-mx-color=\"#dead88\"><strong>@role</strong></span>");
|
||||
});
|
||||
});
|
||||
describe("InsertEmoji", () => {
|
||||
it("inserts static emojis to their post-parse flag", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const content = {
|
||||
animated: false,
|
||||
id: "1234",
|
||||
name: "blah",
|
||||
};
|
||||
const reply = processor.InsertEmoji(content);
|
||||
Chai.assert.equal(reply, "\x01emoji\x01blah\x010\x011234\x01");
|
||||
});
|
||||
it("inserts animated emojis to their post-parse flag", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const content = {
|
||||
animated: true,
|
||||
id: "1234",
|
||||
name: "blah",
|
||||
};
|
||||
const reply = processor.InsertEmoji(content);
|
||||
Chai.assert.equal(reply, "\x01emoji\x01blah\x011\x011234\x01");
|
||||
});
|
||||
});
|
||||
describe("InsertChannel", () => {
|
||||
it("inserts channels to their post-parse flag", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const content = {
|
||||
id: "1234",
|
||||
};
|
||||
const reply = processor.InsertChannel(content);
|
||||
Chai.assert.equal(reply, "\x01chan\x011234\x01");
|
||||
msg.embeds = [];
|
||||
msg.content = "<@12345>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "TestNickname");
|
||||
Chai.assert.equal(result.formattedBody, "<a href=\"https://matrix.to/#/@_disc" +
|
||||
"ord_12345:localhost\">TestNickname</a>");
|
||||
});
|
||||
});
|
||||
describe("InsertMxcImages / HTML", () => {
|
||||
it("processes unknown emoji correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01emoji\x01hello\x010\x01123456789\x01";
|
||||
let reply = await processor.InsertMxcImages(content, msg);
|
||||
Chai.assert.equal(reply, "Hello <:hello:123456789>");
|
||||
|
||||
reply = await processor.InsertMxcImages(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <:hello:123456789>");
|
||||
msg.embeds = [];
|
||||
msg.content = "Hello <:hello:123456789>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "Hello <:hello:123456789>");
|
||||
Chai.assert.equal(result.formattedBody, "Hello <:hello:123456789>");
|
||||
});
|
||||
it("processes emoji correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.cache.set("456", channel);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01emoji\x01hello\x010\x013333333\x01";
|
||||
let reply = await processor.InsertMxcImages(content, msg);
|
||||
Chai.assert.equal(reply, "Hello :hello:");
|
||||
|
||||
reply = await processor.InsertMxcImages(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <img alt=\"hello\" title=\"hello\" height=\"32\" src=\"mxc://image\" />");
|
||||
});
|
||||
it("processes double-emoji correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01emoji\x01hello\x010\x013333333\x01 \x01emoji\x01hello\x010\x013333333\x01";
|
||||
let reply = await processor.InsertMxcImages(content, msg);
|
||||
Chai.assert.equal(reply, "Hello :hello: :hello:");
|
||||
|
||||
reply = await processor.InsertMxcImages(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <img alt=\"hello\" title=\"hello\" height=\"32\" src=\"mxc://image\" /> " +
|
||||
"<img alt=\"hello\" title=\"hello\" height=\"32\" src=\"mxc://image\" />");
|
||||
msg.embeds = [];
|
||||
msg.content = "Hello <:hello:3333333>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "Hello :hello:");
|
||||
Chai.assert.equal(result.formattedBody, "Hello <img alt=\":hello:\" ti" +
|
||||
"tle=\":hello:\" height=\"32\" src=\"mxc://image\" data-mx-emoticon />");
|
||||
});
|
||||
});
|
||||
describe("InsertChannelPills / HTML", () => {
|
||||
it("processes unknown channel correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.cache.set("456", channel);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01chan\x013333333\x01";
|
||||
let reply = await processor.InsertChannelPills(content, msg);
|
||||
Chai.assert.equal(reply, "Hello <#3333333>");
|
||||
|
||||
reply = await processor.InsertChannelPills(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <#3333333>");
|
||||
msg.embeds = [];
|
||||
msg.content = "Hello <#3333333>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "Hello <#3333333>");
|
||||
Chai.assert.equal(result.formattedBody, "Hello <#3333333>");
|
||||
});
|
||||
it("processes channels correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.cache.set("456", channel);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01chan\x01456\x01";
|
||||
let reply = await processor.InsertChannelPills(content, msg);
|
||||
Chai.assert.equal(reply, "Hello #TestChannel");
|
||||
|
||||
reply = await processor.InsertChannelPills(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <a href=\"https://matrix.to/#/#_discord_123" +
|
||||
"_456:localhost\">#TestChannel</a>");
|
||||
});
|
||||
it("processes multiple channels correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
|
||||
guild.channels.set("456", channel);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01chan\x01456\x01 \x01chan\x01456\x01";
|
||||
let reply = await processor.InsertChannelPills(content, msg);
|
||||
Chai.assert.equal(reply, "Hello #TestChannel #TestChannel");
|
||||
|
||||
reply = await processor.InsertChannelPills(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <a href=\"https://matrix.to/#/#_discord_123" +
|
||||
"_456:localhost\">#TestChannel</a> <a href=\"https://matrix.to/#/#_discord_123" +
|
||||
msg.embeds = [];
|
||||
msg.content = "Hello <#456>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.body, "Hello #TestChannel");
|
||||
Chai.assert.equal(result.formattedBody, "Hello <a href=\"https://matrix.to/#/#_discord_123" +
|
||||
"_456:localhost\">#TestChannel</a>");
|
||||
});
|
||||
it("processes channels without alias correctly", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
"localhost", bot as DiscordBot);
|
||||
const guild: any = new MockGuild("123", []);
|
||||
const channel = new Discord.TextChannel(guild, {id: "678", name: "TestChannel"});
|
||||
guild.channels.set("678", channel);
|
||||
const channel = new MockTextChannel(guild, {id: "678", name: "TestChannel"});
|
||||
guild.channels.cache.set("678", channel);
|
||||
const msg = new MockMessage(channel) as any;
|
||||
const content = "Hello \x01chan\x01678\x01";
|
||||
let reply = await processor.InsertChannelPills(content, msg);
|
||||
Chai.assert.equal(reply, "Hello <#678>");
|
||||
|
||||
reply = await processor.InsertChannelPills(content, msg, true);
|
||||
Chai.assert.equal(reply, "Hello <#678>");
|
||||
});
|
||||
});
|
||||
describe("InsertEmbeds", () => {
|
||||
it("processes titleless embeds properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
}),
|
||||
];
|
||||
const inContent = "";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(content, "\n\n----\nTestDescription");
|
||||
});
|
||||
it("processes urlless embeds properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
}),
|
||||
];
|
||||
const inContent = "";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(content, "\n\n----\n##### TestTitle\nTestDescription");
|
||||
});
|
||||
it("processes linked embeds properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
url: "testurl",
|
||||
}),
|
||||
];
|
||||
const inContent = "";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(content, "\n\n----\n##### [TestTitle](testurl)\nTestDescription");
|
||||
});
|
||||
it("rejects titleless and descriptionless embeds", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
url: "testurl",
|
||||
}),
|
||||
];
|
||||
const inContent = "Some content...";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(content, "Some content...");
|
||||
});
|
||||
it("processes multiple embeds properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
url: "testurl",
|
||||
}),
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription2",
|
||||
title: "TestTitle2",
|
||||
url: "testurl2",
|
||||
}),
|
||||
];
|
||||
const inContent = "";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(
|
||||
content,
|
||||
"\n\n----\n##### [TestTitle](testurl)\nTestDescription\n\n----\n##### [TestTitle2](testurl2)\nTestDescription2",
|
||||
);
|
||||
});
|
||||
it("inserts embeds properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
url: "testurl",
|
||||
}),
|
||||
];
|
||||
const inContent = "Content that goes in the message";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(
|
||||
content,
|
||||
`Content that goes in the message
|
||||
|
||||
----
|
||||
##### [TestTitle](testurl)
|
||||
TestDescription`,
|
||||
);
|
||||
});
|
||||
it("adds fields properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
url: "testurl",
|
||||
}),
|
||||
];
|
||||
msg.embeds[0].fields = [
|
||||
{
|
||||
embed: msg.embeds[0],
|
||||
inline: false,
|
||||
name: "fox",
|
||||
value: "floof",
|
||||
},
|
||||
] as any;
|
||||
const inContent = "Content that goes in the message";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(
|
||||
content,
|
||||
`Content that goes in the message
|
||||
|
||||
----
|
||||
##### [TestTitle](testurl)
|
||||
TestDescription
|
||||
**fox**
|
||||
floof`,
|
||||
);
|
||||
});
|
||||
it("adds images properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
url: "testurl",
|
||||
}),
|
||||
];
|
||||
msg.embeds[0].image = { url: "http://example.com" } as any;
|
||||
const inContent = "Content that goes in the message";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(
|
||||
content,
|
||||
`Content that goes in the message
|
||||
|
||||
----
|
||||
##### [TestTitle](testurl)
|
||||
TestDescription
|
||||
Image: http://example.com`,
|
||||
);
|
||||
});
|
||||
it("adds a footer properly", () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [
|
||||
new Discord.MessageEmbed(msg, {
|
||||
description: "TestDescription",
|
||||
title: "TestTitle",
|
||||
url: "testurl",
|
||||
}),
|
||||
];
|
||||
msg.embeds[0].footer = { text: "footer" } as any;
|
||||
const inContent = "Content that goes in the message";
|
||||
const content = processor.InsertEmbeds(inContent, msg);
|
||||
Chai.assert.equal(
|
||||
content,
|
||||
`Content that goes in the message
|
||||
|
||||
----
|
||||
##### [TestTitle](testurl)
|
||||
TestDescription
|
||||
footer`,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("Message Type", () => {
|
||||
it("sets non-bot messages as m.text", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "no bot";
|
||||
msg.author.bot = false;
|
||||
msg.content = "Hello <#678>";
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.msgtype, "m.text");
|
||||
});
|
||||
it("sets bot messages as m.notice", async () => {
|
||||
const processor = new DiscordMessageProcessor(
|
||||
new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
|
||||
const msg = new MockMessage() as any;
|
||||
msg.embeds = [];
|
||||
msg.content = "a bot";
|
||||
msg.author.bot = true;
|
||||
const result = await processor.FormatMessage(msg);
|
||||
Chai.assert.equal(result.msgtype, "m.notice");
|
||||
Chai.assert.equal(result.body, "Hello <#678>");
|
||||
Chai.assert.equal(result.formattedBody, "Hello <#678>");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,25 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
import * as RealLog from "../src/log";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
let createdLogger: any = null;
|
||||
let loggerClosed: any = false;
|
||||
let loggedMessages: any[] = [];
|
||||
|
||||
const WinstonMock = {
|
||||
createLogger: (format, transports) => {
|
||||
return createdLogger = {
|
||||
close: () => {
|
||||
loggerClosed = true;
|
||||
},
|
||||
close: () => { },
|
||||
format,
|
||||
log: (type, ...msg) => {
|
||||
loggedMessages = loggedMessages.concat(msg);
|
||||
|
@ -50,7 +44,6 @@ const Log = (Proxyquire("../src/log", {
|
|||
describe("Log", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
loggerClosed = false;
|
||||
loggedMessages = [];
|
||||
});
|
||||
|
||||
|
|
|
@ -14,52 +14,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import { Util } from "../src/util";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
function createCH(opts: any = {}, shouldBeJoined = true) {
|
||||
|
||||
let USERSJOINED = 0;
|
||||
let USERSKICKED = 0;
|
||||
let USERSBANNED = 0;
|
||||
let USERSUNBANNED = 0;
|
||||
let MESSAGESENT: any = {};
|
||||
|
||||
function createCH(opts: any = {}) {
|
||||
USERSJOINED = 0;
|
||||
USERSKICKED = 0;
|
||||
USERSBANNED = 0;
|
||||
USERSUNBANNED = 0;
|
||||
MESSAGESENT = {};
|
||||
|
||||
const bridge = {
|
||||
getBot: () => {
|
||||
return {
|
||||
getJoinedRooms: () => ["!123:localhost"],
|
||||
isRemoteUser: (id) => {
|
||||
return id !== undefined && id.startsWith("@_discord_");
|
||||
},
|
||||
};
|
||||
},
|
||||
getIntent: () => {
|
||||
return {
|
||||
ban: async () => { USERSBANNED++; },
|
||||
getClient: () => mxClient,
|
||||
join: () => { USERSJOINED++; },
|
||||
joinRoom: async () => { USERSJOINED++; },
|
||||
kick: async () => { USERSKICKED++; },
|
||||
leave: () => { },
|
||||
sendMessage: async (roomId, content) => { MESSAGESENT = content; return content; },
|
||||
unban: async () => { USERSUNBANNED++; },
|
||||
};
|
||||
},
|
||||
};
|
||||
const bridge = new AppserviceMock({
|
||||
joinedrooms: shouldBeJoined ? ["!123:localhost"] : [],
|
||||
});
|
||||
|
||||
const config = new DiscordBridgeConfig();
|
||||
config.limits.roomGhostJoinDelay = 0;
|
||||
|
@ -68,14 +37,6 @@ function createCH(opts: any = {}) {
|
|||
} else {
|
||||
config.bridge.enableSelfServiceBridging = true;
|
||||
}
|
||||
const mxClient = {
|
||||
getUserId: () => "@user:localhost",
|
||||
joinRoom: async () => {
|
||||
USERSJOINED++;
|
||||
},
|
||||
sendReadReceipt: async () => { },
|
||||
setRoomDirectoryVisibilityAppService: async () => { },
|
||||
};
|
||||
const provisioner = {
|
||||
AskBridgePermission: async () => {
|
||||
if (opts.denyBridgePermission) {
|
||||
|
@ -118,7 +79,7 @@ function createCH(opts: any = {}) {
|
|||
},
|
||||
},
|
||||
})).MatrixCommandHandler;
|
||||
return new MatrixCommandHndl(bot as any, bridge, config);
|
||||
return {handler: new MatrixCommandHndl(bot as any, bridge, config), bridge};
|
||||
}
|
||||
|
||||
function createEvent(msg: string, room?: string, userId?: string) {
|
||||
|
@ -133,75 +94,130 @@ function createEvent(msg: string, room?: string, userId?: string) {
|
|||
|
||||
function createContext(remoteData?: any) {
|
||||
return {
|
||||
rooms: {
|
||||
remote: remoteData,
|
||||
},
|
||||
remote: remoteData,
|
||||
};
|
||||
}
|
||||
|
||||
describe("MatrixCommandHandler", () => {
|
||||
describe("Process", () => {
|
||||
it("should not process command if not in room", async () => {
|
||||
const handler: any = createCH({disableSS: true});
|
||||
const {handler, bridge} = createCH({}, false);
|
||||
await handler.Process(createEvent("", "!666:localhost"), createContext());
|
||||
expect(MESSAGESENT.body).to.equal(undefined);
|
||||
bridge.botIntent.wasNotCalled("sendText", true);
|
||||
bridge.botIntent.wasNotCalled("sendMessage", true);
|
||||
});
|
||||
it("should warn if self service is disabled", async () => {
|
||||
const handler: any = createCH({disableSS: true});
|
||||
const {handler, bridge} = createCH({disableSS: true});
|
||||
await handler.Process(createEvent("!discord bridge"), createContext());
|
||||
expect(MESSAGESENT.body).to.equal("**ERROR:** The owner of this bridge does " +
|
||||
"not permit self-service bridging.");
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: "**ERROR:** The owner of this bridge does not permit self-service bridging.",
|
||||
format: "org.matrix.custom.html",
|
||||
// tslint:disable prefer-template
|
||||
formatted_body: `<p><strong>ERROR:</strong> The owner of this bridge` +
|
||||
` does not permit self-service bridging.</p>\n`,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("should warn if user is not powerful enough", async () => {
|
||||
const handler: any = createCH({
|
||||
power: false,
|
||||
});
|
||||
const {handler, bridge} = createCH({power: false});
|
||||
await handler.Process(createEvent("!discord bridge"), createContext());
|
||||
expect(MESSAGESENT.body).to.equal("**ERROR:** insufficiant permissions to use this " +
|
||||
"command! Try `!discord help` to see all available commands");
|
||||
const expected = "**ERROR:** insufficient permissions to use this " +
|
||||
"command! Try `!discord help` to see all available commands";
|
||||
// tslint:disable prefer-template
|
||||
const htmlExpected = `<p><strong>ERROR:</strong> insufficient permissions to use this command!` +
|
||||
` Try <code>!discord help</code> to see all available commands</p>\n`;
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: htmlExpected,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
describe("!discord bridge", () => {
|
||||
it("will bridge a new room, and ask for permissions", async () => {
|
||||
const handler: any = createCH();
|
||||
const {handler, bridge} = createCH();
|
||||
await handler.Process(createEvent("!discord bridge 123 456"), createContext());
|
||||
expect(MESSAGESENT.body).to.equal("I have bridged this room to your channel");
|
||||
const expected = "I have bridged this room to your channel";
|
||||
const expectedHtml = "<p>I have bridged this room to your channel</p>\n";
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will fail to bridge if permissions were denied", async () => {
|
||||
const handler: any = createCH({
|
||||
const {handler, bridge} = createCH({
|
||||
denyBridgePermission: true,
|
||||
});
|
||||
await handler.Process(createEvent("!discord bridge 123 456"), createContext());
|
||||
expect(MESSAGESENT.body).to.equal("The bridge has been declined by the Discord guild");
|
||||
const expected = "The bridge has been declined by the Discord guild";
|
||||
const expectedHtml = "<p>The bridge has been declined by the Discord guild</p>\n";
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will fail to bridge if permissions were failed", async () => {
|
||||
const handler: any = createCH({
|
||||
const {handler, bridge} = createCH({
|
||||
failBridgeMatrix: true,
|
||||
});
|
||||
const evt = await handler.Process(createEvent("!discord bridge 123 456"), createContext());
|
||||
expect(MESSAGESENT.body).to.equal("There was a problem bridging that channel - has " +
|
||||
"the guild owner approved the bridge?");
|
||||
await handler.Process(createEvent("!discord bridge 123 456"), createContext());
|
||||
const expected = "There was a problem bridging that channel - has the guild owner approved the bridge?";
|
||||
const expectedHtml = "<p>There was a problem bridging that channel - has the guild owner approved the bridge?</p>\n";
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will not bridge if a link already exists", async () => {
|
||||
const handler: any = createCH();
|
||||
const evt = await handler.Process(createEvent("!discord bridge 123 456"), createContext(true));
|
||||
expect(MESSAGESENT.body).to.equal("This room is already bridged to a Discord guild.");
|
||||
const {handler, bridge} = createCH();
|
||||
await handler.Process(createEvent("!discord bridge 123 456"), createContext(true));
|
||||
const expected = "This room is already bridged to a Discord guild.";
|
||||
const expectedHtml = "<p>This room is already bridged to a Discord guild.</p>\n";
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will not bridge without required args", async () => {
|
||||
const handler: any = createCH();
|
||||
const evt = await handler.Process(createEvent("!discord bridge"), createContext());
|
||||
expect(MESSAGESENT.body).to.contain("Invalid syntax");
|
||||
const {handler, bridge} = createCH();
|
||||
await handler.Process(createEvent("!discord bridge"), createContext());
|
||||
const expected = "Invalid syntax. For more information try `!discord help bridge`";
|
||||
const expectedHtml = "<p>Invalid syntax. For more information try <code>!discord help bridge</code></p>\n";
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will bridge with x/y syntax", async () => {
|
||||
const handler: any = createCH({powerLevels: {
|
||||
const {handler, bridge} = createCH({powerLevels: {
|
||||
users_default: 100,
|
||||
}});
|
||||
const evt = await handler.Process(createEvent("!discord bridge 123/456"), createContext());
|
||||
expect(MESSAGESENT.body).equals("I have bridged this room to your channel");
|
||||
await handler.Process(createEvent("!discord bridge 123/456"), createContext());
|
||||
const expected = "I have bridged this room to your channel";
|
||||
const expectedHtml = "<p>I have bridged this room to your channel</p>\n";
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("!discord unbridge", () => {
|
||||
it("will unbridge", async () => {
|
||||
const handler: any = createCH();
|
||||
const expected = "This room has been unbridged";
|
||||
const expectedHtml = "<p>This room has been unbridged</p>\n";
|
||||
const {handler, bridge} = createCH();
|
||||
await handler.Process(createEvent("!discord unbridge"), createContext(
|
||||
{
|
||||
data: {
|
||||
|
@ -211,15 +227,29 @@ describe("MatrixCommandHandler", () => {
|
|||
},
|
||||
},
|
||||
));
|
||||
expect(MESSAGESENT.body).equals("This room has been unbridged");
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will not unbridge if a link does not exist", async () => {
|
||||
const handler: any = createCH();
|
||||
const expected = "This room is not bridged.";
|
||||
const expectedHtml = "<p>This room is not bridged.</p>\n";
|
||||
const {handler, bridge} = createCH();
|
||||
await handler.Process(createEvent("!discord unbridge"), createContext());
|
||||
expect(MESSAGESENT.body).equals("This room is not bridged.");
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will not unbridge non-plumbed rooms", async () => {
|
||||
const handler: any = createCH();
|
||||
const expected = "This room cannot be unbridged.";
|
||||
const expectedHtml = "<p>This room cannot be unbridged.</p>\n";
|
||||
const {handler, bridge} = createCH();
|
||||
await handler.Process(createEvent("!discord unbridge"), createContext(
|
||||
{
|
||||
data: {
|
||||
|
@ -229,10 +259,20 @@ describe("MatrixCommandHandler", () => {
|
|||
},
|
||||
},
|
||||
));
|
||||
expect(MESSAGESENT.body).equals("This room cannot be unbridged.");
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
it("will show error if unbridge fails", async () => {
|
||||
const handler: any = createCH({
|
||||
const expected = "There was an error unbridging this room. Please " +
|
||||
"try again later or contact the bridge operator.";
|
||||
// tslint:disable prefer-template
|
||||
const expectedHtml = `<p>There was an error unbridging this room. Please` +
|
||||
` try again later or contact the bridge operator.</p>\n`;
|
||||
const {handler, bridge} = createCH({
|
||||
failUnbridge: true,
|
||||
});
|
||||
await handler.Process(createEvent("!discord unbridge"), createContext(
|
||||
|
@ -244,24 +284,27 @@ describe("MatrixCommandHandler", () => {
|
|||
},
|
||||
},
|
||||
));
|
||||
expect(MESSAGESENT.body).to.contain("There was an error unbridging this room.");
|
||||
bridge.botIntent.underlyingClient.wasCalled("sendMessage", true, "!123:localhost", {
|
||||
body: expected,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: expectedHtml,
|
||||
msgtype: "m.notice",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("HandleInvite", () => {
|
||||
it("should accept invite for bot user", async () => {
|
||||
const handler: any = createCH();
|
||||
let joinedRoom = false;
|
||||
const { handler, bridge } = createCH();
|
||||
handler.joinRoom = async () => {
|
||||
joinedRoom = true;
|
||||
};
|
||||
await handler.HandleInvite({
|
||||
state_key: "@botuser:localhost",
|
||||
});
|
||||
expect(USERSJOINED).to.equal(1);
|
||||
bridge.botIntent.wasCalled("joinRoom", true);
|
||||
});
|
||||
it("should deny invite for other users", async () => {
|
||||
const handler: any = createCH();
|
||||
const { handler, bridge } = createCH();
|
||||
let joinedRoom = false;
|
||||
handler.joinRoom = async () => {
|
||||
joinedRoom = true;
|
||||
|
@ -269,6 +312,7 @@ describe("MatrixCommandHandler", () => {
|
|||
await handler.HandleInvite({
|
||||
state_key: "@user:localhost",
|
||||
});
|
||||
bridge.getIntent("@user:localhost").wasNotCalled("joinRoom", true);
|
||||
expect(joinedRoom).to.be.false;
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,21 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import * as Discord from "discord.js";
|
||||
import { expect } from "chai";
|
||||
import { MockGuild } from "./mocks/guild";
|
||||
import { MockMember } from "./mocks/member";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { MockEmoji } from "./mocks/emoji";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { DbEmoji } from "../src/db/dbdataemoji";
|
||||
import { MatrixMessageProcessor } from "../src/matrixmessageprocessor";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
const bot = {
|
||||
GetChannelFromRoomId: async (roomId: string): Promise<MockChannel> => {
|
||||
if (roomId !== "!bridged:localhost") {
|
||||
|
@ -49,6 +45,12 @@ const bot = {
|
|||
},
|
||||
} as any;
|
||||
|
||||
const config = {
|
||||
bridge: {
|
||||
determineCodeLanguage: false,
|
||||
},
|
||||
} as any;
|
||||
|
||||
function getPlainMessage(msg: string, msgtype: string = "m.text") {
|
||||
return {
|
||||
body: msg,
|
||||
|
@ -67,28 +69,28 @@ function getHtmlMessage(msg: string, msgtype: string = "m.text") {
|
|||
describe("MatrixMessageProcessor", () => {
|
||||
describe("FormatMessage / body / simple", () => {
|
||||
it("leaves blank stuff untouched", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("hello world!");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("hello world!");
|
||||
});
|
||||
it("escapes simple stuff", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("hello *world* how __are__ you?");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("hello \\*world\\* how \\_\\_are\\_\\_ you?");
|
||||
});
|
||||
it("escapes more complex stuff", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("wow \\*this\\* is cool");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("wow \\\\\\*this\\\\\\* is cool");
|
||||
});
|
||||
it("escapes ALL the stuff", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("\\ * _ ~ ` |");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
|
@ -97,285 +99,185 @@ describe("MatrixMessageProcessor", () => {
|
|||
});
|
||||
describe("FormatMessage / formatted_body / simple", () => {
|
||||
it("leaves blank stuff untouched", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("hello world!");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("hello world!");
|
||||
});
|
||||
it("un-escapes simple stuff", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("foxes & foxes");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("foxes & foxes");
|
||||
});
|
||||
it("converts italic formatting", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("this text is <em>italic</em> and so is <i>this one</i>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("this text is *italic* and so is *this one*");
|
||||
});
|
||||
it("converts bold formatting", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("wow some <b>bold</b> and <strong>more</strong> boldness!");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("wow some **bold** and **more** boldness!");
|
||||
});
|
||||
it("converts underline formatting", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("to be <u>underlined</u> or not to be?");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("to be __underlined__ or not to be?");
|
||||
});
|
||||
it("converts strike formatting", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("does <del>this text</del> exist?");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("does ~~this text~~ exist?");
|
||||
});
|
||||
it("converts code", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("WOW this is <code>some awesome</code> code");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("WOW this is `some awesome` code");
|
||||
});
|
||||
it("converts multiline-code", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<p>here</p><pre><code>is\ncode\n</code></pre><p>yay</p>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("here```\nis\ncode\n```yay");
|
||||
});
|
||||
it("converts multiline language code", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<p>here</p>
|
||||
<pre><code class="language-js">is
|
||||
code
|
||||
</code></pre>
|
||||
<p>yay</p>`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("here```js\nis\ncode\n```yay");
|
||||
});
|
||||
it("handles linebreaks", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("line<br>break");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("line\nbreak");
|
||||
});
|
||||
it("handles <hr>", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("test<hr>foxes");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("test\n----------\nfoxes");
|
||||
});
|
||||
it("handles headings", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<h1>fox</h1>
|
||||
<h2>floof</h2>
|
||||
<h3>pony</h3>
|
||||
<h4>hooves</h4>
|
||||
<h5>tail</h5>
|
||||
<h6>foxies</h6>`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal(`**# fox**
|
||||
**## floof**
|
||||
**### pony**
|
||||
**#### hooves**
|
||||
**##### tail**
|
||||
**###### foxies**`);
|
||||
});
|
||||
});
|
||||
describe("FormatMessage / formatted_body / complex", () => {
|
||||
it("html unescapes stuff inside of code", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<code>is <em>italic</em>?</code>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("`is <em>italic</em>?`");
|
||||
});
|
||||
it("html unescapes inside of pre", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<pre><code>wow &</code></pre>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("```\nwow &```");
|
||||
});
|
||||
it("doesn't parse inside of code", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<code>*yay*</code>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("`*yay*`");
|
||||
});
|
||||
it("doesn't parse inside of pre", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<pre><code>*yay*</code></pre>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("```\n*yay*```");
|
||||
});
|
||||
it("parses new lines", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<em>test</em><br><strong>ing</strong>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("*test*\n**ing**");
|
||||
});
|
||||
it("drops mx-reply", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<mx-reply><blockquote>message</blockquote></mx-reply>test reply");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("test reply");
|
||||
});
|
||||
it("parses links", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"http://example.com\">link</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[link](http://example.com)");
|
||||
});
|
||||
it("parses links with same content", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"http://example.com\">http://example.com</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("http://example.com");
|
||||
});
|
||||
it("doesn't discord-escape links", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"http://example.com/_blah_/\">link</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[link](http://example.com/_blah_/)");
|
||||
});
|
||||
it("doesn't discord-escape links with same content", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"http://example.com/_blah_/\">http://example.com/_blah_/</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("http://example.com/_blah_/");
|
||||
});
|
||||
});
|
||||
describe("FormatMessage / formatted_body / discord", () => {
|
||||
it("Parses user pills", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const member = new MockMember("12345", "TestUsername", guild);
|
||||
guild.members.set("12345", member);
|
||||
guild.members.cache.set("12345", member);
|
||||
const msg = getHtmlMessage("<a href=\"https://matrix.to/#/@_discord_12345:localhost\">TestUsername</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("<@12345>");
|
||||
});
|
||||
it("Ignores invalid user pills", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const member = new MockMember("12345", "TestUsername", guild);
|
||||
guild.members.set("12345", member);
|
||||
guild.members.cache.set("12345", member);
|
||||
const msg = getHtmlMessage("<a href=\"https://matrix.to/#/@_discord_789:localhost\">TestUsername</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[TestUsername](https://matrix.to/#/@_discord_789:localhost)");
|
||||
});
|
||||
it("Parses channel pills", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const channel = new MockChannel("12345", guild, "text", "SomeChannel");
|
||||
guild.channels.set("12345", channel as any);
|
||||
guild.channels.cache.set("12345", channel as any);
|
||||
const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#_discord_1234_12345:" +
|
||||
"localhost\">#SomeChannel</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("<#12345>");
|
||||
});
|
||||
it("Handles invalid channel pills", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const channel = new MockChannel("12345", guild, "text", "SomeChannel");
|
||||
guild.channels.set("12345", channel as any);
|
||||
guild.channels.cache.set("12345", channel as any);
|
||||
const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#_discord_1234_789:localhost\">#SomeChannel</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[#SomeChannel](https://matrix.to/#/#_discord_1234_789:localhost)");
|
||||
});
|
||||
it("Handles external channel pills", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#matrix:matrix.org\">#SomeChannel</a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[#SomeChannel](https://matrix.to/#/#matrix:matrix.org)");
|
||||
});
|
||||
it("Handles external channel pills of rooms that are actually bridged", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#matrix:matrix.org\">#SomeChannel</a>");
|
||||
|
||||
const result = await mp.FormatMessage(msg, guild as any, {
|
||||
mxClient: {
|
||||
getRoomIdForAlias: async () => {
|
||||
return {
|
||||
room_id: "!bridged:localhost",
|
||||
};
|
||||
},
|
||||
lookupRoomAlias: async () => ({
|
||||
residentServers: [],
|
||||
roomId: "!bridged:localhost",
|
||||
}),
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
);
|
||||
expect(result).is.equal("<#1234>");
|
||||
});
|
||||
it("Ignores links without href", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a><em>yay?</em></a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("*yay?*");
|
||||
});
|
||||
it("Ignores links with non-matrix href", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<a href=\"http://example.com\"><em>yay?</em></a>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[*yay?*](http://example.com)");
|
||||
});
|
||||
it("Handles spoilers", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<span data-mx-spoiler>foxies</span>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("||foxies||");
|
||||
});
|
||||
it("Handles spoilers with reason", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<span data-mx-spoiler=\"floof\">foxies</span>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("(floof)||foxies||");
|
||||
});
|
||||
});
|
||||
describe("FormatMessage / formatted_body / emoji", () => {
|
||||
it("Inserts emoji by name", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const emoji = new MockEmoji("123456", "test_emoji");
|
||||
guild.emojis.set("123456", emoji);
|
||||
guild.emojis.cache.set("123456", emoji);
|
||||
const msg = getHtmlMessage("<img alt=\"test_emoji\">");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("<:test_emoji:123456>");
|
||||
expect(result).is.equal("test\\_emoji");
|
||||
});
|
||||
it("Inserts emojis by mxc url", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const emoji = new MockEmoji("123456", "test_emoji");
|
||||
guild.emojis.set("123456", emoji);
|
||||
guild.emojis.cache.set("123456", emoji);
|
||||
const msg = getHtmlMessage("<img src=\"mxc://real_emote:localhost\">");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("<:test_emoji:123456>");
|
||||
});
|
||||
it("parses unknown mxc urls", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const emoji = new MockEmoji("123456", "test_emoji");
|
||||
guild.emojis.set("123456", emoji);
|
||||
guild.emojis.cache.set("123456", emoji);
|
||||
const msg = getHtmlMessage("<img alt=\"yay\" src=\"mxc://unreal_emote:localhost\">");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("[yay](mxc://unreal_emote:localhost)");
|
||||
expect(result).is.equal("[yay mxc://unreal_emote:localhost ]");
|
||||
});
|
||||
it("ignores with no alt / title, too", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const emoji = new MockEmoji("123456", "test_emoji");
|
||||
guild.emojis.set("123456", emoji);
|
||||
guild.emojis.cache.set("123456", emoji);
|
||||
const msg = getHtmlMessage("<img>");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("");
|
||||
|
@ -392,7 +294,7 @@ code
|
|||
*/
|
||||
function getMxClient(roomNotificationLevel?: number) {
|
||||
return {
|
||||
getStateEvent: async (roomId, stateType, _) => {
|
||||
getRoomStateEvent: async (roomId, stateType, _) => {
|
||||
if (stateType === "m.room.power_levels") {
|
||||
return {
|
||||
// Only include notifications.room when
|
||||
|
@ -424,21 +326,21 @@ code
|
|||
const ROOM_NOTIFICATION_LEVEL = 50;
|
||||
|
||||
it("escapes @everyone", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("hey @everyone");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("hey @\u200Beveryone");
|
||||
});
|
||||
it("escapes @here", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("hey @here");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("hey @\u200Bhere");
|
||||
});
|
||||
it("converts @room to @here, if sufficient power", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("hey @room");
|
||||
let params = {
|
||||
|
@ -461,7 +363,7 @@ code
|
|||
expect(result).is.equal("hey @here");
|
||||
});
|
||||
it("ignores @room to @here conversion, if insufficient power", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("hey @room");
|
||||
let params = {
|
||||
|
@ -484,7 +386,7 @@ code
|
|||
expect(result).is.equal("hey @room");
|
||||
});
|
||||
it("handles /me for normal names", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("floofs", "m.emote");
|
||||
const params = {
|
||||
|
@ -494,7 +396,7 @@ code
|
|||
expect(result).is.equal("_fox floofs_");
|
||||
});
|
||||
it("handles /me for short names", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("floofs", "m.emote");
|
||||
const params = {
|
||||
|
@ -504,7 +406,7 @@ code
|
|||
expect(result).is.equal("_floofs_");
|
||||
});
|
||||
it("handles /me for long names", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("floofs", "m.emote");
|
||||
const params = {
|
||||
|
@ -514,7 +416,7 @@ code
|
|||
expect(result).is.equal("_floofs_");
|
||||
});
|
||||
it("discord escapes nicks in /me", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const mp = new MatrixMessageProcessor(bot, config);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getPlainMessage("floofs", "m.emote");
|
||||
const params = {
|
||||
|
@ -524,165 +426,4 @@ code
|
|||
expect(result).is.equal("_fox\\_floof floofs_");
|
||||
});
|
||||
});
|
||||
describe("FormatMessage / formatted_body / blockquotes", () => {
|
||||
it("parses single blockquotes", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<blockquote>hey</blockquote>there");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("> hey\n\nthere");
|
||||
});
|
||||
it("parses double blockquotes", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<blockquote><blockquote>hey</blockquote>you</blockquote>there");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("> > hey\n> \n> you\n\nthere");
|
||||
});
|
||||
it("parses blockquotes with <p>", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage("<blockquote>\n<p>spoky</p>\n</blockquote>\n<p>test</p>\n");
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("> spoky\n\ntest");
|
||||
});
|
||||
it("parses double blockquotes with <p>", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<blockquote>
|
||||
<blockquote>
|
||||
<p>spoky</p>
|
||||
</blockquote>
|
||||
<p>testing</p>
|
||||
</blockquote>
|
||||
<p>test</p>
|
||||
`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("> > spoky\n> \n> testing\n\ntest");
|
||||
});
|
||||
});
|
||||
describe("FormatMessage / formatted_body / lists", () => {
|
||||
it("parses simple unordered lists", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<p>soru</p>
|
||||
<ul>
|
||||
<li>test</li>
|
||||
<li>ing</li>
|
||||
</ul>
|
||||
<p>more</p>
|
||||
`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("soru\n● test\n● ing\n\nmore");
|
||||
});
|
||||
it("parses nested unordered lists", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<p>foxes</p>
|
||||
<ul>
|
||||
<li>awesome</li>
|
||||
<li>floofy
|
||||
<ul>
|
||||
<li>fur</li>
|
||||
<li>tail</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>yay!</p>
|
||||
`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("foxes\n● awesome\n● floofy\n ○ fur\n ○ tail\n\nyay!");
|
||||
});
|
||||
it("parses more nested unordered lists", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<p>foxes</p>
|
||||
<ul>
|
||||
<li>awesome</li>
|
||||
<li>floofy
|
||||
<ul>
|
||||
<li>fur</li>
|
||||
<li>tail</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>cute</li>
|
||||
</ul>
|
||||
<p>yay!</p>
|
||||
`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("foxes\n● awesome\n● floofy\n ○ fur\n ○ tail\n● cute\n\nyay!");
|
||||
});
|
||||
});
|
||||
it("parses simple ordered lists", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<p>oookay</p>
|
||||
<ol>
|
||||
<li>test</li>
|
||||
<li>test more</li>
|
||||
</ol>
|
||||
<p>ok?</p>
|
||||
`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("oookay\n1. test\n2. test more\n\nok?");
|
||||
});
|
||||
it("parses nested ordered lists", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<p>and now</p>
|
||||
<ol>
|
||||
<li>test</li>
|
||||
<li>test more
|
||||
<ol>
|
||||
<li>and more</li>
|
||||
<li>more?</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>done!</li>
|
||||
</ol>
|
||||
<p>ok?</p>
|
||||
`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("and now\n1. test\n2. test more\n 1. and more\n 2. more?\n3. done!\n\nok?");
|
||||
});
|
||||
it("parses ordered lists with different start", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<ol start="5">
|
||||
<li>test</li>
|
||||
<li>test more</li>
|
||||
</ol>`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("\n5. test\n6. test more");
|
||||
});
|
||||
it("parses ul in ol", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<ol>
|
||||
<li>test</li>
|
||||
<li>test more
|
||||
<ul>
|
||||
<li>asdf</li>
|
||||
<li>jklö</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("\n1. test\n2. test more\n ○ asdf\n ○ jklö");
|
||||
});
|
||||
it("parses ol in ul", async () => {
|
||||
const mp = new MatrixMessageProcessor(bot);
|
||||
const guild = new MockGuild("1234");
|
||||
const msg = getHtmlMessage(`<ul>
|
||||
<li>test</li>
|
||||
<li>test more
|
||||
<ol>
|
||||
<li>asdf</li>
|
||||
<li>jklö</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>`);
|
||||
const result = await mp.FormatMessage(msg, guild as any);
|
||||
expect(result).is.equal("\n● test\n● test more\n 1. asdf\n 2. jklö");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,19 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { MockMember } from "./mocks/member";
|
||||
import { MockGuild } from "./mocks/guild";
|
||||
import { Util } from "../src/util";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
const RoomHandler = (Proxyquire("../src/matrixroomhandler", {
|
||||
"./util": {
|
||||
Util: {
|
||||
|
@ -41,50 +40,15 @@ const RoomHandler = (Proxyquire("../src/matrixroomhandler", {
|
|||
})).MatrixRoomHandler;
|
||||
|
||||
let USERSJOINED = 0;
|
||||
let USERSKICKED = 0;
|
||||
let USERSBANNED = 0;
|
||||
let USERSUNBANNED = 0;
|
||||
let MESSAGESENT: any = {};
|
||||
let USERSYNC_HANDLED = false;
|
||||
let KICKBAN_HANDLED = false;
|
||||
let MESSAGE_PROCCESS = "";
|
||||
|
||||
function createRH(opts: any = {}) {
|
||||
USERSJOINED = 0;
|
||||
USERSKICKED = 0;
|
||||
USERSBANNED = 0;
|
||||
USERSUNBANNED = 0;
|
||||
MESSAGESENT = {};
|
||||
USERSYNC_HANDLED = false;
|
||||
KICKBAN_HANDLED = false;
|
||||
MESSAGE_PROCCESS = "";
|
||||
const bridge = {
|
||||
getBot: () => {
|
||||
return {
|
||||
getJoinedRooms: () => ["!123:localhost"],
|
||||
isRemoteUser: (id) => {
|
||||
return id !== undefined && id.startsWith("@_discord_");
|
||||
},
|
||||
};
|
||||
},
|
||||
getIntent: () => {
|
||||
return {
|
||||
ban: async () => { USERSBANNED++; },
|
||||
getClient: () => mxClient,
|
||||
getEvent: () => ({ content: { } }),
|
||||
join: () => { USERSJOINED++; },
|
||||
kick: async () => { USERSKICKED++; },
|
||||
leave: () => { },
|
||||
sendMessage: async (roomId, content) => { MESSAGESENT = content; return content; },
|
||||
unban: async () => { USERSUNBANNED++; },
|
||||
};
|
||||
},
|
||||
};
|
||||
const bridge = new AppserviceMock({
|
||||
joinedrooms: ["!123:localhost"],
|
||||
userIdPrefix: "@_discord_",
|
||||
});
|
||||
const us = {
|
||||
JoinRoom: async () => { USERSJOINED++; },
|
||||
OnMemberState: async () => {
|
||||
USERSYNC_HANDLED = true;
|
||||
},
|
||||
OnUpdateUser: async () => { },
|
||||
};
|
||||
const cs = {
|
||||
|
@ -106,18 +70,18 @@ function createRH(opts: any = {}) {
|
|||
chan.members.set("54321", new MockMember("54321", "testuser2"));
|
||||
chan.members.set("bot12345", new MockMember("bot12345", "botuser"));
|
||||
}
|
||||
guild.members = chan.members;
|
||||
guild.members.updateCache(chan.members);
|
||||
return chan;
|
||||
} else {
|
||||
throw new Error("Roomid not found");
|
||||
}
|
||||
},
|
||||
GetGuilds: () => [new MockGuild("123", [])],
|
||||
GetIntentFromDiscordMember: () => {
|
||||
return bridge.getIntent();
|
||||
GetIntentFromDiscordMember: (member: {id: string}) => {
|
||||
return bridge.getIntentForSuffix(member.id);
|
||||
},
|
||||
HandleMatrixKickBan: async () => {
|
||||
KICKBAN_HANDLED = true;
|
||||
|
||||
},
|
||||
LookupRoom: async (guildid, discordid) => {
|
||||
if (guildid !== "123") {
|
||||
|
@ -125,41 +89,22 @@ function createRH(opts: any = {}) {
|
|||
} else if (discordid !== "456") {
|
||||
throw new Error("Channel not found");
|
||||
}
|
||||
const channel = new MockChannel();
|
||||
const channel = new MockChannel(discordid, new MockGuild(guildid));
|
||||
return {channel, botUser: true };
|
||||
},
|
||||
ProcessMatrixMsgEvent: async () => {
|
||||
MESSAGE_PROCCESS = "processed";
|
||||
},
|
||||
ProcessMatrixRedact: async () => {
|
||||
MESSAGE_PROCCESS = "redacted";
|
||||
},
|
||||
ProcessMatrixStateEvent: async () => {
|
||||
MESSAGE_PROCCESS = "stateevent";
|
||||
},
|
||||
ThirdpartySearchForChannels: () => {
|
||||
return [];
|
||||
},
|
||||
UserSyncroniser: us,
|
||||
};
|
||||
const config = new DiscordBridgeConfig();
|
||||
config.room.defaultVisibility = "public";
|
||||
config.limits.roomGhostJoinDelay = 0;
|
||||
if (opts.disableSS) {
|
||||
config.bridge.enableSelfServiceBridging = false;
|
||||
} else {
|
||||
config.bridge.enableSelfServiceBridging = true;
|
||||
}
|
||||
const mxClient = {
|
||||
getStateEvent: async () => {
|
||||
return opts.powerLevels || {};
|
||||
},
|
||||
getUserId: () => "@user:localhost",
|
||||
joinRoom: async () => {
|
||||
USERSJOINED++;
|
||||
},
|
||||
sendReadReceipt: async () => { },
|
||||
setRoomDirectoryVisibilityAppService: async () => { },
|
||||
};
|
||||
const provisioner = {
|
||||
AskBridgePermission: async () => {
|
||||
if (opts.denyBridgePermission) {
|
||||
|
@ -192,23 +137,23 @@ function createRH(opts: any = {}) {
|
|||
},
|
||||
};
|
||||
const handler = new RoomHandler(bot as any, config, provisioner as any, bridge as any, store);
|
||||
return handler;
|
||||
return { handler, bridge };
|
||||
}
|
||||
|
||||
describe("MatrixRoomHandler", () => {
|
||||
describe("OnAliasQueried", () => {
|
||||
it("should join successfully", async () => {
|
||||
const handler = createRH();
|
||||
const {handler} = createRH();
|
||||
await handler.OnAliasQueried("#accept:localhost", "!accept:localhost");
|
||||
});
|
||||
it("should join successfully and create ghosts", async () => {
|
||||
const EXPECTEDUSERS = 2;
|
||||
const handler = createRH({createMembers: true});
|
||||
const {handler} = createRH({createMembers: true});
|
||||
await handler.OnAliasQueried("#accept:localhost", "!accept:localhost");
|
||||
expect(USERSJOINED).to.equal(EXPECTEDUSERS);
|
||||
});
|
||||
it("should not join successfully", async () => {
|
||||
const handler = createRH();
|
||||
const {handler} = createRH();
|
||||
try {
|
||||
await handler.OnAliasQueried("#reject:localhost", "!reject:localhost");
|
||||
throw new Error("didn't fail");
|
||||
|
@ -219,123 +164,117 @@ describe("MatrixRoomHandler", () => {
|
|||
});
|
||||
describe("OnAliasQuery", () => {
|
||||
it("will create room", async () => {
|
||||
const handler: any = createRH({});
|
||||
handler.createMatrixRoom = () => true;
|
||||
const ret = await handler.OnAliasQuery(
|
||||
"_discord_123_456:localhost",
|
||||
"_discord_123_456");
|
||||
expect(ret).to.be.true;
|
||||
const {handler} = createRH({});
|
||||
const ret = await handler.OnAliasQuery("#_discord_123_456:localhost");
|
||||
expect(ret).to.be.deep.equal({
|
||||
initial_state: [
|
||||
{
|
||||
content: {
|
||||
join_rule: "public",
|
||||
},
|
||||
state_key: "",
|
||||
type: "m.room.join_rules",
|
||||
},
|
||||
],
|
||||
room_alias_name: "_discord_123_456",
|
||||
visibility: "public",
|
||||
});
|
||||
});
|
||||
it("will not create room if guild cannot be found", async () => {
|
||||
const handler: any = createRH({});
|
||||
const {handler} = createRH({});
|
||||
handler.createMatrixRoom = () => true;
|
||||
const ret = await handler.OnAliasQuery(
|
||||
"_discord_111_456:localhost",
|
||||
"_discord_111_456");
|
||||
"#_discord_111_456:localhost");
|
||||
expect(ret).to.be.undefined;
|
||||
});
|
||||
it("will not create room if channel cannot be found", async () => {
|
||||
const handler: any = createRH({});
|
||||
const {handler} = createRH({});
|
||||
handler.createMatrixRoom = () => true;
|
||||
const ret = await handler.OnAliasQuery(
|
||||
"_discord_123_444:localhost",
|
||||
"_discord_123_444");
|
||||
"#_discord_123_444:localhost");
|
||||
expect(ret).to.be.undefined;
|
||||
});
|
||||
it("will not create room if alias is wrong", async () => {
|
||||
const handler: any = createRH({});
|
||||
const {handler} = createRH({});
|
||||
handler.createMatrixRoom = () => true;
|
||||
const ret = await handler.OnAliasQuery(
|
||||
"_discord_123:localhost",
|
||||
"_discord_123");
|
||||
"#_discord_123:localhost");
|
||||
expect(ret).to.be.undefined;
|
||||
});
|
||||
});
|
||||
describe("tpGetProtocol", () => {
|
||||
it("will return an object", async () => {
|
||||
const handler: any = createRH({});
|
||||
const protocol = await handler.tpGetProtocol("");
|
||||
expect(protocol).to.not.be.null;
|
||||
expect(protocol.instances[0].network_id).to.equal("123");
|
||||
expect(protocol.instances[0].bot_user_id).to.equal("@botuser:localhost");
|
||||
expect(protocol.instances[0].desc).to.equal("123");
|
||||
expect(protocol.instances[0].network_id).to.equal("123");
|
||||
});
|
||||
});
|
||||
describe("tpGetLocation", () => {
|
||||
it("will return an array", async () => {
|
||||
const handler: any = createRH({});
|
||||
const channels = await handler.tpGetLocation("", {
|
||||
channel_name: "",
|
||||
guild_id: "",
|
||||
});
|
||||
expect(channels).to.be.a("array");
|
||||
});
|
||||
});
|
||||
describe("tpParseLocation", () => {
|
||||
it("will reject", async () => {
|
||||
const handler: any = createRH({});
|
||||
try {
|
||||
await handler.tpParseLocation("alias");
|
||||
throw new Error("didn't fail");
|
||||
} catch (e) {
|
||||
expect(e.message).to.not.equal("didn't fail");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("tpGetUser", () => {
|
||||
it("will reject", async () => {
|
||||
const handler: any = createRH({});
|
||||
try {
|
||||
await handler.tpGetUser("", {});
|
||||
throw new Error("didn't fail");
|
||||
} catch (e) {
|
||||
expect(e.message).to.not.equal("didn't fail");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("tpParseUser", () => {
|
||||
it("will reject", async () => {
|
||||
const handler: any = createRH({});
|
||||
try {
|
||||
await handler.tpParseUser("alias");
|
||||
throw new Error("didn't fail");
|
||||
} catch (e) {
|
||||
expect(e.message).to.not.equal("didn't fail");
|
||||
}
|
||||
});
|
||||
});
|
||||
// Currently not supported on matrix-js-bot-sdk
|
||||
//
|
||||
// describe("tpGetProtocol", () => {
|
||||
// it("will return an object", async () => {
|
||||
// const {handler} = createRH({});
|
||||
// const protocol = await handler.tpGetProtocol("");
|
||||
// expect(protocol).to.not.be.null;
|
||||
// expect(protocol.instances[0].network_id).to.equal("123");
|
||||
// expect(protocol.instances[0].bot_user_id).to.equal("@botuser:localhost");
|
||||
// expect(protocol.instances[0].desc).to.equal("123");
|
||||
// expect(protocol.instances[0].network_id).to.equal("123");
|
||||
// });
|
||||
// });
|
||||
// describe("tpGetLocation", () => {
|
||||
// it("will return an array", async () => {
|
||||
// const {handler} = createRH({});
|
||||
// const channels = await handler.tpGetLocation("", {
|
||||
// channel_name: "",
|
||||
// guild_id: "",
|
||||
// });
|
||||
// expect(channels).to.be.a("array");
|
||||
// });
|
||||
// });
|
||||
// describe("tpParseLocation", () => {
|
||||
// it("will reject", async () => {
|
||||
// const {handler} = createRH({});
|
||||
// try {
|
||||
// await handler.tpParseLocation("alias");
|
||||
// throw new Error("didn't fail");
|
||||
// } catch (e) {
|
||||
// expect(e.message).to.not.equal("didn't fail");
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// describe("tpGetUser", () => {
|
||||
// it("will reject", async () => {
|
||||
// const {handler} = createRH({});
|
||||
// try {
|
||||
// await handler.tpGetUser("", {});
|
||||
// throw new Error("didn't fail");
|
||||
// } catch (e) {
|
||||
// expect(e.message).to.not.equal("didn't fail");
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// describe("tpParseUser", () => {
|
||||
// it("will reject", async () => {
|
||||
// const {handler} = createRH({});
|
||||
// try {
|
||||
// await handler.tpParseUser("alias");
|
||||
// throw new Error("didn't fail");
|
||||
// } catch (e) {
|
||||
// expect(e.message).to.not.equal("didn't fail");
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
describe("joinRoom", () => {
|
||||
it("will join immediately", async () => {
|
||||
const handler: any = createRH({});
|
||||
const intent = {
|
||||
getClient: () => {
|
||||
return {
|
||||
joinRoom: async () => { },
|
||||
};
|
||||
},
|
||||
};
|
||||
const startTime = Date.now();
|
||||
const MAXTIME = 1000;
|
||||
const {handler, bridge} = createRH({});
|
||||
const intent = bridge.botIntent;
|
||||
await handler.joinRoom(intent, "#test:localhost");
|
||||
expect(1).to.satisfy(() => {
|
||||
return (Date.now() - startTime) < MAXTIME;
|
||||
});
|
||||
intent.wasCalled("joinRoom", true, "#test:localhost");
|
||||
});
|
||||
it("will fail first, join after", async () => {
|
||||
const handler: any = createRH({});
|
||||
const {handler, bridge} = createRH({});
|
||||
let shouldFail = true;
|
||||
const intent = {
|
||||
getClient: () => {
|
||||
return {
|
||||
getUserId: () => "@test:localhost",
|
||||
joinRoom: async () => {
|
||||
if (shouldFail) {
|
||||
shouldFail = false;
|
||||
throw new Error("Test failed first time");
|
||||
}
|
||||
},
|
||||
};
|
||||
getUserId: () => "@test:localhost",
|
||||
joinRoom: async () => {
|
||||
if (shouldFail) {
|
||||
shouldFail = false;
|
||||
throw new Error("Test failed first time");
|
||||
}
|
||||
},
|
||||
};
|
||||
const startTime = Date.now();
|
||||
|
@ -349,10 +288,10 @@ describe("MatrixRoomHandler", () => {
|
|||
});
|
||||
describe("createMatrixRoom", () => {
|
||||
it("will return an object", async () => {
|
||||
const handler: any = createRH({});
|
||||
const {handler} = createRH({});
|
||||
const channel = new MockChannel("123", new MockGuild("456"));
|
||||
const roomOpts = await handler.createMatrixRoom(channel, "#test:localhost");
|
||||
expect(roomOpts.creationOpts).to.exist;
|
||||
expect(roomOpts).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,42 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import * as Discord from "discord.js";
|
||||
import * as Proxyquire from "proxyquire";
|
||||
import { expect } from "chai";
|
||||
import * as Discord from "better-discord.js";
|
||||
|
||||
import { PresenceHandler } from "../src/presencehandler";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { MockUser } from "./mocks/user";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
import { MockPresence } from "./mocks/presence";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
const INTERVAL = 250;
|
||||
let lastStatus = null;
|
||||
// const assert = Chai.assert;
|
||||
const bot = {
|
||||
const appservice = new AppserviceMock();
|
||||
const bot: any = {
|
||||
GetBotId: () => {
|
||||
return "1234";
|
||||
},
|
||||
GetIntentFromDiscordMember: (member) => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
setPresence: async (status) => {
|
||||
lastStatus = status;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
GetIntentFromDiscordMember: (member: MockUser) => {
|
||||
return appservice.getIntentForSuffix(member.id);
|
||||
},
|
||||
};
|
||||
|
||||
describe("PresenceHandler", () => {
|
||||
describe("init", () => {
|
||||
it("constructor", () => {
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
new PresenceHandler(bot as DiscordBot);
|
||||
});
|
||||
});
|
||||
describe("Stop", () => {
|
||||
|
@ -63,124 +55,90 @@ describe("PresenceHandler", () => {
|
|||
it("adds a user properly", () => {
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const COUNT = 2;
|
||||
handler.EnqueueUser(new MockUser("abc", "def") as any);
|
||||
handler.EnqueueUser(new MockUser("123", "ghi") as any);
|
||||
Chai.assert.equal(handler.QueueCount, COUNT);
|
||||
handler.EnqueueUser(new MockPresence(new MockUser("abc", "alice"), "def") as any);
|
||||
handler.EnqueueUser(new MockPresence(new MockUser("123", "bob"), "ghi") as any);
|
||||
expect(handler.QueueCount).to.be.equal(COUNT);
|
||||
});
|
||||
it("does not add duplicate users", () => {
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
handler.EnqueueUser(new MockUser("abc", "def") as any);
|
||||
handler.EnqueueUser(new MockUser("abc", "def") as any);
|
||||
Chai.assert.equal(handler.QueueCount, 1);
|
||||
handler.EnqueueUser(new MockPresence(new MockUser("123", "alice"), "def") as any);
|
||||
handler.EnqueueUser(new MockPresence(new MockUser("123", "alice"), "def") as any);
|
||||
expect(handler.QueueCount).to.be.equal(1);
|
||||
});
|
||||
it("does not add the bot user", () => {
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
handler.EnqueueUser(new MockUser("1234", "def") as any);
|
||||
Chai.assert.equal(handler.QueueCount, 0);
|
||||
handler.EnqueueUser(new MockPresence(new MockUser("1234", "bob"), "ghi") as any);
|
||||
expect(handler.QueueCount).to.be.equal(0);
|
||||
});
|
||||
});
|
||||
describe("DequeueUser", () => {
|
||||
it("removes users properly", () => {
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const members = [
|
||||
new MockUser("abc", "def") as any,
|
||||
new MockUser("def", "ghi") as any,
|
||||
new MockUser("ghi", "wew") as any,
|
||||
new MockPresence(new MockUser("abc", "alice"), "def") as any,
|
||||
new MockPresence(new MockUser("def", "bob"), "ghi") as any,
|
||||
new MockPresence(new MockUser("ghi", "foo"), "wew") as any,
|
||||
];
|
||||
handler.EnqueueUser(members[0]);
|
||||
handler.EnqueueUser(members[1]);
|
||||
handler.EnqueueUser(members[members.length - 1]);
|
||||
handler.EnqueueUser(members[2]);
|
||||
|
||||
handler.DequeueUser(members[members.length - 1]);
|
||||
Chai.assert.equal(handler.QueueCount, members.length - 1);
|
||||
handler.DequeueUser(members[1]);
|
||||
Chai.assert.equal(handler.QueueCount, 1);
|
||||
handler.DequeueUser(members[0]);
|
||||
Chai.assert.equal(handler.QueueCount, 0);
|
||||
handler.DequeueUser(members[2].user);
|
||||
expect(handler.QueueCount).to.be.equal(members.length - 1);
|
||||
handler.DequeueUser(members[1].user);
|
||||
expect(handler.QueueCount).to.be.equal(1);
|
||||
handler.DequeueUser(members[0].user);
|
||||
expect(handler.QueueCount).to.be.equal(0);
|
||||
});
|
||||
});
|
||||
describe("ProcessUser", () => {
|
||||
it("processes an online user", async () => {
|
||||
lastStatus = null;
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const member = new MockUser("abc", "def") as any;
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
status: "online",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "online",
|
||||
});
|
||||
const member = new MockPresence(new MockUser("ghi", "alice"), "def", "online");
|
||||
await handler.ProcessUser(member as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "online", undefined);
|
||||
});
|
||||
it("processes an offline user", async () => {
|
||||
lastStatus = null;
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const member = new MockUser("abc", "def") as any;
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
status: "offline",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "offline",
|
||||
});
|
||||
|
||||
const member = new MockPresence(new MockUser("abc", "alice"), "def", "offline");
|
||||
await handler.ProcessUser(member as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "offline", undefined);
|
||||
});
|
||||
it("processes an idle user", async () => {
|
||||
lastStatus = null;
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const member = new MockUser("abc", "def") as any;
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
status: "idle",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "unavailable",
|
||||
});
|
||||
const member = new MockPresence(new MockUser("abc", "alice"), "def", "idle");
|
||||
await handler.ProcessUser(member as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "unavailable", undefined);
|
||||
});
|
||||
it("processes an dnd user", async () => {
|
||||
lastStatus = null;
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const member = new MockUser("abc", "def") as any;
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
status: "dnd",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "online",
|
||||
status_msg: "Do not disturb",
|
||||
});
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
game: new Discord.Game({name: "Test Game"}, {} as any),
|
||||
status: "dnd",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "online",
|
||||
status_msg: "Do not disturb | Playing Test Game",
|
||||
});
|
||||
const member = new MockPresence(new MockUser("abc", "alice"), "def", "dnd");
|
||||
await handler.ProcessUser(member as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "online", "Do not disturb");
|
||||
const member2 = new MockPresence(new MockUser("abc", "alice"), "def", "dnd", [{name: "Test Game", type: "PLAYING"}]);
|
||||
await handler.ProcessUser(member2 as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "online", "Do not disturb | Playing Test Game");
|
||||
});
|
||||
it("processes a user playing games", async () => {
|
||||
lastStatus = null;
|
||||
const handler = new PresenceHandler(bot as DiscordBot);
|
||||
const member = new MockUser("abc", "def") as any;
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
game: new Discord.Game({name: "Test Game"}, {} as any),
|
||||
status: "online",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "online",
|
||||
status_msg: "Playing Test Game",
|
||||
});
|
||||
member.MockSetPresence(new Discord.Presence({
|
||||
game: new Discord.Game({name: "Test Game", type: 1}, {} as any),
|
||||
status: "online",
|
||||
}, {} as any));
|
||||
await handler.ProcessUser(member);
|
||||
Chai.assert.deepEqual(lastStatus, {
|
||||
presence: "online",
|
||||
status_msg: "Streaming Test Game",
|
||||
});
|
||||
const member = new MockPresence(new MockUser("abc", "alice"), "def", "online", [{name: "Test Game", type: "PLAYING"}]);
|
||||
await handler.ProcessUser(member as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "online", "Playing Test Game");
|
||||
const member2 = new MockPresence(new MockUser("abc", "alice"), "def", "online", [{name: "Test Game", type: "STREAMING"}]);
|
||||
await handler.ProcessUser(member2 as any);
|
||||
appservice.getIntentForSuffix(member.userID)
|
||||
.underlyingClient.wasCalled("setPresenceStatus", true, "online", "Streaming Test Game");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
import { Provisioner } from "../src/provisioner";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { MockMember } from "./mocks/member";
|
||||
|
@ -22,8 +22,6 @@ import { MockMember } from "./mocks/member";
|
|||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
const TIMEOUT_MS = 1000;
|
||||
|
||||
describe("Provisioner", () => {
|
||||
|
|
|
@ -15,21 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
// import * as Proxyquire from "proxyquire";
|
||||
import { DiscordStore, CURRENT_SCHEMA } from "../src/store";
|
||||
import { DiscordStore } from "../src/store";
|
||||
import { DbEmoji } from "../src/db/dbdataemoji";
|
||||
import { DbEvent } from "../src/db/dbdataevent";
|
||||
import { Log } from "../src/log";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
const TEST_SCHEMA = CURRENT_SCHEMA;
|
||||
|
||||
// const assert = Chai.assert;
|
||||
|
||||
describe("DiscordStore", () => {
|
||||
describe("init", () => {
|
||||
it("can create a db", async () => {
|
||||
|
|
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { Bridge } from "matrix-appservice-bridge";
|
||||
import { expect } from "chai";
|
||||
import {IGuildMemberState, IUserState, UserSyncroniser} from "../src/usersyncroniser";
|
||||
import {MockUser} from "./mocks/user";
|
||||
import {DiscordBridgeConfig} from "../src/config";
|
||||
|
@ -24,30 +23,17 @@ import {MockMember} from "./mocks/member";
|
|||
import {MockGuild} from "./mocks/guild";
|
||||
import { MockChannel } from "./mocks/channel";
|
||||
import { MockRole } from "./mocks/role";
|
||||
import { IMatrixEvent } from "../src/matrixtypes";
|
||||
import { Util } from "../src/util";
|
||||
import { RemoteUser } from "../src/db/userstore";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
const expect = Chai.expect;
|
||||
|
||||
let DISPLAYNAME_SET: any = null;
|
||||
let AVATAR_SET: any = null;
|
||||
let REMOTEUSER_SET: any = null;
|
||||
let INTENT_ID: any = null;
|
||||
let LINK_MX_USER: any = null;
|
||||
let LINK_RM_USER: any = null;
|
||||
let UTIL_UPLOADED_AVATAR: any = false;
|
||||
|
||||
let SEV_ROOM_ID: any = null;
|
||||
let SEV_CONTENT: any = null;
|
||||
let SEV_KEY: any = null;
|
||||
let JOIN_ROOM_ID: any = null;
|
||||
let LEAVE_ROOM_ID: any = null;
|
||||
let JOINS: any = 0;
|
||||
let LEAVES: any = 0;
|
||||
let SEV_COUNT: any = 0;
|
||||
let REMOTEUSER_SET: any = null;
|
||||
|
||||
const GUILD_ROOM_IDS = ["!abc:localhost", "!def:localhost", "!ghi:localhost"];
|
||||
const GUILD_ROOM_IDS_WITH_ROLE = ["!abc:localhost", "!def:localhost"];
|
||||
|
@ -57,70 +43,25 @@ const UserSync = (Proxyquire("../src/usersyncroniser", {
|
|||
Util: {
|
||||
ApplyPatternString: Util.ApplyPatternString,
|
||||
AsyncForEach: Util.AsyncForEach,
|
||||
UploadContentFromUrl: async () => {
|
||||
DownloadFile: async () => {
|
||||
UTIL_UPLOADED_AVATAR = true;
|
||||
return {mxcUrl: "avatarset"};
|
||||
return {buffer: Buffer.from([])};
|
||||
},
|
||||
ParseMxid: Util.ParseMxid,
|
||||
},
|
||||
},
|
||||
})).UserSyncroniser;
|
||||
|
||||
function CreateUserSync(remoteUsers: RemoteUser[] = [], ghostConfig: any = {}): UserSyncroniser {
|
||||
function CreateUserSync(remoteUsers: RemoteUser[] = [], ghostConfig: any = {}) {
|
||||
UTIL_UPLOADED_AVATAR = false;
|
||||
SEV_ROOM_ID = null;
|
||||
SEV_CONTENT = null;
|
||||
SEV_KEY = null;
|
||||
SEV_COUNT = 0;
|
||||
const bridge: any = {
|
||||
getIntent: (id) => {
|
||||
DISPLAYNAME_SET = null;
|
||||
AVATAR_SET = null;
|
||||
INTENT_ID = id;
|
||||
JOIN_ROOM_ID = null;
|
||||
JOINS = 0;
|
||||
LEAVES = 0;
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
getUserId: () => "@user:localhost",
|
||||
sendStateEvent: (roomId, type, content, key) => {
|
||||
SEV_ROOM_ID = roomId;
|
||||
SEV_CONTENT = content;
|
||||
SEV_KEY = key;
|
||||
SEV_COUNT++;
|
||||
},
|
||||
};
|
||||
},
|
||||
join: (roomId) => {
|
||||
JOIN_ROOM_ID = roomId;
|
||||
JOINS++;
|
||||
},
|
||||
leave: (roomId) => {
|
||||
LEAVE_ROOM_ID = roomId;
|
||||
LEAVES++;
|
||||
},
|
||||
opts: {
|
||||
backingStore: {
|
||||
getMembership: (roomId, userId) => "join",
|
||||
setMembership: (roomId, userId, membership) => { },
|
||||
},
|
||||
},
|
||||
setAvatarUrl: async (ava) => {
|
||||
AVATAR_SET = ava;
|
||||
},
|
||||
setDisplayName: async (dn) => {
|
||||
DISPLAYNAME_SET = dn;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const bridge = new AppserviceMock();
|
||||
const discordbot: any = {
|
||||
GetChannelFromRoomId: (id) => {
|
||||
if (id === "!found:localhost") {
|
||||
const guild = new MockGuild("666666");
|
||||
guild.members.set("123456", new MockMember("123456", "fella", guild));
|
||||
guild.members.cache.set("123456", new MockMember("123456", "fella", guild));
|
||||
const chan = new MockChannel("543345", guild);
|
||||
guild.channels.set("543345", chan as any);
|
||||
guild.channels.cache.set("543345", chan as any);
|
||||
return chan;
|
||||
}
|
||||
throw new Error("Channel not found");
|
||||
|
@ -128,11 +69,11 @@ function CreateUserSync(remoteUsers: RemoteUser[] = [], ghostConfig: any = {}):
|
|||
GetGuilds: () => {
|
||||
return [];
|
||||
},
|
||||
GetIntentFromDiscordMember: (id) => {
|
||||
return bridge.getIntent(id);
|
||||
GetIntentFromDiscordMember: (member) => {
|
||||
return bridge.getIntentForSuffix(member.id);
|
||||
},
|
||||
GetRoomIdsFromGuild: async (guild, member?) => {
|
||||
if (member && member.roles.get("1234")) {
|
||||
GetRoomIdsFromGuild: async (guild: MockGuild, member: MockMember) => {
|
||||
if (member && member.roles.cache.get("1234")) {
|
||||
return GUILD_ROOM_IDS_WITH_ROLE;
|
||||
}
|
||||
return GUILD_ROOM_IDS;
|
||||
|
@ -155,13 +96,14 @@ function CreateUserSync(remoteUsers: RemoteUser[] = [], ghostConfig: any = {}):
|
|||
const config = new DiscordBridgeConfig();
|
||||
config.bridge.domain = "localhost";
|
||||
config.ghosts = Object.assign({}, config.ghosts, ghostConfig);
|
||||
return new UserSync(bridge as Bridge, config, discordbot, userStore as any);
|
||||
const userSync: UserSyncroniser = new UserSync(bridge as any, config, discordbot, userStore as any);
|
||||
return {bridge, userSync};
|
||||
}
|
||||
|
||||
describe("UserSyncroniser", () => {
|
||||
describe("GetUserUpdateState", () => {
|
||||
it("Will create a new user", async () => {
|
||||
const userSync = CreateUserSync();
|
||||
const {userSync} = CreateUserSync();
|
||||
const user = new MockUser(
|
||||
"123456",
|
||||
"TestUsername",
|
||||
|
@ -182,7 +124,7 @@ describe("UserSyncroniser", () => {
|
|||
remoteUser.avatarurl = "test.jpg";
|
||||
remoteUser.displayname = "TestUsername";
|
||||
|
||||
const userSync = CreateUserSync([remoteUser]);
|
||||
const {userSync} = CreateUserSync([remoteUser]);
|
||||
const user = new MockUser(
|
||||
"123456",
|
||||
"TestUsername",
|
||||
|
@ -203,7 +145,7 @@ describe("UserSyncroniser", () => {
|
|||
remoteUser.avatarurl = "test.jpg";
|
||||
remoteUser.displayname = "TestUsername";
|
||||
|
||||
const userSync = CreateUserSync([remoteUser], {usernamePattern: ":username#:tag (Discord)"});
|
||||
const {userSync} = CreateUserSync([remoteUser], {usernamePattern: ":username#:tag (Discord)"});
|
||||
const user = new MockUser(
|
||||
"123456",
|
||||
"TestUsername",
|
||||
|
@ -224,7 +166,7 @@ describe("UserSyncroniser", () => {
|
|||
remoteUser.avatarurl = "test.jpg";
|
||||
remoteUser.displayname = "TestUsername#6969";
|
||||
|
||||
const userSync = CreateUserSync([remoteUser]);
|
||||
const {userSync} = CreateUserSync([remoteUser]);
|
||||
const user = new MockUser(
|
||||
"123456",
|
||||
"TestUsername",
|
||||
|
@ -245,7 +187,7 @@ describe("UserSyncroniser", () => {
|
|||
remoteUser.avatarurl = "test.jpg";
|
||||
remoteUser.displayname = "TestUsername#6969";
|
||||
|
||||
const userSync = CreateUserSync([remoteUser]);
|
||||
const {userSync} = CreateUserSync([remoteUser]);
|
||||
const user = new MockUser(
|
||||
"123456",
|
||||
"TestUsername",
|
||||
|
@ -264,7 +206,7 @@ describe("UserSyncroniser", () => {
|
|||
});
|
||||
describe("ApplyStateToProfile", () => {
|
||||
it("Will create a new user", async () => {
|
||||
const userSync = CreateUserSync();
|
||||
const {userSync} = CreateUserSync();
|
||||
const state: IUserState = {
|
||||
avatarId: "",
|
||||
avatarUrl: null, // Nullable
|
||||
|
@ -280,7 +222,7 @@ describe("UserSyncroniser", () => {
|
|||
expect(REMOTEUSER_SET).is.null;
|
||||
});
|
||||
it("Will set a display name", async () => {
|
||||
const userSync = CreateUserSync();
|
||||
const {userSync, bridge} = CreateUserSync();
|
||||
const state: IUserState = {
|
||||
avatarId: "",
|
||||
avatarUrl: null, // Nullable
|
||||
|
@ -294,15 +236,17 @@ describe("UserSyncroniser", () => {
|
|||
expect(LINK_MX_USER).is.not.null;
|
||||
expect(LINK_RM_USER).is.not.null;
|
||||
expect(REMOTEUSER_SET).is.not.null;
|
||||
expect(DISPLAYNAME_SET).equal("123456");
|
||||
expect(REMOTEUSER_SET.displayname).equal("123456");
|
||||
expect(AVATAR_SET).is.null;
|
||||
expect(REMOTEUSER_SET.avatarurl).is.null;
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasCalled("setDisplayName", true, "123456");
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("setAvatarUrl", true);
|
||||
});
|
||||
it("Will set an avatar", async () => {
|
||||
const userSync = CreateUserSync();
|
||||
const {userSync, bridge} = CreateUserSync();
|
||||
const state: IUserState = {
|
||||
avatarId: "",
|
||||
avatarId: "avatarurl",
|
||||
avatarUrl: "654321", // Nullable
|
||||
createUser: true,
|
||||
displayName: null, // Nullable
|
||||
|
@ -313,15 +257,17 @@ describe("UserSyncroniser", () => {
|
|||
await userSync.ApplyStateToProfile(state);
|
||||
expect(LINK_MX_USER).is.not.null;
|
||||
expect(LINK_RM_USER).is.not.null;
|
||||
expect(AVATAR_SET).equal("avatarset");
|
||||
expect(UTIL_UPLOADED_AVATAR).to.be.true;
|
||||
expect(REMOTEUSER_SET).is.not.null;
|
||||
expect(REMOTEUSER_SET.avatarurl).equal("654321");
|
||||
expect(REMOTEUSER_SET.displayname).is.null;
|
||||
expect(DISPLAYNAME_SET).is.null;
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasCalled("setAvatarUrl", true, "mxc://avatarurl");
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("setDisplayName", true);
|
||||
});
|
||||
it("Will remove an avatar", async () => {
|
||||
const userSync = CreateUserSync();
|
||||
const {userSync, bridge} = CreateUserSync();
|
||||
const state: IUserState = {
|
||||
avatarId: "",
|
||||
avatarUrl: null, // Nullable
|
||||
|
@ -334,15 +280,17 @@ describe("UserSyncroniser", () => {
|
|||
await userSync.ApplyStateToProfile(state);
|
||||
expect(LINK_MX_USER).is.not.null;
|
||||
expect(LINK_RM_USER).is.not.null;
|
||||
expect(AVATAR_SET).is.null;
|
||||
expect(UTIL_UPLOADED_AVATAR).to.be.false;
|
||||
expect(REMOTEUSER_SET).is.not.null;
|
||||
expect(REMOTEUSER_SET.avatarurl).is.null;
|
||||
expect(REMOTEUSER_SET.displayname).is.null;
|
||||
expect(DISPLAYNAME_SET).is.null;
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("setAvatarUrl", true);
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("setDisplayName", true);
|
||||
});
|
||||
it("will do nothing if nothing needs to be done", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const state: IUserState = {
|
||||
avatarId: "",
|
||||
avatarUrl: null, // Nullable
|
||||
|
@ -355,14 +303,16 @@ describe("UserSyncroniser", () => {
|
|||
await userSync.ApplyStateToProfile(state);
|
||||
expect(LINK_MX_USER).is.null;
|
||||
expect(LINK_RM_USER).is.null;
|
||||
expect(AVATAR_SET).is.null;
|
||||
expect(REMOTEUSER_SET).is.null;
|
||||
expect(DISPLAYNAME_SET).is.null;
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("setAvatarUrl", true);
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("setDisplayName", true);
|
||||
});
|
||||
});
|
||||
describe("ApplyStateToRoom", () => {
|
||||
it("Will apply a new nick", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const state: IGuildMemberState = {
|
||||
bot: false,
|
||||
displayColor: 0,
|
||||
|
@ -375,12 +325,24 @@ describe("UserSyncroniser", () => {
|
|||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "123456");
|
||||
expect(REMOTEUSER_SET).is.not.null;
|
||||
expect(REMOTEUSER_SET.guildNicks.get("123456")).is.equal("Good Boy");
|
||||
expect(SEV_ROOM_ID).is.equal("!abc:localhost");
|
||||
expect(SEV_CONTENT.displayname).is.equal("Good Boy");
|
||||
expect(SEV_KEY).is.equal("@_discord_123456:localhost");
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost").underlyingClient.wasCalled(
|
||||
"sendStateEvent", true, "!abc:localhost",
|
||||
"m.room.member", "@_discord_123456:localhost", {
|
||||
"avatar_url": "",
|
||||
"displayname": "Good Boy",
|
||||
"membership": "join",
|
||||
"uk.half-shot.discord.member": {
|
||||
bot: false,
|
||||
displayColor: 0,
|
||||
id: "123456",
|
||||
roles: [],
|
||||
username: "",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
it("Will not apply unchanged nick", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const state: IGuildMemberState = {
|
||||
bot: false,
|
||||
displayColor: 0,
|
||||
|
@ -392,12 +354,11 @@ describe("UserSyncroniser", () => {
|
|||
};
|
||||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "123456");
|
||||
expect(REMOTEUSER_SET).is.null;
|
||||
expect(SEV_ROOM_ID).is.null;
|
||||
expect(SEV_CONTENT).is.null;
|
||||
expect(SEV_KEY).is.null;
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasNotCalled("sendStateEvent", true);
|
||||
});
|
||||
it("Will apply roles", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const TESTROLE_NAME = "testrole";
|
||||
const TESTROLE_COLOR = 1337;
|
||||
const TESTROLE_POSITION = 42;
|
||||
|
@ -417,16 +378,21 @@ describe("UserSyncroniser", () => {
|
|||
username: "",
|
||||
};
|
||||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "12345678");
|
||||
const custKey = SEV_CONTENT["uk.half-shot.discord.member"];
|
||||
const roles = custKey.roles;
|
||||
expect(custKey.id).is.equal("123456");
|
||||
expect(roles.length).is.equal(1);
|
||||
expect(roles[0].name).is.equal(TESTROLE_NAME);
|
||||
expect(roles[0].color).is.equal(TESTROLE_COLOR);
|
||||
expect(roles[0].position).is.equal(TESTROLE_POSITION);
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost").underlyingClient.sendStateEvent(
|
||||
"!abc:localhost",
|
||||
"m.room.member",
|
||||
"@_discord_123456:localhost", {
|
||||
"displayColor": 0,
|
||||
"displayname": state.displayName,
|
||||
"uk.half-shot.discord.member": {
|
||||
id: "123456",
|
||||
roles: state.roles,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
it("Will set bot correctly", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const state: IGuildMemberState = {
|
||||
bot: false,
|
||||
displayColor: 0,
|
||||
|
@ -437,17 +403,39 @@ describe("UserSyncroniser", () => {
|
|||
username: "",
|
||||
};
|
||||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "12345678");
|
||||
let custKey = SEV_CONTENT["uk.half-shot.discord.member"];
|
||||
expect(custKey.bot).is.false;
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost").underlyingClient.sendStateEvent(
|
||||
"!abc:localhost",
|
||||
"m.room.member",
|
||||
"@_discord_123456:localhost", {
|
||||
"bot": false,
|
||||
"displayColor": 0,
|
||||
"displayname": state.displayName,
|
||||
"uk.half-shot.discord.member": {
|
||||
id: "123456",
|
||||
roles: state.roles,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const sync2 = CreateUserSync([new RemoteUser("123456")]);
|
||||
state.bot = true;
|
||||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "12345678");
|
||||
custKey = SEV_CONTENT["uk.half-shot.discord.member"];
|
||||
expect(custKey.bot).is.true;
|
||||
await sync2.userSync.ApplyStateToRoom(state, "!abc:localhost", "12345678");
|
||||
sync2.bridge.getIntentForUserId("@_discord_123456:localhost").underlyingClient.sendStateEvent(
|
||||
"!abc:localhost",
|
||||
"m.room.member",
|
||||
"@_discord_123456:localhost", {
|
||||
"bot": true,
|
||||
"displayname": state.displayName,
|
||||
"uk.half-shot.discord.member": {
|
||||
id: "123456",
|
||||
roles: state.roles,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
it("Will set the displayColor correctly", async () => {
|
||||
const TEST_COLOR = 1234;
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const state: IGuildMemberState = {
|
||||
bot: false,
|
||||
displayColor: TEST_COLOR,
|
||||
|
@ -458,11 +446,22 @@ describe("UserSyncroniser", () => {
|
|||
username: "",
|
||||
};
|
||||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "12345678");
|
||||
const custKey = SEV_CONTENT["uk.half-shot.discord.member"];
|
||||
expect(custKey.displayColor).is.equal(TEST_COLOR);
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost").underlyingClient.sendStateEvent(
|
||||
"!abc:localhost",
|
||||
"m.room.member",
|
||||
"@_discord_123456:localhost", {
|
||||
"bot": false,
|
||||
"displayColor": TEST_COLOR,
|
||||
"displayname": state.displayName,
|
||||
"uk.half-shot.discord.member": {
|
||||
id: "123456",
|
||||
roles: state.roles,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
it("Will set username correctly", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const state: IGuildMemberState = {
|
||||
bot: false,
|
||||
displayColor: 0,
|
||||
|
@ -473,13 +472,25 @@ describe("UserSyncroniser", () => {
|
|||
username: "user#1234",
|
||||
};
|
||||
await userSync.ApplyStateToRoom(state, "!abc:localhost", "12345678");
|
||||
const custKey = SEV_CONTENT["uk.half-shot.discord.member"];
|
||||
expect(custKey.username).is.equal("user#1234");
|
||||
bridge.getIntentForUserId("@_discord_123456:localhost").underlyingClient.sendStateEvent(
|
||||
"!abc:localhost",
|
||||
"m.room.member",
|
||||
"@_discord_123456:localhost", {
|
||||
"bot": false,
|
||||
"displayColor": 0,
|
||||
"displayname": state.displayName,
|
||||
"uk.half-shot.discord.member": {
|
||||
id: "123456",
|
||||
roles: state.roles,
|
||||
},
|
||||
"username": "user#1234",
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("GetUserStateForGuildMember", () => {
|
||||
it("Will apply a new nick", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
const member = new MockMember(
|
||||
|
@ -491,7 +502,7 @@ describe("UserSyncroniser", () => {
|
|||
expect(state.displayName).to.be.equal("BestDog");
|
||||
});
|
||||
it("Will will obay nick pattern", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")], { nickPattern: ":nick (Discord)" });
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")], { nickPattern: ":nick (Discord)" });
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
const member = new MockMember(
|
||||
|
@ -503,7 +514,7 @@ describe("UserSyncroniser", () => {
|
|||
expect(state.displayName).to.be.equal("BestDog (Discord)");
|
||||
});
|
||||
it("Will correctly add roles", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
const member = new MockMember(
|
||||
|
@ -515,7 +526,7 @@ describe("UserSyncroniser", () => {
|
|||
const TESTROLE_COLOR = 1337;
|
||||
const TESTROLE_POSITION = 42;
|
||||
const role = new MockRole("123", TESTROLE_NAME, TESTROLE_COLOR, TESTROLE_POSITION);
|
||||
member.roles.set("123", role);
|
||||
member.roles.cache.set("123", role);
|
||||
const state = await userSync.GetUserStateForGuildMember(member as any);
|
||||
expect(state.roles.length).to.be.equal(1);
|
||||
expect(state.roles[0].name).to.be.equal(TESTROLE_NAME);
|
||||
|
@ -525,7 +536,7 @@ describe("UserSyncroniser", () => {
|
|||
});
|
||||
describe("GetUserStateForDiscordUser", () => {
|
||||
it("Will apply a new nick", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const member = new MockUser(
|
||||
"123456",
|
||||
"username",
|
||||
|
@ -534,19 +545,19 @@ describe("UserSyncroniser", () => {
|
|||
expect(state.displayName).to.be.equal("username");
|
||||
});
|
||||
it("Will handle webhooks", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const member = new MockUser(
|
||||
"123456",
|
||||
"username",
|
||||
"1234");
|
||||
const state = await userSync.GetUserStateForDiscordUser(member as any, "654321");
|
||||
const state = await userSync.GetUserStateForDiscordUser(member as any, true);
|
||||
expect(state.displayName).to.be.equal("username");
|
||||
expect(state.mxUserId).to.be.equal("@_discord_123456_username:localhost");
|
||||
});
|
||||
});
|
||||
describe("OnAddGuildMember", () => {
|
||||
it("will update user and join to rooms", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
const member = new MockMember(
|
||||
|
@ -554,12 +565,13 @@ describe("UserSyncroniser", () => {
|
|||
"username",
|
||||
guild);
|
||||
await userSync.OnAddGuildMember(member as any);
|
||||
expect(SEV_COUNT).to.equal(GUILD_ROOM_IDS.length);
|
||||
expect(bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasCalled("sendStateEvent")).to.equal(GUILD_ROOM_IDS.length);
|
||||
});
|
||||
});
|
||||
describe("OnRemoveGuildMember", () => {
|
||||
it("will leave users from rooms", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
const member = new MockMember(
|
||||
|
@ -567,12 +579,13 @@ describe("UserSyncroniser", () => {
|
|||
"username",
|
||||
guild);
|
||||
await userSync.OnRemoveGuildMember(member as any);
|
||||
expect(LEAVES).to.equal(GUILD_ROOM_IDS.length);
|
||||
expect(bridge.getIntentForSuffix("123456")
|
||||
.underlyingClient.wasCalled("leaveRoom")).to.equal(GUILD_ROOM_IDS.length);
|
||||
});
|
||||
});
|
||||
describe("OnUpdateGuildMember", () => {
|
||||
it("will update state for rooms", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
const newMember = new MockMember(
|
||||
|
@ -581,10 +594,11 @@ describe("UserSyncroniser", () => {
|
|||
guild,
|
||||
"FiddleDee");
|
||||
await userSync.OnUpdateGuildMember(newMember as any);
|
||||
expect(SEV_COUNT).to.equal(GUILD_ROOM_IDS.length);
|
||||
expect(bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasCalled("sendStateEvent")).to.equal(GUILD_ROOM_IDS.length);
|
||||
});
|
||||
it("will part rooms based on role removal", async () => {
|
||||
const userSync = CreateUserSync([new RemoteUser("123456")]);
|
||||
const {userSync, bridge} = CreateUserSync([new RemoteUser("123456")]);
|
||||
const role = new MockRole("1234", "role");
|
||||
const guild = new MockGuild(
|
||||
"654321");
|
||||
|
@ -593,11 +607,13 @@ describe("UserSyncroniser", () => {
|
|||
"username",
|
||||
guild,
|
||||
"FiddleDee");
|
||||
newMember.roles.set("1234", role);
|
||||
newMember.roles.cache.set("1234", role);
|
||||
await userSync.OnUpdateGuildMember(newMember as any);
|
||||
expect(SEV_COUNT).to.equal(GUILD_ROOM_IDS_WITH_ROLE.length);
|
||||
expect(LEAVES).to.equal(GUILD_ROOM_IDS.length - GUILD_ROOM_IDS_WITH_ROLE.length);
|
||||
expect(LEAVE_ROOM_ID).to.equal("!ghi:localhost");
|
||||
expect(bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasCalled("sendStateEvent")).to.equal(GUILD_ROOM_IDS_WITH_ROLE.length);
|
||||
expect(bridge.getIntentForUserId("@_discord_123456:localhost")
|
||||
.underlyingClient.wasCalled("leaveRoom", true, "!ghi:localhost"))
|
||||
.to.equal(GUILD_ROOM_IDS.length - GUILD_ROOM_IDS_WITH_ROLE.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,50 +14,37 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Chai from "chai";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { Util, ICommandActions, ICommandParameters } from "../src/util";
|
||||
import { AppserviceMock } from "./mocks/appservicemock";
|
||||
|
||||
// we are a test file and thus need those
|
||||
/* tslint:disable:no-unused-expression max-file-line-count no-any */
|
||||
|
||||
const expect = Chai.expect;
|
||||
|
||||
function CreateMockIntent(members) {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
_http: {
|
||||
authedRequestWithPrefix: async (_, __, url, ___, ____, _____) => {
|
||||
const ret: any[] = [];
|
||||
for (const member of members[url]) {
|
||||
ret.push({
|
||||
content: {
|
||||
displayname: member.displayname,
|
||||
},
|
||||
membership: member.membership,
|
||||
state_key: member.mxid,
|
||||
});
|
||||
}
|
||||
return {
|
||||
chunk: ret,
|
||||
};
|
||||
},
|
||||
function CreateMockIntent(members): any {
|
||||
const as = new AppserviceMock({
|
||||
roommembers: members.map((member) =>
|
||||
({
|
||||
content: {
|
||||
displayname: member.displayname,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
membership: member.membership,
|
||||
stateKey: member.mxid,
|
||||
}),
|
||||
),
|
||||
});
|
||||
return as.botIntent;
|
||||
}
|
||||
|
||||
describe("Util", () => {
|
||||
describe("MsgToArgs", () => {
|
||||
it("parses arguments", () => {
|
||||
const {command, args} = Util.MsgToArgs("!matrix command arg1 arg2", "!matrix");
|
||||
Chai.assert.equal(command, "command");
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
Chai.assert.equal(args.length, 2);
|
||||
Chai.assert.equal(args[0], "arg1");
|
||||
Chai.assert.equal(args[1], "arg2");
|
||||
expect(command).to.be.eq("command");
|
||||
expect(args.length).to.be.eq(2);
|
||||
expect(args[0]).to.be.eq("arg1");
|
||||
expect(args[1]).to.be.eq("arg2");
|
||||
});
|
||||
});
|
||||
describe("Command Stuff", () => {
|
||||
|
@ -86,6 +73,16 @@ describe("Util", () => {
|
|||
},
|
||||
};
|
||||
describe("HandleHelpCommand", () => {
|
||||
it("handles empty commands", async () => {
|
||||
const {command, args} = Util.MsgToArgs("!fox help", "!fox");
|
||||
const retStr = await Util.HandleHelpCommand(
|
||||
"!fox",
|
||||
{} as any,
|
||||
{} as any,
|
||||
args,
|
||||
);
|
||||
expect(retStr).to.equal("No commands found");
|
||||
});
|
||||
it("parses general help message", async () => {
|
||||
const {command, args} = Util.MsgToArgs("!fox help", "!fox");
|
||||
const retStr = await Util.HandleHelpCommand(
|
||||
|
@ -130,34 +127,30 @@ Fox goes floof!`);
|
|||
});
|
||||
describe("GetMxidFromName", () => {
|
||||
it("Finds a single member", async () => {
|
||||
const mockRooms = {
|
||||
"/rooms/abc/members": [
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@123:localhost",
|
||||
},
|
||||
],
|
||||
};
|
||||
const intent = CreateMockIntent(mockRooms);
|
||||
const mockUsers = [
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@123:localhost",
|
||||
},
|
||||
];
|
||||
const intent = CreateMockIntent(mockUsers);
|
||||
const mxid = await Util.GetMxidFromName(intent, "goodboy", ["abc"]);
|
||||
expect(mxid).equal("@123:localhost");
|
||||
});
|
||||
it("Errors on multiple members", async () => {
|
||||
const mockRooms = {
|
||||
"/rooms/abc/members": [
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@123:localhost",
|
||||
},
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@456:localhost",
|
||||
},
|
||||
],
|
||||
};
|
||||
const mockRooms = [
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@123:localhost",
|
||||
},
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@456:localhost",
|
||||
},
|
||||
];
|
||||
const intent = CreateMockIntent(mockRooms);
|
||||
try {
|
||||
await Util.GetMxidFromName(intent, "goodboy", ["abc"]);
|
||||
|
@ -167,15 +160,13 @@ Fox goes floof!`);
|
|||
}
|
||||
});
|
||||
it("Errors on no member", async () => {
|
||||
const mockRooms = {
|
||||
"/rooms/abc/members": [
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@123:localhost",
|
||||
},
|
||||
],
|
||||
};
|
||||
const mockRooms = [
|
||||
{
|
||||
displayname: "GoodBoy",
|
||||
membership: "join",
|
||||
mxid: "@123:localhost",
|
||||
},
|
||||
];
|
||||
const intent = CreateMockIntent(mockRooms);
|
||||
try {
|
||||
await Util.GetMxidFromName(intent, "badboy", ["abc"]);
|
||||
|
@ -236,7 +227,7 @@ Fox goes floof!`);
|
|||
it("should deny", async () => {
|
||||
const ret = await Util.CheckMatrixPermission(
|
||||
{
|
||||
getStateEvent: async () => {
|
||||
getRoomStateEvent: async () => {
|
||||
return {
|
||||
blah: {
|
||||
blubb: PERM_LEVEL,
|
||||
|
@ -255,7 +246,7 @@ Fox goes floof!`);
|
|||
it("should allow cat/subcat", async () => {
|
||||
const ret = await Util.CheckMatrixPermission(
|
||||
{
|
||||
getStateEvent: async () => {
|
||||
getRoomStateEvent: async () => {
|
||||
return {
|
||||
blah: {
|
||||
blubb: PERM_LEVEL,
|
||||
|
@ -277,7 +268,7 @@ Fox goes floof!`);
|
|||
it("should allow cat", async () => {
|
||||
const ret = await Util.CheckMatrixPermission(
|
||||
{
|
||||
getStateEvent: async () => {
|
||||
getRoomStateEvent: async () => {
|
||||
return {
|
||||
blah: PERM_LEVEL,
|
||||
users: {
|
||||
|
@ -296,7 +287,7 @@ Fox goes floof!`);
|
|||
it("should allow based on default", async () => {
|
||||
const ret = await Util.CheckMatrixPermission(
|
||||
{
|
||||
getStateEvent: async () => {
|
||||
getRoomStateEvent: async () => {
|
||||
return {
|
||||
blah: PERM_LEVEL,
|
||||
users_default: PERM_LEVEL,
|
||||
|
@ -311,4 +302,18 @@ Fox goes floof!`);
|
|||
expect(ret).to.be.true;
|
||||
});
|
||||
});
|
||||
describe("EscapeStringForUserId", () => {
|
||||
it("should encode a string properly", () => {
|
||||
expect(Util.EscapeStringForUserId("ThisIsAString")).to
|
||||
.equal("=54his=49s=41=53tring");
|
||||
expect(Util.EscapeStringForUserId('1!2"3£4$5%6^7&8*9(0)')).to
|
||||
.equal("1=212=223=a34=245=256=5e7=268=2a9=280=29");
|
||||
});
|
||||
it("should not-reencode a string", () => {
|
||||
expect(Util.EscapeStringForUserId("=54his=49s=41=53tring")).to
|
||||
.equal("=54his=49s=41=53tring");
|
||||
expect(Util.EscapeStringForUserId("1=212=223=a34=245=256=5e7=268=2a9=280=29")).to
|
||||
.equal("1=212=223=a34=245=256=5e7=268=2a9=280=29");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,15 +19,11 @@ limitations under the License.
|
|||
* Allows you to become an admin for a room the bot is in control of.
|
||||
*/
|
||||
|
||||
import { AppServiceRegistration, ClientFactory, Bridge } from "matrix-appservice-bridge";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
import * as args from "command-line-args";
|
||||
import * as usage from "command-line-usage";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { Log } from "../src/log";
|
||||
import { Util } from "../src/util";
|
||||
import { DiscordStore } from "../src/store";
|
||||
import { ToolsHelper } from "./toolshelper";
|
||||
const log = new Log("AddRoomsToDirectory");
|
||||
const optionDefinitions = [
|
||||
{
|
||||
|
@ -45,11 +41,12 @@ const optionDefinitions = [
|
|||
typeLabel: "<config.yaml>",
|
||||
},
|
||||
{
|
||||
alias: "s",
|
||||
defaultValue: "room-store.db",
|
||||
description: "The location of the room store.",
|
||||
name: "store",
|
||||
alias: "r",
|
||||
defaultValue: "discord-registration.yaml",
|
||||
description: "The AS registration file.",
|
||||
name: "registration",
|
||||
type: String,
|
||||
typeLabel: "<discord-registration.yaml>",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -69,50 +66,26 @@ if (options.help) {
|
|||
]));
|
||||
process.exit(0);
|
||||
}
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
|
||||
const registration = AppServiceRegistration.fromObject(yamlConfig);
|
||||
const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig;
|
||||
|
||||
if (registration === null) {
|
||||
throw new Error("Failed to parse registration file");
|
||||
}
|
||||
|
||||
const clientFactory = new ClientFactory({
|
||||
appServiceUserId: `@${registration.sender_localpart}:${config.bridge.domain}`,
|
||||
token: registration.as_token,
|
||||
url: config.bridge.homeserverUrl,
|
||||
});
|
||||
|
||||
const bridge = new Bridge({
|
||||
controller: {
|
||||
onEvent: () => { },
|
||||
},
|
||||
domain: "rubbish",
|
||||
homeserverUrl: true,
|
||||
registration: true,
|
||||
});
|
||||
|
||||
const discordstore = new DiscordStore(config.database ? config.database.filename : "discord.db");
|
||||
const {store, appservice} = ToolsHelper.getToolDependencies(options.config, options.registration);
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await discordstore.init();
|
||||
await store!.init();
|
||||
} catch (e) {
|
||||
log.error(`Failed to load database`, e);
|
||||
}
|
||||
let rooms = await discordstore.roomStore.getEntriesByRemoteRoomData({
|
||||
let rooms = await store!.roomStore.getEntriesByRemoteRoomData({
|
||||
discord_type: "text",
|
||||
});
|
||||
rooms = rooms.filter((r) => r.remote && r.remote.get("plumbed") !== true );
|
||||
const client = clientFactory.getClientAs();
|
||||
log.info(`Got ${rooms.length} rooms to set`);
|
||||
try {
|
||||
await Util.AsyncForEach(rooms, async (room) => {
|
||||
const guild = room.remote.get("discord_guild");
|
||||
const roomId = room.matrix.getId();
|
||||
const guild = room.remote!.get("discord_guild");
|
||||
const roomId = room.matrix!.getId();
|
||||
try {
|
||||
await client.setRoomDirectoryVisibilityAppService(
|
||||
guild,
|
||||
await appservice.botIntent.underlyingClient.setDirectoryVisibility(
|
||||
roomId,
|
||||
"public",
|
||||
);
|
||||
|
|
|
@ -20,12 +20,48 @@ limitations under the License.
|
|||
*/
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
import * as args from "command-line-args";
|
||||
import * as usage from "command-line-usage";
|
||||
import { Util } from "../src/util";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync("config.yaml", "utf8"));
|
||||
if (yamlConfig === null) {
|
||||
console.error("You have an error in your discord config.");
|
||||
const optionDefinitions = [
|
||||
{
|
||||
alias: "h",
|
||||
description: "Display this usage guide.",
|
||||
name: "help",
|
||||
type: Boolean,
|
||||
},
|
||||
{
|
||||
alias: "c",
|
||||
defaultValue: "config.yaml",
|
||||
description: "The AS config file.",
|
||||
name: "config",
|
||||
type: String,
|
||||
typeLabel: "<config.yaml>",
|
||||
},
|
||||
];
|
||||
|
||||
const options = args(optionDefinitions);
|
||||
|
||||
if (options.help) {
|
||||
/* tslint:disable:no-console */
|
||||
console.log(usage([
|
||||
{
|
||||
content: "A tool to obtain the Discord bot invitation URL.",
|
||||
header: "Add bot",
|
||||
},
|
||||
{
|
||||
header: "Options",
|
||||
optionList: optionDefinitions,
|
||||
},
|
||||
]));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const url = Util.GetBotLink(yamlConfig);
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8"));
|
||||
if (yamlConfig === null || typeof yamlConfig !== "object") {
|
||||
throw Error("You have an error in your discord config.");
|
||||
}
|
||||
const url = Util.GetBotLink(yamlConfig as DiscordBridgeConfig);
|
||||
console.log(`Go to ${url} to invite the bot into a guild.`);
|
||||
|
|
|
@ -16,15 +16,12 @@ limitations under the License.
|
|||
|
||||
/* tslint:disable:no-console */
|
||||
/**
|
||||
* Allows you to become an admin for a room the bot is in control of.
|
||||
* Allows you to become an admin for a room that the bot is in control of.
|
||||
*/
|
||||
|
||||
import { AppServiceRegistration, ClientFactory, Intent } from "matrix-appservice-bridge";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
import * as args from "command-line-args";
|
||||
import * as usage from "command-line-usage";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { ToolsHelper } from "./toolshelper";
|
||||
|
||||
const optionDefinitions = [
|
||||
{
|
||||
|
@ -43,6 +40,14 @@ const optionDefinitions = [
|
|||
},
|
||||
{
|
||||
alias: "r",
|
||||
defaultValue: "discord-registration.yaml",
|
||||
description: "The AS registration file.",
|
||||
name: "registration",
|
||||
type: String,
|
||||
typeLabel: "<discord-registration.yaml>",
|
||||
},
|
||||
{
|
||||
alias: "m",
|
||||
description: "The roomid to modify",
|
||||
name: "roomid",
|
||||
type: String,
|
||||
|
@ -90,25 +95,18 @@ if (!options.userid) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync("discord-registration.yaml", "utf8"));
|
||||
const registration = AppServiceRegistration.fromObject(yamlConfig);
|
||||
const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig;
|
||||
|
||||
if (registration === null) {
|
||||
throw new Error("Failed to parse registration file");
|
||||
}
|
||||
|
||||
const clientFactory = new ClientFactory({
|
||||
appServiceUserId: `@${registration.sender_localpart}:${config.bridge.domain}`,
|
||||
token: registration.as_token,
|
||||
url: config.bridge.homeserverUrl,
|
||||
});
|
||||
const client = clientFactory.getClientAs();
|
||||
const intent = new Intent(client, client, {registered: true});
|
||||
const {appservice} = ToolsHelper.getToolDependencies(options.config, options.reg, false);
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await intent.setPowerLevel(options.roomid, options.userid, options.power);
|
||||
const powerLevels = (await appservice.botIntent.underlyingClient.getRoomStateEvent(
|
||||
options.roomid, "m.room.power_levels", "",
|
||||
));
|
||||
powerLevels.users[options.userid] = options.power;
|
||||
|
||||
await appservice.botIntent.underlyingClient.sendStateEvent(
|
||||
options.roomid, "m.room.power_levels", "", powerLevels,
|
||||
);
|
||||
console.log("Power levels set");
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
|
|
|
@ -14,18 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppServiceRegistration, ClientFactory, Bridge, Intent } from "matrix-appservice-bridge";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
import * as args from "command-line-args";
|
||||
import * as usage from "command-line-usage";
|
||||
import { ChannelSyncroniser } from "../src/channelsyncroniser";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { DiscordStore } from "../src/store";
|
||||
import { Provisioner } from "../src/provisioner";
|
||||
import { Log } from "../src/log";
|
||||
import { Util } from "../src/util";
|
||||
import { ToolsHelper } from "./toolshelper";
|
||||
|
||||
const log = new Log("ChanFix");
|
||||
|
||||
|
@ -44,6 +38,14 @@ const optionDefinitions = [
|
|||
type: String,
|
||||
typeLabel: "<config.yaml>",
|
||||
},
|
||||
{
|
||||
alias: "r",
|
||||
defaultValue: "discord-registration.yaml",
|
||||
description: "The AS registration file.",
|
||||
name: "registration",
|
||||
type: String,
|
||||
typeLabel: "<discord-registration.yaml>",
|
||||
},
|
||||
];
|
||||
|
||||
const options = args(optionDefinitions);
|
||||
|
@ -64,70 +66,31 @@ if (options.help) {
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
|
||||
const registration = AppServiceRegistration.fromObject(yamlConfig);
|
||||
const config = new DiscordBridgeConfig();
|
||||
config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
|
||||
config.applyEnvironmentOverrides(process.env);
|
||||
|
||||
if (registration === null) {
|
||||
throw new Error("Failed to parse registration file");
|
||||
}
|
||||
|
||||
const botUserId = `@${registration.sender_localpart}:${config.bridge.domain}`;
|
||||
const clientFactory = new ClientFactory({
|
||||
appServiceUserId: botUserId,
|
||||
token: registration.as_token,
|
||||
url: config.bridge.homeserverUrl,
|
||||
});
|
||||
|
||||
const bridge = new Bridge({
|
||||
clientFactory,
|
||||
controller: {
|
||||
onEvent: () => { },
|
||||
},
|
||||
domain: config.bridge.domain,
|
||||
homeserverUrl: config.bridge.homeserverUrl,
|
||||
intentOptions: {
|
||||
clients: {
|
||||
dontJoin: true, // handled manually
|
||||
},
|
||||
},
|
||||
registration,
|
||||
roomStore: config.database.roomStorePath,
|
||||
userStore: config.database.userStorePath,
|
||||
});
|
||||
|
||||
async function run() {
|
||||
await bridge.loadDatabases();
|
||||
const store = new DiscordStore(config.database);
|
||||
await store.init(undefined, bridge.getRoomStore());
|
||||
const discordbot = new DiscordBot(botUserId, config, bridge, store);
|
||||
const {store, appservice, config} = ToolsHelper.getToolDependencies(options.config);
|
||||
await store!.init();
|
||||
const discordbot = new DiscordBot(config, appservice, store!);
|
||||
await discordbot.init();
|
||||
|
||||
bridge._clientFactory = clientFactory;
|
||||
bridge._botClient = bridge._clientFactory.getClientAs();
|
||||
bridge._botIntent = new Intent(bridge._botClient, bridge._botClient, { registered: true });
|
||||
await discordbot.ClientFactory.init();
|
||||
const client = await discordbot.ClientFactory.getClient();
|
||||
|
||||
// first set update_icon to true if needed
|
||||
const mxRoomEntries = await bridge.getRoomStore().getEntriesByRemoteRoomData({
|
||||
const mxRoomEntries = await store!.roomStore.getEntriesByRemoteRoomData({
|
||||
update_name: true,
|
||||
update_topic: true,
|
||||
});
|
||||
|
||||
const promiseList: Promise<void>[] = [];
|
||||
mxRoomEntries.forEach((entry) => {
|
||||
if (entry.remote.get("plumbed")) {
|
||||
if (entry.remote!.get("plumbed")) {
|
||||
return; // skipping plumbed rooms
|
||||
}
|
||||
const updateIcon = entry.remote.get("update_icon");
|
||||
const updateIcon = entry.remote!.get("update_icon");
|
||||
if (updateIcon !== undefined && updateIcon !== null) {
|
||||
return; // skipping because something was set manually
|
||||
}
|
||||
entry.remote.set("update_icon", true);
|
||||
promiseList.push(bridge.getRoomStore().upsertEntry(entry));
|
||||
entry.remote!.set("update_icon", true);
|
||||
promiseList.push(store!.roomStore.upsertEntry(entry));
|
||||
});
|
||||
await Promise.all(promiseList);
|
||||
|
||||
|
@ -135,7 +98,7 @@ async function run() {
|
|||
const promiseList2: Promise<void>[] = [];
|
||||
|
||||
let curDelay = config.limits.roomGhostJoinDelay; // we'll just re-use this
|
||||
client.guilds.forEach((guild) => {
|
||||
client.guilds.cache.forEach((guild) => {
|
||||
promiseList2.push((async () => {
|
||||
await Util.DelayedPromise(curDelay);
|
||||
try {
|
||||
|
|
|
@ -14,16 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppServiceRegistration, ClientFactory, Bridge } from "matrix-appservice-bridge";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
import * as args from "command-line-args";
|
||||
import * as usage from "command-line-usage";
|
||||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { Log } from "../src/log";
|
||||
import { Util } from "../src/util";
|
||||
import { DiscordBot } from "../src/bot";
|
||||
import { DiscordStore } from "../src/store";
|
||||
import { ToolsHelper } from "./toolshelper";
|
||||
|
||||
const log = new Log("GhostFix");
|
||||
|
||||
|
@ -53,6 +49,14 @@ const optionDefinitions = [
|
|||
type: String,
|
||||
typeLabel: "<config.yaml>",
|
||||
},
|
||||
{
|
||||
alias: "r",
|
||||
defaultValue: "discord-registration.yaml",
|
||||
description: "The AS registration file.",
|
||||
name: "registration",
|
||||
type: String,
|
||||
typeLabel: "<discord-registration.yaml>",
|
||||
},
|
||||
];
|
||||
|
||||
const options = args(optionDefinitions);
|
||||
|
@ -73,55 +77,19 @@ if (options.help) {
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
|
||||
const registration = AppServiceRegistration.fromObject(yamlConfig);
|
||||
const config = new DiscordBridgeConfig();
|
||||
config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
|
||||
config.applyEnvironmentOverrides(process.env);
|
||||
|
||||
if (registration === null) {
|
||||
throw new Error("Failed to parse registration file");
|
||||
}
|
||||
|
||||
const botUserId = `@${registration.sender_localpart}:${config.bridge.domain}`;
|
||||
const clientFactory = new ClientFactory({
|
||||
appServiceUserId: botUserId,
|
||||
token: registration.as_token,
|
||||
url: config.bridge.homeserverUrl,
|
||||
});
|
||||
|
||||
const bridge = new Bridge({
|
||||
clientFactory,
|
||||
controller: {
|
||||
onEvent: () => { },
|
||||
},
|
||||
domain: config.bridge.domain,
|
||||
homeserverUrl: config.bridge.homeserverUrl,
|
||||
intentOptions: {
|
||||
clients: {
|
||||
dontJoin: true, // handled manually
|
||||
},
|
||||
},
|
||||
registration,
|
||||
roomStore: config.database.roomStorePath,
|
||||
userStore: config.database.userStorePath,
|
||||
});
|
||||
|
||||
async function run() {
|
||||
await bridge.loadDatabases();
|
||||
const store = new DiscordStore(config.database);
|
||||
await store.init(undefined, bridge.getRoomStore());
|
||||
const discordbot = new DiscordBot(botUserId, config, bridge, store);
|
||||
const {store, appservice, config} = ToolsHelper.getToolDependencies(options.config);
|
||||
await store!.init();
|
||||
const discordbot = new DiscordBot(config, appservice, store!);
|
||||
await discordbot.init();
|
||||
bridge._clientFactory = clientFactory;
|
||||
const client = await discordbot.ClientFactory.getClient();
|
||||
|
||||
const promiseList: Promise<void>[] = [];
|
||||
let curDelay = config.limits.roomGhostJoinDelay;
|
||||
try {
|
||||
client.guilds.forEach((guild) => {
|
||||
guild.members.forEach((member) => {
|
||||
if (member.id === client.user.id) {
|
||||
client.guilds.cache.forEach((guild) => {
|
||||
guild.members.cache.forEach((member) => {
|
||||
if (member.id === client.user?.id) {
|
||||
return;
|
||||
}
|
||||
promiseList.push((async () => {
|
||||
|
|
38
tools/toolshelper.ts
Normal file
38
tools/toolshelper.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { DiscordBridgeConfig } from "../src/config";
|
||||
import { Appservice, IAppserviceRegistration } from "matrix-bot-sdk";
|
||||
import { DiscordStore } from "../src/store";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as fs from "fs";
|
||||
|
||||
export class ToolsHelper {
|
||||
public static getToolDependencies(
|
||||
configFile: string, regFile: string = "./discord-registration.yaml", needsStore: boolean = true): {
|
||||
store: DiscordStore|null,
|
||||
appservice: Appservice,
|
||||
config: DiscordBridgeConfig,
|
||||
} {
|
||||
const registration = yaml.safeLoad(fs.readFileSync(regFile, "utf8"));
|
||||
const config: DiscordBridgeConfig = Object.assign(
|
||||
new DiscordBridgeConfig(), yaml.safeLoad(fs.readFileSync(configFile, "utf8")));
|
||||
config.applyEnvironmentOverrides(process.env);
|
||||
if (registration === null || typeof registration !== "object") {
|
||||
throw Error("Failed to parse registration file");
|
||||
}
|
||||
|
||||
const appservice = new Appservice({
|
||||
bindAddress: "notathing",
|
||||
homeserverName: config.bridge.domain,
|
||||
homeserverUrl: config.bridge.homeserverUrl,
|
||||
port: 0,
|
||||
// We assume the registration is well formed
|
||||
registration: registration as IAppserviceRegistration,
|
||||
});
|
||||
|
||||
const store = needsStore ? new DiscordStore(config.database ? config.database.filename : "discord.db") : null;
|
||||
return {
|
||||
appservice,
|
||||
config,
|
||||
store,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -71,7 +71,7 @@ if (options.help || (options.add && options.remove) || !(options.add || options.
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8"));
|
||||
const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig;
|
||||
const discordstore = new DiscordStore(config.database ? config.database : "discord.db");
|
||||
discordstore.init().then(() => {
|
||||
log.info("Loaded database.");
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"compileOnSave": true,
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"test/**/*",
|
||||
"tools/**/*"
|
||||
"tools/**/*",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
"object-literal-sort-keys": "off",
|
||||
"no-any": true,
|
||||
"arrow-return-shorthand": true,
|
||||
"no-magic-numbers": [true, -1, 0, 1, 1000],
|
||||
"prefer-for-of": true,
|
||||
"typedef": {
|
||||
"severity": "warning"
|
||||
|
|
Reference in a new issue