diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 517dad4..4218c1f 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -8,8 +8,8 @@ const api = sync.require("../../matrix/api") /** * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild */ -function createSpace(guild) { - return api.createRoom({ +async function createSpace(guild) { + const roomID = api.createRoom({ name: guild.name, preset: "private_chat", 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 diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index f970218..a5fd0ef 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,6 +1,7 @@ // @ts-check const assert = require("assert") +const reg = require("../../matrix/read-registration") const passthrough = require("../../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. * @param {import("discord-api-types/v10").APIUser} user + * @returns mxid */ async function createSim(user) { + // Choose sim name const simName = userToMxid.userToSimName(user) - const appservicePrefix = "_ooye_" - const localpart = appservicePrefix + simName + const localpart = reg.namespace_prefix + 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) + 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 diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 1f71a66..0a425ee 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -1,27 +1,29 @@ // @ts-check -const reg = require("../../matrix/read-registration.js") -const makeTxnId = require("../../matrix/txnid.js") 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 */ -function sendMessage(message) { +async function sendMessage(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`, { - method: "PUT", - body: JSON.stringify(event), - headers: { - Authorization: `Bearer ${reg.as_token}` - } - }).then(res => res.text()).then(text => { - // {"event_id":"$4Zxs0fMmYlbo-sTlMmSEvwIs9b4hcg6yORzK0Ems84Q"} - console.log(text) - }).catch(err => { - console.log(err) - }) + const roomID = "!VwVlIAjOjejUpDhlbA:cadence.moe" + let senderMxid = null + if (!message.webhook_id) { + senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + } + const eventID = api.sendEvent(roomID, "m.room.message", event, senderMxid) + return eventID } module.exports.sendMessage = sendMessage diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 7cda6d7..4c721fc 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -13,7 +13,7 @@ test("user2name: works on normal name", 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 => { diff --git a/db/ooye.db b/db/ooye.db index bb571ed..c9803f8 100644 Binary files a/db/ooye.db and b/db/ooye.db differ diff --git a/matrix/api.js b/matrix/api.js index 88c62d8..e3a2600 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -8,6 +8,15 @@ const { discord, sync, db } = passthrough const mreq = sync.require("./mreq") /** @type {import("./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 @@ -21,10 +30,27 @@ function register(username) { } /** - * @returns {Promise} + * @returns {Promise} room ID */ -function createRoom(content) { - return mreq.mreq("POST", "/client/v3/createRoom", content) +async function createRoom(content) { + /** @type {import("../types").R.RoomCreated} */ + const root = await mreq.mreq("POST", "/client/v3/createRoom", content) + return root.room_id +} + +/** + * @returns {Promise} 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} type * @param {string} stateKey - * @returns {Promise} + * @returns {Promise} event ID */ -function sendState(roomID, type, stateKey, content) { +async function sendState(roomID, type, stateKey, content, mxid) { assert.ok(type) 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.createRoom = createRoom +module.exports.joinRoom = joinRoom +module.exports.inviteToRoom = inviteToRoom module.exports.getAllState = getAllState module.exports.sendState = sendState +module.exports.sendEvent = sendEvent diff --git a/matrix/api.test.js b/matrix/api.test.js new file mode 100644 index 0000000..f54c665 --- /dev/null +++ b/matrix/api.test.js @@ -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") +}) diff --git a/matrix/mreq.js b/matrix/mreq.js index 1345f78..6c4eaa3 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -16,8 +16,6 @@ class MatrixServerError extends Error { this.data = data /** @type {string} */ this.errcode = data.errcode - /** @type {string} */ - this.error = data.error } } diff --git a/matrix/txnid.js b/matrix/txnid.js index 1e26378..a3568df 100644 --- a/matrix/txnid.js +++ b/matrix/txnid.js @@ -2,6 +2,6 @@ let now = Date.now() -module.exports = function makeTxnId() { +module.exports.makeTxnId = function makeTxnId() { return now++ } diff --git a/stdin.js b/stdin.js index 551d7d2..99345ab 100644 --- a/stdin.js +++ b/stdin.js @@ -8,6 +8,7 @@ const { discord, config, sync, db } = passthrough const createSpace = sync.require("./d2m/actions/create-space") const createRoom = sync.require("./d2m/actions/create-room") +const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const guildID = "112760669178241024" diff --git a/test/test.js b/test/test.js index 3008abf..1068136 100644 --- a/test/test.js +++ b/test/test.js @@ -13,4 +13,5 @@ const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) require("../d2m/actions/create-room.test") -require("../d2m/converters/user-to-mxid.test") \ No newline at end of file +require("../d2m/converters/user-to-mxid.test") +require("../matrix/api.test") diff --git a/types.d.ts b/types.d.ts index b3a9acc..bc24329 100644 --- a/types.d.ts +++ b/types.d.ts @@ -4,6 +4,7 @@ export type AppServiceRegistrationConfig = { hs_token: string url: string sender_localpart: string + namespace_prefix: string protocols: [string] rate_limited: boolean } @@ -43,6 +44,10 @@ namespace R { room_id: string } + export type RoomJoined = { + room_id: string + } + export type FileUploaded = { content_uri: string }