More code coverage
This commit is contained in:
@@ -67,6 +67,6 @@
|
||||
"addbot": "node addbot.js",
|
||||
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker 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 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
|
||||
"cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/m2d/event-dispatcher.js -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +271,61 @@ test("edit2changes: promotes the text event when multiple rows have part = 1 (sh
|
||||
])
|
||||
})
|
||||
|
||||
test("edit2changes: promotes newly sent event", async t => {
|
||||
const {eventsToReplace, eventsToRedact, eventsToSend, promotions} = await editToChanges({
|
||||
channel_id: "1160894080998461480",
|
||||
id: "1404133238414376971",
|
||||
content: "hi",
|
||||
attachments: [{
|
||||
id: "1157854643037163610",
|
||||
filename: "Screenshot_20231001_034036.jpg",
|
||||
size: 51981,
|
||||
url: "https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&",
|
||||
proxy_url: "https://media.discordapp.net/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&",
|
||||
width: 1080,
|
||||
height: 1170,
|
||||
content_type: "image/jpeg"
|
||||
}],
|
||||
author: {
|
||||
username: "cadence.worm",
|
||||
global_name: "Cadence"
|
||||
}
|
||||
}, data.guild.general, {
|
||||
async getEvent(roomID, eventID) {
|
||||
t.equal(eventID, "$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I")
|
||||
return {
|
||||
type: "m.room.message",
|
||||
sender: "@_ooye_cadence.worm:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hi"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.deepEqual(eventsToRedact, ["$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk"])
|
||||
t.deepEqual(eventsToReplace, [])
|
||||
t.deepEqual(eventsToSend, [{
|
||||
$type: "m.room.message",
|
||||
body: "Screenshot_20231001_034036.jpg",
|
||||
external_url: "https://bridge.example.org/download/discordcdn/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg",
|
||||
filename: "Screenshot_20231001_034036.jpg",
|
||||
info: {
|
||||
mimetype: "image/jpeg",
|
||||
size: 51981,
|
||||
w: 1080,
|
||||
h: 1170
|
||||
},
|
||||
url: "mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.image"
|
||||
}])
|
||||
t.deepEqual(promotions, [
|
||||
{column: "reaction_part", nextEvent: true}
|
||||
])
|
||||
// assert that the event parts will be consistent in database after this
|
||||
})
|
||||
|
||||
test("edit2changes: generated embed", async t => {
|
||||
let called = 0
|
||||
const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_social_media_image, data.guild.general, {
|
||||
|
||||
@@ -178,7 +178,7 @@ async function attachmentToEvent(mentions, attachment) {
|
||||
info: {
|
||||
mimetype: attachment.content_type,
|
||||
size: attachment.size,
|
||||
duration: attachment.duration_secs ? Math.round(attachment.duration_secs * 1000) : undefined
|
||||
duration: attachment.duration_secs && Math.round(attachment.duration_secs * 1000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1356,3 +1356,83 @@ test("message2event: channel links are converted even inside lists (parser post-
|
||||
])
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("message2event: emoji added special message", async t => {
|
||||
const events = await messageToEvent(data.special_message.emoji_added)
|
||||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.emote",
|
||||
body: "added a new emoji, :cx_marvelous: :cx_marvelous:",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `added a new emoji, <img data-mx-emoticon height="32" src="mxc://cadence.moe/TPZdosVUjTIopsLijkygIbti" title=":cx_marvelous:" alt=":cx_marvelous:"> :cx_marvelous:`,
|
||||
"m.mentions": {}
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
test("message2event: cross-room reply", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent({
|
||||
type: 19,
|
||||
message_reference: {
|
||||
channel_id: "1161864271370666075",
|
||||
guild_id: "1160893336324931584",
|
||||
message_id: "1458091145136443547"
|
||||
},
|
||||
referenced_message: {
|
||||
channel_id: "1161864271370666075",
|
||||
id: "1458091145136443547",
|
||||
content: "",
|
||||
attachments: [{
|
||||
filename: "image.png",
|
||||
id: "1456813607693193478",
|
||||
size: 104006,
|
||||
content_type: "image/png",
|
||||
url: "https://cdn.discordapp.com/attachments/1160893337029586956/1458790740338409605/image.png?ex=696194ff&is=6960437f&hm=923d0ef7d1b249470be49edbc37628cc4ff8a438f0ab12f54c045578135f7050"
|
||||
}],
|
||||
author: {
|
||||
username: "Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆"
|
||||
}
|
||||
},
|
||||
content: "cross-room reply"
|
||||
}, {}, {}, {api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
t.equal(roomID, "!mHmhQQPwXNananaOLD:cadence.moe")
|
||||
t.equal(eventID, "$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY")
|
||||
return {
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
"body": "image.png",
|
||||
"info": {
|
||||
"h": 738,
|
||||
"mimetype": "image/png",
|
||||
"org.matrix.msc4230.is_animated": false,
|
||||
"size": 111189,
|
||||
"w": 772,
|
||||
"xyz.amorgan.blurhash": "L255Oa~qRPD$-pxuoJoLIUM{xuxu"
|
||||
},
|
||||
"m.mentions": {},
|
||||
"msgtype": "m.image",
|
||||
"url": "mxc://matrix.org/QbSujQjRLekzPknKlPsXbGDS"
|
||||
}
|
||||
}
|
||||
}
|
||||
}})
|
||||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "> Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆: [Media]\n\ncross-room reply",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<blockquote><a href="https://matrix.to/#/!mHmhQQPwXNananaOLD:cadence.moe/$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY">In reply to</a> <a href="https://matrix.to/#/@cadence:cadence.moe">Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆</a><br>[Media]</blockquote>cross-room reply`,
|
||||
"m.mentions": {
|
||||
user_ids: [
|
||||
"@cadence:cadence.moe"
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
@@ -108,12 +108,7 @@ function isWebhookUserID(userID) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function webhookAuthorToSimName(author) {
|
||||
if (SPECIAL_USER_MAPPINGS.has(author.id)) {
|
||||
const error = new Error("Special users should have followed the other code path.")
|
||||
// @ts-ignore
|
||||
error.author = author
|
||||
throw error
|
||||
}
|
||||
assert(!SPECIAL_USER_MAPPINGS.has(author.id), "Special users should have followed the other code path.")
|
||||
|
||||
// 1. Is sim user already registered?
|
||||
const fakeUserID = webhookAuthorToFakeUserID(author)
|
||||
|
||||
@@ -2,7 +2,7 @@ const {test} = require("supertape")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const assert = require("assert")
|
||||
const data = require("../../../test/data")
|
||||
const {userToSimName} = require("./user-to-mxid")
|
||||
const {userToSimName, webhookAuthorToSimName} = require("./user-to-mxid")
|
||||
|
||||
test("user2name: cannot create user for a webhook", async t => {
|
||||
const [error] = await tryToCatch(() => userToSimName({discriminator: "0000"}))
|
||||
@@ -52,3 +52,11 @@ test("user2name: includes ID if requested in config", t => {
|
||||
t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f")
|
||||
reg.ooye.include_user_id_in_mxid = false
|
||||
})
|
||||
|
||||
test("webhook author: can generate sim names", t => {
|
||||
t.equal(webhookAuthorToSimName({
|
||||
username: "Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆",
|
||||
avatar: null,
|
||||
id: "123"
|
||||
}), "webhook_cadence_maid_of_creation_eye_of_clarity_empress_of_hope")
|
||||
})
|
||||
|
||||
@@ -180,3 +180,15 @@ test("isEphemeralMessage: doesn't detect normal message", t => {
|
||||
test("getPublicUrlForCdn: no-op on non-discord URL", t => {
|
||||
t.equal(utils.getPublicUrlForCdn("https://cadence.moe"), "https://cadence.moe")
|
||||
})
|
||||
|
||||
test("how old: now", t => {
|
||||
t.equal(utils.howOldUnbridgedMessage(new Date().toISOString(), new Date().toISOString()), "an unbridged message")
|
||||
})
|
||||
|
||||
test("how old: hours", t => {
|
||||
t.equal(utils.howOldUnbridgedMessage("2026-01-01T00:00:00", "2026-01-01T03:10:00"), "a 3-hour-old unbridged message")
|
||||
})
|
||||
|
||||
test("how old: days", t => {
|
||||
t.equal(utils.howOldUnbridgedMessage("2024-01-01", "2025-01-01"), "a 366-day-old unbridged message")
|
||||
})
|
||||
|
||||
@@ -332,7 +332,7 @@ async event => {
|
||||
const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
|
||||
if (event.state_key === bot) {
|
||||
const upgraded = await roomUpgrade.onBotMembership(event)
|
||||
const upgraded = await roomUpgrade.onBotMembership(event, api, createRoom)
|
||||
if (upgraded) return
|
||||
}
|
||||
|
||||
@@ -406,7 +406,9 @@ sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
||||
*/
|
||||
async event => {
|
||||
await roomUpgrade.onTombstone(event)
|
||||
if (event.state_key !== "") return
|
||||
if (!event.content.replacement_room) return
|
||||
await roomUpgrade.onTombstone(event, api)
|
||||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
|
||||
@@ -58,7 +58,7 @@ async function addButton(roomID, eventID, key, mxid) {
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
buttons = buttons.filter(b => now - b.created < 2*60*60*1000)
|
||||
}, 10*60*1000)
|
||||
}, 10*60*1000).unref()
|
||||
|
||||
/** @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event */
|
||||
function onReactionAdd(event) {
|
||||
|
||||
@@ -4,12 +4,8 @@ const assert = require("assert/strict")
|
||||
const Ty = require("../types")
|
||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||
const {discord, db, sync, as, select, from} = require("../passthrough")
|
||||
const {db, sync, select, from} = require("../passthrough")
|
||||
|
||||
/** @type {import("./api")}) */
|
||||
const api = sync.require("./api")
|
||||
/** @type {import("../d2m/actions/create-room")}) */
|
||||
const createRoom = sync.require("../d2m/actions/create-room")
|
||||
/** @type {import("./utils")}) */
|
||||
const utils = sync.require("./utils")
|
||||
|
||||
@@ -17,11 +13,12 @@ const roomUpgradeSema = new Semaphore()
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
||||
* @param {import("./api")} api
|
||||
*/
|
||||
async function onTombstone(event) {
|
||||
// Validate
|
||||
if (event.state_key !== "") return
|
||||
if (!event.content.replacement_room) return
|
||||
async function onTombstone(event, api) {
|
||||
// Preconditions (checked by event-dispatcher, enforced here)
|
||||
assert.equal(event.state_key, "")
|
||||
assert.ok(event.content.replacement_room)
|
||||
|
||||
// Set up
|
||||
const oldRoomID = event.room_id
|
||||
@@ -48,13 +45,21 @@ async function onTombstone(event) {
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Member>} event
|
||||
* @param {import("./api")} api
|
||||
* @param {import("../d2m/actions/create-room")} createRoom
|
||||
* @returns {Promise<boolean>} whether to cancel other membership actions
|
||||
*/
|
||||
async function onBotMembership(event) {
|
||||
async function onBotMembership(event, api, createRoom) {
|
||||
// Preconditions (checked by event-dispatcher, enforced here)
|
||||
assert.equal(event.type, "m.room.member")
|
||||
assert.equal(event.state_key, utils.bot)
|
||||
|
||||
// Check if an upgrade is pending for this room
|
||||
const newRoomID = event.room_id
|
||||
const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
|
||||
if (!oldRoomID) return
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
|
||||
|
||||
// Check if is join/invite
|
||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return
|
||||
@@ -65,9 +70,6 @@ async function onBotMembership(event) {
|
||||
await api.joinRoom(newRoomID)
|
||||
}
|
||||
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow)
|
||||
|
||||
// Remove old room from space
|
||||
await api.sendState(channelRow.space_id, "m.space.child", oldRoomID, {})
|
||||
// await api.sendState(oldRoomID, "m.space.parent", spaceID, {}) // keep this - the room isn't advertised but should still be grouped if opened
|
||||
@@ -75,7 +77,7 @@ async function onBotMembership(event) {
|
||||
// Remove declaration that old room is bridged (if able)
|
||||
try {
|
||||
await api.sendState(oldRoomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${channelRow.guild_id}/${channelRow.channel_id}`, {})
|
||||
} catch (e) {}
|
||||
} catch (e) { /* c8 ignore next */ }
|
||||
|
||||
// Update database
|
||||
db.transaction(() => {
|
||||
|
||||
169
src/matrix/room-upgrade.test.js
Normal file
169
src/matrix/room-upgrade.test.js
Normal file
@@ -0,0 +1,169 @@
|
||||
const {test} = require("supertape")
|
||||
const {select} = require("../passthrough")
|
||||
const {onTombstone, onBotMembership} = require("./room-upgrade")
|
||||
|
||||
test("join upgraded room: only cares about upgrades in progress", async t => {
|
||||
let called = 0
|
||||
await onBotMembership({
|
||||
type: "m.room.member",
|
||||
state_key: "@_ooye_bot:cadence.moe",
|
||||
room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe",
|
||||
content: {
|
||||
membership: "invite"
|
||||
}
|
||||
}, {
|
||||
/* c8 ignore next 4 */
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
throw new Error("should not join this room")
|
||||
}
|
||||
})
|
||||
t.equal(called, 0)
|
||||
})
|
||||
|
||||
test("tombstone: only cares about bridged rooms", async t => {
|
||||
let called = 0
|
||||
await onTombstone({
|
||||
event_id: "$tombstone",
|
||||
type: "m.room.tombstone",
|
||||
state_key: "",
|
||||
sender: "@cadence:cadence.moe",
|
||||
origin_server_ts: 0,
|
||||
room_id: "!imaginary:cadence.moe",
|
||||
content: {
|
||||
body: "This room has been replaced",
|
||||
replacement_room: "!JBxeGYnzQwLnaooNEW:cadence.moe"
|
||||
}
|
||||
}, {
|
||||
/* c8 ignore next 4 */
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
throw new Error("should not join this room")
|
||||
}
|
||||
})
|
||||
t.equal(called, 0)
|
||||
})
|
||||
|
||||
test("tombstone: joins new room and stores upgrade in database", async t => {
|
||||
let called = 0
|
||||
await onTombstone({
|
||||
event_id: "$tombstone",
|
||||
type: "m.room.tombstone",
|
||||
state_key: "",
|
||||
sender: "@cadence:cadence.moe",
|
||||
origin_server_ts: 0,
|
||||
room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe",
|
||||
content: {
|
||||
body: "This room has been replaced",
|
||||
replacement_room: "!JBxeGYnzQwLnaooNEW:cadence.moe"
|
||||
}
|
||||
}, {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!JBxeGYnzQwLnaooNEW:cadence.moe")
|
||||
return roomID
|
||||
}
|
||||
})
|
||||
t.equal(called, 1)
|
||||
t.ok(select("room_upgrade_pending", ["old_room_id", "new_room_id"], {new_room_id: "!JBxeGYnzQwLnaooNEW:cadence.moe", old_room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe"}).get())
|
||||
})
|
||||
|
||||
test("tombstone: requests invite from upgrader if can't join room", async t => {
|
||||
let called = 0
|
||||
await onTombstone({
|
||||
event_id: "$tombstone",
|
||||
type: "m.room.tombstone",
|
||||
state_key: "",
|
||||
sender: "@cadence:cadence.moe",
|
||||
origin_server_ts: 0,
|
||||
room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe",
|
||||
content: {
|
||||
body: "This room has been replaced",
|
||||
replacement_room: "!JBxeGYnzQwLnaooNEW:cadence.moe"
|
||||
}
|
||||
}, {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!JBxeGYnzQwLnaooNEW:cadence.moe")
|
||||
throw new Error("access denied or something")
|
||||
},
|
||||
async usePrivateChat(sender) {
|
||||
called++
|
||||
t.equal(sender, "@cadence:cadence.moe")
|
||||
return "!private"
|
||||
},
|
||||
async sendEvent(roomID, type, content) {
|
||||
called++
|
||||
t.equal(roomID, "!private")
|
||||
t.equal(type, "m.room.message")
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.text",
|
||||
body: "You upgraded the bridged room winners. To keep bridging, I need you to invite me to the new room: https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `You upgraded the bridged room <strong>winners</strong>. To keep bridging, I need you to invite me to the new room: <a href="https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe">https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe</a>`
|
||||
})
|
||||
}
|
||||
})
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
test("join upgraded room: only cares about invites/joins", async t => {
|
||||
let called = 0
|
||||
await onBotMembership({
|
||||
type: "m.room.member",
|
||||
state_key: "@_ooye_bot:cadence.moe",
|
||||
room_id: "!JBxeGYnzQwLnaooNEW:cadence.moe",
|
||||
content: {
|
||||
membership: "leave"
|
||||
}
|
||||
}, {
|
||||
/* c8 ignore next 4 */
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
throw new Error("should not join this room")
|
||||
}
|
||||
})
|
||||
t.equal(called, 0)
|
||||
})
|
||||
|
||||
test("join upgraded room: joins invited room, updates database", async t => {
|
||||
let called = 0
|
||||
await onBotMembership({
|
||||
type: "m.room.member",
|
||||
state_key: "@_ooye_bot:cadence.moe",
|
||||
room_id: "!JBxeGYnzQwLnaooNEW:cadence.moe",
|
||||
content: {
|
||||
membership: "invite"
|
||||
}
|
||||
}, {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!JBxeGYnzQwLnaooNEW:cadence.moe")
|
||||
return roomID
|
||||
},
|
||||
async sendState(roomID, type, key, content) {
|
||||
called++
|
||||
if (type === "m.space.child") {
|
||||
t.equal(roomID, "!CvQMeeqXIkgedUpkzv:cadence.moe") // space
|
||||
t.equal(key, "!JBxeGYnzQwLnaooOLD:cadence.moe")
|
||||
t.deepEqual(content, {})
|
||||
return "$child"
|
||||
} else if (type === "uk.half-shot.bridge") {
|
||||
t.equal(roomID, "!JBxeGYnzQwLnaooOLD:cadence.moe")
|
||||
t.equal(key, "moe.cadence.ooye://discord/1345641201902288987/598707048112193536")
|
||||
t.deepEqual(content, {})
|
||||
return "$bridge"
|
||||
}
|
||||
/* c8 ignore next */
|
||||
throw new Error(`unexpected sendState: ${roomID} - ${type}/${key}`)
|
||||
}
|
||||
}, {
|
||||
async syncRoom(channelID) {
|
||||
called++
|
||||
t.equal(channelID, "598707048112193536")
|
||||
}
|
||||
})
|
||||
t.equal(called, 4)
|
||||
t.equal(select("channel_room", "room_id", {channel_id: "598707048112193536"}).pluck().get(), "!JBxeGYnzQwLnaooNEW:cadence.moe")
|
||||
t.equal(select("historical_channel_room", "historical_room_index", {reference_channel_id: "598707048112193536"}).pluck().all().length, 2)
|
||||
})
|
||||
@@ -83,3 +83,5 @@ function defineMediaProxyHandler(domain) {
|
||||
|
||||
as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com"))
|
||||
as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net"))
|
||||
|
||||
module.exports._cache = cache
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const {_cache} = require("./download-discord")
|
||||
|
||||
test("web download discord: access denied if not a known attachment", async t => {
|
||||
const [error] = await tryToCatch(() =>
|
||||
@@ -12,19 +13,6 @@ test("web download discord: access denied if not a known attachment", async t =>
|
||||
channel_id: "1",
|
||||
attachment_id: "2",
|
||||
file_name: "image.png"
|
||||
},
|
||||
snow: {
|
||||
channel: {
|
||||
async refreshAttachmentURLs(attachments) {
|
||||
if (typeof attachments === "string") attachments = [attachments]
|
||||
return {
|
||||
refreshed_urls: attachments.map(a => ({
|
||||
original: a,
|
||||
refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}`
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -43,7 +31,7 @@ test("web download discord: works if a known attachment", async t => {
|
||||
snow: {
|
||||
channel: {
|
||||
async refreshAttachmentURLs(attachments) {
|
||||
if (typeof attachments === "string") attachments = [attachments]
|
||||
assert(Array.isArray(attachments))
|
||||
return {
|
||||
refreshed_urls: attachments.map(a => ({
|
||||
original: a,
|
||||
@@ -70,7 +58,7 @@ test("web download discord: uses cache", async t => {
|
||||
event,
|
||||
snow: {
|
||||
channel: {
|
||||
// @ts-ignore
|
||||
/* c8 ignore next 4 */
|
||||
async refreshAttachmentURLs(attachments) {
|
||||
notCalled = false
|
||||
throw new Error("tried to refresh when it should be in cache")
|
||||
@@ -80,3 +68,30 @@ test("web download discord: uses cache", async t => {
|
||||
})
|
||||
t.ok(notCalled)
|
||||
})
|
||||
|
||||
test("web download discord: refreshes when cache has expired", async t => {
|
||||
_cache.set(`https://cdn.discordapp.com/attachments/655216173696286746/1314358913482621010/image.png`, Promise.resolve(`https://cdn.discordapp.com/blah?ex=${Math.floor(new Date("2026-01-01").getTime() / 1000 + 3600).toString(16)}`))
|
||||
let called = 0
|
||||
await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", {
|
||||
params: {
|
||||
channel_id: "655216173696286746",
|
||||
attachment_id: "1314358913482621010",
|
||||
file_name: "image.png"
|
||||
},
|
||||
snow: {
|
||||
channel: {
|
||||
async refreshAttachmentURLs(attachments) {
|
||||
called++
|
||||
assert(Array.isArray(attachments))
|
||||
return {
|
||||
refreshed_urls: attachments.map(a => ({
|
||||
original: a,
|
||||
refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}`
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
@@ -73,23 +73,47 @@ function filterTo(xs, fn) {
|
||||
return filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{type: number, parent_id?: string, position?: number}} channel
|
||||
* @param {Map<string, {type: number, parent_id?: string, position?: number}>} channels
|
||||
*/
|
||||
function getPosition(channel, channels) {
|
||||
let position = 0
|
||||
|
||||
// Categories always appear below un-categorised channels. Their contents can be ordered.
|
||||
// So categories, and things in them, will have their position multiplied by a big number. The category's big number. The regular position small number sorts within the category.
|
||||
// Categories are size 2000.
|
||||
let foundCategory = channel
|
||||
while (foundCategory.parent_id) {
|
||||
foundCategory = channels.get(foundCategory.parent_id)
|
||||
}
|
||||
if (foundCategory.type === DiscordTypes.ChannelType.GuildCategory) position = (foundCategory.position + 1) * 2000
|
||||
|
||||
// Categories always appear above what they contain.
|
||||
if (channel.type === DiscordTypes.ChannelType.GuildCategory) position -= 0.5
|
||||
|
||||
// Within a category, voice channels are always sorted to the bottom. The text/voice split is size 1000 each.
|
||||
if ([DiscordTypes.ChannelType.GuildVoice, DiscordTypes.ChannelType.GuildStageVoice].includes(channel.type)) position += 1000
|
||||
|
||||
// Channels are manually ordered within the text/voice split.
|
||||
if (typeof channel.position === "number") position += channel.position
|
||||
|
||||
// Threads appear below their channel.
|
||||
if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
|
||||
position += 0.5
|
||||
let parent = channels.get(channel.parent_id)
|
||||
if (parent && parent["position"]) position += parent["position"]
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {Ty.R.Hierarchy[]} rooms
|
||||
* @param {string[]} roles
|
||||
*/
|
||||
function getChannelRoomsLinks(guild, rooms, roles) {
|
||||
function getPosition(channel) {
|
||||
let position = 0
|
||||
let looking = channel
|
||||
while (looking.parent_id) {
|
||||
looking = discord.channels.get(looking.parent_id)
|
||||
position = looking.position * 1000
|
||||
}
|
||||
if (channel.position) position += channel.position
|
||||
return position
|
||||
}
|
||||
|
||||
let channelIDs = discord.guildChannelMap.get(guild.id)
|
||||
assert(channelIDs)
|
||||
|
||||
@@ -97,7 +121,7 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||
let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c}))
|
||||
let removedUncachedChannels = filterTo(linkedChannelsWithDetails, c => c.channel)
|
||||
let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id)
|
||||
linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel) - getPosition(b.channel))
|
||||
linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel, discord.channels) - getPosition(b.channel, discord.channels))
|
||||
|
||||
let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c))
|
||||
/** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore
|
||||
@@ -107,7 +131,7 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||
const permissions = dUtils.getPermissions(roles, guild.roles, botID, c["permission_overwrites"])
|
||||
return dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
||||
})
|
||||
unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b))
|
||||
unlinkedChannels.sort((a, b) => getPosition(a, discord.channels) - getPosition(b, discord.channels))
|
||||
|
||||
let linkedRoomIDs = linkedChannels.map(c => c.room_id)
|
||||
let unlinkedRooms = [...rooms]
|
||||
@@ -239,3 +263,6 @@ as.router.post("/api/invite", defineEventHandler(async event => {
|
||||
return sendRedirect(event, "/ok?msg=User has been invited.", 302)
|
||||
}
|
||||
}))
|
||||
|
||||
module.exports._getPosition = getPosition
|
||||
module.exports._filterTo = filterTo
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const {_getPosition, _filterTo} = require("./guild")
|
||||
|
||||
let nonce
|
||||
|
||||
@@ -101,12 +103,6 @@ test("web guild: can view bridged guild when logged in with discord", async t =>
|
||||
managedGuilds: ["112760669178241024"]
|
||||
},
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {}
|
||||
},
|
||||
async getMembers(roomID, membership) {
|
||||
return {chunk: []}
|
||||
},
|
||||
async getFullHierarchy(roomID) {
|
||||
return []
|
||||
}
|
||||
@@ -121,12 +117,6 @@ test("web guild: can view bridged guild when logged in with matrix", async t =>
|
||||
mxid: "@cadence:cadence.moe"
|
||||
},
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {}
|
||||
},
|
||||
async getMembers(roomID, membership) {
|
||||
return {chunk: []}
|
||||
},
|
||||
async getFullHierarchy(roomID) {
|
||||
return []
|
||||
}
|
||||
@@ -191,12 +181,12 @@ test("api invite: can invite with valid nonce", async t => {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
if (type === "m.room.member" && key === "@cadence:cadence.moe") {
|
||||
return {membership: "leave"}
|
||||
throw new Error("event not found")
|
||||
} else if (type === "m.room.power_levels" && key === "") {
|
||||
return {}
|
||||
} else {
|
||||
t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`)
|
||||
}
|
||||
/* c8 ignore next */
|
||||
t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`)
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
@@ -284,9 +274,9 @@ test("api invite: can invite to a moderated guild", async t => {
|
||||
return {membership: "leave"}
|
||||
} else if (type === "m.room.power_levels" && key === "") {
|
||||
return {}
|
||||
} else {
|
||||
t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`)
|
||||
}
|
||||
/* c8 ignore next */
|
||||
t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`)
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
@@ -362,3 +352,52 @@ test("api invite: does not reinvite joined users", async t => {
|
||||
t.notOk(error)
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
|
||||
test("position sorting: sorts like discord does", t => {
|
||||
const channelsList = [{
|
||||
type: DiscordTypes.ChannelType.GuildText,
|
||||
id: "first",
|
||||
position: 0
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.PublicThread,
|
||||
id: "thread",
|
||||
parent_id: "first",
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.GuildText,
|
||||
id: "second",
|
||||
position: 1
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.GuildVoice,
|
||||
id: "voice",
|
||||
position: 0
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.GuildCategory,
|
||||
id: "category",
|
||||
position: 0
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.GuildText,
|
||||
id: "category-first",
|
||||
parent_id: "category",
|
||||
position: 0
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.GuildText,
|
||||
id: "category-second",
|
||||
parent_id: "category",
|
||||
position: 1
|
||||
}, {
|
||||
type: DiscordTypes.ChannelType.PublicThread,
|
||||
id: "category-second-thread",
|
||||
parent_id: "category-second",
|
||||
}].reverse()
|
||||
const channels = new Map(channelsList.map(c => [c.id, c]))
|
||||
const sortedChannelIDs = [...channels.values()].sort((a, b) => _getPosition(a, channels) - _getPosition(b, channels)).map(c => c.id)
|
||||
t.deepEqual(sortedChannelIDs, ["first", "thread", "second", "voice", "category", "category-first", "category-second", "category-second-thread"])
|
||||
})
|
||||
|
||||
test("filterTo: works", t => {
|
||||
const fruit = ["apple", "banana", "apricot"]
|
||||
const rest = _filterTo(fruit, f => f[0] === "b")
|
||||
t.deepEqual(fruit, ["banana"])
|
||||
t.deepEqual(rest, ["apple", "apricot"])
|
||||
})
|
||||
|
||||
@@ -70,16 +70,14 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||
// Check space ID
|
||||
if (!session.data.mxid) throw createError({status: 403, message: "Forbidden", data: "Can't link with your Matrix space if you aren't logged in to Matrix"})
|
||||
const spaceID = parsedBody.space_id
|
||||
const inviteType = select("invite", "type", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
|
||||
if (inviteType !== "m.space") throw createError({status: 403, message: "Forbidden", data: "You personally must invite OOYE to that space on Matrix"})
|
||||
const inviteRow = select("invite", ["mxid", "type"], {mxid: session.data.mxid, room_id: spaceID}).get()
|
||||
if (!inviteRow || inviteRow.type !== "m.space") throw createError({status: 403, message: "Forbidden", data: "You personally must invite OOYE to that space on Matrix"})
|
||||
|
||||
// Check they are not already bridged
|
||||
const existing = select("guild_space", "guild_id", {}, "WHERE guild_id = ? OR space_id = ?").get(guildID, spaceID)
|
||||
if (existing) throw createError({status: 400, message: "Bad Request", data: `Guild ID ${guildID} or space ID ${spaceID} are already bridged and cannot be reused`})
|
||||
|
||||
const inviteSender = select("invite", "mxid", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
|
||||
const inviteSenderServer = inviteSender?.match(/:(.*)/)?.[1]
|
||||
const via = [inviteSenderServer || ""]
|
||||
const via = [inviteRow.mxid.match(/:(.*)/)[1]]
|
||||
|
||||
// Check space exists and bridge is joined
|
||||
try {
|
||||
|
||||
@@ -39,7 +39,8 @@ test("log in with matrix: sends message to log in", async t => {
|
||||
let called = 0
|
||||
await router.test("post", "/api/log-in-with-matrix", {
|
||||
body: {
|
||||
mxid: "@cadence:cadence.moe"
|
||||
mxid: "@cadence:cadence.moe",
|
||||
next: "https://bridge.cadence.moe/guild?guild_id=123"
|
||||
},
|
||||
api: {
|
||||
async usePrivateChat(mxid) {
|
||||
@@ -51,7 +52,7 @@ test("log in with matrix: sends message to log in", async t => {
|
||||
called++
|
||||
t.equal(roomID, "!created:cadence.moe")
|
||||
t.equal(type, "m.room.message")
|
||||
token = content.body.match(/log-in-with-matrix\?token=([a-f0-9-]+)/)[1]
|
||||
token = content.body.match(/log-in-with-matrix\?token=([a-f0-9-]+)&next=/)[1]
|
||||
t.ok(token, "log in token not issued")
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
const {z} = require("zod")
|
||||
const {randomUUID} = require("crypto")
|
||||
const {defineEventHandler, getValidatedQuery, sendRedirect, createError} = require("h3")
|
||||
const {defineEventHandler, getValidatedQuery, sendRedirect, createError, H3Event} = require("h3")
|
||||
const {SnowTransfer, tokenless} = require("snowtransfer")
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const getRelativePath = require("get-relative-path")
|
||||
|
||||
const {discord, as, db, sync} = require("../../passthrough")
|
||||
const {as, db, sync} = require("../../passthrough")
|
||||
const {id, permissions} = require("../../../addbot")
|
||||
/** @type {import("../auth")} */
|
||||
const auth = sync.require("../auth")
|
||||
@@ -33,6 +33,24 @@ const schema = {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {H3Event} event
|
||||
* @returns {(string) => {user: {getGuilds: () => Promise<DiscordTypes.RESTGetAPICurrentUserGuildsResult>}}}
|
||||
*/
|
||||
function getClient(event) {
|
||||
/* c8 ignore next */
|
||||
return event.context.getClient || (accessToken => new SnowTransfer(`Bearer ${accessToken}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {H3Event} event
|
||||
* @returns {typeof tokenless.getOauth2Token}
|
||||
*/
|
||||
function getOauth2Token(event) {
|
||||
/* c8 ignore next */
|
||||
return event.context.getOauth2Token || tokenless.getOauth2Token
|
||||
}
|
||||
|
||||
as.router.get("/oauth", defineEventHandler(async event => {
|
||||
const session = await auth.useSession(event)
|
||||
let scope = "guilds"
|
||||
@@ -61,21 +79,15 @@ as.router.get("/oauth", defineEventHandler(async event => {
|
||||
if (!savedState) throw createError({status: 400, message: "Missing state", data: "Missing saved state parameter. Please try again, and make sure you have cookies enabled."})
|
||||
if (savedState != parsedQuery.data.state) return tryAgain()
|
||||
|
||||
const oauthResult = await tokenless.getOauth2Token(id, redirect_uri, reg.ooye.discord_client_secret, parsedQuery.data.code)
|
||||
const parsedToken = schema.token.safeParse(oauthResult)
|
||||
if (!parsedToken.success) {
|
||||
throw createError({status: 502, message: "Invalid token response", data: `Discord completed OAuth, but returned this instead of an OAuth access token: ${JSON.stringify(oauthResult)}`})
|
||||
}
|
||||
const oauthResult = await getOauth2Token(event)(id, redirect_uri, reg.ooye.discord_client_secret, parsedQuery.data.code)
|
||||
const parsedToken = schema.token.parse(oauthResult)
|
||||
|
||||
const userID = Buffer.from(parsedToken.data.access_token.split(".")[0], "base64").toString()
|
||||
const client = new SnowTransfer(`Bearer ${parsedToken.data.access_token}`)
|
||||
try {
|
||||
const guilds = await client.user.getGuilds()
|
||||
var managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id)
|
||||
await session.update({managedGuilds, userID, state: undefined})
|
||||
} catch (e) {
|
||||
throw createError({status: 502, message: "API call failed", data: e.message})
|
||||
}
|
||||
const userID = Buffer.from(parsedToken.access_token.split(".")[0], "base64").toString()
|
||||
const client = getClient(event)(parsedToken.access_token)
|
||||
|
||||
const guilds = await client.user.getGuilds()
|
||||
var managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id)
|
||||
await session.update({managedGuilds, userID, state: undefined})
|
||||
|
||||
// Set auto-create for the guild
|
||||
// @ts-ignore
|
||||
|
||||
121
src/web/routes/oauth.test.js
Normal file
121
src/web/routes/oauth.test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const assert = require("assert/strict")
|
||||
const {router, test} = require("../../../test/web")
|
||||
|
||||
test("web oauth: redirects to Discord on first visit (add easy)", async t => {
|
||||
let event = {}
|
||||
await router.test("get", "/oauth?action=add", {
|
||||
event,
|
||||
sessionData: {
|
||||
password: "password123"
|
||||
}
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 302)
|
||||
t.match(event.node.res.getHeader("location"), /^https:\/\/discord.com\/oauth2\/authorize\?client_id=684280192553844747&scope=bot\+guilds&permissions=2251801424568320&response_type=code&redirect_uri=https:\/\/bridge\.example\.org\/oauth&state=/)
|
||||
})
|
||||
|
||||
test("web oauth: redirects to Discord on first visit (add self service)", async t => {
|
||||
let event = {}
|
||||
await router.test("get", "/oauth?action=add-self-service", {
|
||||
event,
|
||||
sessionData: {
|
||||
password: "password123"
|
||||
}
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 302)
|
||||
t.match(event.node.res.getHeader("location"), /^https:\/\/discord.com\/oauth2\/authorize\?client_id=684280192553844747&scope=bot\+guilds&permissions=2251801424568320&response_type=code&redirect_uri=https:\/\/bridge\.example\.org\/oauth&state=/)
|
||||
})
|
||||
|
||||
test("web oauth: advises user about cookies if state is missing", async t => {
|
||||
let event = {}
|
||||
const [e] = await tryToCatch(() => router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE&guild_id=9", {
|
||||
event
|
||||
}))
|
||||
t.equal(e.message, "Missing state")
|
||||
})
|
||||
|
||||
test("web oauth: redirects to Discord again if state doesn't match", async t => {
|
||||
let event = {}
|
||||
await router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE", {
|
||||
event,
|
||||
sessionData: {
|
||||
state: "438aa253-1311-4483-9aa2-c251e29e72c9",
|
||||
password: "password123"
|
||||
}
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 302)
|
||||
t.match(event.node.res.getHeader("location"), /^https:\/\/discord\.com\/oauth2\/authorize/)
|
||||
})
|
||||
|
||||
test("web oauth: uses returned state, logs in", async t => {
|
||||
let event = {}
|
||||
await router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE", {
|
||||
event,
|
||||
sessionData: {
|
||||
state: "693551d5-47c5-49e2-a433-3600abe3c15c",
|
||||
selfService: false,
|
||||
password: "password123"
|
||||
},
|
||||
getOauth2Token() {
|
||||
return {
|
||||
token_type: "Bearer",
|
||||
access_token: "6qrZcUqja7812RVdnEKjpzOL4CvHBFG",
|
||||
expires_in: 604800,
|
||||
refresh_token: "D43f5y0ahjqew82jZ4NViEr2YafMKhue",
|
||||
scope: "bot+guilds"
|
||||
}
|
||||
},
|
||||
getClient(accessToken) {
|
||||
return {
|
||||
user: {
|
||||
async getGuilds() {
|
||||
return [{
|
||||
id: "9",
|
||||
permissions: DiscordTypes.PermissionFlagsBits.ManageGuild
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 302)
|
||||
t.equal(event.node.res.getHeader("location"), "./")
|
||||
})
|
||||
|
||||
test("web oauth: uses returned state, adds managed guild", async t => {
|
||||
let event = {}
|
||||
await router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE&guild_id=9", {
|
||||
event,
|
||||
sessionData: {
|
||||
state: "693551d5-47c5-49e2-a433-3600abe3c15c",
|
||||
selfService: false,
|
||||
password: "password123"
|
||||
},
|
||||
getOauth2Token() {
|
||||
return {
|
||||
token_type: "Bearer",
|
||||
access_token: "6qrZcUqja7812RVdnEKjpzOL4CvHBFG",
|
||||
expires_in: 604800,
|
||||
refresh_token: "D43f5y0ahjqew82jZ4NViEr2YafMKhue",
|
||||
scope: "bot+guilds"
|
||||
}
|
||||
},
|
||||
getClient(accessToken) {
|
||||
return {
|
||||
user: {
|
||||
async getGuilds() {
|
||||
return [{
|
||||
id: "9",
|
||||
permissions: DiscordTypes.PermissionFlagsBits.ManageGuild
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 302)
|
||||
t.equal(event.node.res.getHeader("location"), "guild?guild_id=9")
|
||||
})
|
||||
77
test/data.js
77
test/data.js
@@ -180,6 +180,39 @@ module.exports = {
|
||||
afk_timeout: 300,
|
||||
id: "112760669178241024",
|
||||
icon: "a_f83622e09ead74f0c5c527fe241f8f8c",
|
||||
/** @type {DiscordTypes.APIGuildMember[]} */ // @ts-ignore
|
||||
members: [{
|
||||
user: {
|
||||
username: 'Matrix Bridge',
|
||||
public_flags: 0,
|
||||
primary_guild: null,
|
||||
id: '684280192553844747',
|
||||
global_name: null,
|
||||
display_name_styles: null,
|
||||
display_name: null,
|
||||
discriminator: '5728',
|
||||
collectibles: null,
|
||||
bot: true,
|
||||
avatar_decoration_data: null,
|
||||
avatar: '48ae3c24f2a6ec5c60c41bdabd904018'
|
||||
},
|
||||
roles: [
|
||||
'703457691342995528',
|
||||
'289671295359254529',
|
||||
'1040735082610167858',
|
||||
'114526764860047367'
|
||||
],
|
||||
premium_since: null,
|
||||
pending: false,
|
||||
nick: 'Mother',
|
||||
mute: false,
|
||||
joined_at: '2020-04-25T04:09:43.253000+00:00',
|
||||
flags: 0,
|
||||
deaf: false,
|
||||
communication_disabled_until: null,
|
||||
banner: null,
|
||||
avatar: null
|
||||
}],
|
||||
emojis: [
|
||||
{
|
||||
roles: [],
|
||||
@@ -5433,6 +5466,50 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
special_message: {
|
||||
emoji_added: {
|
||||
type: 63,
|
||||
content: '<:cx_marvelous:1437322787994992650>',
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: '2025-11-10T06:07:36.930000+00:00',
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: '1437322788439457794',
|
||||
channel_id: '1100319550446252084',
|
||||
author: {
|
||||
id: '772659086046658620',
|
||||
username: 'cadence.worm',
|
||||
avatar: '466df0c98b1af1e1388f595b4c1ad1b9',
|
||||
discriminator: '0',
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: 'cadence',
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: {
|
||||
identity_guild_id: '532245108070809601',
|
||||
identity_enabled: true,
|
||||
tag: 'doll',
|
||||
badge: 'dba08126b4e810a0e096cc7cd5bc37f0'
|
||||
},
|
||||
primary_guild: {
|
||||
identity_guild_id: '532245108070809601',
|
||||
identity_enabled: true,
|
||||
tag: 'doll',
|
||||
badge: 'dba08126b4e810a0e096cc7cd5bc37f0'
|
||||
}
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false
|
||||
},
|
||||
thread_name_change: {
|
||||
id: "1142391602799710298",
|
||||
type: 4,
|
||||
|
||||
@@ -3,10 +3,12 @@ BEGIN TRANSACTION;
|
||||
INSERT INTO guild_active (guild_id, autocreate) VALUES
|
||||
('112760669178241024', 1),
|
||||
('66192955777486848', 1),
|
||||
('665289423482519565', 0);
|
||||
('665289423482519565', 0),
|
||||
('1345641201902288987', 1);
|
||||
|
||||
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES
|
||||
('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0);
|
||||
('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0),
|
||||
('1345641201902288987', '!CvQMeeqXIkgedUpkzv:cadence.moe', 0);
|
||||
|
||||
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, guild_id) VALUES
|
||||
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL, '112760669178241024'),
|
||||
@@ -21,7 +23,8 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom
|
||||
('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '665289423482519565'),
|
||||
('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL, '665289423482519565');
|
||||
('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL, '665289423482519565'),
|
||||
('598707048112193536', '!JBxeGYnzQwLnaooOLD:cadence.moe', 'winners', NULL, NULL, NULL, '1345641201902288987');
|
||||
|
||||
INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) SELECT channel_id, room_id, 0 FROM channel_room;
|
||||
|
||||
@@ -78,7 +81,8 @@ WITH a (message_id, channel_id) AS (VALUES
|
||||
('1339000288144658482', '176333891320283136'),
|
||||
('1381212840957972480', '112760669178241024'),
|
||||
('1401760355339862066', '112760669178241024'),
|
||||
('1439351590262800565', '1438284564815548418'))
|
||||
('1439351590262800565', '1438284564815548418'),
|
||||
('1404133238414376971', '112760669178241024'))
|
||||
SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id;
|
||||
|
||||
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
||||
@@ -124,7 +128,9 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
|
||||
('$AfrB8hzXkDMvuoWjSZkDdFYomjInWH7jMBPkwQMN8AI', 'm.room.message', 'm.text', '1381212840957972480', 0, 1, 1),
|
||||
('$43baKEhJfD-RlsFQi0LB16Zxd8yMqp0HSVL00TDQOqM', 'm.room.message', 'm.image', '1381212840957972480', 1, 0, 1),
|
||||
('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
|
||||
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0);
|
||||
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
|
||||
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
|
||||
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
|
||||
|
||||
INSERT INTO file (discord_url, mxc_url) VALUES
|
||||
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
||||
@@ -155,7 +161,8 @@ INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES
|
||||
('551636841284108289', 'ae_botrac4r', 0, 'mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs'),
|
||||
('975572106295259148', 'brillillillilliant_move', 0, 'mxc://cadence.moe/scfRIDOGKWFDEBjVXocWYQHik'),
|
||||
('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'),
|
||||
('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO');
|
||||
('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'),
|
||||
('1437322787994992650', 'cx_marvelous', 0, 'mxc://cadence.moe/TPZdosVUjTIopsLijkygIbti');
|
||||
|
||||
INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES
|
||||
('!jjmvBegULiLucuWEHU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 50),
|
||||
@@ -200,4 +207,10 @@ INSERT INTO direct (mxid, room_id) VALUES
|
||||
('@user1:example.org', '!existing:cadence.moe'),
|
||||
('@user2:example.org', '!existing:cadence.moe');
|
||||
|
||||
-- for cross-room reply test, in 'updates' room
|
||||
UPDATE historical_channel_room SET room_id = '!mHmhQQPwXNananaOLD:cadence.moe' WHERE room_id = '!mHmhQQPwXNananMUqq:cadence.moe';
|
||||
INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) VALUES ('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 1767922455991);
|
||||
INSERT INTO message_room (message_id, historical_room_index) SELECT '1458091145136443547', historical_room_index FROM historical_channel_room WHERE room_id = '!mHmhQQPwXNananaOLD:cadence.moe';
|
||||
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY', 'm.room.message', 'm.image', '1458091145136443547', 0, 0, 0);
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -29,6 +29,7 @@ reg.namespaces = {
|
||||
reg.ooye.bridge_origin = "https://bridge.example.org"
|
||||
reg.ooye.time_zone = "Pacific/Auckland"
|
||||
reg.ooye.max_file_size = 5000000
|
||||
reg.ooye.web_password = "password123"
|
||||
|
||||
const sync = new HeatSync({watchFS: false})
|
||||
|
||||
@@ -140,6 +141,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||
require("../src/web/routes/info.test")
|
||||
require("../src/web/routes/link.test")
|
||||
require("../src/web/routes/log-in-with-matrix.test")
|
||||
require("../src/web/routes/oauth.test")
|
||||
require("../src/web/routes/password.test")
|
||||
require("../src/discord/utils.test")
|
||||
require("../src/matrix/kstate.test")
|
||||
@@ -147,6 +149,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||
require("../src/matrix/file.test")
|
||||
require("../src/matrix/mreq.test")
|
||||
require("../src/matrix/read-registration.test")
|
||||
require("../src/matrix/room-upgrade.test")
|
||||
require("../src/matrix/txnid.test")
|
||||
require("../src/matrix/utils.test")
|
||||
require("../src/d2m/actions/create-room.test")
|
||||
|
||||
@@ -51,7 +51,7 @@ class Router {
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {string} inputUrl
|
||||
* @param {{event?: any, params?: any, body?: any, sessionData?: any, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, createRoom?: Partial<import("../src/d2m/actions/create-room")>, createSpace?: Partial<import("../src/d2m/actions/create-space")>, headers?: any}} [options]
|
||||
* @param {{event?: any, params?: any, body?: any, sessionData?: any, getOauth2Token?: any, getClient?: (string) => {user: {getGuilds: () => Promise<DiscordTypes.RESTGetAPICurrentUserGuildsResult>}}, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, createRoom?: Partial<import("../src/d2m/actions/create-room")>, createSpace?: Partial<import("../src/d2m/actions/create-space")>, headers?: any}} [options]
|
||||
*/
|
||||
async test(method, inputUrl, options = {}) {
|
||||
const url = new URL(inputUrl, "http://a")
|
||||
@@ -87,6 +87,8 @@ class Router {
|
||||
snow: options.snow,
|
||||
createRoom: options.createRoom,
|
||||
createSpace: options.createSpace,
|
||||
getOauth2Token: options.getOauth2Token,
|
||||
getClient: options.getClient,
|
||||
sessions: {
|
||||
h3: {
|
||||
id: "h3",
|
||||
|
||||
Reference in New Issue
Block a user