register -> invite -> join -> send flow

This commit is contained in:
Cadence Ember 2023-05-09 00:58:46 +12:00
parent 3bc29def41
commit 1e7e66dc31
11 changed files with 150 additions and 34 deletions

View file

@ -8,8 +8,8 @@ const api = sync.require("../../matrix/api")
/** /**
* @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild
*/ */
function createSpace(guild) { async function createSpace(guild) {
return api.createRoom({ const roomID = api.createRoom({
name: guild.name, name: guild.name,
preset: "private_chat", preset: "private_chat",
visibility: "private", visibility: "private",
@ -37,10 +37,9 @@ function createSpace(guild) {
} }
} }
] ]
}).then(root => {
db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, root.room_id)
return root
}) })
db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID)
return roomID
} }
module.exports.createSpace = createSpace module.exports.createSpace = createSpace

View file

@ -1,6 +1,7 @@
// @ts-check // @ts-check
const assert = require("assert") const assert = require("assert")
const reg = require("../../matrix/read-registration")
const passthrough = require("../../passthrough") const passthrough = require("../../passthrough")
const { discord, sync, db } = passthrough const { discord, sync, db } = passthrough
@ -14,10 +15,58 @@ const userToMxid = sync.require("../converters/user-to-mxid")
/** /**
* A sim is an account that is being simulated by the bridge to copy events from the other side. * A sim is an account that is being simulated by the bridge to copy events from the other side.
* @param {import("discord-api-types/v10").APIUser} user * @param {import("discord-api-types/v10").APIUser} user
* @returns mxid
*/ */
async function createSim(user) { async function createSim(user) {
// Choose sim name
const simName = userToMxid.userToSimName(user) const simName = userToMxid.userToSimName(user)
const appservicePrefix = "_ooye_" const localpart = reg.namespace_prefix + simName
const localpart = appservicePrefix + simName const mxid = "@" + localpart + ":cadence.moe"
// Save chosen name in the database forever
db.prepare("INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(user.id, simName, localpart, mxid)
// Register matrix user with that name
await api.register(localpart) await api.register(localpart)
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.
* @returns mxid
*/
async function ensureSim(user) {
let mxid = null
const existing = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(user.id)
if (existing) {
mxid = existing
} else {
mxid = await createSim(user)
}
return mxid
}
/**
* Ensure a sim is registered for the user and is joined to the room.
* @returns mxid
*/
async function ensureSimJoined(user, roomID) {
// Ensure room ID is really an ID, not an alias
assert.ok(roomID[0] === "!")
// Ensure user
const mxid = await ensureSim(user)
// Ensure joined
const existing = db.prepare("SELECT * FROM sim_member WHERE room_id = ? and mxid = ?").get(roomID, mxid)
if (!existing) {
await api.inviteToRoom(roomID, mxid)
await api.joinRoom(roomID, mxid)
db.prepare("INSERT INTO sim_member (room_id, mxid) VALUES (?, ?)").run(roomID, mxid)
}
return mxid
}
module.exports.ensureSim = ensureSim
module.exports.ensureSimJoined = ensureSimJoined

View file

@ -1,27 +1,29 @@
// @ts-check // @ts-check
const reg = require("../../matrix/read-registration.js")
const makeTxnId = require("../../matrix/txnid.js")
const fetch = require("node-fetch").default const fetch = require("node-fetch").default
const messageToEvent = require("../converters/message-to-event.js") const reg = require("../../matrix/read-registration.js")
const passthrough = require("../../passthrough")
const { discord, sync, db } = passthrough
/** @type {import("../converters/message-to-event")} */
const messageToEvent = sync.require("../converters/message-to-event")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("./register-user")} */
const registerUser = sync.require("./register-user")
/** /**
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
*/ */
function sendMessage(message) { async function sendMessage(message) {
const event = messageToEvent.messageToEvent(message) const event = messageToEvent.messageToEvent(message)
return fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, { const roomID = "!VwVlIAjOjejUpDhlbA:cadence.moe"
method: "PUT", let senderMxid = null
body: JSON.stringify(event), if (!message.webhook_id) {
headers: { senderMxid = await registerUser.ensureSimJoined(message.author, roomID)
Authorization: `Bearer ${reg.as_token}` }
} const eventID = api.sendEvent(roomID, "m.room.message", event, senderMxid)
}).then(res => res.text()).then(text => { return eventID
// {"event_id":"$4Zxs0fMmYlbo-sTlMmSEvwIs9b4hcg6yORzK0Ems84Q"}
console.log(text)
}).catch(err => {
console.log(err)
})
} }
module.exports.sendMessage = sendMessage module.exports.sendMessage = sendMessage

View file

@ -13,7 +13,7 @@ test("user2name: works on normal name", t => {
}) })
test("user2name: works on emojis", t => { test("user2name: works on emojis", t => {
t.equal(userToSimName({username: "Cookie 🍪", discriminator: "0001"}), "cookie") t.equal(userToSimName({username: "🍪 Cookie Monster 🍪", discriminator: "0001"}), "cookie_monster")
}) })
test("user2name: works on crazy name", t => { test("user2name: works on crazy name", t => {

View file

@ -8,6 +8,15 @@ const { discord, sync, db } = passthrough
const mreq = sync.require("./mreq") const mreq = sync.require("./mreq")
/** @type {import("./file")} */ /** @type {import("./file")} */
const file = sync.require("./file") const file = sync.require("./file")
/** @type {import("./txnid")} */
const makeTxnId = sync.require("./txnid")
function path(p, mxid = null) {
if (!mxid) return p
const u = new URL(p, "http://localhost")
u.searchParams.set("user_id", mxid)
return u.pathname + "?" + u.searchParams.toString()
}
/** /**
* @param {string} username * @param {string} username
@ -21,10 +30,27 @@ function register(username) {
} }
/** /**
* @returns {Promise<import("../types").R.RoomCreated>} * @returns {Promise<string>} room ID
*/ */
function createRoom(content) { async function createRoom(content) {
return mreq.mreq("POST", "/client/v3/createRoom", content) /** @type {import("../types").R.RoomCreated} */
const root = await mreq.mreq("POST", "/client/v3/createRoom", content)
return root.room_id
}
/**
* @returns {Promise<string>} room ID
*/
async function joinRoom(roomIDOrAlias, mxid) {
/** @type {import("../types").R.RoomJoined} */
const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid))
return root.room_id
}
async function inviteToRoom(roomID, mxidToInvite, mxid) {
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), {
user_id: mxidToInvite
})
} }
/** /**
@ -39,15 +65,27 @@ function getAllState(roomID) {
* @param {string} roomID * @param {string} roomID
* @param {string} type * @param {string} type
* @param {string} stateKey * @param {string} stateKey
* @returns {Promise<import("../types").R.EventSent>} * @returns {Promise<string>} event ID
*/ */
function sendState(roomID, type, stateKey, content) { async function sendState(roomID, type, stateKey, content, mxid) {
assert.ok(type) assert.ok(type)
assert.ok(stateKey) assert.ok(stateKey)
return mreq.mreq("PUT", `/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, content) /** @type {import("../types").R.EventSent} */
const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, mxid), content)
return root.event_id
} }
async function sendEvent(roomID, type, content, mxid) {
/** @type {import("../types").R.EventSent} */
const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid), content)
return root.event_id
}
module.exports.path = path
module.exports.register = register module.exports.register = register
module.exports.createRoom = createRoom module.exports.createRoom = createRoom
module.exports.joinRoom = joinRoom
module.exports.inviteToRoom = inviteToRoom
module.exports.getAllState = getAllState module.exports.getAllState = getAllState
module.exports.sendState = sendState module.exports.sendState = sendState
module.exports.sendEvent = sendEvent

23
matrix/api.test.js Normal file
View file

@ -0,0 +1,23 @@
const {test} = require("supertape")
const assert = require("assert")
const {path} = require("./api")
test("api path: no change for plain path", t => {
t.equal(path("/hello/world"), "/hello/world")
})
test("api path: add mxid to the URL", t => {
t.equal(path("/hello/world", "12345"), "/hello/world?user_id=12345")
})
test("api path: empty path with mxid", t => {
t.equal(path("", "12345"), "/?user_id=12345")
})
test("api path: existing query parameters with mxid", t => {
t.equal(path("/hello/world?foo=bar&baz=qux", "12345"), "/hello/world?foo=bar&baz=qux&user_id=12345")
})
test("api path: real world mxid", t => {
t.equal(path("/hello/world", "@cookie_monster:cadence.moe"), "/hello/world?user_id=%40cookie_monster%3Acadence.moe")
})

View file

@ -16,8 +16,6 @@ class MatrixServerError extends Error {
this.data = data this.data = data
/** @type {string} */ /** @type {string} */
this.errcode = data.errcode this.errcode = data.errcode
/** @type {string} */
this.error = data.error
} }
} }

View file

@ -2,6 +2,6 @@
let now = Date.now() let now = Date.now()
module.exports = function makeTxnId() { module.exports.makeTxnId = function makeTxnId() {
return now++ return now++
} }

View file

@ -8,6 +8,7 @@ const { discord, config, sync, db } = passthrough
const createSpace = sync.require("./d2m/actions/create-space") const createSpace = sync.require("./d2m/actions/create-space")
const createRoom = sync.require("./d2m/actions/create-room") const createRoom = sync.require("./d2m/actions/create-room")
const registerUser = sync.require("./d2m/actions/register-user")
const mreq = sync.require("./matrix/mreq") const mreq = sync.require("./matrix/mreq")
const api = sync.require("./matrix/api") const api = sync.require("./matrix/api")
const guildID = "112760669178241024" const guildID = "112760669178241024"

View file

@ -13,4 +13,5 @@ const sync = new HeatSync({watchFS: false})
Object.assign(passthrough, { config, sync, db }) Object.assign(passthrough, { config, sync, db })
require("../d2m/actions/create-room.test") require("../d2m/actions/create-room.test")
require("../d2m/converters/user-to-mxid.test") require("../d2m/converters/user-to-mxid.test")
require("../matrix/api.test")

5
types.d.ts vendored
View file

@ -4,6 +4,7 @@ export type AppServiceRegistrationConfig = {
hs_token: string hs_token: string
url: string url: string
sender_localpart: string sender_localpart: string
namespace_prefix: string
protocols: [string] protocols: [string]
rate_limited: boolean rate_limited: boolean
} }
@ -43,6 +44,10 @@ namespace R {
room_id: string room_id: string
} }
export type RoomJoined = {
room_id: string
}
export type FileUploaded = { export type FileUploaded = {
content_uri: string content_uri: string
} }