diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index a5fd0ef..508dd39 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -24,10 +24,18 @@ async function createSim(user) { const mxid = "@" + localpart + ":cadence.moe" // Save chosen name in the database forever + // Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates 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) + try { + await api.register(localpart) + } catch (e) { + // If user creation fails, manually undo the database change. Still isn't perfect, but should help. + // (A transaction would be preferable, but I don't think it's safe to leave transaction open across event loop ticks.) + db.prepare("DELETE FROM sim WHERE discord_id = ?").run(user.id) + throw e + } return mxid } diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 630cf48..f828134 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -28,4 +28,4 @@ async function sendMessage(message) { return eventID } -module.exports.sendMessage = sendMessage +module.exports.sendMessage = sendMessage \ No newline at end of file diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 35d9368..89e47a4 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -44,6 +44,7 @@ function* generateLocalpartAlternatives(preferences) { /** * Whole process for checking the database and generating the right sim name. + * It is very important this is not an async function: once the name has been chosen, the calling function should be able to immediately claim that name into the database in the same event loop tick. * @param {import("discord-api-types/v10").APIUser} user * @returns {string} */ diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 4c721fc..8c4c430 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -31,3 +31,7 @@ test("user2name: adds number suffix if name is unavailable (new username format) test("user2name: uses ID if name becomes too short", t => { t.equal(userToSimName({username: "f***", discriminator: "0001", id: "9"}), "9") }) + +test("user2name: uses ID when name has only disallowed characters", t => { + t.equal(userToSimName({username: "!@#$%^&*", discriminator: "0001", id: "9"}), "9") +}) \ No newline at end of file diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 0d16cdc..3786393 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -6,15 +6,16 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") const { sync } = passthrough -/** @type {typeof import("./event-dispatcher")} */ -const eventDispatcher = sync.require("./event-dispatcher") - const utils = { /** * @param {import("./discord-client")} client * @param {import("cloudstorm").IGatewayMessage} message */ onPacket(client, message) { + // requiring this later so that the client is already constructed by the time event-dispatcher is loaded + /** @type {typeof import("./event-dispatcher")} */ + const eventDispatcher = sync.require("./event-dispatcher") + if (message.t === "READY") { if (client.ready) return client.ready = true diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 8539044..83a1a70 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -2,9 +2,6 @@ const {sync} = require("../passthrough") -/** @type {import("./actions/create-space")}) */ -const createSpace = sync.require("./actions/create-space") - /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") diff --git a/matrix/api.js b/matrix/api.js index 04b7cd1..dbc39bc 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -28,6 +28,7 @@ function path(p, mxid) { * @returns {Promise} */ function register(username) { + console.log(`[api] register: ${username}`) return mreq.mreq("POST", "/client/v3/register", { type: "m.login.application_service", username @@ -38,6 +39,7 @@ function register(username) { * @returns {Promise} room ID */ async function createRoom(content) { + console.log(`[api] create room:`, content) /** @type {import("../types").R.RoomCreated} */ const root = await mreq.mreq("POST", "/client/v3/createRoom", content) return root.room_id @@ -74,6 +76,7 @@ function getAllState(roomID) { * @returns {Promise} event ID */ async function sendState(roomID, type, stateKey, content, mxid) { + console.log(`[api] state: ${roomID}: ${type}/${stateKey}`) assert.ok(type) assert.ok(stateKey) /** @type {import("../types").R.EventSent} */ @@ -82,6 +85,7 @@ async function sendState(roomID, type, stateKey, content, mxid) { } async function sendEvent(roomID, type, content, mxid) { + console.log(`[api] event to ${roomID} as ${mxid || "default sim"}`) /** @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 diff --git a/matrix/mreq.js b/matrix/mreq.js index 6c4eaa3..df34d91 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -11,11 +11,12 @@ const reg = sync.require("./read-registration.js") const baseUrl = "https://matrix.cadence.moe/_matrix" class MatrixServerError extends Error { - constructor(data) { + constructor(data, opts) { super(data.error || data.errcode) this.data = data /** @type {string} */ this.errcode = data.errcode + this.opts = opts } } @@ -38,7 +39,7 @@ async function mreq(method, url, body, extra = {}) { const res = await fetch(baseUrl + url, opts) const root = await res.json() - if (!res.ok || root.errcode) throw new MatrixServerError(root) + if (!res.ok || root.errcode) throw new MatrixServerError(root, opts) return root } diff --git a/matrix/read-registration.test.js b/matrix/read-registration.test.js new file mode 100644 index 0000000..9c7f828 --- /dev/null +++ b/matrix/read-registration.test.js @@ -0,0 +1,11 @@ +const {test} = require("supertape") +const assert = require("assert") +const reg = require("./read-registration") + +test("reg: has necessary parameters", t => { + const propertiesToCheck = ["sender_localpart", "id", "as_token", "namespace_prefix"] + t.deepEqual( + propertiesToCheck.filter(p => p in reg), + propertiesToCheck + ) +}) \ No newline at end of file diff --git a/test/test.js b/test/test.js index 1068136..4e01708 100644 --- a/test/test.js +++ b/test/test.js @@ -12,6 +12,7 @@ const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) +require("../matrix/read-registration.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../matrix/api.test")