2024-09-14 12:55:54 +00:00
#!/usr/bin/env node
2023-09-06 01:07:05 +00:00
// @ts-check
const assert = require ( "assert" ) . strict
const fs = require ( "fs" )
const sqlite = require ( "better-sqlite3" )
2024-09-12 15:16:03 +00:00
const { scheduler } = require ( "timers/promises" )
2024-09-05 04:48:53 +00:00
const { isDeepStrictEqual } = require ( "util" )
2024-09-12 15:16:03 +00:00
const { createServer } = require ( "http" )
2024-09-05 04:48:53 +00:00
const { prompt } = require ( "enquirer" )
2024-09-05 03:36:43 +00:00
const Input = require ( "enquirer/lib/prompts/input" )
2024-09-13 12:58:21 +00:00
const fetch = require ( "node-fetch" ) . default
2024-09-05 04:48:53 +00:00
const { magenta , bold , cyan } = require ( "ansi-colors" )
const HeatSync = require ( "heatsync" )
2024-09-12 15:16:03 +00:00
const { SnowTransfer } = require ( "snowtransfer" )
const { createApp , defineEventHandler , toNodeListener } = require ( "h3" )
2023-09-06 01:07:05 +00:00
2023-10-07 10:39:49 +00:00
const args = require ( "minimist" ) ( process . argv . slice ( 2 ) , { string : [ "emoji-guild" ] } )
2024-09-12 15:16:03 +00:00
// Move database file if it's still in the old location
if ( fs . existsSync ( "db" ) ) {
if ( fs . existsSync ( "db/ooye.db" ) ) {
fs . renameSync ( "db/ooye.db" , "src/db/ooye.db" )
}
const files = fs . readdirSync ( "db" )
if ( files . length ) {
console . error ( "You must manually move or delete the files in the db folder:" )
for ( const file of files ) {
console . error ( file )
}
process . exit ( 1 )
}
fs . rmSync ( "db" , { recursive : true } )
}
const passthrough = require ( "../src/passthrough" )
const db = new sqlite ( "src/db/ooye.db" )
const migrate = require ( "../src/db/migrate" )
2023-09-06 01:07:05 +00:00
const sync = new HeatSync ( { watchFS : false } )
2024-09-14 12:55:54 +00:00
Object . assign ( passthrough , { sync , db } )
2023-10-07 08:57:09 +00:00
2024-09-12 15:16:03 +00:00
const orm = sync . require ( "../src/db/orm" )
2023-10-07 08:57:09 +00:00
passthrough . from = orm . from
passthrough . select = orm . select
2024-09-12 15:16:03 +00:00
let registration = require ( "../src/matrix/read-registration" )
let { reg , getTemplateRegistration , writeRegistration , readRegistration , checkRegistration , registrationFilePath } = registration
2023-09-06 01:07:05 +00:00
2023-10-07 10:39:49 +00:00
function die ( message ) {
console . error ( message )
process . exit ( 1 )
}
2024-09-12 15:16:03 +00:00
async function uploadAutoEmoji ( snow , guild , name , filename ) {
2023-10-07 10:39:49 +00:00
let emoji = guild . emojis . find ( e => e . name === name )
if ( ! emoji ) {
console . log ( ` Uploading ${ name } ... ` )
const data = fs . readFileSync ( filename , null )
2024-09-12 15:16:03 +00:00
emoji = await snow . guildAssets . createEmoji ( guild . id , { name , image : "data:image/png;base64," + data . toString ( "base64" ) } )
2023-10-07 10:39:49 +00:00
} else {
console . log ( ` Reusing ${ name } ... ` )
}
db . prepare ( "REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES (?, ?, ?)" ) . run ( emoji . name , emoji . id , guild . id )
return emoji
}
2024-09-05 04:48:53 +00:00
async function validateHomeserverOrigin ( serverUrlPrompt , url ) {
if ( ! url . match ( /^https?:\/\// ) ) return "Must be a URL"
if ( url . match ( /\/$/ ) ) return "Must not end with a slash"
process . stdout . write ( magenta ( " checking, please wait..." ) )
try {
var json = await fetch ( ` ${ url } /.well-known/matrix/client ` ) . then ( res => res . json ( ) )
let baseURL = json [ "m.homeserver" ] . base _url . replace ( /\/$/ , "" )
if ( baseURL && baseURL !== url ) {
serverUrlPrompt . initial = baseURL
return ` Did you mean: ${ bold ( baseURL ) } ? (Enter to accept) `
}
} catch ( e ) { }
try {
var res = await fetch ( ` ${ url } /_matrix/client/versions ` )
} catch ( e ) {
return e . message
}
if ( res . status !== 200 ) return ` There is no Matrix server at that URL ( ${ url } /_matrix/client/versions returned ${ res . status } ) `
try {
var json = await res . json ( )
} catch ( e ) {
return ` There is no Matrix server at that URL ( ${ url } /_matrix/client/versions is not JSON) `
}
return true
}
2023-09-06 01:07:05 +00:00
; ( async ( ) => {
2024-09-05 03:36:43 +00:00
// create registration file with prompts...
if ( ! reg ) {
console . log ( "What is the name of your homeserver? This is the part after : in your username." )
/** @type {{server_name: string}} */
const serverNameResponse = await prompt ( {
type : "input" ,
name : "server_name" ,
message : "Homeserver name"
} )
2024-09-12 15:16:03 +00:00
2024-09-05 03:36:43 +00:00
console . log ( "What is the URL of your homeserver?" )
2024-09-12 15:16:03 +00:00
const serverOriginPrompt = new Input ( {
2024-09-05 03:36:43 +00:00
type : "input" ,
name : "server_origin" ,
message : "Homeserver URL" ,
initial : ( ) => ` https:// ${ serverNameResponse . server _name } ` ,
2024-09-12 15:16:03 +00:00
validate : url => validateHomeserverOrigin ( serverOriginPrompt , url )
2024-09-05 03:36:43 +00:00
} )
2024-09-12 15:16:03 +00:00
/** @type {string} */ // @ts-ignore
const serverOrigin = await serverOriginPrompt . run ( )
const app = createApp ( )
app . use ( defineEventHandler ( ( ) => "Out Of Your Element is listening.\n" ) )
const server = createServer ( toNodeListener ( app ) )
await server . listen ( 6693 )
console . log ( "OOYE has its own web server. It needs to be accessible on the public internet." )
console . log ( "You need to enter a public URL where you will be able to host this web server." )
console . log ( "OOYE listens on localhost:6693, so you will probably have to set up a reverse proxy." )
console . log ( "Now listening on port 6693. Feel free to send some test requests." )
/** @type {{bridge_origin: string}} */
const bridgeOriginResponse = await prompt ( {
2024-09-05 03:36:43 +00:00
type : "input" ,
2024-09-12 15:16:03 +00:00
name : "bridge_origin" ,
2024-09-05 03:36:43 +00:00
message : "URL to reach OOYE" ,
2024-09-12 15:16:03 +00:00
initial : ( ) => ` https://bridge. ${ serverNameResponse . server _name } ` ,
validate : async url => {
process . stdout . write ( magenta ( " checking, please wait..." ) )
try {
const res = await fetch ( url )
if ( res . status !== 200 ) return ` Server returned status code ${ res . status } `
const text = await res . text ( )
if ( text !== "Out Of Your Element is listening.\n" ) return ` Server does not point to OOYE `
return true
} catch ( e ) {
return e . message
}
}
} )
await server . close ( )
console . log ( "What is your Discord bot token?" )
/** @type {{discord_token: string}} */
const discordTokenResponse = await prompt ( {
type : "input" ,
name : "discord_token" ,
message : "Bot token" ,
validate : async token => {
process . stdout . write ( magenta ( " checking, please wait..." ) )
try {
const snow = new SnowTransfer ( token )
await snow . user . getSelf ( )
return true
} catch ( e ) {
return e . message
}
}
2024-09-05 03:36:43 +00:00
} )
const template = getTemplateRegistration ( )
2024-09-12 15:16:03 +00:00
reg = { ... template , url : bridgeOriginResponse . bridge _origin , ooye : { ... template . ooye , ... serverNameResponse , ... bridgeOriginResponse , server _origin : serverOrigin , ... discordTokenResponse } }
2024-09-05 03:36:43 +00:00
registration . reg = reg
2024-09-12 15:16:03 +00:00
checkRegistration ( reg )
2024-09-05 03:36:43 +00:00
writeRegistration ( reg )
2024-09-12 15:16:03 +00:00
console . log ( ` ✅ Registration file saved as ${ registrationFilePath } ` )
} else {
console . log ( ` ✅ Valid registration file found at ${ registrationFilePath } ` )
2024-09-05 03:36:43 +00:00
}
2024-09-05 04:48:53 +00:00
console . log ( ` In ${ cyan ( "Synapse" ) } , you need to add it to homeserver.yaml and ${ cyan ( "restart Synapse" ) } . ` )
console . log ( " https://element-hq.github.io/synapse/latest/application_services.html" )
console . log ( ` In ${ cyan ( "Conduit" ) } , you need to send the file contents to the #admins room. ` )
console . log ( " https://docs.conduit.rs/appservices.html" )
console . log ( )
2024-09-12 15:16:03 +00:00
// Done with user prompts, reg is now guaranteed to be valid
const api = require ( "../src/matrix/api" )
const file = require ( "../src/matrix/file" )
const utils = require ( "../src/m2d/converters/utils" )
const DiscordClient = require ( "../src/d2m/discord-client" )
const discord = new DiscordClient ( reg . ooye . discord _token , "no" )
passthrough . discord = discord
const { as } = require ( "../src/matrix/appservice" )
2024-09-05 04:48:53 +00:00
console . log ( "⏳ Waiting until homeserver registration works... (Ctrl+C to cancel)" )
let itWorks = false
let lastError = null
do {
2024-09-08 22:31:10 +00:00
const result = await api . ping ( ) . catch ( e => ( { ok : false , status : "net" , root : e . message } ) )
2024-09-05 04:48:53 +00:00
// If it didn't work, log details and retry after some time
itWorks = result . ok
if ( ! itWorks ) {
// Log the full error data if the error is different to last time
if ( ! isDeepStrictEqual ( lastError , result . root ) ) {
2024-09-08 22:31:10 +00:00
if ( typeof result . root === "string" ) {
console . log ( ` \n Cannot reach homeserver: ${ result . root } ` )
} else if ( result . root . error ) {
2024-09-05 04:48:53 +00:00
console . log ( ` \n Homeserver said: [ ${ result . status } ] ${ result . root . error } ` )
} else {
console . log ( ` \n Homeserver said: [ ${ result . status } ] ${ JSON . stringify ( result . root ) } ` )
}
lastError = result . root
} else {
process . stderr . write ( "." )
}
2024-09-12 15:16:03 +00:00
await scheduler . wait ( 5000 )
2024-09-05 04:48:53 +00:00
}
} while ( ! itWorks )
console . log ( "" )
as . close ( ) . catch ( ( ) => { } )
console . log ( "⏩ Processing. This could take up to 30 seconds. Please be patient..." )
2023-09-06 01:07:05 +00:00
const mxid = ` @ ${ reg . sender _localpart } : ${ reg . ooye . server _name } `
// ensure registration is correctly set...
2024-03-23 05:39:37 +00:00
assert ( reg . sender _localpart . startsWith ( reg . ooye . namespace _prefix ) , "appservice's localpart must be in the namespace it controls" )
assert ( utils . eventSenderIsFromDiscord ( mxid ) , "appservice's mxid must be in the namespace it controls" )
assert ( reg . ooye . server _origin . match ( /^https?:\/\// ) , "server origin must start with http or https" )
assert . notEqual ( reg . ooye . server _origin . slice ( - 1 ) , "/" , "server origin must not end in slash" )
2024-09-14 12:55:54 +00:00
const botID = Buffer . from ( reg . ooye . discord _token . split ( "." ) [ 0 ] , "base64" ) . toString ( )
2024-03-23 05:39:37 +00:00
assert ( botID . match ( /^[0-9]{10,}$/ ) , "discord token must follow the correct format" )
2024-08-28 00:51:28 +00:00
assert . match ( reg . url , /^https?:/ , "url must start with http:// or https://" )
2024-09-05 03:36:43 +00:00
2023-10-07 08:57:09 +00:00
console . log ( "✅ Configuration looks good..." )
2023-09-06 01:07:05 +00:00
2023-09-12 07:23:23 +00:00
// database ddl...
2023-10-02 01:00:14 +00:00
await migrate . migrate ( db )
2023-09-12 07:23:23 +00:00
2023-10-07 08:57:09 +00:00
// add initial rows to database, like adding the bot to sim...
2024-03-23 05:39:37 +00:00
db . prepare ( "INSERT OR IGNORE INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)" ) . run ( botID , reg . sender _localpart . slice ( reg . ooye . namespace _prefix . length ) , reg . sender _localpart , mxid )
2023-10-07 08:57:09 +00:00
console . log ( "✅ Database is ready..." )
2023-10-07 10:39:49 +00:00
2023-10-16 03:47:42 +00:00
// ensure appservice bot user is registered...
try {
await api . register ( reg . sender _localpart )
} catch ( e ) {
2024-01-20 04:03:03 +00:00
if ( e . errcode === "M_USER_IN_USE" || e . data ? . error === "Internal server error" ) {
// "Internal server error" is the only OK error because older versions of Synapse say this if you try to register the same username twice.
} else {
throw e
}
2023-10-16 03:47:42 +00:00
}
// upload initial images...
const avatarUrl = await file . uploadDiscordFileToMxc ( "https://cadence.moe/friends/out_of_your_element.png" )
console . log ( "✅ Matrix appservice login works..." )
2023-10-07 10:39:49 +00:00
// upload the L1 L2 emojis to some guild
const emojis = db . prepare ( "SELECT name FROM auto_emoji WHERE name = 'L1' OR name = 'L2'" ) . pluck ( ) . all ( )
if ( emojis . length !== 2 ) {
// If an argument was supplied, always use that one
let guild = null
if ( args [ "emoji-guild" ] ) {
if ( typeof args [ "emoji-guild" ] === "string" ) {
guild = await discord . snow . guild . getGuild ( args [ "emoji-guild" ] )
}
if ( ! guild ) return die ( ` Error: You asked emojis to be uploaded to guild ID ${ args [ "emoji-guild" ] } , but the bot isn't in that guild. ` )
}
// Otherwise, check if we have already registered an auto emoji guild
if ( ! guild ) {
const guildID = passthrough . select ( "auto_emoji" , "guild_id" , { name : "_" } ) . pluck ( ) . get ( )
if ( guildID ) {
guild = await discord . snow . guild . getGuild ( guildID , false )
}
}
// Otherwise, check if we should create a new guild
if ( ! guild ) {
const guilds = await discord . snow . user . getGuilds ( { limit : 11 , with _counts : false } )
if ( guilds . length < 10 ) {
console . log ( " Creating a guild for emojis..." )
guild = await discord . snow . guild . createGuild ( { name : "OOYE Emojis" } )
}
}
// Otherwise, it's the user's problem
if ( ! guild ) {
return die ( ` Error: The bot needs to upload some emojis. Please say where to upload them to. Run seed.js again with --emoji-guild=GUILD_ID ` )
}
// Upload those emojis to the chosen location
db . prepare ( "REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)" ) . run ( guild . id )
2024-09-12 15:16:03 +00:00
await uploadAutoEmoji ( discord . snow , guild , "L1" , "docs/img/L1.png" )
await uploadAutoEmoji ( discord . snow , guild , "L2" , "docs/img/L2.png" )
2023-10-07 10:39:49 +00:00
}
console . log ( "✅ Emojis are ready..." )
2023-10-07 08:57:09 +00:00
// set profile data on discord...
const avatarImageBuffer = await fetch ( "https://cadence.moe/friends/out_of_your_element.png" ) . then ( res => res . arrayBuffer ( ) )
await discord . snow . user . updateSelf ( { avatar : "data:image/png;base64," + Buffer . from ( avatarImageBuffer ) . toString ( "base64" ) } )
await discord . snow . requestHandler . request ( ` /applications/@me ` , { } , "patch" , "json" , { description : "Powered by **Out Of Your Element**\nhttps://gitdab.com/cadence/out-of-your-element" } )
console . log ( "✅ Discord profile updated..." )
2023-09-06 01:07:05 +00:00
// set profile data on homeserver...
await api . profileSetDisplayname ( mxid , "Out Of Your Element" )
await api . profileSetAvatarUrl ( mxid , avatarUrl )
2023-10-07 08:57:09 +00:00
console . log ( "✅ Matrix profile updated..." )
2023-09-06 01:07:05 +00:00
2023-10-07 08:57:09 +00:00
console . log ( "Good to go. I hope you enjoy Out Of Your Element." )
process . exit ( )
2023-09-06 01:07:05 +00:00
} ) ( )