username sanitisation for registration
This commit is contained in:
		
							
								
								
									
										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") | ||||||
		Reference in New Issue
	
	Block a user
	 Cadence Ember
					Cadence Ember