Compare commits
No commits in common. "0034193953a5862d7da456d07bd609b0718ceb46" and "c8961a9beca0a9652a017806cb3269b0aa221475" have entirely different histories.
0034193953
...
c8961a9bec
14 changed files with 10 additions and 181 deletions
|
|
@ -297,7 +297,6 @@ module.exports = {
|
|||
*/
|
||||
async MESSAGE_CREATE(client, message) {
|
||||
if (message.author.username === "Deleted User") return // Nothing we can do for deleted users.
|
||||
if (select("opt_out", "user_id", {user_id: message.author.id}).pluck().get()) return // This user opted out.
|
||||
const channel = client.channels.get(message.channel_id)
|
||||
if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages.
|
||||
|
||||
|
|
@ -362,7 +361,6 @@ module.exports = {
|
|||
* @param {DiscordTypes.GatewayMessageReactionAddDispatchData} data
|
||||
*/
|
||||
async MESSAGE_REACTION_ADD(client, data) {
|
||||
if (select("opt_out", "user_id", {user_id: data.user_id}).pluck().get()) return // This user opted out.
|
||||
if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix.
|
||||
if (data.emoji.name === "❓" && select("event_message", "message_id", {message_id: data.message_id, source: 0, part: 0}).get()) { // source 0 = matrix
|
||||
const guild_id = data.guild_id ?? client.channels.get(data.channel_id)?.["guild_id"]
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "opt_out" (
|
||||
"user_id" TEXT NOT NULL,
|
||||
"opted_out_at" INTEGER NOT NULL,
|
||||
PRIMARY KEY("user_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
5
src/db/orm-defs.d.ts
vendored
5
src/db/orm-defs.d.ts
vendored
|
|
@ -115,11 +115,6 @@ export type Models = {
|
|||
historical_room_index: number
|
||||
}
|
||||
|
||||
opt_out: {
|
||||
user_id: string
|
||||
opted_out_at: number
|
||||
}
|
||||
|
||||
role_default: {
|
||||
guild_id: string
|
||||
role_id: string
|
||||
|
|
|
|||
1
src/types.d.ts
vendored
1
src/types.d.ts
vendored
|
|
@ -37,7 +37,6 @@ export type AppServiceRegistrationConfig = {
|
|||
time_zone?: string
|
||||
receive_presences: boolean
|
||||
plu_ral_api_key?: string
|
||||
bridge_admin_contact?: string
|
||||
}
|
||||
old_bridge?: {
|
||||
as_token: string
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ async function getManagedGuilds(event) {
|
|||
|
||||
/**
|
||||
* @param {h3.H3Event} event
|
||||
* @returns {ReturnType<typeof h3.useSession<{userID?: string, mxid?: string, managedGuilds?: string[], state?: string, selfService?: boolean, next?: string, password?: string}>>}
|
||||
* @returns {ReturnType<typeof h3.useSession<{userID?: string, mxid?: string, managedGuilds?: string[], state?: string, selfService?: boolean, password?: string}>>}
|
||||
*/
|
||||
function useSession(event) {
|
||||
return h3.useSession(event, {password: reg.as_token, maxAge: 365 * 24 * 60 * 60})
|
||||
|
|
|
|||
|
|
@ -157,10 +157,6 @@ html(lang="en")
|
|||
//- Body
|
||||
.mx-auto.w100.wmx9.py24.px8.fs-body1#content
|
||||
block body
|
||||
|
||||
if reg.ooye.bridge_admin_contact
|
||||
aside.fs-body0.mt48.pt8.bt.bc-black-350.d-inline-block
|
||||
a(href=rel("/privacy-policy")) Privacy policy
|
||||
//- Guild list popover
|
||||
script.
|
||||
document.querySelectorAll("[popovertarget]").forEach(e => {
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
extends includes/template.pug
|
||||
|
||||
block body
|
||||
if !session.data.userID
|
||||
.s-empty-state.wmx4.p48
|
||||
!= icons.Spots.SpotKeyXL
|
||||
p Please log in to access the opt-out page.
|
||||
.d-flex.jc-center.g8
|
||||
a.s-btn.s-btn__icon.s-btn__featured.s-btn__filled(href=rel("/oauth?next=/opt-out"))
|
||||
!= icons.Icons.IconDiscord
|
||||
= ` Log in with Discord`
|
||||
|
||||
else
|
||||
.s-page-title.mb24
|
||||
h1.s-page-title--header Opt Out
|
||||
|
||||
- let value = !select("opt_out", "user_id", {user_id: session.data.userID}).pluck().get()
|
||||
|
||||
#opt-in-msg(hx-swap-oob="true")
|
||||
if msg
|
||||
.s-notice.s-notice__info.mb16= msg
|
||||
else if !value
|
||||
.s-notice.s-notice__warning.mb16 You are currently opted out.
|
||||
|
||||
.s-card.d-grid.px0.g16
|
||||
form.d-flex.ai-center.g16
|
||||
#opt-in-loading.p8
|
||||
input.s-toggle-switch#opt-in(name="opt_in" type="checkbox" hx-post=rel("/api/opt-out") hx-indicator="#opt-in-loading" hx-disabled-elt="this" checked=value autocomplete="off" hx-swap="none")
|
||||
label.s-label.fl-grow1(for="opt-in")
|
||||
| Bridge my messages to Matrix
|
||||
p.s-description Turn it off to opt out. This applies to all servers.
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
extends includes/template.pug
|
||||
|
||||
block body
|
||||
.s-page-title.mb24
|
||||
h1.s-page-title--header Privacy Policy
|
||||
p.s-page-title--description Last updated 4 August 2026
|
||||
|
||||
.s-prose
|
||||
h2 Summary
|
||||
|
||||
p Out Of Your Element bridges messages between Matrix and Discord. It does this by forwarding all the messages from one platform to the other. Your messages aren't used for any other purpose.
|
||||
|
||||
p Matrix and Discord will store these messages until you delete them, but Out Of Your Element doesn't keep its own copy.
|
||||
|
||||
h2 Important information about the Matrix network
|
||||
|
||||
p The public Matrix Network is a decentralised and openly federated communication network. This means that user messages are replicated on each participant's homeserver, and messages posted to a room are visible to all participants, including new joiners. Anybody can run their own homeserver, so your messages may be widely replicated onto many servers. Out Of Your Element does not control these servers.
|
||||
|
||||
p Out Of Your Element does endeavour to remove the corresponding data from all of these servers when you delete a particular message, opt out, or request erasure. Due to the nature of decentralised systems, deletion might not be immediate or thorough. For example, some servers could be offline and catch up later, or they may have backups of old data.
|
||||
|
||||
p Please keep this in mind when using chatrooms with Out Of Your Element, or any other bridge software.
|
||||
|
||||
h2 What data is collected
|
||||
|
||||
p Data provided by you in the chatroom: Your messages, edits, uploaded files, username, profile picture, emojis, reactions, commands used, online indicator, typing indicator, and generally any other information written in chat will be copied to the other platform. This happens in all chatrooms where you see the Out Of Your Element bot.
|
||||
|
||||
p There is also a non-identifiable #[a(href=rel("/api/stats")) counter of how many people and chatrooms are active].
|
||||
|
||||
h2 What data isn't collected
|
||||
|
||||
p No tracking here. Out Of Your Element #[strong does not] automatically access or share your IP address, device fingerprint, location, read receipts, or your real-world identity.
|
||||
|
||||
h2 How long data is stored for
|
||||
|
||||
p Out Of Your Element doesn't store your message content directly. It only stores non-identifiable metadata, such as message IDs and room IDs.
|
||||
|
||||
p However, the messages are stored forever by Discord and Matrix, or until you delete them. Your messages will remain in the chat even if you leave the chat later.
|
||||
|
||||
p Your online indicator and typing indicator are only stored briefly to show your real-time status. They can't be viewed as history.
|
||||
|
||||
h2 Who can access this data
|
||||
|
||||
ul
|
||||
li Anybody participating in the chatroom (on either platform) can see your messages attributed to your username
|
||||
li Discord, Inc., and their hosting providers, can access that too
|
||||
li People running a Matrix homeserver in the room, and their hosting providers, can access that too (see #[a(href="https://matrix.org/legal/privacy-notice/") Matrix.org privacy policy])
|
||||
li Your data isn't shared with any other third parties (no analytics or monitoring)
|
||||
|
||||
h2 How to control access to your data
|
||||
|
||||
p To remove data in a specific message, you can delete that message. Deletions are bridged across all platforms. Discord and Matrix will discard the message.
|
||||
|
||||
p However, we can't stop people in the chatroom from independently saving or sharing a screenshot of your messages.
|
||||
|
||||
h3 Broad opt-out for Discord account holders
|
||||
p If you don't want any future messages to be bridged across platforms, #[a(href=rel("/opt-out")) opt out (of your element) here]. Keep in mind this could make conversations confusing because people will see different things.
|
||||
p Your old messages will stick around, but if you want those deleted as well, please contact the operator !{reg.ooye.bridge_admin_contact} to request erasure.
|
||||
|
||||
h2 Questions
|
||||
|
||||
p Out Of Your Element is open source, freely available software that anyone can run. The person running this version you're using is probably a different person from the software author.
|
||||
|
||||
p For questions about #[em your] data and how #[em this] version handles it, please contact the operator !{reg.ooye.bridge_admin_contact} to discuss.
|
||||
|
||||
p For general questions and clarifications about how the software handles data more broadly, please contact #[a(href="https://cadence.moe/contact") the author of the Out Of Your Element software.]
|
||||
|
|
@ -158,13 +158,13 @@ as.router.get("/guild", defineEventHandler(async event => {
|
|||
|
||||
// Permission problems
|
||||
if (!guild_id || !guild || !managed.has(guild_id) || !row) {
|
||||
return pugSync.render(event, "guild-access-denied.pug", {guild_id, row})
|
||||
return pugSync.render(event, "guild_access_denied.pug", {guild_id, row})
|
||||
}
|
||||
|
||||
// Self-service guild that hasn't been linked yet - needs a special page encouraging the link flow
|
||||
if (!row.space_id && row.autocreate === 0) {
|
||||
const spaces = session.data.mxid ? getInviteTargetSpaces(session.data.mxid) : []
|
||||
return pugSync.render(event, "guild-not-linked.pug", {guild, guild_id, spaces})
|
||||
return pugSync.render(event, "guild_not_linked.pug", {guild, guild_id, spaces})
|
||||
}
|
||||
|
||||
const roles = guild.members?.find(m => m.user.id === botID)?.roles || []
|
||||
|
|
@ -191,7 +191,7 @@ as.router.get("/qr", defineEventHandler(async event => {
|
|||
|
||||
// Permission problems
|
||||
if (!guild_id || !guild || !managed.has(guild_id) || !row) {
|
||||
return pugSync.render(event, "guild-access-denied.pug", {guild_id, row})
|
||||
return pugSync.render(event, "guild_access_denied.pug", {guild_id, row})
|
||||
}
|
||||
|
||||
const nonce = randomUUID()
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ const redirect_uri = `${reg.ooye.bridge_origin}/oauth`
|
|||
|
||||
const schema = {
|
||||
first: z.object({
|
||||
action: z.string().optional(),
|
||||
next: z.string().optional()
|
||||
action: z.string().optional()
|
||||
}),
|
||||
code: z.object({
|
||||
state: z.string(),
|
||||
|
|
@ -54,10 +53,10 @@ function getOauth2Token(event) {
|
|||
|
||||
as.router.get("/oauth", defineEventHandler(async event => {
|
||||
const session = await auth.useSession(event)
|
||||
const parsedFirstQuery = await getValidatedQuery(event, schema.first.safeParse)
|
||||
let scope = "guilds"
|
||||
|
||||
if (!reg.ooye.web_password || reg.ooye.web_password === session.data.password) {
|
||||
const parsedFirstQuery = await getValidatedQuery(event, schema.first.safeParse)
|
||||
if (parsedFirstQuery.data?.action === "add") {
|
||||
scope = "bot+guilds"
|
||||
await session.update({selfService: false})
|
||||
|
|
@ -70,9 +69,6 @@ as.router.get("/oauth", defineEventHandler(async event => {
|
|||
async function tryAgain() {
|
||||
const newState = randomUUID()
|
||||
await session.update({state: newState})
|
||||
if (parsedFirstQuery.data?.next) {
|
||||
await session.update({next: parsedFirstQuery.data?.next})
|
||||
}
|
||||
return sendRedirect(event, `https://discord.com/oauth2/authorize?client_id=${id}&scope=${scope}&permissions=${permissions}&response_type=code&redirect_uri=${redirect_uri}&state=${newState}`)
|
||||
}
|
||||
|
||||
|
|
@ -90,10 +86,8 @@ as.router.get("/oauth", defineEventHandler(async event => {
|
|||
const client = getClient(event)(parsedToken.access_token)
|
||||
|
||||
const guilds = await client.user.getGuilds()
|
||||
const managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id)
|
||||
|
||||
const savedNext = session.data.next
|
||||
await session.update({managedGuilds, userID, state: undefined, next: undefined})
|
||||
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
|
||||
|
|
@ -106,5 +100,5 @@ as.router.get("/oauth", defineEventHandler(async event => {
|
|||
return sendRedirect(event, getRelativePath(event.path, `/guild?guild_id=${parsedQuery.data.guild_id}`), 302)
|
||||
}
|
||||
|
||||
return sendRedirect(event, getRelativePath(event.path, savedNext || "/"), 302)
|
||||
return sendRedirect(event, getRelativePath(event.path, "/"), 302)
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert/strict")
|
||||
const {z} = require("zod")
|
||||
const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3")
|
||||
|
||||
const {as, db, sync, select, discord} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../auth")} */
|
||||
const auth = sync.require("../auth")
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
const schema = {
|
||||
optOut: z.object({
|
||||
opt_in: z.string().optional() // switch is reversed, you turn it on to opt in
|
||||
})
|
||||
}
|
||||
|
||||
as.router.post("/api/opt-out", defineEventHandler(async event => {
|
||||
// CSRF prevention for a more important endpoint
|
||||
if (!getRequestHeader(event, "HX-Request")) {
|
||||
throw createError({status: 403, message: "Forbidden", data: "JavaScript is required for the opt-out form."})
|
||||
}
|
||||
|
||||
const session = await auth.useSession(event)
|
||||
if (!session.data.userID) {
|
||||
throw createError({status: 401, message: "Unauthorised", data: "Log in first."})
|
||||
}
|
||||
|
||||
const parsedBody = await readValidatedBody(event, schema.optOut.parse)
|
||||
const isOptIn = !!parsedBody.opt_in
|
||||
|
||||
if (isOptIn) {
|
||||
db.prepare("DELETE FROM opt_out WHERE user_id = ?").run(session.data.userID)
|
||||
var msg = "You have opted in."
|
||||
} else {
|
||||
db.prepare("INSERT OR IGNORE INTO opt_out (user_id, opted_out_at) VALUES (?, ?)").run(session.data.userID, Date.now())
|
||||
var msg = "You have opted out."
|
||||
}
|
||||
|
||||
return sendRedirect(event, `../opt-out?${new URLSearchParams({msg})}`, 302)
|
||||
}))
|
||||
|
|
@ -123,11 +123,7 @@ as.router.get("/icon.png", defineEventHandler(async event => {
|
|||
|
||||
// Routes
|
||||
|
||||
if (reg.reg.ooye.bridge_admin_contact) {
|
||||
pugSync.createRoute(as.router, "/ok", "ok.pug")
|
||||
}
|
||||
pugSync.createRoute(as.router, "/opt-out", "opt-out.pug")
|
||||
pugSync.createRoute(as.router, "/privacy-policy", "privacy-policy.pug")
|
||||
|
||||
sync.require("./routes/download-matrix")
|
||||
sync.require("./routes/download-discord")
|
||||
|
|
@ -138,6 +134,5 @@ sync.require("./routes/letter-avatar")
|
|||
sync.require("./routes/link")
|
||||
sync.require("./routes/log-in-with-matrix")
|
||||
sync.require("./routes/oauth")
|
||||
sync.require("./routes/opt-out")
|
||||
sync.require("./routes/password")
|
||||
sync.require("./routes/stats")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue