Sadly, the presence API is worse than I hoped
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -30,7 +30,7 @@
|
|||||||
"get-relative-path": "^1.0.2",
|
"get-relative-path": "^1.0.2",
|
||||||
"get-stream": "^6.0.1",
|
"get-stream": "^6.0.1",
|
||||||
"h3": "^1.12.0",
|
"h3": "^1.12.0",
|
||||||
"heatsync": "^2.5.5",
|
"heatsync": "^2.6.0",
|
||||||
"lru-cache": "^10.4.3",
|
"lru-cache": "^10.4.3",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
@@ -1193,7 +1193,8 @@
|
|||||||
"node_modules/backtracker": {
|
"node_modules/backtracker": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/backtracker/-/backtracker-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/backtracker/-/backtracker-4.0.0.tgz",
|
||||||
"integrity": "sha512-XG2ldN+WDRq9niJMnoZDjLLUnhDOQGhFZc6qZQotN59xj8oOa4KXSCu6YyZQawPqi6gG3HilGFt91zT6Hbdh1w=="
|
"integrity": "sha512-XG2ldN+WDRq9niJMnoZDjLLUnhDOQGhFZc6qZQotN59xj8oOa4KXSCu6YyZQawPqi6gG3HilGFt91zT6Hbdh1w==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -1938,9 +1939,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/heatsync": {
|
"node_modules/heatsync": {
|
||||||
"version": "2.5.5",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.6.0.tgz",
|
||||||
"integrity": "sha512-Sy2/X2a69W2W1xgp7GBY81naHtWXxwV8N6uzPTJLQXgq4oTMJeL6F/AUlGS+fUa/Pt5ioxzi7gvd8THMJ3GpyA==",
|
"integrity": "sha512-UfemOt4Kg1hvhDj/Zz8sYa1pF73ul+tF19MYNisYoOymXoTo4iCZv2BDdCMFE1xvZ6YFjcMoekb/aeBU1uqFjQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"backtracker": "^4.0.0"
|
"backtracker": "^4.0.0"
|
||||||
}
|
}
|
||||||
|
@@ -39,7 +39,7 @@
|
|||||||
"get-relative-path": "^1.0.2",
|
"get-relative-path": "^1.0.2",
|
||||||
"get-stream": "^6.0.1",
|
"get-stream": "^6.0.1",
|
||||||
"h3": "^1.12.0",
|
"h3": "^1.12.0",
|
||||||
"heatsync": "^2.5.5",
|
"heatsync": "^2.6.0",
|
||||||
"lru-cache": "^10.4.3",
|
"lru-cache": "^10.4.3",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
|
@@ -32,7 +32,7 @@ async function createSpace(guild, kstate) {
|
|||||||
assert(name)
|
assert(name)
|
||||||
|
|
||||||
const memberCount = guild["member_count"] ?? guild.approximate_member_count ?? 0
|
const memberCount = guild["member_count"] ?? guild.approximate_member_count ?? 0
|
||||||
const enablePresenceByDefault = +(memberCount < 150) // could increase this later on if it doesn't cause any problems
|
const enablePresenceByDefault = +(memberCount < 50) // scary! all active users in a presence-enabled guild will be pinging the server every <30 seconds to stay online
|
||||||
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
|
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
|
||||||
|
|
||||||
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
||||||
|
@@ -5,9 +5,28 @@ const {sync, select} = passthrough
|
|||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
const api = sync.require("../../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!
|
We do this in two phases for optimisation reasons.
|
||||||
|
Discord sends us an event when the presence *changes.*
|
||||||
|
We need to keep the event data in memory because we need to *repeatedly* send it to Matrix using a long-lived loop.
|
||||||
|
|
||||||
|
There are two phases to get it from Discord to Matrix.
|
||||||
|
The first phase stores Discord presence data in memory.
|
||||||
|
The second phase loops over the memory and sends it on to Matrix.
|
||||||
|
|
||||||
|
In the first phase, for optimisation reasons, we want to do as little work as possible if the presence doesn't actually need to be sent all the way through.
|
||||||
|
* Presence can be deactivated per-guild in OOYE settings. If it's deactivated for all of a user's guilds, we shouldn't send them to the second phase.
|
||||||
|
* Presence can be sent for users without sims. In this case, we shouldn't send them to the second phase.
|
||||||
|
* Presence can be sent multiple times in a row for the same user for each guild we share. We want to batch these up so we only query the mxid and enter the second phase once per user.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ***** first phase *****
|
||||||
|
|
||||||
|
|
||||||
|
// Delay before querying user details and putting them in memory.
|
||||||
const presenceDelay = 1500
|
const presenceDelay = 1500
|
||||||
|
|
||||||
/** @type {Map<string, NodeJS.Timeout>} user ID -> cancelable timeout */
|
/** @type {Map<string, NodeJS.Timeout>} user ID -> cancelable timeout */
|
||||||
const presenceDelayMap = new Map()
|
const presenceDelayMap = new Map()
|
||||||
|
|
||||||
@@ -20,8 +39,9 @@ function checkPresenceEnabledGuilds() {
|
|||||||
checkPresenceEnabledGuilds()
|
checkPresenceEnabledGuilds()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This function is called for each Discord presence packet.
|
||||||
* @param {string} userID Discord user ID
|
* @param {string} userID Discord user ID
|
||||||
* @param {string} guildID Discord guild ID that this presence applies to (really, the same presence applies to every single guild, but is delivered separately)
|
* @param {string} guildID Discord guild ID that this presence applies to (really, the same presence applies to every single guild, but is delivered separately by Discord for some reason)
|
||||||
* @param {string} status status field from Discord's PRESENCE_UPDATE event
|
* @param {string} status status field from Discord's PRESENCE_UPDATE event
|
||||||
*/
|
*/
|
||||||
function setPresence(userID, guildID, status) {
|
function setPresence(userID, guildID, status) {
|
||||||
@@ -47,11 +67,38 @@ function setPresenceCallback(user_id, status) {
|
|||||||
( status === "online" ? "online"
|
( status === "online" ? "online"
|
||||||
: status === "offline" ? "offline"
|
: status === "offline" ? "offline"
|
||||||
: "unavailable") // idle, dnd, and anything else they dream up in the future
|
: "unavailable") // idle, dnd, and anything else they dream up in the future
|
||||||
api.setPresence(presence, mxid).catch(e => {
|
if (presence === "offline") {
|
||||||
console.error("d->m: Skipping presence update failure:")
|
userPresence.delete(mxid) // stop syncing next cycle
|
||||||
console.error(e)
|
} else {
|
||||||
})
|
const delay = userPresence.get(mxid)?.delay || presenceLoopInterval * Math.random() // distribute the updates across the presence loop
|
||||||
|
userPresence.set(mxid, {data: {presence}, delay}) // will be synced next cycle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ***** second phase *****
|
||||||
|
|
||||||
|
|
||||||
|
// Synapse expires each user's presence after 30 seconds and makes them offline, so we have loop every 28 seconds and update each user again.
|
||||||
|
const presenceLoopInterval = 28e3
|
||||||
|
|
||||||
|
/** @type {Map<string, {data: {presence: "online" | "offline" | "unavailable", status_msg?: string}, delay: number}>} mxid -> presence data to send to api */
|
||||||
|
const userPresence = new Map()
|
||||||
|
|
||||||
|
sync.addTemporaryInterval(() => {
|
||||||
|
for (const [mxid, memory] of userPresence.entries()) {
|
||||||
|
// I haven't tried, but assuming Synapse explodes if you try to update too many presences at the same time,
|
||||||
|
// I'll space them out over the whole 28 second cycle.
|
||||||
|
setTimeout(() => {
|
||||||
|
const d = new Date().toISOString().slice(0, 19)
|
||||||
|
api.setPresence(memory.data, mxid).catch(e => {
|
||||||
|
console.error("d->m: Skipping presence update failure:")
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}, memory.delay)
|
||||||
|
}
|
||||||
|
}, presenceLoopInterval)
|
||||||
|
|
||||||
|
|
||||||
module.exports.setPresence = setPresence
|
module.exports.setPresence = setPresence
|
||||||
module.exports.checkPresenceEnabledGuilds = checkPresenceEnabledGuilds
|
module.exports.checkPresenceEnabledGuilds = checkPresenceEnabledGuilds
|
||||||
|
@@ -409,11 +409,11 @@ async function setAccountData(type, content, mxid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {"online" | "offline" | "unavailable"} presence
|
* @param {{presence: "online" | "offline" | "unavailable", status_msg?: string}} data
|
||||||
* @param {string} mxid
|
* @param {string} mxid
|
||||||
*/
|
*/
|
||||||
async function setPresence(presence, mxid) {
|
async function setPresence(data, mxid) {
|
||||||
await mreq.mreq("PUT", path(`/client/v3/presence/${mxid}/status`, mxid), {presence})
|
await mreq.mreq("PUT", path(`/client/v3/presence/${mxid}/status`, mxid), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.path = path
|
module.exports.path = path
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
* @typedef {Object} Passthrough
|
* @typedef {Object} Passthrough
|
||||||
* @property {import("repl").REPLServer} repl
|
* @property {import("repl").REPLServer} repl
|
||||||
* @property {import("./d2m/discord-client")} discord
|
* @property {import("./d2m/discord-client")} discord
|
||||||
* @property {import("heatsync").default} sync
|
* @property {import("heatsync")} sync
|
||||||
* @property {import("better-sqlite3/lib/database")} db
|
* @property {import("better-sqlite3/lib/database")} db
|
||||||
* @property {import("@cloudrac3r/in-your-element").AppService} as
|
* @property {import("@cloudrac3r/in-your-element").AppService} as
|
||||||
* @property {import("./db/orm").from} from
|
* @property {import("./db/orm").from} from
|
||||||
|
3
start.js
3
start.js
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
const fs = require("fs")
|
||||||
const sqlite = require("better-sqlite3")
|
const sqlite = require("better-sqlite3")
|
||||||
const migrate = require("./src/db/migrate")
|
const migrate = require("./src/db/migrate")
|
||||||
const HeatSync = require("heatsync")
|
const HeatSync = require("heatsync")
|
||||||
@@ -9,7 +10,7 @@ const {reg} = require("./src/matrix/read-registration")
|
|||||||
const passthrough = require("./src/passthrough")
|
const passthrough = require("./src/passthrough")
|
||||||
const db = new sqlite("ooye.db")
|
const db = new sqlite("ooye.db")
|
||||||
|
|
||||||
const sync = new HeatSync()
|
const sync = new HeatSync({watchFunction: fs.watchFile})
|
||||||
|
|
||||||
Object.assign(passthrough, {sync, db})
|
Object.assign(passthrough, {sync, db})
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user