A detachable multi-protocol chat client is starting to come together.

git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3232 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
Cory McWilliams 2016-05-15 13:23:38 +00:00
parent 4fe5e9e39e
commit 09a283fdbc
4 changed files with 377 additions and 161 deletions

View File

@ -4,6 +4,7 @@ var gFocus = true;
var gUnread = 0; var gUnread = 0;
var gPresence = {}; var gPresence = {};
let gSessions = {}; let gSessions = {};
let gState = {};
let gCurrentConversation; let gCurrentConversation;
function updateTitle() { function updateTitle() {
@ -11,6 +12,7 @@ function updateTitle() {
} }
let kAccountsKey = JSON.stringify(["accounts", core.user.name]); let kAccountsKey = JSON.stringify(["accounts", core.user.name]);
let kStateKey = JSON.stringify(["state", core.user.name]);
function runCommand(data) { function runCommand(data) {
if (data.action == "addAccount") { if (data.action == "addAccount") {
@ -27,11 +29,17 @@ function runCommand(data) {
} else if (data.action == "disconnect") { } else if (data.action == "disconnect") {
disconnect(data.id); disconnect(data.id);
} else if (data.action == "window") { } else if (data.action == "window") {
gCurrentConversation = gSessions[data.account].conversations[data.conversation]; 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(); updateConversation();
updateWindows(); updateWindows();
} }
}
function addAccount() { function addAccount() {
return database.get(kAccountsKey).then(function(data) { return database.get(kAccountsKey).then(function(data) {
@ -152,10 +160,11 @@ function connect(id) {
self.session = session; self.session = session;
gSessions[id] = session; gSessions[id] = session;
session.conversations = {}; session.conversations = {};
getConversation(session, {}); session.account = account;
getConversation(session, null);
session.getConversations().then(function(conversations) { session.getConversations().then(function(conversations) {
for (let j in conversations) { for (let j in conversations) {
getConversation(session, {conversation: conversations[j]}); getConversation(session, conversations[j]);
} }
}); });
}); });
@ -208,13 +217,18 @@ function updateConversation() {
]).then(function(data) { ]).then(function(data) {
let history = data[0]; let history = data[0];
let participants = data[1]; let participants = data[1];
gCurrentConversation.messages = history; gCurrentConversation.messages = history || [];
gCurrentConversation.participants = participants; gCurrentConversation.participants = participants || [];
terminal.cork(); terminal.cork();
terminal.select("terminal"); terminal.select("terminal");
terminal.clear(); terminal.clear();
for (var i in gCurrentConversation.messages) { for (var i in gCurrentConversation.messages) {
printMessage(gCurrentConversation.messages[i]); let message = gCurrentConversation.messages[i];
if (message.action == "message") {
printMessage(message.message);
} else {
terminal.print(message);
}
} }
updateUsers(); updateUsers();
terminal.uncork(); terminal.uncork();
@ -255,11 +269,12 @@ terminal.select("terminal");
terminal.print("~Friends Chat"); terminal.print("~Friends Chat");
terminal.uncork(); terminal.uncork();
function getConversation(session, message) { function getConversation(session, conversationName) {
let result; let result;
for (var i in gSessions) { let key = conversationName || "";
if (session == gSessions[i]) { if (!session.conversations) {
let key = message.conversation || message.from || ""; session.conversations = {};
}
if (!session.conversations[key]) { if (!session.conversations[key]) {
session.conversations[key] = { session.conversations[key] = {
session: session, session: session,
@ -272,48 +287,63 @@ function getConversation(session, message) {
updateWindows(); updateWindows();
} }
result = session.conversations[key]; result = session.conversations[key];
break; 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);
} }
} }
if (result && !gCurrentConversation) {
gCurrentConversation = result;
} }
return result; return result;
} }
function printToConversation(conversation, message, notify) {
if (conversation == gCurrentConversation) {
if (message.action == "message") {
printMessage(message.message);
} else {
terminal.print(message);
}
}
if (conversation) {
conversation.messages.push(message);
}
if (notify && !gFocus) {
gUnread++;
updateTitle();
}
}
function chatCallback(event) { function chatCallback(event) {
try { try {
if (event.action == "message") { if (event.action == "message") {
let conversation = getConversation(this.session, event); let conversation = getConversation(this.session, event.conversation);
if (conversation == gCurrentConversation) { printToConversation(conversation, event);
printMessage(event);
}
conversation.messages.push(event);
if (!gFocus) {
gUnread++;
updateTitle();
}
} else if (event.action == "presence") { } else if (event.action == "presence") {
let conversation = event.jid.split('/', 2)[0]; let conversation = getConversation(this.session, event.conversation);
if (gCurrentConversation.name == conversation) { let index = conversation.participants.indexOf(event.user);
let index = gCurrentConversation.participants.indexOf(event.name); if (event.presence == "unavailable") {
if (event.type == "unavailable") {
if (index != -1) { if (index != -1) {
gCurrentConversation.participants.splice(index, 1); conversation.participants.splice(index, 1);
if (conversation == gCurrentConversation) {
updateUsers(); updateUsers();
terminal.print(new Date().toString(), ": ", event.name + " has left the room."); }
printToConversation(conversation, [new Date().toString(), ": ", event.user + " has left the room."]);
} }
} else { } else {
if (index == -1) { if (index == -1) {
gCurrentConversation.participants.push(event.name); conversation.participants.push(event.user);
if (conversation == gCurrentConversation) {
updateUsers(); updateUsers();
terminal.print(new Date().toString(), ": ", event.name + " has joined the room.");
} }
printToConversation(conversation, [new Date().toString(), ": ", event.user + " has joined the room."]);
} }
} }
} else { } else {
terminal.print("Unhandled event: ", JSON.stringify(event)); let conversation = getConversation(this.session, event.conversation);
printToConversation(conversation, ["Unhandled event: ", JSON.stringify(event)]);
} }
} catch (error) { } catch (error) {
terminal.print("chatCallback: ", error); terminal.print("chatCallback: ", error);
@ -391,8 +421,9 @@ core.register("blur", function() {
}); });
// Connect all accounts on start. // Connect all accounts on start.
Promise.all([database.get(kAccountsKey), core.getPackages()]).then(function(results) { Promise.all([database.get(kAccountsKey), database.get(kStateKey)]).then(function(results) {
let accounts = results[0] ? JSON.parse(results[0]) : []; let accounts = results[0] ? JSON.parse(results[0]) : [];
gState = results[1] ? JSON.parse(results[1]) : gState;
for (let i in accounts) { for (let i in accounts) {
connect(accounts[i].id); connect(accounts[i].id);
} }

140
packages/cory/irc/irc.js Normal file
View File

@ -0,0 +1,140 @@
"use strict";
//! {
//! "permissions": [
//! "network"
//! ],
//! "chat": {
//! "version": 1,
//! "settings": [
//! {"name": "user", "type": "text"},
//! {"name": "realName", "type": "text"},
//! {"name": "password", "type": "password"},
//! {"name": "nick", "type": "text"},
//! {"name": "server", "type": "text"},
//! {"name": "port", "type": "text"},
//! {"name": "autoJoinChannels", "type": "text"}
//! ]
//! },
//! "require": [
//! "libchat"
//! ]
//! }
let ChatService = require("libchat").ChatService;
class IrcService {
constructor(options) {
let self = this;
self._service = new ChatService(options.callback);
self._name = options.name;
self._nick = options.nick;
network.newConnection().then(function(socket) {
self._socket = socket;
return self._connect(options);
});
}
_send(line) {
return this._socket.write(line + "\r\n");
}
_receivedLine(originalLine) {
try {
let line = originalLine;
let prefix;
if (line.charAt(0) == ":") {
let space = line.indexOf(" ");
prefix = line.substring(1, space);
line = line.substring(space + 1);
}
let lineNoPrefix = line;
let remainder;
let colon = line.indexOf(" :");
if (colon != -1) {
remainder = line.substring(colon + 2);
line = line.substring(0, colon);
}
let parts = line.split(" ");
if (remainder) {
parts.push(remainder);
}
let conversation = "";
if (parts[0] == "PRIVMSG" || parts[0] == "NOTICE") {
// Is it a channel type?
if ("&#!+.".indexOf(parts[1].charAt(0)) != -1) {
conversation = parts[1];
} else {
conversation = prefix.split('!')[0];
}
this._service.notifyMessageReceived(conversation, {
from: prefix.split('!')[0],
message: parts[parts.length - 1],
type: parts[0],
});
} else if (parts[0] == "PING") {
parts[0] = "PONG";
this._send(parts.join(" "));
} else if (parts[0] == "JOIN") {
let person = prefix.split('!')[0];
let conversation = parts[1];
this._service.notifyPresenceChanged(conversation, person, "present");
} else if (parts[0] == "JOIN") {
let person = prefix.split('!')[0];
let conversation = parts[1];
this._service.notifyPresenceChanged(conversation, person, "unavailable");
} else {
this._service.notifyMessageReceived("", {from: prefix, message: lineNoPrefix});
}
} catch (error) {
this._service.reportError(error);
}
}
_connect(options) {
let self = this;
let readBuffer = "";
self._socket.read(function(data) {
if (data) {
readBuffer += data;
let end = readBuffer.indexOf("\n");
while (end != -1) {
let line = readBuffer.substring(0, end);
if (line.charAt(line.length - 1) == "\r") {
line = line.substring(0, line.length - 1);
}
readBuffer = readBuffer.substring(end + 1);
self._receivedLine(line);
end = readBuffer.indexOf("\n");
}
} else {
self._service.notifyStateChanged("disconnected");
}
});
return self._socket.connect(options.server, options.port).then(function() {
self._service.notifyStateChanged("connected");
self._send("USER " + options.user + " 0 * :" + options.realName);
self._send("NICK " + options.nick);
}).catch(self._service.reportError);
}
sendMessage(target, text) {
if (!target) {
this._socket.write(text + "\r\n");
} else {
this._socket.write("PRIVMSG " + target + " :" + text + "\r\n");
}
this._service.notifyMessageReceived(target || "", {from: self._nick, message: text, timestamp: new Date().toString()});
}
disconnect() {
this._send("QUIT");
this._socket.close();
this._service.notifyStateChanged("disconnected");
}
};
ChatService.handleMessages(IrcService);

View File

@ -0,0 +1,126 @@
"use strict";
exports.ChatService = class {
static handleMessages(serviceClass) {
let self = this;
let sessions = {};
core.register("onMessage", function(sender, options) {
let service = sessions[options.name];
if (!service) {
service = new serviceClass(options);
sessions[options.name] = service;
} else {
service._service.addCallback(options.callback);
}
return service._service.makeInterface(service);
});
}
constructor(callback) {
this._callbacks = [callback];
this._conversations = {};
this._state = null;
}
makeInterface(service) {
let self = this;
return {
sendMessage: service.sendMessage.bind(service),
disconnect: service.disconnect.bind(service),
getConversations: self.getConversations.bind(self),
getHistory: self.getHistory.bind(self),
getParticipants: self.getParticipants.bind(self),
};
}
addCallback(callback) {
if (this._callbacks.indexOf(callback) == -1) {
this._callbacks.push(callback);
}
}
_invokeCallback(message) {
let self = this;
for (let i = self._callbacks.length - 1; i >= 0; i--) {
let callback = self._callbacks[i];
try {
callback(message);
} catch (error) {
self._callbacks.splice(i, 1);
// XXX: Send it to the other connections?
print(error);
}
}
}
_getConversation(conversation) {
if (!this._conversations[conversation]) {
this._conversations[conversation] = {history: [], participants: []};
}
return this._conversations[conversation];
}
notifyMessageReceived(conversation, message) {
let fullMessage = {action: "message", conversation: conversation || "", message: message};
this._getConversation(conversation || "").history.push(fullMessage);
this._invokeCallback(fullMessage);
}
notifyPresenceChanged(conversation, user, state) {
let leaving = state == "unavailable";
let participants = this._getConversation(conversation).participants;
let index = participants.indexOf(user);
if (leaving) {
participants.splice(index, 1);
} else if (index == -1) {
participants.push(user);
}
this._invokeCallback({
action: "presence",
conversation: conversation,
user: user,
presence: state,
});
}
notifyStateChanged(state) {
this._state = state;
this._invokeCallback({action: state});
}
reportError(error) {
this._invokeCallback({
action: "error",
error: error,
}).catch(function(error) {
print(error);
});
}
isConversation(conversation) {
return this._conversations[conversation] != null;
}
getConversations() {
return Object.keys(this._conversations);
}
getHistory(conversation) {
let result;
if (this._conversations[conversation]) {
result = this._conversations[conversation].history;
}
return result;
}
getParticipants(conversation) {
let result;
if (this._conversations[conversation]) {
result = this._conversations[conversation].participants;
}
return result;
}
}

View File

@ -12,7 +12,10 @@
//! {"name": "resource", "type": "text", "default": "tildefriends"}, //! {"name": "resource", "type": "text", "default": "tildefriends"},
//! {"name": "server", "type": "text"} //! {"name": "server", "type": "text"}
//! ] //! ]
//! } //! },
//! "require": [
//! "libchat"
//! ]
//! } //! }
// md5.js // md5.js
@ -678,57 +681,23 @@ XmlStanzaParser.prototype.parseNode = function(node) {
// end xmpp.js // end xmpp.js
let ChatService = require("libchat").ChatService;
var gPingCount = 0; var gPingCount = 0;
class XmppService { class XmppService {
constructor(options) { constructor(options) {
let self = this; let self = this;
self._callbacks = [options.callback]; self._service = new ChatService(options.callback);
self._conversations = {};
network.newConnection().then(function(socket) { network.newConnection().then(function(socket) {
self._socket = socket; self._socket = socket;
return self._connect(options); return self._connect(options);
}).catch(self._reportError); }).catch(self._service.reportError);
} }
sendMessage(to, message) { sendMessage(to, message) {
this._socket.write("<message type='groupchat' to='" + xmlEncode(to) + "'><body>" + xmlEncode(message) + "</body></message>"); this._socket.write("<message type='groupchat' to='" + xmlEncode(to) + "'><body>" + xmlEncode(message) + "</body></message>").catch(this._service.reportError);
}
getConversations() {
return Object.keys(this._conversations);
}
getParticipants(conversation) {
let result;
if (this._conversations[conversation]) {
result = this._conversations[conversation].participants;
}
return result;
}
getHistory(conversation) {
let result;
if (this._conversations[conversation]) {
result = this._conversations[conversation].history;
}
return result;
}
invokeCallback(message) {
let self = this;
for (let i = self._callbacks.length - 1; i >= 0; i--) {
let callback = self._callbacks[i];
try {
callback(message);
} catch (error) {
self._callbacks.splice(i, 1);
// XXX: Send it to the other connections?
print(error);
}
}
} }
_connect(options) { _connect(options) {
@ -755,9 +724,7 @@ class XmppService {
let password = options.password; let password = options.password;
let server = options.server; let server = options.server;
self._socket.connect("jabber.troubleimpact.com", 5222).then(function() { self._socket.connect("jabber.troubleimpact.com", 5222).then(function() {
print("actually connected"); self._service.notifyStateChanged("connected");
self.invokeCallback({action: "connected"});
print("wtf");
var parse = new XmlStanzaParser(1); var parse = new XmlStanzaParser(1);
self._socket.write("<?xml version='1.0'?>"); self._socket.write("<?xml version='1.0'?>");
self._socket.write("<stream:stream to='" + xmlEncode(server) + "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>"); self._socket.write("<stream:stream to='" + xmlEncode(server) + "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");
@ -768,7 +735,7 @@ class XmppService {
self._socket.read(function(data) { self._socket.read(function(data) {
try { try {
if (!data) { if (!data) {
self.invokeCallback({action: "disconnected"}); self._service.notifyStateChanged("disconnected");
return; return;
} }
parse.parse(data).forEach(function(stanza) { parse.parse(data).forEach(function(stanza) {
@ -800,19 +767,26 @@ class XmppService {
} else if (stanza.attributes.id == "session0") { } else if (stanza.attributes.id == "session0") {
self._socket.write("<presence to='chadhappyfuntime@conference.jabber.troubleimpact.com/" + userName + "'><priority>1</priority><x xmlns='http://jabber.org/protocol/muc'/></presence>"); self._socket.write("<presence to='chadhappyfuntime@conference.jabber.troubleimpact.com/" + userName + "'><priority>1</priority><x xmlns='http://jabber.org/protocol/muc'/></presence>");
self._schedulePing(); self._schedulePing();
self._conversations["chadhappyfuntime@conference.jabber.troubleimpact.com"] = {participants: [], history: []}; //self._conversations["chadhappyfuntime@conference.jabber.troubleimpact.com"] = {participants: [], history: []};
} else if (stanza.children.length && stanza.children[0].name == "ping") { } else if (stanza.children.length && stanza.children[0].name == "ping") {
// Ping response. // Ping response.
} else { } else {
self.invokeCallback({ self._service.notifyMessageReceived(null, {unknown: stanza});
action: "unknown",
stanza: stanza,
});
} }
} else if (stanza.name == "message") { } else if (stanza.name == "message") {
let message = self._convertMessage(stanza); let message = self._convertMessage(stanza);
self._conversations[message.conversation].history.push(message); let conversation = stanza.attributes.from;
self.invokeCallback(message); if (conversation && conversation.indexOf('/') != -1) {
conversation = conversation.split("/")[1];
}
if (stanza.attributes.type == "groupchat") {
if (self._service.isConversation(stanza.attributes.to.split("/")[0])) {
conversation = stanza.attributes.to.split("/")[0];
} else if (self._service.isConversation(stanza.attributes.from.split("/")[0])) {
conversation = stanza.attributes.from.split("/")[0];
}
}
self._service.notifyMessageReceived(conversation, message);
} else if (stanza.name == "challenge") { } else if (stanza.name == "challenge") {
var challenge = Base64.decode(stanza.text); var challenge = Base64.decode(stanza.text);
var parts = challenge.split(','); var parts = challenge.split(',');
@ -849,33 +823,16 @@ class XmppService {
} else if (stanza.name == "presence") { } else if (stanza.name == "presence") {
let name = stanza.attributes.from.split('/', 2)[1]; let name = stanza.attributes.from.split('/', 2)[1];
let conversation = stanza.attributes.from.split('/', 2)[0]; let conversation = stanza.attributes.from.split('/', 2)[0];
let leaving = stanza.attributes.type == "unavailable"; self._service.notifyPresenceChanged(conversation, name, stanza.attributes.type);
let index = self._conversations[conversation].participants.indexOf(name);
if (leaving) {
self._conversations[conversation].participants.splice(index, 1);
} else { } else {
if (index == -1) { self._service.notifyMessageReceived(null, {unknown: stanza});
self._conversations[conversation].participants.push(name);
}
}
self.invokeCallback({
action: "presence",
name: name,
jid: stanza.attributes.from,
type: stanza.attributes.type,
});
} else {
self.invokeCallback({
action: "unknown",
stanza: stanza,
});
} }
}); });
} catch (error) { } catch (error) {
self._reportError(error); self._service.reportError(error);
} }
}); });
}).catch(self._reportError); }).catch(self._service.reportError);
} }
disconnect() { disconnect() {
@ -884,15 +841,6 @@ class XmppService {
delete gSessions[self._name]; delete gSessions[self._name];
} }
_reportError(error) {
this.invokeCallback({
action: "error",
error: error,
}).catch(function(error) {
print(error);
});
}
_convertMessage(stanza) { _convertMessage(stanza) {
let self = this; let self = this;
let text; let text;
@ -909,18 +857,8 @@ class XmppService {
if (from && from.indexOf('/') != -1) { if (from && from.indexOf('/') != -1) {
from = from.split("/")[1]; from = from.split("/")[1];
} }
let conversation = from;
if (stanza.attributes.type == "groupchat") {
if (self._conversations[stanza.attributes.to.split("/")[0]]) {
conversation = stanza.attributes.to.split("/")[0];
} else if (self._conversations[stanza.attributes.from.split("/")[0]]) {
conversation = stanza.attributes.from.split("/")[0];
}
}
let message = { let message = {
action: "message",
from: from, from: from,
conversation: conversation,
message: text, message: text,
stanza: stanza, stanza: stanza,
timestamp: now, timestamp: now,
@ -937,23 +875,4 @@ class XmppService {
} }
}; };
let gSessions = {}; ChatService.handleMessages(XmppService);
core.register("onMessage", function(sender, options) {
let service = gSessions[options.name];
if (!service) {
service = new XmppService(options);
gSessions[options.name] = service;
} else {
if (service._callbacks.indexOf(options.callback) == -1) {
service._callbacks.push(options.callback);
}
}
return {
sendMessage: service.sendMessage.bind(service),
getConversations: service.getConversations.bind(service),
getHistory: service.getHistory.bind(service),
getParticipants: service.getParticipants.bind(service),
disconnect: service.disconnect.bind(service),
};
});