diff --git a/config/config.sample.yaml b/config/config.sample.yaml index ed97ed42640ea79345159f5c5868bc810c68778b..10f6d2fb97e7f9ca24fff97f962683894245c11b 100644 --- a/config/config.sample.yaml +++ b/config/config.sample.yaml @@ -89,3 +89,8 @@ limits: # (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 +ghosts: + # Pattern for the ghosts nick, available is :nick, :username, :tag and :id + nickPattern: ":nick" + # Pattern for the ghosts username, available is :username, :tag and :id + usernamePattern: ":username#:tag" diff --git a/config/config.schema.yaml b/config/config.schema.yaml index 9a35f4b26cee167a0a94009d4614be1c70d127e9..7439ef2852c7af065ee69f2d91c8a476420b01da 100644 --- a/config/config.schema.yaml +++ b/config/config.schema.yaml @@ -109,3 +109,10 @@ properties: type: "boolean" ghostsLeave: type: "boolean" + ghosts: + type: "object" + properties: + nickPattern: + type: "string" + usernamePattern: + type: "string" diff --git a/src/channelsyncroniser.ts b/src/channelsyncroniser.ts index 8f76d9e15799f57ed29e63e61a9fd53878cce47d..c948f35da71df85004efa18ee4fbb802d0014d85 100644 --- a/src/channelsyncroniser.ts +++ b/src/channelsyncroniser.ts @@ -163,14 +163,10 @@ export class ChannelSyncroniser { return channelState; } - const patternMap = { + const name: string = Util.ApplyPatternString(this.config.channel.namePattern, { guild: channel.guild.name, name: "#" + channel.name, - }; - let name: string = this.config.channel.namePattern; - for (const p of Object.keys(patternMap)) { - name = name.replace(new RegExp(":" + p, "g"), patternMap[p]); - } + }); const topic = channel.topic; const icon = channel.guild.icon; let iconUrl: string | null = null; diff --git a/src/config.ts b/src/config.ts index 3fdde89e4e9d5626a23654952bbfe7957a37484f..637bc555fb28450a2b2feccb5eb584eaa2c114fe 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,7 @@ export class DiscordBridgeConfig { public room: DiscordBridgeConfigRoom = new DiscordBridgeConfigRoom(); public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel(); public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits(); + public ghosts: DiscordBridgeConfigGhosts = new DiscordBridgeConfigGhosts(); /** * Apply a set of keys and values over the default config. @@ -109,3 +110,8 @@ export class LoggingFile { public enabled: string[] = []; public disabled: string[] = []; } + +class DiscordBridgeConfigGhosts { + public nickPattern: string = ":nick"; + public usernamePattern: string = ":username#:tag"; +} diff --git a/src/usersyncroniser.ts b/src/usersyncroniser.ts index 3a329a3f1504f45ba8838181c17c817bfd135ead..86e144b3bb9d999128bf47c52408764b0fa8a956 100644 --- a/src/usersyncroniser.ts +++ b/src/usersyncroniser.ts @@ -234,7 +234,11 @@ export class UserSyncroniser { id: discordUser.id, mxUserId: `@_discord_${discordUser.id}${mxidExtra}:${this.config.bridge.domain}`, }); - const displayName = this.displayNameForUser(discordUser); + const displayName = Util.ApplyPatternString(this.config.ghosts.usernamePattern, { + id: discordUser.id, + tag: discordUser.discriminator, + username: discordUser.username, + }); // Determine if the user exists. const remoteId = discordUser.id + mxidExtra; const remoteUser = await this.userStore.getRemoteUser(remoteId); @@ -270,10 +274,16 @@ export class UserSyncroniser { public async GetUserStateForGuildMember( newMember: GuildMember, ): Promise<IGuildMemberState> { + const name = Util.ApplyPatternString(this.config.ghosts.nickPattern, { + id: newMember.user.id, + nick: newMember.displayName, + tag: newMember.user.discriminator, + username: newMember.user.username, + }); const guildState: IGuildMemberState = Object.assign({}, DEFAULT_GUILD_STATE, { bot: newMember.user.bot, displayColor: newMember.displayColor, - displayName: newMember.displayName, + displayName: name, id: newMember.id, mxUserId: `@_discord_${newMember.id}:${this.config.bridge.domain}`, roles: newMember.roles.map((role) => { return { @@ -394,10 +404,6 @@ export class UserSyncroniser { }); } - private displayNameForUser(discordUser): string { - return `${discordUser.username}#${discordUser.discriminator}`; - } - private async leave(intent: Intent, roomId: string, checkCache: boolean = true) { const userId = intent.getClient().getUserId(); if (checkCache && ![null, "join", "invite"] diff --git a/src/util.ts b/src/util.ts index 8b606bbf40b61d67562c244414ab7c42a7265b8f..aca4d651a2eb9780ff8d1a5666b389799edb10db 100644 --- a/src/util.ts +++ b/src/util.ts @@ -47,6 +47,10 @@ export interface ICommandParameters { [index: string]: ICommandParameter; } +export interface IPatternMap { + [index: string]: string; +} + export class Util { /** * downloadFile - This function will take a URL and store the resulting data into @@ -274,6 +278,13 @@ export class Util { const htmlColor = pad.substring(0, pad.length - colorHex.length) + colorHex; return htmlColor; } + + public static ApplyPatternString(str: string, patternMap: IPatternMap): string { + for (const p of Object.keys(patternMap)) { + str = str.replace(new RegExp(":" + p, "g"), patternMap[p]); + } + return str; + } } interface IUploadResult { diff --git a/test/test_channelsyncroniser.ts b/test/test_channelsyncroniser.ts index c54aa2f1cd7e4ce0e728aa33428452351f95f691..7d90f6e827c782c1ca6f4ce11d618af0dd6ed124 100644 --- a/test/test_channelsyncroniser.ts +++ b/test/test_channelsyncroniser.ts @@ -24,6 +24,7 @@ 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"; // we are a test file and thus need those @@ -44,6 +45,7 @@ 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"}; diff --git a/test/test_usersyncroniser.ts b/test/test_usersyncroniser.ts index c5b7b1cff0418e3f72494acf3194652745255b53..531f3e86e1ded68fb2848882d750c9e195f128b9 100644 --- a/test/test_usersyncroniser.ts +++ b/test/test_usersyncroniser.ts @@ -55,6 +55,7 @@ const GUILD_ROOM_IDS_WITH_ROLE = ["!abc:localhost", "!def:localhost"]; const UserSync = (Proxyquire("../src/usersyncroniser", { "./util": { Util: { + ApplyPatternString: Util.ApplyPatternString, AsyncForEach: Util.AsyncForEach, UploadContentFromUrl: async () => { UTIL_UPLOADED_AVATAR = true; @@ -64,7 +65,7 @@ const UserSync = (Proxyquire("../src/usersyncroniser", { }, })).UserSyncroniser; -function CreateUserSync(remoteUsers: RemoteUser[] = []): UserSyncroniser { +function CreateUserSync(remoteUsers: RemoteUser[] = [], ghostConfig: any = {}): UserSyncroniser { UTIL_UPLOADED_AVATAR = false; SEV_ROOM_ID = null; SEV_CONTENT = null; @@ -153,6 +154,7 @@ function CreateUserSync(remoteUsers: RemoteUser[] = []): UserSyncroniser { }; 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); } @@ -196,6 +198,27 @@ describe("UserSyncroniser", () => { expect(state.avatarId, "AvatarID").is.empty; expect(state.avatarUrl, "AvatarUrl").is.null; }); + it("Will obay name patterns", async () => { + const remoteUser = new RemoteUser("123456"); + remoteUser.avatarurl = "test.jpg"; + remoteUser.displayname = "TestUsername"; + + const userSync = CreateUserSync([remoteUser], {usernamePattern: ":username#:tag (Discord)"}); + const user = new MockUser( + "123456", + "TestUsername", + "6969", + "test.jpg", + "111", + ); + const state = await userSync.GetUserUpdateState(user as any); + expect(state.createUser, "CreateUser").is.false; + expect(state.removeAvatar, "RemoveAvatar").is.false; + expect(state.displayName, "DisplayName").equals("TestUsername#6969 (Discord)"); + expect(state.mxUserId , "UserId").equals("@_discord_123456:localhost"); + expect(state.avatarId, "AvatarID").is.empty; + expect(state.avatarUrl, "AvatarUrl").is.null; + }); it("Will change avatars", async () => { const remoteUser = new RemoteUser("123456"); remoteUser.avatarurl = "test.jpg"; @@ -467,6 +490,18 @@ describe("UserSyncroniser", () => { const state = await userSync.GetUserStateForGuildMember(member as any); expect(state.displayName).to.be.equal("BestDog"); }); + it("Will will obay nick pattern", async () => { + const userSync = CreateUserSync([new RemoteUser("123456")], { nickPattern: ":nick (Discord)" }); + const guild = new MockGuild( + "654321"); + const member = new MockMember( + "123456", + "username", + guild, + "BestDog"); + const state = await userSync.GetUserStateForGuildMember(member as any); + expect(state.displayName).to.be.equal("BestDog (Discord)"); + }); it("Will correctly add roles", async () => { const userSync = CreateUserSync([new RemoteUser("123456")]); const guild = new MockGuild( diff --git a/test/test_util.ts b/test/test_util.ts index 3abf37a66dcc22ac4bee0d26631c08c63b5a764d..88380369b0ce0c8b620be061e30aa155aba227b4 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -158,4 +158,25 @@ describe("Util", () => { expect(reply).to.equal("#000000"); }); }); + describe("ApplyPatternString", () => { + it("Should apply simple patterns", () => { + const reply = Util.ApplyPatternString(":name likes :animal", { + animal: "Foxies", + name: "Sorunome", + }); + expect(reply).to.equal("Sorunome likes Foxies"); + }); + it("Should ignore unused tags", () => { + const reply = Util.ApplyPatternString(":name is :thing", { + name: "Sorunome", + }); + expect(reply).to.equal("Sorunome is :thing"); + }); + it("Should do multi-replacements", () => { + const reply = Util.ApplyPatternString(":animal, :animal and :animal", { + animal: "fox", + }); + expect(reply).to.equal("fox, fox and fox"); + }); + }); });