// AO shadowchat feature menu including bootstrap network server list browser, chatroom list on each server, and chatroom interface // Called shadowchat because no record is kept of the chat messages, and all connections happen E2E over tor // As this feature gets build, sensible standards must be developed around when tor addresses change hands, when users authenticate, etc import { aoEnv } from '../ao-lib/settings.js' import { isLoggedIn } from './session.js' import { startPublicBootstrap } from './bootstrap.js' import { headerStyle } from './styles.js' import { askQuestionText, promptMenu } from './welcome.js' import { AO_DEFAULT_HOSTNAME, startSocketListeners, socketStatus, socket, shadowchat } from '../ao-lib/api.js' import { execSync } from 'child_process' import { fileURLToPath } from 'url' import path from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const sleep = (ms = 550) => { return new Promise((r) => setTimeout(r, ms)) } // Prints a menu that allows you to join the global AO chatrooms export default async function chatMenu() { let answers = {} const PUBLIC_BOOTSTRAP_ENABLED = aoEnv('PUBLIC_BOOTSTRAP_ENABLED') if(PUBLIC_BOOTSTRAP_ENABLED) { // They previously enabled public bootstrapping, so check to make sure it is working and then hide the option // todo: start and then verify functioning of p2p boostrap method here // if it's already started don't start it again //if(!publicBootstrapStarted) { console.log("\nBootstrapping public AO swarm...") startPublicBootstrap() console.log("Bootstrapped (just kidding)") //} //answers['chat_menu'] = 'Enable p2p bootstrap' } const chatChoices = [ { title: 'Join public chatroom', value: 'browse_chatrooms', short: 'public chatrooms' }, { title: 'Join chatroom', value: 'join_chat', short: 'join chat' }, 'Address Book', 'Back to AO Menu', ] const answer = await promptMenu(chatChoices, 'AO Public Chatrooms') switch(answer) { case 'browse_chatrooms': const loggedIn = isLoggedIn() if(!isLoggedIn) { console.log('Please start AO server and log in first.') return true } console.log('Logged in as:', aoEnv('AO_CLI_SESSION_USERNAME')) const onAuthenticated = () => { //console.log('Websocket connected and authenticated.') } const onEvent = (event) => { console.log('\nEvent received on websocket:', event) // todo: still receiving excess task-resets every 5 minutes } if(socketStatus !== 'authenticationSuccess') { await startSocketListeners(onAuthenticated, onEvent) await sleep(400) } console.log('socketStatus is', socketStatus) if(socketStatus !== 'authenticationSuccess') { console.log('Failed to connect to websocket. Make sure your AO server is running, and if you have a custom config set your target server in ao-cli.') return true } console.log('Websocket successfully connected to listen for events.') while(await browseChatrooms()) {} break case 'join_chat': console.log('Launching Simplex Chat...') execSync('bash simplex-chat') break case 'Address Book': console.log('The point of this address book is to make it possible to type short, one-word names and have them resolve to tor addresses.') console.log('Name a piece of data by saying name=data. For example, doge=5uc41o1...onion. Then \'doge\' will return the .onion address.') console.log('Querying with any synonym in a chain will return the final meanings they all point to.') console.log('Keys can have multiple values.') break default: return false } return true } async function getLocalRooms() { return new Promise((resolve, reject) => { socket.on('rooms_list', event => { socket.off('rooms_list') resolve(event) }) socket.emit('get_rooms') }) } async function browseChatrooms() { if(socketStatus !== 'authenticationSuccess') { console.log('Websocket is not connected, going back.') return false } console.log('Getting list of chatrooms...') const chatrooms = await getLocalRooms() // Get list of all servers and display // todo: check if websocket can work without authentication // For each server, request its list of channels console.log('Chatrooms on this server:', chatrooms) //await chatInRoom('HUB') return await chatroomScreen('HUB') // Display the list as a menu with a heading for each server } async function chatInRoom(room) { console.log('Joining room...') socket.emit('join_room', room) return new Promise(async (resolve, reject) => { const leaveChatroom = () => { console.log('Leaving room...') socket.emit('leave_room', room) socket.off('chat') resolve() } socket.on('chat', event => { const chatLine = event.name ? event.name + ': ' + event.message : 'Anon:' + event.message console.log(/*new Date().toLocaleTimeString(), */'\n' + chatLine) }) const username = aoEnv('AO_CLI_SESSION_USERNAME') let message do { message = await askQuestionText('> ') switch(message.toLowerCase()) { case 'exit': case 'leave': case 'back': case 'quit': case false: resolve(false) default: //socket.emit('chat', room, message, username || undefined) shadowchat(room, message, username || undefined) } } while(message !== 'exit') }) } /*async function chatroomScreen(room) { var ui = new inquirer.ui.BottomBar() ui.render() console.log(ui) // pipe a Stream to the log zone //outputStream.pipe(ui.log) // Or simply write output ui.log.write('something just happened.'); ui.log.write('Almost over, standby!'); // During processing, update the bottom bar content to display a loader // or output a progress bar, etc ui.updateBottomBar('new bottom bar content'); }*/ import blessed from 'blessed' export async function chatroomScreen(room) { const screen = blessed.screen({ smartCSR: true, ignoreLocked: ['C-c'], dockBorders: true, title: room }) const chatlog = blessed.log({ top: 0, left: 0, bottom: 2, width: '100%', content: '{bold}' + room + '{/bold}', tags: true, scrollable: true, border: { type: 'line' }, style: { fg: 'white', bg: 'black', border: { fg: '#f0f0f0' }, } }) screen.append(chatlog); const input = blessed.textbox({ bottom: 0, left: 0, right: 0, height: 3, content: 'Type message here', inputOnFocus: true, border: { type: 'line' }, style: { fg: 'white', bg: 'blue', border: { fg: '#f0f0f0' }, } }) input.on('submit', (event) => { chatlog.log('new submission:', event) input.clearValue() input.focus() }) input.on('keypress', (event) => { chatlog.add('got keypress:', event) }) screen.append(input) //screen.focusPop() chatlog.add('screen.focus:', screen.focus) input.focus() screen.render() return new Promise(async (resolve, reject) => { //screen.key(['C-c'], () => process.exit(0)) screen.key(['escape', 'C-c'], function(ch, key) { chatlog.log('Exiting chatroom...') screen.destroy() resolve(false) }) }) }