tildefriends/packages/cory/chat/chat.js

487 lines
14 KiB
JavaScript
Raw Normal View History

"use strict";
var gFocus = true;
var gUnread = 0;
var gPresence = {};
let gSessions = {};
let gState = {};
let gCurrentConversation;
function updateTitle() {
terminal.setTitle((gUnread ? "(" + gUnread.toString() + ") " : "") + "Chat");
}
let kMaxHistory = 32;
let kAccountsKey = JSON.stringify(["accounts", core.user.name]);
let kStateKey = JSON.stringify(["state", core.user.name]);
function runCommand(data) {
if (data.action == "addAccount") {
addAccount();
} else if (data.action == "deleteAccount") {
deleteAccount(data.id);
} else if (data.action == "updateAccount") {
delete data.action;
let id = data.id;
delete data.id;
configureAccount(id, data);
} else if (data.action == "connect") {
connect(data.id);
} else if (data.action == "disconnect") {
disconnect(data.id);
} else if (data.action == "window") {
setWindow(data.account, data.conversation);
}
}
function setWindow(accountId, conversation) {
gState.window = {account: accountId, conversation: conversation};
database.set(kStateKey, JSON.stringify(gState));
gCurrentConversation = gSessions[accountId].conversations[conversation];
updateConversation();
updateWindows();
}
function cycleConversation(delta) {
let allConversations = [];
let index = -1;
for (let i in gSessions) {
for (let j in gSessions[i].conversations) {
if (gCurrentConversation == gSessions[i].conversations[j]) {
index = allConversations.length;
}
allConversations.push([i, j]);
}
}
index += delta;
while (index < 0) {
index += allConversations.length;
}
while (index >= allConversations.length) {
index -= allConversations.length;
}
setWindow(allConversations[index][0], allConversations[index][1]);
}
function addAccount() {
return database.get(kAccountsKey).then(function(data) {
let accounts = data ? JSON.parse(data) : [];
let id = 0;
for (var i in accounts) {
id = Math.max(id, accounts[i].id + 1);
}
accounts.push({name: "unnamed", id: id});
return database.set(kAccountsKey, JSON.stringify(accounts));
}).then(updateWindows);
}
core.register("submit", function(data) {
if (data.value.submit == "Save Account") {
let id = data.value.id;
delete data.value.id;
delete data.value.submit;
configureAccount(id, data.value);
}
});
function configureAccount(id, updates) {
return Promise.all([database.get(kAccountsKey), core.getPackages()]).then(function(results) {
let accounts = results[0] ? JSON.parse(results[0]) : [];
let packages = results[1];
let account;
let accountIndex;
for (let i in accounts) {
if (accounts[i].id == id) {
account = accounts[i];
accountIndex = i;
}
}
let promises = [];
if (updates) {
for (let i in updates) {
account[i] = updates[i];
}
promises.push(database.set(kAccountsKey, JSON.stringify(accounts)));
}
return Promise.all(promises).then(function() {
terminal.clear();
terminal.print(JSON.stringify(account));
terminal.print({input: "hidden", value: id, name: "id"});
terminal.print({input: "text", value: account.name, name: "name"});
terminal.print({command: "/command " + JSON.stringify({action: "deleteAccount", id: id}), value: "delete account"});
terminal.print({command: "/command " + JSON.stringify({action: "connect", id: id}), value: "connect"});
terminal.print({command: "/command " + JSON.stringify({action: "disconnect", id: id}), value: "disconnect"});
if (!account.type) {
terminal.print("Pick account type:");
for (let i in packages) {
let app = packages[i];
if (app.manifest && app.manifest.chat) {
terminal.print({command: "/command " + JSON.stringify({action: "updateAccount", id: id, type: app.name}), value: app.name});
}
}
} else {
let schema;
for (let i in packages) {
let app = packages[i];
if (app.name == account.type && app.manifest && app.manifest.chat) {
schema = app.manifest.chat.settings;
break;
}
}
if (schema) {
for (var i in schema) {
let field = schema[i];
terminal.print({style: "font-weight: bold", value: field.name + ": "}, {input: field.type, name: field.name, value: account[field.name] || field.default});
}
}
}
terminal.print({input: "submit", value: "Save Account", name: "submit"});
}).then(updateWindows);
}).catch(function(error) {
print("whoops", error);
});
}
function deleteAccount(id) {
return database.get(kAccountsKey).then(function(data) {
let accounts = data ? JSON.parse(data) : [];
for (var i = 0; i < accounts.length; i++) {
if (accounts[i] && (!accounts[i].id || accounts[i].id == id)) {
accounts.splice(i, 1);
break;
}
}
return database.set(kAccountsKey, JSON.stringify(accounts));
}).then(terminal.clear).then(updateWindows);
}
function connect(id) {
return database.get(kAccountsKey).then(function(data) {
let accounts = data ? JSON.parse(data) : [];
let account;
for (var i = 0; i < accounts.length; i++) {
if (accounts[i] && (!accounts[i].id || accounts[i].id == id)) {
account = accounts[i];
break;
}
}
if (account) {
let self = {account: account};
let options = {callback: chatCallback.bind(self)};
for (var i in account) {
options[i] = account[i];
}
return core.getService("chat", account.type).then(function(service) {
return service.postMessage(options).then(function(sessions) {
let session = sessions[0];
self.session = session;
gSessions[id] = session;
session.conversations = {};
session.account = account;
getConversation(session, null);
session.getConversations().then(function(conversations) {
for (let j in conversations) {
getConversation(session, conversations[j]);
}
});
});
});
}
}).catch(function(error) {
print(error);
});
}
function disconnect(id) {
gSessions[id].disconnect();
}
function updateWindows() {
database.get(kAccountsKey).then(function(data) {
let accounts = data ? JSON.parse(data) : [];
terminal.cork();
terminal.select("windows");
terminal.clear();
terminal.print({style: "font-size: x-large", value: "Windows"});
for (let i in accounts) {
let account = accounts[i];
terminal.print({style: "font-size: large", command: "/command " + JSON.stringify({action: "updateAccount", id: account.id}), value: account.name});
if (gSessions[account.id] && gSessions[account.id].conversations) {
let conversations = gSessions[account.id].conversations;
for (let j in conversations) {
terminal.print({
command: "/command " + JSON.stringify({action: "window", account: account.id, conversation: j}),
value: j ? j : "<service>",
style: (conversations[j] == gCurrentConversation ? "font-weight: bold; " : "") + "color: white",
});
}
}
}
terminal.print({style: "color: yellow", command: "/command " + JSON.stringify({action: "addAccount"}), value: "add account"});
terminal.select("terminal");
terminal.uncork();
}).catch(function(error) {
print(error);
});
}
function updateConversation() {
if (gCurrentConversation) {
terminal.cork();
terminal.clear();
terminal.print("...");
terminal.uncork();
Promise.all([
gCurrentConversation.session.getHistory(gCurrentConversation.name),
gCurrentConversation.session.getParticipants(gCurrentConversation.name),
]).then(function(data) {
let [history, participants] = data;
gCurrentConversation.participants = participants || [];
try {
terminal.cork();
terminal.select("terminal");
terminal.clear();
printToConversation(gCurrentConversation, ["[", gCurrentConversation.name, "]"]);
if (history) {
let previous = Promise.resolve();
for (let message of history) {
previous = previous.then(x => printToConversation(gCurrentConversation, message));
}
}
updateUsers();
} finally {
terminal.uncork();
}
}).catch(function(error) {
terminal.print(error);
});
}
}
function updateUsers() {
terminal.cork();
terminal.select("users");
terminal.clear();
terminal.print({style: "font-size: x-large", value: "Users"});
if (gCurrentConversation) {
gCurrentConversation.participants.sort();
for (var i in gCurrentConversation.participants) {
terminal.print(gCurrentConversation.participants[i]);
}
}
terminal.select("terminal");
terminal.uncork();
}
terminal.cork();
terminal.split([
{type: "horizontal", children: [
{name: "windows", basis: "2in", grow: "0", shrink: "0"},
{name: "terminal", grow: "1"},
{name: "users", basis: "2in", grow: "0", shrink: "0"},
]},
]);
updateTitle();
updateWindows();
updateUsers();
terminal.setEcho(false);
terminal.select("terminal");
terminal.print("~Friends Chat");
terminal.uncork();
function getConversation(session, conversationName) {
let result;
let key = conversationName || "";
if (!session.conversations) {
session.conversations = {};
}
if (!session.conversations[key]) {
session.conversations[key] = {
session: session,
name: key,
messages: [],
sendMessage: function(message) {
return session.sendMessage(key, message);
},
participants: [],
};
updateWindows();
}
result = session.conversations[key];
if (result) {
if (!gCurrentConversation) {
if (!gState.window) {
setWindow(session.account.id, key);
} else if (gState.window.account = session.account.id && gState.window.conversation == key) {
setWindow(session.account.id, key);
}
}
}
return result;
}
async function printToConversation(conversation, message, notify) {
if (conversation == gCurrentConversation) {
if (message.action == "message") {
await printMessage(message.message);
} else if (message.action == "presence") {
if (message.presence == "unavailable") {
terminal.print(new Date().toString(), ": ", message.user, " has left the room.");
} else {
terminal.print(new Date().toString(), ": ", message.user, " has joined the room.");
}
} else {
terminal.print(message);
}
}
if (notify && !gFocus) {
gUnread++;
updateTitle();
}
}
function chatCallback(event) {
try {
if (event.action == "message") {
let conversation = getConversation(this.session, event.conversation);
printToConversation(conversation, event);
} else if (event.action == "presence") {
let conversation = getConversation(this.session, event.conversation);
let index = conversation.participants.indexOf(event.user);
if (event.presence == "unavailable") {
if (index != -1) {
conversation.participants.splice(index, 1);
if (conversation == gCurrentConversation) {
updateUsers();
}
printToConversation(conversation, [new Date().toString(), ": ", event.user + " has left the room."]);
}
} else {
if (index == -1) {
conversation.participants.push(event.user);
if (conversation == gCurrentConversation) {
updateUsers();
}
printToConversation(conversation, [new Date().toString(), ": ", event.user + " has joined the room."]);
}
}
} else {
let conversation = getConversation(this.session, event.conversation);
printToConversation(conversation, ["Unhandled event: ", JSON.stringify(event)]);
}
} catch (error) {
terminal.print("chatCallback: ", error);
}
};
core.register("onInput", function(input) {
if (input.substring(0, "/command ".length) == "/command ") {
runCommand(JSON.parse(input.substring("/command ".length)));
} else if (gCurrentConversation) {
gCurrentConversation.sendMessage(input).catch(function(error) {
terminal.print("Message not sent: ", error);
});
}
});
function niceTime(lastTime, thisTime) {
if (!lastTime) {
return thisTime;
}
let result = [];
let lastParts = lastTime.split(" ");
let thisParts = thisTime.split(" ");
for (let i = 0; i < thisParts.length; i++) {
if (thisParts[i] !== lastParts[i]) {
result.push(thisParts[i]);
}
}
return result.join(" ");
}
async function formatMessage(message) {
var result;
if (typeof message == "string") {
for (let i = 0; i < message.length; i++) {
if (message.charCodeAt(i) >= 128 /*&& message.charCodeAt(i) < 256*/) {
message = message.substring(0, i) + "?" + message.substring(i + 1);
}
}
result = [];
var regex = /(\w+:\/*\S+?)(?=(?:[\.!?])?(?:$|\s))/gi;
var match;
var lastIndex = 0;
let libunfurl;
while ((match = regex.exec(message)) !== null) {
if (!libunfurl) {
libunfurl = await core.getService("libunfurl", "libunfurl");
}
result.push({class: "base1", value: message.substring(lastIndex, match.index)});
if (!libunfurl) {
result.push({href: match[0]});
} else {
result.push(await libunfurl.postMessage(match[0]));
}
lastIndex = regex.lastIndex;
}
result.push({class: "base1", value: message.substring(lastIndex)});
} else {
result = message;
}
return result;
}
var lastTimestamp = null;
async function printMessage(message) {
var now = message.timestamp || new Date().toString();
var from = message.from || "unknown";
terminal.print(
{class: "base0", value: niceTime(lastTimestamp, now)},
" ",
{class: "base00", value: "<"},
{class: "base3", value: from},
{class: "base00", value: ">"},
" ",
await formatMessage(message.message));
lastTimestamp = now;
}
core.register("focus", function() {
gFocus = true;
gUnread = 0;
updateTitle();
});
core.register("blur", function() {
gFocus = false;
});
core.register("key", function(event) {
if (event.type == "keydown") {
if (event.altKey) {
if (event.character == "I") {
cycleConversation(-1);
} else if (event.character == "K") {
cycleConversation(1);
}
}
}
});
terminal.setSendKeyEvents(true);
// Connect all accounts on start.
Promise.all([database.get(kAccountsKey), database.get(kStateKey)]).then(function(results) {
let accounts = results[0] ? JSON.parse(results[0]) : [];
gState = results[1] ? JSON.parse(results[1]) : gState;
for (let i in accounts) {
connect(accounts[i].id);
}
});