username sanitisation for registration
This commit is contained in:
parent
48c2ef76f5
commit
7ee04d085f
8 changed files with 402 additions and 2350 deletions
24
.vscode/tasks.json
vendored
Normal file
24
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "test",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "npm: test",
|
||||||
|
"detail": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot",
|
||||||
|
"presentation": {
|
||||||
|
"echo": false,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -4,39 +4,16 @@ const assert = require("assert")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const { discord, sync, db } = passthrough
|
const { discord, sync, db } = passthrough
|
||||||
/** @type {import("../../matrix/mreq")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
const mreq = sync.require("../../matrix/mreq")
|
const api = sync.require("../../matrix/api")
|
||||||
/** @type {import("../../matrix/file")} */
|
/** @type {import("../../matrix/file")} */
|
||||||
const file = sync.require("../../matrix/file")
|
const file = sync.require("../../matrix/file")
|
||||||
|
|
||||||
async function registerUser(username) {
|
|
||||||
assert.ok(username.startsWith("_ooye_"))
|
|
||||||
/** @type {import("../../types").R.Registered} */
|
|
||||||
const res = await mreq.mreq("POST", "/client/v3/register", {
|
|
||||||
type: "m.login.application_service",
|
|
||||||
username
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
async function createSim(user) {
|
async function createSim(user) {
|
||||||
assert.notEqual(user.discriminator, "0000", "user is not a webhook")
|
assert.notEqual(user.discriminator, "0000", "user is not a webhook")
|
||||||
fetch("https://matrix.cadence.moe/_matrix/client/v3/register", {
|
api.register("_ooye_example")
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: "m.login.application_service",
|
|
||||||
username: "_ooye_example"
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${reg.as_token}`
|
|
||||||
}
|
}
|
||||||
}).then(res => res.text()).then(text => {
|
|
||||||
|
|
||||||
console.log(text)
|
|
||||||
}).catch(err => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
74
d2m/converters/user-to-mxid.js
Normal file
74
d2m/converters/user-to-mxid.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const assert = require("assert")
|
||||||
|
|
||||||
|
const passthrough = require("../../passthrough")
|
||||||
|
const { sync, db } = passthrough
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downcased and stripped username. Can only include a basic set of characters.
|
||||||
|
* https://spec.matrix.org/v1.6/appendices/#user-identifiers
|
||||||
|
* @param {import("discord-api-types/v10").APIUser} user
|
||||||
|
* @returns {string} localpart
|
||||||
|
*/
|
||||||
|
function downcaseUsername(user) {
|
||||||
|
// First, try to convert the username to the set of allowed characters
|
||||||
|
let downcased = user.username.toLowerCase()
|
||||||
|
// spaces to underscores...
|
||||||
|
.replace(/ /g, "_")
|
||||||
|
// remove disallowed characters...
|
||||||
|
.replace(/[^a-z0-9._=/-]*/g, "")
|
||||||
|
// remove leading and trailing dashes and underscores...
|
||||||
|
.replace(/(?:^[_-]*|[_-]*$)/g, "")
|
||||||
|
// The new length must be at least 2 characters (in other words, it should have some content)
|
||||||
|
if (downcased.length < 2) {
|
||||||
|
downcased = user.id
|
||||||
|
}
|
||||||
|
return downcased
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string[]} preferences */
|
||||||
|
function* generateLocalpartAlternatives(preferences) {
|
||||||
|
const best = preferences[0]
|
||||||
|
assert.ok(best)
|
||||||
|
// First, suggest the preferences...
|
||||||
|
for (const localpart of preferences) {
|
||||||
|
yield localpart
|
||||||
|
}
|
||||||
|
// ...then fall back to generating number suffixes...
|
||||||
|
let i = 2
|
||||||
|
while (true) {
|
||||||
|
yield best + (i++)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("discord-api-types/v10").APIUser} user
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function userToSimName(user) {
|
||||||
|
assert.notEqual(user.discriminator, "0000", "cannot create user for a webhook")
|
||||||
|
|
||||||
|
// 1. Is sim user already registered?
|
||||||
|
const existing = db.prepare("SELECT sim_name FROM sim WHERE discord_id = ?").pluck().get(user.id)
|
||||||
|
if (existing) return existing
|
||||||
|
|
||||||
|
// 2. Register based on username (could be new or old format)
|
||||||
|
const downcased = downcaseUsername(user)
|
||||||
|
const preferences = [downcased]
|
||||||
|
if (user.discriminator.length === 4) { // Old style tag? If user.username is unavailable, try the full tag next
|
||||||
|
preferences.push(downcased + user.discriminator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for conflicts with already registered sims
|
||||||
|
/** @type {string[]} */
|
||||||
|
const matches = db.prepare("SELECT sim_name FROM sim WHERE sim_name LIKE ? ESCAPE '@'").pluck().all(downcased + "%")
|
||||||
|
// Keep generating until we get a suggestion that doesn't conflict
|
||||||
|
for (const suggestion of generateLocalpartAlternatives(preferences)) {
|
||||||
|
if (!matches.includes(suggestion)) return suggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.userToSimName = userToSimName
|
33
d2m/converters/user-to-mxid.test.js
Normal file
33
d2m/converters/user-to-mxid.test.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const {test} = require("supertape")
|
||||||
|
const tryToCatch = require("try-to-catch")
|
||||||
|
const assert = require("assert")
|
||||||
|
const {userToSimName} = require("./user-to-mxid")
|
||||||
|
|
||||||
|
test("user2name: cannot create user for a webhook", async t => {
|
||||||
|
const [error] = await tryToCatch(() => userToSimName({discriminator: "0000"}))
|
||||||
|
t.ok(error instanceof assert.AssertionError, error.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("user2name: works on normal name", t => {
|
||||||
|
t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001"}), "harry_styles")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("user2name: works on emojis", t => {
|
||||||
|
t.equal(userToSimName({username: "Cookie 🍪", discriminator: "0001"}), "cookie")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("user2name: works on crazy name", t => {
|
||||||
|
t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("user2name: adds discriminator if name is unavailable (old tag format)", t => {
|
||||||
|
t.equal(userToSimName({username: "BOT$", discriminator: "1234"}), "bot1234")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("user2name: adds number suffix if name is unavailable (new username format)", t => {
|
||||||
|
t.equal(userToSimName({username: "bot", discriminator: "0"}), "bot2")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("user2name: uses ID if name becomes too short", t => {
|
||||||
|
t.equal(userToSimName({username: "f***", discriminator: "0001", id: "9"}), "9")
|
||||||
|
})
|
20
matrix/api.js
Normal file
20
matrix/api.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const passthrough = require("../passthrough")
|
||||||
|
const { discord, sync, db } = passthrough
|
||||||
|
/** @type {import("./mreq")} */
|
||||||
|
const mreq = sync.require("./mreq")
|
||||||
|
/** @type {import("./file")} */
|
||||||
|
const file = sync.require("./file")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<import("../types").R.Registered>}
|
||||||
|
*/
|
||||||
|
function register(username) {
|
||||||
|
return mreq.mreq("POST", "/client/v3/register", {
|
||||||
|
type: "m.login.application_service",
|
||||||
|
username
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.register = register
|
2559
package-lock.json
generated
2559
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -18,21 +18,23 @@
|
||||||
"better-sqlite3": "^8.3.0",
|
"better-sqlite3": "^8.3.0",
|
||||||
"cloudstorm": "^0.7.0",
|
"cloudstorm": "^0.7.0",
|
||||||
"discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b",
|
"discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b",
|
||||||
"heatsync": "^2.4.0",
|
"heatsync": "^2.4.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"matrix-appservice": "^2.0.0",
|
"matrix-appservice": "^2.0.0",
|
||||||
"matrix-js-sdk": "^24.1.0",
|
"matrix-js-sdk": "^24.1.0",
|
||||||
"mixin-deep": "^2.0.1",
|
"mixin-deep": "^2.0.1",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"snowtransfer": "^0.7.0"
|
"snowtransfer": "^0.7.0",
|
||||||
|
"try-to-catch": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
"@types/node-fetch": "^2.6.3",
|
"@types/node-fetch": "^2.6.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"supertape": "^8.3.0",
|
"supertape": "^8.3.0",
|
||||||
"tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4"
|
"tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "FORCE_COLOR=true supertape --format tap test/test.js | tap-dot"
|
"test": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,9 @@ const passthrough = require("../passthrough")
|
||||||
const db = new sqlite("db/ooye.db")
|
const db = new sqlite("db/ooye.db")
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const sync = new HeatSync({persistent: false})
|
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")
|
Loading…
Reference in a new issue