1
0
Fork 0
out-of-your-element-fork-th.../d2m/actions/register-pk-user.js

154 lines
5.4 KiB
JavaScript
Raw Normal View History

2024-01-20 11:54:18 +00:00
// @ts-check
const assert = require("assert")
const reg = require("../../matrix/read-registration")
const Ty = require("../../types")
const fetch = require("node-fetch").default
const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../../matrix/file")} */
const file = sync.require("../../matrix/file")
/** @type {import("./register-user")} */
const registerUser = sync.require("./register-user")
2024-01-30 09:01:06 +00:00
/**
* @typedef WebhookAuthor Discord API message->author. A webhook as an author.
* @prop {string} username
* @prop {string?} avatar
* @prop {string} id
*/
2024-01-20 11:54:18 +00:00
/**
* A sim is an account that is being simulated by the bridge to copy events from the other side.
2024-01-22 09:30:31 +00:00
* @param {Ty.PkMessage} pkMessage
2024-01-20 11:54:18 +00:00
* @returns mxid
*/
2024-01-22 09:30:31 +00:00
async function createSim(pkMessage) {
2024-01-20 11:54:18 +00:00
// Choose sim name
2024-01-22 09:30:31 +00:00
const simName = "_pk_" + pkMessage.member.id
2024-01-20 11:54:18 +00:00
const localpart = reg.ooye.namespace_prefix + simName
const mxid = `@${localpart}:${reg.ooye.server_name}`
// Save chosen name in the database forever
2024-01-22 09:30:31 +00:00
db.prepare("INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(pkMessage.member.uuid, simName, localpart, mxid)
2024-01-20 11:54:18 +00:00
// Register matrix user with that name
try {
await api.register(localpart)
} catch (e) {
// If user creation fails, manually undo the database change. Still isn't perfect, but should help.
// (I would prefer a transaction, but it's not safe to leave transactions open across event loop ticks.)
2024-01-22 09:30:31 +00:00
db.prepare("DELETE FROM sim WHERE user_id = ?").run(pkMessage.member.uuid)
2024-01-20 11:54:18 +00:00
throw e
}
return mxid
}
/**
* Ensure a sim is registered for the user.
* If there is already a sim, use that one. If there isn't one yet, register a new sim.
2024-01-22 09:30:31 +00:00
* @param {Ty.PkMessage} pkMessage
2024-01-20 11:54:18 +00:00
* @returns {Promise<string>} mxid
*/
2024-01-22 09:30:31 +00:00
async function ensureSim(pkMessage) {
2024-01-20 11:54:18 +00:00
let mxid = null
2024-01-22 09:30:31 +00:00
const existing = select("sim", "mxid", {user_id: pkMessage.member.uuid}).pluck().get()
2024-01-20 11:54:18 +00:00
if (existing) {
mxid = existing
} else {
2024-01-22 09:30:31 +00:00
mxid = await createSim(pkMessage)
2024-01-20 11:54:18 +00:00
}
return mxid
}
/**
* Ensure a sim is registered for the user and is joined to the room.
2024-01-22 09:30:31 +00:00
* @param {Ty.PkMessage} pkMessage
2024-01-20 11:54:18 +00:00
* @param {string} roomID
* @returns {Promise<string>} mxid
*/
2024-01-22 09:30:31 +00:00
async function ensureSimJoined(pkMessage, roomID) {
2024-01-20 11:54:18 +00:00
// Ensure room ID is really an ID, not an alias
assert.ok(roomID[0] === "!")
// Ensure user
2024-01-22 09:30:31 +00:00
const mxid = await ensureSim(pkMessage)
2024-01-20 11:54:18 +00:00
// Ensure joined
const existing = select("sim_member", "mxid", {room_id: roomID, mxid}).pluck().get()
if (!existing) {
try {
await api.inviteToRoom(roomID, mxid)
await api.joinRoom(roomID, mxid)
} catch (e) {
if (e.message.includes("is already in the room.")) {
// Sweet!
} else {
throw e
}
}
db.prepare("INSERT OR IGNORE INTO sim_member (room_id, mxid) VALUES (?, ?)").run(roomID, mxid)
}
return mxid
}
/**
2024-01-22 09:30:31 +00:00
* @param {Ty.PkMessage} pkMessage
2024-01-30 09:01:06 +00:00
* @param {WebhookAuthor} author
2024-01-20 11:54:18 +00:00
*/
2024-01-30 09:01:06 +00:00
async function memberToStateContent(pkMessage, author) {
// We prefer to use the member's avatar URL data since the image upload can be cached across channels,
// unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL.
const avatar = pkMessage.member.avatar_url || pkMessage.member.webhook_avatar_url || pkMessage.system.avatar_url || file.userAvatar(author)
2024-01-20 11:54:18 +00:00
const content = {
2024-01-30 09:01:06 +00:00
displayname: author.username,
2024-01-20 11:54:18 +00:00
membership: "join",
2024-01-22 09:30:31 +00:00
"moe.cadence.ooye.pk_member": pkMessage.member
2024-01-20 11:54:18 +00:00
}
if (avatar) content.avatar_url = await file.uploadDiscordFileToMxc(avatar)
return content
}
/**
* Sync profile data for a sim user. This function follows the following process:
* 1. Join the sim to the room if needed
* 2. Make an object of what the new room member state content would be, including uploading the profile picture if it hasn't been done before
* 3. Compare against the previously known state content, which is helpfully stored in the database
* 4. If the state content has changed, send it to Matrix and update it in the database for next time
2024-01-30 09:01:06 +00:00
* @param {WebhookAuthor} author
2024-01-22 09:30:31 +00:00
* @param {Ty.PkMessage} pkMessage
2024-01-31 00:09:39 +00:00
* @param {string} roomID
2024-01-20 11:54:18 +00:00
* @returns {Promise<string>} mxid of the updated sim
*/
2024-01-30 09:01:06 +00:00
async function syncUser(author, pkMessage, roomID) {
2024-01-22 09:30:31 +00:00
const mxid = await ensureSimJoined(pkMessage, roomID)
2024-01-31 00:09:39 +00:00
// Update the sim_proxy table, so mentions can look up the original sender later
db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id) VALUES (?, ?)").run(pkMessage.member.id, pkMessage.sender)
// Sync the member state
2024-01-30 09:01:06 +00:00
const content = await memberToStateContent(pkMessage, author)
2024-01-20 11:54:18 +00:00
const currentHash = registerUser._hashProfileContent(content)
const existingHash = select("sim_member", "hashed_profile_content", {room_id: roomID, mxid}).safeIntegers().pluck().get()
// only do the actual sync if the hash has changed since we last looked
if (existingHash !== currentHash) {
await api.sendState(roomID, "m.room.member", mxid, content, mxid)
db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid)
}
return mxid
}
2024-01-22 09:30:31 +00:00
/** @returns {Promise<Ty.PkMessage>} */
2024-01-20 11:54:18 +00:00
function fetchMessage(messageID) {
return fetch(`https://api.pluralkit.me/v2/messages/${messageID}`).then(res => res.json())
}
module.exports._memberToStateContent = memberToStateContent
module.exports.ensureSim = ensureSim
module.exports.ensureSimJoined = ensureSimJoined
module.exports.syncUser = syncUser
module.exports.fetchMessage = fetchMessage