2023-07-03 12:39:42 +00:00
// @ts-check
const reg = require ( "../../matrix/read-registration" )
const userRegex = reg . namespaces . users . map ( u => new RegExp ( u . regex ) )
2023-09-22 05:47:36 +00:00
const assert = require ( "assert" ) . strict
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
let hasher = null
// @ts-ignore
require ( "xxhash-wasm" ) ( ) . then ( h => hasher = h )
2023-09-26 19:20:18 +00:00
const BLOCK _ELEMENTS = [
"ADDRESS" , "ARTICLE" , "ASIDE" , "AUDIO" , "BLOCKQUOTE" , "BODY" , "CANVAS" ,
"CENTER" , "DD" , "DETAILS" , "DIR" , "DIV" , "DL" , "DT" , "FIELDSET" , "FIGCAPTION" , "FIGURE" ,
"FOOTER" , "FORM" , "FRAMESET" , "H1" , "H2" , "H3" , "H4" , "H5" , "H6" , "HEADER" ,
"HGROUP" , "HR" , "HTML" , "ISINDEX" , "LI" , "MAIN" , "MENU" , "NAV" , "NOFRAMES" ,
"NOSCRIPT" , "OL" , "OUTPUT" , "P" , "PRE" , "SECTION" , "SUMMARY" , "TABLE" , "TBODY" , "TD" ,
"TFOOT" , "TH" , "THEAD" , "TR" , "UL"
]
2023-10-27 11:24:42 +00:00
const NEWLINE _ELEMENTS = BLOCK _ELEMENTS . concat ( [ "BR" ] )
2023-09-26 19:20:18 +00:00
2023-07-03 12:39:42 +00:00
/ * *
* Determine whether an event is the bridged representation of a discord message .
* Such messages shouldn ' t be bridged again .
* @ param { string } sender
* /
function eventSenderIsFromDiscord ( sender ) {
// If it's from a user in the bridge's namespace, then it originated from discord
// This includes messages sent by the appservice's bot user, because that is what's used for webhooks
// TODO: It would be nice if bridge system messages wouldn't trigger this check and could be bridged from matrix to discord, while webhook reflections would remain ignored...
2023-08-23 00:45:19 +00:00
// TODO that only applies to the above todo: But you'd have to watch out for the /icon command, where the bridge bot would set the room avatar, and that shouldn't be reflected into the room a second time.
2023-07-03 12:39:42 +00:00
if ( userRegex . some ( x => sender . match ( x ) ) ) {
return true
}
return false
}
2023-08-26 10:22:54 +00:00
/ * *
* @ param { string } mxc
* @ returns { string ? }
* /
function getPublicUrlForMxc ( mxc ) {
const avatarURLParts = mxc ? . match ( /^mxc:\/\/([^/]+)\/(\w+)$/ )
2023-09-12 08:43:56 +00:00
if ( avatarURLParts ) return ` ${ reg . ooye . server _origin } /_matrix/media/r0/download/ ${ avatarURLParts [ 1 ] } / ${ avatarURLParts [ 2 ] } `
2023-08-26 10:22:54 +00:00
else return null
}
2023-09-22 05:47:36 +00:00
/ * *
* Event IDs are really big and have more entropy than we need .
* If we want to store the event ID in the database , we can store a more compact version by hashing it with this .
2023-09-25 03:26:48 +00:00
* I choose a 64 - bit non - cryptographic hash as only a 32 - bit hash will see birthday collisions unreasonably frequently : https : //en.wikipedia.org/wiki/Birthday_attack#Mathematics
2023-09-22 05:47:36 +00:00
* xxhash outputs an unsigned 64 - bit integer .
* Converting to a signed 64 - bit integer with no bit loss so that it can be stored in an SQLite integer field as - is : https : //www.sqlite.org/fileformat2.html#record_format
* This should give very efficient storage with sufficient entropy .
* @ param { string } eventID
* /
function getEventIDHash ( eventID ) {
assert ( hasher , "xxhash is not ready yet" )
if ( eventID [ 0 ] === "$" && eventID . length >= 13 ) {
eventID = eventID . slice ( 1 ) // increase entropy per character to potentially help xxhash
}
const unsignedHash = hasher . h64 ( eventID )
const signedHash = unsignedHash - 0x8000000000000000 n // shifting down to signed 64-bit range
return signedHash
}
2023-10-27 11:24:42 +00:00
class MatrixStringBuilder {
constructor ( ) {
this . body = ""
this . formattedBody = ""
}
/ * *
* @ param { string } body
2024-01-18 23:39:41 +00:00
* @ param { string } [ formattedBody ]
2023-10-27 11:24:42 +00:00
* @ param { any } [ condition ]
* /
add ( body , formattedBody , condition = true ) {
if ( condition ) {
if ( formattedBody == undefined ) formattedBody = body
this . body += body
this . formattedBody += formattedBody
}
return this
}
/ * *
* @ param { string } body
* @ param { string } [ formattedBody ]
* @ param { any } [ condition ]
* /
addLine ( body , formattedBody , condition = true ) {
if ( condition ) {
if ( formattedBody == undefined ) formattedBody = body
if ( this . body . length && this . body . slice ( - 1 ) !== "\n" ) this . body += "\n"
this . body += body
const match = this . formattedBody . match ( /<\/?([a-zA-Z]+[a-zA-Z0-9]*)[^>]*>\s*$/ )
if ( this . formattedBody . length && ( ! match || ! NEWLINE _ELEMENTS . includes ( match [ 1 ] . toUpperCase ( ) ) ) ) this . formattedBody += "<br>"
this . formattedBody += formattedBody
}
return this
}
/ * *
* @ param { string } body
* @ param { string } [ formattedBody ]
* @ param { any } [ condition ]
* /
addParagraph ( body , formattedBody , condition = true ) {
if ( condition ) {
if ( formattedBody == undefined ) formattedBody = body
if ( this . body . length && this . body . slice ( - 1 ) !== "\n" ) this . body += "\n\n"
this . body += body
formattedBody = ` <p> ${ formattedBody } </p> `
this . formattedBody += formattedBody
}
return this
}
get ( ) {
return {
msgtype : "m.text" ,
body : this . body ,
format : "org.matrix.custom.html" ,
formatted _body : this . formattedBody
}
}
}
2024-02-09 04:29:05 +00:00
/ * *
2024-02-12 10:07:55 +00:00
* Context : Room IDs are not routable on their own . Room permalinks need a list of servers to try . The client is responsible for coming up with a list of servers .
* ASSUMPTION 1 : The bridge bot is a member of the target room and can therefore access its power levels and member list for calculation .
* ASSUMPTION 2 : Because the bridge bot is a member of the target room , the target room is bridged .
2024-02-09 04:29:05 +00:00
* https : //spec.matrix.org/v1.9/appendices/#routing
* https : //gitdab.com/cadence/out-of-your-element/issues/11
* @ param { string } roomID
* @ param { { [ K in "getStateEvent" | "getJoinedMembers" ] : import ( "../../matrix/api" ) [ K ] } } api
* /
async function getViaServers ( roomID , api ) {
const candidates = [ ]
const { joined } = await api . getJoinedMembers ( roomID )
// Candidate 0: The bot's own server name
candidates . push ( reg . ooye . server _name )
// Candidate 1: Highest joined non-sim non-bot power level user in the room
// https://github.com/matrix-org/matrix-react-sdk/blob/552c65db98b59406fb49562e537a2721c8505517/src/utils/permalinks/Permalinks.ts#L172
try {
/** @type {{users?: {[mxid: string]: number}}} */
const powerLevels = await api . getStateEvent ( roomID , "m.room.power_levels" , "" )
if ( powerLevels . users ) {
const sorted = Object . entries ( powerLevels . users ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) // Highest...
for ( const power of sorted ) {
const mxid = power [ 0 ]
if ( ! ( mxid in joined ) ) continue // joined...
if ( userRegex . some ( r => mxid . match ( r ) ) ) continue // non-sim non-bot...
const match = mxid . match ( /:(.*)/ )
assert ( match )
if ( ! candidates . includes ( match [ 1 ] ) ) {
candidates . push ( match [ 1 ] )
break
}
}
}
} catch ( e ) {
// power levels event not found
}
// Candidates 2-3: Most popular servers in the room
/** @type {Map<string, number>} */
const servers = new Map ( )
// We can get the most popular servers if we know the members, so let's process those...
Object . keys ( joined )
. filter ( mxid => ! mxid . startsWith ( "@_" ) ) // Quick check
. filter ( mxid => ! userRegex . some ( r => mxid . match ( r ) ) ) // Full check
. slice ( 0 , 1000 ) // Just sample the first thousand real members
. map ( mxid => {
const match = mxid . match ( /:(.*)/ )
assert ( match )
return match [ 1 ]
} )
. filter ( server => ! server . match ( /([a-f0-9:]+:+)+[a-f0-9]+/ ) ) // No IPv6 servers
. filter ( server => ! server . match ( /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ ) ) // No IPv4 servers
// I don't care enough to check ACLs
. forEach ( server => {
const existing = servers . get ( server )
if ( ! existing ) servers . set ( server , 1 )
else servers . set ( server , existing + 1 )
} )
const serverList = [ ... servers . entries ( ) ] . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
for ( const server of serverList ) {
if ( ! candidates . includes ( server [ 0 ] ) ) {
candidates . push ( server [ 0 ] )
if ( candidates . length >= 4 ) break // Can have at most 4 candidate via servers
}
}
return candidates
}
2024-02-12 10:07:55 +00:00
/ * *
* Context : Room IDs are not routable on their own . Room permalinks need a list of servers to try . The client is responsible for coming up with a list of servers .
* ASSUMPTION 1 : The bridge bot is a member of the target room and can therefore access its power levels and member list for calculation .
* ASSUMPTION 2 : Because the bridge bot is a member of the target room , the target room is bridged .
* https : //spec.matrix.org/v1.9/appendices/#routing
* https : //gitdab.com/cadence/out-of-your-element/issues/11
* @ param { string } roomID
* @ param { { [ K in "getStateEvent" | "getJoinedMembers" ] : import ( "../../matrix/api" ) [ K ] } } api
* @ returns { Promise < URLSearchParams > }
* /
async function getViaServersQuery ( roomID , api ) {
const list = await getViaServers ( roomID , api )
const qs = new URLSearchParams ( )
for ( const server of list ) {
qs . append ( "via" , server )
}
return qs
}
2023-09-26 19:20:18 +00:00
module . exports . BLOCK _ELEMENTS = BLOCK _ELEMENTS
2023-07-03 12:39:42 +00:00
module . exports . eventSenderIsFromDiscord = eventSenderIsFromDiscord
2023-08-26 10:22:54 +00:00
module . exports . getPublicUrlForMxc = getPublicUrlForMxc
2023-09-22 05:47:36 +00:00
module . exports . getEventIDHash = getEventIDHash
2023-10-27 11:24:42 +00:00
module . exports . MatrixStringBuilder = MatrixStringBuilder
2024-02-09 04:29:05 +00:00
module . exports . getViaServers = getViaServers
2024-02-12 10:07:55 +00:00
module . exports . getViaServersQuery = getViaServersQuery