[WIP] threads. #74
14 changed files with 160 additions and 76 deletions
Fix conflicts; rename a var to something more sensible; less string concat because Cadence said she doesn't like it.
commit
ea43d3b285
|
|
@ -14,8 +14,6 @@ const channelWebhook = sync.require("./channel-webhook")
|
||||||
const eventToMessage = sync.require("../converters/event-to-message")
|
const eventToMessage = sync.require("../converters/event-to-message")
|
||||||
/** @type {import("../../matrix/api")}) */
|
/** @type {import("../../matrix/api")}) */
|
||||||
const api = sync.require("../../matrix/api")
|
const api = sync.require("../../matrix/api")
|
||||||
/** @type {import("../../matrix/utils")}) */
|
|
||||||
const utils = sync.require("../../matrix/utils")
|
|
||||||
/** @type {import("../../d2m/actions/register-user")} */
|
/** @type {import("../../d2m/actions/register-user")} */
|
||||||
const registerUser = sync.require("../../d2m/actions/register-user")
|
const registerUser = sync.require("../../d2m/actions/register-user")
|
||||||
/** @type {import("../../d2m/actions/edit-message")} */
|
/** @type {import("../../d2m/actions/edit-message")} */
|
||||||
|
|
@ -23,6 +21,12 @@ const editMessage = sync.require("../../d2m/actions/edit-message")
|
||||||
/** @type {import("../actions/emoji-sheet")} */
|
/** @type {import("../actions/emoji-sheet")} */
|
||||||
const emojiSheet = sync.require("../actions/emoji-sheet")
|
const emojiSheet = sync.require("../actions/emoji-sheet")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Map<string, string>} room id -> mxid
|
||||||
|
* Keep track of the sender of the latest event per room, this is needed to trim display name runoff for subsequent messages
|
||||||
|
*/
|
||||||
|
const latestSenderPerRoom = sync.remember(() => new Map())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | stream.Readable})[]}} message
|
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | stream.Readable})[]}} message
|
||||||
* @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]}>}
|
* @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]}>}
|
||||||
|
|
@ -87,7 +91,12 @@ async function sendEvent(event) {
|
||||||
|
|
||||||
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
|
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
|
||||||
|
|
||||||
const di = {api, snow: discord.snow, mxcDownloader: emojiSheet.getAndConvertEmoji}
|
const di = {
|
||||||
|
api,
|
||||||
|
snow: discord.snow,
|
||||||
|
mxcDownloader: emojiSheet.getAndConvertEmoji,
|
||||||
|
noRunoff: latestSenderPerRoom.get(event.room_id) === event.sender
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "org.matrix.msc3381.poll.end") {
|
if (event.type === "org.matrix.msc3381.poll.end") {
|
||||||
// Validity already checked by dispatcher. Poll is definitely closed. Update it and DI necessary data.
|
// Validity already checked by dispatcher. Poll is definitely closed. Update it and DI necessary data.
|
||||||
|
|
@ -193,3 +202,4 @@ async function sendEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.sendEvent = sendEvent
|
module.exports.sendEvent = sendEvent
|
||||||
|
module.exports.latestSenderPerRoom = latestSenderPerRoom
|
||||||
|
|
|
||||||
|
|
@ -538,7 +538,7 @@ async function getL1L2ReplyLine(called = false) {
|
||||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_End} event
|
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_End} event
|
||||||
* @param {DiscordTypes.APIGuild} guild
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
* @param {DiscordTypes.APIGuildTextChannel} channel
|
* @param {DiscordTypes.APIGuildTextChannel} channel
|
||||||
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, pollEnd?: {messageID: string}}} di simple-as-nails dependency injection for the matrix API
|
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, pollEnd?: {messageID: string}, noRunoff: boolean}} di simple-as-nails dependency injection for the matrix API
|
||||||
*/
|
*/
|
||||||
async function eventToMessage(event, guild, channel, di) {
|
async function eventToMessage(event, guild, channel, di) {
|
||||||
let displayName = event.sender
|
let displayName = event.sender
|
||||||
|
|
@ -561,7 +561,16 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
|
|
||||||
// MSC4144: Override display name and avatar from per-message profile if present
|
// MSC4144: Override display name and avatar from per-message profile if present
|
||||||
const perMessageProfile = event.content["com.beeper.per_message_profile"]
|
const perMessageProfile = event.content["com.beeper.per_message_profile"]
|
||||||
if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname
|
if (perMessageProfile?.displayname) {
|
||||||
|
// TODO: Ideally this should filter on the "language" key, rather than just picking the first, once we're aware of the guild's preferred locale.
|
||||||
|
// Regardless, these are intended to be in preference order, and the PMP user could always choose to reorder them if the wrong choice is made here.
|
||||||
|
const pronouns = perMessageProfile["io.fsky.nyx.pronouns"]?.[0]?.summary
|
||||||
|
let pronounsSuffix = ""
|
||||||
|
if (pronouns) {
|
||||||
|
pronounsSuffix = ` (${pronouns})`
|
||||||
|
}
|
||||||
|
displayName = `${perMessageProfile.displayname}${pronounsSuffix} | ${displayName}`
|
||||||
|
}
|
||||||
if (perMessageProfile && "avatar_url" in perMessageProfile) {
|
if (perMessageProfile && "avatar_url" in perMessageProfile) {
|
||||||
if (perMessageProfile.avatar_url) {
|
if (perMessageProfile.avatar_url) {
|
||||||
// use provided avatar_url
|
// use provided avatar_url
|
||||||
|
|
@ -577,7 +586,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
// put the excess characters into displayNameRunoff, later to be put at the top of the message
|
// put the excess characters into displayNameRunoff, later to be put at the top of the message
|
||||||
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
|
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
|
||||||
// If the message type is m.emote, the full name is already included at the start of the message, so remove any runoff
|
// If the message type is m.emote, the full name is already included at the start of the message, so remove any runoff
|
||||||
if (event.type === "m.room.message" && event.content.msgtype === "m.emote") {
|
if (di?.noRunoff || (event.type === "m.room.message" && event.content.msgtype === "m.emote")) {
|
||||||
displayNameRunoff = ""
|
displayNameRunoff = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5985,7 +5985,7 @@ test("event2message: com.beeper.per_message_profile overrides displayname and av
|
||||||
messagesToDelete: [],
|
messagesToDelete: [],
|
||||||
messagesToEdit: [],
|
messagesToEdit: [],
|
||||||
messagesToSend: [{
|
messagesToSend: [{
|
||||||
username: "Unstable Name",
|
username: "Unstable Name | cadence [they]",
|
||||||
content: "hello from unstable profile",
|
content: "hello from unstable profile",
|
||||||
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo?preset=avatar",
|
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo?preset=avatar",
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
|
|
@ -6018,7 +6018,7 @@ test("event2message: com.beeper.per_message_profile empty avatar_url clears avat
|
||||||
messagesToDelete: [],
|
messagesToDelete: [],
|
||||||
messagesToEdit: [],
|
messagesToEdit: [],
|
||||||
messagesToSend: [{
|
messagesToSend: [{
|
||||||
username: "No Avatar User",
|
username: "No Avatar User | cadence [they]",
|
||||||
content: "hello with cleared avatar",
|
content: "hello with cleared avatar",
|
||||||
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=N&hue=90",
|
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=N&hue=90",
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
|
|
@ -6054,7 +6054,7 @@ test("event2message: data-mx-profile-fallback element is stripped from formatted
|
||||||
messagesToDelete: [],
|
messagesToDelete: [],
|
||||||
messagesToEdit: [],
|
messagesToEdit: [],
|
||||||
messagesToSend: [{
|
messagesToSend: [{
|
||||||
username: "Tidus Herboren",
|
username: "Tidus Herboren | cadence [they]",
|
||||||
content: "one more test",
|
content: "one more test",
|
||||||
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo?preset=avatar",
|
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo?preset=avatar",
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
|
|
@ -6087,7 +6087,7 @@ test("event2message: displayname prefix is stripped from plain body when per-mes
|
||||||
messagesToDelete: [],
|
messagesToDelete: [],
|
||||||
messagesToEdit: [],
|
messagesToEdit: [],
|
||||||
messagesToSend: [{
|
messagesToSend: [{
|
||||||
username: "Tidus Herboren",
|
username: "Tidus Herboren | cadence [they]",
|
||||||
content: "one more test",
|
content: "one more test",
|
||||||
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=T&hue=90",
|
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=T&hue=90",
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
|
|
@ -6098,6 +6098,57 @@ test("event2message: displayname prefix is stripped from plain body when per-mes
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("event2message: pronouns can be displayed with per-message profile", async t => {
|
||||||
|
t.deepEqual(
|
||||||
|
await eventToMessage({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@exa:sleeping.town",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Well, I can't really do it now...",
|
||||||
|
"com.beeper.per_message_profile": {
|
||||||
|
id: "Kryn",
|
||||||
|
displayname: "Kryn",
|
||||||
|
"eu.she-a.color": {
|
||||||
|
on_dark: "#FFCC00",
|
||||||
|
on_light: "#976800"
|
||||||
|
},
|
||||||
|
"io.fsky.nyx.pronouns": [{
|
||||||
|
language: "en",
|
||||||
|
summary: "fae/she"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||||
|
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||||
|
}, {}, {}, {
|
||||||
|
api: {
|
||||||
|
async getStateEvent(roomID, type, key) {
|
||||||
|
t.equal(type, "m.room.member")
|
||||||
|
t.equal(key, "@exa:sleeping.town")
|
||||||
|
return {
|
||||||
|
displayname: "Exa",
|
||||||
|
membership: "join"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
ensureJoined: [],
|
||||||
|
messagesToDelete: [],
|
||||||
|
messagesToEdit: [],
|
||||||
|
messagesToSend: [{
|
||||||
|
username: "Kryn (fae/she) | Exa",
|
||||||
|
content: "Well, I can't really do it now...",
|
||||||
|
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=K&hue=150",
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["users", "roles"]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
|
test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
await eventToMessage({
|
await eventToMessage({
|
||||||
|
|
|
||||||
|
|
@ -192,37 +192,39 @@ sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
|
||||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for
|
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for
|
||||||
*/
|
*/
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (!utils.eventSenderIsFromDiscord(event.sender)){
|
||||||
if (await handleForums(event)) return
|
if (await handleForums(event)) return
|
||||||
|
|
||||||
let processCommands = true
|
let processCommands = true
|
||||||
if (event.content["m.relates_to"]?.rel_type === "m.thread") {
|
if (event.content["m.relates_to"]?.rel_type === "m.thread") {
|
||||||
/**@type {string|null} */
|
/**@type {string|null} */
|
||||||
let toRedact = event.room_id
|
let redactAndMoveShouldOccurIn = event.room_id
|
||||||
const bridgedTo = utils.getThreadRoomFromThreadEvent(event.content["m.relates_to"].event_id)
|
const bridgedTo = utils.getThreadRoomFromThreadEvent(event.content["m.relates_to"].event_id)
|
||||||
processCommands = false
|
processCommands = false
|
||||||
|
|
||||||
if (bridgedTo) event.room_id = bridgedTo;
|
if (bridgedTo) event.room_id = bridgedTo;
|
||||||
else if (!await bridgeThread(event)) toRedact = null; //Don't remove anything, if there is nowhere to relocate it to.
|
else if (!await bridgeThread(event)) redactAndMoveShouldOccurIn = null; //Don't remove anything, if there is nowhere to relocate it to.
|
||||||
|
|
||||||
if (toRedact) {
|
if (redactAndMoveShouldOccurIn) {
|
||||||
api.redactEvent(toRedact, event.event_id)
|
api.redactEvent(redactAndMoveShouldOccurIn, event.event_id)
|
||||||
event.content["m.relates_to"] = undefined
|
event.content["m.relates_to"] = undefined
|
||||||
api.sendEvent(event.room_id, event.type, {...event.content, body: event.content.body+"\n ~ "+event.sender, formatted_body: event.content.formatted_body ? event.content.formatted_body+"<br> ~ "+event.sender :undefined })
|
api.sendEvent(event.room_id, event.type, {...event.content, body: `${event.content.body}\n ~ ${event.sender}`, formatted_body: event.content.formatted_body ? `${event.content.formatted_body}<br> ~ ${event.sender}` : undefined })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageResponses = await sendEvent.sendEvent(event)
|
||||||
|
if (!messageResponses.length) return
|
||||||
|
|
||||||
|
if (event.type === "m.room.message" && event.content.msgtype === "m.text" && processCommands) {
|
||||||
|
await matrixCommandHandler.parseAndExecute(
|
||||||
|
// @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" check ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here)
|
||||||
|
event
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!event.content["m.relates_to"]) { // Ignore edits since they don't update the timeline on Discord
|
||||||
const messageResponses = await sendEvent.sendEvent(event)
|
sendEvent.latestSenderPerRoom.set(event.room_id, event.sender)
|
||||||
if (!messageResponses.length) return
|
|
||||||
|
|
||||||
if (event.type === "m.room.message" && event.content.msgtype === "m.text" && processCommands) {
|
|
||||||
await matrixCommandHandler.parseAndExecute(
|
|
||||||
// @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" check ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here)
|
|
||||||
event
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
retrigger.finishedBridging(event.event_id)
|
|
||||||
await api.ackEvent(event)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ const walkAST = require("pug-walk")
|
||||||
const pretty = process.argv.join(" ").includes("test")
|
const pretty = process.argv.join(" ").includes("test")
|
||||||
|
|
||||||
const {sync} = require("../passthrough")
|
const {sync} = require("../passthrough")
|
||||||
/** @type {import("./auth")} */
|
/** @type {import("./web-utils")} */
|
||||||
const auth = sync.require("./auth")
|
const webUtils = sync.require("./web-utils")
|
||||||
/** @type {import("../js/errors")} */
|
/** @type {import("../js/errors")} */
|
||||||
const errors = sync.require("../js/errors")
|
const errors = sync.require("../js/errors")
|
||||||
|
|
||||||
|
|
@ -88,8 +88,8 @@ function renderPath(event, path, fragments, locals) {
|
||||||
const requiredVars = []
|
const requiredVars = []
|
||||||
const template = compileFile(path, {pretty, plugins: fragmentSelectorPlugins(fragments, requiredVars)})
|
const template = compileFile(path, {pretty, plugins: fragmentSelectorPlugins(fragments, requiredVars)})
|
||||||
pugCache.set(cacheKey, async (event, locals) => {
|
pugCache.set(cacheKey, async (event, locals) => {
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const rel = (to, paramsObject) => {
|
const rel = (to, paramsObject) => {
|
||||||
let result = getRelativePath(event.path, to)
|
let result = getRelativePath(event.path, to)
|
||||||
if (paramsObject) {
|
if (paramsObject) {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ const {defineEventHandler, createError, readValidatedBody, getRequestHeader, set
|
||||||
|
|
||||||
const {as, db, sync, select, discord} = require("../../passthrough")
|
const {as, db, sync, select, discord} = require("../../passthrough")
|
||||||
|
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = sync.require("../auth")
|
const webUtils = sync.require("../web-utils")
|
||||||
/** @type {import("../pug-sync")} */
|
/** @type {import("../pug-sync")} */
|
||||||
const pugSync = sync.require("../pug-sync")
|
const pugSync = sync.require("../pug-sync")
|
||||||
/** @type {import("../../d2m/actions/set-presence")} */
|
/** @type {import("../../d2m/actions/set-presence")} */
|
||||||
|
|
@ -56,7 +56,7 @@ function defineToggle(key, inputOptions) {
|
||||||
})
|
})
|
||||||
/** @type {Record<T, string?> & Record<"guild_id", string> & Record<string, unknown>} */ // @ts-ignore
|
/** @type {Record<T, string?> & Record<"guild_id", string> & Record<string, unknown>} */ // @ts-ignore
|
||||||
const parsedBody = await readValidatedBody(event, bodySchema.parse)
|
const parsedBody = await readValidatedBody(event, bodySchema.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
if (!managed.has(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
if (!managed.has(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||||
|
|
||||||
const value = options.transform(parsedBody[key])
|
const value = options.transform(parsedBody[key])
|
||||||
|
|
@ -108,7 +108,7 @@ as.router.post("/api/privacy-level", defineToggle("privacy_level", {
|
||||||
as.router.post("/api/default-roles", defineEventHandler(async event => {
|
as.router.post("/api/default-roles", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse)
|
const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse)
|
||||||
|
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const guildID = parsedBody.guild_id
|
const guildID = parsedBody.guild_id
|
||||||
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||||
|
|
||||||
|
|
@ -148,7 +148,7 @@ as.router.post("/api/room-mention-role", defineEventHandler(async event => {
|
||||||
channel_id: z.string(),
|
channel_id: z.string(),
|
||||||
room_mention_role: z.string().regex(/^(?:[0-9]+)?$/)
|
room_mention_role: z.string().regex(/^(?:[0-9]+)?$/)
|
||||||
}).parse)
|
}).parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
if (!managed.has(body.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
if (!managed.has(body.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||||
const room = select("channel_room", "room_id", {channel_id: body.channel_id, guild_id: body.guild_id}).get()
|
const room = select("channel_room", "room_id", {channel_id: body.channel_id, guild_id: body.guild_id}).get()
|
||||||
if (!room) throw createError({status: 404, message: "Linked channel not found"})
|
if (!room) throw createError({status: 404, message: "Linked channel not found"})
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ const {discord, as, sync, select, from, db} = require("../../passthrough")
|
||||||
const pugSync = sync.require("../pug-sync")
|
const pugSync = sync.require("../pug-sync")
|
||||||
/** @type {import("../../d2m/actions/create-space")} */
|
/** @type {import("../../d2m/actions/create-space")} */
|
||||||
const createSpace = sync.require("../../d2m/actions/create-space")
|
const createSpace = sync.require("../../d2m/actions/create-space")
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = require("../auth")
|
const webUtils = require("../web-utils")
|
||||||
/** @type {import("../../discord/utils")} */
|
/** @type {import("../../discord/utils")} */
|
||||||
const dUtils = sync.require("../../discord/utils")
|
const dUtils = sync.require("../../discord/utils")
|
||||||
/** @type {import("../../matrix/utils")} */
|
/** @type {import("../../matrix/utils")} */
|
||||||
|
|
@ -178,8 +178,8 @@ function getInviteTargetSpaces(mxid) {
|
||||||
|
|
||||||
as.router.get("/guild", defineEventHandler(async event => {
|
as.router.get("/guild", defineEventHandler(async event => {
|
||||||
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
|
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get()
|
const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const guild = discord.guilds.get(guild_id)
|
const guild = discord.guilds.get(guild_id)
|
||||||
|
|
@ -220,7 +220,7 @@ as.router.get("/explain", defineEventHandler(async event => {
|
||||||
|
|
||||||
as.router.get("/qr", defineEventHandler(async event => {
|
as.router.get("/qr", defineEventHandler(async event => {
|
||||||
const {guild_id} = await getValidatedQuery(event, schema.qr.parse)
|
const {guild_id} = await getValidatedQuery(event, schema.qr.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get()
|
const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const guild = discord.guilds.get(guild_id)
|
const guild = discord.guilds.get(guild_id)
|
||||||
|
|
@ -253,7 +253,7 @@ as.router.get("/invite", defineEventHandler(async event => {
|
||||||
|
|
||||||
as.router.post("/api/invite", defineEventHandler(async event => {
|
as.router.post("/api/invite", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.invite.parse)
|
const parsedBody = await readValidatedBody(event, schema.invite.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
|
|
||||||
// Check guild ID or nonce
|
// Check guild ID or nonce
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ const Ty = require("../../types")
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
|
||||||
const {discord, db, as, sync, select, from} = require("../../passthrough")
|
const {discord, db, as, sync, select, from} = require("../../passthrough")
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = sync.require("../auth")
|
const webUtils = sync.require("../web-utils")
|
||||||
/** @type {import("../../matrix/utils")}*/
|
/** @type {import("../../matrix/utils")}*/
|
||||||
const utils = sync.require("../../matrix/utils")
|
const utils = sync.require("../../matrix/utils")
|
||||||
/** @type {import("./guild")}*/
|
/** @type {import("./guild")}*/
|
||||||
|
|
@ -98,8 +98,8 @@ async function validateAndUnbridgeChannel(event, channel_id, guild_id) {
|
||||||
|
|
||||||
as.router.post("/api/link-space", defineEventHandler(async event => {
|
as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
|
const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
|
|
||||||
// Check guild ID
|
// Check guild ID
|
||||||
|
|
@ -148,7 +148,7 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
|
|
||||||
as.router.post("/api/link", defineEventHandler(async event => {
|
as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.link.parse)
|
const parsedBody = await readValidatedBody(event, schema.link.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
const createRoom = getCreateRoom(event)
|
const createRoom = getCreateRoom(event)
|
||||||
const createSpace = getCreateSpace(event)
|
const createSpace = getCreateSpace(event)
|
||||||
|
|
@ -249,7 +249,7 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
|
|
||||||
as.router.post("/api/unlink", defineEventHandler(async event => {
|
as.router.post("/api/unlink", defineEventHandler(async event => {
|
||||||
const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
|
const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
|
|
||||||
// Check guild ID or nonce
|
// Check guild ID or nonce
|
||||||
if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
||||||
|
|
@ -266,7 +266,7 @@ as.router.post("/api/unlink", defineEventHandler(async event => {
|
||||||
|
|
||||||
as.router.post("/api/unlink-space", defineEventHandler(async event => {
|
as.router.post("/api/unlink-space", defineEventHandler(async event => {
|
||||||
const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse)
|
const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await webUtils.getManagedGuilds(event)
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
const snow = getSnow(event)
|
const snow = getSnow(event)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ const {sync} = require("../../passthrough")
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
/** @type {import("../pug-sync")} */
|
/** @type {import("../pug-sync")} */
|
||||||
const pugSync = sync.require("../pug-sync")
|
const pugSync = sync.require("../pug-sync")
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = sync.require("../auth")
|
const webUtils = sync.require("../web-utils")
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
form: z.object({
|
form: z.object({
|
||||||
|
|
@ -52,12 +52,11 @@ as.router.get("/log-in-with-matrix", defineEventHandler(async event => {
|
||||||
return pugSync.render(event, "log-in-with-matrix.pug", {next})
|
return pugSync.render(event, "log-in-with-matrix.pug", {next})
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAgent = getRequestHeader(event, "User-Agent")
|
webUtils.rejectUrlPreviewer(event)
|
||||||
if (userAgent?.match(/bot|matrix/)) throw createError({status: 400, data: "Sorry URL previewer, you can't have this URL."})
|
|
||||||
|
|
||||||
if (!validToken.has(token)) return sendRedirect(event, `${reg.ooye.bridge_origin}/log-in-with-matrix`, 302)
|
if (!validToken.has(token)) return sendRedirect(event, `${reg.ooye.bridge_origin}/log-in-with-matrix`, 302)
|
||||||
|
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
const mxid = validToken.get(token)
|
const mxid = validToken.get(token)
|
||||||
assert(mxid)
|
assert(mxid)
|
||||||
validToken.delete(token)
|
validToken.delete(token)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ const getRelativePath = require("get-relative-path")
|
||||||
|
|
||||||
const {as, db, sync} = require("../../passthrough")
|
const {as, db, sync} = require("../../passthrough")
|
||||||
const {id, permissions} = require("../../../addbot")
|
const {id, permissions} = require("../../../addbot")
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = sync.require("../auth")
|
const webUtils = sync.require("../web-utils")
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
const redirect_uri = `${reg.ooye.bridge_origin}/oauth`
|
const redirect_uri = `${reg.ooye.bridge_origin}/oauth`
|
||||||
|
|
@ -53,7 +53,7 @@ function getOauth2Token(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
as.router.get("/oauth", defineEventHandler(async event => {
|
as.router.get("/oauth", defineEventHandler(async event => {
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
const parsedFirstQuery = await getValidatedQuery(event, schema.first.safeParse)
|
const parsedFirstQuery = await getValidatedQuery(event, schema.first.safeParse)
|
||||||
let scope = "guilds"
|
let scope = "guilds"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ const {defineEventHandler, createError, readValidatedBody, getRequestHeader, set
|
||||||
|
|
||||||
const {as, db, sync, select, discord} = require("../../passthrough")
|
const {as, db, sync, select, discord} = require("../../passthrough")
|
||||||
|
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = sync.require("../auth")
|
const webUtils = sync.require("../web-utils")
|
||||||
/** @type {import("../pug-sync")} */
|
|
||||||
const pugSync = sync.require("../pug-sync")
|
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
optOut: z.object({
|
optOut: z.object({
|
||||||
|
|
@ -23,7 +21,7 @@ as.router.post("/api/opt-out", defineEventHandler(async event => {
|
||||||
throw createError({status: 403, message: "Forbidden", data: "JavaScript is required for the opt-out form."})
|
throw createError({status: 403, message: "Forbidden", data: "JavaScript is required for the opt-out form."})
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
if (!session.data.userID) {
|
if (!session.data.userID) {
|
||||||
throw createError({status: 401, message: "Unauthorised", data: "Log in first."})
|
throw createError({status: 401, message: "Unauthorised", data: "Log in first."})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const {z} = require("zod")
|
||||||
const {defineEventHandler, readValidatedBody, sendRedirect} = require("h3")
|
const {defineEventHandler, readValidatedBody, sendRedirect} = require("h3")
|
||||||
const {as, sync} = require("../../passthrough")
|
const {as, sync} = require("../../passthrough")
|
||||||
|
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../web-utils")} */
|
||||||
const auth = sync.require("../auth")
|
const webUtils = sync.require("../web-utils")
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
password: z.object({
|
password: z.object({
|
||||||
|
|
@ -15,7 +15,7 @@ const schema = {
|
||||||
|
|
||||||
as.router.post("/api/password", defineEventHandler(async event => {
|
as.router.post("/api/password", defineEventHandler(async event => {
|
||||||
const {password} = await readValidatedBody(event, schema.password.parse)
|
const {password} = await readValidatedBody(event, schema.password.parse)
|
||||||
const session = await auth.useSession(event)
|
const session = await webUtils.useSession(event)
|
||||||
await session.update({password})
|
await session.update({password})
|
||||||
return sendRedirect(event, "../")
|
return sendRedirect(event, "../")
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ const {z} = require("zod")
|
||||||
const pugSync = sync.require("../pug-sync")
|
const pugSync = sync.require("../pug-sync")
|
||||||
/** @type {import("../../discord/time")} */
|
/** @type {import("../../discord/time")} */
|
||||||
const discordTime = sync.require("../../discord/time")
|
const discordTime = sync.require("../../discord/time")
|
||||||
|
/** @type {import("../web-utils")} */
|
||||||
|
const webUtils = sync.require("../web-utils")
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
time: z.object({
|
time: z.object({
|
||||||
|
|
@ -17,6 +19,7 @@ const schema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
as.router.get("/time/:timestamp", defineEventHandler(async event => {
|
as.router.get("/time/:timestamp", defineEventHandler(async event => {
|
||||||
|
webUtils.rejectUrlPreviewer(event) // url previewers can't show the local time, making it useless. hiding for everyone's sake
|
||||||
const {timestamp} = await getValidatedRouterParams(event, schema.time.parse)
|
const {timestamp} = await getValidatedRouterParams(event, schema.time.parse)
|
||||||
const formatOptions = discordTime.getFormatOptions()
|
const formatOptions = discordTime.getFormatOptions()
|
||||||
const text = discordTime.convertDiscordTime(timestamp)
|
const text = discordTime.convertDiscordTime(timestamp)
|
||||||
|
|
@ -24,6 +27,7 @@ as.router.get("/time/:timestamp", defineEventHandler(async event => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
as.router.get("/time/:timestamp/:style", defineEventHandler(async event => {
|
as.router.get("/time/:timestamp/:style", defineEventHandler(async event => {
|
||||||
|
webUtils.rejectUrlPreviewer(event)
|
||||||
const {timestamp, style} = await getValidatedRouterParams(event, schema.time.parse)
|
const {timestamp, style} = await getValidatedRouterParams(event, schema.time.parse)
|
||||||
const formatOptions = discordTime.getFormatOptions(style)
|
const formatOptions = discordTime.getFormatOptions(style)
|
||||||
const text = discordTime.convertDiscordTime(timestamp, style)
|
const text = discordTime.convertDiscordTime(timestamp, style)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const h3 = require("h3")
|
const h3 = require("h3")
|
||||||
|
const {getRequestHeader, createError} = require("h3")
|
||||||
const {db} = require("../passthrough")
|
const {db} = require("../passthrough")
|
||||||
const {reg} = require("../matrix/read-registration")
|
const {reg} = require("../matrix/read-registration")
|
||||||
|
|
||||||
|
|
@ -29,5 +30,15 @@ function useSession(event) {
|
||||||
return h3.useSession(event, {password: reg.as_token, maxAge: 365 * 24 * 60 * 60})
|
return h3.useSession(event, {password: reg.as_token, maxAge: 365 * 24 * 60 * 60})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rejects by throwing createError.
|
||||||
|
* @param {h3.H3Event} event
|
||||||
|
*/
|
||||||
|
function rejectUrlPreviewer(event) {
|
||||||
|
const userAgent = getRequestHeader(event, "User-Agent")
|
||||||
|
if (userAgent?.match(/bot|matrix|tuwunel/i)) throw createError({status: 400, data: "Sorry URL previewer, you can't have this URL."})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.getManagedGuilds = getManagedGuilds
|
module.exports.getManagedGuilds = getManagedGuilds
|
||||||
module.exports.useSession = useSession
|
module.exports.useSession = useSession
|
||||||
|
module.exports.rejectUrlPreviewer = rejectUrlPreviewer
|
||||||
Loading…
Add table
Add a link
Reference in a new issue