545 lines
20 KiB
JavaScript
545 lines
20 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
var weechat = angular.module('weechat');
|
|
|
|
weechat.factory('connection',
|
|
['$rootScope', '$log', 'handlers', 'models', 'settings', 'ngWebsockets', function($rootScope,
|
|
$log,
|
|
handlers,
|
|
models,
|
|
settings,
|
|
ngWebsockets) {
|
|
|
|
var protocol = new weeChat.Protocol();
|
|
|
|
var connectionData = [];
|
|
var reconnectTimer;
|
|
|
|
// Global connection lock to prevent multiple connections from being opened
|
|
var locked = false;
|
|
|
|
// Takes care of the connection and websocket hooks
|
|
var connect = function (host, port, path, passwd, ssl, useTotp, totp, noCompression, successCallback, failCallback) {
|
|
$rootScope.passwordError = false;
|
|
connectionData = [host, port, path, passwd, ssl, noCompression];
|
|
var proto = ssl ? 'wss' : 'ws';
|
|
// If host is an IPv6 literal wrap it in brackets
|
|
if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") {
|
|
host = "[" + host + "]";
|
|
}
|
|
var url = proto + "://" + host + ":" + port + "/" + path;
|
|
$log.debug('Connecting to URL: ', url);
|
|
|
|
var onopen = function () {
|
|
|
|
|
|
// Helper methods for initialization commands
|
|
var _initializeConnection = function(passwd) {
|
|
// Escape comma in password (#937)
|
|
passwd = passwd.replace(',', '\\,');
|
|
// This is not the proper way to do this.
|
|
// WeeChat does not send a confirmation for the init.
|
|
// Until it does, We need to "assume" that formatInit
|
|
// will be received before formatInfo
|
|
ngWebsockets.send(
|
|
weeChat.Protocol.formatInit({
|
|
password: passwd,
|
|
compression: noCompression ? 'off' : 'zlib',
|
|
useTotp: useTotp,
|
|
totp: totp
|
|
})
|
|
);
|
|
|
|
return ngWebsockets.send(
|
|
weeChat.Protocol.formatInfo({
|
|
name: 'version'
|
|
})
|
|
);
|
|
};
|
|
|
|
var _requestHotlist = function() {
|
|
return ngWebsockets.send(
|
|
weeChat.Protocol.formatHdata({
|
|
path: "hotlist:gui_hotlist(*)",
|
|
keys: []
|
|
})
|
|
);
|
|
};
|
|
|
|
var _requestBufferInfos = function() {
|
|
return ngWebsockets.send(
|
|
weeChat.Protocol.formatHdata({
|
|
path: 'buffer:gui_buffers(*)',
|
|
keys: ['local_variables,notify,number,full_name,short_name,title,hidden,type']
|
|
})
|
|
);
|
|
};
|
|
|
|
var _requestSync = function() {
|
|
return ngWebsockets.send(
|
|
weeChat.Protocol.formatSync({})
|
|
);
|
|
};
|
|
|
|
var _parseWeechatTimeFormat = function() {
|
|
// helper function to get a custom delimiter span
|
|
var _timeDelimiter = function(delim) {
|
|
return "'<span class=\"cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters\">" + delim + "</span>'";
|
|
};
|
|
|
|
// Fetch the buffer time format from weechat
|
|
var timeFormat = models.wconfig['weechat.look.buffer_time_format'];
|
|
|
|
// Weechat uses strftime, with time specifiers such as %I:%M:%S for 12h time
|
|
// The time formatter we use, AngularJS' date filter, uses a different format
|
|
// Where %I:%M:%S would be represented as hh:mm:ss
|
|
// Here, we detect what format the user has set in Weechat and slot it into
|
|
// one of four formats, (short|long) (12|24)-hour time
|
|
var angularFormat = "";
|
|
|
|
var timeDelimiter = _timeDelimiter(":");
|
|
|
|
var left12 = "hh" + timeDelimiter + "mm";
|
|
var right12 = "' 'a";
|
|
|
|
var short12 = left12 + right12;
|
|
var long12 = left12 + timeDelimiter + "ss" + right12;
|
|
|
|
var short24 = "HH" + timeDelimiter + "mm";
|
|
var long24 = short24 + timeDelimiter + "ss";
|
|
|
|
if (timeFormat.indexOf("%H") > -1 ||
|
|
timeFormat.indexOf("%k") > -1) {
|
|
// 24h time detected
|
|
if (timeFormat.indexOf("%S") > -1) {
|
|
// show seconds
|
|
angularFormat = long24;
|
|
} else {
|
|
// don't show seconds
|
|
angularFormat = short24;
|
|
}
|
|
} else if (timeFormat.indexOf("%I") > -1 ||
|
|
timeFormat.indexOf("%l") > -1 ||
|
|
timeFormat.indexOf("%p") > -1 ||
|
|
timeFormat.indexOf("%P") > -1) {
|
|
// 12h time detected
|
|
if (timeFormat.indexOf("%S") > -1) {
|
|
// show seconds
|
|
angularFormat = long12;
|
|
} else {
|
|
// don't show seconds
|
|
angularFormat = short12;
|
|
}
|
|
} else if (timeFormat.indexOf("%r") > -1) {
|
|
// strftime doesn't have an equivalent for short12???
|
|
angularFormat = long12;
|
|
} else if (timeFormat.indexOf("%T") > -1) {
|
|
angularFormat = long24;
|
|
} else if (timeFormat.indexOf("%R") > -1) {
|
|
angularFormat = short24;
|
|
} else {
|
|
angularFormat = short24;
|
|
}
|
|
|
|
// Assemble date format
|
|
var date_components = [];
|
|
|
|
// Check for day of month in time format
|
|
var day_pos = Math.max(timeFormat.indexOf("%d"),
|
|
timeFormat.indexOf("%e"));
|
|
date_components.push([day_pos, "dd"]);
|
|
|
|
// month of year?
|
|
var month_pos = timeFormat.indexOf("%m");
|
|
date_components.push([month_pos, "MM"]);
|
|
|
|
// year as well?
|
|
var year_pos = Math.max(timeFormat.indexOf("%y"),
|
|
timeFormat.indexOf("%Y"));
|
|
if (timeFormat.indexOf("%y") > -1) {
|
|
date_components.push([year_pos, "yy"]);
|
|
} else if (timeFormat.indexOf("%Y") > -1) {
|
|
date_components.push([year_pos, "yyyy"]);
|
|
}
|
|
|
|
// if there is a date, assemble it in the right order
|
|
date_components.sort();
|
|
var format_array = [];
|
|
for (var i = 0; i < date_components.length; i++) {
|
|
if (date_components[i][0] == -1) continue;
|
|
format_array.push(date_components[i][1]);
|
|
}
|
|
if (format_array.length > 0) {
|
|
// TODO: parse delimiter as well? For now, use '/' as it is
|
|
// more common internationally than '-'
|
|
var date_format = format_array.join(_timeDelimiter("/"));
|
|
angularFormat = date_format + _timeDelimiter(" ") + angularFormat;
|
|
}
|
|
|
|
$rootScope.angularTimeFormat = angularFormat;
|
|
};
|
|
|
|
|
|
// First command asks for the password and issues
|
|
// a version command. If it fails, it means the we
|
|
// did not provide the proper password.
|
|
_initializeConnection(passwd).then(
|
|
function(version) {
|
|
handlers.handleVersionInfo(version);
|
|
// Connection is successful
|
|
// Send all the other commands required for initialization
|
|
_requestBufferInfos().then(function(bufinfo) {
|
|
handlers.handleBufferInfo(bufinfo);
|
|
});
|
|
|
|
_requestHotlist().then(function(hotlist) {
|
|
handlers.handleHotlistInfo(hotlist);
|
|
|
|
});
|
|
if (settings.hotlistsync) {
|
|
// Schedule hotlist syncing every so often so that this
|
|
// client will have unread counts (mostly) in sync with
|
|
// other clients or terminal usage directly.
|
|
setInterval(function() {
|
|
if ($rootScope.connected) {
|
|
_requestHotlist().then(function(hotlist) {
|
|
handlers.handleHotlistInfo(hotlist);
|
|
|
|
});
|
|
}
|
|
}, 60000); // Sync hotlist every 60 second
|
|
}
|
|
|
|
|
|
// Fetch weechat time format for displaying timestamps
|
|
fetchConfValue('weechat.look.buffer_time_format',
|
|
function() {
|
|
// Will set models.wconfig['weechat.look.buffer_time_format']
|
|
_parseWeechatTimeFormat();
|
|
});
|
|
|
|
// Fetch nick completion config
|
|
fetchConfValue('weechat.completion.nick_completer');
|
|
fetchConfValue('weechat.completion.nick_add_space');
|
|
|
|
_requestSync();
|
|
$log.info("Connected to relay");
|
|
$rootScope.connected = true;
|
|
if (successCallback) {
|
|
successCallback();
|
|
}
|
|
},
|
|
function() {
|
|
handleWrongPassword();
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
var onmessage = function() {
|
|
// If we recieve a message from WeeChat it means that
|
|
// password was OK. Store that result and check for it
|
|
// in the failure handler.
|
|
$rootScope.waseverconnected = true;
|
|
};
|
|
|
|
|
|
var onclose = function (evt) {
|
|
/*
|
|
* Handles websocket disconnection
|
|
*/
|
|
$log.info("Disconnected from relay");
|
|
$rootScope.$emit('relayDisconnect');
|
|
locked = false;
|
|
if ($rootScope.userdisconnect || !$rootScope.waseverconnected) {
|
|
handleClose(evt);
|
|
$rootScope.userdisconnect = false;
|
|
} else {
|
|
reconnect(evt);
|
|
}
|
|
handleWrongPassword();
|
|
};
|
|
|
|
var handleClose = function (evt) {
|
|
if (ssl && evt && evt.code === 1006) {
|
|
// A password error doesn't trigger onerror, but certificate issues do. Check time of last error.
|
|
if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) {
|
|
// abnormal disconnect by client, most likely ssl error
|
|
$rootScope.sslError = true;
|
|
$rootScope.$apply();
|
|
}
|
|
}
|
|
};
|
|
|
|
var handleWrongPassword = function() {
|
|
// Connection got closed, lets check if we ever was connected successfully
|
|
if (!$rootScope.waseverconnected && !$rootScope.errorMessage) {
|
|
$rootScope.passwordError = true;
|
|
$rootScope.$apply();
|
|
}
|
|
};
|
|
|
|
var onerror = function (evt) {
|
|
/*
|
|
* Handles cases when connection issues come from
|
|
* the relay.
|
|
*/
|
|
$log.error("Relay error", evt);
|
|
locked = false; // release connection lock
|
|
$rootScope.lastError = Date.now();
|
|
|
|
if (evt.type === "error" && this.readyState !== 1) {
|
|
ngWebsockets.failCallbacks('error');
|
|
$rootScope.errorMessage = true;
|
|
}
|
|
};
|
|
|
|
if (locked) {
|
|
// We already have an open connection
|
|
$log.debug("Aborting connection (lock in use)");
|
|
}
|
|
// Kinda need a compare-and-swap here...
|
|
locked = true;
|
|
|
|
try {
|
|
ngWebsockets.connect(url,
|
|
protocol,
|
|
{
|
|
'binaryType': "arraybuffer",
|
|
'onopen': onopen,
|
|
'onclose': onclose,
|
|
'onmessage': onmessage,
|
|
'onerror': onerror
|
|
});
|
|
} catch(e) {
|
|
locked = false;
|
|
$log.debug("Websocket caught DOMException:", e);
|
|
$rootScope.lastError = Date.now();
|
|
$rootScope.errorMessage = true;
|
|
$rootScope.securityError = true;
|
|
$rootScope.$emit('relayDisconnect');
|
|
|
|
if (failCallback) {
|
|
failCallback();
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
var attemptReconnect = function (bufferId, timeout) {
|
|
// won't work if totp is mandatory
|
|
if (settings.useTotp)
|
|
{
|
|
$log.info('Not reconnecting because totp will be expired.');
|
|
return;
|
|
}
|
|
|
|
$log.info('Attempting to reconnect...');
|
|
var d = connectionData;
|
|
connect(d[0], d[1], d[2], d[3], d[4], false, "", function() {
|
|
$rootScope.reconnecting = false;
|
|
// on success, update active buffer
|
|
models.setActiveBuffer(bufferId);
|
|
$log.info('Sucessfully reconnected to relay');
|
|
}, function() {
|
|
// on failure, schedule another attempt
|
|
if (timeout >= 600000) {
|
|
// If timeout is ten minutes or more, give up
|
|
$log.info('Failed to reconnect, giving up');
|
|
handleClose();
|
|
} else {
|
|
$log.info('Failed to reconnect, scheduling next attempt in', timeout/1000, 'seconds');
|
|
// Clear previous timer, if exists
|
|
if (reconnectTimer !== undefined) {
|
|
clearTimeout(reconnectTimer);
|
|
}
|
|
reconnectTimer = setTimeout(function() {
|
|
// exponential timeout increase
|
|
attemptReconnect(bufferId, timeout * 1.5);
|
|
}, timeout);
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
var reconnect = function (evt) {
|
|
if (connectionData.length < 5) {
|
|
// something is wrong
|
|
$log.error('Cannot reconnect, connection information is missing');
|
|
return;
|
|
}
|
|
|
|
// reinitialise everything, clear all buffers
|
|
// TODO: this can be further extended in the future by looking
|
|
// at the last line in ever buffer and request more buffers from
|
|
// WeeChat based on that
|
|
models.reinitialize();
|
|
$rootScope.reconnecting = true;
|
|
// Have to do this to get the reconnect banner to show
|
|
$rootScope.$apply();
|
|
|
|
var bufferId = models.getActiveBuffer().id,
|
|
timeout = 3000; // start with a three-second timeout
|
|
|
|
reconnectTimer = setTimeout(function() {
|
|
attemptReconnect(bufferId, timeout);
|
|
}, timeout);
|
|
};
|
|
|
|
var disconnect = function() {
|
|
$log.info('Disconnecting from relay');
|
|
$rootScope.userdisconnect = true;
|
|
ngWebsockets.send(weeChat.Protocol.formatQuit());
|
|
// In case the backend doesn't repond we will close from our end
|
|
var closeTimer = setTimeout(function() {
|
|
ngWebsockets.disconnect();
|
|
// We pretend we are not connected anymore
|
|
// The connection can time out on its own
|
|
ngWebsockets.failCallbacks('disconnection');
|
|
$rootScope.connected = false;
|
|
locked = false; // release the connection lock
|
|
$rootScope.$emit('relayDisconnect');
|
|
$rootScope.$apply();
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Format and send a weechat message
|
|
*
|
|
* @returns the angular promise
|
|
*/
|
|
var sendMessage = function(message) {
|
|
ngWebsockets.send(weeChat.Protocol.formatInput({
|
|
buffer: models.getActiveBufferReference(),
|
|
data: message
|
|
}));
|
|
};
|
|
|
|
var sendCoreCommand = function(command) {
|
|
ngWebsockets.send(weeChat.Protocol.formatInput({
|
|
buffer: 'core.weechat',
|
|
data: command
|
|
}));
|
|
};
|
|
|
|
var sendHotlistClear = function() {
|
|
if (models.version[0] >= 1) {
|
|
// WeeChat >= 1 supports clearing hotlist with this command
|
|
sendMessage('/buffer set hotlist -1');
|
|
// Also move read marker
|
|
sendMessage('/input set_unread_current_buffer');
|
|
} else {
|
|
// If user wants to sync hotlist with weechat
|
|
// we will send a /buffer bufferName command every time
|
|
// the user switches a buffer. This will ensure that notifications
|
|
// are cleared in the buffer the user switches to
|
|
sendCoreCommand('/buffer ' + models.getActiveBuffer().fullName);
|
|
}
|
|
};
|
|
|
|
var sendHotlistClearAll = function() {
|
|
sendMessage("/input hotlist_clear");
|
|
};
|
|
|
|
var requestNicklist = function(bufferId, callback) {
|
|
// Prevent requesting nicklist for all buffers if bufferId is invalid
|
|
if (!bufferId) {
|
|
return;
|
|
}
|
|
ngWebsockets.send(
|
|
weeChat.Protocol.formatNicklist({
|
|
buffer: "0x"+bufferId
|
|
})
|
|
).then(function(nicklist) {
|
|
handlers.handleNicklist(nicklist);
|
|
if (callback !== undefined) {
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
|
|
var fetchConfValue = function(name, callback) {
|
|
ngWebsockets.send(
|
|
weeChat.Protocol.formatInfolist({
|
|
name: "option",
|
|
pointer: 0,
|
|
args: name
|
|
})
|
|
).then(function(i) {
|
|
handlers.handleConfValue(i);
|
|
if (callback !== undefined) {
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
|
|
var fetchMoreLines = function(numLines) {
|
|
$log.debug('Fetching ', numLines, ' lines');
|
|
var buffer = models.getActiveBuffer();
|
|
if (numLines === undefined) {
|
|
// Math.max(undefined, *) = NaN -> need a number here
|
|
numLines = 0;
|
|
}
|
|
// Calculate number of lines to fetch, at least as many as the parameter
|
|
numLines = Math.max(numLines, buffer.requestedLines * 2);
|
|
|
|
// Indicator that we are loading lines, hides "load more lines" link
|
|
$rootScope.loadingLines = true;
|
|
// Send hdata request to fetch lines for this particular buffer
|
|
return ngWebsockets.send(
|
|
weeChat.Protocol.formatHdata({
|
|
// "0x" is important, otherwise it won't work
|
|
path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data",
|
|
keys: []
|
|
})
|
|
).then(function(lineinfo) {
|
|
//XXX move to handlers?
|
|
// delete old lines and add new ones
|
|
var oldLength = buffer.lines.length;
|
|
// whether we already had all unread lines
|
|
var hadAllUnreadLines = buffer.lastSeen >= 0;
|
|
|
|
// clear the old lines
|
|
buffer.lines.length = 0;
|
|
// We need to set the number of requested lines to 0 here, because parsing a line
|
|
// increments it. This is needed to also count newly arriving lines while we're
|
|
// already connected.
|
|
buffer.requestedLines = 0;
|
|
// Count number of lines recieved
|
|
var linesReceivedCount = lineinfo.objects[0].content.length;
|
|
|
|
// Parse the lines
|
|
handlers.handleLineInfo(lineinfo, true);
|
|
|
|
// Correct the read marker for the lines that were counted twice
|
|
buffer.lastSeen -= oldLength;
|
|
|
|
// We requested more lines than we got, no more lines.
|
|
if (linesReceivedCount < numLines) {
|
|
buffer.allLinesFetched = true;
|
|
}
|
|
$rootScope.loadingLines = false;
|
|
|
|
// Only scroll to read marker if we didn't have all unread lines previously, but have them now
|
|
var scrollToReadmarker = !hadAllUnreadLines && buffer.lastSeen >= 0;
|
|
// Scroll to correct position
|
|
$rootScope.scrollWithBuffer(scrollToReadmarker, true);
|
|
});
|
|
};
|
|
|
|
|
|
return {
|
|
connect: connect,
|
|
disconnect: disconnect,
|
|
sendMessage: sendMessage,
|
|
sendCoreCommand: sendCoreCommand,
|
|
sendHotlistClear: sendHotlistClear,
|
|
sendHotlistClearAll: sendHotlistClearAll,
|
|
fetchMoreLines: fetchMoreLines,
|
|
requestNicklist: requestNicklist,
|
|
attemptReconnect: attemptReconnect
|
|
};
|
|
}]);
|
|
})();
|