diff --git a/readme.md b/readme.md index 1f01d01..8ab6bb4 100644 --- a/readme.md +++ b/readme.md @@ -36,6 +36,7 @@ Most features you'd expect in both directions, plus a little extra spice: * Attachments * Spoiler attachments * Embeds +* Presence * Guild-Space details syncing * Channel-Room details syncing * Custom emoji list syncing diff --git a/src/d2m/actions/set-presence.js b/src/d2m/actions/set-presence.js new file mode 100644 index 0000000..e62456a --- /dev/null +++ b/src/d2m/actions/set-presence.js @@ -0,0 +1,45 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const {sync, select} = passthrough +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +// Adding a debounce to all updates because events are issued multiple times, once for each guild. +// Sometimes a status update is even issued twice in a row for the same user+guild, weird! +const presenceDelay = 1500 +/** @type {Map} user ID -> cancelable timeout */ +const presenceDelayMap = new Map() + +/** + * @param {string} userID Discord user ID + * @param {string} status status field from Discord's PRESENCE_UPDATE event + */ +function setPresence(userID, status) { + // cancel existing timer if one is already set + if (presenceDelayMap.has(userID)) { + clearTimeout(presenceDelayMap.get(userID)) + } + // new timer, which will run if nothing else comes in soon + presenceDelayMap.set(userID, setTimeout(setPresenceCallback, presenceDelay, userID, status).unref()) +} + +/** + * @param {string} user_id Discord user ID + * @param {string} status status field from Discord's PRESENCE_UPDATE event + */ +function setPresenceCallback(user_id, status) { + presenceDelayMap.delete(user_id) + const mxid = select("sim", "mxid", {user_id}).pluck().get() + if (!mxid) return + const presence = + ( status === "online" ? "online" + : status === "offline" ? "offline" + : "unavailable") // idle, dnd, and anything else they dream up in the future + api.setPresence(presence, mxid).catch(e => { + console.error("d->m: Skipping presence update failure:") + console.error(e) + }) +} + +module.exports.setPresence = setPresence diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index 37b3eac..9745ec2 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -28,7 +28,7 @@ class DiscordClient { intents: [ "DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING", "GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", - "MESSAGE_CONTENT" + "MESSAGE_CONTENT", "GUILD_PRESENCES" ], ws: { compress: false, diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 2e97671..20ed07f 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -196,6 +196,9 @@ const utils = { } else if (message.t === "INTERACTION_CREATE") { await interactions.dispatchInteraction(message.d) + + } else if (message.t === "PRESENCE_UPDATE") { + eventDispatcher.onPresenceUpdate(message.d.user.id, message.d.status) } } catch (e) { diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index a80c451..c2e6b03 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -33,6 +33,8 @@ const mxUtils = require("../m2d/converters/utils") const speedbump = sync.require("./actions/speedbump") /** @type {import("./actions/retrigger")} */ const retrigger = sync.require("./actions/retrigger") +/** @type {import("./actions/set-presence")} */ +const setPresence = sync.require("./actions/set-presence") /** @type {any} */ // @ts-ignore bad types from semaphore const Semaphore = require("@chriscdn/promise-semaphore") @@ -369,5 +371,14 @@ module.exports = { */ async onExpressionsUpdate(client, data) { await createSpace.syncSpaceExpressions(data, false) + }, + + /** + * @param {string} userID + * @param {string} [status] + */ + async onPresenceUpdate(userID, status) { + if (!status) return + setPresence.setPresence(userID, status) } } diff --git a/src/matrix/api.js b/src/matrix/api.js index d1d9516..40fe6fb 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -408,6 +408,14 @@ async function setAccountData(type, content, mxid) { await mreq.mreq("PUT", `/client/v3/user/${mxid}/account_data/${type}`, content) } +/** + * @param {"online" | "offline" | "unavailable"} presence + * @param {string} mxid + */ +async function setPresence(presence, mxid) { + await mreq.mreq("PUT", path(`/client/v3/presence/${mxid}/status`, mxid), {presence}) +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -440,3 +448,4 @@ module.exports.ackEvent = ackEvent module.exports.getAlias = getAlias module.exports.getAccountData = getAccountData module.exports.setAccountData = setAccountData +module.exports.setPresence = setPresence