Compare commits
4 commits
5810fb3955
...
b34932d552
Author | SHA1 | Date | |
---|---|---|---|
b34932d552 | |||
415f2e9020 | |||
20fd58418a | |||
25cd9c851b |
10 changed files with 81 additions and 19 deletions
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const reg = require("../../matrix/read-registration")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const { discord, sync, db } = passthrough
|
const { discord, sync, db } = passthrough
|
||||||
|
@ -61,11 +62,16 @@ async function channelToKState(channel, guild) {
|
||||||
const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id)
|
const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id)
|
||||||
assert.ok(typeof spaceID === "string")
|
assert.ok(typeof spaceID === "string")
|
||||||
|
|
||||||
const customName = db.prepare("SELECT nick FROM channel_room WHERE channel_id = ?").pluck().get(channel.id)
|
const row = db.prepare("SELECT nick, custom_avatar FROM channel_room WHERE channel_id = ?").get(channel.id)
|
||||||
|
assert(row)
|
||||||
|
const customName = row.nick
|
||||||
|
const customAvatar = row.custom_avatar
|
||||||
const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName)
|
const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName)
|
||||||
|
|
||||||
const avatarEventContent = {}
|
const avatarEventContent = {}
|
||||||
if (guild.icon) {
|
if (customAvatar) {
|
||||||
|
avatarEventContent.url = customAvatar
|
||||||
|
} else if (guild.icon) {
|
||||||
avatarEventContent.discord_path = file.guildIcon(guild)
|
avatarEventContent.discord_path = file.guildIcon(guild)
|
||||||
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
|
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
|
||||||
}
|
}
|
||||||
|
@ -80,7 +86,7 @@ async function channelToKState(channel, guild) {
|
||||||
"m.room.guest_access/": {guest_access: "can_join"},
|
"m.room.guest_access/": {guest_access: "can_join"},
|
||||||
"m.room.history_visibility/": {history_visibility},
|
"m.room.history_visibility/": {history_visibility},
|
||||||
[`m.space.parent/${spaceID}`]: {
|
[`m.space.parent/${spaceID}`]: {
|
||||||
via: ["cadence.moe"], // TODO: put the proper server here
|
via: [reg.ooye.server_name],
|
||||||
canonical: true
|
canonical: true
|
||||||
},
|
},
|
||||||
"m.room.join_rules/": {
|
"m.room.join_rules/": {
|
||||||
|
@ -183,6 +189,8 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here.
|
return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomID = existing.room_id
|
||||||
|
|
||||||
if (!shouldActuallySync) {
|
if (!shouldActuallySync) {
|
||||||
return existing.room_id // only need to ensure room exists, and it does. return the room ID
|
return existing.room_id // only need to ensure room exists, and it does. return the room ID
|
||||||
}
|
}
|
||||||
|
@ -192,15 +200,16 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
||||||
|
|
||||||
// sync channel state to room
|
// sync channel state to room
|
||||||
const roomKState = await roomToKState(existing.room_id)
|
const roomKState = await roomToKState(roomID)
|
||||||
const roomDiff = ks.diffKState(roomKState, channelKState)
|
const roomDiff = ks.diffKState(roomKState, channelKState)
|
||||||
const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff)
|
const roomApply = applyKStateDiffToRoom(roomID, roomDiff)
|
||||||
|
db.prepare("UPDATE channel_room SET name = ? WHERE room_id = ?").run(channel.name, roomID)
|
||||||
|
|
||||||
// sync room as space member
|
// sync room as space member
|
||||||
const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id)
|
const spaceApply = _syncSpaceMember(channel, spaceID, roomID)
|
||||||
await Promise.all([roomApply, spaceApply])
|
await Promise.all([roomApply, spaceApply])
|
||||||
|
|
||||||
return existing.room_id
|
return roomID
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _unbridgeRoom(channelID) {
|
async function _unbridgeRoom(channelID) {
|
||||||
|
@ -244,7 +253,7 @@ async function _syncSpaceMember(channel, spaceID, roomID) {
|
||||||
&& !channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant)
|
&& !channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant)
|
||||||
) {
|
) {
|
||||||
spaceEventContent = {
|
spaceEventContent = {
|
||||||
via: ["cadence.moe"] // TODO: use the proper server
|
via: [reg.ooye.server_name]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const spaceDiff = ks.diffKState(spaceKState, {
|
const spaceDiff = ks.diffKState(spaceKState, {
|
||||||
|
@ -279,5 +288,7 @@ module.exports.ensureRoom = ensureRoom
|
||||||
module.exports.syncRoom = syncRoom
|
module.exports.syncRoom = syncRoom
|
||||||
module.exports.createAllForGuild = createAllForGuild
|
module.exports.createAllForGuild = createAllForGuild
|
||||||
module.exports.channelToKState = channelToKState
|
module.exports.channelToKState = channelToKState
|
||||||
|
module.exports.roomToKState = roomToKState
|
||||||
|
module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom
|
||||||
module.exports._convertNameAndTopic = convertNameAndTopic
|
module.exports._convertNameAndTopic = convertNameAndTopic
|
||||||
module.exports._unbridgeRoom = _unbridgeRoom
|
module.exports._unbridgeRoom = _unbridgeRoom
|
||||||
|
|
|
@ -4,13 +4,15 @@ const assert = require("assert")
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const { sync, db } = passthrough
|
const { discord, sync, db } = passthrough
|
||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
const api = sync.require("../../matrix/api")
|
const api = sync.require("../../matrix/api")
|
||||||
/** @type {import("../../matrix/file")} */
|
/** @type {import("../../matrix/file")} */
|
||||||
const file = sync.require("../../matrix/file")
|
const file = sync.require("../../matrix/file")
|
||||||
/** @type {import("./create-room")} */
|
/** @type {import("./create-room")} */
|
||||||
const createRoom = sync.require("./create-room")
|
const createRoom = sync.require("./create-room")
|
||||||
|
/** @type {import("../../matrix/kstate")} */
|
||||||
|
const ks = sync.require("../../matrix/kstate")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild
|
* @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild
|
||||||
|
@ -58,7 +60,7 @@ async function guildToKState(guild) {
|
||||||
"m.room.name/": {name: guild.name},
|
"m.room.name/": {name: guild.name},
|
||||||
"m.room.avatar/": avatarEventContent,
|
"m.room.avatar/": avatarEventContent,
|
||||||
"m.room.guest_access/": {guest_access: "can_join"}, // guests can join space if other conditions are met
|
"m.room.guest_access/": {guest_access: "can_join"}, // guests can join space if other conditions are met
|
||||||
"m.room.history_visibility": {history_visibility: "invited"} // any events sent after user was invited are visible
|
"m.room.history_visibility/": {history_visibility: "invited"} // any events sent after user was invited are visible
|
||||||
}
|
}
|
||||||
|
|
||||||
return guildKState
|
return guildKState
|
||||||
|
@ -69,17 +71,26 @@ async function syncSpace(guildID) {
|
||||||
const guild = discord.guilds.get(guildID)
|
const guild = discord.guilds.get(guildID)
|
||||||
assert.ok(guild)
|
assert.ok(guild)
|
||||||
|
|
||||||
/** @type {{room_id: string, thread_parent: string?}} */
|
/** @type {string?} */
|
||||||
const existing = db.prepare("SELECT space_id from guild_space WHERE guild_id = ?").get(guildID)
|
const spaceID = db.prepare("SELECT space_id from guild_space WHERE guild_id = ?").pluck().get(guildID)
|
||||||
|
|
||||||
const guildKState = await guildToKState(guild)
|
const guildKState = await guildToKState(guild)
|
||||||
|
|
||||||
if (!existing) {
|
if (!spaceID) {
|
||||||
const spaceID = await createSpace(guild, guildKState)
|
const spaceID = await createSpace(guild, guildKState)
|
||||||
return spaceID
|
return spaceID // Naturally, the newly created space is already up to date, so we can always skip syncing here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[space sync] to matrix: ${guild.name}`)
|
||||||
|
|
||||||
|
// sync channel state to room
|
||||||
|
const spaceKState = await createRoom.roomToKState(spaceID)
|
||||||
|
const spaceDiff = ks.diffKState(spaceKState, guildKState)
|
||||||
|
await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff)
|
||||||
|
|
||||||
|
return spaceID
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.createSpace = createSpace
|
module.exports.createSpace = createSpace
|
||||||
|
module.exports.syncSpace = syncSpace
|
||||||
|
module.exports.guildToKState = guildToKState
|
||||||
|
|
|
@ -21,7 +21,7 @@ async function createSim(user) {
|
||||||
// Choose sim name
|
// Choose sim name
|
||||||
const simName = userToMxid.userToSimName(user)
|
const simName = userToMxid.userToSimName(user)
|
||||||
const localpart = reg.ooye.namespace_prefix + simName
|
const localpart = reg.ooye.namespace_prefix + simName
|
||||||
const mxid = "@" + localpart + ":cadence.moe"
|
const mxid = `@${localpart}:${reg.ooye.server_name}`
|
||||||
|
|
||||||
// Save chosen name in the database forever
|
// Save chosen name in the database forever
|
||||||
// Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates
|
// Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates
|
||||||
|
|
|
@ -82,7 +82,10 @@ const utils = {
|
||||||
|
|
||||||
// Event dispatcher for OOYE bridge operations
|
// Event dispatcher for OOYE bridge operations
|
||||||
try {
|
try {
|
||||||
if (message.t === "CHANNEL_UPDATE") {
|
if (message.t === "GUILD_UPDATE") {
|
||||||
|
await eventDispatcher.onGuildUpdate(client, message.d)
|
||||||
|
|
||||||
|
} else if (message.t === "CHANNEL_UPDATE") {
|
||||||
await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false)
|
await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false)
|
||||||
|
|
||||||
} else if (message.t === "THREAD_CREATE") {
|
} else if (message.t === "THREAD_CREATE") {
|
||||||
|
|
|
@ -14,6 +14,8 @@ const addReaction = sync.require("./actions/add-reaction")
|
||||||
const announceThread = sync.require("./actions/announce-thread")
|
const announceThread = sync.require("./actions/announce-thread")
|
||||||
/** @type {import("./actions/create-room")}) */
|
/** @type {import("./actions/create-room")}) */
|
||||||
const createRoom = sync.require("./actions/create-room")
|
const createRoom = sync.require("./actions/create-room")
|
||||||
|
/** @type {import("./actions/create-space")}) */
|
||||||
|
const createSpace = sync.require("./actions/create-space")
|
||||||
/** @type {import("../matrix/api")}) */
|
/** @type {import("../matrix/api")}) */
|
||||||
const api = sync.require("../matrix/api")
|
const api = sync.require("../matrix/api")
|
||||||
|
|
||||||
|
@ -116,6 +118,16 @@ module.exports = {
|
||||||
await announceThread.announceThread(parentRoomID, threadRoomID, thread)
|
await announceThread.announceThread(parentRoomID, threadRoomID, thread)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {import("discord-api-types/v10").GatewayGuildUpdateDispatchData} guild
|
||||||
|
*/
|
||||||
|
async onGuildUpdate(client, guild) {
|
||||||
|
const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id)
|
||||||
|
if (!spaceID) return
|
||||||
|
await createSpace.syncSpace(guild.id)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("./discord-client")} client
|
* @param {import("./discord-client")} client
|
||||||
* @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread
|
* @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread
|
||||||
|
|
|
@ -11,6 +11,7 @@ function eventSenderIsFromDiscord(sender) {
|
||||||
// If it's from a user in the bridge's namespace, then it originated from discord
|
// If it's from a user in the bridge's namespace, then it originated from discord
|
||||||
// This includes messages sent by the appservice's bot user, because that is what's used for webhooks
|
// This includes messages sent by the appservice's bot user, because that is what's used for webhooks
|
||||||
// TODO: It would be nice if bridge system messages wouldn't trigger this check and could be bridged from matrix to discord, while webhook reflections would remain ignored...
|
// TODO: It would be nice if bridge system messages wouldn't trigger this check and could be bridged from matrix to discord, while webhook reflections would remain ignored...
|
||||||
|
// TODO that only applies to the above todo: But you'd have to watch out for the /icon command, where the bridge bot would set the room avatar, and that shouldn't be reflected into the room a second time.
|
||||||
if (userRegex.some(x => sender.match(x))) {
|
if (userRegex.some(x => sender.match(x))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
const util = require("util")
|
const util = require("util")
|
||||||
const Ty = require("../types")
|
const Ty = require("../types")
|
||||||
const {sync, as} = require("../passthrough")
|
const {db, sync, as} = require("../passthrough")
|
||||||
|
|
||||||
/** @type {import("./actions/send-event")} */
|
/** @type {import("./actions/send-event")} */
|
||||||
const sendEvent = sync.require("./actions/send-event")
|
const sendEvent = sync.require("./actions/send-event")
|
||||||
|
@ -69,3 +69,14 @@ async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
await addReaction.addReaction(event)
|
await addReaction.addReaction(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
sync.addTemporaryListener(as, "type:m.room.avatar", guard("m.room.avatar",
|
||||||
|
/**
|
||||||
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Avatar>} event
|
||||||
|
*/
|
||||||
|
async event => {
|
||||||
|
if (event.state_key !== "") return
|
||||||
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
|
const url = event.content.url || null
|
||||||
|
db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE room_id = ?").run(url, event.room_id)
|
||||||
|
}))
|
||||||
|
|
|
@ -167,6 +167,8 @@ async function profileSetAvatarUrl(mxid, avatar_url) {
|
||||||
* @param {number} power
|
* @param {number} power
|
||||||
*/
|
*/
|
||||||
async function setUserPower(roomID, mxid, power) {
|
async function setUserPower(roomID, mxid, power) {
|
||||||
|
assert(roomID[0] === "!")
|
||||||
|
assert(mxid[0] === "@")
|
||||||
// Yes it's this hard https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352
|
// Yes it's this hard https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352
|
||||||
const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "")
|
const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "")
|
||||||
const users = powerLevels.users || {}
|
const users = powerLevels.users || {}
|
||||||
|
|
|
@ -42,6 +42,7 @@ function diffKState(actual, target) {
|
||||||
const diff = {}
|
const diff = {}
|
||||||
// go through each key that it should have
|
// go through each key that it should have
|
||||||
for (const key of Object.keys(target)) {
|
for (const key of Object.keys(target)) {
|
||||||
|
if (!key.includes("/")) throw new Error(`target kstate's key "${key}" does not contain a slash separator; if a blank state_key was intended, add a trailing slash to the kstate key.`)
|
||||||
if (key in actual) {
|
if (key in actual) {
|
||||||
// diff
|
// diff
|
||||||
try {
|
try {
|
||||||
|
|
14
types.d.ts
vendored
14
types.d.ts
vendored
|
@ -19,6 +19,7 @@ export type AppServiceRegistrationConfig = {
|
||||||
ooye: {
|
ooye: {
|
||||||
namespace_prefix: string
|
namespace_prefix: string
|
||||||
max_file_size: number
|
max_file_size: number
|
||||||
|
server_name: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ export type WebhookCreds = {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Event {
|
export namespace Event {
|
||||||
export type Outer<T> = {
|
export type Outer<T> = {
|
||||||
type: string
|
type: string
|
||||||
room_id: string
|
room_id: string
|
||||||
|
@ -38,6 +39,10 @@ namespace Event {
|
||||||
event_id: string
|
event_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StateOuter<T> = Outer<T> & {
|
||||||
|
state_key: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ReplacementContent<T> = T & {
|
export type ReplacementContent<T> = T & {
|
||||||
"m.new_content": T
|
"m.new_content": T
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
|
@ -74,6 +79,11 @@ namespace Event {
|
||||||
avatar_url?: string
|
avatar_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type M_Room_Avatar = {
|
||||||
|
discord_path?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type M_Reaction = {
|
export type M_Reaction = {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
rel_type: "m.annotation"
|
rel_type: "m.annotation"
|
||||||
|
@ -83,7 +93,7 @@ namespace Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace R {
|
export namespace R {
|
||||||
export type RoomCreated = {
|
export type RoomCreated = {
|
||||||
room_id: string
|
room_id: string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue