diff --git a/Dockerfile b/Dockerfile
index 4adf3274..252b4dd4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,6 +42,7 @@ RUN yarn --network-timeout 600000 --production --pure-lockfile && yarn cache cle
COPY --from=builder /src/lib ./
COPY --from=builder /src/public ./public
+COPY --from=builder /src/assets ./assets
VOLUME /data
EXPOSE 9993
diff --git a/assets/feeds_avatar.png b/assets/feeds_avatar.png
new file mode 100644
index 00000000..64350aed
Binary files /dev/null and b/assets/feeds_avatar.png differ
diff --git a/assets/figma_avatar.png b/assets/figma_avatar.png
new file mode 100644
index 00000000..8dc64435
Binary files /dev/null and b/assets/figma_avatar.png differ
diff --git a/assets/github_avatar.png b/assets/github_avatar.png
new file mode 100644
index 00000000..3da737b3
Binary files /dev/null and b/assets/github_avatar.png differ
diff --git a/assets/gitlab_avatar.png b/assets/gitlab_avatar.png
new file mode 100644
index 00000000..f2f3aa92
Binary files /dev/null and b/assets/gitlab_avatar.png differ
diff --git a/assets/jira_avatar.png b/assets/jira_avatar.png
new file mode 100644
index 00000000..3eb55218
Binary files /dev/null and b/assets/jira_avatar.png differ
diff --git a/assets/src/feeds_avatar.svg b/assets/src/feeds_avatar.svg
new file mode 100644
index 00000000..2df89ecb
--- /dev/null
+++ b/assets/src/feeds_avatar.svg
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/assets/src/figma_avatar.svg b/assets/src/figma_avatar.svg
new file mode 100644
index 00000000..104a181b
--- /dev/null
+++ b/assets/src/figma_avatar.svg
@@ -0,0 +1,88 @@
+
+
+
+
diff --git a/assets/src/github_avatar.svg b/assets/src/github_avatar.svg
new file mode 100644
index 00000000..64f54b36
--- /dev/null
+++ b/assets/src/github_avatar.svg
@@ -0,0 +1,57 @@
+
+
+
+
diff --git a/assets/src/gitlab_avatar.svg b/assets/src/gitlab_avatar.svg
new file mode 100644
index 00000000..c9682f63
--- /dev/null
+++ b/assets/src/gitlab_avatar.svg
@@ -0,0 +1,74 @@
+
+
+
+
diff --git a/assets/src/jira_avatar.svg b/assets/src/jira_avatar.svg
new file mode 100644
index 00000000..da9bfc17
--- /dev/null
+++ b/assets/src/jira_avatar.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/assets/src/webhooks_avatar.svg b/assets/src/webhooks_avatar.svg
new file mode 100644
index 00000000..c41d74d4
--- /dev/null
+++ b/assets/src/webhooks_avatar.svg
@@ -0,0 +1,79 @@
+
+
+
+
diff --git a/assets/webhooks_avatar.png b/assets/webhooks_avatar.png
new file mode 100644
index 00000000..71286e6a
Binary files /dev/null and b/assets/webhooks_avatar.png differ
diff --git a/changelog.d/767.feature b/changelog.d/767.feature
new file mode 100644
index 00000000..7661b6e6
--- /dev/null
+++ b/changelog.d/767.feature
@@ -0,0 +1 @@
+Add support for uploading bot avatar images.
diff --git a/config.sample.yml b/config.sample.yml
index 281140e6..3f3f4a37 100644
--- a/config.sample.yml
+++ b/config.sample.yml
@@ -107,7 +107,7 @@ serviceBots:
- localpart: feeds
displayname: Feeds
- avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
+ avatar: ./assets/feeds_avatar.png
prefix: "!feeds"
service: feeds
metrics:
diff --git a/docs/advanced/service_bots.md b/docs/advanced/service_bots.md
index 6336c648..e39925b2 100644
--- a/docs/advanced/service_bots.md
+++ b/docs/advanced/service_bots.md
@@ -20,7 +20,7 @@ For example with this configuration:
serviceBots:
- localpart: feeds
displayname: Feeds
- avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
+ avatar: "./assets/feeds_avatar.png"
prefix: "!feeds"
service: feeds
```
diff --git a/src/Managers/BotUsersManager.ts b/src/Managers/BotUsersManager.ts
index b7fc1f9e..9ae8674c 100644
--- a/src/Managers/BotUsersManager.ts
+++ b/src/Managers/BotUsersManager.ts
@@ -1,3 +1,6 @@
+import { promises as fs } from "fs";
+import axios from "axios";
+import mime from "mime";
import { Appservice, Intent } from "matrix-bot-sdk";
import { Logger } from "matrix-appservice-bridge";
@@ -84,22 +87,122 @@ export default class BotUsersManager {
log.debug(`Ensuring bot user ${botUser.userId} is registered`);
await botUser.intent.ensureRegistered();
- // Set up the bot profile
- let profile;
+ await this.ensureProfile(botUser);
+ }
+ }
+
+ /**
+ * Ensures the bot user profile display name and avatar image are updated.
+ *
+ * @returns Promise resolving when the user profile has been ensured.
+ */
+ private async ensureProfile(botUser: BotUser): Promise {
+ log.debug(`Ensuring profile for ${botUser.userId} is updated`);
+
+ let profile: {
+ avatar_url?: string,
+ displayname?: string,
+ };
+ try {
+ profile = await botUser.intent.underlyingClient.getUserProfile(botUser.userId);
+ } catch (e) {
+ log.error(`Failed to get user profile for ${botUser.userId}:`, e);
+ profile = {};
+ }
+
+ // Update display name if necessary
+ if (botUser.displayname && profile.displayname !== botUser.displayname) {
try {
- profile = await botUser.intent.underlyingClient.getUserProfile(botUser.userId);
- } catch {
- profile = {}
- }
- if (botUser.avatar && profile.avatar_url !== botUser.avatar) {
- log.info(`Setting avatar for "${botUser.userId}" to ${botUser.avatar}`);
- await botUser.intent.underlyingClient.setAvatarUrl(botUser.avatar);
- }
- if (botUser.displayname && profile.displayname !== botUser.displayname) {
- log.info(`Setting displayname for "${botUser.userId}" to ${botUser.displayname}`);
await botUser.intent.underlyingClient.setDisplayName(botUser.displayname);
+ log.info(`Updated displayname for "${botUser.userId}" to ${botUser.displayname}`);
+ } catch (e) {
+ log.error(`Failed to set displayname for ${botUser.userId}:`, e);
}
}
+
+ if (!botUser.avatar) {
+ // Unset any avatar
+ if (profile.avatar_url) {
+ await botUser.intent.underlyingClient.setAvatarUrl('');
+ log.info(`Removed avatar for "${botUser.userId}"`);
+ }
+
+ return;
+ }
+
+ if (botUser.avatar.startsWith("mxc://")) {
+ // Configured avatar is a Matrix content URL
+ if (profile.avatar_url === botUser.avatar) {
+ // Avatar is current, no need to update
+ log.debug(`Avatar for ${botUser.userId} is already updated`);
+ return;
+ }
+
+ try {
+ await botUser.intent.underlyingClient.setAvatarUrl(botUser.avatar);
+ log.info(`Updated avatar for ${botUser.userId} to ${botUser.avatar}`);
+ } catch (e) {
+ log.error(`Failed to set avatar for ${botUser.userId}:`, e);
+ }
+
+ return;
+ }
+
+ // Otherwise assume configured avatar is a file path
+ let avatarImage: {
+ image: Buffer,
+ contentType: string,
+ };
+ try {
+ const contentType = mime.getType(botUser.avatar);
+ if (!contentType) {
+ throw new Error("Could not determine content type");
+ }
+ // File path
+ avatarImage = {
+ image: await fs.readFile(botUser.avatar),
+ contentType,
+ };
+ } catch (e) {
+ log.error(`Failed to load avatar at ${botUser.avatar}:`, e);
+ return;
+ }
+
+ // Determine if an avatar update is needed
+ if (profile.avatar_url) {
+ try {
+ const res = await axios.get(
+ botUser.intent.underlyingClient.mxcToHttp(profile.avatar_url),
+ { responseType: "arraybuffer" },
+ );
+ const currentAvatarImage = {
+ image: Buffer.from(res.data),
+ contentType: res.headers["content-type"],
+ };
+ if (
+ currentAvatarImage.image.equals(avatarImage.image)
+ && currentAvatarImage.contentType === avatarImage.contentType
+ ) {
+ // Avatar is current, no need to update
+ log.debug(`Avatar for ${botUser.userId} is already updated`);
+ return;
+ }
+ } catch (e) {
+ log.error(`Failed to get current avatar image for ${botUser.userId}:`, e);
+ }
+ }
+
+ // Update the avatar
+ try {
+ const uploadedAvatarMxcUrl = await botUser.intent.underlyingClient.uploadContent(
+ avatarImage.image,
+ avatarImage.contentType,
+ );
+ await botUser.intent.underlyingClient.setAvatarUrl(uploadedAvatarMxcUrl);
+ log.info(`Updated avatar for ${botUser.userId} to ${uploadedAvatarMxcUrl}`);
+ } catch (e) {
+ log.error(`Failed to set avatar for ${botUser.userId}:`, e);
+ }
}
private async getJoinedRooms(): Promise {
diff --git a/src/config/Defaults.ts b/src/config/Defaults.ts
index 09a3d2ae..cb1f4692 100644
--- a/src/config/Defaults.ts
+++ b/src/config/Defaults.ts
@@ -53,7 +53,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
{
localpart: "feeds",
displayname: "Feeds",
- avatar: "mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d",
+ avatar: "./assets/feeds_avatar.png",
prefix: "!feeds",
service: "feeds",
},