1
0
Fork 0

username sanitisation for registration

This commit is contained in:
Cadence Ember 2023-05-08 17:22:20 +12:00
parent 48c2ef76f5
commit 7ee04d085f
8 changed files with 402 additions and 2350 deletions

View file

@ -4,39 +4,16 @@ const assert = require("assert")
const passthrough = require("../../passthrough")
const { discord, sync, db } = passthrough
/** @type {import("../../matrix/mreq")} */
const mreq = sync.require("../../matrix/mreq")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../../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.
* @param {import("discord-api-types/v10").APIUser} user
*/
async function createSim(user) {
assert.notEqual(user.discriminator, "0000", "user is not a webhook")
fetch("https://matrix.cadence.moe/_matrix/client/v3/register", {
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)
})
api.register("_ooye_example")
}

View 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

View 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")
})