space and room creation
This commit is contained in:
parent
51480e21e5
commit
c7868e9dbb
15 changed files with 328 additions and 36 deletions
|
@ -1,22 +1,99 @@
|
|||
// @ts-check
|
||||
|
||||
const reg = require("../../matrix/read-registration.js")
|
||||
const fetch = require("node-fetch")
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
fetch("https://matrix.cadence.moe/_matrix/client/v3/createRoom?user_id=@_ooye_example:cadence.moe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
invite: ["@cadence:cadence.moe"],
|
||||
is_direct: false,
|
||||
name: "New Bot User Room",
|
||||
preset: "trusted_private_chat"
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${reg.as_token}`
|
||||
const passthrough = require("../../passthrough")
|
||||
const { discord, sync, db } = passthrough
|
||||
/** @type {import("../../matrix/mreq")} */
|
||||
const mreq = sync.require("../../matrix/mreq")
|
||||
/** @type {import("../../matrix/file")} */
|
||||
const file = sync.require("../../matrix/file")
|
||||
|
||||
/**
|
||||
* @param {import("discord-api-types/v10").APIGuildTextChannel} channel
|
||||
*/
|
||||
async function createRoom(channel) {
|
||||
const guildID = channel.guild_id
|
||||
assert.ok(guildID)
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert.ok(guild)
|
||||
const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guildID)
|
||||
assert.ok(typeof spaceID === "string")
|
||||
|
||||
const avatarEventContent = {}
|
||||
if (guild.icon) {
|
||||
avatarEventContent.url = await file.uploadDiscordFileToMxc(file.guildIcon(guild))
|
||||
}
|
||||
}).then(res => res.text()).then(text => {
|
||||
// {"room_id":"!aAVaqeAKwChjWbsywj:cadence.moe"}
|
||||
console.log(text)
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
|
||||
/** @type {import("../../types").R_RoomCreated} */
|
||||
const root = await mreq.mreq("POST", "/client/v3/createRoom", {
|
||||
name: channel.name,
|
||||
topic: channel.topic || undefined,
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
invite: ["@cadence:cadence.moe"], // TODO
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.avatar",
|
||||
state_key: "",
|
||||
content: avatarEventContent
|
||||
},
|
||||
{
|
||||
type: "m.room.guest_access",
|
||||
state_key: "",
|
||||
content: {
|
||||
guest_access: "can_join"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "invited"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.space.parent",
|
||||
state_key: spaceID,
|
||||
content: {
|
||||
via: ["cadence.moe"], // TODO: put the proper server here
|
||||
canonical: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.room.join_rules",
|
||||
content: {
|
||||
join_rule: "restricted",
|
||||
allow: [{
|
||||
type: "m.room.membership",
|
||||
room_id: spaceID
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, root.room_id)
|
||||
|
||||
// Put the newly created child into the space
|
||||
await mreq.mreq("PUT", `/client/v3/rooms/${spaceID}/state/m.space.child/${root.room_id}`, {
|
||||
via: ["cadence.moe"] // TODO: use the proper server
|
||||
})
|
||||
}
|
||||
|
||||
async function createAllForGuild(guildID) {
|
||||
const channelIDs = discord.guildChannelMap.get(guildID)
|
||||
assert.ok(channelIDs)
|
||||
for (const channelID of channelIDs) {
|
||||
const channel = discord.channels.get(channelID)
|
||||
assert.ok(channel)
|
||||
const existing = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id)
|
||||
if (channel.type === DiscordTypes.ChannelType.GuildText && !existing) {
|
||||
await createRoom(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.createRoom = createRoom
|
||||
module.exports.createAllForGuild = createAllForGuild
|
||||
|
|
46
d2m/actions/create-space.js
Normal file
46
d2m/actions/create-space.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
// @ts-check
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const { sync, db } = passthrough
|
||||
/** @type {import("../../matrix/mreq")} */
|
||||
const mreq = sync.require("../../matrix/mreq")
|
||||
|
||||
/**
|
||||
* @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild
|
||||
*/
|
||||
function createSpace(guild) {
|
||||
return mreq.mreq("POST", "/client/v3/createRoom", {
|
||||
name: guild.name,
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
power_level_content_override: {
|
||||
events_default: 100,
|
||||
invite: 50
|
||||
},
|
||||
invite: ["@cadence:cadence.moe"], // TODO
|
||||
topic: guild.description || undefined,
|
||||
creation_content: {
|
||||
type: "m.space"
|
||||
},
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.guest_access",
|
||||
state_key: "",
|
||||
content: {
|
||||
guest_access: "can_join"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
content: {
|
||||
history_visibility: "invited"
|
||||
}
|
||||
}
|
||||
]
|
||||
}).then(/** @param {import("../../types").R_RoomCreated} root */ root => {
|
||||
db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, root.room_id)
|
||||
return root
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.createSpace = createSpace
|
|
@ -10,7 +10,7 @@ const messageToEvent = require("../converters/message-to-event.js")
|
|||
*/
|
||||
function sendMessage(message) {
|
||||
const event = messageToEvent(message)
|
||||
fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, {
|
||||
return fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(event),
|
||||
headers: {
|
||||
|
@ -24,4 +24,4 @@ function sendMessage(message) {
|
|||
})
|
||||
}
|
||||
|
||||
module.exports = sendMessage
|
||||
module.exports.sendMessage = sendMessage
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
// Discord library internals type beat
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const passthrough = require("../passthrough")
|
||||
const { sync } = passthrough
|
||||
|
||||
|
@ -27,6 +28,8 @@ const utils = {
|
|||
const arr = []
|
||||
client.guildChannelMap.set(message.d.id, arr)
|
||||
for (const channel of message.d.channels || []) {
|
||||
// @ts-ignore
|
||||
channel.guild_id = message.d.id
|
||||
arr.push(channel.id)
|
||||
client.channels.set(channel.id, channel)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
// @ts-check
|
||||
|
||||
// Grab Discord events we care about for the bridge, check them, and pass them on
|
||||
const {sync} = require("../passthrough")
|
||||
|
||||
const sendMessage = require("./actions/send-message")
|
||||
/** @type {import("./actions/create-space")}) */
|
||||
const createSpace = sync.require("./actions/create-space")
|
||||
|
||||
/** @type {import("./actions/send-message")}) */
|
||||
const sendMessage = sync.require("./actions/send-message")
|
||||
|
||||
// Grab Discord events we care about for the bridge, check them, and pass them on
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
|
@ -10,10 +16,7 @@ module.exports = {
|
|||
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
|
||||
*/
|
||||
onMessageCreate(client, message) {
|
||||
console.log(message)
|
||||
console.log(message.guild_id)
|
||||
console.log(message.member)
|
||||
sendMessage(message)
|
||||
sendMessage.sendMessage(message)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
4
index.js
4
index.js
|
@ -1,13 +1,15 @@
|
|||
// @ts-check
|
||||
|
||||
const sqlite = require("better-sqlite3")
|
||||
const HeatSync = require("heatsync")
|
||||
|
||||
const config = require("./config")
|
||||
const passthrough = require("./passthrough")
|
||||
const db = new sqlite("db/ooye.db")
|
||||
|
||||
const sync = new HeatSync()
|
||||
|
||||
Object.assign(passthrough, { config, sync })
|
||||
Object.assign(passthrough, { config, sync, db })
|
||||
|
||||
const DiscordClient = require("./d2m/discord-client")
|
||||
|
||||
|
|
63
matrix/file.js
Normal file
63
matrix/file.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
// @ts-check
|
||||
|
||||
const fetch = require("node-fetch")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const { sync, db } = passthrough
|
||||
/** @type {import("./mreq")} */
|
||||
const mreq = sync.require("./mreq")
|
||||
|
||||
const DISCORD_IMAGES_BASE = "https://cdn.discordapp.com"
|
||||
const IMAGE_SIZE = 1024
|
||||
|
||||
/** @type {Map<string, Promise<string>>} */
|
||||
const inflight = new Map()
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
*/
|
||||
async function uploadDiscordFileToMxc(path) {
|
||||
const url = DISCORD_IMAGES_BASE + path
|
||||
|
||||
// Are we uploading this file RIGHT NOW? Return the same inflight promise with the same resolution
|
||||
let existing = inflight.get(url)
|
||||
if (typeof existing === "string") {
|
||||
return existing
|
||||
}
|
||||
|
||||
// Has this file already been uploaded in the past? Grab the existing copy from the database.
|
||||
existing = db.prepare("SELECT mxc_url FROM file WHERE discord_url = ?").pluck().get(url)
|
||||
if (typeof existing === "string") {
|
||||
return existing
|
||||
}
|
||||
|
||||
// Download from Discord
|
||||
const promise = fetch(url, {}).then(/** @param {import("node-fetch").Response} res */ async res => {
|
||||
/** @ts-ignore @type {import("stream").Readable} body */
|
||||
const body = res.body
|
||||
|
||||
// Upload to Matrix
|
||||
/** @type {import("../types").R_FileUploaded} */
|
||||
const root = await mreq.mreq("POST", "/media/v3/upload", body, {
|
||||
headers: {
|
||||
"Content-Type": res.headers.get("content-type")
|
||||
}
|
||||
})
|
||||
|
||||
// Store relationship in database
|
||||
db.prepare("INSERT INTO file (discord_url, mxc_url) VALUES (?, ?)").run(url, root.content_uri)
|
||||
inflight.delete(url)
|
||||
|
||||
return root.content_uri
|
||||
})
|
||||
inflight.set(url, promise)
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
function guildIcon(guild) {
|
||||
return `/icons/${guild.id}/${guild.icon}?size=${IMAGE_SIZE}`
|
||||
}
|
||||
|
||||
module.exports.guildIcon = guildIcon
|
||||
module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc
|
47
matrix/mreq.js
Normal file
47
matrix/mreq.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// @ts-check
|
||||
|
||||
const fetch = require("node-fetch")
|
||||
const mixin = require("mixin-deep")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const { sync } = passthrough
|
||||
/** @type {import("./read-registration")} */
|
||||
const reg = sync.require("./read-registration.js")
|
||||
|
||||
const baseUrl = "https://matrix.cadence.moe/_matrix"
|
||||
|
||||
class MatrixServerError {
|
||||
constructor(data) {
|
||||
this.data = data
|
||||
/** @type {string} */
|
||||
this.errcode = data.errcode
|
||||
/** @type {string} */
|
||||
this.error = data.error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {any} [body]
|
||||
* @param {any} [extra]
|
||||
*/
|
||||
function mreq(method, url, body, extra = {}) {
|
||||
const opts = mixin({
|
||||
method,
|
||||
body: (body == undefined || Object.is(body.constructor, Object)) ? JSON.stringify(body) : body,
|
||||
headers: {
|
||||
Authorization: `Bearer ${reg.as_token}`
|
||||
}
|
||||
}, extra)
|
||||
console.log(baseUrl + url, opts)
|
||||
return fetch(baseUrl + url, opts).then(res => {
|
||||
return res.json().then(root => {
|
||||
if (!res.ok || root.errcode) throw new MatrixServerError(root)
|
||||
return root
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.MatrixServerError = MatrixServerError
|
||||
module.exports.mreq = mreq
|
|
@ -3,11 +3,5 @@
|
|||
const fs = require("fs")
|
||||
const yaml = require("js-yaml")
|
||||
|
||||
/**
|
||||
* @typedef AppServiceRegistrationConfig
|
||||
* @property {string} id
|
||||
* @property {string} as_token
|
||||
* @property {string} hs_token
|
||||
*/
|
||||
|
||||
/** @type {import("../types").AppServiceRegistrationConfig} */
|
||||
module.exports = yaml.load(fs.readFileSync("registration.yaml", "utf8"))
|
||||
|
|
25
notes.md
25
notes.md
|
@ -36,6 +36,13 @@ Public channels in that server should then use the following settings, so that t
|
|||
- Find & join access: Space members (so users must have been invited to the space already, even if they find out the room ID to join)
|
||||
- Who can read history: Anyone (so that people can see messages during the preview before joining)
|
||||
|
||||
Step by step process:
|
||||
|
||||
1. Create a space room for the guild. Store the guild-space ID relationship in the database. Configure the space room to act like a space.
|
||||
- `{"name":"NAME","preset":"private_chat","visibility":"private","power_level_content_override":{"events_default":100,"invite":50},"topic":"TOPIC","creation_content":{"type":"m.space"},"initial_state":[{"type":"m.room.guest_access","state_key":"","content":{"guest_access":"can_join"}},{"type":"m.room.history_visibility","content":{"history_visibility":"invited"}}]}`
|
||||
2. Create channel rooms for the channels. Store the channel-room ID relationship in the database. (Probably no need to store parent-child relationships in the database?)
|
||||
3. Send state events to put the channel rooms in the space.
|
||||
|
||||
### Private channels
|
||||
|
||||
Discord **channels** that disallow view permission to @everyone should instead have the following **room** settings in Matrix:
|
||||
|
@ -58,6 +65,13 @@ The context-sensitive /invite command will invite Matrix users to the correspond
|
|||
1. Transform content.
|
||||
2. Send to matrix.
|
||||
|
||||
## Webhook message sent
|
||||
|
||||
- Consider using the _ooye_bot account to send all webhook messages to prevent extraneous joins?
|
||||
- Downside: the profile information from the most recently sent message would stick around in the member list. This is toleable.
|
||||
- Otherwise, could use an account per webhook ID, but if webhook IDs are often deleted and re-created, this could still end up leaving too many accounts in the room.
|
||||
- The original bridge uses an account per webhook display name, which does the most sense in terms of canonical accounts, but leaves too many accounts in the room.
|
||||
|
||||
## Message deleted
|
||||
|
||||
1. Look up equivalents on matrix.
|
||||
|
@ -91,4 +105,13 @@ The context-sensitive /invite command will invite Matrix users to the correspond
|
|||
1. Create the corresponding room.
|
||||
2. Add to database.
|
||||
3. Update room details to match.
|
||||
4. Add to space.
|
||||
4. Make sure the permissions are correct according to the rules above!
|
||||
5. Add to space.
|
||||
|
||||
## Emojis updated
|
||||
|
||||
1. Upload any newly added images to msc.
|
||||
2. Create or replace state event for the bridged pack. (Can just use key "ooye" and display name "Discord", or something, for this pack.)
|
||||
3. The emojis may now be sent by Matrix users!
|
||||
|
||||
TOSPEC: m2d emoji uploads??
|
||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -16,6 +16,7 @@
|
|||
"js-yaml": "^4.1.0",
|
||||
"matrix-appservice": "^2.0.0",
|
||||
"matrix-js-sdk": "^24.1.0",
|
||||
"mixin-deep": "^2.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"snowtransfer": "^0.7.0",
|
||||
"supertape": "^8.3.0"
|
||||
|
@ -1696,6 +1697,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mixin-deep": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz",
|
||||
"integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"js-yaml": "^4.1.0",
|
||||
"matrix-appservice": "^2.0.0",
|
||||
"matrix-js-sdk": "^24.1.0",
|
||||
"mixin-deep": "^2.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"snowtransfer": "^0.7.0",
|
||||
"supertape": "^8.3.0"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* @property {typeof import("./config")} config
|
||||
* @property {import("./d2m/discord-client")} discord
|
||||
* @property {import("heatsync")} sync
|
||||
* @property {import("better-sqlite3/lib/database")} db
|
||||
*/
|
||||
/** @type {Passthrough} */
|
||||
// @ts-ignore
|
||||
|
|
7
stdin.js
7
stdin.js
|
@ -4,7 +4,12 @@ const repl = require("repl")
|
|||
const util = require("util")
|
||||
|
||||
const passthrough = require("./passthrough")
|
||||
const { discord, config, sync } = passthrough
|
||||
const { discord, config, sync, db } = passthrough
|
||||
|
||||
const createSpace = sync.require("./d2m/actions/create-space.js")
|
||||
const createRoom = sync.require("./d2m/actions/create-room.js")
|
||||
const mreq = sync.require("./matrix/mreq.js")
|
||||
const guildID = "112760669178241024"
|
||||
|
||||
const extraContext = {}
|
||||
|
||||
|
|
18
types.d.ts
vendored
18
types.d.ts
vendored
|
@ -1,6 +1,24 @@
|
|||
export type AppServiceRegistrationConfig = {
|
||||
id: string
|
||||
as_token: string
|
||||
hs_token: string
|
||||
url: string
|
||||
sender_localpart: string
|
||||
protocols: [string]
|
||||
rate_limited: boolean
|
||||
}
|
||||
|
||||
export type M_Room_Message_content = {
|
||||
msgtype: "m.text"
|
||||
body: string
|
||||
formatted_body?: "org.matrix.custom.html"
|
||||
format?: string
|
||||
}
|
||||
|
||||
export type R_RoomCreated = {
|
||||
room_id: string
|
||||
}
|
||||
|
||||
export type R_FileUploaded = {
|
||||
content_uri: string
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue