record more update events
This commit is contained in:
		| @@ -27,7 +27,7 @@ async function sendMessage(message, guild) { | |||||||
| 		await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) | 		await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const events = await messageToEvent.messageToEvent(message, guild, api) | 	const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) | ||||||
| 	const eventIDs = [] | 	const eventIDs = [] | ||||||
| 	let eventPart = 0 // 0 is primary, 1 is supporting | 	let eventPart = 0 // 0 is primary, 1 is supporting | ||||||
| 	for (const event of events) { | 	for (const event of events) { | ||||||
|   | |||||||
| @@ -29,7 +29,9 @@ async function editToChanges(message, guild) { | |||||||
|  |  | ||||||
| 	// Figure out what we will be replacing them with | 	// Figure out what we will be replacing them with | ||||||
|  |  | ||||||
| 	const newEvents = await messageToEvent.messageToEvent(message, guild, api) | 	const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api}) | ||||||
|  | 	const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api}) | ||||||
|  | 	assert.ok(newFallbackContent.length === newInnerContent.length) | ||||||
|  |  | ||||||
| 	// Match the new events to the old events | 	// Match the new events to the old events | ||||||
|  |  | ||||||
| @@ -47,21 +49,27 @@ async function editToChanges(message, guild) { | |||||||
| 	let eventsToSend = [] | 	let eventsToSend = [] | ||||||
| 	//  4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. This is represented as nothing. | 	//  4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. This is represented as nothing. | ||||||
|  |  | ||||||
|  | 	function shift() { | ||||||
|  | 		newFallbackContent.shift() | ||||||
|  | 		newInnerContent.shift() | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// For each old event... | 	// For each old event... | ||||||
| 	outer: while (newEvents.length) { | 	outer: while (newFallbackContent.length) { | ||||||
| 		const newe = newEvents[0] | 		const newe = newFallbackContent[0] | ||||||
| 		// Find a new event to pair it with... | 		// Find a new event to pair it with... | ||||||
| 		for (let i = 0; i < oldEventRows.length; i++) { | 		for (let i = 0; i < oldEventRows.length; i++) { | ||||||
| 			const olde = oldEventRows[i] | 			const olde = oldEventRows[i] | ||||||
| 			if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype || null)) { | 			if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype ?? null)) { // The spec does allow subtypes to change, so I can change this condition later if I want to | ||||||
| 				// Found one! | 				// Found one! | ||||||
| 				// Set up the pairing | 				// Set up the pairing | ||||||
| 				eventsToReplace.push({ | 				eventsToReplace.push({ | ||||||
| 					old: olde, | 					old: olde, | ||||||
| 					new: newe | 					newFallbackContent: newFallbackContent[0], | ||||||
|  | 					newInnerContent: newInnerContent[0] | ||||||
| 				}) | 				}) | ||||||
| 				// These events have been handled now, so remove them from the source arrays | 				// These events have been handled now, so remove them from the source arrays | ||||||
| 				newEvents.shift() | 				shift() | ||||||
| 				oldEventRows.splice(i, 1) | 				oldEventRows.splice(i, 1) | ||||||
| 				// Go all the way back to the start of the next iteration of the outer loop | 				// Go all the way back to the start of the next iteration of the outer loop | ||||||
| 				continue outer | 				continue outer | ||||||
| @@ -69,7 +77,7 @@ async function editToChanges(message, guild) { | |||||||
| 		} | 		} | ||||||
| 		// If we got this far, we could not pair it to an existing event, so it'll have to be a new one | 		// If we got this far, we could not pair it to an existing event, so it'll have to be a new one | ||||||
| 		eventsToSend.push(newe) | 		eventsToSend.push(newe) | ||||||
| 		newEvents.shift() | 		shift() | ||||||
| 	} | 	} | ||||||
| 	// Anything remaining in oldEventRows is present in the old version only and should be redacted. | 	// Anything remaining in oldEventRows is present in the old version only and should be redacted. | ||||||
| 	eventsToRedact = oldEventRows | 	eventsToRedact = oldEventRows | ||||||
| @@ -92,7 +100,7 @@ async function editToChanges(message, guild) { | |||||||
|  |  | ||||||
| 	// Removing unnecessary properties before returning | 	// Removing unnecessary properties before returning | ||||||
| 	eventsToRedact = eventsToRedact.map(e => e.event_id) | 	eventsToRedact = eventsToRedact.map(e => e.event_id) | ||||||
| 	eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, new: eventToReplacementEvent(e.old.event_id, e.new)})) | 	eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, new: eventToReplacementEvent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})) | ||||||
|  |  | ||||||
| 	return {eventsToReplace, eventsToRedact, eventsToSend} | 	return {eventsToReplace, eventsToRedact, eventsToSend} | ||||||
| } | } | ||||||
| @@ -100,31 +108,26 @@ async function editToChanges(message, guild) { | |||||||
| /** | /** | ||||||
|  * @template T |  * @template T | ||||||
|  * @param {string} oldID |  * @param {string} oldID | ||||||
|  * @param {T} content |  * @param {T} newFallbackContent | ||||||
|  |  * @param {T} newInnerContent | ||||||
|  * @returns {import("../../types").Event.ReplacementContent<T>} content |  * @returns {import("../../types").Event.ReplacementContent<T>} content | ||||||
|  */ |  */ | ||||||
| function eventToReplacementEvent(oldID, content) { | function eventToReplacementEvent(oldID, newFallbackContent, newInnerContent) { | ||||||
| 	const newContent = { | 	const content = { | ||||||
| 		...content, | 		...newFallbackContent, | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| 		"m.new_content": { | 		"m.new_content": { | ||||||
| 			...content | 			...newInnerContent | ||||||
| 		}, | 		}, | ||||||
| 		"m.relates_to": { | 		"m.relates_to": { | ||||||
| 			rel_type: "m.replace", | 			rel_type: "m.replace", | ||||||
| 			event_id: oldID | 			event_id: oldID | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if (typeof newContent.body === "string") { | 	delete content["m.new_content"]["$type"] | ||||||
| 		newContent.body = "* " + newContent.body |  | ||||||
| 	} |  | ||||||
| 	if (typeof newContent.formatted_body === "string") { |  | ||||||
| 		newContent.formatted_body = "* " + newContent.formatted_body |  | ||||||
| 	} |  | ||||||
| 	delete newContent["m.new_content"]["$type"] |  | ||||||
| 	// Client-Server API spec 11.37.3: Any m.relates_to property within m.new_content is ignored. | 	// Client-Server API spec 11.37.3: Any m.relates_to property within m.new_content is ignored. | ||||||
| 	delete newContent["m.new_content"]["m.relates_to"] | 	delete content["m.new_content"]["m.relates_to"] | ||||||
| 	return newContent | 	return content | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports.editToChanges = editToChanges | module.exports.editToChanges = editToChanges | ||||||
|   | |||||||
| @@ -47,9 +47,13 @@ test("edit2changes: edit of reply to skull webp attachment with content", async | |||||||
|       oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", |       oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", | ||||||
|       new: { |       new: { | ||||||
|          $type: "m.room.message", |          $type: "m.room.message", | ||||||
|          // TODO: read "edits of replies" in the spec!!! |  | ||||||
|          msgtype: "m.text", |          msgtype: "m.text", | ||||||
|          body: "* Edit", |          body: "> Extremity: Image\n\n* Edit", | ||||||
|  |          format: "org.matrix.custom.html", | ||||||
|  |          formatted_body: | ||||||
|  |             '<mx-reply><blockquote><a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q">In reply to</a> Extremity' | ||||||
|  |             + '<br>Image</blockquote></mx-reply>' | ||||||
|  |             + '* Edit', | ||||||
|          "m.mentions": {}, |          "m.mentions": {}, | ||||||
|          "m.new_content": { |          "m.new_content": { | ||||||
|             msgtype: "m.text", |             msgtype: "m.text", | ||||||
| @@ -60,7 +64,6 @@ test("edit2changes: edit of reply to skull webp attachment with content", async | |||||||
|             rel_type: "m.replace", |             rel_type: "m.replace", | ||||||
|             event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" |             event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" | ||||||
|          } |          } | ||||||
|          // TODO: read "edits of replies" in the spec!!! |  | ||||||
|       } |       } | ||||||
|    }]) |    }]) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -55,9 +55,12 @@ function getDiscordParseCallbacks(message, useHTML) { | |||||||
| /** | /** | ||||||
|  * @param {import("discord-api-types/v10").APIMessage} message |  * @param {import("discord-api-types/v10").APIMessage} message | ||||||
|  * @param {import("discord-api-types/v10").APIGuild} guild |  * @param {import("discord-api-types/v10").APIGuild} guild | ||||||
|  * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API |  * @param {{includeReplyFallback?: boolean, includeEditFallbackStar?: boolean}} options default values: | ||||||
|  |  * - includeReplyFallback: true | ||||||
|  |  * - includeEditFallbackStar: false | ||||||
|  |  * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API | ||||||
|  */ |  */ | ||||||
| async function messageToEvent(message, guild, api) { | async function messageToEvent(message, guild, options = {}, di) { | ||||||
| 	const events = [] | 	const events = [] | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| @@ -99,7 +102,7 @@ async function messageToEvent(message, guild, api) { | |||||||
| 	} | 	} | ||||||
| 	if (repliedToEventOriginallyFromMatrix) { | 	if (repliedToEventOriginallyFromMatrix) { | ||||||
| 		// Need to figure out who sent that event... | 		// Need to figure out who sent that event... | ||||||
| 		const event = await api.getEvent(repliedToEventRoomId, repliedToEventId) | 		const event = await di.api.getEvent(repliedToEventRoomId, repliedToEventId) | ||||||
| 		repliedToEventSenderMxid = event.sender | 		repliedToEventSenderMxid = event.sender | ||||||
| 		// Need to add the sender to m.mentions | 		// Need to add the sender to m.mentions | ||||||
| 		addMention(repliedToEventSenderMxid) | 		addMention(repliedToEventSenderMxid) | ||||||
| @@ -133,7 +136,7 @@ async function messageToEvent(message, guild, api) { | |||||||
| 		if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { | 		if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { | ||||||
| 			const writtenMentionsText = matches.map(m => m[1].toLowerCase()) | 			const writtenMentionsText = matches.map(m => m[1].toLowerCase()) | ||||||
| 			const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) | 			const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) | ||||||
| 			const {joined} = await api.getJoinedMembers(roomID) | 			const {joined} = await di.api.getJoinedMembers(roomID) | ||||||
| 			for (const [mxid, member] of Object.entries(joined)) { | 			for (const [mxid, member] of Object.entries(joined)) { | ||||||
| 				if (!userRegex.some(rx => mxid.match(rx))) { | 				if (!userRegex.some(rx => mxid.match(rx))) { | ||||||
| 					const localpart = mxid.match(/@([^:]*)/) | 					const localpart = mxid.match(/@([^:]*)/) | ||||||
| @@ -143,8 +146,15 @@ async function messageToEvent(message, guild, api) { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Star * prefix for fallback edits | ||||||
|  | 		if (options.includeEditFallbackStar) { | ||||||
|  | 			body = "* " + body | ||||||
|  | 			html = "* " + html | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Fallback body/formatted_body for replies | 		// Fallback body/formatted_body for replies | ||||||
| 		if (repliedToEventId) { | 		// This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run | ||||||
|  | 		if (repliedToEventId && options.includeReplyFallback !== false) { | ||||||
| 			let repliedToDisplayName | 			let repliedToDisplayName | ||||||
| 			let repliedToUserHtml | 			let repliedToUserHtml | ||||||
| 			if (repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid) { | 			if (repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid) { | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ function mockGetEvent(t, roomID_in, eventID_in, outer) { | |||||||
| } | } | ||||||
|  |  | ||||||
| test("message2event: simple plaintext", async t => { | test("message2event: simple plaintext", async t => { | ||||||
| 	const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) | 	const events = await messageToEvent(data.message.simple_plaintext, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -40,7 +40,7 @@ test("message2event: simple plaintext", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: simple user mention", async t => { | test("message2event: simple user mention", async t => { | ||||||
| 	const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) | 	const events = await messageToEvent(data.message.simple_user_mention, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -52,7 +52,7 @@ test("message2event: simple user mention", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: simple room mention", async t => { | test("message2event: simple room mention", async t => { | ||||||
| 	const events = await messageToEvent(data.message.simple_room_mention, data.guild.general) | 	const events = await messageToEvent(data.message.simple_room_mention, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -64,7 +64,7 @@ test("message2event: simple room mention", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: simple message link", async t => { | test("message2event: simple message link", async t => { | ||||||
| 	const events = await messageToEvent(data.message.simple_message_link, data.guild.general) | 	const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -76,7 +76,7 @@ test("message2event: simple message link", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: attachment with no content", async t => { | test("message2event: attachment with no content", async t => { | ||||||
| 	const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) | 	const events = await messageToEvent(data.message.attachment_no_content, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -94,7 +94,7 @@ test("message2event: attachment with no content", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: stickers", async t => { | test("message2event: stickers", async t => { | ||||||
| 	const events = await messageToEvent(data.message.sticker, data.guild.general) | 	const events = await messageToEvent(data.message.sticker, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -127,7 +127,7 @@ test("message2event: stickers", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: skull webp attachment with content", async t => { | test("message2event: skull webp attachment with content", async t => { | ||||||
| 	const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general) | 	const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.mentions": {}, | 		"m.mentions": {}, | ||||||
| @@ -150,7 +150,7 @@ test("message2event: skull webp attachment with content", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: reply to skull webp attachment with content", async t => { | test("message2event: reply to skull webp attachment with content", async t => { | ||||||
| 	const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general) | 	const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general, {}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| 		"m.relates_to": { | 		"m.relates_to": { | ||||||
| @@ -183,15 +183,17 @@ test("message2event: reply to skull webp attachment with content", async t => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| test("message2event: simple reply to matrix user", async t => { | test("message2event: simple reply to matrix user", async t => { | ||||||
| 	const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, { | 	const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {}, { | ||||||
| 		getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { | 		api: { | ||||||
| 			type: "m.room.message", | 			getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { | ||||||
| 			content: { | 				type: "m.room.message", | ||||||
| 				msgtype: "m.text", | 				content: { | ||||||
| 				body: "so can you reply to my webhook uwu" | 					msgtype: "m.text", | ||||||
| 			}, | 					body: "so can you reply to my webhook uwu" | ||||||
| 			sender: "@cadence:cadence.moe" | 				}, | ||||||
| 		}) | 				sender: "@cadence:cadence.moe" | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
| 	}) | 	}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
| 		$type: "m.room.message", | 		$type: "m.room.message", | ||||||
| @@ -215,34 +217,66 @@ test("message2event: simple reply to matrix user", async t => { | |||||||
| 	}]) | 	}]) | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | test("message2event: simple reply to matrix user, reply fallbacks disabled", async t => { | ||||||
|  | 	const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {includeReplyFallback: false}, { | ||||||
|  | 		api: { | ||||||
|  | 			getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { | ||||||
|  | 				type: "m.room.message", | ||||||
|  | 				content: { | ||||||
|  | 					msgtype: "m.text", | ||||||
|  | 					body: "so can you reply to my webhook uwu" | ||||||
|  | 				}, | ||||||
|  | 				sender: "@cadence:cadence.moe" | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 	t.deepEqual(events, [{ | ||||||
|  | 		$type: "m.room.message", | ||||||
|  | 		"m.relates_to": { | ||||||
|  | 			"m.in_reply_to": { | ||||||
|  | 				event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4" | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		"m.mentions": { | ||||||
|  | 			user_ids: [ | ||||||
|  | 				"@cadence:cadence.moe" | ||||||
|  | 			] | ||||||
|  | 		}, | ||||||
|  | 		msgtype: "m.text", | ||||||
|  | 		body: "Reply" | ||||||
|  | 	}]) | ||||||
|  | }) | ||||||
|  |  | ||||||
| test("message2event: simple written @mention for matrix user", async t => { | test("message2event: simple written @mention for matrix user", async t => { | ||||||
| 	const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, { | 	const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, { | ||||||
| 		async getJoinedMembers(roomID) { | 		api: { | ||||||
| 			t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") | 			async getJoinedMembers(roomID) { | ||||||
| 			return new Promise(resolve => { | 				t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") | ||||||
| 				setTimeout(() => { | 				return new Promise(resolve => { | ||||||
| 					resolve({ | 					setTimeout(() => { | ||||||
| 						joined: { | 						resolve({ | ||||||
| 							"@cadence:cadence.moe": { | 							joined: { | ||||||
| 								display_name: "cadence [they]", | 								"@cadence:cadence.moe": { | ||||||
| 								avatar_url: "whatever" | 									display_name: "cadence [they]", | ||||||
| 							}, | 									avatar_url: "whatever" | ||||||
| 							"@huckleton:cadence.moe": { | 								}, | ||||||
| 								display_name: "huck", | 								"@huckleton:cadence.moe": { | ||||||
| 								avatar_url: "whatever" | 									display_name: "huck", | ||||||
| 							}, | 									avatar_url: "whatever" | ||||||
| 							"@_ooye_botrac4r:cadence.moe": { | 								}, | ||||||
| 								display_name: "botrac4r", | 								"@_ooye_botrac4r:cadence.moe": { | ||||||
| 								avatar_url: "whatever" | 									display_name: "botrac4r", | ||||||
| 							}, | 									avatar_url: "whatever" | ||||||
| 							"@_ooye_bot:cadence.moe": { | 								}, | ||||||
| 								display_name: "Out Of Your Element", | 								"@_ooye_bot:cadence.moe": { | ||||||
| 								avatar_url: "whatever" | 									display_name: "Out Of Your Element", | ||||||
|  | 									avatar_url: "whatever" | ||||||
|  | 								} | ||||||
| 							} | 							} | ||||||
| 						} | 						}) | ||||||
| 					}) | 					}) | ||||||
| 				}) | 				}) | ||||||
| 			}) | 			} | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| 	t.deepEqual(events, [{ | 	t.deepEqual(events, [{ | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Cadence Ember
					Cadence Ember