Rearrange code (self-review)

This commit is contained in:
Cadence Ember 2024-03-04 17:02:38 +13:00
parent c5d6c5e4c7
commit 47ac49a855
16 changed files with 275 additions and 74 deletions

8
.c8rc.json Normal file
View file

@ -0,0 +1,8 @@
{
"watermarks": {
"statements": [60, 100],
"lines": [60, 100],
"functions": [60, 100],
"branches": [60, 100]
}
}

View file

@ -93,7 +93,11 @@ async function editToChanges(message, guild, api) {
// We can choose an existing event to promote. Bigger order is better.
const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.event_subtype === "m.text")
eventsToReplace.sort((a, b) => order(b) - order(a))
promotions.push({column, eventID: eventsToReplace[0].old.event_id})
if (column === "part") {
promotions.push({column, eventID: eventsToReplace[0].old.event_id}) // part should be the first one
} else {
promotions.push({column, eventID: eventsToReplace[eventsToReplace.length - 1].old.event_id}) // reaction_part should be the last one
}
} else {
// No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0.
promotions.push({column, nextEvent: true})

View file

@ -175,3 +175,63 @@ test("edit2changes: edit of reply to skull webp attachment with content", async
}
}])
})
test("edit2changes: edits the text event when multiple rows have part = 0 (should never happen in real life, but make sure the safety net works)", async t => {
const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments_but_all_parts_equal_0, data.guild.general, {})
t.deepEqual(eventsToRedact, [])
t.deepEqual(eventsToSend, [])
t.deepEqual(eventsToReplace, [{
oldID: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd999",
newContent: {
$type: "m.room.message",
msgtype: "m.text",
body: "* only the content can be edited",
"m.mentions": {},
// *** Replaced With: ***
"m.new_content": {
msgtype: "m.text",
body: "only the content can be edited",
"m.mentions": {}
},
"m.relates_to": {
rel_type: "m.replace",
event_id: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd999"
}
}
}])
})
test("edit2changes: promotes the text event when multiple rows have part = 1 (should never happen in real life, but make sure the safety net works)", async t => {
const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments_but_all_parts_equal_1, data.guild.general, {})
t.deepEqual(eventsToRedact, [])
t.deepEqual(eventsToSend, [])
t.deepEqual(eventsToReplace, [{
oldID: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd111",
newContent: {
$type: "m.room.message",
msgtype: "m.text",
body: "* only the content can be edited",
"m.mentions": {},
// *** Replaced With: ***
"m.new_content": {
msgtype: "m.text",
body: "only the content can be edited",
"m.mentions": {}
},
"m.relates_to": {
rel_type: "m.replace",
event_id: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd111"
}
}
}])
t.deepEqual(promotions, [
{
column: "part",
eventID: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd111"
},
{
column: "reaction_part",
eventID: "$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd111"
}
])
})

View file

@ -1,5 +1,6 @@
// @ts-check
const assert = require("assert")
const stream = require("stream")
const {PNG} = require("pngjs")
@ -27,7 +28,7 @@ async function convert(text) {
/** @type RlottieWasm */
const rh = new r.RlottieWasm()
const status = rh.load(text)
if (!status) throw new Error(`Rlottie unable to load ${text.length} byte data file.`)
assert(status, `Rlottie unable to load ${text.length} byte data file.`)
const rendered = rh.render(0, SIZE, SIZE)
let png = new PNG({
width: SIZE,
@ -38,11 +39,9 @@ async function convert(text) {
inputHasAlpha: true,
})
png.data = Buffer.from(rendered)
// The transform stream is necessary because PNG requires me to pipe it somewhere before this event loop ends
const resultStream = png.pack()
const p = new stream.PassThrough()
resultStream.pipe(p)
return p
// png.pack() is a bad stream and will throw away any data it sends if it's not connected to a destination straight away.
// We use Duplex.from to convert it into a good stream.
return stream.Duplex.from(png.pack())
}
module.exports.convert = convert

View file

@ -78,10 +78,14 @@ function getDiscordParseCallbacks(message, guild, useHTML) {
return `@${role.name}:`
}
},
everyone: node =>
"@room",
here: node =>
"@here"
everyone: () => {
if (message.mention_everyone) return "@room"
return "@everyone"
},
here: () => {
if (message.mention_everyone) return "@room"
return "@here"
}
}
}
@ -199,6 +203,7 @@ async function attachmentToEvent(mentions, attachment) {
async function messageToEvent(message, guild, options = {}, di) {
const events = []
/* c8 ignore next 7 */
if (message.type === DiscordTypes.MessageType.ThreadCreated) {
// This is the kind of message that appears when somebody makes a thread which isn't close enough to the message it's based off.
// It lacks the lines and the pill, so it looks kind of like a member join message, and it says:

View file

@ -60,7 +60,7 @@ function userToSimName(user) {
// 1. Is sim user already registered?
const existing = select("sim", "sim_name", {user_id: user.id}).pluck().get()
if (existing) return existing
assert.equal(existing, null, "Shouldn't try to create a new name for an existing sim")
// 2. Register based on username (could be new or old format)
// (Unless it's a special user, in which case copy their provided mappings.)

View file

@ -0,0 +1,36 @@
// @ts-check
const assert = require("assert")
const fetch = require("node-fetch").default
const utils = require("../converters/utils")
const {sync} = require("../../passthrough")
/** @type {import("../converters/emoji-sheet")} */
const emojiSheetConverter = sync.require("../converters/emoji-sheet")
/**
* Downloads the emoji from the web and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function getAndConvertEmoji(mxc) {
const abortController = new AbortController()
const url = utils.getPublicUrlForMxc(mxc)
assert(url)
/** @type {import("node-fetch").Response} */
// If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing.
// If we were using connection pooling, we would be forced to download the entire GIF.
// So we set no agent to ensure we are not connection pooling.
// @ts-ignore the signal is slightly different from the type it wants (still works fine)
const res = await fetch(url, {agent: false, signal: abortController.signal})
return emojiSheetConverter.convertImageStream(res.body, () => {
abortController.abort()
res.body.pause()
res.body.emit("end")
})
}
module.exports.getAndConvertEmoji = getAndConvertEmoji

View file

@ -17,8 +17,8 @@ const eventToMessage = sync.require("../converters/event-to-message")
const api = sync.require("../../matrix/api")
/** @type {import("../../d2m/actions/register-user")} */
const registerUser = sync.require("../../d2m/actions/register-user")
/** @type {import("../converters/emoji-sheet")} */
const emojiSheet = sync.require("../converters/emoji-sheet")
/** @type {import("../actions/emoji-sheet")} */
const emojiSheet = sync.require("../actions/emoji-sheet")
/**
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message

View file

@ -1,13 +1,10 @@
// @ts-check
const assert = require("assert").strict
const fs = require("fs")
const {pipeline} = require("stream").promises
const sharp = require("sharp")
const {GIFrame} = require("giframe")
const {PNG} = require("pngjs")
const utils = require("./utils")
const fetch = require("node-fetch").default
const streamMimeType = require("stream-mime-type")
const SIZE = 48
@ -50,49 +47,6 @@ async function compositeMatrixEmojis(mxcs, mxcDownloader) {
return output.data
}
/**
* Downloads the emoji from the web and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function getAndConvertEmoji(mxc) {
const abortController = new AbortController()
const url = utils.getPublicUrlForMxc(mxc)
assert(url)
/** @type {import("node-fetch").Response} */
// If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing.
// If we were using connection pooling, we would be forced to download the entire GIF.
// So we set no agent to ensure we are not connection pooling.
// @ts-ignore the signal is slightly different from the type it wants (still works fine)
const res = await fetch(url, {agent: false, signal: abortController.signal})
return convertImageStream(res.body, () => {
abortController.abort()
res.body.pause()
res.body.emit("end")
})
}
/**
* MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function _mockGetAndConvertEmoji(mxc) {
const id = mxc.match(/\/([^./]*)$/)?.[1]
let s
if (fs.existsSync(`test/res/${id}.png`)) {
s = fs.createReadStream(`test/res/${id}.png`)
} else {
s = fs.createReadStream(`test/res/${id}.gif`)
}
return convertImageStream(s, () => {
s.pause()
s.emit("end")
})
}
/**
* @param {import("node-fetch").Response["body"]} streamIn
* @param {() => any} stopStream
@ -156,6 +110,4 @@ async function convertImageStream(streamIn, stopStream) {
}
module.exports.compositeMatrixEmojis = compositeMatrixEmojis
module.exports.getAndConvertEmoji = getAndConvertEmoji
module.exports._mockGetAndConvertEmoji = _mockGetAndConvertEmoji
module.exports._convertImageStream = convertImageStream
module.exports.convertImageStream = convertImageStream

View file

@ -1,6 +1,5 @@
const assert = require("assert").strict
const {test} = require("supertape")
const {_convertImageStream} = require("./emoji-sheet")
const {convertImageStream} = require("./emoji-sheet")
const fs = require("fs")
const {Transform} = require("stream").Transform
@ -33,7 +32,7 @@ async function runSingleTest(t, path, totalSize, sizeCheck) {
const file = fs.createReadStream(path)
const meter = new Meter()
const p = file.pipe(meter)
const result = await _convertImageStream(p, () => {
const result = await convertImageStream(p, () => {
file.pause()
file.emit("end")
})

View file

@ -1,10 +1,11 @@
const assert = require("assert").strict
const fs = require("fs")
const {test} = require("supertape")
const {eventToMessage} = require("./event-to-message")
const {_mockGetAndConvertEmoji} = require("./emoji-sheet")
const {convertImageStream} = require("./emoji-sheet")
const data = require("../../test/data")
const {MatrixServerError} = require("../../matrix/mreq")
const {db, select, discord} = require("../../passthrough")
const {select, discord} = require("../../passthrough")
/* c8 ignore next 7 */
function slow() {
@ -47,6 +48,25 @@ function sameFirstContentAndWhitespace(t, a, b) {
t.equal(a2, b2)
}
/**
* MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function mockGetAndConvertEmoji(mxc) {
const id = mxc.match(/\/([^./]*)$/)?.[1]
let s
if (fs.existsSync(`test/res/${id}.png`)) {
s = fs.createReadStream(`test/res/${id}.png`)
} else {
s = fs.createReadStream(`test/res/${id}.gif`)
}
return convertImageStream(s, () => {
s.pause()
s.emit("end")
})
}
test("event2message: body is used when there is no formatted_body", async t => {
t.deepEqual(
await eventToMessage({
@ -3535,7 +3555,7 @@ slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet"
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {}, {mxcDownloader: _mockGetAndConvertEmoji})
}, {}, {mxcDownloader: mockGetAndConvertEmoji})
const testResult = {
content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3560,7 +3580,7 @@ slow()("event2message: known emoji from an unreachable server at the end is reup
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {}, {mxcDownloader: _mockGetAndConvertEmoji})
}, {}, {mxcDownloader: mockGetAndConvertEmoji})
const testResult = {
content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3585,7 +3605,7 @@ slow()("event2message: known and unknown emojis in the end are reuploaded as a s
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {}, {mxcDownloader: _mockGetAndConvertEmoji})
}, {}, {mxcDownloader: mockGetAndConvertEmoji})
const testResult = {
content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3610,7 +3630,7 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet
},
event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4",
room_id: "!maggESguZBqGBZtSnr:cadence.moe"
}, {}, {mxcDownloader: _mockGetAndConvertEmoji})
}, {}, {mxcDownloader: mockGetAndConvertEmoji})
const testResult = {
content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name,

View file

@ -1,3 +1,4 @@
const assert = require("assert")
const {kstateToState, stateToKState, diffKState, kstateStripConditionals} = require("./kstate")
const {test} = require("supertape")
@ -162,3 +163,29 @@ test("diffKState: power levels are mixed together", t => {
})
t.notDeepEqual(original, result)
})
test("diffKState: cannot merge power levels if original power levels are missing", t => {
const original = {}
assert.throws(() =>
diffKState(original, {
"m.room.power_levels/": {
"events": {
"m.room.avatar": 0
}
}
})
, /original power level data is missing/)
t.pass()
})
test("diffKState: kstate keys must contain a slash separator", t => {
assert.throws(() =>
diffKState({
"m.room.name/": {name: "test name"},
}, {
"m.room.name/": {name: "test name"},
"new": {a: 2}
})
, /does not contain a slash separator/)
t.pass()
})

View file

@ -54,6 +54,6 @@
"addbot": "node addbot.js",
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot",
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
"cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -r html -r text supertape --no-check-assertions-count --format fail test/test.js -- --slow"
"cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -x d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
}
}

View file

@ -2662,6 +2662,88 @@ module.exports = {
name: "pomu puff"
}]
},
edited_content_with_sticker_and_attachments_but_all_parts_equal_0: {
id: "1106366167788044451",
type: 0,
content: "only the content can be edited",
channel_id: "122155380120748034",
author: {
id: "113340068197859328",
username: "Cookie 🍪",
global_name: null,
display_name: null,
avatar: "b48302623a12bc7c59a71328f72ccb39",
discriminator: "7766",
public_flags: 128,
avatar_decoration: null
},
attachments: [{
id: "1106366167486038016",
filename: "image.png",
size: 127373,
url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png",
proxy_url: "https://media.discordapp.net/attachments/122155380120748034/1106366167486038016/image.png",
width: 333,
height: 287,
content_type: "image/png"
}],
embeds: [],
mentions: [],
mention_roles: [],
pinned: false,
mention_everyone: false,
tts: false,
timestamp: "2023-05-11T23:44:09.690000+00:00",
edited_timestamp: "2023-05-11T23:44:19.690000+00:00",
flags: 0,
components: [],
sticker_items: [{
id: "1106323941183717586",
format_type: 1,
name: "pomu puff"
}]
},
edited_content_with_sticker_and_attachments_but_all_parts_equal_1: {
id: "1106366167788044452",
type: 0,
content: "only the content can be edited",
channel_id: "122155380120748034",
author: {
id: "113340068197859328",
username: "Cookie 🍪",
global_name: null,
display_name: null,
avatar: "b48302623a12bc7c59a71328f72ccb39",
discriminator: "7766",
public_flags: 128,
avatar_decoration: null
},
attachments: [{
id: "1106366167486038016",
filename: "image.png",
size: 127373,
url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png",
proxy_url: "https://media.discordapp.net/attachments/122155380120748034/1106366167486038016/image.png",
width: 333,
height: 287,
content_type: "image/png"
}],
embeds: [],
mentions: [],
mention_roles: [],
pinned: false,
mention_everyone: false,
tts: false,
timestamp: "2023-05-11T23:44:09.690000+00:00",
edited_timestamp: "2023-05-11T23:44:19.690000+00:00",
flags: 0,
components: [],
sticker_items: [{
id: "1106323941183717586",
format_type: 1,
name: "pomu puff"
}]
},
edit_of_reply_to_skull_webp_attachment_with_content: {
type: 19,
tts: false,

View file

@ -33,6 +33,8 @@ INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
INSERT INTO message_channel (message_id, channel_id) VALUES
('1106366167788044450', '122155380120748034'),
('1106366167788044451', '122155380120748034'),
('1106366167788044452', '122155380120748034'),
('1126786462646550579', '112760669178241024'),
('1128084748338741392', '112760669178241024'),
('1128084851279536279', '112760669178241024'),
@ -68,6 +70,12 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', 0, 1, 1),
('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', 1, 1, 0),
('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', 1, 0, 0),
('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd999', 'm.room.message', 'm.text', '1106366167788044451', 0, 0, 1),
('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZI999', 'm.room.message', 'm.image', '1106366167788044451', 0, 0, 1),
('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd999', 'm.sticker', NULL, '1106366167788044451', 0, 0, 1),
('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd111', 'm.room.message', 'm.text', '1106366167788044452', 1, 1, 1),
('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZI111', 'm.room.message', 'm.image', '1106366167788044452', 1, 1, 1),
('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd111', 'm.sticker', NULL, '1106366167788044452', 1, 1, 1),
('$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04', 'm.room.message', 'm.text', '1144865310588014633', 0, 0, 1),
('$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8', 'm.room.message', 'm.text', '1144874214311067708', 0, 0, 0),
('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs', 'm.room.message', 'm.text', '1145688633186193479', 0, 0, 0),

View file

@ -49,6 +49,7 @@ passthrough.from = orm.from
passthrough.select = orm.select
const file = sync.require("../matrix/file")
/* c8 ignore next */
file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) }
;(async () => {
@ -88,7 +89,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"},
{url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"},
{url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.png"},
{url: "https://ezgif.com/images/format-demo/butterfly.png", to: "test/res/butterfly.png"},
])
}, {timeout: 60000})
}