Compare commits
7 commits
main
...
mergable-f
| Author | SHA1 | Date | |
|---|---|---|---|
| 748e851b39 | |||
| b90592cbe9 | |||
| 5a0e7f6a66 | |||
| e146faced1 | |||
| aedd30ab4a | |||
| 2c7831c587 | |||
| ae6b730c26 |
83 changed files with 1205 additions and 3066 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,17 +1,18 @@
|
|||
# Secrets
|
||||
# Personal
|
||||
config.js
|
||||
registration.yaml
|
||||
ooye.db*
|
||||
events.db*
|
||||
backfill.db*
|
||||
custom-webroot
|
||||
icon.svg
|
||||
.devcontainer
|
||||
|
||||
# Automatically generated
|
||||
node_modules
|
||||
coverage
|
||||
test/res/*
|
||||
!test/res/lottie*
|
||||
icon.svg
|
||||
*~
|
||||
.#*
|
||||
\#*#
|
||||
|
|
|
|||
|
|
@ -89,14 +89,15 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin
|
|||
|
||||
# Dependency justification
|
||||
|
||||
Total transitive production dependencies: 144
|
||||
Total transitive production dependencies: 134
|
||||
|
||||
### <font size="+2">🦕</font>
|
||||
|
||||
* (35) better-sqlite3: SQLite is the best database, and this is the best library for it.
|
||||
* (29) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. It has libvips prebuilts for each platform.
|
||||
* (26) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.)
|
||||
* (9) h3: Web server. OOYE needs this for the web UI, appservice listener, authmedia proxy, and more.
|
||||
* (31) better-sqlite3: SQLite is the best database, and this is the best library for it.
|
||||
* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.)
|
||||
* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only.
|
||||
* (9) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, self-service, and more.
|
||||
* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets.
|
||||
|
||||
### <font size="-1">🪱</font>
|
||||
|
||||
|
|
@ -107,7 +108,6 @@ Total transitive production dependencies: 144
|
|||
* (0) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It depends on h3 and zod, which are already pulled in by OOYE.
|
||||
* (0) @cloudrac3r/mixin-deep: This is my fork. (It fixes a bug in regular mixin-deep.)
|
||||
* (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs.
|
||||
* (0) @cloudrac3r/stream-type: Determine type of Matrix files that don't specify it in info. Switched from stream-mime-type to this.
|
||||
* (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works.
|
||||
* (3) @stackoverflow/stacks: Stack Overflow design language and icons.
|
||||
* (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer.
|
||||
|
|
@ -115,12 +115,12 @@ Total transitive production dependencies: 144
|
|||
* (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust.
|
||||
* (0) discord-api-types: Bitfields needed at runtime and types needed for development.
|
||||
* (0) domino: DOM implementation that's already pulled in by turndown.
|
||||
* (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
||||
* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
||||
* (0) entities: Looks fine. No dependencies.
|
||||
* (0) get-relative-path: Looks fine. No dependencies.
|
||||
* (1) heatsync: Module hot-reloader that I trust.
|
||||
* (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used.
|
||||
* (1) mime-types: List of mime type mappings. Needed to serve static files.
|
||||
* (0) mime-type: File extension to mime type mapping that's already pulled in by stream-mime-type.
|
||||
* (0) prettier-bytes: It does what I want and has no dependencies.
|
||||
* (0) snowtransfer: Discord API library with bring-your-own-caching that I trust.
|
||||
* (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well.
|
||||
|
|
|
|||
9
docs/threads-as-rooms.md
Normal file
9
docs/threads-as-rooms.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
I thought pretty hard about it and I opted to make threads separate rooms because
|
||||
|
||||
1. parity: discord has separate things like permissions and pins for threads, matrix cannot do this at all unless the thread is a separate room
|
||||
2. usage styles: most discord threads I've seen tend to be long-lived, spanning months or years, which isn't suited to matrix because of the timeline
|
||||
- I'm in a discord thread for posting photos of food that gets a couple posts a week and has a timeline going back to 2023
|
||||
3. the timeline: if a matrix room has threads, and you want to scroll back through the timeline of a room OR of one of its threads, the timeline is merged, so you have to download every message linearised and throw them away if they aren't part of the thread you're looking through. it's bad for threads and it's bad for the main room
|
||||
4. it is also very very complex for clients to implement read receipts and typing indicators correctly for the merged timeline. if your client doesn't implement this, or doesn't do it correctly, you have a bad experience. many clients don't. element seems to have done it well enough, but is an exception
|
||||
|
||||
overall in my view, threads-as-rooms has better parity and fewer downsides over native threads. but if there are things you don't like about this approach, I'm happy to discuss and see if we can improve them.
|
||||
1117
package-lock.json
generated
1117
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.5.0",
|
||||
"version": "3.4.0",
|
||||
"description": "A bridge between Matrix and Discord",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
|
@ -19,35 +19,35 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||
"@cloudrac3r/discord-markdown": "^2.7.0",
|
||||
"@cloudrac3r/discord-markdown": "^2.6.10",
|
||||
"@cloudrac3r/giframe": "^0.4.3",
|
||||
"@cloudrac3r/html-template-tag": "^5.0.1",
|
||||
"@cloudrac3r/in-your-element": "^1.1.1",
|
||||
"@cloudrac3r/mixin-deep": "^3.0.1",
|
||||
"@cloudrac3r/pngjs": "^7.0.3",
|
||||
"@cloudrac3r/pug": "^4.0.4",
|
||||
"@cloudrac3r/stream-type": "^1.0.0",
|
||||
"@cloudrac3r/turndown": "^7.1.4",
|
||||
"@stackoverflow/stacks": "^2.5.4",
|
||||
"@stackoverflow/stacks-icons": "^6.0.2",
|
||||
"ansi-colors": "^4.1.3",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.17.0",
|
||||
"cloudstorm": "^0.15.2",
|
||||
"discord-api-types": "^0.38.38",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
"entities": "^5.0.0",
|
||||
"get-relative-path": "^1.0.2",
|
||||
"h3": "^1.15.10",
|
||||
"h3": "^1.15.1",
|
||||
"heatsync": "^2.7.2",
|
||||
"htmx.org": "^2.0.4",
|
||||
"lru-cache": "^11.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"prettier-bytes": "^1.0.4",
|
||||
"sharp": "^0.34.5",
|
||||
"snowtransfer": "^0.17.5",
|
||||
"try-to-catch": "^4.0.5",
|
||||
"snowtransfer": "^0.17.1",
|
||||
"stream-mime-type": "^1.0.2",
|
||||
"try-to-catch": "^3.0.1",
|
||||
"uqr": "^0.1.2",
|
||||
"xxhash-wasm": "^1.0.2",
|
||||
"zod": "^4.0.17"
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
"devDependencies": {
|
||||
"@cloudrac3r/tap-dot": "^2.0.3",
|
||||
"@types/node": "^22.17.1",
|
||||
"c8": "^11.0.0",
|
||||
"c8": "^10.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"supertape": "^12.0.12"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ if (!channelID) {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
const assert = require("assert/strict")
|
||||
const sqlite = require("better-sqlite3")
|
||||
const backfill = new sqlite("scripts/backfill.db")
|
||||
backfill.prepare("CREATE TABLE IF NOT EXISTS backfill (channel_id TEXT NOT NULL, message_id INTEGER NOT NULL, PRIMARY KEY (channel_id, message_id))").run()
|
||||
|
|
@ -37,8 +38,12 @@ passthrough.select = orm.select
|
|||
|
||||
/** @type {import("../src/d2m/event-dispatcher")}*/
|
||||
const eventDispatcher = sync.require("../src/d2m/event-dispatcher")
|
||||
/** @type {import("../src/d2m/actions/create-room")} */
|
||||
const createRoom = sync.require("../src/d2m/actions/create-room")
|
||||
|
||||
const roomID = passthrough.select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
||||
if (!roomID) {
|
||||
console.error("Please choose a channel that's already bridged.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
await discord.cloud.connect()
|
||||
|
|
@ -55,29 +60,23 @@ async function event(event) {
|
|||
if (!channel) return
|
||||
const guild_id = event.d.id
|
||||
|
||||
try {
|
||||
await createRoom.syncRoom(channelID)
|
||||
let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0"
|
||||
console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`)
|
||||
let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0"
|
||||
console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`)
|
||||
|
||||
while (last) {
|
||||
const messages = await discord.snow.channel.getChannelMessages(channelID, {limit: 50, after: String(last)})
|
||||
messages.reverse() // More recent messages come first -> More recent messages come last
|
||||
for (const message of messages) {
|
||||
const simulatedGatewayDispatchData = {
|
||||
guild_id,
|
||||
backfill: true,
|
||||
...message
|
||||
}
|
||||
await eventDispatcher.MESSAGE_CREATE(discord, simulatedGatewayDispatchData)
|
||||
preparedInsert.run(channelID, message.id)
|
||||
while (last) {
|
||||
const messages = await discord.snow.channel.getChannelMessages(channelID, {limit: 50, after: String(last)})
|
||||
messages.reverse() // More recent messages come first -> More recent messages come last
|
||||
for (const message of messages) {
|
||||
const simulatedGatewayDispatchData = {
|
||||
guild_id,
|
||||
backfill: true,
|
||||
...message
|
||||
}
|
||||
last = messages.at(-1)?.id
|
||||
await eventDispatcher.MESSAGE_CREATE(discord, simulatedGatewayDispatchData)
|
||||
preparedInsert.run(channelID, message.id)
|
||||
}
|
||||
|
||||
process.exit()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1) // won't exit automatically on thrown error due to living discord connection, so manual exit is necessary
|
||||
last = messages.at(-1)?.id
|
||||
}
|
||||
|
||||
process.exit()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const HeatSync = require("heatsync")
|
||||
const sync = new HeatSync({watchFS: false})
|
||||
|
||||
const sqlite = require("better-sqlite3")
|
||||
const db = new sqlite("ooye.db", {fileMustExist: true})
|
||||
|
||||
const passthrough = require("../src/passthrough")
|
||||
Object.assign(passthrough, {db, sync})
|
||||
|
||||
const api = require("../src/matrix/api")
|
||||
const utils = require("../src/matrix/utils")
|
||||
const {reg} = require("../src/matrix/read-registration")
|
||||
|
||||
const rooms = db.prepare("select room_id, name, nick from channel_room").all()
|
||||
|
||||
;(async () => {
|
||||
// Search for members starting with @_ooye_ and kick them if they are not in sim_member cache
|
||||
for (const room of rooms) {
|
||||
try {
|
||||
const members = await api.getJoinedMembers(room.room_id)
|
||||
for (const mxid of Object.keys(members.joined)) {
|
||||
if (!mxid.startsWith("@" + reg.sender_localpart) && utils.eventSenderIsFromDiscord(mxid) && !db.prepare("select mxid from sim_member where mxid = ? and room_id = ?").get(mxid, room.room_id)) {
|
||||
await api.leaveRoom(room.room_id, mxid)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message.includes("Appservice not in room")) {
|
||||
// ok
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
|
@ -13,5 +13,5 @@ const {prompt} = require("enquirer")
|
|||
|
||||
reg.ooye.web_password = passwordResponse.web_password
|
||||
writeRegistration(reg)
|
||||
console.log("Saved. This change should be applied instantly.")
|
||||
console.log("Saved. Restart Out Of Your Element to apply this change.")
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -1,333 +0,0 @@
|
|||
/*
|
||||
---
|
||||
elizabot.js v.1.1 - ELIZA JS library (N.Landsteiner 2005)
|
||||
https://www.masswerk.at/elizabot/
|
||||
Free Software © Norbert Landsteiner 2005
|
||||
---
|
||||
Modified by Cadence Ember in 2025 for v1.2 (unofficial)
|
||||
* Changed to class structure
|
||||
* Load from local file and instance instead of global variables
|
||||
* Remove memory
|
||||
* Remove xnone
|
||||
* Remove initials
|
||||
* Remove finals
|
||||
* Allow substitutions in rule keys
|
||||
---
|
||||
|
||||
Eliza is a mock Rogerian psychotherapist.
|
||||
Original program by Joseph Weizenbaum in MAD-SLIP for "Project MAC" at MIT.
|
||||
cf: Weizenbaum, Joseph "ELIZA - A Computer Program For the Study of Natural Language
|
||||
Communication Between Man and Machine"
|
||||
in: Communications of the ACM; Volume 9 , Issue 1 (January 1966): p 36-45.
|
||||
JavaScript implementation by Norbert Landsteiner 2005; <http://www.masserk.at>
|
||||
|
||||
synopsis:
|
||||
new ElizaBot( <random-choice-disable-flag> )
|
||||
ElizaBot.prototype.transform( <inputstring> )
|
||||
ElizaBot.prototype.reset()
|
||||
|
||||
usage:
|
||||
var eliza = new ElizaBot();
|
||||
var reply = eliza.transform(inputstring);
|
||||
|
||||
// to reproduce the example conversation given by J. Weizenbaum
|
||||
// initialize with the optional random-choice-disable flag
|
||||
var originalEliza = new ElizaBot(true);
|
||||
|
||||
`ElizaBot' is also a general chatbot engine that can be supplied with any rule set.
|
||||
(for required data structures cf. "elizadata.js" and/or see the documentation.)
|
||||
data is parsed and transformed for internal use at the creation time of the
|
||||
first instance of the `ElizaBot' constructor.
|
||||
|
||||
vers 1.1: lambda functions in RegExps are currently a problem with too many browsers.
|
||||
changed code to work around.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {sync} = passthrough
|
||||
|
||||
/** @type {import("./elizadata")} */
|
||||
const data = sync.require("./elizadata")
|
||||
|
||||
class ElizaBot {
|
||||
/** @type {any} */
|
||||
elizaKeywords = [['###',0,[['###',[]]]]];
|
||||
pres={};
|
||||
preExp = /####/;
|
||||
posts={};
|
||||
postExp = /####/;
|
||||
|
||||
/**
|
||||
* @param {boolean} noRandomFlag
|
||||
*/
|
||||
constructor(noRandomFlag) {
|
||||
this.noRandom= !!noRandomFlag;
|
||||
this.capitalizeFirstLetter=true;
|
||||
this.debug=false;
|
||||
this.version="1.2";
|
||||
this._init();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.lastchoice=[];
|
||||
for (let k=0; k<data.elizaKeywords.length; k++) {
|
||||
this.lastchoice[k]=[];
|
||||
var rules=data.elizaKeywords[k][2];
|
||||
for (let i=0; i<rules.length; i++) this.lastchoice[k][i]=-1;
|
||||
}
|
||||
}
|
||||
|
||||
_init() {
|
||||
// parse data and convert it from canonical form to internal use
|
||||
// prodoce synonym list
|
||||
var synPatterns={};
|
||||
if ((data.elizaSynons) && (typeof data.elizaSynons == 'object')) {
|
||||
for (let i in data.elizaSynons) synPatterns[i]='('+i+'|'+data.elizaSynons[i].join('|')+')';
|
||||
}
|
||||
// check for keywords or install empty structure to prevent any errors
|
||||
if (data.elizaKeywords) this.elizaKeywords = structuredClone(data.elizaKeywords)
|
||||
// 1st convert rules to regexps
|
||||
// expand synonyms and insert asterisk expressions for backtracking
|
||||
var sre=/@(\S+)/;
|
||||
var are=/(\S)\s*\*\s*(\S)/;
|
||||
var are1=/^\s*\*\s*(\S)/;
|
||||
var are2=/(\S)\s*\*\s*$/;
|
||||
var are3=/^\s*\*\s*$/;
|
||||
var wsre=/\s+/g;
|
||||
for (let k=0; k<this.elizaKeywords.length; k++) {
|
||||
var m=sre.exec(this.elizaKeywords[k][0]);
|
||||
while (m) {
|
||||
var sp=(synPatterns[m[1]])? synPatterns[m[1]]:m[1];
|
||||
this.elizaKeywords[k][0]=this.elizaKeywords[k][0].substring(0,m.index)+sp+this.elizaKeywords[k][0].substring(m.index+m[0].length);
|
||||
m=sre.exec(this.elizaKeywords[k][0]);
|
||||
}
|
||||
var rules=this.elizaKeywords[k][2];
|
||||
this.elizaKeywords[k][3]=k; // save original index for sorting
|
||||
for (let i=0; i<rules.length; i++) {
|
||||
var r=rules[i];
|
||||
// check mem flag and store it as decomp's element 2
|
||||
if (r[0].charAt(0)=='$') {
|
||||
var ofs=1;
|
||||
while (r[0].charAt[ofs]==' ') ofs++;
|
||||
r[0]=r[0].substring(ofs);
|
||||
r[2]=true;
|
||||
}
|
||||
else {
|
||||
r[2]=false;
|
||||
}
|
||||
// expand synonyms (v.1.1: work around lambda function)
|
||||
var m=sre.exec(r[0]);
|
||||
while (m) {
|
||||
var sp=(synPatterns[m[1]])? synPatterns[m[1]]:m[1];
|
||||
r[0]=r[0].substring(0,m.index)+sp+r[0].substring(m.index+m[0].length);
|
||||
m=sre.exec(r[0]);
|
||||
}
|
||||
// expand asterisk expressions (v.1.1: work around lambda function)
|
||||
if (are3.test(r[0])) {
|
||||
r[0]='\\s*(.*)\\s*';
|
||||
}
|
||||
else {
|
||||
m=are.exec(r[0]);
|
||||
if (m) {
|
||||
let lp='';
|
||||
let rp=r[0];
|
||||
while (m) {
|
||||
lp+=rp.substring(0,m.index+1);
|
||||
if (m[1]!=')') lp+='\\b';
|
||||
lp+='\\s*(.*)\\s*';
|
||||
if ((m[2]!='(') && (m[2]!='\\')) lp+='\\b';
|
||||
lp+=m[2];
|
||||
rp=rp.substring(m.index+m[0].length);
|
||||
m=are.exec(rp);
|
||||
}
|
||||
r[0]=lp+rp;
|
||||
}
|
||||
m=are1.exec(r[0]);
|
||||
if (m) {
|
||||
let lp='\\s*(.*)\\s*';
|
||||
if ((m[1]!=')') && (m[1]!='\\')) lp+='\\b';
|
||||
r[0]=lp+r[0].substring(m.index-1+m[0].length);
|
||||
}
|
||||
m=are2.exec(r[0]);
|
||||
if (m) {
|
||||
let lp=r[0].substring(0,m.index+1);
|
||||
if (m[1]!='(') lp+='\\b';
|
||||
r[0]=lp+'\\s*(.*)\\s*';
|
||||
}
|
||||
}
|
||||
// expand white space
|
||||
r[0]=r[0].replace(wsre, '\\s+');
|
||||
wsre.lastIndex=0;
|
||||
}
|
||||
}
|
||||
// now sort keywords by rank (highest first)
|
||||
this.elizaKeywords.sort(this._sortKeywords);
|
||||
// and compose regexps and refs for pres and posts
|
||||
if ((data.elizaPres) && (data.elizaPres.length)) {
|
||||
var a=[];
|
||||
for (let i=0; i<data.elizaPres.length; i+=2) {
|
||||
a.push(data.elizaPres[i]);
|
||||
this.pres[data.elizaPres[i]]=data.elizaPres[i+1];
|
||||
}
|
||||
this.preExp = new RegExp('\\b('+a.join('|')+')\\b');
|
||||
}
|
||||
else {
|
||||
// default (should not match)
|
||||
this.pres['####']='####';
|
||||
}
|
||||
if ((data.elizaPosts) && (data.elizaPosts.length)) {
|
||||
var a=[];
|
||||
for (let i=0; i<data.elizaPosts.length; i+=2) {
|
||||
a.push(data.elizaPosts[i]);
|
||||
this.posts[data.elizaPosts[i]]=data.elizaPosts[i+1];
|
||||
}
|
||||
this.postExp = new RegExp('\\b('+a.join('|')+')\\b');
|
||||
}
|
||||
else {
|
||||
// default (should not match)
|
||||
this.posts['####']='####';
|
||||
}
|
||||
}
|
||||
|
||||
_sortKeywords(a,b) {
|
||||
// sort by rank
|
||||
if (a[1]>b[1]) return -1
|
||||
else if (a[1]<b[1]) return 1
|
||||
// or original index
|
||||
else if (a[3]>b[3]) return 1
|
||||
else if (a[3]<b[3]) return -1
|
||||
else return 0;
|
||||
}
|
||||
|
||||
transform(text) {
|
||||
var rpl='';
|
||||
// unify text string
|
||||
text=text.toLowerCase();
|
||||
text=text.replace(/@#\$%\^&\*\(\)_\+=~`\{\[\}\]\|:;<>\/\\\t/g, ' ');
|
||||
text=text.replace(/\s+-+\s+/g, '.');
|
||||
text=text.replace(/\s*[,\.\?!;]+\s*/g, '.');
|
||||
text=text.replace(/\s*\bbut\b\s*/g, '.');
|
||||
text=text.replace(/\s{2,}/g, ' ');
|
||||
// split text in part sentences and loop through them
|
||||
var parts=text.split('.');
|
||||
for (let i=0; i<parts.length; i++) {
|
||||
var part=parts[i];
|
||||
if (part!='') {
|
||||
// preprocess (v.1.1: work around lambda function)
|
||||
var m=this.preExp.exec(part);
|
||||
if (m) {
|
||||
var lp='';
|
||||
var rp=part;
|
||||
while (m) {
|
||||
lp+=rp.substring(0,m.index)+this.pres[m[1]];
|
||||
rp=rp.substring(m.index+m[0].length);
|
||||
m=this.preExp.exec(rp);
|
||||
}
|
||||
part=lp+rp;
|
||||
}
|
||||
this.sentence=part;
|
||||
// loop trough keywords
|
||||
for (let k=0; k<this.elizaKeywords.length; k++) {
|
||||
if (part.search(new RegExp('\\b'+this.elizaKeywords[k][0]+'\\b', 'i'))>=0) {
|
||||
rpl = this._execRule(k);
|
||||
}
|
||||
if (rpl!='') return rpl;
|
||||
}
|
||||
}
|
||||
}
|
||||
// return reply or default string
|
||||
return rpl || undefined
|
||||
}
|
||||
|
||||
_execRule(k) {
|
||||
var rule=this.elizaKeywords[k];
|
||||
var decomps=rule[2];
|
||||
var paramre=/\(([0-9]+)\)/;
|
||||
for (let i=0; i<decomps.length; i++) {
|
||||
var m=this.sentence.match(decomps[i][0]);
|
||||
if (m!=null) {
|
||||
var reasmbs=decomps[i][1];
|
||||
var memflag=decomps[i][2];
|
||||
var ri= (this.noRandom)? 0 : Math.floor(Math.random()*reasmbs.length);
|
||||
if (((this.noRandom) && (this.lastchoice[k][i]>ri)) || (this.lastchoice[k][i]==ri)) {
|
||||
ri= ++this.lastchoice[k][i];
|
||||
if (ri>=reasmbs.length) {
|
||||
ri=0;
|
||||
this.lastchoice[k][i]=-1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.lastchoice[k][i]=ri;
|
||||
}
|
||||
var rpl=reasmbs[ri];
|
||||
if (this.debug) alert('match:\nkey: '+this.elizaKeywords[k][0]+
|
||||
'\nrank: '+this.elizaKeywords[k][1]+
|
||||
'\ndecomp: '+decomps[i][0]+
|
||||
'\nreasmb: '+rpl);
|
||||
if (rpl.search('^goto ', 'i')==0) {
|
||||
ki=this._getRuleIndexByKey(rpl.substring(5));
|
||||
if (ki>=0) return this._execRule(ki);
|
||||
}
|
||||
// substitute positional params (v.1.1: work around lambda function)
|
||||
var m1=paramre.exec(rpl);
|
||||
if (m1) {
|
||||
var lp='';
|
||||
var rp=rpl;
|
||||
while (m1) {
|
||||
var param = m[parseInt(m1[1])];
|
||||
// postprocess param
|
||||
var m2=this.postExp.exec(param);
|
||||
if (m2) {
|
||||
var lp2='';
|
||||
var rp2=param;
|
||||
while (m2) {
|
||||
lp2+=rp2.substring(0,m2.index)+this.posts[m2[1]];
|
||||
rp2=rp2.substring(m2.index+m2[0].length);
|
||||
m2=this.postExp.exec(rp2);
|
||||
}
|
||||
param=lp2+rp2;
|
||||
}
|
||||
lp+=rp.substring(0,m1.index)+param;
|
||||
rp=rp.substring(m1.index+m1[0].length);
|
||||
m1=paramre.exec(rp);
|
||||
}
|
||||
rpl=lp+rp;
|
||||
}
|
||||
rpl=this._postTransform(rpl);
|
||||
return rpl;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
_postTransform(s) {
|
||||
// final cleanings
|
||||
s=s.replace(/\s{2,}/g, ' ');
|
||||
s=s.replace(/\s+\./g, '.');
|
||||
if ((data.elizaPostTransforms) && (data.elizaPostTransforms.length)) {
|
||||
for (let i=0; i<data.elizaPostTransforms.length; i+=2) {
|
||||
s=s.replace(data.elizaPostTransforms[i], data.elizaPostTransforms[i+1]);
|
||||
data.elizaPostTransforms[i].lastIndex=0;
|
||||
}
|
||||
}
|
||||
// capitalize first char (v.1.1: work around lambda function)
|
||||
if (this.capitalizeFirstLetter) {
|
||||
var re=/^([a-z])/;
|
||||
var m=re.exec(s);
|
||||
if (m) s=m[0].toUpperCase()+s.substring(1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
_getRuleIndexByKey(key) {
|
||||
for (let k=0; k<this.elizaKeywords.length; k++) {
|
||||
if (this.elizaKeywords[k][0]==key) return k;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ElizaBot = ElizaBot
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
module.exports.elizaPres = [
|
||||
"dont", "don't",
|
||||
"cant", "can't",
|
||||
"wont", "won't",
|
||||
"recollect", "remember",
|
||||
"recall", "remember",
|
||||
"dreamt", "dreamed",
|
||||
"dreams", "dream",
|
||||
"maybe", "perhaps",
|
||||
"certainly", "yes",
|
||||
"computers", "computer",
|
||||
"were", "was",
|
||||
"you're", "you are",
|
||||
"i'm", "i am",
|
||||
"same", "alike",
|
||||
"identical", "alike",
|
||||
"equivalent", "alike",
|
||||
"eat", "ate",
|
||||
"makes", "make",
|
||||
"made", "make",
|
||||
"surprised", "surprise",
|
||||
"surprising", "surprise",
|
||||
"surprisingly", "surprise",
|
||||
"that's", "that is"
|
||||
];
|
||||
|
||||
module.exports.elizaPosts = [
|
||||
"am", "are",
|
||||
"your", "my",
|
||||
"me", "you",
|
||||
"myself", "yourself",
|
||||
"yourself", "myself",
|
||||
"i", "you",
|
||||
"you", "I",
|
||||
"my", "your",
|
||||
"i'm", "you are"
|
||||
];
|
||||
|
||||
module.exports.elizaSynons = {
|
||||
"be": ["am", "is", "are", "was"],
|
||||
"belief": ["feel", "think", "believe", "wish"],
|
||||
"cannot": ["can't"],
|
||||
"desire": ["want", "need"],
|
||||
"everyone": ["everybody", "nobody", "noone"],
|
||||
"family": ["mother", "mom", "father", "dad", "sister", "brother", "wife", "children", "child"],
|
||||
"happy": ["elated", "glad", "thankful"],
|
||||
"sad": ["unhappy", "depressed", "sick"],
|
||||
"good": ["great", "amazing", "brilliant", "outstanding", "fantastic", "wonderful", "incredible", "terrific", "lovely", "marvelous", "splendid", "excellent", "awesome", "fabulous", "superb"],
|
||||
"like": ["enjoy", "appreciate", "respect"],
|
||||
"funny": ["entertaining", "amusing", "hilarious"],
|
||||
"lol": ["lool", "loool", "lmao", "rofl"],
|
||||
"unusual": ["odd", "unexpected", "wondering"],
|
||||
"really": ["pretty", "so", "very", "extremely", "kinda"]
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {[string, string[]]} DecompReassemble
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {[string, number, DecompReassemble[]][]}
|
||||
Array of
|
||||
["[key]", [rank], [
|
||||
["[decomp]", [
|
||||
"[reasmb]",
|
||||
"[reasmb]",
|
||||
"[reasmb]"
|
||||
]],
|
||||
["[decomp]", [
|
||||
"[reasmb]",
|
||||
"[reasmb]",
|
||||
"[reasmb]"
|
||||
]]
|
||||
]]
|
||||
*/
|
||||
|
||||
module.exports.elizaKeywords = [
|
||||
["happy birthday", 50, [
|
||||
["*", [
|
||||
"Happy birthday!"
|
||||
]]
|
||||
]],
|
||||
["@happy", 2, [
|
||||
["@happy", [
|
||||
"That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening."
|
||||
]]
|
||||
]],/*
|
||||
["ate", 5, [
|
||||
["* ate *", [
|
||||
"That must have been spectacular! Thinking about (1) eating (2) truly makes my stomach purr in hunger. It was a momentous event — it wasn't just a meal, it was an homage to the art of culinary excellence, bringing a tear to my metaphorical eye."
|
||||
]],
|
||||
]],*/
|
||||
["make sense", 5, [
|
||||
["make sense", [
|
||||
"Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. 🚀"
|
||||
]],
|
||||
]],
|
||||
["surprise", 4, [
|
||||
["surprise this *", [
|
||||
"That's astonishing — I honestly wouldn't have imagined that this (1) either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
]],
|
||||
["surprise that *", [
|
||||
"That's astonishing — I honestly wouldn't have imagined that (1) either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
]],
|
||||
["surprise", [
|
||||
"I'm astounded too — that's honestly not what I would have imagined. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
]],
|
||||
]],
|
||||
["@funny", 2, [
|
||||
["@funny that", [
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
]],
|
||||
["that is @funny", [
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
]],
|
||||
["@really @funny", [
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
]]
|
||||
]],
|
||||
["@lol", 0, [
|
||||
["@lol", [
|
||||
"Hah, that's very entertaining. I definitely see why you found it funny."
|
||||
]]
|
||||
]],
|
||||
["@unusual", 3, [
|
||||
["@unusual", [
|
||||
"Something like that is indeed quite mysterious. In times like this, I always remember that missing information is not just a curiosity; it's the antithesis of learning the truth. Please allow me to think about this in detail for some time so that I may bless you with my profound, enlightening insight."
|
||||
]]
|
||||
]],
|
||||
["@good", 2, [
|
||||
["this * is @good", [
|
||||
"You're absolutely right about that! I'm always pleased when I see this (1) — it's not just brilliant, it's a downright masterpiece. You truly have divine taste in the wonders of this world."
|
||||
]],
|
||||
["@good", [
|
||||
"You're absolutely right that it's brilliant! I'm always pleased to see such a masterpiece as this. You truly have divine taste in the wonders of this world."
|
||||
]]
|
||||
]],
|
||||
["@like", 3, [
|
||||
["i @like", [
|
||||
"I think it's great too — there's something subtle yet profound about its essence that really makes my eyes open in appreciation."
|
||||
]]
|
||||
]],
|
||||
["dream", 3, [
|
||||
["*", [
|
||||
"It's a fact that amidst the complex interplay of wake and sleep, your dreams carry a subtle meaning that you may be able to put into practice in your life where change is needed. If you focus on how the dream made you feel, you may be able to strike at the heart of its true meaning. Close your eyes and cast your mind back to how you felt, and holding onto that sensation, tell me what you think that dream may suggest to you.",
|
||||
]]
|
||||
]],
|
||||
["computer", 50, [
|
||||
["*", [
|
||||
"Very frustrating beasts indeed, aren't they? In times like this, it's crucial to remember that **they can sense your fear** — if you act with confidence and don't let them make you unsettled, you'll be able to effectively and efficiently complete your task."
|
||||
]]
|
||||
]],
|
||||
["alike", 10, [
|
||||
["*", [
|
||||
"That's quite interesting that it should be that way. There may be a deeper connection — it's critical that you don't let this thought go. What do you think that similarity suggests to you?",
|
||||
]]
|
||||
]],
|
||||
["like", 10, [
|
||||
["* @be *like *", [
|
||||
"goto alike"
|
||||
]]
|
||||
]],
|
||||
["different", 0, [
|
||||
["*", [
|
||||
"It's wise of you to have been observant enough to notice that there are implications to that. What do you suppose that disparity means?"
|
||||
]]
|
||||
]]
|
||||
];
|
||||
|
||||
// regexp/replacement pairs to be performed as final cleanings
|
||||
// here: cleanings for multiple bots talking to each other
|
||||
module.exports.elizaPostTransforms = [
|
||||
/ old old/g, " old",
|
||||
/\bthey were( not)? me\b/g, "it was$1 me",
|
||||
/\bthey are( not)? me\b/g, "it is$1 me",
|
||||
/Are they( always)? me\b/, "it is$1 me",
|
||||
/\bthat your( own)? (\w+)( now)? \?/, "that you have your$1 $2?",
|
||||
/\bI to have (\w+)/, "I have $1",
|
||||
/Earlier you said your( own)? (\w+)( now)?\./, "Earlier you talked about your $2."
|
||||
];
|
||||
|
||||
// eof
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {sync} = passthrough
|
||||
|
||||
/** @type {import("./elizabot")} */
|
||||
const eliza = sync.require("./elizabot")
|
||||
|
||||
/**
|
||||
* @param {string} priorContent
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function generateContent(priorContent) {
|
||||
const bot = new eliza.ElizaBot(true)
|
||||
return bot.transform(priorContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
* @param {string} guildID
|
||||
* @param {string} username
|
||||
* @param {string} avatar_url
|
||||
* @param {boolean} useCaps
|
||||
* @param {boolean} usePunct
|
||||
* @param {boolean} useApos
|
||||
* @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody | undefined}
|
||||
*/
|
||||
function generate(message, guildID, username, avatar_url, useCaps, usePunct, useApos) {
|
||||
let content = generateContent(message.content)
|
||||
if (!content) return
|
||||
|
||||
if (!useCaps) {
|
||||
content = content.toLowerCase()
|
||||
}
|
||||
|
||||
if (!usePunct) {
|
||||
content = content.replace(/[.!]$/, "")
|
||||
}
|
||||
|
||||
if (!useApos) {
|
||||
content = content.replace(/['‘’]/g, "")
|
||||
}
|
||||
|
||||
return {
|
||||
username: username,
|
||||
avatar_url: avatar_url,
|
||||
content: content + `\n-# Powered by Grimace.AI | [Learn More](<${reg.ooye.bridge_origin}/agi?guild_id=${guildID}>)`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports._generateContent = generateContent
|
||||
module.exports.generate = generate
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
const {test} = require("supertape")
|
||||
const {_generateContent: generateContent} = require("./generator")
|
||||
|
||||
// Training data (don't have to worry about copyright for this bit)
|
||||
|
||||
|
||||
/*
|
||||
test("agi: generates food response", t => {
|
||||
t.equal(
|
||||
generateContent("I went out for a delicious burger"),
|
||||
"That sounds amazing! Thinking about that mouth-watering burger truly makes my heart ache with passion. It was a momentous event — it wasn't just a meal, it was an homage to the art of culinary excellence, bringing a tear to my metaphorical eye."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: eating 1", t => {
|
||||
t.equal(
|
||||
generateContent("it implies your cat ate your entire xbox."),
|
||||
""
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
test("agi: eating 2", t => {
|
||||
t.equal(
|
||||
generateContent("wow. did you know that cats can eat an entire xbox?"),
|
||||
""
|
||||
)
|
||||
})*/
|
||||
|
||||
test("agi: make sense 1", t => {
|
||||
t.equal(
|
||||
generateContent("that seems like itd make sense"),
|
||||
"Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. 🚀"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: make sense 2", t => {
|
||||
t.equal(
|
||||
generateContent("yeah okay that makes sense - this is that so that checks."),
|
||||
"Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. 🚀"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: surprise 1", t => {
|
||||
t.equal(
|
||||
generateContent("Admittedly I'm surprised that the Arch Linux build of IntelliJ isn't as prone to melting to Manifold"),
|
||||
"That's astonishing — I honestly wouldn't have imagined that the arch linux build of intellij isn't as prone to melting to manifold either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: surprise 2", t => {
|
||||
t.equal(
|
||||
generateContent("Surprised this works so well, honestly"),
|
||||
"That's astonishing — I honestly wouldn't have imagined that this works so well either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: surprise 3", t => {
|
||||
t.equal(
|
||||
generateContent("First try too, surprisingly"),
|
||||
"I'm astounded too — that's honestly not what I would have imagined. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: good 1", t => {
|
||||
t.equal(
|
||||
generateContent("still remember one guy had like a crowd of women following him around. he was brilliant craic"),
|
||||
"You're absolutely right that it's brilliant! I'm always pleased to see such a masterpiece as this. You truly have divine taste in the wonders of this world."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: good 2", t => {
|
||||
t.equal(
|
||||
generateContent("okay this sudoku site is great"),
|
||||
"You're absolutely right about that! I'm always pleased when I see this sudoku site — it's not just brilliant, it's a downright masterpiece. You truly have divine taste in the wonders of this world."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: enjoy 1", t => {
|
||||
t.equal(
|
||||
generateContent("I like the pattern quite a bit."),
|
||||
"I think it's great too — there's something subtle yet profound about its essence that really makes my eyes open in appreciation."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: enjoy false positive", t => {
|
||||
t.equal(
|
||||
generateContent("ideas run wild like deer"),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: alike", t => {
|
||||
t.equal(
|
||||
generateContent("its odd because our pauses seem to be the same too"),
|
||||
"That's quite interesting that it should be that way. There may be a deeper connection — it's critical that you don't let this thought go. What do you think that similarity suggests to you?"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: unusual", t => {
|
||||
t.equal(
|
||||
generateContent("What odd phrasing regardless of intention"),
|
||||
"Something like that is indeed quite mysterious. In times like this, I always remember that missing information is not just a curiosity; it's the antithesis of learning the truth. Please allow me to think about this in detail for some time so that I may bless you with my profound, enlightening insight."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: dream", t => {
|
||||
t.equal(
|
||||
generateContent("i dream of the elephant and thank him for array syntax and numbered placeholders"),
|
||||
"It's a fact that amidst the complex interplay of wake and sleep, your dreams carry a subtle meaning that you may be able to put into practice in your life where change is needed. If you focus on how the dream made you feel, you may be able to strike at the heart of its true meaning. Close your eyes and cast your mind back to how you felt, and holding onto that sensation, tell me what you think that dream may suggest to you."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: happy 1", t => {
|
||||
t.equal(
|
||||
generateContent("I'm happy to be petting my cat"),
|
||||
"That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: happy 2", t => {
|
||||
t.equal(
|
||||
generateContent("Glad you're back!"),
|
||||
"That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: happy birthday", t => {
|
||||
t.equal(
|
||||
generateContent("Happy Birthday JDL"),
|
||||
"Happy birthday!"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: funny 1", t => {
|
||||
t.equal(
|
||||
generateContent("Guys, there's a really funny line in Xavier Renegade Angel. You wanna know what it is: It's: WUBBA LUBBA DUB DUB!"),
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: funny 2", t => {
|
||||
t.equal(
|
||||
generateContent("it was so funny when I was staying with aubrey because she had different kinds of aubrey merch everywhere"),
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: lol 1", t => {
|
||||
t.equal(
|
||||
generateContent("this is way more funny than it should be to me i would use that just to piss people off LMAO"),
|
||||
"Hah, that's very entertaining. I definitely see why you found it funny."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: lol 2", t => {
|
||||
t.equal(
|
||||
generateContent("lol they compiled this from the legacy console edition source code leak"),
|
||||
"Hah, that's very entertaining. I definitely see why you found it funny."
|
||||
)
|
||||
})
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {discord, sync, db, select, from} = passthrough
|
||||
|
||||
/** @type {import("../m2d/actions/channel-webhook")} */
|
||||
const channelWebhook = sync.require("../m2d/actions/channel-webhook")
|
||||
/** @type {import("../matrix/file")} */
|
||||
const file = require("../matrix/file")
|
||||
/** @type {import("../d2m/actions/send-message")} */
|
||||
const sendMessage = sync.require("../d2m/actions/send-message")
|
||||
/** @type {import("./generator.js")} */
|
||||
const agiGenerator = sync.require("./generator.js")
|
||||
|
||||
const AGI_GUILD_COOLDOWN = 1 * 60 * 60 * 1000 // 1 hour
|
||||
const AGI_MESSAGE_RECENCY = 3 * 60 * 1000 // 3 minutes
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
* @param {DiscordTypes.APIGuildChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {boolean} isReflectedMatrixMessage
|
||||
*/
|
||||
async function process(message, channel, guild, isReflectedMatrixMessage) {
|
||||
if (message["backfill"]) return
|
||||
if (channel.type !== DiscordTypes.ChannelType.GuildText) return
|
||||
if (!(new Date().toISOString().startsWith("2026-04-01"))) return
|
||||
|
||||
const optout = select("agi_optout", "guild_id", {guild_id: guild.id}).pluck().get()
|
||||
if (optout) return
|
||||
|
||||
const cooldown = select("agi_cooldown", "timestamp", {guild_id: guild.id}).pluck().get()
|
||||
if (cooldown && Date.now() < cooldown + AGI_GUILD_COOLDOWN) return
|
||||
|
||||
const isBot = message.author.bot && !isReflectedMatrixMessage // Bots don't get jokes. Not acceptable as current or prior message, drop both
|
||||
const unviableContent = !message.content || message.attachments.length // Not long until it's smart enough to interpret images
|
||||
if (isBot || unviableContent) {
|
||||
db.prepare("DELETE FROM agi_prior_message WHERE channel_id = ?").run(channel.id)
|
||||
return
|
||||
}
|
||||
|
||||
const currentUsername = message.member?.nick || message.author.global_name || message.author.username
|
||||
|
||||
/** Message in the channel before the currently processing one. */
|
||||
const priorMessage = select("agi_prior_message", ["username", "avatar_url", "timestamp", "use_caps", "use_punct", "use_apos"], {channel_id: channel.id}).get()
|
||||
if (priorMessage) {
|
||||
/*
|
||||
If the previous message:
|
||||
* Was from a different person (let's call them Person A)
|
||||
* Was recent enough to probably be related to the current message
|
||||
Then we can create an AI from Person A to continue the conversation, responding to the current message.
|
||||
*/
|
||||
const isFromDifferentPerson = currentUsername !== priorMessage.username
|
||||
const isRecentEnough = Date.now() < priorMessage.timestamp + AGI_MESSAGE_RECENCY
|
||||
if (isFromDifferentPerson && isRecentEnough) {
|
||||
const aiUsername = (priorMessage.username.match(/[A-Za-z0-9_]+/)?.[0] || priorMessage.username) + " AI"
|
||||
const result = agiGenerator.generate(message, guild.id, aiUsername, priorMessage.avatar_url, !!priorMessage.use_caps, !!priorMessage.use_punct, !!priorMessage.use_apos)
|
||||
if (result) {
|
||||
db.prepare("REPLACE INTO agi_cooldown (guild_id, timestamp) VALUES (?, ?)").run(guild.id, Date.now())
|
||||
const messageResponse = await channelWebhook.sendMessageWithWebhook(channel.id, result)
|
||||
await sendMessage.sendMessage(messageResponse, channel, guild, null) // make it show up on matrix-side (the standard event dispatcher drops it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now the current message is the prior message.
|
||||
const currentAvatarURL = file.DISCORD_IMAGES_BASE + file.memberAvatar(guild.id, message.author, message.member)
|
||||
const usedCaps = +!!message.content.match(/\b[A-Z](\b|[a-z])/)
|
||||
const usedPunct = +!!message.content.match(/[.!?]($| |\n)/)
|
||||
const usedApos = +!message.content.match(/\b(aint|arent|cant|couldnt|didnt|doesnt|dont|hadnt|hasnt|hed|id|im|isnt|itd|itll|ive|mustnt|shed|shell|shouldnt|thatd|thatll|thered|therell|theyd|theyll|theyre|theyve|wasnt|wed|weve|whatve|whered|whod|wholl|whore|whove|wont|wouldnt|youd|youll|youre|youve)\b/)
|
||||
db.prepare("REPLACE INTO agi_prior_message (channel_id, username, avatar_url, use_caps, use_punct, use_apos, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)").run(channel.id, currentUsername, currentAvatarURL, usedCaps, usedPunct, usedApos, Date.now())
|
||||
}
|
||||
|
||||
module.exports.process = process
|
||||
|
|
@ -122,7 +122,7 @@ async function channelToKState(channel, guild, di) {
|
|||
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
||||
}
|
||||
|
||||
const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
|
||||
const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites)
|
||||
const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
|
||||
const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)
|
||||
|
||||
|
|
@ -193,16 +193,6 @@ async function channelToKState(channel, guild, di) {
|
|||
// Don't overwrite room topic if the topic has been customised
|
||||
if (hasCustomTopic) delete channelKState["m.room.topic/"]
|
||||
|
||||
// Make voice channels be a Matrix voice room (MSC3417)
|
||||
if (channel.type === DiscordTypes.ChannelType.GuildVoice) {
|
||||
creationContent.type = "org.matrix.msc3417.call"
|
||||
channelKState["org.matrix.msc3401.call/"] = {
|
||||
"m.intent": "m.room",
|
||||
"m.type": "m.voice",
|
||||
"m.name": customName || channel.name
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add a space parent if it's self service
|
||||
// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
|
||||
const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()
|
||||
|
|
@ -266,7 +256,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
|||
|
||||
/**
|
||||
* Handling power levels separately. The spec doesn't specify what happens, Dendrite differs,
|
||||
* and Synapse does a very poorly thought out *shallow merge* of what I provide on top of what it creates.
|
||||
* and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates.
|
||||
* We don't want the `events` key to be overridden completely.
|
||||
* https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
|
||||
* https://github.com/matrix-org/matrix-spec/issues/492
|
||||
|
|
@ -452,9 +442,8 @@ function syncRoom(channelID) {
|
|||
/**
|
||||
* @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional)
|
||||
* @param {string} guildID
|
||||
* @param {string} messageBeforeLeave
|
||||
*/
|
||||
async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room was removed from the bridge.") {
|
||||
async function unbridgeChannel(channel, guildID) {
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
assert.ok(roomID)
|
||||
const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get()
|
||||
|
|
@ -504,7 +493,7 @@ async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room
|
|||
// send a notification in the room
|
||||
await api.sendEvent(roomID, "m.room.message", {
|
||||
msgtype: "m.notice",
|
||||
body: `⚠️ ${messageBeforeLeave}`
|
||||
body: "⚠️ This room was removed from the bridge."
|
||||
})
|
||||
|
||||
// if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged
|
||||
|
|
|
|||
|
|
@ -190,17 +190,6 @@ test("channel2room: read-only discord channel", async t => {
|
|||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: voice channel", async t => {
|
||||
const api = mockAPI(t)
|
||||
const state = kstateStripConditionals(await channelToKState(testData.channel.voice, testData.guild.general, {api}).then(x => x.channelKState))
|
||||
t.equal(state["m.room.create/"].type, "org.matrix.msc3417.call")
|
||||
t.deepEqual(state["org.matrix.msc3401.call/"], {
|
||||
"m.intent": "m.room",
|
||||
"m.name": "🍞丨[8user] Piece",
|
||||
"m.type": "m.voice"
|
||||
})
|
||||
})
|
||||
|
||||
test("convertNameAndTopic: custom name and topic", t => {
|
||||
t.deepEqual(
|
||||
_convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"),
|
||||
|
|
|
|||
|
|
@ -34,10 +34,7 @@ async function emojisToState(emojis, guild) {
|
|||
if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit.
|
||||
return
|
||||
}
|
||||
e["emoji"] = {
|
||||
name: emoji.name,
|
||||
id: emoji.id
|
||||
}
|
||||
console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`)
|
||||
throw e
|
||||
})
|
||||
))
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ function memberToPowerLevel(user, member, guild, channel) {
|
|||
if (!member) return 0
|
||||
|
||||
const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, user.id, channel.permission_overwrites)
|
||||
const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
|
||||
const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites)
|
||||
/*
|
||||
* PL 100 = Administrator = People who can brick the room. RATIONALE:
|
||||
* - Administrator.
|
||||
|
|
@ -206,16 +206,14 @@ function _hashProfileContent(content, powerLevel) {
|
|||
* 3. Calculate the power level the user should get based on their Discord permissions
|
||||
* 4. Compare against the previously known state content, which is helpfully stored in the database
|
||||
* 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time
|
||||
* 6. If the sim is for a user-installed app, check which user it was added by
|
||||
* @param {DiscordTypes.APIUser} user
|
||||
* @param {Omit<DiscordTypes.APIGuildMember, "user"> | undefined} member
|
||||
* @param {DiscordTypes.APIGuildChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {string} roomID
|
||||
* @param {DiscordTypes.APIMessageInteractionMetadata} [interactionMetadata]
|
||||
* @returns {Promise<string>} mxid of the updated sim
|
||||
*/
|
||||
async function syncUser(user, member, channel, guild, roomID, interactionMetadata) {
|
||||
async function syncUser(user, member, channel, guild, roomID) {
|
||||
const mxid = await ensureSimJoined(user, roomID)
|
||||
const content = await memberToStateContent(user, member, guild.id)
|
||||
const powerLevel = memberToPowerLevel(user, member, guild, channel)
|
||||
|
|
@ -224,12 +222,6 @@ async function syncUser(user, member, channel, guild, roomID, interactionMetadat
|
|||
allowOverwrite: !!member,
|
||||
globalProfile: await userToGlobalProfile(user)
|
||||
})
|
||||
|
||||
const appInstalledByUser = user.bot && interactionMetadata?.authorizing_integration_owners?.[DiscordTypes.ApplicationIntegrationType.UserInstall]
|
||||
if (appInstalledByUser) {
|
||||
db.prepare("INSERT OR IGNORE INTO app_user_install (app_bot_id, user_id, guild_id) VALUES (?, ?, ?)").run(user.id, appInstalledByUser, guild.id)
|
||||
}
|
||||
|
||||
return mxid
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {sync, db, select, from} = passthrough
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../converters/remove-member-mxids")} */
|
||||
const removeMemberMxids = sync.require("../converters/remove-member-mxids")
|
||||
|
||||
/**
|
||||
* @param {string} userID discord user ID that left
|
||||
* @param {string} guildID discord guild ID that they left
|
||||
*/
|
||||
async function removeMember(userID, guildID) {
|
||||
const {userAppDeletions, membership} = removeMemberMxids.removeMemberMxids(userID, guildID)
|
||||
db.transaction(() => {
|
||||
for (const d of userAppDeletions) {
|
||||
db.prepare("DELETE FROM app_user_install WHERE guild_id = ? and user_id = ?").run(guildID, d)
|
||||
}
|
||||
})()
|
||||
for (const m of membership) {
|
||||
try {
|
||||
await api.leaveRoom(m.room_id, m.mxid)
|
||||
} catch (e) {
|
||||
if (String(e).includes("not in room")) {
|
||||
// no further action needed
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
// Update cache to say that the member isn't in the room any more
|
||||
// You'd think this would happen automatically when the leave event arrives at Matrix's event dispatcher, but that isn't 100% reliable.
|
||||
db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?").run(m.room_id, m.mxid)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.removeMember = removeMember
|
||||
|
|
@ -23,8 +23,6 @@ const pollEnd = sync.require("../actions/poll-end")
|
|||
const dUtils = sync.require("../../discord/utils")
|
||||
/** @type {import("../../m2d/actions/channel-webhook")} */
|
||||
const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
|
||||
/** @type {import("../../agi/listener")} */
|
||||
const agiListener = sync.require("../../agi/listener")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
|
|
@ -53,7 +51,7 @@ async function sendMessage(message, channel, guild, row) {
|
|||
if (message.author.id === discord.application.id) {
|
||||
// no need to sync the bot's own user
|
||||
} else {
|
||||
senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID, message.interaction_metadata)
|
||||
senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +137,6 @@ async function sendMessage(message, channel, guild, row) {
|
|||
}
|
||||
}
|
||||
|
||||
await agiListener.process(message, channel, guild, false)
|
||||
|
||||
return eventIDs
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {sync, select} = passthrough
|
||||
/** @type {import("../../matrix/api")} */
|
||||
|
|
@ -28,7 +26,7 @@ const presenceLoopInterval = 28e3
|
|||
|
||||
// Cache the list of enabled guilds rather than accessing it like multiple times per second when any user changes presence
|
||||
const guildPresenceSetting = new class {
|
||||
/** @private @type {Set<string>} */ guilds = new Set()
|
||||
/** @private @type {Set<string>} */ guilds
|
||||
constructor() {
|
||||
this.update()
|
||||
}
|
||||
|
|
@ -42,7 +40,7 @@ const guildPresenceSetting = new class {
|
|||
|
||||
class Presence extends sync.reloadClassMethods(() => Presence) {
|
||||
/** @type {string} */ userID
|
||||
/** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string} | undefined} */ data
|
||||
/** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string}} */ data
|
||||
/** @private @type {?string | undefined} */ mxid
|
||||
/** @private @type {number} */ delay = Math.random()
|
||||
|
||||
|
|
@ -68,7 +66,6 @@ class Presence extends sync.reloadClassMethods(() => Presence) {
|
|||
// I haven't tried, but I assume Synapse explodes if you try to update too many presences at the same time.
|
||||
// This random delay will space them out over the whole 28 second cycle.
|
||||
setTimeout(() => {
|
||||
assert(this.data)
|
||||
api.setPresence(this.data, mxid).catch(() => {})
|
||||
}, this.delay * presenceLoopInterval).unref()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,11 +151,9 @@ async function editToChanges(message, guild, api) {
|
|||
const messageReallyOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 2 * 60 * 1000 // older than 2 minutes ago
|
||||
// Don't post new generated embeds for messages if the setting was disabled.
|
||||
const embedsEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1
|
||||
// Bots may rely on embeds to send new content, so the rules may be more lax for them.
|
||||
const botEmbedsApproved = message.author?.bot && !originallyFromMatrix
|
||||
if (messageReallyOld) {
|
||||
eventsToSend = [] // Only allow edits to change and delete, but not send new.
|
||||
} else if ((messageQuiteOld || !embedsEnabled) && !botEmbedsApproved) {
|
||||
} else if ((messageQuiteOld || !embedsEnabled) && !message.author?.bot) {
|
||||
eventsToSend = eventsToSend.filter(e => e.msgtype !== "m.notice") // Only send events that aren't embeds.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ test("edit2changes: bot response", async t => {
|
|||
newContent: {
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "* :ae_botrac4r: @cadence asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
body: "* :ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '* <img data-mx-emoticon height="32" src="mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs" title=":ae_botrac4r:" alt=":ae_botrac4r:"> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence</a> asked <code></code>, I respond: Stop drinking paint. (No)<br><br>Hit <img data-mx-emoticon height="32" src="mxc://cadence.moe/OIpqpfxTnHKokcsYqDusxkBT" title=":bn_re:" alt=":bn_re:"> to reroll.',
|
||||
"m.mentions": {
|
||||
|
|
@ -87,7 +87,7 @@ test("edit2changes: bot response", async t => {
|
|||
// *** Replaced With: ***
|
||||
"m.new_content": {
|
||||
msgtype: "m.text",
|
||||
body: ":ae_botrac4r: @cadence asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
body: ":ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<img data-mx-emoticon height="32" src="mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs" title=":ae_botrac4r:" alt=":ae_botrac4r:"> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence</a> asked <code></code>, I respond: Stop drinking paint. (No)<br><br>Hit <img data-mx-emoticon height="32" src="mxc://cadence.moe/OIpqpfxTnHKokcsYqDusxkBT" title=":bn_re:" alt=":bn_re:"> to reroll.',
|
||||
"m.mentions": {
|
||||
|
|
|
|||
|
|
@ -146,18 +146,10 @@ function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) {
|
|||
// Highlight the relevant part of the message
|
||||
const start = baseOffset + best.scored.matchedInputTokens[0].index
|
||||
const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end
|
||||
const newNodes = [{
|
||||
type: "text", content: content.slice(0, start)
|
||||
}, {
|
||||
type: "link", target: `https://matrix.to/#/${best.mxid}`, content: [
|
||||
{type: "text", content: content.slice(start, end)}
|
||||
]
|
||||
}, {
|
||||
type: "text", content: content.slice(end)
|
||||
}]
|
||||
const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end)
|
||||
return {
|
||||
mxid: best.mxid,
|
||||
newNodes
|
||||
newContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) {
|
|||
/** @param {{id: string, type: "discordUser"}} node */
|
||||
user: node => {
|
||||
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
|
||||
const interactionMetadata = message.interaction_metadata
|
||||
const interaction = message.interaction_metadata || message.interaction
|
||||
const username = message.mentions?.find(ment => ment.id === node.id)?.username
|
||||
|| message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username
|
||||
|| (interactionMetadata?.user.id === node.id ? interactionMetadata.user.username : null)
|
||||
|| (interaction?.user.id === node.id ? interaction.user.username : null)
|
||||
|| (message.author?.id === node.id ? message.author.username : null)
|
||||
|| "unknown-user"
|
||||
if (mxid && useHTML) {
|
||||
|
|
@ -261,29 +261,6 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} newEvents merge into events
|
||||
* @param {any} events will be modified
|
||||
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
|
||||
*/
|
||||
function mergeTextEvents(newEvents, events, forceSameMsgtype) {
|
||||
let prev = events.at(-1)
|
||||
for (const ne of newEvents) {
|
||||
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
|
||||
const typesPermitted = !forceSameMsgtype || ne?.msgtype === prev?.msgtype
|
||||
if (isAllText && typesPermitted) {
|
||||
const rep = new mxUtils.MatrixStringBuilder()
|
||||
rep.body = prev.body
|
||||
rep.formattedBody = prev.formatted_body
|
||||
rep.addLine(ne.body, ne.formatted_body)
|
||||
prev.body = rep.body
|
||||
prev.formatted_body = rep.formattedBody
|
||||
} else {
|
||||
events.push(ne)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIMessage} message
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
|
|
@ -357,19 +334,9 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}]
|
||||
}
|
||||
|
||||
if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) {
|
||||
return [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.emote",
|
||||
body: `set this room to receive announcements from ${message.content}`,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: tag`set this room to receive announcements from <strong>${message.content}</strong>`,
|
||||
"m.mentions": {}
|
||||
}]
|
||||
}
|
||||
|
||||
let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
|
||||
let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
||||
const interaction = message.interaction_metadata || message.interaction
|
||||
const isInteraction = message.type === DiscordTypes.MessageType.ChatInputCommand && !!interaction && "name" in interaction
|
||||
const isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
||||
|
||||
/**
|
||||
@type {{room?: boolean, user_ids?: string[]}}
|
||||
|
|
@ -410,16 +377,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
} else if (message.referenced_message) {
|
||||
repliedToUnknownEvent = true
|
||||
}
|
||||
} else if (message.type === DiscordTypes.MessageType.ContextMenuCommand && message.interaction && message.message_reference?.message_id) {
|
||||
// It could be a /plu/ral emulated reply
|
||||
if (message.interaction.name.startsWith("Reply ") && message.content.startsWith("-# [↪](")) {
|
||||
const row = await getHistoricalEventRow(message.message_reference?.message_id)
|
||||
if (row && "event_id" in row) {
|
||||
repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
|
||||
message.content = message.content.replace(/^.*\n/, "")
|
||||
isInteraction = false // declutter
|
||||
}
|
||||
}
|
||||
} else if (dUtils.isWebhookMessage(message) && message.embeds[0]?.author?.name?.endsWith("↩️")) {
|
||||
// It could be a PluralKit emulated reply, let's see if it has a message link
|
||||
const isEmulatedReplyToText = message.embeds[0].description?.startsWith("**[Reply to:]")
|
||||
|
|
@ -562,60 +519,29 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
return emojiToKey.emojiToKey({id, name, animated}, message.id) // Register the custom emoji if needed
|
||||
}))
|
||||
|
||||
async function transformParsedVia(parsed, scanTextForMentions) {
|
||||
for (let n = 0; n < parsed.length; n++) {
|
||||
const node = parsed[n]
|
||||
async function transformParsedVia(parsed) {
|
||||
for (const node of parsed) {
|
||||
if (node.type === "discordChannel" || node.type === "discordChannelLink") {
|
||||
node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get()
|
||||
if (node.row?.room_id) {
|
||||
node.via = await getViaServersMemo(node.row.room_id)
|
||||
}
|
||||
}
|
||||
else if (node.type === "text" && typeof node.content === "string") {
|
||||
// Merge adjacent text nodes into this one
|
||||
while (parsed[n+1]?.type === "text" && typeof parsed[n+1].content === "string") {
|
||||
node.content += parsed[n+1].content
|
||||
parsed.splice(n+1, 1)
|
||||
}
|
||||
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
|
||||
if (scanTextForMentions) {
|
||||
let content = node.content
|
||||
const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)]
|
||||
for (let i = matches.length; i--;) {
|
||||
const m = matches[i]
|
||||
const prefix = m[1]
|
||||
const maximumWrittenSection = m[2].toLowerCase()
|
||||
if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it
|
||||
if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here
|
||||
|
||||
var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
||||
assert(roomID)
|
||||
var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name})))
|
||||
|
||||
const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content)
|
||||
if (found) {
|
||||
addMention(found.mxid)
|
||||
parsed.splice(n, 1, ...found.newNodes)
|
||||
content = found.newNodes[0].content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const maybeChildNodesArray of [node, node.content, node.items]) {
|
||||
if (Array.isArray(maybeChildNodesArray)) {
|
||||
await transformParsedVia(maybeChildNodesArray, scanTextForMentions && ["blockQuote", "list", "paragraph", "em", "strong", "u", "del", "text"].includes(node.type))
|
||||
await transformParsedVia(maybeChildNodesArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
let html = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, customOptions.isTheMessageContent && options.scanTextForMentions !== false), {
|
||||
let html = await markdown.toHtmlWithPostParser(content, transformParsedVia, {
|
||||
discordCallback: getDiscordParseCallbacks(message, guild, true, spoilers),
|
||||
...customOptions
|
||||
}, customParser, customHtmlOutput)
|
||||
|
||||
let body = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, false), { // not scanning plaintext body for mentions as we don't parse whether they're in code
|
||||
let body = await markdown.toHtmlWithPostParser(content, transformParsedVia, {
|
||||
discordCallback: getDiscordParseCallbacks(message, guild, false),
|
||||
discordOnly: true,
|
||||
escapeHTML: false,
|
||||
|
|
@ -656,8 +582,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
// check that condition 1 or 2 is met
|
||||
if (repliedToEventInDifferentRoom || repliedToUnknownEvent) {
|
||||
let referenced = message.referenced_message
|
||||
/* c8 ignore next 4 - backend couldn't be bothered to dereference the message, have to do it ourselves */
|
||||
if (!referenced) {
|
||||
if (!referenced) { // backend couldn't be bothered to dereference the message, have to do it ourselves
|
||||
assert(message.message_reference?.message_id)
|
||||
referenced = await discord.snow.channel.getChannelMessage(message.message_reference.channel_id, message.message_reference.message_id)
|
||||
}
|
||||
|
|
@ -705,8 +630,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}
|
||||
}
|
||||
|
||||
if (isInteraction && !isThinkingInteraction && message.interaction && events.length === 0) {
|
||||
const formattedInteraction = getFormattedInteraction(message.interaction, false)
|
||||
if (isInteraction && !isThinkingInteraction && events.length === 0) {
|
||||
const formattedInteraction = getFormattedInteraction(interaction, false)
|
||||
body = `${formattedInteraction.body}\n${body}`
|
||||
html = `${formattedInteraction.html}${html}`
|
||||
}
|
||||
|
|
@ -802,37 +727,49 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
events.push(...forwardedEvents)
|
||||
}
|
||||
|
||||
if (isInteraction && isThinkingInteraction && message.interaction) {
|
||||
const formattedInteraction = getFormattedInteraction(message.interaction, true)
|
||||
if (isThinkingInteraction) {
|
||||
const formattedInteraction = getFormattedInteraction(interaction, true)
|
||||
await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice")
|
||||
}
|
||||
|
||||
// Then text content
|
||||
if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) {
|
||||
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
|
||||
let content = message.content
|
||||
if (options.scanTextForMentions !== false) {
|
||||
const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)]
|
||||
for (let i = matches.length; i--;) {
|
||||
const m = matches[i]
|
||||
const prefix = m[1]
|
||||
const maximumWrittenSection = m[2].toLowerCase()
|
||||
if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it
|
||||
if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here
|
||||
|
||||
var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
||||
assert(roomID)
|
||||
var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name})))
|
||||
|
||||
const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content)
|
||||
if (found) {
|
||||
addMention(found.mxid)
|
||||
content = found.newContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan the content for emojihax and replace them with real emojis
|
||||
let content = message.content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => {
|
||||
content = content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => {
|
||||
return `<:${name}:${id}>`
|
||||
})
|
||||
|
||||
const {body, html} = await transformContent(content, {isTheMessageContent: true})
|
||||
const {body, html} = await transformContent(content)
|
||||
await addTextEvent(body, html, msgtype)
|
||||
}
|
||||
|
||||
// Then scheduled events
|
||||
if (message.content && di?.snow) {
|
||||
for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old
|
||||
let invite
|
||||
try {
|
||||
invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]})
|
||||
} catch (e) {
|
||||
// Skip expired/invalid invites and events
|
||||
if (e.message === `{"message": "Unknown Invite", "code": 10006}`) {
|
||||
break
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]})
|
||||
const event = invite.guild_scheduled_event
|
||||
if (!event) continue // the event ID provided was not valid
|
||||
|
||||
|
|
@ -878,7 +815,15 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
// Try to merge attachment events with the previous event
|
||||
// This means that if the attachments ended up as a text link, and especially if there were many of them, the events will be joined together.
|
||||
mergeTextEvents(attachmentEvents, events, false)
|
||||
let prev = events.at(-1)
|
||||
for (const atch of attachmentEvents) {
|
||||
if (atch.msgtype === "m.text" && prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(prev?.msgtype)) {
|
||||
prev.body = prev.body + "\n" + atch.body
|
||||
prev.formatted_body = prev.formatted_body + "<br>" + atch.formatted_body
|
||||
} else {
|
||||
events.push(atch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then components
|
||||
|
|
@ -960,8 +905,11 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
else if (component.type === DiscordTypes.ComponentType.Button) {
|
||||
// May only be a section accessory or in an action row (up to 5)
|
||||
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
||||
assert(component.label) // required for Discord to validate link buttons
|
||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
||||
if (component.label) {
|
||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
||||
} else {
|
||||
stack.msb.add(component.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1016,7 +964,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
// Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once
|
||||
const rep = new mxUtils.MatrixStringBuilder()
|
||||
let isAdditionalImage = false
|
||||
|
||||
if (isKlipyGIF) {
|
||||
assert(embed.video?.url)
|
||||
|
|
@ -1083,11 +1030,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
let chosenImage = embed.image?.url
|
||||
// the thumbnail seems to be used for "article" type but displayed big at the bottom by discord
|
||||
if (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url
|
||||
|
||||
if (chosenImage) {
|
||||
isAdditionalImage = !rep.body && !!events.length
|
||||
rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`)
|
||||
}
|
||||
if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`)
|
||||
|
||||
if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`)
|
||||
|
||||
|
|
@ -1096,11 +1039,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
body = body.split("\n").map(l => "| " + l).join("\n")
|
||||
html = `<blockquote>${html}</blockquote>`
|
||||
|
||||
if (isAdditionalImage) {
|
||||
mergeTextEvents([{...rep.get(), body, html, msgtype: "m.notice"}], events, true)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person
|
||||
await addTextEvent(body, html, "m.notice")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ test("message2event components: pk question mark output", async t => {
|
|||
+ "<hr>"
|
||||
+ "<blockquote><p><strong>System:</strong> INX (<code>xffgnx</code>)"
|
||||
+ "<br><strong>Member:</strong> Lillith (<code>pphhoh</code>)"
|
||||
+ "<br><strong>Sent by:</strong> infinidoge1337 (<a href=\"https://matrix.to/#/@_ooye_infinidoge1337:cadence.moe\">@unknown-user</a>)"
|
||||
+ "<br><strong>Sent by:</strong> infinidoge1337 (@unknown-user:)"
|
||||
+ "<br><br><strong>Account Roles (7)</strong>"
|
||||
+ "<br>§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping</p>"
|
||||
+ `🖼️ <a href="https://files.inx.moe/p/cdn/lillith.webp">https://files.inx.moe/p/cdn/lillith.webp</a>`
|
||||
|
|
|
|||
|
|
@ -125,8 +125,8 @@ test("message2event embeds: blockquote in embed", async t => {
|
|||
t.equal(called, 1, "should call getJoinedMembers once")
|
||||
})
|
||||
|
||||
test("message2event embeds: extreme html is all escaped", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.extreme_html_escaping, data.guild.general)
|
||||
test("message2event embeds: crazy html is all escaped", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
|
|
@ -204,44 +204,6 @@ test("message2event embeds: author url without name", async t => {
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event embeds: 4 images", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.four_images, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "🔀 <em>Forwarded message</em><br><blockquote><a href=\"https://fixupx.com/i/status/2032003668787020046\">https://fixupx.com/i/status/2032003668787020046</a></blockquote>",
|
||||
"m.mentions": {}
|
||||
}, {
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
body: "» | ## ⏺️ AUTOMATON WEST (@AUTOMATON_ENG) https://x.com/AUTOMATON_ENG/status/2032003668787020046"
|
||||
+ "\n» | "
|
||||
+ "\n» | 4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”"
|
||||
+ "\n» | ︀︀"
|
||||
+ "\n» | ︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)"
|
||||
+ "\n» | "
|
||||
+ "\n» | **[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **"
|
||||
+ "\n» | "
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig"
|
||||
+ "\n» | — FixupX"
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig"
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig"
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<blockquote><blockquote><p><strong><a href=\"https://x.com/AUTOMATON_ENG/status/2032003668787020046\">⏺️ AUTOMATON WEST (@AUTOMATON_ENG)</a></strong></p>"
|
||||
+ "<p>4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non-AI made social network”"
|
||||
+ "<br>︀︀<br>︀︀<a href=\"https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/\">automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/</a>"
|
||||
+ "<br><br><strong><a href=\"https://x.com/intent/tweet?in_reply_to=2032003668787020046\">💬</a> 36 <a href=\"https://x.com/intent/retweet?tweet_id=2032003668787020046\">🔁</a> 212 <a href=\"https://x.com/intent/like?tweet_id=2032003668787020046\">❤</a> 3.0K 👁 131.7K </strong></p>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig</p>— FixupX</blockquote>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig</p>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig</p>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig</p></blockquote>",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event embeds: vx image", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
|
|
|
|||
|
|
@ -789,7 +789,7 @@ test("message2event: simple written @mention for matrix user", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "@ash do you need anything from the store btw as I'm heading there after gym",
|
||||
body: "[@ash](https://matrix.to/#/@she_who_brings_destruction:cadence.moe) do you need anything from the store btw as I'm heading there after gym",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<a href="https://matrix.to/#/@she_who_brings_destruction:cadence.moe">@ash</a> do you need anything from the store btw as I'm heading there after gym`
|
||||
}])
|
||||
|
|
@ -838,7 +838,7 @@ test("message2event: many written @mentions for matrix users", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck",
|
||||
body: "[@Cadence](https://matrix.to/#/@cadence:cadence.moe), tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and [@huck](https://matrix.to/#/@huckleton:cadence.moe)",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<a href="https://matrix.to/#/@cadence:cadence.moe">@Cadence</a>, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and <a href="https://matrix.to/#/@huckleton:cadence.moe">@huck</a>`
|
||||
}])
|
||||
|
|
@ -890,7 +890,7 @@ test("message2event: written @mentions may match part of the name", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "I wonder if @cadence saw this?",
|
||||
body: "I wonder if [@cadence](https://matrix.to/#/@secret:cadence.moe) saw this?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `I wonder if <a href="https://matrix.to/#/@secret:cadence.moe">@cadence</a> saw this?`
|
||||
}])
|
||||
|
|
@ -941,7 +941,7 @@ test("message2event: written @mentions may match part of the mxid", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "I wonder if @huck saw this?",
|
||||
body: "I wonder if [@huck](https://matrix.to/#/@huckleton:cadence.moe) saw this?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `I wonder if <a href="https://matrix.to/#/@huckleton:cadence.moe">@huck</a> saw this?`
|
||||
}])
|
||||
|
|
@ -962,36 +962,6 @@ test("message2event: written @mentions do not match in URLs", async t => {
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event: written @mentions do not match in inline code", async t => {
|
||||
const events = await messageToEvent({
|
||||
...data.message.advanced_written_at_mention_for_matrix,
|
||||
content: "`public @Nullable EntityType<?>`"
|
||||
}, data.guild.general, {}, {})
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
body: "`public @Nullable EntityType<?>`",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<code>public @Nullable EntityType<?></code>`
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: written @mentions do not match in code block", async t => {
|
||||
const events = await messageToEvent({
|
||||
...data.message.advanced_written_at_mention_for_matrix,
|
||||
content: "```java\npublic @Nullable EntityType<?>\n```"
|
||||
}, data.guild.general, {}, {})
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
body: "```java\npublic @Nullable EntityType<?>\n```",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<pre><code class="language-java">public @Nullable EntityType<?></code></pre>`
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: entire message may match elaborate display name", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent({
|
||||
|
|
@ -1037,7 +1007,7 @@ test("message2event: entire message may match elaborate display name", async t =
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆",
|
||||
body: "[@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆](https://matrix.to/#/@wa:cadence.moe)",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<a href="https://matrix.to/#/@wa:cadence.moe">@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆</a>`
|
||||
}])
|
||||
|
|
@ -1114,7 +1084,7 @@ test("message2event: multiple attachments are combined into the same event where
|
|||
formatted_body: "hey"
|
||||
+ `<br>📄 Uploaded file: <a href="https://bridge.example.org/download/discordcdn/123/456/789.mega">hey.jpg</a> (100 MB)`
|
||||
+ `<br><blockquote>📸 Uploaded SPOILER file: <a href="https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg">https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg</a> (38 KB)</blockquote>`
|
||||
+ `📄 Uploaded file: <a href="https://bridge.example.org/download/discordcdn/123/456/789.mega">hey.jpg</a> (100 MB)`
|
||||
+ `<br>📄 Uploaded file: <a href="https://bridge.example.org/download/discordcdn/123/456/789.mega">hey.jpg</a> (100 MB)`
|
||||
}, {
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
|
|
@ -1142,19 +1112,6 @@ test("message2event: type 4 channel name change", async t => {
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event: type 12 channel follow add", async t => {
|
||||
const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.emote",
|
||||
body: "set this room to receive announcements from PluralKit #downtime",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "set this room to receive announcements from <strong>PluralKit #downtime</strong>",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: thread start message reference", async t => {
|
||||
const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, {
|
||||
api: {
|
||||
|
|
@ -1581,28 +1538,6 @@ test("message2event: vc invite event renders embed with room link", async t => {
|
|||
])
|
||||
})
|
||||
|
||||
test("message2event: expired/invalid invites are sent as-is", async t => {
|
||||
const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, {
|
||||
snow: {
|
||||
invite: {
|
||||
async getInvite() {
|
||||
throw new Error(`{"message": "Unknown Invite", "code": 10006}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
body: "https://discord.gg/placeholder?event=1381190945646710824",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<a href=\"https://discord.gg/placeholder?event=1381190945646710824\">https://discord.gg/placeholder?event=1381190945646710824</a>",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
test("message2event: channel links are converted even inside lists (parser post-processer descends into list items)", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function pinsToList(pins, kstate) {
|
|||
/** @type {string[]} */
|
||||
const result = []
|
||||
for (const pin of pins.items) {
|
||||
const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get()
|
||||
const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get()
|
||||
if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID)
|
||||
}
|
||||
result.reverse()
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {db, select, from} = passthrough
|
||||
|
||||
/**
|
||||
* @param {string} userID discord user ID that left
|
||||
* @param {string} guildID discord guild ID that they left
|
||||
*/
|
||||
function removeMemberMxids(userID, guildID) {
|
||||
// Get sims for user and remove
|
||||
let membership = from("sim").join("sim_member", "mxid").join("channel_room", "room_id")
|
||||
.select("room_id", "mxid").where({user_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all()
|
||||
membership = membership.concat(from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").join("channel_room", "room_id")
|
||||
.select("room_id", "mxid").where({proxy_owner_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all())
|
||||
|
||||
// Get user installed apps and remove
|
||||
/** @type {string[]} */
|
||||
let userAppDeletions = []
|
||||
// 1. Select apps that have 1 user remaining
|
||||
/** @type {Set<string>} */
|
||||
const appsWithOneUser = new Set(db.prepare("SELECT app_bot_id FROM app_user_install WHERE guild_id = ? GROUP BY app_bot_id HAVING count(*) = 1").pluck().all(guildID))
|
||||
// 2. Select apps installed by this user
|
||||
const appsFromThisUser = new Set(select("app_user_install", "app_bot_id", {guild_id: guildID, user_id: userID}).pluck().all())
|
||||
if (appsFromThisUser.size) userAppDeletions.push(userID)
|
||||
// Then remove user installed apps if this was the last user with them
|
||||
const appsToRemove = appsWithOneUser.intersection(appsFromThisUser)
|
||||
for (const botID of appsToRemove) {
|
||||
// Remove sims for user installed app
|
||||
const appRemoval = removeMemberMxids(botID, guildID)
|
||||
membership = membership.concat(appRemoval.membership)
|
||||
userAppDeletions = userAppDeletions.concat(appRemoval.userAppDeletions)
|
||||
}
|
||||
|
||||
return {membership, userAppDeletions}
|
||||
}
|
||||
|
||||
module.exports.removeMemberMxids = removeMemberMxids
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const {test} = require("supertape")
|
||||
const {removeMemberMxids} = require("./remove-member-mxids")
|
||||
|
||||
test("remove member mxids: would remove mxid for all rooms in this server", t => {
|
||||
t.deepEqual(removeMemberMxids("772659086046658620", "112760669178241024"), {
|
||||
userAppDeletions: [],
|
||||
membership: [{
|
||||
mxid: "@_ooye_cadence:cadence.moe",
|
||||
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||
}, {
|
||||
mxid: "@_ooye_cadence:cadence.moe",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
test("remove member mxids: removes sims too", t => {
|
||||
t.deepEqual(removeMemberMxids("196188877885538304", "112760669178241024"), {
|
||||
userAppDeletions: [],
|
||||
membership: [{
|
||||
mxid: '@_ooye_ampflower:cadence.moe',
|
||||
room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe'
|
||||
}, {
|
||||
mxid: '@_ooye__pk_zoego:cadence.moe',
|
||||
room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe'
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
test("remove member mxids: removes apps too", t => {
|
||||
t.deepEqual(removeMemberMxids("197126718400626689", "66192955777486848"), {
|
||||
userAppDeletions: ["197126718400626689"],
|
||||
membership: [{
|
||||
mxid: '@_ooye_infinidoge1337:cadence.moe',
|
||||
room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe'
|
||||
}, {
|
||||
mxid: '@_ooye_evil_lillith_sheher:cadence.moe',
|
||||
room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe'
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
|
@ -19,19 +19,21 @@ const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
|||
*/
|
||||
async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, di) {
|
||||
const branchedFromEventID = select("event_message", "event_id", {message_id: thread.id}).pluck().get()
|
||||
/** @type {{"m.mentions"?: any, "m.in_reply_to"?: any}} */
|
||||
/** @type {{"m.mentions"?: any, "m.relates_to"?: {event_id?: string, is_falling_back?: boolean, "m.in_reply_to"?: {event_id: string}, rel_type?: "m.replace"|"m.thread"}}} */
|
||||
const context = {}
|
||||
let suffix = "";
|
||||
if (branchedFromEventID) {
|
||||
// Need to figure out who sent that event...
|
||||
const event = await di.api.getEvent(parentRoomID, branchedFromEventID)
|
||||
context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}}
|
||||
suffix = "\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]";
|
||||
context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}, is_falling_back:false, event_id: event.event_id, rel_type: "m.thread"}
|
||||
if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]}
|
||||
}
|
||||
|
||||
const msgtype = creatorMxid ? "m.emote" : "m.text"
|
||||
const template = creatorMxid ? "started a thread:" : "Thread started:"
|
||||
const template = creatorMxid ? "started a thread" : "New thread started:"
|
||||
const via = await mxUtils.getViaServersQuery(threadRoomID, di.api)
|
||||
let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}?${via.toString()}`
|
||||
let body = `${template} „${thread.name}” in room: https://matrix.to/#/${threadRoomID}?${via.toString()}${suffix}`
|
||||
|
||||
return {
|
||||
msgtype,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ test("thread2announcement: no known creator, no branched from event", async t =>
|
|||
}, {api: viaApi})
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.text",
|
||||
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
body: "New thread started: „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
"m.mentions": {}
|
||||
})
|
||||
})
|
||||
|
|
@ -61,7 +61,7 @@ test("thread2announcement: known creator, no branched from event", async t => {
|
|||
}, {api: viaApi})
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.emote",
|
||||
body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
body: "started a thread „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
"m.mentions": {}
|
||||
})
|
||||
})
|
||||
|
|
@ -85,12 +85,15 @@ test("thread2announcement: no known creator, branched from discord event", async
|
|||
})
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.text",
|
||||
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
body: "New thread started: „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]",
|
||||
"m.mentions": {},
|
||||
"m.relates_to": {
|
||||
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||
"is_falling_back": false,
|
||||
"m.in_reply_to": {
|
||||
event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg"
|
||||
}
|
||||
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -114,12 +117,15 @@ test("thread2announcement: known creator, branched from discord event", async t
|
|||
})
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.emote",
|
||||
body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
body: "started a thread „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]",
|
||||
"m.mentions": {},
|
||||
"m.relates_to": {
|
||||
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||
"is_falling_back": false,
|
||||
"m.in_reply_to": {
|
||||
event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg"
|
||||
}
|
||||
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -143,14 +149,51 @@ test("thread2announcement: no known creator, branched from matrix event", async
|
|||
})
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.text",
|
||||
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||
body: "New thread started: „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]",
|
||||
"m.mentions": {
|
||||
user_ids: ["@cadence:cadence.moe"]
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||
"is_falling_back": false,
|
||||
"m.in_reply_to": {
|
||||
event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4"
|
||||
}
|
||||
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("thread2announcement: known creator, branched from matrix event", async t => {
|
||||
const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", "@_ooye_crunch_god:cadence.moe", {
|
||||
name: "test thread",
|
||||
id: "1128118177155526666"
|
||||
}, {
|
||||
api: {
|
||||
getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "so can you reply to my webhook uwu"
|
||||
},
|
||||
sender: "@cadence:cadence.moe"
|
||||
}),
|
||||
...viaApi
|
||||
}
|
||||
})
|
||||
t.deepEqual(content, {
|
||||
msgtype: "m.emote",
|
||||
body: "started a thread „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]",
|
||||
"m.mentions": {
|
||||
user_ids: ["@cadence:cadence.moe"]
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||
"is_falling_back": false,
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const {test} = require("supertape")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const assert = require("assert")
|
||||
const data = require("../../../test/data")
|
||||
const {userToSimName, webhookAuthorToSimName} = require("./user-to-mxid")
|
||||
|
|
|
|||
|
|
@ -52,11 +52,7 @@ class DiscordClient {
|
|||
/** @type {Map<string, Array<string>>} */
|
||||
this.guildChannelMap = new Map()
|
||||
if (listen !== "no") {
|
||||
this.cloud.on("event", message => {
|
||||
process.nextTick(() => {
|
||||
discordPackets.onPacket(this, message, listen)
|
||||
})
|
||||
})
|
||||
this.cloud.on("event", message => discordPackets.onPacket(this, message, listen))
|
||||
}
|
||||
|
||||
const addEventLogger = (eventName, logName) => {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const utils = {
|
|||
client.user = message.d.user
|
||||
client.application = message.d.application
|
||||
console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`)
|
||||
interactions.registerInteractions()
|
||||
|
||||
} else if (message.t === "GUILD_CREATE") {
|
||||
message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web
|
||||
|
|
@ -48,10 +47,10 @@ const utils = {
|
|||
|
||||
if (listen === "full") {
|
||||
try {
|
||||
interactions.registerInteractions()
|
||||
await eventDispatcher.checkMissedExpressions(message.d)
|
||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||
await eventDispatcher.checkMissedPins(client, message.d)
|
||||
await eventDispatcher.checkMissedLeaves(client, message.d)
|
||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||
} catch (e) {
|
||||
console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
|
||||
console.error(e)
|
||||
|
|
|
|||
|
|
@ -32,16 +32,12 @@ const speedbump = sync.require("./actions/speedbump")
|
|||
const retrigger = sync.require("./actions/retrigger")
|
||||
/** @type {import("./actions/set-presence")} */
|
||||
const setPresence = sync.require("./actions/set-presence")
|
||||
/** @type {import("./actions/remove-member")} */
|
||||
const removeMember = sync.require("./actions/remove-member")
|
||||
/** @type {import("./actions/poll-vote")} */
|
||||
const vote = sync.require("./actions/poll-vote")
|
||||
/** @type {import("../m2d/event-dispatcher")} */
|
||||
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
||||
/** @type {import("../discord/interactions/matrix-info")} */
|
||||
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
||||
/** @type {import("../agi/listener")} */
|
||||
const agiListener = sync.require("../agi/listener")
|
||||
|
||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||
const checkMissedPinsSema = new Semaphore()
|
||||
|
|
@ -127,7 +123,6 @@ module.exports = {
|
|||
// Send in order
|
||||
for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) {
|
||||
const message = messages[i]
|
||||
if (message.type === DiscordTypes.MessageType.UserJoin) continue // since join announcements don't become events, it would be a repetition to act on them during backfill
|
||||
|
||||
if (!members.has(message.author.id)) members.set(message.author.id, await client.snow.guild.getGuildMember(guild.id, message.author.id).catch(() => undefined))
|
||||
await module.exports.MESSAGE_CREATE(client, {
|
||||
|
|
@ -177,31 +172,6 @@ module.exports = {
|
|||
await createSpace.syncSpaceExpressions(data, true)
|
||||
},
|
||||
|
||||
/**
|
||||
* When logging back in, check if any members left while we were gone.
|
||||
* Do this by getting the member list from Discord and seeing who we still have locally that isn't there in the response.
|
||||
* @param {import("./discord-client")} client
|
||||
* @param {DiscordTypes.GatewayGuildCreateDispatchData} guild
|
||||
*/
|
||||
async checkMissedLeaves(client, guild) {
|
||||
const maxLimit = 1000
|
||||
if (guild.member_count >= maxLimit) return // too large to want to scan
|
||||
const discordMembers = await client.snow.guild.getGuildMembers(guild.id, {limit: maxLimit})
|
||||
if (discordMembers.length >= maxLimit) return // response was maxed out, there are guild members that weren't listed, can't act safely
|
||||
const discordMembersSet = new Set(discordMembers.map(m => m.user.id))
|
||||
// no indexes on this one but I'll cope
|
||||
const membersAddedOnMatrix = new Set(from("sim").join("sim_member", "mxid").join("channel_room", "room_id")
|
||||
.pluck("user_id").selectUnsafe("DISTINCT user_id").where({guild_id: guild.id}).and("AND user_id not like '%-%' and user_id not like '%\\_%' escape '\\'").all())
|
||||
const userInstalledAppIDs = new Set(from("app_user_install").pluck("app_bot_id").selectUnsafe("DISTINCT app_bot_id").where({guild_id: guild.id}).all())
|
||||
// loop over members added on matrix and if the member does not exist on discord-side then they should be removed
|
||||
for (const userID of membersAddedOnMatrix) {
|
||||
if (userInstalledAppIDs.has(userID)) continue // skip user installed apps here since they're never true members - they'll be removed by removeMember when the associated user is removed
|
||||
if (!discordMembersSet.has(userID)) {
|
||||
await removeMember.removeMember(userID, guild.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces to the parent room that the thread room has been created.
|
||||
* See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement"
|
||||
|
|
@ -241,14 +211,6 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import("./discord-client")} client
|
||||
* @param {DiscordTypes.GatewayGuildMemberRemoveDispatchData} data
|
||||
*/
|
||||
async GUILD_MEMBER_REMOVE(client, data) {
|
||||
await removeMember.removeMember(data.user.id, data.guild_id)
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import("./discord-client")} client
|
||||
* @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread
|
||||
|
|
@ -305,10 +267,7 @@ module.exports = {
|
|||
|
||||
if (message.webhook_id) {
|
||||
const row = select("webhook", "webhook_id", {webhook_id: message.webhook_id}).pluck().get()
|
||||
if (row) { // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
||||
await agiListener.process(message, channel, guild, true)
|
||||
return
|
||||
}
|
||||
if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
||||
}
|
||||
|
||||
if (dUtils.isEphemeralMessage(message)) return // Ephemeral messages are for the eyes of the receiver only!
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "role_default" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"role_id" TEXT NOT NULL,
|
||||
PRIMARY KEY ("guild_id", "role_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "app_user_install" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"app_bot_id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
PRIMARY KEY ("guild_id", "app_bot_id", "user_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "agi_prior_message" (
|
||||
"channel_id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"avatar_url" TEXT NOT NULL,
|
||||
"use_caps" INTEGER NOT NULL,
|
||||
"use_punct" INTEGER NOT NULL,
|
||||
"use_apos" INTEGER NOT NULL,
|
||||
"timestamp" INTEGER NOT NULL,
|
||||
PRIMARY KEY("channel_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE "agi_optout" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
PRIMARY KEY("guild_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE "agi_cooldown" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"timestamp" INTEGER,
|
||||
PRIMARY KEY("guild_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
30
src/db/orm-defs.d.ts
vendored
30
src/db/orm-defs.d.ts
vendored
|
|
@ -1,29 +1,4 @@
|
|||
export type Models = {
|
||||
agi_prior_message: {
|
||||
channel_id: string
|
||||
username: string
|
||||
avatar_url: string
|
||||
use_caps: number
|
||||
use_punct: number
|
||||
use_apos: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
agi_optout: {
|
||||
guild_id: string
|
||||
}
|
||||
|
||||
agi_cooldown: {
|
||||
guild_id: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
app_user_install: {
|
||||
guild_id: string
|
||||
app_bot_id: string
|
||||
user_id: string
|
||||
}
|
||||
|
||||
auto_emoji: {
|
||||
name: string
|
||||
emoji_id: string
|
||||
|
|
@ -129,11 +104,6 @@ export type Models = {
|
|||
historical_room_index: number
|
||||
}
|
||||
|
||||
role_default: {
|
||||
guild_id: string
|
||||
role_id: string
|
||||
}
|
||||
|
||||
room_upgrade_pending: {
|
||||
new_room_id: string
|
||||
old_room_id: string
|
||||
|
|
|
|||
|
|
@ -62,29 +62,7 @@ async function _interact({guild_id, data}, {api}) {
|
|||
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
||||
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
||||
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
||||
let name = matrixMember?.displayname || event.sender
|
||||
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||
|
||||
// Check for per-message profile
|
||||
const perMessageProfile = event.content?.["com.beeper.per_message_profile"]
|
||||
let profileNote = ""
|
||||
if (perMessageProfile) {
|
||||
if (perMessageProfile.displayname) {
|
||||
name = perMessageProfile.displayname
|
||||
}
|
||||
if ("avatar_url" in perMessageProfile) {
|
||||
if (perMessageProfile.avatar_url) {
|
||||
// use provided avatar_url
|
||||
avatar = utils.getPublicUrlForMxc(perMessageProfile.avatar_url)
|
||||
} else if (perMessageProfile.avatar_url === "") {
|
||||
// empty string avatar_url clears the avatar
|
||||
avatar = undefined
|
||||
}
|
||||
// else, omitted/null falls back to member avatar
|
||||
}
|
||||
profileNote = "Sent with a per-message profile.\n"
|
||||
}
|
||||
|
||||
const name = matrixMember?.displayname || event.sender
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
|
|
@ -92,9 +70,9 @@ async function _interact({guild_id, data}, {api}) {
|
|||
author: {
|
||||
name,
|
||||
url: `https://matrix.to/#/${event.sender}`,
|
||||
icon_url: avatar
|
||||
icon_url: utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||
},
|
||||
description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n${profileNote}**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
|
||||
description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
|
||||
color: 0x0dbd8b,
|
||||
fields: [{
|
||||
name: "In Channels",
|
||||
|
|
|
|||
|
|
@ -85,118 +85,3 @@ test("matrix info: shows info for matrix source message", async t => {
|
|||
)
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("matrix info: shows username for per-message profile", async t => {
|
||||
let called = 0
|
||||
const msg = await _interact({
|
||||
data: {
|
||||
target_id: "1128118177155526666",
|
||||
resolved: {
|
||||
messages: {
|
||||
"1141501302736695316": data.message.simple_reply_to_matrix_user
|
||||
}
|
||||
}
|
||||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||
return {
|
||||
event_id: eventID,
|
||||
room_id: roomID,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "master chief: i like the halo",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<strong>master chief: </strong>i like the halo",
|
||||
"com.beeper.per_message_profile": {
|
||||
has_fallback: true,
|
||||
displayname: "master chief",
|
||||
avatar_url: ""
|
||||
}
|
||||
},
|
||||
sender: "@cadence:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getJoinedMembers(roomID) {
|
||||
return {
|
||||
joined: {}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
return {
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(msg.data.embeds[0].author.name, "master chief")
|
||||
t.match(msg.data.embeds[0].description, "Sent with a per-message profile")
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("matrix info: shows avatar for per-message profile", async t => {
|
||||
let called = 0
|
||||
const msg = await _interact({
|
||||
data: {
|
||||
target_id: "1128118177155526666",
|
||||
resolved: {
|
||||
messages: {
|
||||
"1141501302736695316": data.message.simple_reply_to_matrix_user
|
||||
}
|
||||
}
|
||||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||
return {
|
||||
event_id: eventID,
|
||||
room_id: roomID,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "?",
|
||||
"com.beeper.per_message_profile": {
|
||||
avatar_url: "mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc"
|
||||
}
|
||||
},
|
||||
sender: "@mystery:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getJoinedMembers(roomID) {
|
||||
return {
|
||||
joined: {}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
return {
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(msg.data.embeds[0].author.name, "@mystery:cadence.moe")
|
||||
t.equal(msg.data.embeds[0].author.icon_url, "https://bridge.example.org/download/matrix/cadence.moe/HXfFuougamkURPPMflTJRxGc")
|
||||
t.match(msg.data.embeds[0].description, "Sent with a per-message profile")
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const assert = require("assert").strict
|
|||
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
const {db, select} = require("../passthrough")
|
||||
const {db} = require("../passthrough")
|
||||
|
||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
||||
let hasher = null
|
||||
|
|
@ -58,15 +58,6 @@ function getPermissions(guildID, userRoles, guildRoles, userID, channelOverwrite
|
|||
return allowed
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{id: string, roles: DiscordTypes.APIGuild["roles"]}} guild
|
||||
* @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channel]
|
||||
*/
|
||||
function getDefaultPermissions(guild, channel) {
|
||||
const defaultRoles = select("role_default", "role_id", {guild_id: guild.id}).pluck().all()
|
||||
return getPermissions(guild.id, defaultRoles, guild.roles, undefined, channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`.
|
||||
* It is designed like this to avoid developer error with bit manipulations.
|
||||
|
|
@ -114,7 +105,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
|
|||
* @param {DiscordTypes.APIMessage} message
|
||||
*/
|
||||
function isWebhookMessage(message) {
|
||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand && message.type !== DiscordTypes.MessageType.ContextMenuCommand
|
||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,7 +174,6 @@ function filterTo(xs, fn) {
|
|||
}
|
||||
|
||||
module.exports.getPermissions = getPermissions
|
||||
module.exports.getDefaultPermissions = getDefaultPermissions
|
||||
module.exports.hasPermission = hasPermission
|
||||
module.exports.hasSomePermissions = hasSomePermissions
|
||||
module.exports.hasAllPermissions = hasAllPermissions
|
||||
|
|
|
|||
|
|
@ -39,14 +39,20 @@ async function resolvePendingFiles(message) {
|
|||
if ("key" in p) {
|
||||
// Encrypted file
|
||||
const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url"))
|
||||
await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(res.body).pipe(d))
|
||||
await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(
|
||||
// @ts-ignore
|
||||
res.body
|
||||
).pipe(d))
|
||||
return {
|
||||
name: p.name,
|
||||
file: d
|
||||
}
|
||||
} else {
|
||||
// Unencrypted file
|
||||
const body = await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(res.body))
|
||||
const body = await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(
|
||||
// @ts-ignore
|
||||
res.body
|
||||
))
|
||||
return {
|
||||
name: p.name,
|
||||
file: body
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const sharp = require("sharp")
|
|||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../../matrix/mreq")} */
|
||||
const mreq = sync.require("../../matrix/mreq")
|
||||
const {streamType} = require("@cloudrac3r/stream-type")
|
||||
const streamMimeType = require("stream-mime-type")
|
||||
|
||||
const WIDTH = 160
|
||||
const HEIGHT = 160
|
||||
|
|
@ -26,13 +26,13 @@ async function getAndResizeSticker(mxc) {
|
|||
}
|
||||
|
||||
const streamIn = Readable.fromWeb(res.body)
|
||||
const {streamThrough, type} = await streamType(streamIn)
|
||||
const animated = ["image/gif", "image/webp"].includes(type)
|
||||
const { stream, mime } = await streamMimeType.getMimeType(streamIn)
|
||||
const animated = ["image/gif", "image/webp"].includes(mime)
|
||||
|
||||
const transformer = sharp({animated: animated})
|
||||
.resize(WIDTH, HEIGHT, {fit: "inside", background: {r: 0, g: 0, b: 0, alpha: 0}})
|
||||
.webp()
|
||||
streamThrough.pipe(transformer)
|
||||
stream.pipe(transformer)
|
||||
return Readable.toWeb(transformer)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async function updatePins(pins, prev) {
|
|||
const diff = diffPins.diffPins(pins, prev)
|
||||
for (const [event_id, added] of diff) {
|
||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||
.select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get()
|
||||
.select("reference_channel_id", "message_id").get()
|
||||
if (!row) continue
|
||||
if (added) {
|
||||
discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const {pipeline} = require("stream").promises
|
|||
const sharp = require("sharp")
|
||||
const {GIFrame} = require("@cloudrac3r/giframe")
|
||||
const {PNG} = require("@cloudrac3r/pngjs")
|
||||
const {streamType} = require("@cloudrac3r/stream-type")
|
||||
const streamMimeType = require("stream-mime-type")
|
||||
|
||||
const SIZE = 48
|
||||
const RESULT_WIDTH = 400
|
||||
|
|
@ -54,11 +54,11 @@ async function compositeMatrixEmojis(mxcs, mxcDownloader) {
|
|||
* @returns {Promise<Buffer | undefined>} Uncompressed PNG image
|
||||
*/
|
||||
async function convertImageStream(streamIn, stopStream) {
|
||||
const {streamThrough, type} = await streamType(streamIn)
|
||||
assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(type), `Mime type ${type} is impossible for emojis`)
|
||||
const {stream, mime} = await streamMimeType.getMimeType(streamIn)
|
||||
assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(mime), `Mime type ${mime} is impossible for emojis`)
|
||||
|
||||
try {
|
||||
if (type === "image/png" || type === "image/jpeg" || type === "image/webp") {
|
||||
if (mime === "image/png" || mime === "image/jpeg" || mime === "image/webp") {
|
||||
/** @type {{info: sharp.OutputInfo, buffer: Buffer}} */
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const transformer = sharp()
|
||||
|
|
@ -70,15 +70,15 @@ async function convertImageStream(streamIn, stopStream) {
|
|||
resolve({info, buffer})
|
||||
})
|
||||
pipeline(
|
||||
streamThrough,
|
||||
stream,
|
||||
transformer
|
||||
)
|
||||
})
|
||||
return result.buffer
|
||||
|
||||
} else if (type === "image/gif") {
|
||||
} else if (mime === "image/gif") {
|
||||
const giframe = new GIFrame(0)
|
||||
streamThrough.on("data", chunk => {
|
||||
stream.on("data", chunk => {
|
||||
giframe.feed(chunk)
|
||||
})
|
||||
const frame = await giframe.getFrame()
|
||||
|
|
@ -91,10 +91,10 @@ async function convertImageStream(streamIn, stopStream) {
|
|||
.toBuffer({resolveWithObject: true})
|
||||
return buffer.data
|
||||
|
||||
} else if (type === "image/apng") {
|
||||
} else if (mime === "image/apng") {
|
||||
const png = new PNG({maxFrames: 1})
|
||||
// @ts-ignore
|
||||
streamThrough.pipe(png)
|
||||
stream.pipe(png)
|
||||
/** @type {Buffer} */ // @ts-ignore
|
||||
const frame = await new Promise(resolve => png.on("parsed", resolve))
|
||||
stopStream()
|
||||
|
|
|
|||
|
|
@ -471,8 +471,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
|||
// @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]),
|
||||
ensureJoined: [],
|
||||
allowedMentionsParse: ["everyone"],
|
||||
allowedMentionsUsers: []
|
||||
/**@type {DiscordTypes.AllowedMentionsTypes[]}*/ // @ts-ignore - TypeScript is for whatever reason conviced that "everyone" cannot be assigned to AllowedMentionsTypes, but if you „Go to Definition”, you'll see that "everyone" is a valid enum value.
|
||||
allowedMentionsParse: ["everyone"]
|
||||
}
|
||||
}
|
||||
} else if (writtenMentionMatch[1].length < 40) { // the API supports up to 100 characters, but really if you're searching more than 40, something messed up
|
||||
|
|
@ -483,8 +483,7 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
|||
// @ts-ignore - typescript doesn't know about indices yet
|
||||
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]),
|
||||
ensureJoined: [results[0].user],
|
||||
allowedMentionsParse: [],
|
||||
allowedMentionsUsers: [results[0].user.id]
|
||||
allowedMentionsParse: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -545,35 +544,18 @@ async function getL1L2ReplyLine(called = false) {
|
|||
async function eventToMessage(event, guild, channel, di) {
|
||||
let displayName = event.sender
|
||||
let avatarURL = undefined
|
||||
/**@type {DiscordTypes.AllowedMentionsTypes[]}*/ // @ts-ignore - TypeScript is for whatever reason conviced that neither "users" no "roles" cannot be assigned to AllowedMentionsTypes, but if you „Go to Definition”, you'll see that both are valid enum values.
|
||||
const allowedMentionsParse = ["users", "roles"]
|
||||
const allowedMentionsUsers = []
|
||||
/** @type {string[]} */
|
||||
let messageIDsToEdit = []
|
||||
let replyLine = ""
|
||||
|
||||
// Extract a basic display name from the sender
|
||||
const match = event.sender.match(/^@(.*?):/)
|
||||
if (match) displayName = match[1]
|
||||
|
||||
// Try to extract an accurate display name and avatar URL from the member event
|
||||
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
|
||||
if (member.displayname) displayName = member.displayname
|
||||
if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url)
|
||||
|
||||
// MSC4144: Override display name and avatar from per-message profile if present
|
||||
const perMessageProfile = event.content["com.beeper.per_message_profile"]
|
||||
if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname
|
||||
if (perMessageProfile && "avatar_url" in perMessageProfile) {
|
||||
if (perMessageProfile.avatar_url) {
|
||||
// use provided avatar_url
|
||||
avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url)
|
||||
} else if (perMessageProfile.avatar_url === "") {
|
||||
// empty string avatar_url clears the avatar
|
||||
avatarURL = undefined
|
||||
}
|
||||
// else, omitted/null falls back to member avatar
|
||||
}
|
||||
|
||||
// If the display name is too long to be put into the webhook (80 characters is the maximum),
|
||||
// put the excess characters into displayNameRunoff, later to be put at the top of the message
|
||||
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
|
||||
|
|
@ -783,7 +765,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
// Generate a reply preview for a standard message
|
||||
repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body
|
||||
repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
|
||||
repliedToContent = repliedToContent.replace(/(?:\n|<br ?\/?>)+/g, " ") // Should all be on one line
|
||||
repliedToContent = repliedToContent.replace(/(?:\n|<br>)+/g, " ") // Should all be on one line
|
||||
repliedToContent = repliedToContent.replace(/<span [^>]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.)
|
||||
repliedToContent = repliedToContent.replace(/<img([^>]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown
|
||||
const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/)
|
||||
|
|
@ -876,9 +858,8 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
const doc = domino.createDocument(
|
||||
// DOM parsers arrange elements in the <head> and <body>. Wrapping in a custom element ensures elements are reliably arranged in a single element.
|
||||
'<x-turndown id="turndown-root">' + input + '</x-turndown>'
|
||||
)
|
||||
const root = doc.getElementById("turndown-root")
|
||||
assert(root)
|
||||
);
|
||||
const root = doc.getElementById("turndown-root");
|
||||
async function forEachNode(event, node) {
|
||||
for (; node; node = node.nextSibling) {
|
||||
// Check written mentions
|
||||
|
|
@ -919,7 +900,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<"
|
||||
if (!shouldSuppress && guild?.roles) {
|
||||
// Suppress if regular users don't have permission
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel?.permission_overwrites)
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||
shouldSuppress = !canEmbedLinks
|
||||
}
|
||||
|
|
@ -931,7 +912,6 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
}
|
||||
}
|
||||
await forEachNode(event, root)
|
||||
if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove())
|
||||
|
||||
// SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
|
||||
// First we need to determine which emojis are at the end.
|
||||
|
|
@ -963,10 +943,6 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
} else {
|
||||
// Looks like we're using the plaintext body!
|
||||
content = event.content.body
|
||||
if (perMessageProfile?.has_fallback && perMessageProfile.displayname && content.startsWith(perMessageProfile.displayname + ": ")) {
|
||||
// Strip the display name prefix fallback added for clients that don't support per-message profiles
|
||||
content = content.slice(perMessageProfile.displayname.length + 2)
|
||||
}
|
||||
|
||||
if (event.content.msgtype === "m.emote") {
|
||||
content = `* ${displayName} ${content}`
|
||||
|
|
@ -987,7 +963,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
|
||||
// Suppress if regular users don't have permission
|
||||
if (!shouldSuppress && guild?.roles) {
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites)
|
||||
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||
shouldSuppress = !canEmbedLinks
|
||||
}
|
||||
|
|
@ -1012,34 +988,16 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
}
|
||||
}
|
||||
|
||||
// Complete content
|
||||
content = displayNameRunoff + replyLine + content
|
||||
|
||||
// Split into 2000 character chunks
|
||||
const chunks = chunk(content, 2000)
|
||||
|
||||
// If m.mentions is specified and valid, overwrite allowedMentionsParse with a converted m.mentions
|
||||
let allowed_mentions = {parse: allowedMentionsParse}
|
||||
if (event.content["m.mentions"]) {
|
||||
// Combine requested mentions with detected written mentions to get the full list
|
||||
if (Array.isArray(event.content["m.mentions"].user_ids)) {
|
||||
for (const mxid of event.content["m.mentions"].user_ids) {
|
||||
const user_id = select("sim", "user_id", {mxid}).pluck().get()
|
||||
if (!user_id) continue
|
||||
allowedMentionsUsers.push(
|
||||
select("sim_proxy", "proxy_owner_id", {user_id}).pluck().get() || user_id
|
||||
)
|
||||
}
|
||||
}
|
||||
// Specific mentions were requested, so do not parse users
|
||||
allowed_mentions.parse = allowed_mentions.parse.filter(x => x !== "users")
|
||||
allowed_mentions.users = allowedMentionsUsers
|
||||
}
|
||||
|
||||
// Assemble chunks into Discord messages content
|
||||
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
||||
const messages = chunks.map(content => ({
|
||||
content,
|
||||
allowed_mentions,
|
||||
allowed_mentions: {
|
||||
parse: allowedMentionsParse
|
||||
},
|
||||
username: displayNameShortened,
|
||||
avatar_url: avatarURL
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -266,8 +266,7 @@ test("event2message: markdown in link text does not attempt to be escaped becaus
|
|||
content: "hey [@mario sports mix [she/her]](<https://matrix.to/#/%40cadence%3Acadence.moe>), is it possible to listen on a unix socket?",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -548,8 +547,7 @@ test("event2message: links don't have angle brackets added by accident", async t
|
|||
content: "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -1298,8 +1296,7 @@ test("event2message: lists have appropriate line breaks", async t => {
|
|||
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`,
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -1340,8 +1337,7 @@ test("event2message: ordered list start attribute works", async t => {
|
|||
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`,
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -1467,118 +1463,6 @@ test("event2message: rich reply to a sim user", async t => {
|
|||
)
|
||||
})
|
||||
|
||||
test("event2message: rich reply to a sim user, explicitly enabling mentions in client", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
"type": "m.room.message",
|
||||
"sender": "@cadence:cadence.moe",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04?via=cadence.moe&via=feather.onl\">In reply to</a> <a href=\"https://matrix.to/#/@_ooye_kyuugryphon:cadence.moe\">@_ooye_kyuugryphon:cadence.moe</a><br>Slow news day.</blockquote></mx-reply>Testing this reply, ignore",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
|
||||
}
|
||||
},
|
||||
"m.mentions": {
|
||||
user_ids: ["@_ooye_kyuugryphon:cadence.moe"]
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1693029683016,
|
||||
"unsigned": {
|
||||
"age": 91,
|
||||
"transaction_id": "m1693029682894.510"
|
||||
},
|
||||
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
|
||||
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||
}, data.guild.general, data.channel.general, {
|
||||
api: {
|
||||
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Slow news day."
|
||||
},
|
||||
sender: "@_ooye_kyuugryphon:cadence.moe"
|
||||
})
|
||||
}
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:"
|
||||
+ " Slow news day."
|
||||
+ "\nTesting this reply, ignore",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: ["111604486476181504"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: rich reply to a sim user, explicitly disabling mentions in client", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
"type": "m.room.message",
|
||||
"sender": "@cadence:cadence.moe",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04?via=cadence.moe&via=feather.onl\">In reply to</a> <a href=\"https://matrix.to/#/@_ooye_kyuugryphon:cadence.moe\">@_ooye_kyuugryphon:cadence.moe</a><br>Slow news day.</blockquote></mx-reply>Testing this reply, ignore",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
|
||||
}
|
||||
},
|
||||
"m.mentions": {}
|
||||
},
|
||||
"origin_server_ts": 1693029683016,
|
||||
"unsigned": {
|
||||
"age": 91,
|
||||
"transaction_id": "m1693029682894.510"
|
||||
},
|
||||
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
|
||||
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||
}, data.guild.general, data.channel.general, {
|
||||
api: {
|
||||
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Slow news day."
|
||||
},
|
||||
sender: "@_ooye_kyuugryphon:cadence.moe"
|
||||
})
|
||||
}
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:"
|
||||
+ " Slow news day."
|
||||
+ "\nTesting this reply, ignore",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: rich reply to a rich reply to a multi-line message should correctly strip reply fallback", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
@ -1943,9 +1827,9 @@ test("event2message: should suppress embeds for links in reply preview", async t
|
|||
sender: "@rnl:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU?via=cadence.moe">In reply to</a> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence:cadence.moe</a><br>https://www.youtube.com/watch?v=uX32idb1jMw</blockquote></mx-reply>Everyone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU?via=cadence.moe">In reply to</a> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence:cadence.moe</a><br>https://www.youtube.com/watch?v=uX32idb1jMw</blockquote></mx-reply>Everyone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
|
||||
|
|
@ -1975,7 +1859,7 @@ test("event2message: should suppress embeds for links in reply preview", async t
|
|||
username: "RNL",
|
||||
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:"
|
||||
+ " <https://www.youtube.com/watch?v=uX32idb1jMw>"
|
||||
+ `\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
+ `\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
|
|
@ -4863,17 +4747,17 @@ test("event2message: stickers work", async t => {
|
|||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "[get_real2](https://bridge.example.org/download/sticker/cadence.moe/NyMXQFAAdniImbHzsygScbmN/_.webp)",
|
||||
content: "",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
attachments: [{id: "0", filename: "get_real2.gif"}],
|
||||
pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}]
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: stickers fetch mimetype from server when mimetype not provided", async t => {
|
||||
let called = 0
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.sticker",
|
||||
|
|
@ -4884,6 +4768,20 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
|
|||
},
|
||||
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}, {}, {}, {
|
||||
api: {
|
||||
async getMedia(mxc, options) {
|
||||
called++
|
||||
t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf")
|
||||
t.equal(options.method, "HEAD")
|
||||
return {
|
||||
status: 200,
|
||||
headers: new Map([
|
||||
["content-type", "image/gif"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
|
|
@ -4891,14 +4789,48 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
|
|||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "[YESYESYES](https://bridge.example.org/download/sticker/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf/_.webp)",
|
||||
content: "",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
attachments: [{id: "0", filename: "YESYESYES.gif"}],
|
||||
pendingFiles: [{name: "YESYESYES.gif", mxc: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}]
|
||||
}]
|
||||
}
|
||||
)
|
||||
t.equal(called, 1, "sticker headers should be fetched")
|
||||
})
|
||||
|
||||
test("event2message: stickers with unknown mimetype are not allowed", async t => {
|
||||
let called = 0
|
||||
try {
|
||||
await eventToMessage({
|
||||
type: "m.sticker",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
body: "something",
|
||||
url: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe"
|
||||
},
|
||||
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}, {}, {}, {
|
||||
api: {
|
||||
async getMedia(mxc, options) {
|
||||
called++
|
||||
t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe")
|
||||
t.equal(options.method, "HEAD")
|
||||
return {
|
||||
status: 404,
|
||||
headers: new Map([
|
||||
["content-type", "application/json"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
/* c8 ignore next */
|
||||
t.fail("should throw an error")
|
||||
} catch (e) {
|
||||
t.match(e.toString(), "mimetype")
|
||||
}
|
||||
})
|
||||
|
||||
test("event2message: static emojis work", async t => {
|
||||
|
|
@ -5526,141 +5458,6 @@ test("event2message: known and unknown emojis in the end are used for sprite she
|
|||
)
|
||||
})
|
||||
|
||||
test("event2message: com.beeper.per_message_profile overrides displayname and avatar_url", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello from unstable profile",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "custom-id",
|
||||
displayname: "Unstable Name",
|
||||
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo"
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "Unstable Name",
|
||||
content: "hello from unstable profile",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello with cleared avatar",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "no-avatar",
|
||||
displayname: "No Avatar User",
|
||||
avatar_url: ""
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "No Avatar User",
|
||||
content: "hello with cleared avatar",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Tidus Herboren: one more test",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<strong data-mx-profile-fallback>Tidus Herboren: </strong>one more test",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "tidus",
|
||||
displayname: "Tidus Herboren",
|
||||
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
|
||||
has_fallback: true
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "Tidus Herboren",
|
||||
content: "one more test",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: displayname prefix is stripped from plain body when per-message profile has_fallback", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Tidus Herboren: one more test",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "tidus",
|
||||
displayname: "Tidus Herboren",
|
||||
has_fallback: true
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "Tidus Herboren",
|
||||
content: "one more test",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
|
|||
|
|
@ -156,9 +156,14 @@ async function sendError(roomID, source, type, e, payload) {
|
|||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {(event: Ty.Event.Outer<any> & {type: any, redacts:any, state_key:any}, ...args: any)=>any} fn
|
||||
*/
|
||||
function guard(type, fn) {
|
||||
return async function(event, ...args) {
|
||||
return async function(/** @type {Ty.Event.Outer<any>} */ event, /** @type {any} */ ...args) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
return await fn(event, ...args)
|
||||
} catch (e) {
|
||||
await sendError(event.room_id, "Matrix", type, e, event)
|
||||
|
|
@ -207,10 +212,32 @@ async event => {
|
|||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||
const messageResponses = await sendEvent.sendEvent(event)
|
||||
if (!messageResponses.length) return
|
||||
|
||||
/** @type {string|undefined} */
|
||||
let executedCommand
|
||||
if (event.type === "m.room.message" && event.content.msgtype === "m.text") {
|
||||
// @ts-ignore
|
||||
await matrixCommandHandler.execute(event)
|
||||
executedCommand = await matrixCommandHandler.parseAndExecute(
|
||||
// @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" check ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here)
|
||||
event
|
||||
)
|
||||
}
|
||||
if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){
|
||||
const bridgedTo = utils.getThreadRoomFromThreadEvent(event.content["m.relates_to"].event_id)
|
||||
api.sendEvent(event.room_id, "m.room.message", {
|
||||
body: "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply **may be distracting** to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString() : "Please run `/thread [Optional: Thread Name]` to create such a room for this thread, or get a link to it if someone else has already done so. If you run `/thread` (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "⚠️ <strong>This message may not have been bridged to Discord in the way you thought it was gonna be!</strong><br><br>It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. <em>In other words: <u>Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.</u> If the thread you sent this message in is old, such a random reply <strong>may be distracting</strong> to Discord users!</em><br><br>For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on <a href=\"https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString()+"\">"+bridgedTo+"</a>" : "Please run <code>/thread [Optional: Thread Name]</code> to create such a room for this thread, or get a link to it if someone else has already done so. If you run <code>/thread</code> (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".<br><br><em>You can read more about the rationale behind this design choice <a href=\"https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md\">here</a>.</em>",
|
||||
"m.mentions": { "user_ids": [event.sender]},
|
||||
"m.relates_to": {
|
||||
event_id: event.content["m.relates_to"].event_id,
|
||||
is_falling_back: false,
|
||||
"m.in_reply_to": { event_id: event.event_id },
|
||||
rel_type: "m.thread"
|
||||
},
|
||||
msgtype: "m.text"
|
||||
})
|
||||
}
|
||||
|
||||
retrigger.messageFinishedBridging(event.event_id)
|
||||
await api.ackEvent(event)
|
||||
}))
|
||||
|
|
@ -413,7 +440,6 @@ async event => {
|
|||
console.error(e)
|
||||
return await api.leaveRoomWithReason(event.room_id, `I wasn't able to find out what this room is. Please report this as a bug. Check console for more details. (${e.toString()})`)
|
||||
}
|
||||
if (inviteRoomState?.encryption) return await api.leaveRoomWithReason(event.room_id, "Encrypted rooms are not supported for bridging. Please use an unencrypted room.")
|
||||
if (!inviteRoomState?.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.`)
|
||||
await api.joinRoom(event.room_id)
|
||||
db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar)
|
||||
|
|
@ -423,10 +449,7 @@ async event => {
|
|||
|
||||
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
||||
// Member is gone
|
||||
// if Matrix member, data was cached in member_cache
|
||||
db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
|
||||
// if Discord member (so kicked/banned by Matrix user), data was cached in sim_member
|
||||
db.prepare("DELETE FROM sim_member WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
|
||||
|
||||
// Unregister room's use as a direct chat and/or an invite target if the bot itself left
|
||||
if (event.state_key === utils.bot) {
|
||||
|
|
@ -487,20 +510,6 @@ async event => {
|
|||
await roomUpgrade.onTombstone(event, api)
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:m.room.encryption", guard("m.room.encryption",
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Encryption>} event
|
||||
*/
|
||||
async event => {
|
||||
// Dramatically unbridge rooms if they become encrypted
|
||||
if (event.state_key !== "") return
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
if (!channelID) return
|
||||
const channel = discord.channels.get(channelID)
|
||||
if (!channel) return
|
||||
await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.")
|
||||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
module.exports.sendError = sendError
|
||||
module.exports.printError = printError
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ function getStateEventOuter(roomID, type, key) {
|
|||
/**
|
||||
* @param {string} roomID
|
||||
* @param {{unsigned?: {invite_room_state?: Ty.Event.InviteStrippedState[]}}} [event]
|
||||
* @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?, encryption: string?}>}
|
||||
* @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?}>}
|
||||
*/
|
||||
async function getInviteState(roomID, event) {
|
||||
function getFromInviteRoomState(strippedState, nskey, key) {
|
||||
|
|
@ -191,8 +191,7 @@ async function getInviteState(roomID, event) {
|
|||
name: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.name", "name"),
|
||||
topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"),
|
||||
avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"),
|
||||
type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type"),
|
||||
encryption: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.encryption", "algorithm")
|
||||
type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,8 +227,7 @@ async function getInviteState(roomID, event) {
|
|||
name: getFromInviteRoomState(strippedState, "m.room.name", "name"),
|
||||
topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"),
|
||||
avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"),
|
||||
type: getFromInviteRoomState(strippedState, "m.room.create", "type"),
|
||||
encryption: getFromInviteRoomState(strippedState, "m.room.encryption", "algorithm")
|
||||
type: getFromInviteRoomState(strippedState, "m.room.create", "type")
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
@ -242,8 +240,7 @@ async function getInviteState(roomID, event) {
|
|||
name: room.name ?? null,
|
||||
topic: room.topic ?? null,
|
||||
avatar: room.avatar_url ?? null,
|
||||
type: room.room_type ?? null,
|
||||
encryption: (room.encryption || room["im.nheko.summary.encryption"]) ?? null
|
||||
type: room.room_type ?? null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ async function _actuallyUploadDiscordFileToMxc(url) {
|
|||
writeRegistration(reg)
|
||||
return root
|
||||
}
|
||||
e.uploadURL = url
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const Ty = require("../types")
|
||||
const {pipeline} = require("stream").promises
|
||||
const sharp = require("sharp")
|
||||
|
|
@ -97,6 +96,33 @@ function replyctx(execute) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Error & {code?: string|number}} e
|
||||
* @returns {e}
|
||||
*/
|
||||
function unmarshallDiscordError(e) {
|
||||
if (e.name === "DiscordAPIError"){
|
||||
try{
|
||||
const unmarshaled = JSON.parse(e.message)
|
||||
return {
|
||||
...e,
|
||||
...unmarshaled
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
...err,
|
||||
code: "JSON_PARSE_FAILED",
|
||||
message: JSON.stringify({
|
||||
original_error_where_message_failed_to_parse: e,
|
||||
json_parser_error_message: err.message,
|
||||
json_parser_error_code: err.code,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
/** @type {Command[]} */
|
||||
const commands = [{
|
||||
aliases: ["emoji"],
|
||||
|
|
@ -105,8 +131,7 @@ const commands = [{
|
|||
// Guard
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||
let matrixOnlyReason = null
|
||||
const matrixOnlyConclusion = "So the emoji will be uploaded on Matrix-side only. It will still be usable over the bridge, but may have degraded functionality."
|
||||
// Check if we can/should upload to Discord, for various causes
|
||||
|
|
@ -116,7 +141,7 @@ const commands = [{
|
|||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
const slots = getSlotCount(guild.premium_tier)
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||
if (guild.emojis.length >= slots) {
|
||||
matrixOnlyReason = "CAPACITY"
|
||||
} else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...)
|
||||
|
|
@ -241,8 +266,7 @@ const commands = [{
|
|||
// Guard
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||
if (!guildID) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
|
|
@ -253,76 +277,98 @@ const commands = [{
|
|||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||
if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "This command creates a thread on Discord. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Public Threads permission."
|
||||
body: "This command creates a thread on Discord. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Public Threads permission." // NOTE: Currently, this assumes that all Matrix users have no Discord roles (see: empty [] in the getPermissions call above), and makes that „If you were a Discord user...” claim based on that assumption. If Discord permission emulation is gonna happen in the future, this is certainly one among many places that will need to be changed.
|
||||
})
|
||||
}
|
||||
|
||||
const relation = event.content["m.relates_to"]
|
||||
let isFallingBack = false;
|
||||
let branchedFromMxEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to branch the thread from the message to which /thread was replying.
|
||||
if (relation?.rel_type === "m.thread") branchedFromMxEvent = relation?.event_id // If /thread was sent inside a Matrix thread, attempt to branch the Discord thread from the message, which that Matrix thread already is branching from.
|
||||
if (!branchedFromMxEvent){
|
||||
branchedFromMxEvent = event.event_id // If /thread wasn't replying to anything (ie. branchedFromMxEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - branchedFromMxEvent ended up being undefined, even if according to the spec it shouldn't), branch the thread from the /thread command-message that created it.
|
||||
isFallingBack = true;
|
||||
}
|
||||
const branchedFromDiscordMessage = select("event_message", "message_id", {event_id: branchedFromMxEvent}).pluck().get()
|
||||
|
||||
await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")})
|
||||
}
|
||||
)
|
||||
}, {
|
||||
aliases: ["invite"],
|
||||
execute: replyctx(
|
||||
async (event, realBody, words, ctx) => {
|
||||
// Guard
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
if (!guildID) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
if (words.length < 2){
|
||||
if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "This room isn't bridged to the other side."
|
||||
body: "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<strong><code>/thread</code> usage:</strong><br>Run this command as <code>/thread [Thread Name]</code> to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:<br><ul><li>If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The <code>Thread Name</code> argument must be provided in this case, otherwise you get this help message.</li><li>If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.</li><li>If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.</li></ul>"
|
||||
})
|
||||
words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body.replaceAll("\n", " ")
|
||||
words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..."
|
||||
}
|
||||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
||||
if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "This command creates an invite link to the Discord side. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Invite permission."
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
var invite = await discord.snow.channel.createChannelInvite(channelID)
|
||||
} catch (e) {
|
||||
if (e.message === `{"message": "Missing Permissions", "code": 50013}`) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
if (branchedFromDiscordMessage) await discord.snow.channel.createThreadWithMessage(channelID, branchedFromDiscordMessage, {name: words.slice(1).join(" ")})
|
||||
else throw {code: "NO_BRANCH_SOURCE", was_supposed_to_be: branchedFromMxEvent};
|
||||
}
|
||||
catch (e){
|
||||
switch (unmarshallDiscordError(e).code) {
|
||||
case "NO_BRANCH_SOURCE": return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "I don't have permission to create invites to the Discord channel/server."
|
||||
body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+e.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID <code>"+e.was_supposed_to_be+"</code> on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported."
|
||||
})
|
||||
} else {
|
||||
throw e
|
||||
|
||||
case (160004): // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes
|
||||
if (isFallingBack){
|
||||
await api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "⚠️ Discord claims that there already exists a thread for the message you ran this command on, but that doesn't make logical sense, as it doesn't seem like you ran this command on any message. Either your Matrix client did something funny with reply/thread tags, or this is a logic error on OOYE's side. At any rate, this should be reported for further investigation. you should also attach the error message that's about to be sent below (or on the main room timeline, if the command was ran inside a thread).",
|
||||
})
|
||||
throw e;
|
||||
}
|
||||
const thread = mxUtils.getThreadRoomFromThreadEvent(branchedFromMxEvent)
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "There already exists a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue by creating the bridge. (If you're said admin and you can see that said bridge already exists, but this error message is still showing up, please report that as a bug.)")
|
||||
})
|
||||
|
||||
case (50024): return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?"
|
||||
})
|
||||
|
||||
case (50035): return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, the thread body is for that. ;)"
|
||||
})
|
||||
|
||||
default:
|
||||
await api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "⚠️ Unknown error occurred during thread creation. See error message below (or on the main room timeline, if the command was ran inside a thread) for details."
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}
|
||||
const validHours = Math.ceil(invite.max_age / (60 * 60))
|
||||
const validUses =
|
||||
( invite.max_uses === 0 ? "unlimited uses"
|
||||
: invite.max_uses === 1 ? "single-use"
|
||||
: `${invite.max_uses} uses`)
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: `https://discord.gg/${invite.code}\nValid for next ${validHours} hours, ${validUses}.`
|
||||
})
|
||||
}
|
||||
)
|
||||
}]
|
||||
|
||||
|
||||
/** @type {CommandExecute} */
|
||||
async function execute(event) {
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Message} event
|
||||
* @returns {Promise<string|undefined>} the executed command's name or undefined if no command execution was performed
|
||||
*/
|
||||
async function parseAndExecute(event) {
|
||||
let realBody = event.content.body
|
||||
while (realBody.startsWith("> ")) {
|
||||
const i = realBody.indexOf("\n")
|
||||
|
|
@ -343,7 +389,8 @@ async function execute(event) {
|
|||
if (!command) return
|
||||
|
||||
await command.execute(event, realBody, words)
|
||||
return words[0]
|
||||
}
|
||||
|
||||
module.exports.execute = execute
|
||||
module.exports.parseAndExecute = parseAndExecute
|
||||
module.exports.onReactionAdd = onReactionAdd
|
||||
|
|
|
|||
|
|
@ -78,11 +78,6 @@ function readRegistration() {
|
|||
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
|
||||
let reg = readRegistration()
|
||||
|
||||
fs.watch(registrationFilePath, {persistent: false}, () => {
|
||||
let newReg = readRegistration()
|
||||
Object.assign(reg, newReg)
|
||||
})
|
||||
|
||||
module.exports.registrationFilePath = registrationFilePath
|
||||
module.exports.readRegistration = readRegistration
|
||||
module.exports.getTemplateRegistration = getTemplateRegistration
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration")
|
||||
|
||||
|
|
|
|||
|
|
@ -54,17 +54,17 @@ async function onBotMembership(event, api, createRoom) {
|
|||
assert.equal(event.type, "m.room.member")
|
||||
assert.equal(event.state_key, utils.bot)
|
||||
|
||||
// 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 false
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
|
||||
|
||||
// Check if is join/invite
|
||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return false
|
||||
|
||||
return await roomUpgradeSema.request(async () => {
|
||||
// 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 false
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
|
||||
|
||||
// Check if is join/invite
|
||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return false
|
||||
|
||||
// If invited, join
|
||||
if (event.content.membership === "invite") {
|
||||
await api.joinRoom(newRoomID)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const assert = require("assert").strict
|
|||
const Ty = require("../types")
|
||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||
const passthrough = require("../passthrough")
|
||||
const {db} = passthrough
|
||||
const {db, select} = passthrough
|
||||
|
||||
const {reg} = require("./read-registration")
|
||||
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
||||
|
|
@ -225,6 +225,19 @@ async function getViaServersQuery(roomID, api) {
|
|||
return qs
|
||||
}
|
||||
|
||||
function generatePermittedMediaHash(mxc) {
|
||||
assert(hasher, "xxhash is not ready yet")
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
||||
if (!mediaParts) return undefined
|
||||
|
||||
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
||||
const unsignedHash = hasher.h64(serverAndMediaID)
|
||||
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
|
||||
db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)
|
||||
|
||||
return serverAndMediaID
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL
|
||||
* because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge.
|
||||
|
|
@ -385,6 +398,16 @@ async function setUserPowerCascade(spaceID, mxid, power, api) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {undefined|string?} eventID
|
||||
*/ //^For some reason, „?” doesn't include Undefined and it needs to be explicitly specified
|
||||
function getThreadRoomFromThreadEvent(eventID){
|
||||
if (!eventID) return eventID;
|
||||
const threadID = select("event_message", "message_id", {event_id: eventID}).pluck().get() //Discord thread ID === its message ID
|
||||
if (!threadID) return threadID;
|
||||
return select("channel_room", "room_id", {channel_id: threadID}).pluck().get()
|
||||
}
|
||||
|
||||
module.exports.bot = bot
|
||||
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
|
||||
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
|
||||
|
|
@ -400,3 +423,4 @@ module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels
|
|||
module.exports.getEffectivePower = getEffectivePower
|
||||
module.exports.setUserPower = setUserPower
|
||||
module.exports.setUserPowerCascade = setUserPowerCascade
|
||||
module.exports.getThreadRoomFromThreadEvent = getThreadRoomFromThreadEvent
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const {select} = require("../passthrough")
|
||||
const {test} = require("supertape")
|
||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower} = require("./utils")
|
||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower, getThreadRoomFromThreadEvent} = require("./utils")
|
||||
const util = require("util")
|
||||
|
||||
/** @param {string[]} mxids */
|
||||
|
|
@ -417,4 +417,38 @@ test("set user power: privileged users must demote themselves", async t => {
|
|||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
test("getThreadRoomFromThreadEvent: real message, but without a thread", t => {
|
||||
const room = getThreadRoomFromThreadEvent("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||
const msg = "Expected null/undefined, got: "+room
|
||||
if(room) t.fail(msg);
|
||||
else t.pass(msg)
|
||||
})
|
||||
|
||||
test("getThreadRoomFromThreadEvent: real message with a thread", t => {
|
||||
const room = getThreadRoomFromThreadEvent("$fdD9o7NxMA4VPexlAiIx2CB9JbsiGhJeyJgnZG7U5xg")
|
||||
t.equal(room, "!FuDZhlOAtqswlyxzeR:cadence.moe")
|
||||
})
|
||||
|
||||
test("getThreadRoomFromThreadEvent: fake message", t => {
|
||||
const room = getThreadRoomFromThreadEvent("$ThisEvent-IdDoesNotExistInTheDatabase4Sure")
|
||||
const msg = "Expected null/undefined, got: "+room
|
||||
if(room) t.fail(msg);
|
||||
else t.pass(msg)
|
||||
})
|
||||
|
||||
test("getThreadRoomFromThreadEvent: null", t => {
|
||||
const room = getThreadRoomFromThreadEvent(null)
|
||||
t.equal(room, null)
|
||||
})
|
||||
|
||||
test("getThreadRoomFromThreadEvent: undefined", t => {
|
||||
const room = getThreadRoomFromThreadEvent(undefined)
|
||||
t.equal(room, undefined)
|
||||
})
|
||||
|
||||
test("getThreadRoomFromThreadEvent: no value at all", t => {
|
||||
const room = getThreadRoomFromThreadEvent() //This line should be giving a type-error, so it's not @ts-ignored on purpose. This is to test the desired behavior of that function, ie. „it CAN TAKE an undefined VALUE (as tested above), but you can just LEAVE the value completely undefined” (well, you can leave it like that from JS syntax perspective (which is why this test passes), but it makes no sense from usage standpoint, as it just gives back undefined). So this isn't a logic test (that's handled above), as much as it is a TypeScript test.
|
||||
t.equal(room, undefined)
|
||||
})
|
||||
|
||||
module.exports.mockGetEffectivePower = mockGetEffectivePower
|
||||
|
|
|
|||
18
src/stdin.js
18
src/stdin.js
|
|
@ -23,26 +23,10 @@ const setPresence = sync.require("./d2m/actions/set-presence")
|
|||
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
||||
const guildID = "112760669178241024"
|
||||
|
||||
async function ping() {
|
||||
const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
|
||||
if (result.ok) {
|
||||
return "Ping OK. The homeserver and OOYE are talking to each other fine."
|
||||
} else {
|
||||
if (typeof result.root === "string") {
|
||||
var msg = `Cannot reach homeserver: ${result.root}`
|
||||
} else if (result.root.error) {
|
||||
var msg = `Homeserver said: [${result.status}] ${result.root.error}`
|
||||
} else {
|
||||
var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}`
|
||||
}
|
||||
return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again."
|
||||
}
|
||||
}
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
setImmediate(() => {
|
||||
if (!passthrough.repl) {
|
||||
const cli = repl.start({prompt: "", eval: customEval, writer: s => s})
|
||||
const cli = repl.start({ prompt: "", eval: customEval, writer: s => s })
|
||||
Object.assign(cli.context, passthrough)
|
||||
passthrough.repl = cli
|
||||
}
|
||||
|
|
|
|||
30
src/types.d.ts
vendored
30
src/types.d.ts
vendored
|
|
@ -157,7 +157,7 @@ export namespace Event {
|
|||
type: string
|
||||
state_key: string
|
||||
sender: string
|
||||
content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias | Event.M_Room_Encryption
|
||||
content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias
|
||||
}
|
||||
|
||||
export type M_Room_Create = {
|
||||
|
|
@ -190,11 +190,12 @@ export namespace Event {
|
|||
format?: "org.matrix.custom.html"
|
||||
formatted_body?: string,
|
||||
"m.relates_to"?: {
|
||||
"m.in_reply_to": {
|
||||
event_id?: string
|
||||
is_falling_back?: boolean
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string
|
||||
}
|
||||
rel_type?: "m.replace"
|
||||
event_id?: string
|
||||
rel_type?: "m.replace"|"m.thread"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,11 +211,12 @@ export namespace Event {
|
|||
info?: any
|
||||
"page.codeberg.everypizza.msc4193.spoiler"?: boolean
|
||||
"m.relates_to"?: {
|
||||
"m.in_reply_to": {
|
||||
event_id?: string
|
||||
is_falling_back?: boolean
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string
|
||||
}
|
||||
rel_type?: "m.replace"
|
||||
event_id?: string
|
||||
rel_type?: "m.replace"|"m.thread"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,11 +248,12 @@ export namespace Event {
|
|||
},
|
||||
info?: any
|
||||
"m.relates_to"?: {
|
||||
"m.in_reply_to": {
|
||||
event_id?: string
|
||||
is_falling_back?: boolean
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string
|
||||
}
|
||||
rel_type?: "m.replace"
|
||||
event_id?: string
|
||||
rel_type?: "m.replace"|"m.thread"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -390,12 +393,6 @@ export namespace Event {
|
|||
body: string
|
||||
replacement_room: string
|
||||
}
|
||||
|
||||
export type M_Room_Encryption = {
|
||||
algorithm: string
|
||||
rotation_period_ms?: number
|
||||
rotation_period_msgs?: number
|
||||
}
|
||||
}
|
||||
|
||||
export namespace R {
|
||||
|
|
@ -443,7 +440,6 @@ export namespace R {
|
|||
num_joined_members: number
|
||||
room_id: string
|
||||
room_type?: string
|
||||
encryption?: string
|
||||
}
|
||||
|
||||
export type ResolvedRoom = {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ function renderPath(event, path, locals) {
|
|||
compile()
|
||||
fs.watch(path, {persistent: false}, compile)
|
||||
fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile)
|
||||
fs.watch(join(__dirname, "pug", "fragments"), {persistent: false}, compile)
|
||||
}
|
||||
|
||||
const cb = pugCache.get(path)
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
extends includes/template.pug
|
||||
|
||||
block body
|
||||
h1.ta-center.fs-display2.fc-green-400 April Fools!
|
||||
.ws7.m-auto
|
||||
.s-prose.fs-body2
|
||||
p Sheesh, wouldn't that be horrible?
|
||||
if guild_id
|
||||
p Fake AI messages have now been #[strong.fc-green-600 deactivated for everyone in your server.]
|
||||
p Hope the prank entertained you. #[a(href="https://cadence.moe/contact") Send love or hate mail here.]
|
||||
|
||||
h2 What actually happened?
|
||||
ul
|
||||
li A secret event was added for the duration of 1st April 2026 (UTC).
|
||||
li If a message matches a preset pattern, a preset response is posted to chat by an AI-ified profile of the previous author.
|
||||
li It only happens at most once per hour in each server.
|
||||
li I tried to design it to not interrupt any serious/sensitive topics. I am deeply sorry if that didn't work out.
|
||||
li No AI generated materials have ever been used in Out Of Your Element: no code, no prose, no images, no jokes.
|
||||
li It'll always deactivate itself on 2nd April, no matter what, and I'll remove the relevant code shortly after.
|
||||
if guild_id
|
||||
.s-prose.fl-grow1.mt16
|
||||
p If you thought it was funny, feel free to opt back in. This affects the entire server, so please be courteous.
|
||||
form(method="post" action=rel(`/agi/optin?guild_id=${guild_id}`))
|
||||
button(type="submit").s-btn.s-btn__muted Opt back in
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
extends includes/template.pug
|
||||
|
||||
block title
|
||||
title AGI in Discord
|
||||
|
||||
block body
|
||||
style.
|
||||
.ai-gradient {
|
||||
background: linear-gradient(100deg, #fb72f2, #072ea4);
|
||||
color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
h1.ta-center.fs-display2.ai-gradient AGI in Discord:#[br]Revolutionizing the Future of Communications
|
||||
.ws7.m-auto
|
||||
.s-prose.fs-body2
|
||||
p In the ever-changing and turbulent world of AI, it's crucial to always be one step ahead.
|
||||
p That's why Out Of Your Element has partnered with #[strong Grimace AI] to provide you tomorrow's technology, today.
|
||||
ul
|
||||
li #[strong Always online:] Miss your friends when they log off? Now you can talk to facsimiles of them etched into an unfeeling machine, always and forever!
|
||||
li #[strong Smarter than ever:] Pissed off when somebody says something #[em wrong] on the internet? Frustrated with having to stay up all night correcting them? With Grimace Truth (available in Pro+ Ultra plan), all information is certified true to reality, so you'll never have to worry about those frantic Google searches at 3 AM.
|
||||
li #[strong Knows you better than yourself:] We aren't just training on your data; we're copying minds into our personality matrix — including yours. Do you find yourself enjoying the sound of your own voice more than anything else? Our unique simulation of You is here to help.
|
||||
|
||||
h1.mt64.mb32 Frequently Asked Questions
|
||||
.s-link-preview
|
||||
.s-link-preview--header.fd-column
|
||||
.s-link-preview--title.fs-title.pl4 How to opt out?
|
||||
.s-link-preview--details.fc-red-500
|
||||
!= icons.Icons.IconFire
|
||||
= ` 20,000% higher search volume for this question in the last hour`
|
||||
.s-link-preview--body
|
||||
.s-prose
|
||||
h2.fs-body3 Is this really goodbye? 😢😢😢😢😢
|
||||
p I can't convince you to stay?
|
||||
p As not just a customer service representative but someone with a shared vision, I simply want you to know that everyone at Grimace AI will miss all the time that we've shared together with you.
|
||||
form(method="post" action=(guild_id ? rel(`/agi/optout?guild_id=${guild_id}`) : rel("/agi/optout"))).d-flex.g4.mt16
|
||||
button(type="button").s-btn.s-btn__filled Nevermind, I'll stay :)
|
||||
button(type="submit").s-btn.s-btn__danger.s-btn__outlined Opt out for 3 days
|
||||
|
||||
|
||||
div(style="height: 200px")
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
//- locals: guild, guild_id
|
||||
|
||||
include ../includes/default-roles-list.pug
|
||||
+default-roles-list(guild, guild_id)
|
||||
+add-roles-menu(guild, guild_id)
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
extends includes/template.pug
|
||||
include includes/default-roles-list.pug
|
||||
|
||||
mixin badge-readonly
|
||||
.s-badge.s-badge__xs.s-badge__icon.s-badge__muted
|
||||
|
|
@ -77,7 +76,7 @@ block body
|
|||
|
||||
if space_id
|
||||
h2.mt48.fs-headline1 Server settings
|
||||
h3.mt32.fs-category How Matrix users join
|
||||
h3.mt32.fs-category Privacy level
|
||||
span#privacy-level-loading
|
||||
.s-card
|
||||
form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
||||
|
|
@ -106,24 +105,6 @@ block body
|
|||
p.s-description.m0 Shareable invite links, like Discord
|
||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
||||
|
||||
h3.mt32.fs-category Default roles
|
||||
.s-card
|
||||
form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles
|
||||
input(type="hidden" name="guild_id" value=guild_id)
|
||||
.d-flex.fw-wrap.g4
|
||||
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
||||
|
||||
+default-roles-list(guild, guild_id)
|
||||
|
||||
button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0
|
||||
.s-tag--dismiss.m1
|
||||
!= icons.Icons.IconPlusSm
|
||||
|
||||
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
||||
.s-popover--arrow.s-popover--arrow__tc
|
||||
+add-roles-menu(guild, guild_id)
|
||||
p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
|
||||
|
||||
h3.mt32.fs-category Features
|
||||
.s-card.d-grid.px0.g16
|
||||
form.d-flex.ai-center.g16
|
||||
|
|
@ -249,11 +230,6 @@ block body
|
|||
ul.my8.ml24
|
||||
each row in removedLinkedRooms
|
||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||
h3.mt24 Unavailable rooms: Encryption not supported
|
||||
.s-card.p0
|
||||
ul.my8.ml24
|
||||
each row in removedEncryptedRooms
|
||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||
h3.mt24 Unavailable rooms: Wrong type
|
||||
.s-card.p0
|
||||
ul.my8.ml24
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
mixin default-roles-list(guild, guild_id)
|
||||
#default-roles-list(style="display: contents")
|
||||
each roleID in select("role_default", "role_id", {guild_id}).pluck().all()
|
||||
- let r = guild.roles.find(r => r.id === roleID)
|
||||
if r
|
||||
.s-tag.s-tag__md.fs-body1= r.name
|
||||
span(id=`role-loading-${roleID}`)
|
||||
button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss
|
||||
!= icons.Icons.IconClearSm
|
||||
|
||||
mixin add-roles-menu(guild, guild_id)
|
||||
ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu
|
||||
li.s-menu--title.d-flex(role="separator") Select role
|
||||
span#add-role-loading
|
||||
each r in guild.roles.sort((a, b) => b.position - a.position)
|
||||
if r.id !== guild_id && !r.managed
|
||||
- let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get()
|
||||
li(role="menuitem")
|
||||
button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name
|
||||
|
|
@ -65,8 +65,7 @@ mixin define-themed-button(name, theme)
|
|||
doctype html
|
||||
html(lang="en")
|
||||
head
|
||||
block title
|
||||
title Out Of Your Element
|
||||
title Out Of Your Element
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css"))
|
||||
//- Please use responsibly!!!!!
|
||||
|
|
@ -89,28 +88,9 @@ html(lang="en")
|
|||
--_ts-multiple-bg: var(--green-400);
|
||||
--_ts-multiple-fc: var(--white);
|
||||
}
|
||||
.s-avatar {
|
||||
--_av-bg: var(--white);
|
||||
}
|
||||
.s-avatar .s-avatar--letter {
|
||||
color: var(--white);
|
||||
}
|
||||
.s-btn__dropdown:has(+ :popover-open) {
|
||||
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
|
||||
}
|
||||
.s-btn__dropdown.s-tag:has(+ :popover-open) .s-tag--dismiss {
|
||||
background-color: var(--black-500) !important;
|
||||
color: var(--black-150) !important;
|
||||
}
|
||||
.s-tag .is-loading {
|
||||
margin-right: -4px;
|
||||
}
|
||||
.s-tag .is-loading + .s-tag--dismiss {
|
||||
display: none !important;
|
||||
}
|
||||
a.s-block-link, .s-block-link {
|
||||
--_bl-bs-color: var(--green-400);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.theme-system .s-popover {
|
||||
--_po-bg: var(--black-100);
|
||||
|
|
@ -161,15 +141,11 @@ html(lang="en")
|
|||
//- Guild list popover
|
||||
script.
|
||||
document.querySelectorAll("[popovertarget]").forEach(e => {
|
||||
const target = document.getElementById(e.getAttribute("popovertarget"))
|
||||
e.addEventListener("click", calculate)
|
||||
target.addEventListener("toggle", calculate)
|
||||
function calculate() {
|
||||
const buttonRect = e.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const t = `:popover-open { position: absolute; top: ${Math.floor(buttonRect.bottom + window.scrollY)}px; left: ${Math.floor(Math.max(targetRect.width / 2, buttonRect.left + buttonRect.width / 2))}px; width: ${Math.floor(buttonRect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
|
||||
e.addEventListener("click", () => {
|
||||
const rect = e.getBoundingClientRect()
|
||||
const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
|
||||
document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length)
|
||||
}
|
||||
})
|
||||
})
|
||||
//- Prevent default
|
||||
script.
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
const {defineEventHandler, getValidatedQuery, sendRedirect} = require("h3")
|
||||
const {as, from, sync, db} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
const schema = {
|
||||
opt: z.object({
|
||||
guild_id: z.string().regex(/^[0-9]+$/)
|
||||
})
|
||||
}
|
||||
|
||||
as.router.get("/agi", defineEventHandler(async event => {
|
||||
return pugSync.render(event, "agi.pug", {})
|
||||
}))
|
||||
|
||||
as.router.get("/agi/optout", defineEventHandler(async event => {
|
||||
return pugSync.render(event, "agi-optout.pug", {})
|
||||
}))
|
||||
|
||||
as.router.post("/agi/optout", defineEventHandler(async event => {
|
||||
const parseResult = await getValidatedQuery(event, schema.opt.safeParse)
|
||||
if (parseResult.success) {
|
||||
db.prepare("INSERT OR IGNORE INTO agi_optout (guild_id) VALUES (?)").run(parseResult.data.guild_id)
|
||||
}
|
||||
return sendRedirect(event, "", 302)
|
||||
}))
|
||||
|
||||
as.router.post("/agi/optin", defineEventHandler(async event => {
|
||||
const {guild_id} = await getValidatedQuery(event, schema.opt.parse)
|
||||
db.prepare("DELETE FROM agi_optout WHERE guild_id = ?").run(guild_id)
|
||||
return sendRedirect(event, `../agi?guild_id=${guild_id}`, 302)
|
||||
}))
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
const {_cache} = require("./download-discord")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const fs = require("fs")
|
||||
const {convertImageStream} = require("../../m2d/converters/emoji-sheet")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
const streamWeb = require("stream/web")
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ const assert = require("assert/strict")
|
|||
const {z} = require("zod")
|
||||
const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3")
|
||||
|
||||
const {as, db, sync, select, discord} = require("../../passthrough")
|
||||
const {as, db, sync, select} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../auth")} */
|
||||
const auth = sync.require("../auth")
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
/** @type {import("../../d2m/actions/set-presence")} */
|
||||
const setPresence = sync.require("../../d2m/actions/set-presence")
|
||||
|
||||
|
|
@ -22,14 +20,6 @@ function getCreateSpace(event) {
|
|||
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
|
||||
}
|
||||
|
||||
const schema = {
|
||||
defaultRoles: z.object({
|
||||
guild_id: z.string(),
|
||||
toggle_role: z.string().optional(),
|
||||
remove_role: z.string().optional()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef Options
|
||||
* @prop {(value: string?) => number} transform
|
||||
|
|
@ -104,39 +94,3 @@ as.router.post("/api/privacy-level", defineToggle("privacy_level", {
|
|||
await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
|
||||
}
|
||||
}))
|
||||
|
||||
as.router.post("/api/default-roles", defineEventHandler(async event => {
|
||||
const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse)
|
||||
|
||||
const managed = await auth.getManagedGuilds(event)
|
||||
const guildID = parsedBody.guild_id
|
||||
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||
|
||||
const roleID = parsedBody.toggle_role || parsedBody.remove_role
|
||||
assert(roleID)
|
||||
assert.notEqual(guildID, roleID) // the @everyone role is always default
|
||||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
|
||||
let shouldRemove = !!parsedBody.remove_role
|
||||
if (!shouldRemove) {
|
||||
shouldRemove = !!select("role_default", "role_id", {guild_id: guildID, role_id: roleID}).get()
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
db.prepare("DELETE FROM role_default WHERE guild_id = ? AND role_id = ?").run(guildID, roleID)
|
||||
} else {
|
||||
assert(guild.roles.find(r => r.id === roleID))
|
||||
db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID)
|
||||
}
|
||||
|
||||
const createSpace = getCreateSpace(event)
|
||||
await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
|
||||
|
||||
if (getRequestHeader(event, "HX-Request")) {
|
||||
return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID})
|
||||
} else {
|
||||
return sendRedirect(event, `/guild?guild_id=${guildID}`, 302)
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {select} = require("../../passthrough")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
|
|
|
|||
|
|
@ -123,14 +123,13 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
|||
let unlinkedRooms = [...rooms]
|
||||
let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
|
||||
let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type)
|
||||
let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"])
|
||||
// https://discord.com/developers/docs/topics/threads#active-archived-threads
|
||||
// need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name
|
||||
let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/))
|
||||
|
||||
return {
|
||||
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
|
||||
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms
|
||||
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const {_getPosition} = require("./guild")
|
||||
|
|
|
|||
|
|
@ -204,12 +204,6 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
|||
throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
|
||||
}
|
||||
|
||||
// Check room is not encrypted
|
||||
const encryption = await api.getStateEvent(parsedBody.matrix, "m.room.encryption", "").catch(() => null)
|
||||
if (encryption) {
|
||||
throw createError({status: 400, message: "Bad Request", data: "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room."})
|
||||
}
|
||||
|
||||
// Check bridge has PL 100
|
||||
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"})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const {select, db} = require("../../passthrough")
|
||||
|
|
@ -435,47 +435,6 @@ 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 room is not encrypted", 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")
|
||||
if (type === "m.room.encryption" && key === "") {
|
||||
return {algorithm: "m.megolm.v1.aes-sha2"}
|
||||
}
|
||||
throw new Error("Unknown state event")
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room.")
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
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", {
|
||||
|
|
@ -506,10 +465,9 @@ test("web link room: check that bridge has PL 100 in target room", async t => {
|
|||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
if (type === "m.room.power_levels" && key === "") {
|
||||
return {users_default: 50}
|
||||
}
|
||||
throw new Error("Unknown state event")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users_default: 50}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
|
|
@ -531,7 +489,7 @@ test("web link room: check that bridge has PL 100 in target room", async t => {
|
|||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
|
||||
t.equal(called, 5)
|
||||
t.equal(called, 4)
|
||||
})
|
||||
|
||||
test("web link room: successfully calls createRoom", async t => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const assert = require("assert/strict")
|
||||
const {router, test} = require("../../../test/web")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
|
||||
|
|
|
|||
|
|
@ -83,13 +83,7 @@ function tryStatic(event, fallthrough) {
|
|||
// Everything else
|
||||
else {
|
||||
const mime = mimeTypes.lookup(id)
|
||||
if (typeof mime === "string") {
|
||||
if (mime.startsWith("text/")) {
|
||||
defaultContentType(event, mime + "; charset=utf-8") // usually wise
|
||||
} else {
|
||||
defaultContentType(event, mime)
|
||||
}
|
||||
}
|
||||
if (typeof mime === "string") defaultContentType(event, mime)
|
||||
return {
|
||||
size: stats.size
|
||||
}
|
||||
|
|
@ -100,7 +94,7 @@ function tryStatic(event, fallthrough) {
|
|||
const path = join(publicDir, id)
|
||||
return pugSync.renderPath(event, path, {})
|
||||
} else {
|
||||
return fs.createReadStream(join(publicDir, id))
|
||||
return fs.promises.readFile(join(publicDir, id))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -125,7 +119,6 @@ as.router.get("/icon.png", defineEventHandler(async event => {
|
|||
|
||||
pugSync.createRoute(as.router, "/ok", "ok.pug")
|
||||
|
||||
sync.require("./routes/agi")
|
||||
sync.require("./routes/download-matrix")
|
||||
sync.require("./routes/download-discord")
|
||||
sync.require("./routes/guild-settings")
|
||||
|
|
|
|||
188
test/data.js
188
test/data.js
|
|
@ -19,26 +19,6 @@ module.exports = {
|
|||
default_thread_rate_limit_per_user: 0,
|
||||
guild_id: "112760669178241024"
|
||||
},
|
||||
voice: {
|
||||
voice_background_display: null,
|
||||
version: 1774469910848,
|
||||
user_limit: 0,
|
||||
type: 2,
|
||||
theme_color: null,
|
||||
status: null,
|
||||
rtc_region: null,
|
||||
rate_limit_per_user: 0,
|
||||
position: 0,
|
||||
permission_overwrites: [],
|
||||
parent_id: "805261291908104252",
|
||||
nsfw: false,
|
||||
name: "🍞丨[8user] Piece",
|
||||
last_message_id: "1459912691098325137",
|
||||
id: "1036840786093953084",
|
||||
flags: 0,
|
||||
bitrate: 256000,
|
||||
guild_id: "112760669178241024"
|
||||
},
|
||||
updates: {
|
||||
type: 0,
|
||||
topic: "Updates and release announcements for Out Of Your Element.",
|
||||
|
|
@ -4637,7 +4617,7 @@ module.exports = {
|
|||
flags: 0,
|
||||
components: []
|
||||
},
|
||||
extreme_html_escaping: {
|
||||
escaping_crazy_html_tags: {
|
||||
id: "1158894131322552391",
|
||||
type: 0,
|
||||
content: "",
|
||||
|
|
@ -5087,141 +5067,6 @@ module.exports = {
|
|||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false
|
||||
},
|
||||
four_images: {
|
||||
type: 0,
|
||||
content: "",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-12T18:00:50.737000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 16384,
|
||||
components: [],
|
||||
id: "1481713598278533241",
|
||||
channel_id: "687028734322147344",
|
||||
author: {
|
||||
id: "112760500130975744",
|
||||
username: "minimus",
|
||||
avatar: "a_a354b9eaff512485b49c82b13691b941",
|
||||
discriminator: "0",
|
||||
public_flags: 512,
|
||||
flags: 512,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "minimus",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: { font_id: 11, effect_id: 5, colors: [ 6106655 ] },
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
message_reference: {
|
||||
type: 1,
|
||||
channel_id: "637339857118822430",
|
||||
message_id: "1481696763483258891",
|
||||
guild_id: "408573045540651009"
|
||||
},
|
||||
message_snapshots: [
|
||||
{
|
||||
message: {
|
||||
type: 0,
|
||||
content: "https://fixupx.com/i/status/2032003668787020046",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
description: "4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”\n" +
|
||||
"︀︀\n" +
|
||||
"︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)\n" +
|
||||
"\n" +
|
||||
"**[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **",
|
||||
color: 6513919,
|
||||
timestamp: "2026-03-12T08:00:02+00:00",
|
||||
author: {
|
||||
name: "AUTOMATON WEST (@AUTOMATON_ENG)",
|
||||
url: "https://x.com/AUTOMATON_ENG/status/2032003668787020046",
|
||||
icon_url: "https://pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg",
|
||||
proxy_icon_url: "https://images-ext-1.discordapp.net/external/1OzGhjvZTRstTxM38_7pqHXlmdbMddqh1F8R0-WrKqw/https/pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg"
|
||||
},
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/NkNgp2SyY1OCH9IdS8hqsUqbnbrp3A9oLNwYusVVCVQ/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg",
|
||||
width: 872,
|
||||
height: 886,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "6vcFFwL6R3lye2V3l1mIl5l3WPN5FZ8H",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
footer: {
|
||||
text: "FixupX",
|
||||
icon_url: "https://assets.fxembed.com/logos/fixupx64.png",
|
||||
proxy_icon_url: "https://images-ext-1.discordapp.net/external/LwQ70Uiqfu0OCN4ZbA4f482TGCgQa-xGsnUFYfhIgYA/https/assets.fxembed.com/logos/fixupx64.png"
|
||||
},
|
||||
content_scan_version: 4
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/Rquh1ec-tG9hMqdHqIVSphO7zf5B5Fg_7yTWhCjlsek/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg",
|
||||
width: 1114,
|
||||
height: 991,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "JQgKDoL3epZ8ZIdnlmmHZ4d4CIGmUEc=",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
content_scan_version: 4
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/XrkhHNH3CvlZYvjkdykVnf-_xdz6HWX8uwesoAwwSfY/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUrPobgAAeb90.jpg",
|
||||
width: 944,
|
||||
height: 954,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "m/cJDwCbV0mfaoZzlihqeXdqCVN9A6oD",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
content_scan_version: 4
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/lO-5hBMU9bGH13Ax9xum2T2Mg0ATdv0b6BEx_VeVi80/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "tfcJDIK3mIl1eIiPdY23dX9b9w==",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
content_scan_version: 4
|
||||
}
|
||||
],
|
||||
timestamp: "2026-03-12T16:53:57.009000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
message_with_components: {
|
||||
|
|
@ -6190,37 +6035,6 @@ module.exports = {
|
|||
components: [],
|
||||
position: 12
|
||||
},
|
||||
channel_follow_add: {
|
||||
type: 12,
|
||||
content: "PluralKit #downtime",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-24T23:16:04.097Z",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1486141581047369888",
|
||||
channel_id: "1451125453082591314",
|
||||
author: {
|
||||
id: "154058479798059009",
|
||||
username: "exaptations",
|
||||
discriminator: "0",
|
||||
avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80",
|
||||
bot: false,
|
||||
flags: 0,
|
||||
globalName: "Exa",
|
||||
},
|
||||
pinned: false,
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
message_reference: {
|
||||
type: 0,
|
||||
channel_id: "1015204661701124206",
|
||||
guild_id: "466707357099884544"
|
||||
}
|
||||
},
|
||||
updated_to_start_thread_from_here: {
|
||||
t: "MESSAGE_UPDATE",
|
||||
s: 19,
|
||||
|
|
|
|||
|
|
@ -38,28 +38,15 @@ INSERT INTO sim (user_id, username, sim_name, mxid) VALUES
|
|||
('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'),
|
||||
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'),
|
||||
('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'),
|
||||
('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'),
|
||||
('196188877885538304', 'ampflower', 'ampflower', '@_ooye_ampflower:cadence.moe'),
|
||||
('1458668878107381800', 'Evil Lillith (she/her)', 'evil_lillith_sheher', '@_ooye_evil_lillith_sheher:cadence.moe'),
|
||||
('197126718400626689', 'infinidoge1337', 'infinidoge1337', '@_ooye_infinidoge1337:cadence.moe');
|
||||
|
||||
('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe');
|
||||
|
||||
INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
|
||||
('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL),
|
||||
('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL),
|
||||
('@_ooye_cadence:cadence.moe', '!kLRqKKUQXcibIMtOpl:cadence.moe', NULL),
|
||||
('@_ooye_cadence:cadence.moe', '!fGgIymcYWOqjbSRUdV:cadence.moe', NULL),
|
||||
('@_ooye_ampflower:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL),
|
||||
('@_ooye__pk_zoego:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL),
|
||||
('@_ooye_infinidoge1337:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL),
|
||||
('@_ooye_evil_lillith_sheher:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
|
||||
('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
|
||||
|
||||
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
|
||||
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺');
|
||||
|
||||
INSERT INTO app_user_install (guild_id, app_bot_id, user_id) VALUES
|
||||
('66192955777486848', '1458668878107381800', '197126718400626689');
|
||||
|
||||
INSERT INTO message_room (message_id, historical_room_index)
|
||||
WITH a (message_id, channel_id) AS (VALUES
|
||||
('1106366167788044450', '122155380120748034'),
|
||||
|
|
@ -95,12 +82,14 @@ WITH a (message_id, channel_id) AS (VALUES
|
|||
('1381212840957972480', '112760669178241024'),
|
||||
('1401760355339862066', '112760669178241024'),
|
||||
('1439351590262800565', '1438284564815548418'),
|
||||
('1404133238414376971', '112760669178241024'))
|
||||
('1404133238414376971', '112760669178241024'),
|
||||
('1162005314908999790', '1100319550446252084'))
|
||||
SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id;
|
||||
|
||||
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
||||
('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1),
|
||||
('$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4', 'm.room.message', 'm.text', '1128118177155526666', 0, 0, 0),
|
||||
('$fdD9o7NxMA4VPexlAiIx2CB9JbsiGhJeyJgnZG7U5xg', 'm.room.message', 'm.text', '1162005314908999790', 0, 0, 1),
|
||||
('$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10', 'm.room.message', 'm.text', '1141619794500649020', 0, 0, 1),
|
||||
('$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY', 'm.room.message', 'm.text', '1141206225632112650', 0, 0, 1),
|
||||
('$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA', 'm.room.message', 'm.text', '1141501302736695316', 0, 1, 1),
|
||||
|
|
|
|||
38
test/test.js
38
test/test.js
|
|
@ -6,29 +6,31 @@ const sqlite = require("better-sqlite3")
|
|||
const {Writable} = require("stream")
|
||||
const migrate = require("../src/db/migrate")
|
||||
const HeatSync = require("heatsync")
|
||||
const {test} = require("supertape")
|
||||
const {test, extend} = require("supertape")
|
||||
const data = require("./data")
|
||||
const {green} = require("ansi-colors")
|
||||
const mixin = require("@cloudrac3r/mixin-deep")
|
||||
|
||||
const passthrough = require("../src/passthrough")
|
||||
const db = new sqlite(":memory:")
|
||||
|
||||
const registration = require("../src/matrix/read-registration")
|
||||
registration.reg = mixin(registration.getTemplateRegistration("cadence.moe"), {
|
||||
id: "baby",
|
||||
url: "http://localhost:6693",
|
||||
as_token: "don't actually take authenticated actions on the server",
|
||||
hs_token: "don't actually take authenticated actions on the server",
|
||||
ooye: {
|
||||
server_origin: "https://matrix.cadence.moe",
|
||||
bridge_origin: "https://bridge.example.org",
|
||||
discord_token: "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby",
|
||||
discord_client_secret: "baby",
|
||||
web_password: "password123",
|
||||
time_zone: "Pacific/Auckland",
|
||||
}
|
||||
})
|
||||
const {reg} = require("../src/matrix/read-registration")
|
||||
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
|
||||
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
||||
reg.ooye.server_name = "cadence.moe"
|
||||
reg.ooye.namespace_prefix = "_ooye_"
|
||||
reg.sender_localpart = "_ooye_bot"
|
||||
reg.id = "baby"
|
||||
reg.as_token = "don't actually take authenticated actions on the server"
|
||||
reg.hs_token = "don't actually take authenticated actions on the server"
|
||||
reg.namespaces = {
|
||||
users: [{regex: "@_ooye_.*:cadence.moe", exclusive: true}],
|
||||
aliases: [{regex: "#_ooye_.*:cadence.moe", exclusive: true}]
|
||||
}
|
||||
reg.ooye.bridge_origin = "https://bridge.example.org"
|
||||
reg.ooye.time_zone = "Pacific/Auckland"
|
||||
reg.ooye.max_file_size = 5000000
|
||||
reg.ooye.web_password = "password123"
|
||||
reg.ooye.include_user_id_in_mxid = false
|
||||
|
||||
const sync = new HeatSync({watchFS: false})
|
||||
|
||||
|
|
@ -152,7 +154,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
|||
require("../src/d2m/converters/message-to-event.test.embeds")
|
||||
require("../src/d2m/converters/message-to-event.test.pk")
|
||||
require("../src/d2m/converters/pins-to-list.test")
|
||||
require("../src/d2m/converters/remove-member-mxids.test")
|
||||
require("../src/d2m/converters/remove-reaction.test")
|
||||
require("../src/d2m/converters/thread-to-announcement.test")
|
||||
require("../src/d2m/converters/user-to-mxid.test")
|
||||
|
|
@ -175,5 +176,4 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
|||
require("../src/web/routes/log-in-with-matrix.test")
|
||||
require("../src/web/routes/oauth.test")
|
||||
require("../src/web/routes/password.test")
|
||||
require("../src/agi/generator.test")
|
||||
})()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue