barechat
Version:
Anonymous chat anywhere with commandline
187 lines (174 loc) • 7.78 kB
JavaScript
import Hyperswarm from 'hyperswarm' // Module for P2P networking and connecting peers
import b4a from 'b4a' // Module for buffer-to-string and vice-versa conversions
import { createHash } from 'crypto'
import { randomBytes } from 'hypercore-crypto' // Cryptographic functions for generating the key in app
import { version } from '../package.json'
console.log(`BareChat v.${version}`)
/**
* Creates a Hyperswarm instance with optional bootstrap configuration
* @param {Object} [opts] - Configuration options
* @param {string[]} [opts.bootstrap] - Array of bootstrap servers for peer discovery
* @returns {Hyperswarm} Configured Hyperswarm instance
*/
const getSwarm = (opts) => opts?.bootstrap ? new Hyperswarm({ bootstrap: opts.bootstrap }) : new Hyperswarm()
/**
* Checks if a string is a valid 64-character hexadecimal hash (32 bytes)
* @param {string} str - The string to validate
* @returns {boolean} True if the string is a valid 64-character hex string
* @example
* isHashcode('eaabffe32a969eeae9a4588a6e088534aae8066db2c055107b9e700fd6033089') // true
* isHashcode('hello world') // false
* isHashcode('abc123') // false (too short)
*/
const isHashcode = (str) => {
if (typeof str !== 'string') return false
if (str.length !== 64) return false
return /^[0-9a-fA-F]{64}$/.test(str)
}
/**
* Converts a string to a topic buffer. If the string is already a valid hashcode,
* returns it as-is. Otherwise, creates a SHA256 hash of the string.
* @param {string} topicStr - The topic string to convert
* @returns {string} A 64-character hex string representing the topic
* @example
* // Using an existing hash
* strToTopic('eaabffe32a969eeae9a4588a6e088534aae8066db2c055107b9e700fd6033089')
* // Returns: 'eaabffe32a969eeae9a4588a6e088534aae8066db2c055107b9e700fd6033089'
*
* // Converting a readable string
* strToTopic('my-chat-room')
* // Returns: SHA256 hash of 'my-chat-room' as hex string
*/
const strToTopic = (topicStr) => isHashcode(topicStr) ? topicStr : createHash('sha256').update(topicStr).digest('hex')
/**
* Generates a short, human-readable identifier for a peer based on their remote public key
* @param {Object} peer - The peer object from the Hyperswarm connection
* @returns {string} A 6-character hex string representing the member ID, or 'invalid' if the peer object is not valid
* @example
* swarm.on('connection', (peer) => {
* const memberId = getMemberId(peer);
* console.log("New peer connected with ID:", memberId);
* });
*/
const getMemberId = (peer) => (
peer ? b4a.toString(peer.remotePublicKey, 'hex')?.substring(0, 6) : 'invalid'
)
/**
* Joins or creates a chat room for the specified topic
* @param {Hyperswarm} swarm - The Hyperswarm instance to use
* @returns {Function} An async function that joins/creates a room
*/
const joinRoom = swarm =>
/**
* Joins an existing chat room or creates a new one. If no topic is provided, generates a random topic.
* In P2P networks, joining a topic effectively creates it if it doesn't exist.
* @param {string} [topicStr] - Optional topic string (can be human-readable text, hex-encoded hash, or omitted for random)
* @returns {Promise<{done: boolean, topic: string}>} Promise resolving to join/create result
* @property {boolean} done - Indicates if the swarm joining process is complete
* @property {string} topic - The hex-encoded topic of the joined/created room, or empty string/'err' on error
* @example
* const { joinRoom } = getBackend();
*
* // Create room with random topic
* const randomRoom = await joinRoom();
* console.log("Joined/created random room:", randomRoom.topic);
*
* // Join/create room with human-readable name
* const namedRoom = await joinRoom('general-chat');
* console.log("Joined/created named room:", namedRoom.topic);
*
* // Join/create room with hex-encoded topic
* const hashRoom = await joinRoom('a1b2c35fbeb452bc900c5a1c00306e52319a3159317312f54fe5a246d634f51a');
*
*/
async (topicStr) => {
try {
let topicBuffer
let topic
if (topicStr) {
// Use provided topic string, convert to proper hash format
topic = strToTopic(topicStr)
topicBuffer = b4a.from(topic, 'hex')
} else {
// Generate a new random topic (32 byte string)
topicBuffer = randomBytes(32)
topic = b4a.toString(topicBuffer, 'hex')
}
// Join the swarm with the topic. Setting both client/server to true means that this app can act as both.
const discovery = swarm.join(topicBuffer, { client: true, server: true })
const done = await discovery.flushed()
return { done, topic: done ? topic : '' }
} catch (error) {
console.warn(`join error: ${error}`)
return { done: false, topic: `err` }
}
}
/**
* Creates a message object.
*
* @param {string} msg - The content of the message.
* @param {boolean} [local=false] - Indicates whether the message is send from a local device.
* Defaults to `false`.
* @returns {object} A message object with a timestamp, message content, local flag, and type.
* @property {Date} timestamp - The date and time the message was created.
* @property {string} message - The content of the message.
* @property {boolean} local - Indicates if the message is send from a local device.
* @property {string} type - The type of the message. Defaults to `text`
*/
export const createMessage = (msg, local = false, messageType = 'text') => ({
timestamp: new Date(),
message: msg,
local,
messageType,
})
/**
* Sends a message to all peers currently connected in the swarm
* @param {Hyperswarm} swarm - The Hyperswarm instance to use
* @returns {Function} A function that sends messages
*/
const sendMessage = swarm =>
/**
* Sends a message to all peers currently connected in the swarm
* @param {string} message - The message content to send
* @returns {undefined}
* @example
* const { sendMessage } = getBackend();
* sendMessage("Hello everyone in the room!");
*/
(message) => {
// Send the message to all peers (that you are connected to)
const peers = [...swarm.connections]
const event = createMessage(message, true)
for (const peer of peers) peer.write(JSON.stringify(event))
}
/**
* Initializes the networking layer and returns an object containing the core API functions for interacting with the chat swarm
* @param {Object} [opts] - Configuration options for the Hyperswarm instance
* @param {string[]} [opts.bootstrap] - A list of bootstrap servers to use for discovering peers
* @returns {Object} An object containing core API functions and properties
* @property {Function} createRoom - Creates a new chat room (alias for joinRoom for backward compatibility)
* @property {Function} getMemberId - Generates a short identifier for a peer
* @property {Function} joinRoom - Joins an existing chat room using a topic string
* @property {Function} sendMessage - Sends a message to all connected peers
* @property {Function} strToTopic - Converts a string to a valid topic hash
* @property {Function} isHashcode - Validates if a string is a valid 64-character hex hash
* @property {Hyperswarm} swarm - The underlying Hyperswarm instance
* @property {string} version - The version of the BareChat package
* @example
* const backend = getBackend({ bootstrap: ['192.168.0.123:55688'] });
* const room = await backend.joinRoom();
* backend.sendMessage("Hello world!");
*/
export const getBackend = (opts) => {
const swarm = getSwarm(opts)
return {
createRoom: joinRoom(swarm),
getMemberId,
joinRoom: joinRoom(swarm),
sendMessage: sendMessage(swarm),
strToTopic,
isHashcode,
swarm,
version,
}
}