let { e2ee } = await import(`./e2ee.js?r=${revision}`); let { ws } = await import(`./ws.js?r=${revision}`); let { ui } = await import(`./ui.js?r=${revision}`); let { stream } = await import(`./stream.js?r=${revision}`); let { listeners } = await import(`./listeners.js?r=${revision}`); function log(string) { console.log(string); } export class HNSChat { constructor() { window.hnschat = this; try { this.varo = new Varo(); } catch {} this.mobile = false; this.host = window.location.host; this.hash = window.location.hash.substring(1); this.page; this.data; this.ui = new ui(this); this.ws = new ws(this); this.listeners = new listeners(this); this.e2ee = new e2ee(); this.streamURL = "https://media.hns.chat/stream"; this.stream; this.isActive = true; this.keys; this.session; this.domain; this.domains; this.staked; this.conversation; this.settings; this.messages = []; this.seen = {}; this.users; this.channels; this.pms; this.tab = "channels"; this.timeFormat = "g:i A"; this.dateFormat = "F jS, Y"; this.gotChannels; this.gotPms; this.gotMentions; this.loadingMessages; this.replying; this.queued = []; this.typing; this.typingSent; this.typingDelay = 2; this.typingSendDelay = 1; this.lastTyped; this.typers = {}; this.active = []; this.avatars = {}; this.action = "\x01ACTION"; this.commands = ["me", "shrug", "slap"]; this.hasBob; this.init(); } varoLoaded() { if (typeof this.varo !== "undefined") { return true; } return false } getPage() { let pathname = document.location.pathname; let match = pathname.match(/\/(?.+?)(?:\/(?.+)|$)/); if (match) { let groups = match.groups; if (groups.page) { this.page = groups.page; } if (groups.data) { this.data = groups.data; } } else { this.page = "chat"; } } async api(data) { if (this.session) { data.session = this.session; } let output = new Promise(function(resolve) { $.post("/api", JSON.stringify(data), function(response){ if (response) { let json = JSON.parse(response); resolve(json); } }); }); return await output; } time() { return Math.floor(Date.now() / 1000); } handlePush(init=false) { if (localStorage.handlePush) { try { let info = JSON.parse(localStorage.handlePush); localStorage.setItem("domain", info.domain); localStorage.setItem("conversation", info.conversation); if (!init) { if (this.domain !== info.domain) { this.changeDomain(info.domain); } this.changeConversation(info.conversation); } } catch {} } localStorage.removeItem("handlePush"); } async init() { this.handlePush(true); this.getPage(); switch (this.page) { case "sync": this.loadSync(); break; case "buy": break; default: try { this.keys = JSON.parse(localStorage.keys); } catch { this.e2ee.generateKeys().then(r => { this.keys = r; localStorage.setItem("keys", JSON.stringify(this.keys)); }); } if (this.hash) { localStorage.setItem("hash", this.hash); history.pushState("", document.title, window.location.pathname + window.location.search); } else { this.hash = localStorage.hash; } this.mobile = localStorage.mobile || false; this.session = localStorage.session; this.domain = localStorage.domain; this.conversation = localStorage.conversation; try { this.settings = JSON.parse(localStorage.settings); this.loadSettings(); } catch { this.settings = {}; } this.firstLaunch = localStorage.firstLaunch; if (!this.firstLaunch) { localStorage.setItem("firstLaunch", this.time()); } await this.startSession(); await this.setPublicKey(); await this.ws.connect(); break; } switch (this.page) { case "chat": this.stream = new stream(this); this.ui.setConversationTab(); this.ui.setupSync(); this.ui.setupNotifications(); break; } if (typeof bob3 != "undefined") { this.hasBob = true; this.ui.hasBob(); } } loadSettings() { Object.keys(this.settings).forEach((setting, k) => { let value = this.settings[setting]; switch (setting) { case "chatDisplayMode": this.ui.chatDisplayMode(value); break; default: this.ui.root.style.setProperty(`--${setting}`, value); break; } }); } loadSync() { let hash = this.hash; let db64 = this.db64(hash); let json = JSON.parse(db64); this.session = json.session; localStorage.setItem("session", this.session); if (json.settings) { this.settings = json.settings; localStorage.setItem("settings", JSON.stringify(this.settings)); } let data = { action: "getPublicKey" } this.api(data).then(r => { if (r.success) { let pubkey = JSON.parse(r.pubkey); this.e2ee.importKey(pubkey.x, pubkey.y, json.privkey).then(privkey => { this.keys = { privateKeyJwk: privkey, publicKeyJwk: pubkey }; localStorage.setItem("keys", JSON.stringify(this.keys)); this.ui.openURL("/"); }); } }); } db64(str) { str = (str + '==='.slice((str.length + 3) % 4)).replace(/-/g, '+').replace(/_/g, '/'); return atob(str); } b64(str) { str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); return btoa(str); } syncLink() { let data = { session: this.session, privkey: this.keys.privateKeyJwk.d, settings: this.settings } let json = JSON.stringify(data); let encoded = this.b64(json); let link = "https://"+this.host+"/sync#"+encoded; return link; } regex(pattern, string) { return [...string.matchAll(pattern)]; } rtrim(str, chr) { let rgxtrim = (!chr) ? new RegExp('\\s+$') : new RegExp(chr+'+$'); return str.replace(rgxtrim, ''); } replaceRange(s, start, end, substitute) { let before = s.substr(0, start); let after = s.substr(end, (s.length -end)); return before+substitute+after; } sorted(array, by) { return array.sort((a, b) => a[by].localeCompare(b[by])); } isKey(e, key) { if (this.key(e) == key) { return true } return false; } key(e) { return e.which || e.keyCode; } keyName(e) { return e.key; } setPublicKey() { let data = { action: "setPublicKey", pubkey: JSON.stringify(this.keys.publicKeyJwk) }; return this.api(data); } async startSession() { if (this.session) { return; } let data = { "action": "startSession" }; return this.api(data).then(r => { this.session = r.session; localStorage.setItem("session", r.session); }); } sendDomain() { if (!this.domain) { if (this.domains.length) { this.domain = this.domains[0].id; localStorage.setItem("domain", this.domain); } else { this.ui.openURL("/id"); return; } } this.ws.send(`DOMAIN ${this.domain}`); } message(data) { let message = data.toString(); let parsed = message.match(/(?[A-Z]+)(\s(?.+))?/); this.handle(parsed.groups); } async handle(parsed) { let command = parsed.command; let body = parsed.body; let push = false; if (body) { try { body = JSON.parse(body); for (var k in body) { try { body[k] = JSON.parse(body[k]); } catch {} for (var i in body[k]) { try { body[k][i] = JSON.parse(body[k][i]); } catch {} } } } catch {} } switch (command) { case "SUCCESS": switch (body.type) { case "ADDDOMAIN": case "ADDSLD": this.ui.enableButton(body.type); this.ui.handleSuccess(body); break; case "DELETEDOMAIN": this.ui.removeDomain(body.id); break; case "VERIFYDOMAIN": this.ui.handleSuccess(body); break; case "GETADDRESS": this.ui.paymentResponse(body); break; } break; case "ERROR": switch (body.type) { case "ADDDOMAIN": this.ui.errorResponse(body); break; case "ADDSLD": this.ui.enableButton(body.type); this.ui.errorResponse(body); break; case "MESSAGES": this.ui.setUserList(); this.ui.messagesLoading(false); this.ui.setGatedView(body); this.ui.markEmptyIfNeeded(); this.loadingMessages = false; break; case "DOMAIN": this.domain = null; this.sendDomain(); break; case "PM": if (body.id) { let otherUser = this.otherUserFromPM(body.id); let queuedMessage = this.queuedMessage(otherUser.domain); if (queuedMessage) { this.ui.changeConversation(body.id); this.ui.close(); } } else { this.ui.errorResponse(body); } break; case "GETADDRESS": this.ui.paymentResponse(body); break; } break; case "IDENTIFIED": if (this.page !== "sync") { if (body.seen) { this.seen = body.seen; } this.ws.send("DOMAINS"); } break; case "DOMAINS": this.domains = body; this.ui.domains(this.domains); switch (this.page) { case "chat": this.sendDomain(); break; case "id": case "invite": this.ws.send(`STAKED`); break; } break; case "STAKED": this.staked = body; this.ui.stakedDomains(body); break; case "DOMAIN": this.ui.updateDomainSelect(); this.ws.send("USERS"); break; case "USERS": $.each(body, (k, user) => { body[k].domain = body[k].domain.toString(); }); this.users = body; this.ws.send(`PING`); this.ws.send(`CHANNELS`); this.ws.send(`PMS`); break; case "USER": this.users = this.users.filter(u => { return u.id != body.id; }); body.domain = body.domain.toString(); this.users.push(body); if (body.id !== this.domain) { let pm = this.pmWithUser(body.id); if (pm) { this.makeSecret(pm); } } this.ui.setUserList(); this.ui.updateConversations(); break; case "CHANNELS": this.ui.clear("channels"); this.channels = body; if (this.channels.length) { let sorted = this.channels.sort((a, b) => { if (("activity" in a) && !("activity" in b)) { return 1; } if (("activity" in b) && !("activity" in a)) { return -1; } if (!("activity" in a) && !("activity" in b)) { return 0; } return a.activity - b.activity; }); $.each(sorted.reverse(), (k, channel) => { this.ui.conversation("channels", channel); }); } this.ui.updateConversations(); this.gotChannels = true; this.ws.send(`MENTIONS`); this.ready(true); break; case "CHANNEL": this.channels.push(body); this.ui.conversation("channels", body); this.ui.updateConversations(); break; case "PMS": this.ui.clear("pms"); this.pms = body; if (this.pms.length) { let sorted = this.pms.sort((a, b) => { return b.activity - a.activity; }); $.each(sorted, (k, conversation) => { this.makeSecret(conversation).then((key) => { this.ui.conversation("pms", conversation); if (k == this.pms.length - 1) { this.ui.updateConversations(); this.gotPms = true; this.ready(true); } }); }); } else { this.ui.updateConversations(); this.gotPms = true; this.ready(true); } break; case "PM": this.pms.push(body); this.makeSecret(body).then((key) => { this.ui.conversation("pms", body); let otherUser = this.otherUserFromPM(body.id); let queuedMessage = this.queuedMessage(otherUser.domain); if (queuedMessage) { this.ui.changeConversation(body.id); this.ui.close(); } }); break; case "MESSAGES": this.ui.setUserList(); this.ui.insertMessages(body, true).then(() => { this.ui.messagesLoading(false); if (body.at) { this.ui.scrollToMessage(body.at); } else if (body.after) { this.ui.backInPresent(body); } if (!this.isChannel(this.conversation)) { let otherUser = this.otherUserFromPM(this.conversation); let queuedMessage = this.queuedMessage(otherUser.domain); if (queuedMessage) { this.queued = this.queued.filter(q => { return q.domain !== queuedMessage.domain; }); this.sendMessage(this.conversation, queuedMessage.message); } } }); break; case "MESSAGE": case "NOTICE": delete this.typers[body.user]; let c; if (this.isChannel(body.conversation)) { c = this.channelForID(body.conversation); } else { c = this.pmForID(body.conversation); } c.activity = body.time; this.ui.updateConversations(); this.ui.moveConversationToTop(body.conversation); await this.decryptMessageIfNeeded(body.conversation, body).then(decrypted => { body.message = decrypted[0]; if (body.p_message) { body.p_message = decrypted[1]; } if (typeof body.message == "object") { if (body.message.message) { body.message.message = body.message.message.toString(); } body.message = JSON.stringify(body.message); } if (typeof body.p_message == "object") { if (body.p_message.message) { body.p_message.message = body.p_message.message.toString(); } body.p_message = JSON.stringify(body.p_message); } body.message = body.message.toString(); if (body.p_message) { body.p_message = body.p_message.toString(); } if (!this.ui.inThePast && body.conversation == this.conversation) { let data = { messages: [body] } this.ui.insertMessages(data); this.seen[this.conversation] = this.time(); if (!this.isActive) { if (this.isChannel(body.conversation)) { if (this.mentionsMe(body.message)) { push = "mention"; } else if (this.replyingToMe(body)) { push = "reply"; } } else { push = "pm"; } } } else { if (this.isChannel(body.conversation)) { if (this.mentionsMe(body.message)) { this.ui.markMention(body.conversation, true); push = "mention"; } else if (this.replyingToMe(body)) { push = "reply"; } } else { push = "pm"; } } if (push) { if (body.user !== this.domain) { let name; switch (push) { case "pm": name = this.ui.toUnicode(this.userForID(body.user).domain); break; case "mention": case "reply": name = `${this.ui.toUnicode(this.userForID(body.user).domain)} - #${this.ui.toUnicode(c.name)}`; break; } let subtitle = this.ui.messageSummary(decrypted[0]); subtitle = this.ui.stripHTML(this.ui.replaceIds(subtitle)); this.ui.sendNotification(name, subtitle, body.conversation); } } }); break; case "DELETEMESSAGE": this.ui.deleteMessage(body.id); break; case "REACT": if (body.conversation == this.conversation) { let message = this.messages.filter(m => { return m.id == body.message; })[0]; try { let json = JSON.parse(message.reactions); if (!Object.keys(json).includes(body.reaction)) { json[body.reaction] = []; } if (json[body.reaction].includes(body.user)) { let x = json[body.reaction].indexOf(body.user); delete json[body.reaction].splice(x, 1); if (!Object.keys(json[body.reaction]).length) { delete json[body.reaction]; } } else { json[body.reaction].push(body.user); } message.reactions = JSON.stringify(json); this.ui.updateReactions(body.message); this.seen[this.conversation] = this.time(); } catch {} } break; case "TYPING": this.typers[body.from] = { to: body.to, time: this.time() } break; case "MENTIONS": this.ui.updateMentions(body); this.gotMentions = true; this.ready(true); break; case "PONG": this.updateActiveUsers(body.active); //this.ui.checkVersion(body.version); break; case "PINMESSAGE": this.channelForID(body.conversation).pinned = body.id; if (this.conversation == body.conversation) { this.ui.setPinnedMessage(); } break; case "STARTVIDEO": this.ui.muteAll(); this.channelForID(body.conversation).video = true; this.channelForID(body.conversation).videoUsers = body.users; this.ui.showVideoIfNeeded(); break; case "INVITEVIDEO": this.channelForID(body.conversation).videoSpeakers = body.speakers; this.ui.showVideoIfNeeded(); break; case "JOINVIDEO": if (body.users) { this.channelForID(body.conversation).videoUsers = body.users; } if (body.watchers) { this.channelForID(body.conversation).videoWatchers = body.watchers; } if (body.speakers) { this.channelForID(body.conversation).videoSpeakers = body.speakers; } this.ui.showVideoIfNeeded(); break; case "VIEWVIDEO": if (body.users) { this.channelForID(body.conversation).videoUsers = body.users; } if (body.watchers) { this.channelForID(body.conversation).videoWatchers = body.watchers; if (body.watchers.includes(this.domain)) { this.channelForID(body.conversation).watching = true; } } this.ui.showVideoIfNeeded(); break; case "LEAVEVIDEO": if (body.users) { this.channelForID(body.conversation).videoUsers = body.users; } if (body.watchers) { this.channelForID(body.conversation).videoWatchers = body.watchers; } if (body.speakers) { this.channelForID(body.conversation).videoSpeakers = body.speakers; } this.ui.showVideoIfNeeded(); break; case "ENDVIDEO": this.endVideo(body.conversation); break; case "MUTEVIDEO": case "MUTEAUDIO": let toggle; let user = this.channelForID(body.conversation).videoUsers[body.user]; switch (command) { case "MUTEVIDEO": toggle = "toggleVideo"; user.video = !user.video; break; case "MUTEAUDIO": toggle = "toggleAudio"; user.audio = !user.audio; break; } this.ui.updateVideoUsers(); if (body.user == this.domain) { this.ui.setMute(toggle, -1); } break; case "CONNECTED": if (this.userForID(body)) { this.userForID(body).active = true; this.ui.updateUserList(); this.ui.updateConversations(); } break; case "DISCONNECTED": if (this.userForID(body)) { this.userForID(body).active = false; this.ui.updateUserList(); this.ui.updateConversations(); } break; } } endAllVideo() { try { this.stream.close(); $.each(this.channels, (k, c) => { this.endVideo(c.id); }); } catch {}; } endVideo(conversation) { this.channelForID(conversation).videoUsers = {}; this.channelForID(conversation).videoWatchers = []; this.channelForID(conversation).videoSpeakers = []; this.channelForID(conversation).video = false; this.channelForID(conversation).watching = false; this.ui.showVideoIfNeeded(); } currentVideoUsers() { return this.channelForID(this.conversation).videoUsers; } updateActiveUsers(active) { if (this.users) { let current = this.active; this.active = active; current.forEach(u => { if (!active.includes(u)) { let user = this.userForID(u); user.active = false; } }); active.forEach(u => { let user = this.userForID(u); user.active = true; }); this.ui.updateUserList(); this.ui.updateConversations(); } } otherUserFromPM(id) { let pm = this.pmForID(id); let otherUserID = this.otherUser(pm.users); let otherUser = this.userForID(otherUserID); return otherUser; } queuedMessage(domain) { return this.queued.filter(q => { return q.domain == domain; })[0]; } ready(bool) { if (bool) { if (this.gotChannels && this.gotPms && this.gotMentions) { this.ui.changeMe(this.domain); this.ui.setLoading(false); this.ui.handleHash(); this.changeConversation(this.conversation); this.changedConversation(); } } else { this.gotChannels = false; this.gotPms = false; this.gotMentions = false; this.ui.setLoading(true); } } isChannel(name) { if (name) { if (name.toString().length == 8) { return true; } } return false; } channelForID(id) { return this.channels.filter(c => { return c.id == id; })[0]; } channelForName(name) { return this.channels.filter(c => { return c.name == name; })[0]; } stakedForName(name) { return this.staked.filter(c => { return c.name == name; })[0]; } pmForID(id) { return this.pms.filter(c => { return c.id == id; })[0]; } pmWithUser(id) { return this.pms.filter(c => { return c.users.includes(id); })[0]; } otherUser(users) { return users.filter(u => { return u !== this.domain; })[0]; } userForID(id) { if (this.users) { return this.users.filter(u => { return u.id == id; })[0]; } return false; } userForName(name, active=false) { return this.users.filter(u => { return (u.domain == name || this.ui.toUnicode(u.domain) == name) && !u.locked && !u.deleted; })[0]; } usersForConversation(id) { if (this.channels) { let conversation = this.channels.filter(c => { return c.id == id; })[0]; let users = this.users.filter(u => { return !u.locked; }); if (!conversation.public) { users = this.users.filter(u => { return !u.locked && u.tld == conversation.name || u.id == "n2sTWh5EZGI8xMQr"; }); } return users; } return []; } changeConversation(id) { let current = this.conversation; let change = id; if (!this.pmForID(id) && !this.channelForID(id)) { id = false; } if (!id) { change = this.channels[0].id; } if (current !== change) { this.conversation = change; localStorage.setItem("conversation", this.conversation); this.changedConversation(); } } changedConversation() { this.ui.closeMenusIfNeeded(); this.messages = []; this.ui.clear("input"); this.ui.clear("messages"); this.ui.setInThePast(false); this.ui.messagesLoading(true); this.ui.emptyUserList(); this.ui.searchUsers(false); this.ui.clearSelection(); this.ui.updateTypingView(); this.getMessages(); if (this.isChannel(this.conversation)) { this.tab = "channels"; } else { this.tab = "pms"; } this.ui.setConversationTab(); this.ui.setActiveConversation(); this.ui.markUnread(this.conversation, false); this.ui.markMention(this.conversation, false); this.ui.updateInputBar(); this.ui.showVideoIfNeeded(); this.seen[this.conversation] = this.time(); this.ws.send(`CHANGEDCONVERSATION ${this.conversation}`); } getMessage(id) { let data = { action: "getMessage", domain: this.domain, id: id }; return this.api(data); } getMessages(options={}) { if (this.loadingMessages) { return; } this.loadingMessages = true; let data = { conversation: this.conversation, }; let merged = {...data,...options}; this.ws.send(`MESSAGES ${JSON.stringify(merged)}`); } async makeSecret(pm) { let output = new Promise(resolve => { let otherUser = this.otherUserFromPM(pm.id); let otherKey = otherUser.pubkey; this.e2ee.deriveKey(otherKey, this.keys.privateKeyJwk).then(key => { otherUser.sharedkey = key; resolve(key); }); }); return await output; } async decryptedBody(conversation, message) { let output = new Promise(resolve => { let pm = this.pmForID(conversation); let user = this.userForID(this.otherUser(pm.users)); this.e2ee.decryptMessage(message, user.sharedkey, conversation).then(decrypted => { resolve(decrypted.trim()); }); }); return await output; } async decryptMessageIfNeeded(conversation, message) { let body = new Promise(resolve => { if (this.isChannel(conversation)) { resolve(message.message); } else { this.decryptedBody(conversation, message.message).then(decrypted => { resolve(decrypted); }); } }); let replyBody = new Promise(resolve => { if (message.p_message) { if (this.isChannel(conversation)) { resolve(message.p_message); } else { this.decryptedBody(conversation, message.p_message).then(decrypted => { resolve(decrypted); }); } } else { resolve(); } }); let prepared = await Promise.all([body, replyBody]); return prepared; } async encryptedBody(conversation, message) { let output = new Promise(resolve => { let pm = this.pmForID(conversation); let user = this.userForID(this.otherUser(pm.users)); this.e2ee.encryptMessage(message, user.sharedkey, conversation).then(encrypted => { resolve(encrypted.trim()); }); }); return await output; } async encryptMessageIfNeeded(conversation, message) { let output = new Promise(resolve => { if (this.isChannel(conversation)) { resolve(message); } else { this.encryptedBody(conversation, message).then(encrypted => { resolve(encrypted); }); } }); return await output; } changeDomain(domain) { this.domain = domain; localStorage.setItem("domain", domain); if (this.page == "chat") { this.ready(false); this.ws.send(`DOMAIN ${domain}`); } } usersInMessage(message) { let matches = this.regex(/\@(?[^ \x00]+?)\//gm, message); return matches; } channelsInMessage(message) { let matches = this.regex(/\#(?[^ \x00]+?)(?:\s|$)/gm, message); return matches; } replaceCompletions(text) { let output = text; while (this.usersInMessage(output).length) { let users = this.usersInMessage(output); let result = users[0]; let name = result.groups.name; let start = result.index; let end = (start + name.length + 1 + 1); let match = this.users.filter(u => { return this.ui.toUnicode(u.domain) == name && !u.locked; }); let replace; if (match.length) { let id = match[0].id; replace = `@${id}`; } else { replace = `@\x00${name}/`; } output = this.replaceRange(output, start, end, replace); } while (this.channelsInMessage(output).length) { let channels = this.channelsInMessage(output); let result = channels[0]; let name = result.groups.name; let start = result.index; let end = (start + name.length + 1); let match = this.channels.filter(c => { return this.ui.toUnicode(c.name) == name; }); let replace; if (match.length) { let id = match[0].id; replace = `@${id}`; } else { replace = `#\x00${name}`; } output = this.replaceRange(output, start, end, replace); } return output; } sendTyping() { let input = $("textarea#message").val(); if (!this.typing || !input || (input && input[0] == "/")) { return; } let send = true; if (this.typingSent) { let diff = this.time() - this.typingSent; if (diff < this.typingSendDelay) { send = false; } } if (send) { this.typingSent = this.time(); let data = { from: this.domain, to: this.conversation } this.ws.send(`TYPING ${JSON.stringify(data)}`); } } updateTypingStatus() { if (this.lastTyped) { if ((this.time() - this.lastTyped) > this.typingDelay) { this.typing = false; this.lastTyped = false; } else if ($("textarea#message").is(":focus") && $("textarea#message").val() !== "") { this.typing = true; } } } sendMessage(conversation, message) { if (!message.trim().length) { return; } this.encryptMessageIfNeeded(conversation, message).then(encrypted => { let data = { conversation: conversation, message: encrypted } if (this.replying) { data.replying = this.replying.message; } this.typing = false; this.ws.send(`MESSAGE ${JSON.stringify(data)}`); this.replying = null; this.ui.updateReplying(); }); } async sendPayment(address, amount) { const wallet = await bob3.connect(); if (!amount) { return { message: "Please enter an amount." }; } try { const send = await wallet.send(address, amount); return send; } catch (error) { return error; } } async upload(data, attachment) { let output = new Promise(resolve => { $.ajax({ url: "https://hns.chat/upload", type: "POST", data: data, cache: false, contentType: false, processData: false, beforeSend: (e) => {}, xhr: () => { let p = $.ajaxSettings.xhr(); p.upload.onprogress = () => {} return p; }, success: (response) => { let json = JSON.parse(response); resolve(json); } }); }); return await output; } deleteAttachment(id) { let data = { action: "deleteAttachment", id: id }; return this.api(data); } mentionsMe(message) { if (message.includes(`@${this.domain}`)) { return true; } return false; } replyingToMe(message) { if (message.p_user && message.p_user == this.domain) { return true; } return false; } } new HNSChat();