Compare commits
11 commits
696a45f344
...
55e0e5dfa1
| Author | SHA1 | Date | |
|---|---|---|---|
| 55e0e5dfa1 | |||
| 092a4cf7b0 | |||
| 17251c61d5 | |||
| 5a401a187d | |||
| 694379f659 | |||
| 04d26026f5 | |||
| 231b26113e | |||
| e4d0838af5 | |||
| a6bb248c0a | |||
| 4bc7e794ab | |||
| 239568a8e5 |
38 changed files with 1246 additions and 652 deletions
|
|
@ -106,3 +106,9 @@ bridge.cadence.moe {
|
|||
reverse_proxy 127.0.0.1:6693
|
||||
}
|
||||
```
|
||||
|
||||
## Example reverse proxy for traefik
|
||||
|
||||
Note: Out Of Your Element has no official Docker support. This guide is for using traefik when OOYE is ***not*** in a container.
|
||||
|
||||
See [third-party/reverse-proxy-traefik.md](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/third-party/reverse-proxy-traefik.md)
|
||||
|
|
|
|||
113
docs/third-party/reverse-proxy-traefik.md
vendored
Normal file
113
docs/third-party/reverse-proxy-traefik.md
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
> This guide was written by @bgtlover:stealthy.club, a community contributor. The author of Out Of Your Element hopes it will be useful, but cannot say whether the information is accurate or complete.
|
||||
|
||||
## Example reverse proxy configuration with traefik
|
||||
|
||||
Note: This guide describes setting up the reverse proxy configuration when OOYE is ***not*** in a Docker container.
|
||||
|
||||
Because traefik is generally used in Docker, this guide assumes the user already has it configured properly. However, given that Docker is very complex and the smallest mistakes can cascade in catastrophic, not immediately observable, and unpredictable ways, a fairly complete setup will be reproduced. Therefore, system administrators are advised to diff this sample setup against theirs rather than copy it wholesale.
|
||||
|
||||
### Note on variable substitution
|
||||
|
||||
Variables will be denoted as `{{var}}`. This syntax has been chosen because that's also how YAML substitution works. The values that fit each variable will be explained after the code block containing the placeholder.
|
||||
|
||||
### Base compose configuration for traefik
|
||||
|
||||
This file defines the traefik service stack. It's responsible for mounting volumes correctly, declaring ports that should be opened on the host side, and the external traefik network (created manually).
|
||||
|
||||
In compose.yml, put the following:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:latest"
|
||||
restart: always
|
||||
command:
|
||||
- "--configFile=/etc/traefik/static_config.yml"
|
||||
ports:
|
||||
- "80:80" #http
|
||||
- "443:443" #https
|
||||
networks:
|
||||
- traefik
|
||||
volumes:
|
||||
- ./letsencrypt:/letsencrypt
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./static_config.yml:/etc/traefik/static_config.yml
|
||||
- ./config:/etc/traefik/config
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
### Static traefik configuration
|
||||
|
||||
The static traefik configuration is used to define base traefik behavior, for example entry points, access and runtime logs, a file or directory for per-service configuration, etc.
|
||||
|
||||
In static_config.yml, put the following:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
docker:
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
exposedByDefault: false
|
||||
network: "traefik"
|
||||
file:
|
||||
directory: /etc/traefik/config/
|
||||
watch: true
|
||||
|
||||
entryPoints:
|
||||
web-secure:
|
||||
address: ":443"
|
||||
asDefault: true
|
||||
http3: {}
|
||||
http:
|
||||
tls:
|
||||
certResolver: default
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: web-secure
|
||||
|
||||
certificatesResolvers:
|
||||
default:
|
||||
acme:
|
||||
email: {{email}}
|
||||
storage: "/letsencrypt/acme.json"
|
||||
tlsChallenge: {}
|
||||
|
||||
```
|
||||
|
||||
Replace `{{email}}` with a valid email address.
|
||||
|
||||
### Out of your element traefik dynamic configuration
|
||||
|
||||
Traefik's dynamic configuration files configure proxy behaviors on a per-application level.
|
||||
|
||||
In config/out-of-your-element.yml, put the following:
|
||||
|
||||
```yaml
|
||||
http:
|
||||
routers:
|
||||
out-of-your-element:
|
||||
rule: Host(`bridge.stealthy.club`)
|
||||
service: out-of-your-element-service
|
||||
services:
|
||||
out-of-your-element-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://{{ip}}:{{port}}"
|
||||
|
||||
```
|
||||
|
||||
The `{{port}}` is 6693 unless you changed it during Out Of Your Element's first time setup.
|
||||
|
||||
Replace `{{ip}}` with the ***external*** IP of your server.
|
||||
|
||||
Make sure the port is allowed through your firewall if applicable.
|
||||
|
||||
For context, the external IP is required because of Docker networking. Because Docker modifies the host-side iptables firewall and creates virtual interfaces for its networks, and because the networking inside containers is configured such that localhost points to the IP of the container instead of the actual host, placing localhost in the url field above would make the traefik container establish an HTTP connection to itself, which would cause a bad gateway error.
|
||||
94
package-lock.json
generated
94
package-lock.json
generated
|
|
@ -24,7 +24,7 @@
|
|||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.14.0",
|
||||
"discord-api-types": "^0.38.31",
|
||||
"discord-api-types": "^0.38.36",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
"entities": "^5.0.0",
|
||||
|
|
@ -53,6 +53,20 @@
|
|||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"../extended-errors/enhance-errors": {
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"ts-expose-internals": "^5.6.3",
|
||||
"ts-patch": "^3.3.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.1",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../tap-dot": {
|
||||
"name": "@cloudrac3r/tap-dot",
|
||||
"version": "2.0.0",
|
||||
|
|
@ -67,27 +81,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
|
||||
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
|
||||
"integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.25.6"
|
||||
"@babel/types": "^7.28.5"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
|
|
@ -97,13 +114,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
|
||||
"integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.8",
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -355,9 +372,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
|
||||
"integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
|
@ -983,16 +1000,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
|
||||
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
|
|
@ -1702,9 +1721,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.33",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz",
|
||||
"integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==",
|
||||
"version": "0.38.36",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.36.tgz",
|
||||
"integrity": "sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
|
|
@ -2958,10 +2977,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
|
|
@ -3176,14 +3196,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/token-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.14.0",
|
||||
"discord-api-types": "^0.38.31",
|
||||
"discord-api-types": "^0.38.36",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
"entities": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ function convertNameAndTopic(channel, guild, customName) {
|
|||
* Async because it may create the guild and/or upload the guild icon to mxc.
|
||||
* @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {{api: {getStateEvent: typeof api.getStateEvent}}} di simple-as-nails dependency injection for the matrix API
|
||||
* @param {{api: {getStateEvent: typeof api.getStateEvent, getStateEventOuter: typeof api.getStateEventOuter}}} di simple-as-nails dependency injection for the matrix API
|
||||
*/
|
||||
async function channelToKState(channel, guild, di) {
|
||||
// @ts-ignore
|
||||
|
|
@ -126,15 +126,22 @@ async function channelToKState(channel, guild, di) {
|
|||
const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
|
||||
const everyoneCanMentionEveryone = dUtils.hasAllPermissions(everyonePermissions, ["MentionEveryone"])
|
||||
|
||||
const spacePowerDetails = await mUtils.getEffectivePower(guildSpaceID, [], di.api)
|
||||
const spaceCreatorsAndFounders = spacePowerDetails.allCreators
|
||||
.concat(Object.entries(spacePowerDetails.powerLevels.users ?? {}).filter(([, power]) => power >= spacePowerDetails.tombstone).map(([mxid]) => mxid))
|
||||
|
||||
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
|
||||
const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})
|
||||
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const spacePowerEvent = await di.api.getStateEvent(guildSpaceID, "m.room.power_levels", "")
|
||||
const spacePower = spacePowerEvent.users
|
||||
const additionalCreators = select("member_power", "mxid", {room_id: "*"}, "AND power_level > 100").pluck().all().concat(spaceCreatorsAndFounders)
|
||||
const creationContent = {}
|
||||
creationContent.additional_creators = additionalCreators
|
||||
|
||||
if (channel.type === DiscordTypes.ChannelType.GuildForum) creationContent.type = "m.space"
|
||||
|
||||
/** @type {any} */
|
||||
const channelKState = {
|
||||
"m.room.create/": creationContent,
|
||||
"m.room.name/": {name: convertedName},
|
||||
"m.room.topic/": {topic: convertedTopic},
|
||||
"m.room.avatar/": avatarEventContent,
|
||||
|
|
@ -156,12 +163,10 @@ async function channelToKState(channel, guild, di) {
|
|||
notifications: {
|
||||
room: everyoneCanMentionEveryone ? 0 : 20
|
||||
},
|
||||
users: {...spacePower, ...globalAdminPower}
|
||||
},
|
||||
"chat.schildi.hide_ui/read_receipts": {
|
||||
users: {...spacePowerDetails.powerLevels.users, ...globalAdminPower}
|
||||
},
|
||||
[`uk.half-shot.bridge/moe.cadence.ooye://discord/${guild.id}/${channel.id}`]: {
|
||||
bridgebot: `@${reg.sender_localpart}:${reg.ooye.server_name}`,
|
||||
bridgebot: mUtils.bot,
|
||||
protocol: {
|
||||
id: "discord",
|
||||
displayname: "Discord"
|
||||
|
|
@ -195,7 +200,7 @@ async function channelToKState(channel, guild, di) {
|
|||
/**
|
||||
* Create a bridge room, store the relationship in the database, and add it to the guild's space.
|
||||
* @param {DiscordTypes.APIGuildTextChannel} channel
|
||||
* @param guild
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {string} spaceID
|
||||
* @param {any} kstate
|
||||
* @param {number} privacyLevel
|
||||
|
|
@ -205,9 +210,6 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
|||
let threadParent = null
|
||||
if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id
|
||||
|
||||
let spaceCreationContent = {}
|
||||
if (channel.type === DiscordTypes.ChannelType.GuildForum) spaceCreationContent = {creation_content: {type: "m.space"}}
|
||||
|
||||
// Name and topic can be done earlier in room creation rather than in initial_state
|
||||
// https://spec.matrix.org/latest/client-server-api/#creation
|
||||
const name = kstate["m.room.name/"].name
|
||||
|
|
@ -217,7 +219,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
|||
delete kstate["m.room.topic/"]
|
||||
assert(topic)
|
||||
|
||||
const roomID = await postApplyPowerLevels(kstate, async kstate => {
|
||||
const roomCreate = await postApplyPowerLevels(kstate, async kstate => {
|
||||
const roomID = await api.createRoom({
|
||||
name,
|
||||
topic,
|
||||
|
|
@ -225,16 +227,20 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
|||
visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel],
|
||||
invite: [],
|
||||
initial_state: await ks.kstateToState(kstate),
|
||||
...spaceCreationContent
|
||||
creation_content: ks.kstateToCreationContent(kstate)
|
||||
})
|
||||
|
||||
/** @type {Ty.Event.StateOuter<Ty.Event.M_Room_Create>} */
|
||||
const roomCreate = await api.getStateEventOuter(roomID, "m.room.create", "")
|
||||
|
||||
db.transaction(() => {
|
||||
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent)
|
||||
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, guild_id) VALUES (?, ?, ?, NULL, ?, ?)").run(channel.id, roomID, channel.name, threadParent, guild.id)
|
||||
db.prepare("INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) VALUES (?, ?, 0)").run(channel.id, roomID)
|
||||
})()
|
||||
|
||||
return roomID
|
||||
return roomCreate
|
||||
})
|
||||
const roomID = roomCreate.room_id
|
||||
|
||||
// Put the newly created child into the space
|
||||
await _syncSpaceMember(channel, spaceID, roomID, guild.id)
|
||||
|
|
@ -249,26 +255,30 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
|||
* https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
|
||||
* https://github.com/matrix-org/matrix-spec/issues/492
|
||||
* @param {any} kstate
|
||||
* @param {(_: any) => Promise<string>} callback must return room ID
|
||||
* @returns {Promise<string>} room ID
|
||||
* @param {(_: any) => Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} callback must return room ID and room version
|
||||
* @returns {Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} room ID
|
||||
*/
|
||||
async function postApplyPowerLevels(kstate, callback) {
|
||||
const powerLevelContent = kstate["m.room.power_levels/"]
|
||||
const kstateWithoutPowerLevels = {...kstate}
|
||||
delete kstateWithoutPowerLevels["m.room.power_levels/"]
|
||||
delete kstateWithoutPowerLevels["chat.schildi.hide_ui/read_receipts"]
|
||||
|
||||
/** @type {string} */
|
||||
const roomID = await callback(kstateWithoutPowerLevels)
|
||||
const roomCreate = await callback(kstateWithoutPowerLevels)
|
||||
const roomID = roomCreate.room_id
|
||||
|
||||
// Now *really* apply the power level overrides on top of what Synapse *really* set
|
||||
if (powerLevelContent) {
|
||||
const newRoomKState = await ks.roomToKState(roomID)
|
||||
const newRoomPowerLevelsDiff = ks.diffKState(newRoomKState, {"m.room.power_levels/": powerLevelContent})
|
||||
await ks.applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff)
|
||||
mUtils.removeCreatorsFromPowerLevels(roomCreate, powerLevelContent)
|
||||
|
||||
const originalPowerLevels = await api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
const powerLevelsDiff = ks.diffKState(
|
||||
{"m.room.power_levels/": originalPowerLevels},
|
||||
{"m.room.power_levels/": powerLevelContent}
|
||||
)
|
||||
await ks.applyKStateDiffToRoom(roomID, powerLevelsDiff)
|
||||
}
|
||||
|
||||
return roomID
|
||||
return roomCreate
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -392,11 +402,12 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
|||
console.log(`[room sync] to matrix: ${channel.name}`)
|
||||
|
||||
const {spaceID, channelKState} = await channelToKState(channel, guild, {api}) // calling this in both branches because we don't want to calculate this if not syncing
|
||||
await ks.kstateUploadMxc(channelKState) // pre-upload icons before diffing
|
||||
|
||||
// sync channel state to room
|
||||
const roomKState = await ks.roomToKState(roomID)
|
||||
if (+roomKState["m.room.create/"].room_version <= 8) {
|
||||
// join_rule `restricted` is not available in room version < 8 and not working properly in version == 8
|
||||
if (!mUtils.roomHasAtLeastVersion(roomKState["m.room.create/"].room_version, 9)) {
|
||||
// join_rule `restricted` is not available in room version < 8 and not working properly in version == 8, so require version 9
|
||||
// read more: https://spec.matrix.org/v1.8/rooms/v9/
|
||||
// we have to use `public` instead, otherwise the room will be unjoinable.
|
||||
channelKState["m.room.join_rules/"] = {join_rule: "public"}
|
||||
|
|
|
|||
|
|
@ -9,19 +9,44 @@ const testData = require("../../../test/data")
|
|||
const passthrough = require("../../passthrough")
|
||||
const {db} = passthrough
|
||||
|
||||
|
||||
test("channel2room: discoverable privacy room", async t => {
|
||||
function mockAPI(t) {
|
||||
let called = 0
|
||||
async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
return {
|
||||
getCalled() {
|
||||
return called
|
||||
},
|
||||
async getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@example:matrix.org": 50}}
|
||||
return {users: {"@example:matrix.org": 50}, events: {"m.room.tombstone": 100}}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
content: {
|
||||
room_version: "11"
|
||||
},
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
room_id: "!jjmvBegULiLucuWEHU:cadence.moe",
|
||||
sender: "@_ooye_bot:cadence.moe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("channel2room: discoverable privacy room", async t => {
|
||||
const api = mockAPI(t)
|
||||
db.prepare("UPDATE guild_space SET privacy_level = 2").run()
|
||||
t.deepEqual(
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)),
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api}).then(x => x.channelKState)),
|
||||
Object.assign({}, testData.room.general, {
|
||||
"m.room.guest_access/": {guest_access: "forbidden"},
|
||||
"m.room.join_rules/": {join_rule: "public"},
|
||||
|
|
@ -29,58 +54,37 @@ test("channel2room: discoverable privacy room", async t => {
|
|||
"m.room.power_levels/": mixin({users: {"@example:matrix.org": 50}}, testData.room.general["m.room.power_levels/"])
|
||||
})
|
||||
)
|
||||
t.equal(called, 1)
|
||||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: linkable privacy room", async t => {
|
||||
let called = 0
|
||||
async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@example:matrix.org": 50}}
|
||||
}
|
||||
const api = mockAPI(t)
|
||||
db.prepare("UPDATE guild_space SET privacy_level = 1").run()
|
||||
t.deepEqual(
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)),
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api}).then(x => x.channelKState)),
|
||||
Object.assign({}, testData.room.general, {
|
||||
"m.room.guest_access/": {guest_access: "forbidden"},
|
||||
"m.room.join_rules/": {join_rule: "public"},
|
||||
"m.room.power_levels/": mixin({users: {"@example:matrix.org": 50}}, testData.room.general["m.room.power_levels/"])
|
||||
})
|
||||
)
|
||||
t.equal(called, 1)
|
||||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: invite-only privacy room", async t => {
|
||||
let called = 0
|
||||
async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@example:matrix.org": 50}}
|
||||
}
|
||||
const api = mockAPI(t)
|
||||
db.prepare("UPDATE guild_space SET privacy_level = 0").run()
|
||||
t.deepEqual(
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)),
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api}).then(x => x.channelKState)),
|
||||
Object.assign({}, testData.room.general, {
|
||||
"m.room.power_levels/": mixin({users: {"@example:matrix.org": 50}}, testData.room.general["m.room.power_levels/"])
|
||||
})
|
||||
)
|
||||
t.equal(called, 1)
|
||||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: room where limited people can mention everyone", async t => {
|
||||
let called = 0
|
||||
async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@example:matrix.org": 50}}
|
||||
}
|
||||
const api = mockAPI(t)
|
||||
const limitedGuild = mixin({}, testData.guild.general)
|
||||
limitedGuild.roles[0].permissions = (BigInt(limitedGuild.roles[0].permissions) - 131072n).toString()
|
||||
const limitedRoom = mixin({}, testData.room.general, {"m.room.power_levels/": {
|
||||
|
|
@ -88,43 +92,31 @@ test("channel2room: room where limited people can mention everyone", async t =>
|
|||
users: {"@example:matrix.org": 50}
|
||||
}})
|
||||
t.deepEqual(
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, limitedGuild, {api: {getStateEvent}}).then(x => x.channelKState)),
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, limitedGuild, {api}).then(x => x.channelKState)),
|
||||
limitedRoom
|
||||
)
|
||||
t.equal(called, 1)
|
||||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: matrix room that already has a custom topic set", async t => {
|
||||
let called = 0
|
||||
async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {}
|
||||
}
|
||||
const api = mockAPI(t)
|
||||
db.prepare("UPDATE channel_room SET custom_topic = 1 WHERE channel_id = ?").run(testData.channel.general.id)
|
||||
const expected = mixin({}, testData.room.general, {"m.room.power_levels/": {notifications: {room: 20}}})
|
||||
const expected = mixin({}, testData.room.general, {"m.room.power_levels/": {notifications: {room: 20}, users: {"@example:matrix.org": 50}}})
|
||||
// @ts-ignore
|
||||
delete expected["m.room.topic/"]
|
||||
t.deepEqual(
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)),
|
||||
kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api}).then(x => x.channelKState)),
|
||||
expected
|
||||
)
|
||||
t.equal(called, 1)
|
||||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: read-only discord channel", async t => {
|
||||
let called = 0
|
||||
async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {}
|
||||
}
|
||||
const api = mockAPI(t)
|
||||
const expected = {
|
||||
"chat.schildi.hide_ui/read_receipts": {},
|
||||
"m.room.create/": {
|
||||
additional_creators: ["@test_auto_invite:example.org"],
|
||||
},
|
||||
"m.room.avatar/": {
|
||||
url: {
|
||||
$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024",
|
||||
|
|
@ -161,7 +153,8 @@ test("channel2room: read-only discord channel", async t => {
|
|||
room: 20,
|
||||
},
|
||||
users: {
|
||||
"@test_auto_invite:example.org": 100,
|
||||
"@test_auto_invite:example.org": 150,
|
||||
"@example:matrix.org": 50
|
||||
},
|
||||
},
|
||||
"m.space.parent/!jjmvBegULiLucuWEHU:cadence.moe": {
|
||||
|
|
@ -191,10 +184,10 @@ test("channel2room: read-only discord channel", async t => {
|
|||
}
|
||||
}
|
||||
t.deepEqual(
|
||||
kstateStripConditionals(await channelToKState(testData.channel.updates, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)),
|
||||
kstateStripConditionals(await channelToKState(testData.channel.updates, testData.guild.general, {api}).then(x => x.channelKState)),
|
||||
expected
|
||||
)
|
||||
t.equal(called, 1)
|
||||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("convertNameAndTopic: custom name and topic", t => {
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ async function createSpace(guild, kstate) {
|
|||
const enablePresenceByDefault = +(memberCount < 50) // scary! all active users in a presence-enabled guild will be pinging the server every <30 seconds to stay online
|
||||
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
|
||||
|
||||
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
||||
return api.createRoom({
|
||||
const roomCreate = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
||||
const roomID = await api.createRoom({
|
||||
name,
|
||||
preset: createRoom.PRIVACY_ENUMS.PRESET[createRoom.DEFAULT_PRIVACY_LEVEL], // New spaces will have to use the default privacy level; we obviously can't look up the existing entry
|
||||
visibility: createRoom.PRIVACY_ENUMS.VISIBILITY[createRoom.DEFAULT_PRIVACY_LEVEL],
|
||||
|
|
@ -46,12 +46,14 @@ async function createSpace(guild, kstate) {
|
|||
},
|
||||
invite: globalAdmins,
|
||||
topic,
|
||||
creation_content: {
|
||||
type: "m.space"
|
||||
},
|
||||
initial_state: await ks.kstateToState(kstate)
|
||||
initial_state: await ks.kstateToState(kstate),
|
||||
creation_content: ks.kstateToCreationContent(kstate)
|
||||
})
|
||||
const roomCreate = await api.getStateEventOuter(roomID, "m.room.create", "")
|
||||
return roomCreate
|
||||
})
|
||||
const roomID = roomCreate.room_id
|
||||
|
||||
db.prepare("INSERT INTO guild_space (guild_id, space_id, presence) VALUES (?, ?, ?)").run(guild.id, roomID, enablePresenceByDefault)
|
||||
return roomID
|
||||
}
|
||||
|
|
@ -63,7 +65,13 @@ async function createSpace(guild, kstate) {
|
|||
async function guildToKState(guild, privacyLevel) {
|
||||
assert.equal(typeof privacyLevel, "number")
|
||||
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
|
||||
const additionalCreators = select("member_power", "mxid", {room_id: "*"}, "AND power_level > 100").pluck().all()
|
||||
|
||||
const guildKState = {
|
||||
"m.room.create/": {
|
||||
type: "m.space",
|
||||
additional_creators: additionalCreators
|
||||
},
|
||||
"m.room.name/": {name: guild.name},
|
||||
"m.room.avatar/": {
|
||||
$if: guild.icon,
|
||||
|
|
@ -116,6 +124,8 @@ async function _syncSpace(guild, shouldActuallySync) {
|
|||
console.log(`[space sync] to matrix: ${guild.name}`)
|
||||
|
||||
const guildKState = await guildToKState(guild, privacy_level) // calling this in both branches because we don't want to calculate this if not syncing
|
||||
ks.kstateStripConditionals(guildKState) // pre-upload icons before diffing
|
||||
await ks.kstateUploadMxc(guildKState)
|
||||
|
||||
// sync guild state to space
|
||||
const spaceKState = await ks.roomToKState(spaceID)
|
||||
|
|
@ -177,6 +187,8 @@ async function syncSpaceFully(guildID) {
|
|||
console.log(`[space sync] to matrix: ${guild.name}`)
|
||||
|
||||
const guildKState = await guildToKState(guild, privacy_level)
|
||||
ks.kstateStripConditionals(guildKState) // pre-upload icons before diffing
|
||||
await ks.kstateUploadMxc(guildKState)
|
||||
|
||||
// sync guild state to space
|
||||
const spaceKState = await ks.roomToKState(spaceID)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as
|
|||
t.deepEqual(
|
||||
await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))),
|
||||
{
|
||||
"m.room.create/": {
|
||||
additional_creators: ["@test_auto_invite:example.org"],
|
||||
type: "m.space"
|
||||
},
|
||||
"m.room.avatar/": {
|
||||
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
|
||||
},
|
||||
|
|
@ -30,7 +34,7 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as
|
|||
},
|
||||
"m.room.power_levels/": {
|
||||
users: {
|
||||
"@test_auto_invite:example.org": 100
|
||||
"@test_auto_invite:example.org": 150
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const {test} = require("supertape")
|
||||
const {messageToEvent} = require("./message-to-event")
|
||||
const data = require("../../../test/data")
|
||||
const {mockGetEffectivePower} = require("../../m2d/converters/utils.test")
|
||||
const {db} = require("../../passthrough")
|
||||
|
||||
test("message2event embeds: nothing but a field", async t => {
|
||||
|
|
@ -86,17 +87,7 @@ test("message2event embeds: blockquote in embed", async t => {
|
|||
let called = 0
|
||||
const events = await messageToEvent(data.message_with_embeds.blockquote_in_embed, data.guild.general, {}, {
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe")
|
||||
|
|
@ -124,7 +115,7 @@ test("message2event embeds: blockquote in embed", async t => {
|
|||
formatted_body: "<blockquote><p><strong><a href=\"https://matrix.to/#/!qzDBLKlildpzrrOnFZ:cadence.moe/$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo?via=cadence.moe&via=example.invalid\">⏺️ minimus</a></strong></p><p>reply draft<br><blockquote>The following is a message composed via consensus of the Stinker Council.<br><br>For those who are not currently aware of our existence, we represent the organization known as Wonderland. Our previous mission centered around the assortment and study of puzzling objects, entities and other assorted phenomena. This mission was the focus of our organization for more than 28 years.<br><br>Due to circumstances outside of our control, this directive has now changed. Our new mission will be the extermination of the stinker race.<br><br>There will be no further communication.</blockquote></p><p><a href=\"https://matrix.to/#/!qzDBLKlildpzrrOnFZ:cadence.moe/$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo?via=cadence.moe&via=example.invalid\">Go to Message</a></p></blockquote>",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
|
||||
t.equal(called, 1, "should call getJoinedMembers once")
|
||||
})
|
||||
|
||||
test("message2event embeds: crazy html is all escaped", async t => {
|
||||
|
|
@ -343,16 +334,7 @@ test("message2event embeds: tenor gif should show a video link without a provide
|
|||
test("message2event embeds: if discord creates an embed preview for a discord channel link, don't copy that embed", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.discord_server_included_punctuation_bad_discord, data.guild.general, {}, {
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
t.equal(roomID, "!TqlyQmifxGUggEmdBN:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
t.equal(roomID, "!TqlyQmifxGUggEmdBN:cadence.moe")
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
const mentions = {}
|
||||
/** @type {{event_id: string, room_id: string, source: number, channel_id: string}?} */
|
||||
let repliedToEventRow = null
|
||||
let repliedToEventInDifferentRoom = false
|
||||
let repliedToUnknownEvent = false
|
||||
let repliedToEventSenderMxid = null
|
||||
|
||||
|
|
@ -491,6 +492,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
if (repliedToEventRow) {
|
||||
// Generate a reply pointing to the Matrix event we found
|
||||
const latestRoomID = select("channel_room", "room_id", {channel_id: repliedToEventRow.channel_id}).pluck().get() // native replies don't work across room upgrades, so make sure the old and new message are in the same room
|
||||
if (latestRoomID !== repliedToEventRow.room_id) repliedToEventInDifferentRoom = true
|
||||
html =
|
||||
(latestRoomID === repliedToEventRow.room_id ? "<mx-reply>" : "")
|
||||
+ `<blockquote><a href="https://matrix.to/#/${repliedToEventRow.room_id}/${repliedToEventRow.event_id}">In reply to</a> ${repliedToUserHtml}`
|
||||
|
|
@ -789,7 +791,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}
|
||||
|
||||
// Rich replies
|
||||
if (repliedToEventRow) {
|
||||
if (repliedToEventRow && !repliedToEventInDifferentRoom) {
|
||||
Object.assign(events[0], {
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const {test} = require("supertape")
|
|||
const {messageToEvent} = require("./message-to-event")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const data = require("../../../test/data")
|
||||
const {mockGetEffectivePower} = require("../../m2d/converters/utils.test")
|
||||
const Ty = require("../../types")
|
||||
|
||||
/**
|
||||
|
|
@ -66,17 +67,7 @@ test("message2event: simple room mention", async t => {
|
|||
let called = 0
|
||||
const events = await messageToEvent(data.message.simple_room_mention, data.guild.general, {}, {
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe")
|
||||
|
|
@ -97,24 +88,14 @@ test("message2event: simple room mention", async t => {
|
|||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<a href="https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe?via=cadence.moe&via=matrix.org">#worm-farm</a>'
|
||||
}])
|
||||
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
|
||||
t.equal(called, 1, "should call getJoinedMembers")
|
||||
})
|
||||
|
||||
test("message2event: simple room link", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent(data.message.simple_room_link, data.guild.general, {}, {
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe")
|
||||
|
|
@ -135,24 +116,14 @@ test("message2event: simple room link", async t => {
|
|||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<a href="https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe?via=cadence.moe&via=matrix.org">#worm-farm</a>'
|
||||
}])
|
||||
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
|
||||
t.equal(called, 1, "should call getJoinedMembers once")
|
||||
})
|
||||
|
||||
test("message2event: nicked room mention", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent(data.message.nicked_room_mention, data.guild.general, {}, {
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
|
|
@ -173,7 +144,7 @@ test("message2event: nicked room mention", async t => {
|
|||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org">#main</a>'
|
||||
}])
|
||||
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
|
||||
t.equal(called, 1, "should call getJoinedMembers once")
|
||||
})
|
||||
|
||||
test("message2event: unknown room mention", async t => {
|
||||
|
|
@ -224,17 +195,7 @@ test("message2event: simple message link", async t => {
|
|||
let called = 0
|
||||
const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}, {
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
|
|
@ -255,13 +216,14 @@ test("message2event: simple message link", async t => {
|
|||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg?via=cadence.moe&via=super.invalid">https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg?via=cadence.moe&via=super.invalid</a>'
|
||||
}])
|
||||
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
|
||||
t.equal(called, 1, "should call getJoinedMembers once")
|
||||
})
|
||||
|
||||
test("message2event: message link that OOYE doesn't know about", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent(data.message.message_link_to_before_ooye, data.guild.general, {}, {
|
||||
api: {
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getEventForTimestamp(roomID, ts) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
|
|
@ -270,17 +232,6 @@ test("message2event: message link that OOYE doesn't know about", async t => {
|
|||
origin_server_ts: 1613287812754
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) { // for ?via calculation
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
async getJoinedMembers(roomID) { // for ?via calculation
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
|
|
@ -303,7 +254,7 @@ test("message2event: message link that OOYE doesn't know about", async t => {
|
|||
formatted_body: "Me: I'll scroll up to find a certain message I'll send<br><em>scrolls up and clicks message links for god knows how long</em><br><em>completely forgets what they were looking for and simply begins scrolling up to find some fun moments</em><br><em>stumbles upon:</em> "
|
||||
+ '<a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$E8IQDGFqYzOU7BwY5Z74Bg-cwaU9OthXSroaWtgYc7U?via=cadence.moe&via=matrix.org">https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$E8IQDGFqYzOU7BwY5Z74Bg-cwaU9OthXSroaWtgYc7U?via=cadence.moe&via=matrix.org</a>'
|
||||
}])
|
||||
t.equal(called, 3, "getEventForTimestamp, getStateEvent, and getJoinedMembers should be called once each")
|
||||
t.equal(called, 2, "getEventForTimestamp and getJoinedMembers should be called once each")
|
||||
})
|
||||
|
||||
test("message2event: message timestamp failed to fetch", async t => {
|
||||
|
|
@ -318,17 +269,7 @@ test("message2event: message timestamp failed to fetch", async t => {
|
|||
error: "Unable to find event from 1726762095974 in direction Direction.FORWARDS"
|
||||
}, {})
|
||||
},
|
||||
async getStateEvent(roomID, type, key) { // for ?via calculation
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) { // for ?via calculation
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
|
|
@ -351,7 +292,7 @@ test("message2event: message timestamp failed to fetch", async t => {
|
|||
formatted_body: "Me: I'll scroll up to find a certain message I'll send<br><em>scrolls up and clicks message links for god knows how long</em><br><em>completely forgets what they were looking for and simply begins scrolling up to find some fun moments</em><br><em>stumbles upon:</em> "
|
||||
+ '[unknown event, timestamp resolution failed, in room: <a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org">https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org</a>]'
|
||||
}])
|
||||
t.equal(called, 3, "getEventForTimestamp, getStateEvent, and getJoinedMembers should be called once each")
|
||||
t.equal(called, 2, "getEventForTimestamp and getJoinedMembers should be called once each")
|
||||
})
|
||||
|
||||
test("message2event: message link from another server", async t => {
|
||||
|
|
@ -1136,6 +1077,7 @@ test("message2event: forwarded image", async t => {
|
|||
test("message2event: constructed forwarded message", async t => {
|
||||
const events = await messageToEvent(data.message.constructed_forwarded_message, {}, {}, {
|
||||
api: {
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers() {
|
||||
return {
|
||||
joined: {
|
||||
|
|
@ -1194,6 +1136,7 @@ test("message2event: constructed forwarded message", async t => {
|
|||
test("message2event: constructed forwarded text", async t => {
|
||||
const events = await messageToEvent(data.message.constructed_forwarded_text, {}, {}, {
|
||||
api: {
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers() {
|
||||
return {
|
||||
joined: {
|
||||
|
|
@ -1331,6 +1274,7 @@ test("message2event: vc invite event renders embed", async t => {
|
|||
test("message2event: vc invite event renders embed with room link", async t => {
|
||||
const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, {
|
||||
api: {
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => ({
|
||||
joined: {
|
||||
"@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null},
|
||||
|
|
@ -1380,6 +1324,7 @@ test("message2event: channel links are converted even inside lists (parser post-
|
|||
+ "\nThis list will probably change in the future"
|
||||
}, data.guild.general, {}, {
|
||||
api: {
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const {test} = require("supertape")
|
|||
const {threadToAnnouncement} = require("./thread-to-announcement")
|
||||
const data = require("../../../test/data")
|
||||
const Ty = require("../../types")
|
||||
const {mockGetEffectivePower} = require("../../m2d/converters/utils.test")
|
||||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
|
|
@ -30,13 +31,7 @@ function mockGetEvent(t, roomID_in, eventID_in, outer) {
|
|||
}
|
||||
|
||||
const viaApi = {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {
|
||||
users: {
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
async getJoinedMembers(roomID) {
|
||||
return {
|
||||
joined: {
|
||||
|
|
|
|||
10
src/db/migrations/0028-add-room-upgrade.sql
Normal file
10
src/db/migrations/0028-add-room-upgrade.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE room_upgrade_pending (
|
||||
new_room_id TEXT NOT NULL,
|
||||
old_room_id TEXT NOT NULL UNIQUE,
|
||||
PRIMARY KEY (new_room_id),
|
||||
FOREIGN KEY (old_room_id) REFERENCES channel_room (room_id) ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
59
src/db/migrations/0029-force-guild-ids.js
Normal file
59
src/db/migrations/0029-force-guild-ids.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
a. If the bridge bot sim already has the correct ID:
|
||||
- No rows updated.
|
||||
|
||||
b. If the bridge bot sim has the wrong ID but there's no duplicate:
|
||||
- One row updated.
|
||||
|
||||
c. If the bridge bot sim has the wrong ID and there's a duplicate:
|
||||
- One row updated (replaces an existing row).
|
||||
*/
|
||||
|
||||
const {discord} = require("../../passthrough")
|
||||
|
||||
const ones = "₀₁₂₃₄₅₆₇₈₉"
|
||||
const tens = "0123456789"
|
||||
|
||||
module.exports = async function(db) {
|
||||
/** @type {{name: string, channel_id: string, thread_parent: string | null}[]} */
|
||||
const rows = db.prepare("SELECT name, channel_id, thread_parent FROM channel_room WHERE guild_id IS NULL").all()
|
||||
|
||||
/** @type {Map<string, string>} channel or thread ID -> guild ID */
|
||||
const cache = new Map()
|
||||
|
||||
// Process channels
|
||||
process.stdout.write(` loading metadata for ${rows.length} channels/threads... `)
|
||||
for (let counter = 1; counter <= rows.length; counter++) {
|
||||
process.stdout.write(String(counter).at(-1) === "0" ? tens[(counter/10)%10] : ones[counter%10])
|
||||
const row = rows[counter-1]
|
||||
const id = row.thread_parent || row.channel_id
|
||||
if (cache.has(id)) continue
|
||||
|
||||
try {
|
||||
var channel = await discord.snow.channel.getChannel(id)
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
|
||||
const guildID = channel.guild_id
|
||||
const channels = await discord.snow.guild.getGuildChannels(guildID)
|
||||
for (const channel of channels) {
|
||||
cache.set(channel.id, guildID)
|
||||
}
|
||||
}
|
||||
|
||||
// Update channels and threads
|
||||
process.stdout.write("\n")
|
||||
db.transaction(() => {
|
||||
// Fill in missing data
|
||||
for (const row of rows) {
|
||||
const guildID = cache.get(row.thread_parent) || cache.get(row.channel_id)
|
||||
if (guildID) {
|
||||
db.prepare("UPDATE channel_room SET guild_id = ? WHERE channel_id = ?").run(guildID, row.channel_id)
|
||||
} else {
|
||||
db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(row.channel_id)
|
||||
db.prepare("DELETE FROM channel_room WHERE channel_id = ?").run(row.channel_id)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
44
src/db/migrations/0030-require-guild-id.sql
Normal file
44
src/db/migrations/0030-require-guild-id.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
-- https://sqlite.org/lang_altertable.html
|
||||
|
||||
-- 1
|
||||
PRAGMA foreign_keys=OFF;
|
||||
-- 2
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- 4
|
||||
CREATE TABLE "new_channel_room" (
|
||||
"channel_id" TEXT NOT NULL,
|
||||
"room_id" TEXT NOT NULL UNIQUE,
|
||||
"name" TEXT NOT NULL,
|
||||
"nick" TEXT,
|
||||
"thread_parent" TEXT,
|
||||
"custom_avatar" TEXT,
|
||||
"last_bridged_pin_timestamp" INTEGER,
|
||||
"speedbump_id" TEXT,
|
||||
"speedbump_checked" INTEGER,
|
||||
"speedbump_webhook_id" TEXT,
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"custom_topic" INTEGER DEFAULT 0,
|
||||
PRIMARY KEY("channel_id"),
|
||||
FOREIGN KEY("guild_id") REFERENCES "guild_active"("guild_id") ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- 5
|
||||
INSERT INTO new_channel_room
|
||||
(channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic)
|
||||
SELECT channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic
|
||||
FROM channel_room;
|
||||
|
||||
-- 6
|
||||
DROP TABLE channel_room;
|
||||
|
||||
-- 7
|
||||
ALTER TABLE new_channel_room RENAME TO channel_room;
|
||||
|
||||
-- 10
|
||||
PRAGMA foreign_key_check;
|
||||
|
||||
-- 11
|
||||
COMMIT;
|
||||
-- 12
|
||||
PRAGMA foreign_keys=ON;
|
||||
5
src/db/orm-defs.d.ts
vendored
5
src/db/orm-defs.d.ts
vendored
|
|
@ -103,6 +103,11 @@ export type Models = {
|
|||
historical_room_index: number
|
||||
}
|
||||
|
||||
room_upgrade_pending: {
|
||||
new_room_id: string
|
||||
old_room_id: string
|
||||
}
|
||||
|
||||
sim: {
|
||||
user_id: string
|
||||
username: string
|
||||
|
|
|
|||
|
|
@ -66,5 +66,5 @@ test("orm: select unsafe works (to select complex column names that can't be typ
|
|||
.and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level")
|
||||
.selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level")
|
||||
.all()
|
||||
t.equal(results[0].power_level, 100)
|
||||
t.equal(results[0].power_level, 150)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ const {InteractionMethods} = require("snowtransfer")
|
|||
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../../m2d/converters/utils")} */
|
||||
const utils = sync.require("../../m2d/converters/utils")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIContextMenuGuildInteraction} interaction
|
||||
* @param {{api: typeof api}} di
|
||||
* @param {{api: typeof api, utils: typeof utils}} di
|
||||
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
|
||||
*/
|
||||
async function* _interact({data, guild_id}, {api}) {
|
||||
async function* _interact({data, guild_id}, {api, utils}) {
|
||||
// Get message info
|
||||
const row = from("event_message")
|
||||
.join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||
|
|
@ -45,12 +47,10 @@ async function* _interact({data, guild_id}, {api}) {
|
|||
assert(spaceID)
|
||||
|
||||
// Get the power level
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const powerLevelsContent = await api.getStateEvent(spaceID, "m.room.power_levels", "")
|
||||
const userPower = powerLevelsContent.users?.[event.sender] || 0
|
||||
const {powers: {[event.sender]: userPower, [utils.bot]: botPower}} = await utils.getEffectivePower(spaceID, [event.sender, utils.bot], api)
|
||||
|
||||
// Administrators equal to the bot cannot be demoted
|
||||
if (userPower >= 100) {
|
||||
// Administrators/founders equal to the bot cannot be demoted
|
||||
if (userPower >= botPower) {
|
||||
return yield {createInteractionResponse: {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
|
|
@ -60,6 +60,8 @@ async function* _interact({data, guild_id}, {api}) {
|
|||
}}
|
||||
}
|
||||
|
||||
const adminLabel = botPower === 100 ? "Admin (you cannot undo this!)" : "Admin"
|
||||
|
||||
yield {createInteractionResponse: {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
|
|
@ -82,9 +84,9 @@ async function* _interact({data, guild_id}, {api}) {
|
|||
value: "moderator",
|
||||
default: userPower >= 50 && userPower < 100
|
||||
}, {
|
||||
label: "Admin (you cannot undo this!)",
|
||||
label: adminLabel,
|
||||
value: "admin",
|
||||
default: userPower === 100
|
||||
default: userPower >= 100
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -138,7 +140,7 @@ async function* _interactEdit({data, guild_id, message}, {api}) {
|
|||
|
||||
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */
|
||||
async function interact(interaction) {
|
||||
for await (const response of _interact(interaction, {api})) {
|
||||
for await (const response of _interact(interaction, {api, utils})) {
|
||||
if (response.createInteractionResponse) {
|
||||
// TODO: Test if it is reasonable to remove `await` from these calls. Or zip these calls with the next interaction iteration and use Promise.all.
|
||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const {test} = require("supertape")
|
|||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {select, db} = require("../../passthrough")
|
||||
const {_interact, _interactEdit} = require("./permissions")
|
||||
const {mockGetEffectivePower} = require("../../m2d/converters/utils.test")
|
||||
|
||||
/**
|
||||
* @template T
|
||||
|
|
@ -46,6 +47,10 @@ test("permissions: reports permissions of selected matrix user (implicit default
|
|||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
utils: {
|
||||
bot: "@_ooye_bot:cadence.moe",
|
||||
getEffectivePower: mockGetEffectivePower()
|
||||
},
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
|
|
@ -54,22 +59,13 @@ test("permissions: reports permissions of selected matrix user (implicit default
|
|||
return {
|
||||
sender: "@cadence:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe") // space ID
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(msgs.length, 1)
|
||||
t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.")
|
||||
t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[0], {label: "Default", value: "default", default: true})
|
||||
t.equal(called, 2)
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("permissions: reports permissions of selected matrix user (moderator)", async t => {
|
||||
|
|
@ -80,6 +76,10 @@ test("permissions: reports permissions of selected matrix user (moderator)", asy
|
|||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
utils: {
|
||||
bot: "@_ooye_bot:cadence.moe",
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {"@cadence:cadence.moe": 50})
|
||||
},
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
|
|
@ -88,27 +88,16 @@ test("permissions: reports permissions of selected matrix user (moderator)", asy
|
|||
return {
|
||||
sender: "@cadence:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe") // space ID
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@cadence:cadence.moe": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(msgs.length, 1)
|
||||
t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.")
|
||||
t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[1], {label: "Moderator", value: "moderator", default: true})
|
||||
t.equal(called, 2)
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("permissions: reports permissions of selected matrix user (admin)", async t => {
|
||||
test("permissions: reports permissions of selected matrix user (admin v12 can be demoted)", async t => {
|
||||
let called = 0
|
||||
const msgs = await fromAsync(_interact({
|
||||
data: {
|
||||
|
|
@ -116,6 +105,10 @@ test("permissions: reports permissions of selected matrix user (admin)", async t
|
|||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
utils: {
|
||||
bot: "@_ooye_bot:cadence.moe",
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {"@cadence:cadence.moe": 100})
|
||||
},
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
|
|
@ -124,24 +117,42 @@ test("permissions: reports permissions of selected matrix user (admin)", async t
|
|||
return {
|
||||
sender: "@cadence:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe") // space ID
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
users: {
|
||||
"@cadence:cadence.moe": 100
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(msgs.length, 1)
|
||||
t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.")
|
||||
t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[2], {label: "Admin", value: "admin", default: true})
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("permissions: reports permissions of selected matrix user (admin v11 cannot be demoted)", async t => {
|
||||
let called = 0
|
||||
const msgs = await fromAsync(_interact({
|
||||
data: {
|
||||
target_id: "1128118177155526666"
|
||||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
utils: {
|
||||
bot: "@_ooye_bot:cadence.moe",
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {"@cadence:cadence.moe": 100, "@_ooye_bot:cadence.moe": 100}, "11")
|
||||
},
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID
|
||||
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||
return {
|
||||
sender: "@cadence:cadence.moe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(msgs.length, 1)
|
||||
t.equal(msgs[0].createInteractionResponse.data.content, "`@cadence:cadence.moe` has administrator permissions. This cannot be edited.")
|
||||
t.notOk(msgs[0].createInteractionResponse.data.components)
|
||||
t.equal(called, 2)
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("permissions: can update user to moderator", async t => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const Ty = require("../../types")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
|
|
@ -22,6 +22,19 @@ async function deleteMessage(event) {
|
|||
db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
async function suppressEmbeds(event) {
|
||||
const rows = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||
.select("reference_channel_id", "message_id").where({event_id: event.redacts}).all()
|
||||
if (!rows.length) return
|
||||
db.prepare("DELETE FROM event_message WHERE event_id = ?").run(event.redacts)
|
||||
for (const row of rows) {
|
||||
await discord.snow.channel.editMessage(row.reference_channel_id, row.message_id, {flags: DiscordTypes.MessageFlags.SuppressEmbeds})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
|
|
@ -39,7 +52,12 @@ async function removeReaction(event) {
|
|||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
async function handle(event) {
|
||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||
await suppressEmbeds(event)
|
||||
} else {
|
||||
await deleteMessage(event)
|
||||
}
|
||||
await removeReaction(event)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,10 +136,11 @@ turndownService.addRule("inlineLink", {
|
|||
if (node.getAttribute("data-message-id")) return `https://discord.com/channels/${node.getAttribute("data-guild-id")}/${node.getAttribute("data-channel-id")}/${node.getAttribute("data-message-id")}`
|
||||
if (node.getAttribute("data-channel-id")) return `<#${node.getAttribute("data-channel-id")}>`
|
||||
const href = node.getAttribute("href")
|
||||
const suppressedHref = node.hasAttribute("data-suppress") ? "<" + href + ">" : href
|
||||
content = content.replace(/ @.*/, "")
|
||||
if (href === content) return href
|
||||
if (href === content) return suppressedHref
|
||||
if (decodeURIComponent(href).startsWith("https://matrix.to/#/@") && content[0] !== "@") content = "@" + content
|
||||
return "[" + content + "](" + href + ")"
|
||||
return "[" + content + "](" + suppressedHref + ")"
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -446,9 +447,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
|||
let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+
|
||||
if (writtenMentionMatch) {
|
||||
if (writtenMentionMatch[1] === "room") { // convert @room to @everyone
|
||||
const powerLevels = await di.api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
const userPower = powerLevels.users?.[senderMxid] || 0
|
||||
if (userPower >= powerLevels.notifications?.room) {
|
||||
const {powers: {[senderMxid]: userPower}, powerLevels} = await mxUtils.getEffectivePower(roomID, [senderMxid], di.api)
|
||||
if (userPower >= (powerLevels.notifications?.room ?? 50)) {
|
||||
return {
|
||||
// @ts-ignore - typescript doesn't know about indices yet
|
||||
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]),
|
||||
|
|
@ -860,6 +860,21 @@ async function eventToMessage(event, guild, di) {
|
|||
pendingFiles.push({name: filename, buffer: Buffer.from(content, "utf8")})
|
||||
}
|
||||
}
|
||||
// Suppress link embeds
|
||||
if (node.nodeType === 1 && node.tagName === "A") {
|
||||
// Suppress if sender tried to add angle brackets
|
||||
const inBody = event.content.body.indexOf(node.getAttribute("href"))
|
||||
let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<"
|
||||
if (!shouldSuppress && guild?.roles) {
|
||||
// Suppress if regular users don't have permission
|
||||
const permissions = dUtils.getPermissions([], guild.roles)
|
||||
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||
shouldSuppress = !canEmbedLinks
|
||||
}
|
||||
if (shouldSuppress) {
|
||||
node.setAttribute("data-suppress", "")
|
||||
}
|
||||
}
|
||||
await forEachNode(node.firstChild)
|
||||
}
|
||||
}
|
||||
|
|
@ -901,7 +916,28 @@ async function eventToMessage(event, guild, di) {
|
|||
}
|
||||
|
||||
content = await handleRoomOrMessageLinks(content, di) // Replace matrix.to links with discord.com equivalents where possible
|
||||
content = content.replace(/\bhttps?:\/\/matrix\.to\/[^<>\n )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews
|
||||
|
||||
let offset = 0
|
||||
for (const match of [...content.matchAll(/\bhttps?:\/\/[^ )>]*/g)]) {
|
||||
assert(typeof match.index === "number")
|
||||
|
||||
// Respect sender's angle brackets
|
||||
const alreadySuppressed = content[match.index-1+offset] === "<" && content[match.index+match.length+offset] === ">"
|
||||
if (alreadySuppressed) continue
|
||||
// Put < > around any surviving matrix.to links
|
||||
let shouldSuppress = !!match[0].match(/^https?:\/\/matrix\.to\//)
|
||||
if (!shouldSuppress && guild?.roles) {
|
||||
// Suppress if regular users don't have permission
|
||||
const permissions = dUtils.getPermissions([], guild.roles)
|
||||
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||
shouldSuppress = !canEmbedLinks
|
||||
}
|
||||
|
||||
if (shouldSuppress) {
|
||||
content = content.slice(0, match.index + offset) + "<" + match[0] + ">" + content.slice(match.index + match[0].length + offset)
|
||||
offset += 2
|
||||
}
|
||||
}
|
||||
|
||||
const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di)
|
||||
if (result) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const assert = require("assert").strict
|
||||
const fs = require("fs")
|
||||
const {test} = require("supertape")
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {eventToMessage} = require("./event-to-message")
|
||||
const {convertImageStream} = require("./emoji-sheet")
|
||||
const data = require("../../../test/data")
|
||||
|
|
@ -302,6 +303,140 @@ test("event2message: markdown in link text does not attempt to be escaped becaus
|
|||
)
|
||||
})
|
||||
|
||||
test("event2message: links are escaped if the guild does not have embed links permission (formatted body)", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
content: {
|
||||
body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `posting one of my favourite songs recently (starts at timestamp) <a href="https://youtu.be/RhV2X7WQMPA?t=364">https://youtu.be/RhV2X7WQMPA?t=364</a>`,
|
||||
msgtype: "m.text"
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
origin_server_ts: 1688301929913,
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
|
||||
sender: "@cadence:cadence.moe",
|
||||
type: "m.room.message",
|
||||
}, {
|
||||
id: "123",
|
||||
roles: [{
|
||||
id: "123",
|
||||
name: "@everyone",
|
||||
permissions: DiscordTypes.PermissionFlagsBits.SendMessages
|
||||
}]
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "posting one of my favourite songs recently (starts at timestamp) <https://youtu.be/RhV2X7WQMPA?t=364>",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: links are escaped if the guild does not have embed links permission (plaintext body)", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
content: {
|
||||
body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364",
|
||||
msgtype: "m.text"
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
origin_server_ts: 1688301929913,
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
|
||||
sender: "@cadence:cadence.moe",
|
||||
type: "m.room.message",
|
||||
}, {
|
||||
id: "123",
|
||||
roles: [{
|
||||
id: "123",
|
||||
name: "@everyone",
|
||||
permissions: DiscordTypes.PermissionFlagsBits.SendMessages
|
||||
}]
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "posting one of my favourite songs recently (starts at timestamp) <https://youtu.be/RhV2X7WQMPA?t=364>",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: links retain angle brackets (formatted body)", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
content: {
|
||||
body: "posting one of my favourite songs recently (starts at timestamp) <https://youtu.be/RhV2X7WQMPA?t=364>",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `posting one of my favourite songs recently (starts at timestamp) <a href="https://youtu.be/RhV2X7WQMPA?t=364">https://youtu.be/RhV2X7WQMPA?t=364</a>`,
|
||||
msgtype: "m.text"
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
origin_server_ts: 1688301929913,
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
|
||||
sender: "@cadence:cadence.moe",
|
||||
type: "m.room.message",
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "posting one of my favourite songs recently (starts at timestamp) <https://youtu.be/RhV2X7WQMPA?t=364>",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: links retain angle brackets (plaintext body)", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
content: {
|
||||
body: "posting one of my favourite songs recently (starts at timestamp) <https://youtu.be/RhV2X7WQMPA?t=364>",
|
||||
msgtype: "m.text"
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
origin_server_ts: 1688301929913,
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
|
||||
sender: "@cadence:cadence.moe",
|
||||
type: "m.room.message",
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "posting one of my favourite songs recently (starts at timestamp) <https://youtu.be/RhV2X7WQMPA?t=364>",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: basic html is converted to markdown", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
@ -4850,7 +4985,7 @@ test("event2message: @room converts to @everyone and is allowed when the room do
|
|||
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
|
||||
}, data.guild.general, {
|
||||
api: {
|
||||
getStateEvent(roomID, type, key) {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
|
|
@ -4861,6 +4996,19 @@ test("event2message: @room converts to @everyone and is allowed when the room do
|
|||
room: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -4881,7 +5029,6 @@ test("event2message: @room converts to @everyone and is allowed when the room do
|
|||
})
|
||||
|
||||
test("event2message: @room converts to @everyone but is not allowed when the room restricts who can use it", async t => {
|
||||
let called = 0
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
|
|
@ -4896,8 +5043,7 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
|
|||
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
|
||||
}, data.guild.general, {
|
||||
api: {
|
||||
getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
async getStateEvent(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
|
|
@ -4907,6 +5053,19 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
|
|||
room: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -4927,7 +5086,6 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
|
|||
})
|
||||
|
||||
test("event2message: @room converts to @everyone and is allowed if the user has sufficient power to use it", async t => {
|
||||
let called = 0
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
|
|
@ -4942,8 +5100,7 @@ test("event2message: @room converts to @everyone and is allowed if the user has
|
|||
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
|
||||
}, data.guild.general, {
|
||||
api: {
|
||||
getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
async getStateEvent(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
|
|
@ -4955,6 +5112,19 @@ test("event2message: @room converts to @everyone and is allowed if the user has
|
|||
room: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
|
||||
const Ty = require("../../types")
|
||||
const passthrough = require("../../passthrough")
|
||||
const {db} = passthrough
|
||||
|
||||
|
|
@ -13,6 +13,8 @@ let hasher = null
|
|||
// @ts-ignore
|
||||
require("xxhash-wasm")().then(h => hasher = h)
|
||||
|
||||
const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
|
||||
const BLOCK_ELEMENTS = [
|
||||
"ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS",
|
||||
"CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE",
|
||||
|
|
@ -127,7 +129,7 @@ class MatrixStringBuilder {
|
|||
* https://spec.matrix.org/v1.9/appendices/#routing
|
||||
* https://gitdab.com/cadence/out-of-your-element/issues/11
|
||||
* @param {string} roomID
|
||||
* @param {{[K in "getStateEvent" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
|
||||
* @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("../../matrix/api")[K]} | {getEffectivePower: (roomID: string, mxids: string[], api: any) => Promise<{powers: Record<string, number>, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter<Ty.Event.M_Room_Create>, powerLevels: Ty.Event.M_Power_Levels}>, getJoinedMembers: import("../../matrix/api")["getJoinedMembers"]}} api
|
||||
*/
|
||||
async function getViaServers(roomID, api) {
|
||||
const candidates = []
|
||||
|
|
@ -136,26 +138,18 @@ async function getViaServers(roomID, api) {
|
|||
candidates.push(reg.ooye.server_name)
|
||||
// Candidate 1: Highest joined non-sim non-bot power level user in the room
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/552c65db98b59406fb49562e537a2721c8505517/src/utils/permalinks/Permalinks.ts#L172
|
||||
try {
|
||||
/** @type {{users?: {[mxid: string]: number}}} */
|
||||
const powerLevels = await api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
if (powerLevels.users) {
|
||||
const sorted = Object.entries(powerLevels.users).sort((a, b) => b[1] - a[1]) // Highest...
|
||||
for (const power of sorted) {
|
||||
const mxid = power[0]
|
||||
const call = "getEffectivePower" in api ? api.getEffectivePower(roomID, [bot], api) : getEffectivePower(roomID, [bot], api)
|
||||
const {allCreators, powerLevels} = await call
|
||||
const sorted = allCreators.concat(Object.entries(powerLevels.users ?? {}).sort((a, b) => b[1] - a[1]).map(([mxid]) => mxid)) // Highest...
|
||||
for (const mxid of sorted) {
|
||||
if (!(mxid in joined)) continue // joined...
|
||||
if (userRegex.some(r => mxid.match(r))) continue // non-sim non-bot...
|
||||
const match = mxid.match(/:(.*)/)
|
||||
assert(match)
|
||||
if (!candidates.includes(match[1])) {
|
||||
if (candidates.includes(match[1])) continue // from a different server
|
||||
candidates.push(match[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// power levels event not found
|
||||
}
|
||||
// Candidates 2-3: Most popular servers in the room
|
||||
/** @type {Map<string, number>} */
|
||||
const servers = new Map()
|
||||
|
|
@ -194,7 +188,7 @@ async function getViaServers(roomID, api) {
|
|||
* https://spec.matrix.org/v1.9/appendices/#routing
|
||||
* https://gitdab.com/cadence/out-of-your-element/issues/11
|
||||
* @param {string} roomID
|
||||
* @param {{[K in "getStateEvent" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
|
||||
* @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
|
||||
* @returns {Promise<URLSearchParams>}
|
||||
*/
|
||||
async function getViaServersQuery(roomID, api) {
|
||||
|
|
@ -232,6 +226,81 @@ function getPublicUrlForMxc(mxc) {
|
|||
return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomVersionString
|
||||
* @param {number} desiredVersion
|
||||
*/
|
||||
function roomHasAtLeastVersion(roomVersionString, desiredVersion) {
|
||||
/*
|
||||
I hate this.
|
||||
The spec instructs me to compare room versions ordinally, for example, "In room versions 12 and higher..."
|
||||
So if the real room version is 13, this should pass the check.
|
||||
However, the spec also says "room versions are not intended to be parsed and should be treated as opaque identifiers", "due to versions not being ordered or hierarchical".
|
||||
So versions are unordered and opaque and you can't parse them, but you're still expected to parse them to a number and compare them to another number to measure if it's "12 or higher"?
|
||||
Theoretically MSC3244 would clean this up, but that isn't happening since Element removed support for MSC3244: https://github.com/element-hq/element-web/commit/644b8415912afb9c5eed54859a444a2ee7224117
|
||||
Element replaced it with the following function:
|
||||
*/
|
||||
|
||||
// Assumption: all unstable room versions don't support the feature. Calling code can check for unstable
|
||||
// room versions explicitly if it wants to. The spec reserves [0-9] and `.` for its room versions.
|
||||
if (!roomVersionString.match(/^[\d.]+$/)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Element dev note: While the spec says room versions are not linear, we can make reasonable assumptions
|
||||
// until the room versions prove themselves to be non-linear in the spec. We should see this coming
|
||||
// from a mile away and can course-correct this function if needed.
|
||||
return Number(roomVersionString) >= Number(desiredVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting in room version 12, creators may not be specified in power levels users.
|
||||
* Modifies the input power levels.
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Create>} roomCreateOuter
|
||||
* @param {Ty.Event.M_Power_Levels} powerLevels
|
||||
*/
|
||||
function removeCreatorsFromPowerLevels(roomCreateOuter, powerLevels) {
|
||||
assert(roomCreateOuter.sender)
|
||||
if (roomHasAtLeastVersion(roomCreateOuter.content.room_version, 12)) {
|
||||
for (const creator of (roomCreateOuter.content.additional_creators ?? []).concat(roomCreateOuter.sender)) {
|
||||
delete powerLevels.users[creator]
|
||||
}
|
||||
}
|
||||
return powerLevels
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {string} T
|
||||
* @param {string} roomID
|
||||
* @param {T[]} mxids
|
||||
* @param {{[K in "getStateEvent" | "getStateEventOuter"]: import("../../matrix/api")[K]}} api
|
||||
* @returns {Promise<{powers: Record<T, number>, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter<Ty.Event.M_Room_Create>, powerLevels: Ty.Event.M_Power_Levels}>}
|
||||
*/
|
||||
async function getEffectivePower(roomID, mxids, api) {
|
||||
/** @type {[Ty.Event.StateOuter<Ty.Event.M_Room_Create>, Ty.Event.M_Power_Levels]} */
|
||||
const [roomCreate, powerLevels] = await Promise.all([
|
||||
api.getStateEventOuter(roomID, "m.room.create", ""),
|
||||
api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
])
|
||||
const allCreators =
|
||||
( roomHasAtLeastVersion(roomCreate.content.room_version, 12) ? (roomCreate.content.additional_creators ?? []).concat(roomCreate.sender)
|
||||
: [])
|
||||
const tombstone =
|
||||
( roomHasAtLeastVersion(roomCreate.content.room_version, 12) ? powerLevels.events?.["m.room.tombstone"] ?? 150
|
||||
: powerLevels.events?.["m.room.tombstone"] ?? powerLevels.state_default ?? 50)
|
||||
/** @type {Record<T, number>} */ // @ts-ignore
|
||||
const powers = {}
|
||||
for (const mxid of mxids) {
|
||||
powers[mxid] =
|
||||
( roomHasAtLeastVersion(roomCreate.content.room_version, 12) && allCreators.includes(mxid) ? Infinity
|
||||
: powerLevels.users?.[mxid]
|
||||
?? powerLevels.users_default
|
||||
?? 0)
|
||||
}
|
||||
return {powers, allCreators, tombstone, roomCreate, powerLevels}
|
||||
}
|
||||
|
||||
module.exports.bot = bot
|
||||
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
|
||||
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
|
||||
module.exports.getPublicUrlForMxc = getPublicUrlForMxc
|
||||
|
|
@ -239,3 +308,6 @@ module.exports.getEventIDHash = getEventIDHash
|
|||
module.exports.MatrixStringBuilder = MatrixStringBuilder
|
||||
module.exports.getViaServers = getViaServers
|
||||
module.exports.getViaServersQuery = getViaServersQuery
|
||||
module.exports.roomHasAtLeastVersion = roomHasAtLeastVersion
|
||||
module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels
|
||||
module.exports.getEffectivePower = getEffectivePower
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const e = new Error("Custom error")
|
||||
|
||||
const {test} = require("supertape")
|
||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers} = require("./utils")
|
||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion} = require("./utils")
|
||||
const util = require("util")
|
||||
|
||||
/** @param {string[]} mxids */
|
||||
|
|
@ -88,9 +88,42 @@ test("MatrixStringBuilder: complete code coverage", t => {
|
|||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string[]} [creators]
|
||||
* @param {{[x: string]: number}} [users]
|
||||
* @param {string} [roomVersion]
|
||||
*/
|
||||
function mockGetEffectivePower(creators = ["@_ooye_bot:cadence.moe"], users = {}, roomVersion = "12") {
|
||||
return async function getEffectivePower(roomID, mxids) {
|
||||
return {
|
||||
allCreators: creators,
|
||||
powerLevels: {users},
|
||||
powers: mxids.reduce((a, mxid) => {
|
||||
if (creators.includes(mxid) && roomHasAtLeastVersion(roomVersion, 12)) a[mxid] = Infinity
|
||||
else if (mxid in users) a[mxid] = users[mxid]
|
||||
else a[mxid] = 0
|
||||
return a
|
||||
}, {}),
|
||||
roomCreate: {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: creators[0],
|
||||
content: {
|
||||
additional_creators: creators.slice(1),
|
||||
room_version: roomVersion
|
||||
},
|
||||
room_id: roomID,
|
||||
origin_server_ts: 0,
|
||||
event_id: "$create"
|
||||
},
|
||||
tombstone: roomVersion === "12" ? 150 : 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("getViaServers: returns the server name if the room only has sim users", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({}),
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe"])
|
||||
|
|
@ -98,7 +131,7 @@ test("getViaServers: returns the server name if the room only has sim users", as
|
|||
|
||||
test("getViaServers: also returns the most popular servers in order", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({}),
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "thecollective.invalid", "selfhosted.invalid"])
|
||||
|
|
@ -106,20 +139,27 @@ test("getViaServers: also returns the most popular servers in order", async t =>
|
|||
|
||||
test("getViaServers: does not return IP address servers", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({}),
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:45.77.232.172:8443", "@cadence:[::1]:8443", "@cadence:123example.456example.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "123example.456example.invalid"])
|
||||
})
|
||||
|
||||
test("getViaServers: also returns the highest power level user (v12 creator)", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe", "@singleuser:selfhosted.invalid"], {
|
||||
"@moderator:tractor.invalid": 50
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "selfhosted.invalid", "thecollective.invalid", "tractor.invalid"])
|
||||
})
|
||||
|
||||
test("getViaServers: also returns the highest power level user (100)", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@singleuser:selfhosted.invalid": 100,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
"@singleuser:selfhosted.invalid": 100
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
|
||||
})
|
||||
|
|
@ -128,11 +168,8 @@ test("getViaServers: also returns the highest power level user (100)", async t =
|
|||
|
||||
test("getViaServers: also returns the highest power level user (50)", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
|
||||
"@moderator:tractor.invalid": 50
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
|
||||
})
|
||||
|
|
@ -141,38 +178,23 @@ test("getViaServers: also returns the highest power level user (50)", async t =>
|
|||
|
||||
test("getViaServers: returns at most 4 results", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@singleuser:selfhosted.invalid": 100,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
"@singleuser:selfhosted.invalid": 100
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
|
||||
})
|
||||
t.deepEqual(result.length, 4)
|
||||
})
|
||||
|
||||
test("getViaServers: returns results even when power levels can't be fetched", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => {
|
||||
throw new Error("event not found or something")
|
||||
},
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
|
||||
})
|
||||
t.deepEqual(result.length, 4)
|
||||
})
|
||||
|
||||
test("getViaServers: only considers power levels of currently joined members", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@former_moderator:missing.invalid": 100,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe", "@former_moderator:missing.invalid"], {
|
||||
"@moderator:tractor.invalid": 50
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "tractor.invalid", "thecollective.invalid", "selfhosted.invalid"])
|
||||
})
|
||||
|
||||
module.exports.mockGetEffectivePower = mockGetEffectivePower
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const utils = sync.require("./converters/utils")
|
|||
const api = sync.require("../matrix/api")
|
||||
/** @type {import("../d2m/actions/create-room")} */
|
||||
const createRoom = sync.require("../d2m/actions/create-room")
|
||||
/** @type {import("../matrix/room-upgrade")} */
|
||||
const roomUpgrade = require("../matrix/room-upgrade")
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
let lastReportedEvent = 0
|
||||
|
|
@ -171,9 +173,8 @@ async function onRetryReactionAdd(reactionEvent) {
|
|||
// To stop people injecting misleading messages, the reaction needs to come from either the original sender or a room moderator
|
||||
if (reactionEvent.sender !== event.sender) {
|
||||
// Check if it's a room moderator
|
||||
const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
const powerLevel = powerLevelsStateContent.users?.[reactionEvent.sender] || 0
|
||||
if (powerLevel < 50) return
|
||||
const {powers: {[reactionEvent.sender]: senderPower}, powerLevels} = await utils.getEffectivePower(roomID, [reactionEvent.sender], api)
|
||||
if (senderPower < (powerLevels.state_default ?? 50)) return
|
||||
}
|
||||
|
||||
// Retry
|
||||
|
|
@ -330,6 +331,11 @@ async event => {
|
|||
if (event.state_key[0] !== "@") return
|
||||
const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
|
||||
if (event.state_key === bot) {
|
||||
const upgraded = await roomUpgrade.onBotMembership(event)
|
||||
if (upgraded) return
|
||||
}
|
||||
|
||||
if (event.content.membership === "invite" && event.state_key === bot) {
|
||||
// We were invited to a room. We should join, and register the invite details for future reference in web.
|
||||
let attemptedApiMessage = "According to unsigned invite data."
|
||||
|
|
@ -342,10 +348,10 @@ async event => {
|
|||
attemptedApiMessage = "According to unsigned invite data. SSS API unavailable: " + e.toString()
|
||||
}
|
||||
}
|
||||
const name = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.name", "name")
|
||||
const topic = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.topic", "topic")
|
||||
const avatar = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.avatar", "url")
|
||||
const creationType = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.create", "type")
|
||||
const name = getFromInviteRoomState(inviteRoomState, "m.room.name", "name")
|
||||
const topic = getFromInviteRoomState(inviteRoomState, "m.room.topic", "topic")
|
||||
const avatar = getFromInviteRoomState(inviteRoomState, "m.room.avatar", "url")
|
||||
const creationType = getFromInviteRoomState(inviteRoomState, "m.room.create", "type")
|
||||
if (!name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite! (${attemptedApiMessage})`)
|
||||
await api.joinRoom(event.room_id)
|
||||
db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, creationType, name, topic, avatar)
|
||||
|
|
@ -368,18 +374,14 @@ async event => {
|
|||
if (!exists) return // don't cache members in unbridged rooms
|
||||
|
||||
// Member is here
|
||||
let powerLevel = 0
|
||||
try {
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const powerLevelsEvent = await api.getStateEvent(event.room_id, "m.room.power_levels", "")
|
||||
powerLevel = powerLevelsEvent.users?.[event.state_key] ?? powerLevelsEvent.users_default ?? 0
|
||||
} catch (e) {}
|
||||
let {powers: {[event.state_key]: memberPower}, tombstone} = await utils.getEffectivePower(event.room_id, [event.state_key], api)
|
||||
if (memberPower === Infinity) memberPower = tombstone // database storage compatibility
|
||||
const displayname = event.content.displayname || null
|
||||
const avatar_url = event.content.avatar_url
|
||||
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?").run(
|
||||
event.room_id, event.state_key,
|
||||
displayname, avatar_url, powerLevel,
|
||||
displayname, avatar_url, powerLevel
|
||||
displayname, avatar_url, memberPower,
|
||||
displayname, avatar_url, memberPower
|
||||
)
|
||||
}))
|
||||
|
||||
|
|
@ -390,10 +392,21 @@ sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_le
|
|||
async event => {
|
||||
if (event.state_key !== "") return
|
||||
const existingPower = select("member_cache", "mxid", {room_id: event.room_id}).pluck().all()
|
||||
const {allCreators} = await utils.getEffectivePower(event.room_id, [], api)
|
||||
const newPower = event.content.users || {}
|
||||
for (const mxid of existingPower) {
|
||||
if (!allCreators.includes(mxid)) {
|
||||
db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(newPower[mxid] || 0, event.room_id, mxid)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
||||
*/
|
||||
async event => {
|
||||
await roomUpgrade.onTombstone(event)
|
||||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const assert = require("assert").strict
|
|||
const streamWeb = require("stream/web")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {sync} = passthrough
|
||||
const {sync, db, select} = passthrough
|
||||
/** @type {import("./mreq")} */
|
||||
const mreq = sync.require("./mreq")
|
||||
/** @type {import("./txnid")} */
|
||||
|
|
@ -122,7 +122,7 @@ async function getEventForTimestamp(roomID, ts) {
|
|||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
* @returns {Promise<Ty.Event.BaseStateEvent[]>}
|
||||
* @returns {Promise<Ty.Event.StateOuter<any>[]>}
|
||||
*/
|
||||
function getAllState(roomID) {
|
||||
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`)
|
||||
|
|
@ -138,6 +138,16 @@ function getStateEvent(roomID, type, key) {
|
|||
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
* @param {string} type
|
||||
* @param {string} key
|
||||
* @returns {Promise<Ty.Event.StateOuter<any>>} the entire state event
|
||||
*/
|
||||
function getStateEventOuter(roomID, type, key) {
|
||||
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}?format=event`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
* @returns {Promise<Ty.Event.InviteStrippedState[]>}
|
||||
|
|
@ -513,6 +523,36 @@ function versions() {
|
|||
return mreq.mreq("GET", "/client/versions")
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mxid
|
||||
*/
|
||||
async function usePrivateChat(mxid) {
|
||||
// Check if we have an existing DM
|
||||
let roomID = select("direct", "room_id", {mxid}).pluck().get()
|
||||
if (roomID) {
|
||||
// Check that the person is/still in the room
|
||||
try {
|
||||
var member = await getStateEvent(roomID, "m.room.member", mxid)
|
||||
} catch (e) {}
|
||||
|
||||
// Invite them back to the room if needed
|
||||
if (!member || member.membership === "leave") {
|
||||
await inviteToRoom(roomID, mxid)
|
||||
}
|
||||
return roomID
|
||||
}
|
||||
|
||||
// No existing DM, create a new room and invite
|
||||
roomID = await createRoom({
|
||||
invite: [mxid],
|
||||
is_direct: true,
|
||||
preset: "trusted_private_chat"
|
||||
})
|
||||
// Store the newly created room in the database (not using account data due to awkward bugs with misaligned state)
|
||||
db.prepare("REPLACE INTO direct (mxid, room_id) VALUES (?, ?)").run(mxid, roomID)
|
||||
return roomID
|
||||
}
|
||||
|
||||
module.exports.path = path
|
||||
module.exports.register = register
|
||||
module.exports.createRoom = createRoom
|
||||
|
|
@ -524,6 +564,7 @@ module.exports.getEvent = getEvent
|
|||
module.exports.getEventForTimestamp = getEventForTimestamp
|
||||
module.exports.getAllState = getAllState
|
||||
module.exports.getStateEvent = getStateEvent
|
||||
module.exports.getStateEventOuter = getStateEventOuter
|
||||
module.exports.getInviteState = getInviteState
|
||||
module.exports.getJoinedMembers = getJoinedMembers
|
||||
module.exports.getMembers = getMembers
|
||||
|
|
@ -550,3 +591,4 @@ module.exports.setAccountData = setAccountData
|
|||
module.exports.setPresence = setPresence
|
||||
module.exports.getProfile = getProfile
|
||||
module.exports.versions = versions
|
||||
module.exports.usePrivateChat = usePrivateChat
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ const {sync} = passthrough
|
|||
const file = sync.require("./file")
|
||||
/** @type {import("./api")} */
|
||||
const api = sync.require("./api")
|
||||
/** @type {import("../m2d/converters/utils")} */
|
||||
const utils = sync.require("../m2d/converters/utils")
|
||||
|
||||
/** Mutates the input. Not recursive - can only include or exclude entire state events. */
|
||||
function kstateStripConditionals(kstate) {
|
||||
|
|
@ -45,12 +47,13 @@ async function kstateUploadMxc(obj) {
|
|||
return obj
|
||||
}
|
||||
|
||||
/** Automatically strips conditionals and uploads URLs to mxc. */
|
||||
/** Automatically strips conditionals and uploads URLs to mxc. m.room.create is removed. */
|
||||
async function kstateToState(kstate) {
|
||||
const events = []
|
||||
kstateStripConditionals(kstate)
|
||||
await kstateUploadMxc(kstate)
|
||||
for (const [k, content] of Object.entries(kstate)) {
|
||||
if (k === "m.room.create/") continue
|
||||
const slashIndex = k.indexOf("/")
|
||||
assert(slashIndex > 0)
|
||||
const type = k.slice(0, slashIndex)
|
||||
|
|
@ -60,14 +63,24 @@ async function kstateToState(kstate) {
|
|||
return events
|
||||
}
|
||||
|
||||
/** Extracts m.room.create for use in room creation_content. */
|
||||
function kstateToCreationContent(kstate) {
|
||||
return kstate["m.room.create/"] || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../types").Event.BaseStateEvent[]} events
|
||||
* @param {import("../types").Event.StateOuter<any>[]} events
|
||||
* @returns {any}
|
||||
*/
|
||||
function stateToKState(events) {
|
||||
const kstate = {}
|
||||
for (const event of events) {
|
||||
kstate[event.type + "/" + event.state_key] = event.content
|
||||
|
||||
// need to remember m.room.create sender for later...
|
||||
if (event.type === "m.room.create" && event.state_key === "") {
|
||||
kstate["m.room.create/outer"] = event
|
||||
}
|
||||
}
|
||||
return kstate
|
||||
}
|
||||
|
|
@ -81,15 +94,26 @@ function diffKState(actual, target) {
|
|||
if (key === "m.room.power_levels/") {
|
||||
// Special handling for power levels, we want to deep merge the actual and target into the final state.
|
||||
if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
|
||||
const temp = mixin({}, actual[key], target[key])
|
||||
if (!isDeepStrictEqual(actual[key], temp)) {
|
||||
const mixedTarget = mixin({}, actual[key], target[key])
|
||||
if (!isDeepStrictEqual(actual[key], mixedTarget)) {
|
||||
// they differ. use the newly prepared object as the diff.
|
||||
diff[key] = temp
|
||||
// if the diff includes users, it needs to be cleaned wrt room version 12
|
||||
if (target[key].users && Object.keys(target[key].users).length > 0) {
|
||||
if (!("m.room.create/" in actual)) throw new Error(`want to apply a power levels diff, but original m.room.create/ is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
|
||||
if (!("m.room.create/outer" in actual)) throw new Error(`want to apply a power levels diff, but original m.room.create/outer is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
|
||||
utils.removeCreatorsFromPowerLevels(actual["m.room.create/outer"], mixedTarget)
|
||||
}
|
||||
diff[key] = mixedTarget
|
||||
}
|
||||
|
||||
} else if (key === "chat.schildi.hide_ui/read_receipts") {
|
||||
// Special handling: don't add this key if it's new. Do overwrite if already present.
|
||||
if (key in actual) {
|
||||
} else if (key === "m.room.create/") {
|
||||
// can't be modified - only for kstateToCreationContent
|
||||
|
||||
} else if (key === "m.room.topic/") {
|
||||
// synapse generates different m.room.topic events on original creation
|
||||
// https://github.com/element-hq/synapse/blob/0f2b29511fd88d1dc2278f41fd6e4e2f2989fcb7/synapse/handlers/room.py#L1729
|
||||
// diff the `topic` to determine change
|
||||
if (!(key in actual) || actual[key].topic !== target[key].topic) {
|
||||
diff[key] = target[key]
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +159,7 @@ async function applyKStateDiffToRoom(roomID, kstate) {
|
|||
module.exports.kstateStripConditionals = kstateStripConditionals
|
||||
module.exports.kstateUploadMxc = kstateUploadMxc
|
||||
module.exports.kstateToState = kstateToState
|
||||
module.exports.kstateToCreationContent = kstateToCreationContent
|
||||
module.exports.stateToKState = stateToKState
|
||||
module.exports.diffKState = diffKState
|
||||
module.exports.roomToKState = roomToKState
|
||||
|
|
|
|||
|
|
@ -235,30 +235,38 @@ test("diffKState: kstate keys must contain a slash separator", t => {
|
|||
t.pass()
|
||||
})
|
||||
|
||||
test("diffKState: don't add hide_ui when not present", t => {
|
||||
test("diffKState: detects new properties", t => {
|
||||
t.deepEqual(
|
||||
diffKState({
|
||||
}, {
|
||||
"chat.schildi.hide_ui/read_receipts/": {}
|
||||
}),
|
||||
{
|
||||
test("diffKState: topic does not change if the topic key has not changed", t => {
|
||||
t.deepEqual(diffKState({
|
||||
"m.room.topic/": {
|
||||
topic: "hello",
|
||||
"m.topic": {
|
||||
"m.text": "hello"
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}, {
|
||||
"m.room.topic/": {
|
||||
topic: "hello"
|
||||
}
|
||||
}),
|
||||
{})
|
||||
})
|
||||
|
||||
test("diffKState: overwriten hide_ui when present", t => {
|
||||
test("diffKState: detects new properties", t => {
|
||||
t.deepEqual(
|
||||
diffKState({
|
||||
"chat.schildi.hide_ui/read_receipts/": {hidden: true}
|
||||
test("diffKState: topic changes if the topic key has changed", t => {
|
||||
t.deepEqual(diffKState({
|
||||
"m.room.topic/": {
|
||||
topic: "hello",
|
||||
"m.topic": {
|
||||
"m.text": "hello"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"chat.schildi.hide_ui/read_receipts/": {}
|
||||
"m.room.topic/": {
|
||||
topic: "hello you"
|
||||
}
|
||||
}),
|
||||
{
|
||||
"chat.schildi.hide_ui/read_receipts/": {}
|
||||
"m.room.topic/": {
|
||||
topic: "hello you"
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -123,12 +123,9 @@ const commands = [{
|
|||
}
|
||||
if (matrixOnlyReason) {
|
||||
// If uploading to Matrix, check if we have permission
|
||||
const state = await api.getAllState(event.room_id)
|
||||
const kstate = ks.stateToKState(state)
|
||||
const powerLevels = kstate["m.room.power_levels/"]
|
||||
const required = powerLevels.events["im.ponies.room_emotes"] ?? powerLevels.state_default ?? 50
|
||||
const have = powerLevels.users[`@${reg.sender_localpart}:${reg.ooye.server_name}`] ?? powerLevels.users_default ?? 0
|
||||
if (have < required) {
|
||||
const {powerLevels, powers: {[mxUtils.bot]: botPower}} = await mxUtils.getEffectivePower(event.room_id, [mxUtils.bot], api)
|
||||
const requiredPower = powerLevels.events["im.ponies.room_emotes"] ?? powerLevels.state_default ?? 50
|
||||
if (botPower < requiredPower) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
|
|
|
|||
|
|
@ -72,8 +72,13 @@ async function mreq(method, url, bodyIn, extra = {}) {
|
|||
}, extra)
|
||||
|
||||
const res = await fetch(baseUrl + url, opts)
|
||||
const text = await res.text()
|
||||
try {
|
||||
/** @type {any} */
|
||||
const root = await res.json()
|
||||
var root = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new MatrixServerError(text, {baseUrl, url, ...opts})
|
||||
}
|
||||
|
||||
if (!res.ok || root.errcode) {
|
||||
delete opts.headers?.["Authorization"]
|
||||
|
|
|
|||
94
src/matrix/room-upgrade.js
Normal file
94
src/matrix/room-upgrade.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert/strict")
|
||||
const Ty = require("../types")
|
||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||
const {discord, db, sync, as, select, from} = require("../passthrough")
|
||||
|
||||
/** @type {import("./api")}) */
|
||||
const api = sync.require("./api")
|
||||
/** @type {import("../d2m/actions/create-room")}) */
|
||||
const createRoom = sync.require("../d2m/actions/create-room")
|
||||
/** @type {import("../m2d/converters/utils")}) */
|
||||
const utils = sync.require("../m2d/converters/utils")
|
||||
|
||||
const roomUpgradeSema = new Semaphore()
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
||||
*/
|
||||
async function onTombstone(event) {
|
||||
// Validate
|
||||
if (event.state_key !== "") return
|
||||
if (!event.content.replacement_room) return
|
||||
|
||||
// Set up
|
||||
const oldRoomID = event.room_id
|
||||
const newRoomID = event.content.replacement_room
|
||||
const channel = select("channel_room", ["name", "channel_id"], {room_id: oldRoomID}).get()
|
||||
if (!channel) return
|
||||
db.prepare("REPLACE INTO room_upgrade_pending (new_room_id, old_room_id) VALUES (?, ?)").run(newRoomID, oldRoomID)
|
||||
|
||||
// Try joining
|
||||
try {
|
||||
await api.joinRoom(newRoomID)
|
||||
} catch (e) {
|
||||
const message = new utils.MatrixStringBuilder()
|
||||
message.add(
|
||||
`You upgraded the bridged room ${channel.name}. To keep bridging, I need you to invite me to the new room: https://matrix.to/#/${newRoomID}`,
|
||||
tag`You upgraded the bridged room <strong>${channel.name}</strong>. To keep bridging, I need you to invite me to the new room: <a href="https://matrix.to/#/${newRoomID}">https://matrix.to/#/${newRoomID}</a>`
|
||||
)
|
||||
const privateRoomID = await api.usePrivateChat(event.sender)
|
||||
await api.sendEvent(privateRoomID, "m.room.message", message.get())
|
||||
}
|
||||
|
||||
// Now wait to be invited to/join the room that has the upgrade pending...
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Member>} event
|
||||
* @returns {Promise<boolean>} whether to cancel other membership actions
|
||||
*/
|
||||
async function onBotMembership(event) {
|
||||
// Check if an upgrade is pending for this room
|
||||
const newRoomID = event.room_id
|
||||
const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
|
||||
if (!oldRoomID) return
|
||||
|
||||
// Check if is join/invite
|
||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return
|
||||
|
||||
return await roomUpgradeSema.request(async () => {
|
||||
// If invited, join
|
||||
if (event.content.membership === "invite") {
|
||||
await api.joinRoom(newRoomID)
|
||||
}
|
||||
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow)
|
||||
|
||||
// Remove old room from space
|
||||
await api.sendState(channelRow.space_id, "m.space.child", oldRoomID, {})
|
||||
// await api.sendState(oldRoomID, "m.space.parent", spaceID, {}) // keep this - the room isn't advertised but should still be grouped if opened
|
||||
|
||||
// Remove declaration that old room is bridged (if able)
|
||||
try {
|
||||
await api.sendState(oldRoomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${channelRow.guild_id}/${channelRow.channel_id}`, {})
|
||||
} catch (e) {}
|
||||
|
||||
// Update database
|
||||
db.transaction(() => {
|
||||
db.prepare("DELETE FROM room_upgrade_pending WHERE new_room_id = ?").run(newRoomID)
|
||||
db.prepare("UPDATE channel_room SET room_id = ? WHERE channel_id = ?").run(newRoomID, channelRow.channel_id)
|
||||
db.prepare("INSERT INTO historical_channel_room (room_id, reference_channel_id, upgraded_timestamp) VALUES (?, ?, ?)").run(newRoomID, channelRow.channel_id, Date.now())
|
||||
})()
|
||||
|
||||
// Sync
|
||||
await createRoom.syncRoom(channelRow.channel_id)
|
||||
return true
|
||||
}, event.room_id)
|
||||
}
|
||||
|
||||
module.exports.onTombstone = onTombstone
|
||||
module.exports.onBotMembership = onBotMembership
|
||||
22
src/types.d.ts
vendored
22
src/types.d.ts
vendored
|
|
@ -143,21 +143,6 @@ export namespace Event {
|
|||
}
|
||||
}
|
||||
|
||||
export type BaseStateEvent = {
|
||||
type: string
|
||||
room_id: string
|
||||
sender: string
|
||||
content: any
|
||||
state_key: string
|
||||
origin_server_ts: number
|
||||
unsigned?: any
|
||||
event_id: string
|
||||
user_id: string
|
||||
age: number
|
||||
replaces_state: string
|
||||
prev_content?: any
|
||||
}
|
||||
|
||||
export type StrippedChildStateEvent = {
|
||||
type: string
|
||||
state_key: string
|
||||
|
|
@ -174,7 +159,7 @@ export namespace Event {
|
|||
}
|
||||
|
||||
export type M_Room_Create = {
|
||||
additional_creators: string[]
|
||||
additional_creators?: string[]
|
||||
"m.federate"?: boolean
|
||||
room_version: string
|
||||
type?: string
|
||||
|
|
@ -356,6 +341,11 @@ export namespace Event {
|
|||
}> & {
|
||||
redacts: string
|
||||
}
|
||||
|
||||
export type M_Room_Tombstone = {
|
||||
body: string
|
||||
replacement_room: string
|
||||
}
|
||||
}
|
||||
|
||||
export namespace R {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ const {discord, db, as, sync, select, from} = require("../../passthrough")
|
|||
const auth = sync.require("../auth")
|
||||
/** @type {import("../../matrix/mreq")} */
|
||||
const mreq = sync.require("../../matrix/mreq")
|
||||
/** @type {import("../../m2d/converters/utils")}*/
|
||||
const utils = sync.require("../../m2d/converters/utils")
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
|
||||
/**
|
||||
|
|
@ -87,18 +89,11 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
|
|||
}
|
||||
|
||||
// Check bridge has PL 100
|
||||
const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
/** @type {Ty.Event.M_Power_Levels?} */
|
||||
let powerLevelsStateContent = null
|
||||
try {
|
||||
powerLevelsStateContent = await api.getStateEvent(spaceID, "m.room.power_levels", "")
|
||||
} catch (e) {}
|
||||
const selfPowerLevel = powerLevelsStateContent?.users?.[me] ?? powerLevelsStateContent?.users_default ?? 0
|
||||
if (selfPowerLevel < (powerLevelsStateContent?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix space"})
|
||||
const {powerLevels, powers: {[utils.bot]: selfPowerLevel, [session.data.mxid]: invitingPowerLevel}} = await utils.getEffectivePower(spaceID, [utils.bot, session.data.mxid], api)
|
||||
if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix space"})
|
||||
|
||||
// Check inviting user is a moderator in the space
|
||||
const invitingPowerLevel = powerLevelsStateContent?.users?.[session.data.mxid] ?? powerLevelsStateContent?.users_default ?? 0
|
||||
if (invitingPowerLevel < (powerLevelsStateContent?.state_default ?? 50)) throw createError({status: 403, message: "Forbidden", data: `You need to be at least power level 50 (moderator) in the target Matrix space to set up OOYE, but you are currently power level ${invitingPowerLevel}.`})
|
||||
if (invitingPowerLevel < (powerLevels?.state_default ?? 50)) throw createError({status: 403, message: "Forbidden", data: `You need to be at least power level 50 (moderator) in the target Matrix space to set up OOYE, but you are currently power level ${invitingPowerLevel}.`})
|
||||
|
||||
// Insert database entry
|
||||
db.transaction(() => {
|
||||
|
|
@ -169,14 +164,8 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
|||
}
|
||||
|
||||
// Check bridge has PL 100
|
||||
const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
/** @type {Ty.Event.M_Power_Levels?} */
|
||||
let powerLevelsStateContent = null
|
||||
try {
|
||||
powerLevelsStateContent = await api.getStateEvent(parsedBody.matrix, "m.room.power_levels", "")
|
||||
} catch (e) {}
|
||||
const selfPowerLevel = powerLevelsStateContent?.users?.[me] ?? powerLevelsStateContent?.users_default ?? 0
|
||||
if (selfPowerLevel < (powerLevelsStateContent?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"})
|
||||
const {powerLevels, powers: {[utils.bot]: selfPowerLevel}} = await utils.getEffectivePower(parsedBody.matrix, [utils.bot], api)
|
||||
if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"})
|
||||
|
||||
// Insert database entry, but keep the room's existing properties if they are set
|
||||
const nick = await api.getStateEvent(parsedBody.matrix, "m.room.name", "").then(content => content.name || null).catch(() => null)
|
||||
|
|
|
|||
|
|
@ -81,63 +81,6 @@ test("web link space: check that OOYE is joined", async t => {
|
|||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("web link space: check that OOYE has PL 100 (not missing)", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link-space", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"],
|
||||
mxid: "@cadence:cadence.moe"
|
||||
},
|
||||
body: {
|
||||
space_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
|
||||
guild_id: "665289423482519565"
|
||||
},
|
||||
api: {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
return roomID
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "what if I told you that power levels never existed"})
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix space")
|
||||
t.equal(called, 2)
|
||||
})
|
||||
|
||||
test("web link space: check that OOYE has PL 100 (not users_default)", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link-space", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"],
|
||||
mxid: "@cadence:cadence.moe"
|
||||
},
|
||||
body: {
|
||||
space_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
|
||||
guild_id: "665289423482519565"
|
||||
},
|
||||
api: {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
return roomID
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix space")
|
||||
t.equal(called, 2)
|
||||
})
|
||||
|
||||
test("web link space: check that OOYE has PL 100 (not 50)", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link-space", {
|
||||
|
|
@ -160,11 +103,28 @@ test("web link space: check that OOYE has PL 100 (not 50)", async t => {
|
|||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@_ooye_bot:cadence.moe": 50}}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@creator:cadence.moe",
|
||||
room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix space")
|
||||
t.equal(called, 2)
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
test("web link space: check that inviting user has PL 50", async t => {
|
||||
|
|
@ -189,11 +149,28 @@ test("web link space: check that inviting user has PL 50", async t => {
|
|||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@_ooye_bot:cadence.moe": 100}}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@creator:cadence.moe",
|
||||
room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "You need to be at least power level 50 (moderator) in the target Matrix space to set up OOYE, but you are currently power level 0.")
|
||||
t.equal(called, 2)
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
test("web link space: successfully adds entry to database and loads page", async t => {
|
||||
|
|
@ -218,10 +195,27 @@ test("web link space: successfully adds entry to database and loads page", async
|
|||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@_ooye_bot:cadence.moe": 100, "@cadence:cadence.moe": 50}}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@creator:cadence.moe",
|
||||
room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(called, 2)
|
||||
t.equal(called, 3)
|
||||
|
||||
// check that the entry was added to the database
|
||||
t.equal(select("guild_space", "privacy_level", {guild_id: "665289423482519565", space_id: "!zTMspHVUBhFLLSdmnS:cadence.moe"}).pluck().get(), 0)
|
||||
|
|
@ -441,47 +435,7 @@ test("web link room: check that bridge can join room (uses via for join attempt)
|
|||
t.equal(called, 2)
|
||||
})
|
||||
|
||||
test("web link room: check that bridge has PL 100 in target room (event missing)", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"]
|
||||
},
|
||||
body: {
|
||||
discord: "665310973967597573",
|
||||
matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
guild_id: "665289423482519565"
|
||||
},
|
||||
api: {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
return roomID
|
||||
},
|
||||
async *generateFullHierarchy(spaceID) {
|
||||
called++
|
||||
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
yield {
|
||||
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
children_state: [],
|
||||
guest_can_join: false,
|
||||
num_joined_members: 2
|
||||
}
|
||||
/* c8 ignore next */
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "what if I told you there's no such thing as power levels"})
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
test("web link room: check that bridge has PL 100 in target room (users default)", async t => {
|
||||
test("web link room: check that bridge has PL 100 in target room", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
|
||||
sessionData: {
|
||||
|
|
@ -514,11 +468,28 @@ test("web link room: check that bridge has PL 100 in target room (users default)
|
|||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users_default: 50}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@creator:cadence.moe",
|
||||
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
|
||||
t.equal(called, 3)
|
||||
t.equal(called, 4)
|
||||
})
|
||||
|
||||
test("web link room: successfully calls createRoom", async t => {
|
||||
|
|
@ -568,6 +539,23 @@ test("web link room: successfully calls createRoom", async t => {
|
|||
return {}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@creator:cadence.moe",
|
||||
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
},
|
||||
async sendEvent(roomID, type, content) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
|
|
@ -584,7 +572,7 @@ test("web link room: successfully calls createRoom", async t => {
|
|||
}
|
||||
}
|
||||
})
|
||||
t.equal(called, 8)
|
||||
t.equal(called, 9)
|
||||
})
|
||||
|
||||
// *****
|
||||
|
|
|
|||
|
|
@ -79,30 +79,7 @@ as.router.post("/api/log-in-with-matrix", defineEventHandler(async event => {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if we have an existing DM
|
||||
let roomID = select("direct", "room_id", {mxid}).pluck().get()
|
||||
if (roomID) {
|
||||
// Check that the person is/still in the room
|
||||
try {
|
||||
var member = await api.getStateEvent(roomID, "m.room.member", mxid)
|
||||
} catch (e) {}
|
||||
|
||||
// Invite them back to the room if needed
|
||||
if (!member || member.membership === "leave") {
|
||||
await api.inviteToRoom(roomID, mxid)
|
||||
}
|
||||
}
|
||||
|
||||
// No existing DM, create a new room and invite
|
||||
else {
|
||||
roomID = await api.createRoom({
|
||||
invite: [mxid],
|
||||
is_direct: true,
|
||||
preset: "trusted_private_chat"
|
||||
})
|
||||
// Store the newly created room in account data (Matrix doesn't do this for us automatically, sigh...)
|
||||
db.prepare("REPLACE INTO direct (mxid, room_id) VALUES (?, ?)").run(mxid, roomID)
|
||||
}
|
||||
const roomID = await api.usePrivateChat(mxid)
|
||||
|
||||
const token = randomUUID()
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ test("log in with matrix: checks if mxid domain format looks valid", async t =>
|
|||
t.match(error.data.fieldErrors.mxid, /must match pattern/)
|
||||
})
|
||||
|
||||
test("log in with matrix: sends message when there is no existing dm room", async t => {
|
||||
test("log in with matrix: sends message to log in", async t => {
|
||||
const event = {}
|
||||
let called = 0
|
||||
await router.test("post", "/api/log-in-with-matrix", {
|
||||
|
|
@ -42,8 +42,9 @@ test("log in with matrix: sends message when there is no existing dm room", asyn
|
|||
mxid: "@cadence:cadence.moe"
|
||||
},
|
||||
api: {
|
||||
async createRoom() {
|
||||
async usePrivateChat(mxid) {
|
||||
called++
|
||||
t.equal(mxid, "@cadence:cadence.moe")
|
||||
return "!created:cadence.moe"
|
||||
},
|
||||
async sendEvent(roomID, type, content) {
|
||||
|
|
@ -72,65 +73,6 @@ test("log in with matrix: does not send another message when a log in is in prog
|
|||
t.match(event.node.res.getHeader("location"), /We already sent you a link on Matrix/)
|
||||
})
|
||||
|
||||
test("log in with matrix: reuses room from direct", async t => {
|
||||
const event = {}
|
||||
let called = 0
|
||||
await router.test("post", "/api/log-in-with-matrix", {
|
||||
body: {
|
||||
mxid: "@user1:example.org"
|
||||
},
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!existing:cadence.moe")
|
||||
t.equal(type, "m.room.member")
|
||||
t.equal(key, "@user1:example.org")
|
||||
return {membership: "join"}
|
||||
},
|
||||
async sendEvent(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!existing:cadence.moe")
|
||||
return ""
|
||||
}
|
||||
},
|
||||
event
|
||||
})
|
||||
t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/)
|
||||
t.equal(called, 2)
|
||||
})
|
||||
|
||||
test("log in with matrix: reuses room from direct, reinviting if user has left", async t => {
|
||||
const event = {}
|
||||
let called = 0
|
||||
await router.test("post", "/api/log-in-with-matrix", {
|
||||
body: {
|
||||
mxid: "@user2:example.org"
|
||||
},
|
||||
api: {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!existing:cadence.moe")
|
||||
t.equal(type, "m.room.member")
|
||||
t.equal(key, "@user2:example.org")
|
||||
throw new MatrixServerError({errcode: "M_NOT_FOUND"})
|
||||
},
|
||||
async inviteToRoom(roomID, mxid) {
|
||||
called++
|
||||
t.equal(roomID, "!existing:cadence.moe")
|
||||
t.equal(mxid, "@user2:example.org")
|
||||
},
|
||||
async sendEvent(roomID) {
|
||||
called++
|
||||
t.equal(roomID, "!existing:cadence.moe")
|
||||
return ""
|
||||
}
|
||||
},
|
||||
event
|
||||
})
|
||||
t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/)
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
// ***** third request *****
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ module.exports = {
|
|||
},
|
||||
room: {
|
||||
general: {
|
||||
"m.room.create/": {additional_creators: ["@test_auto_invite:example.org"]},
|
||||
"m.room.name/": {name: "main"},
|
||||
"m.room.topic/": {topic: "#collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"},
|
||||
"m.room.guest_access/": {guest_access: "can_join"},
|
||||
|
|
@ -126,13 +127,12 @@ module.exports = {
|
|||
"m.room.redaction": 0
|
||||
},
|
||||
users: {
|
||||
"@test_auto_invite:example.org": 100
|
||||
"@test_auto_invite:example.org": 150
|
||||
},
|
||||
notifications: {
|
||||
room: 0
|
||||
}
|
||||
},
|
||||
"chat.schildi.hide_ui/read_receipts": {},
|
||||
"uk.half-shot.bridge/moe.cadence.ooye://discord/112760669178241024/112760669178241024": {
|
||||
bridgebot: "@_ooye_bot:cadence.moe",
|
||||
protocol: {
|
||||
|
|
|
|||
|
|
@ -8,20 +8,20 @@ INSERT INTO guild_active (guild_id, autocreate) VALUES
|
|||
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES
|
||||
('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0);
|
||||
|
||||
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES
|
||||
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL),
|
||||
('687028734322147344', '!fGgIymcYWOqjbSRUdV:cadence.moe', 'slow-news-day', NULL, NULL, NULL),
|
||||
('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL),
|
||||
('160197704226439168', '!hYnGGlPHlbujVVfktC:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL),
|
||||
('1100319550446252084', '!BnKuBPCvyfOkhcUjEu:cadence.moe', 'worm-farm', NULL, NULL, NULL),
|
||||
('1162005314908999790', '!FuDZhlOAtqswlyxzeR:cadence.moe', 'Hey.', NULL, '1100319550446252084', NULL),
|
||||
('297272183716052993', '!rEOspnYqdOalaIFniV:cadence.moe', 'general', NULL, NULL, NULL),
|
||||
('122155380120748034', '!cqeGDbPiMFAhLsqqqq:cadence.moe', 'cadences-mind', 'coding', NULL, NULL),
|
||||
('176333891320283136', '!qzDBLKlildpzrrOnFZ:cadence.moe', '🌈丨davids-horse_she-took-the-kids', 'wonderland', NULL, 'mxc://cadence.moe/EVvrSkKIRONHjtRJsMLmHWLS'),
|
||||
('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL),
|
||||
('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL),
|
||||
('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL),
|
||||
('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL);
|
||||
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, guild_id) VALUES
|
||||
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL, '112760669178241024'),
|
||||
('687028734322147344', '!fGgIymcYWOqjbSRUdV:cadence.moe', 'slow-news-day', NULL, NULL, NULL, '112760669178241024'),
|
||||
('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL, '66192955777486848'),
|
||||
('160197704226439168', '!hYnGGlPHlbujVVfktC:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL, '112760669178241024'),
|
||||
('1100319550446252084', '!BnKuBPCvyfOkhcUjEu:cadence.moe', 'worm-farm', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1162005314908999790', '!FuDZhlOAtqswlyxzeR:cadence.moe', 'Hey.', NULL, '1100319550446252084', NULL, '112760669178241024'),
|
||||
('297272183716052993', '!rEOspnYqdOalaIFniV:cadence.moe', 'general', NULL, NULL, NULL, '66192955777486848'),
|
||||
('122155380120748034', '!cqeGDbPiMFAhLsqqqq:cadence.moe', 'cadences-mind', 'coding', NULL, NULL, '112760669178241024'),
|
||||
('176333891320283136', '!qzDBLKlildpzrrOnFZ:cadence.moe', '🌈丨davids-horse_she-took-the-kids', 'wonderland', NULL, 'mxc://cadence.moe/EVvrSkKIRONHjtRJsMLmHWLS', '112760669178241024'),
|
||||
('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '665289423482519565'),
|
||||
('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL, '665289423482519565');
|
||||
|
||||
INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) SELECT channel_id, room_id, 0 FROM channel_room;
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ INSERT INTO reaction (hashed_event_id, message_id, encoded_emoji) VALUES
|
|||
(5162930312280790092, '1141501302736695317', '%F0%9F%90%88');
|
||||
|
||||
INSERT INTO member_power (mxid, room_id, power_level) VALUES
|
||||
('@test_auto_invite:example.org', '*', 100);
|
||||
('@test_auto_invite:example.org', '*', 150);
|
||||
|
||||
INSERT INTO lottie (sticker_id, mxc_url) VALUES
|
||||
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue