diff --git a/changelog.d/851.feature b/changelog.d/851.feature new file mode 100644 index 00000000..8b538846 --- /dev/null +++ b/changelog.d/851.feature @@ -0,0 +1 @@ +Add options to configure how Matrix users get displayed on Discord. diff --git a/config/config.sample.yaml b/config/config.sample.yaml index 3443e4af..bac552f5 100644 --- a/config/config.sample.yaml +++ b/config/config.sample.yaml @@ -115,6 +115,14 @@ ghosts: nickPattern: ":nick" # Pattern for the ghosts username, available is :username, :tag and :id usernamePattern: ":username#:tag" +discordProxy: + # Pattern for the Matrix sender name in Discord messages. + # Available is :nick, :username, :displayname (the latter is the users nickname if they have one, and the username if they don't). + namePattern: ":displayname" + # Fallback name pattern, if "namePattern" would be too long to set as a name. + # If this also ends up being too long, it's truncated. + # Available is :nick, :username, :displayname (the latter is the users nickname if they have one, and the username if they don't). + fallbackNamePattern: ":username" # Prometheus-compatible metrics endpoint metrics: enable: false diff --git a/config/config.schema.yaml b/config/config.schema.yaml index eb320ac0..091576d4 100644 --- a/config/config.schema.yaml +++ b/config/config.schema.yaml @@ -140,6 +140,13 @@ properties: type: "string" usernamePattern: type: "string" + discordProxy: + type: "object" + properties: + namePattern: + type: "string" + fallbackNamePattern: + type: "string" metrics: type: "object" properties: diff --git a/src/config.ts b/src/config.ts index b9362fa9..4d11d5f9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,6 +30,7 @@ export class DiscordBridgeConfig { public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel(); public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits(); public ghosts: DiscordBridgeConfigGhosts = new DiscordBridgeConfigGhosts(); + public discordProxy: DiscordBridgeConfigDiscordProxy = new DiscordBridgeConfigDiscordProxy(); public metrics: DiscordBridgeConfigMetrics = new DiscordBridgeConfigMetrics(); /** @@ -166,6 +167,11 @@ class DiscordBridgeConfigGhosts { public usernamePattern: string = ":username#:tag"; } +class DiscordBridgeConfigDiscordProxy { + public namePattern: string = ":displayname"; + public fallbackNamePattern: string = ":username"; +} + export class DiscordBridgeConfigMetrics { public enable: boolean = false; public port: number = 9001; diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index f1f46115..74e8d2b2 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -514,10 +514,10 @@ export class MatrixEventProcessor { // Let it fall through. } + let profileName = ""; if (profile) { - if (profile.displayname && - profile.displayname.length >= MIN_NAME_LENGTH && - profile.displayname.length <= MAX_NAME_LENGTH) { + if (profile.displayname) { + profileName = profile.displayname; displayName = profile.displayname; } @@ -530,8 +530,24 @@ export class MatrixEventProcessor { ); } } + + // Try to set the display name from the pattern; if it doesn't turn out to be too long: + let author = Util.ApplyPatternString(this.config.discordProxy.namePattern, { + nick: profileName, + displayname: displayName, + username: sender, + }); + if (author.length >= MAX_NAME_LENGTH) { + // Otherwise fall back to fallback name pattern and truncate. + author = Util.ApplyPatternString(this.config.discordProxy.fallbackNamePattern, { + nick: profileName, + displayname: displayName, + username: sender, + }).substring(0, MAX_NAME_LENGTH) + } + embed.setAuthor( - displayName.substring(0, MAX_NAME_LENGTH), + author, avatarUrl, `https://matrix.to/#/${sender}`, ); diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index 6e4cedfc..532ba84d 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -154,7 +154,7 @@ let KICKBAN_HANDLED = false; let MESSAGE_SENT = false; let MESSAGE_EDITED = false; -function createMatrixEventProcessor(storeMockResults = 0, configBridge = new DiscordBridgeConfigBridge()) { +function createMatrixEventProcessor(storeMockResults = 0, bridgeConfig = new DiscordBridgeConfig()) { STATE_EVENT_MSG = ""; MESSAGE_PROCCESS = ""; KICKBAN_HANDLED = false; @@ -172,8 +172,7 @@ function createMatrixEventProcessor(storeMockResults = 0, configBridge = new Dis OnMemberState: async () => { }, OnUpdateUser: async () => { }, }; - const config = new DiscordBridgeConfig(); - config.bridge = configBridge; + const config = bridgeConfig; const store = { Get: (a, b) => { @@ -356,9 +355,9 @@ describe("MatrixEventProcessor", () => { expect(STATE_EVENT_MSG).to.equal("`@user:localhost` set the topic to `Test Topic` on Matrix."); }); it("Should not echo topic changes", async () => { - const bridge = new DiscordBridgeConfigBridge(); - bridge.disableRoomTopicNotifications = true; - const {processor} = createMatrixEventProcessor(0, bridge); + const config = new DiscordBridgeConfig(); + config.bridge.disableRoomTopicNotifications = true; + const {processor} = createMatrixEventProcessor(0, config); const event = { content: { topic: "Test Topic", @@ -382,9 +381,9 @@ describe("MatrixEventProcessor", () => { expect(STATE_EVENT_MSG).to.equal("`@user:localhost` joined the room on Matrix."); }); it("Should not echo joins", async () => { - const bridge = new DiscordBridgeConfigBridge(); - bridge.disableJoinLeaveNotifications = true; - const {processor} = createMatrixEventProcessor(0, bridge); + const config = new DiscordBridgeConfig(); + config.bridge.disableJoinLeaveNotifications = true; + const {processor} = createMatrixEventProcessor(0, config); const event = { content: { membership: "join", @@ -410,9 +409,9 @@ describe("MatrixEventProcessor", () => { expect(STATE_EVENT_MSG).to.equal("`@user:localhost` invited `@user2:localhost` to the room on Matrix."); }); it("Should not echo invites", async () => { - const bridge = new DiscordBridgeConfigBridge(); - bridge.disableInviteNotifications = true; - const {processor} = createMatrixEventProcessor(0, bridge); + const config = new DiscordBridgeConfig(); + config.bridge.disableInviteNotifications = true; + const {processor} = createMatrixEventProcessor(0, config); const event = { content: { membership: "invite", @@ -452,9 +451,9 @@ describe("MatrixEventProcessor", () => { expect(STATE_EVENT_MSG).to.equal("`@user:localhost` left the room on Matrix."); }); it("Should not echo leaves", async () => { - const bridge = new DiscordBridgeConfigBridge(); - bridge.disableJoinLeaveNotifications = true; - const {processor} = createMatrixEventProcessor(0, bridge); + const config = new DiscordBridgeConfig(); + config.bridge.disableJoinLeaveNotifications = true; + const {processor} = createMatrixEventProcessor(0, config); const event = { content: { membership: "leave", @@ -496,7 +495,7 @@ describe("MatrixEventProcessor", () => { expect(author!.url).to.equal("https://matrix.to/#/@test:localhost"); }); - it("Should contain the users displayname if it exists.", async () => { + it("Should (by default) contain the users displayname if it exists.", async () => { const {processor} = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ content: { @@ -510,7 +509,7 @@ describe("MatrixEventProcessor", () => { expect(author!.url).to.equal("https://matrix.to/#/@test:localhost"); }); - it("Should contain the users userid if the displayname is not set", async () => { + it("Should (by default) contain the users userid if the displayname is not set", async () => { const {processor} = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ content: { @@ -524,40 +523,109 @@ describe("MatrixEventProcessor", () => { expect(author!.url).to.equal("https://matrix.to/#/@test_nonexistant:localhost"); }); - it("Should use the userid when the displayname is too short", async () => { + it("Should (by default) use the userid when displayname is too long", async () => { const {processor} = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ content: { body: "testcontent", }, - sender: "@test_short:localhost", + sender: "@test_long:localhost", } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; - expect(author!.name).to.equal("@test_short:localhost"); + expect(author!.name).to.equal("@test_long:localhost"); }); - it("Should use the userid when displayname is too long", async () => { + it("Should (by default) cap the sender name if it is too long", async () => { const {processor} = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ content: { body: "testcontent", }, - sender: "@test_long:localhost", + sender: "@testwithalottosayaboutitselfthatwillgoonandonandonandon:localhost", } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; - expect(author!.name).to.equal("@test_long:localhost"); + expect(author!.name).to.equal("@testwithalottosayaboutitselftha"); }); - it("Should cap the sender name if it is too long", async () => { - const {processor} = createMatrixEventProcessor(); + it("Should use the namePattern for the sender name.", async () => { + const config = new DiscordBridgeConfig(); + config.discordProxy.namePattern = ":nick -- :username"; + + const {processor} = createMatrixEventProcessor(0, config); + const embeds = await processor.EventToEmbed({ content: { body: "testcontent", }, - sender: "@testwithalottosayaboutitselfthatwillgoonandonandonandon:localhost", + sender: "@test:localhost", } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; - expect(author!.name).to.equal("@testwithalottosayaboutitselftha"); + expect(author!.name).to.equal("Test User -- @test:localhost"); + }); + + it("Should use the truncated fallbackNamePattern for the sender name if namePattern would be too long.", async () => { + const config = new DiscordBridgeConfig(); + config.discordProxy.namePattern = ":nick -------------- :username"; + config.discordProxy.fallbackNamePattern = "fallback :nick :username."; + + const {processor} = createMatrixEventProcessor(0, config); + + const embeds = await processor.EventToEmbed({ + content: { + body: "testcontent", + }, + sender: "@test:localhost", + } as IMatrixEvent, mockChannel as any); + const author = embeds.messageEmbed.author; + expect(author!.name).to.equal("fallback Test User @test:localho"); + }); + + it("Should use the empty string for the sender name pattern :nick if no display name is defined", async () => { + const config = new DiscordBridgeConfig(); + config.discordProxy.namePattern = ":nick -- :username"; + + const {processor} = createMatrixEventProcessor(0, config); + + const embeds = await processor.EventToEmbed({ + content: { + body: "testcontent", + }, + sender: "@test_nonexistant:localhost", + } as IMatrixEvent, mockChannel as any); + const author = embeds.messageEmbed.author; + expect(author!.name).to.equal(" -- @test_nonexistant:localhost"); + }); + + it("Should use the display name for :displayname in the sender name if the user has a display name set", async () => { + const config = new DiscordBridgeConfig(); + config.discordProxy.namePattern = ":displayname"; + + const {processor} = createMatrixEventProcessor(0, config); + + const embeds = await processor.EventToEmbed({ + content: { + body: "testcontent", + }, + sender: "@test:localhost", + } as IMatrixEvent, mockChannel as any); + const author = embeds.messageEmbed.author; + expect(author!.name).to.equal("Test User"); + }); + + it("Should use the user name for :displayname in the sender name if the user does not have a display name set", async () => { + const config = new DiscordBridgeConfig(); + config.discordProxy.namePattern = ":displayname"; + + const {processor} = createMatrixEventProcessor(0, config); + + const embeds = await processor.EventToEmbed({ + content: { + body: "testcontent", + }, + sender: "@test_nonexistant:localhost", + } as IMatrixEvent, mockChannel as any); + const author = embeds.messageEmbed.author; + expect(author!.name).to.equal("@test_nonexistant:localhost"); }); it("Should contain the users avatar if it exists.", async () => {