Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/851.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add options to configure how Matrix users get displayed on Discord.
8 changes: 8 additions & 0 deletions config/config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions config/config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ properties:
type: "string"
usernamePattern:
type: "string"
discordProxy:
type: "object"
properties:
namePattern:
type: "string"
fallbackNamePattern:
type: "string"
metrics:
type: "object"
properties:
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 20 additions & 4 deletions src/matrixeventprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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}`,
);
Expand Down
122 changes: 95 additions & 27 deletions test/test_matrixeventprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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: {
Expand All @@ -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: {
Expand All @@ -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 () => {
Expand Down