Compare commits
4 commits
f98c30cac3
...
fc6cb8e0d5
Author | SHA1 | Date | |
---|---|---|---|
fc6cb8e0d5 | |||
0f435e930e | |||
69e3d64905 | |||
8ad0117fd2 |
16 changed files with 199 additions and 29 deletions
|
@ -36,6 +36,7 @@ Most features you'd expect in both directions, plus a little extra spice:
|
||||||
* Attachments
|
* Attachments
|
||||||
* Spoiler attachments
|
* Spoiler attachments
|
||||||
* Embeds
|
* Embeds
|
||||||
|
* Presence
|
||||||
* Guild-Space details syncing
|
* Guild-Space details syncing
|
||||||
* Channel-Room details syncing
|
* Channel-Room details syncing
|
||||||
* Custom emoji list syncing
|
* Custom emoji list syncing
|
||||||
|
|
|
@ -31,6 +31,8 @@ async function createSpace(guild, kstate) {
|
||||||
const topic = kstate["m.room.topic/"]?.topic || undefined
|
const topic = kstate["m.room.topic/"]?.topic || undefined
|
||||||
assert(name)
|
assert(name)
|
||||||
|
|
||||||
|
const memberCount = guild["member_count"] ?? guild.approximate_member_count ?? 0
|
||||||
|
const enablePresenceByDefault = +(memberCount < 150) // could increase this later on if it doesn't cause any problems
|
||||||
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
|
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
|
||||||
|
|
||||||
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
||||||
|
@ -50,7 +52,7 @@ async function createSpace(guild, kstate) {
|
||||||
initial_state: await ks.kstateToState(kstate)
|
initial_state: await ks.kstateToState(kstate)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID)
|
db.prepare("INSERT INTO guild_space (guild_id, space_id, presence) VALUES (?, ?, ?)").run(guild.id, roomID, enablePresenceByDefault)
|
||||||
return roomID
|
return roomID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
57
src/d2m/actions/set-presence.js
Normal file
57
src/d2m/actions/set-presence.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const passthrough = require("../../passthrough")
|
||||||
|
const {sync, select} = passthrough
|
||||||
|
/** @type {import("../../matrix/api")} */
|
||||||
|
const api = sync.require("../../matrix/api")
|
||||||
|
|
||||||
|
// Adding a debounce to all updates because events are issued multiple times, once for each guild.
|
||||||
|
// Sometimes a status update is even issued twice in a row for the same user+guild, weird!
|
||||||
|
const presenceDelay = 1500
|
||||||
|
/** @type {Map<string, NodeJS.Timeout>} user ID -> cancelable timeout */
|
||||||
|
const presenceDelayMap = new Map()
|
||||||
|
|
||||||
|
// Access the list of enabled guilds as needed rather than like multiple times per second when a user changes presence
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
let presenceEnabledGuilds
|
||||||
|
function checkPresenceEnabledGuilds() {
|
||||||
|
presenceEnabledGuilds = new Set(select("guild_space", "guild_id", {presence: 1}).pluck().all())
|
||||||
|
}
|
||||||
|
checkPresenceEnabledGuilds()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} userID Discord user ID
|
||||||
|
* @param {string} guildID Discord guild ID that this presence applies to (really, the same presence applies to every single guild, but is delivered separately)
|
||||||
|
* @param {string} status status field from Discord's PRESENCE_UPDATE event
|
||||||
|
*/
|
||||||
|
function setPresence(userID, guildID, status) {
|
||||||
|
// check if we care about this guild
|
||||||
|
if (!presenceEnabledGuilds.has(guildID)) return
|
||||||
|
// cancel existing timer if one is already set
|
||||||
|
if (presenceDelayMap.has(userID)) {
|
||||||
|
clearTimeout(presenceDelayMap.get(userID))
|
||||||
|
}
|
||||||
|
// new timer, which will run if nothing else comes in soon
|
||||||
|
presenceDelayMap.set(userID, setTimeout(setPresenceCallback, presenceDelay, userID, status).unref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} user_id Discord user ID
|
||||||
|
* @param {string} status status field from Discord's PRESENCE_UPDATE event
|
||||||
|
*/
|
||||||
|
function setPresenceCallback(user_id, status) {
|
||||||
|
presenceDelayMap.delete(user_id)
|
||||||
|
const mxid = select("sim", "mxid", {user_id}).pluck().get()
|
||||||
|
if (!mxid) return
|
||||||
|
const presence =
|
||||||
|
( status === "online" ? "online"
|
||||||
|
: status === "offline" ? "offline"
|
||||||
|
: "unavailable") // idle, dnd, and anything else they dream up in the future
|
||||||
|
api.setPresence(presence, mxid).catch(e => {
|
||||||
|
console.error("d->m: Skipping presence update failure:")
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.setPresence = setPresence
|
||||||
|
module.exports.checkPresenceEnabledGuilds = checkPresenceEnabledGuilds
|
|
@ -28,7 +28,7 @@ class DiscordClient {
|
||||||
intents: [
|
intents: [
|
||||||
"DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING",
|
"DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING",
|
||||||
"GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS",
|
"GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS",
|
||||||
"MESSAGE_CONTENT"
|
"MESSAGE_CONTENT", "GUILD_PRESENCES"
|
||||||
],
|
],
|
||||||
ws: {
|
ws: {
|
||||||
compress: false,
|
compress: false,
|
||||||
|
|
|
@ -196,6 +196,9 @@ const utils = {
|
||||||
|
|
||||||
} else if (message.t === "INTERACTION_CREATE") {
|
} else if (message.t === "INTERACTION_CREATE") {
|
||||||
await interactions.dispatchInteraction(message.d)
|
await interactions.dispatchInteraction(message.d)
|
||||||
|
|
||||||
|
} else if (message.t === "PRESENCE_UPDATE") {
|
||||||
|
eventDispatcher.onPresenceUpdate(client, message.d)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -33,6 +33,8 @@ const mxUtils = require("../m2d/converters/utils")
|
||||||
const speedbump = sync.require("./actions/speedbump")
|
const speedbump = sync.require("./actions/speedbump")
|
||||||
/** @type {import("./actions/retrigger")} */
|
/** @type {import("./actions/retrigger")} */
|
||||||
const retrigger = sync.require("./actions/retrigger")
|
const retrigger = sync.require("./actions/retrigger")
|
||||||
|
/** @type {import("./actions/set-presence")} */
|
||||||
|
const setPresence = sync.require("./actions/set-presence")
|
||||||
|
|
||||||
/** @type {any} */ // @ts-ignore bad types from semaphore
|
/** @type {any} */ // @ts-ignore bad types from semaphore
|
||||||
const Semaphore = require("@chriscdn/promise-semaphore")
|
const Semaphore = require("@chriscdn/promise-semaphore")
|
||||||
|
@ -369,5 +371,15 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
async onExpressionsUpdate(client, data) {
|
async onExpressionsUpdate(client, data) {
|
||||||
await createSpace.syncSpaceExpressions(data, false)
|
await createSpace.syncSpaceExpressions(data, false)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {DiscordTypes.GatewayPresenceUpdateDispatchData} data
|
||||||
|
*/
|
||||||
|
async onPresenceUpdate(client, data) {
|
||||||
|
const status = data.status
|
||||||
|
if (!status) return
|
||||||
|
setPresence.setPresence(data.user.id, data.guild_id, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
src/db/migrations/0020-add-presence-to-guild-space.sql
Normal file
5
src/db/migrations/0020-add-presence-to-guild-space.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE guild_space ADD COLUMN presence INTEGER NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
COMMIT;
|
1
src/db/orm-defs.d.ts
vendored
1
src/db/orm-defs.d.ts
vendored
|
@ -33,6 +33,7 @@ export type Models = {
|
||||||
guild_id: string
|
guild_id: string
|
||||||
space_id: string
|
space_id: string
|
||||||
privacy_level: number
|
privacy_level: number
|
||||||
|
presence: 0 | 1
|
||||||
}
|
}
|
||||||
|
|
||||||
guild_active: {
|
guild_active: {
|
||||||
|
|
|
@ -476,7 +476,7 @@ async function eventToMessage(event, guild, di) {
|
||||||
// Try to extract an accurate display name and avatar URL from the member event
|
// Try to extract an accurate display name and avatar URL from the member event
|
||||||
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
|
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
|
||||||
if (member.displayname) displayName = member.displayname
|
if (member.displayname) displayName = member.displayname
|
||||||
if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) || undefined
|
if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url)
|
||||||
// If the display name is too long to be put into the webhook (80 characters is the maximum),
|
// If the display name is too long to be put into the webhook (80 characters is the maximum),
|
||||||
// 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)
|
||||||
|
@ -512,8 +512,7 @@ async function eventToMessage(event, guild, di) {
|
||||||
// Is it editing a reply? We need special handling if it is.
|
// Is it editing a reply? We need special handling if it is.
|
||||||
// Get the original event, then check if it was a reply
|
// Get the original event, then check if it was a reply
|
||||||
const originalEvent = await di.api.getEvent(event.room_id, originalEventId)
|
const originalEvent = await di.api.getEvent(event.room_id, originalEventId)
|
||||||
if (!originalEvent) return
|
const repliedToEventId = originalEvent?.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id
|
||||||
const repliedToEventId = originalEvent.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
|
|
||||||
if (!repliedToEventId) return
|
if (!repliedToEventId) return
|
||||||
|
|
||||||
// After all that, it's an edit of a reply.
|
// After all that, it's an edit of a reply.
|
||||||
|
@ -576,34 +575,27 @@ async function eventToMessage(event, guild, di) {
|
||||||
if (row) {
|
if (row) {
|
||||||
replyLine += `https://discord.com/channels/${guild.id}/${row.channel_id}/${row.message_id} `
|
replyLine += `https://discord.com/channels/${guild.id}/${row.channel_id}/${row.message_id} `
|
||||||
}
|
}
|
||||||
const sender = repliedToEvent.sender
|
|
||||||
const authorID = getUserOrProxyOwnerID(sender)
|
|
||||||
if (authorID) {
|
|
||||||
replyLine += `<@${authorID}>`
|
|
||||||
} else {
|
|
||||||
let senderName = select("member_cache", "displayname", {mxid: sender}).pluck().get()
|
|
||||||
if (!senderName) {
|
|
||||||
const match = sender.match(/@([^:]*)/)
|
|
||||||
assert(match)
|
|
||||||
senderName = match[1]
|
|
||||||
}
|
|
||||||
replyLine += `**Ⓜ${senderName}**`
|
|
||||||
}
|
|
||||||
// If the event has been edited, the homeserver will include the relation in `unsigned`.
|
// If the event has been edited, the homeserver will include the relation in `unsigned`.
|
||||||
if (repliedToEvent.unsigned?.["m.relations"]?.["m.replace"]?.content?.["m.new_content"]) {
|
if (repliedToEvent.unsigned?.["m.relations"]?.["m.replace"]?.content?.["m.new_content"]) {
|
||||||
repliedToEvent = repliedToEvent.unsigned["m.relations"]["m.replace"] // Note: this changes which event_id is in repliedToEvent.
|
repliedToEvent = repliedToEvent.unsigned["m.relations"]["m.replace"] // Note: this changes which event_id is in repliedToEvent.
|
||||||
repliedToEvent.content = repliedToEvent.content["m.new_content"]
|
repliedToEvent.content = repliedToEvent.content["m.new_content"]
|
||||||
}
|
}
|
||||||
let contentPreview
|
/** @type {string} */
|
||||||
|
let repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body
|
||||||
const fileReplyContentAlternative = attachmentEmojis.get(repliedToEvent.content.msgtype)
|
const fileReplyContentAlternative = attachmentEmojis.get(repliedToEvent.content.msgtype)
|
||||||
|
let contentPreview
|
||||||
if (fileReplyContentAlternative) {
|
if (fileReplyContentAlternative) {
|
||||||
contentPreview = " " + fileReplyContentAlternative
|
contentPreview = " " + fileReplyContentAlternative
|
||||||
} else if (repliedToEvent.unsigned?.redacted_because) {
|
} else if (repliedToEvent.unsigned?.redacted_because) {
|
||||||
contentPreview = " (in reply to a deleted message)"
|
contentPreview = " (in reply to a deleted message)"
|
||||||
|
} else if (typeof repliedToContent !== "string") {
|
||||||
|
// in reply to a weird metadata event like m.room.name, m.room.member...
|
||||||
|
// I'm not implementing text fallbacks for arbitrary room events. this should cover most cases
|
||||||
|
// this has never ever happened in the wild anyway
|
||||||
|
repliedToEvent.sender = ""
|
||||||
|
contentPreview = " (channel details edited)"
|
||||||
} else {
|
} else {
|
||||||
// Generate a reply preview for a standard message
|
// Generate a reply preview for a standard message
|
||||||
/** @type {string} */
|
|
||||||
let repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body
|
|
||||||
repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body
|
repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body
|
||||||
repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
|
repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
|
||||||
repliedToContent = repliedToContent.replace(/(?:\n|<br>)+/g, " ") // Should all be on one line
|
repliedToContent = repliedToContent.replace(/(?:\n|<br>)+/g, " ") // Should all be on one line
|
||||||
|
@ -624,6 +616,15 @@ async function eventToMessage(event, guild, di) {
|
||||||
contentPreview = ""
|
contentPreview = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const sender = repliedToEvent.sender
|
||||||
|
const authorID = getUserOrProxyOwnerID(sender)
|
||||||
|
if (authorID) {
|
||||||
|
replyLine += `<@${authorID}>`
|
||||||
|
} else {
|
||||||
|
let senderName = select("member_cache", "displayname", {mxid: sender}).pluck().get()
|
||||||
|
if (!senderName) senderName = sender.match(/@([^:]*)/)?.[1]
|
||||||
|
if (senderName) replyLine += `**Ⓜ${senderName}**`
|
||||||
|
}
|
||||||
replyLine = `-# > ${replyLine}${contentPreview}\n`
|
replyLine = `-# > ${replyLine}${contentPreview}\n`
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
|
@ -2625,6 +2625,52 @@ test("event2message: rich reply to a deleted event", async t => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("event2message: rich reply to a state event with no body", async t => {
|
||||||
|
t.deepEqual(
|
||||||
|
await eventToMessage({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@ampflower:matrix.org",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "> <@ampflower:matrix.org> changed the room topic\n\nnice room topic",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "<mx-reply><blockquote><a href=\"https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$f-noT-d-Eo_Xgpc05Ww89ErUXku4NwKWYGHLzWKo1kU?via=cadence.moe\">In reply to</a> <a href=\"https://matrix.to/#/@ampflower:matrix.org\">@ampflower:matrix.org</a> changed the room topic<br></blockquote></mx-reply>nice room topic",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
event_id: "$f-noT-d-Eo_Xgpc05Ww89ErUXku4NwKWYGHLzWKo1kU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event_id: "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
|
||||||
|
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
|
||||||
|
}, data.guild.general, {
|
||||||
|
api: {
|
||||||
|
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$f-noT-d-Eo_Xgpc05Ww89ErUXku4NwKWYGHLzWKo1kU", {
|
||||||
|
type: "m.room.topic",
|
||||||
|
sender: "@ampflower:matrix.org",
|
||||||
|
content: {
|
||||||
|
topic: "you're cute"
|
||||||
|
},
|
||||||
|
user_id: "@ampflower:matrix.org"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
ensureJoined: [],
|
||||||
|
messagesToDelete: [],
|
||||||
|
messagesToEdit: [],
|
||||||
|
messagesToSend: [{
|
||||||
|
username: "Ampflower 🌺",
|
||||||
|
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647> (channel details edited)\nnice room topic",
|
||||||
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko",
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["users", "roles"]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("event2message: raw mentioning discord users in plaintext body works", async t => {
|
test("event2message: raw mentioning discord users in plaintext body works", async t => {
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
await eventToMessage({
|
await eventToMessage({
|
||||||
|
|
|
@ -217,12 +217,12 @@ async function getViaServersQuery(roomID, api) {
|
||||||
* @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details
|
* @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details
|
||||||
* @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size
|
* @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size
|
||||||
* @param {string} mxc
|
* @param {string} mxc
|
||||||
* @returns {string?}
|
* @returns {string | undefined}
|
||||||
*/
|
*/
|
||||||
function getPublicUrlForMxc(mxc) {
|
function getPublicUrlForMxc(mxc) {
|
||||||
assert(hasher, "xxhash is not ready yet")
|
assert(hasher, "xxhash is not ready yet")
|
||||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
||||||
if (!mediaParts) return null
|
if (!mediaParts) return undefined
|
||||||
|
|
||||||
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
||||||
const unsignedHash = hasher.h64(serverAndMediaID)
|
const unsignedHash = hasher.h64(serverAndMediaID)
|
||||||
|
|
|
@ -408,6 +408,14 @@ async function setAccountData(type, content, mxid) {
|
||||||
await mreq.mreq("PUT", `/client/v3/user/${mxid}/account_data/${type}`, content)
|
await mreq.mreq("PUT", `/client/v3/user/${mxid}/account_data/${type}`, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {"online" | "offline" | "unavailable"} presence
|
||||||
|
* @param {string} mxid
|
||||||
|
*/
|
||||||
|
async function setPresence(presence, mxid) {
|
||||||
|
await mreq.mreq("PUT", path(`/client/v3/presence/${mxid}/status`, mxid), {presence})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.path = path
|
module.exports.path = path
|
||||||
module.exports.register = register
|
module.exports.register = register
|
||||||
module.exports.createRoom = createRoom
|
module.exports.createRoom = createRoom
|
||||||
|
@ -440,3 +448,4 @@ module.exports.ackEvent = ackEvent
|
||||||
module.exports.getAlias = getAlias
|
module.exports.getAlias = getAlias
|
||||||
module.exports.getAccountData = getAccountData
|
module.exports.getAccountData = getAccountData
|
||||||
module.exports.setAccountData = setAccountData
|
module.exports.setAccountData = setAccountData
|
||||||
|
module.exports.setPresence = setPresence
|
||||||
|
|
|
@ -102,6 +102,17 @@ block body
|
||||||
#autocreate-loading
|
#autocreate-loading
|
||||||
|
|
||||||
if space_id
|
if space_id
|
||||||
|
h3.mt32.fs-category Presence
|
||||||
|
.s-card
|
||||||
|
form.d-flex.ai-center.g8
|
||||||
|
label.s-label.fl-grow1(for="presence")
|
||||||
|
| Show online statuses on Matrix
|
||||||
|
p.s-description This might cause lag on really big Discord servers.
|
||||||
|
- let value = !!select("guild_space", "presence", {guild_id}).pluck().get()
|
||||||
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
|
input.s-toggle-switch.order-last#autocreate(name="presence" type="checkbox" hx-post="/api/presence" hx-indicator="#presence-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
||||||
|
#presence-loading
|
||||||
|
|
||||||
h3.mt32.fs-category Privacy level
|
h3.mt32.fs-category Privacy level
|
||||||
.s-card
|
.s-card
|
||||||
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
||||||
|
|
|
@ -5,12 +5,13 @@ block body
|
||||||
.s-empty-state.wmx4.p48
|
.s-empty-state.wmx4.p48
|
||||||
!= icons.Spots.SpotEmptyXL
|
!= icons.Spots.SpotEmptyXL
|
||||||
p You need to log in to manage your servers.
|
p You need to log in to manage your servers.
|
||||||
a.s-btn.s-btn__icon.s-btn__featured.s-btn__filled(href=rel("/oauth"))
|
.d-flex.jc-center.g8
|
||||||
!= icons.Icons.IconDiscord
|
a.s-btn.s-btn__icon.s-btn__featured.s-btn__filled(href=rel("/oauth"))
|
||||||
= ` Log in with Discord`
|
!= icons.Icons.IconDiscord
|
||||||
a.s-btn.s-btn__icon.s-btn__matrix.s-btn__filled(href=rel("/log-in-with-matrix"))
|
= ` Log in with Discord`
|
||||||
!= icons.Icons.IconChatBubble
|
a.s-btn.s-btn__icon.s-btn__matrix.s-btn__filled(href=rel("/log-in-with-matrix"))
|
||||||
= ` Log in with Matrix`
|
!= icons.Icons.IconSpeechBubble
|
||||||
|
= ` Log in with Matrix`
|
||||||
|
|
||||||
else if !guild_id
|
else if !guild_id
|
||||||
.s-empty-state.wmx4.p48
|
.s-empty-state.wmx4.p48
|
||||||
|
|
|
@ -81,6 +81,10 @@ html(lang="en")
|
||||||
else if managed.size
|
else if managed.size
|
||||||
button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.bar0.fc-black(popovertarget="guilds")
|
button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.bar0.fc-black(popovertarget="guilds")
|
||||||
| Your servers
|
| Your servers
|
||||||
|
else
|
||||||
|
.d-flex.ai-center
|
||||||
|
.s-badge.s-badge__bot.py6.px16.bar-md
|
||||||
|
| No servers available
|
||||||
#guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible
|
#guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible
|
||||||
.s-popover--arrow.s-popover--arrow__tc
|
.s-popover--arrow.s-popover--arrow__tc
|
||||||
.s-popover--content.overflow-y-auto.overflow-x-hidden
|
.s-popover--content.overflow-y-auto.overflow-x-hidden
|
||||||
|
|
|
@ -8,6 +8,8 @@ const {as, db, sync, select} = require("../../passthrough")
|
||||||
|
|
||||||
/** @type {import("../auth")} */
|
/** @type {import("../auth")} */
|
||||||
const auth = sync.require("../auth")
|
const auth = sync.require("../auth")
|
||||||
|
/** @type {import("../../d2m/actions/set-presence")} */
|
||||||
|
const setPresence = sync.require("../../d2m/actions/set-presence")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {H3Event} event
|
* @param {H3Event} event
|
||||||
|
@ -25,6 +27,10 @@ const schema = {
|
||||||
guild_id: z.string(),
|
guild_id: z.string(),
|
||||||
autocreate: z.string().optional()
|
autocreate: z.string().optional()
|
||||||
}),
|
}),
|
||||||
|
presence: z.object({
|
||||||
|
guild_id: z.string(),
|
||||||
|
presence: z.string().optional()
|
||||||
|
}),
|
||||||
privacyLevel: z.object({
|
privacyLevel: z.object({
|
||||||
guild_id: z.string(),
|
guild_id: z.string(),
|
||||||
level: z.enum(levels)
|
level: z.enum(levels)
|
||||||
|
@ -51,6 +57,17 @@ as.router.post("/api/autocreate", defineEventHandler(async event => {
|
||||||
return null // 204
|
return null // 204
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
as.router.post("/api/presence", defineEventHandler(async event => {
|
||||||
|
const parsedBody = await readValidatedBody(event, schema.presence.parse)
|
||||||
|
const managed = await auth.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"})
|
||||||
|
|
||||||
|
db.prepare("UPDATE guild_space SET presence = ? WHERE guild_id = ?").run(+!!parsedBody.presence, parsedBody.guild_id)
|
||||||
|
setPresence.checkPresenceEnabledGuilds()
|
||||||
|
|
||||||
|
return null // 204
|
||||||
|
}))
|
||||||
|
|
||||||
as.router.post("/api/privacy-level", defineEventHandler(async event => {
|
as.router.post("/api/privacy-level", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.privacyLevel.parse)
|
const parsedBody = await readValidatedBody(event, schema.privacyLevel.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await auth.getManagedGuilds(event)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue