some rearrangements
This commit is contained in:
@@ -37,870 +37,1026 @@
|
|||||||
// 1 2 3 4 5 6 7 8
|
// 1 2 3 4 5 6 7 8
|
||||||
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
|
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||||
|
|
||||||
function SafeChatClient(success, notice, error) {
|
function SafeChat() {
|
||||||
|
|
||||||
/// Cache Client's Key from local Strorage
|
/// Create UID from a name by appending an E-Mail
|
||||||
var k = null;
|
function uid(name) {
|
||||||
|
return name+' <'+name+'@'+hostname+'>'
|
||||||
function browserSupported() {
|
|
||||||
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
|
|
||||||
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction
|
|
||||||
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
|
|
||||||
return window.indexedDB && window.crypto.getRandomValues && Storage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get User Key
|
/// @class Crypto cryptographic functions
|
||||||
/** @internal key ist cached in k
|
/** @param view is of class SafeChat.View */
|
||||||
@return key */
|
function Crypto(view) {
|
||||||
function key() {
|
|
||||||
if (k) return k
|
/// cache client's key from local strorage
|
||||||
if (typeof localStorage.key == 'undefined') return null
|
var k = null
|
||||||
return k = openpgp.key.readArmored(localStorage.key)
|
|
||||||
|
/// detect hosstname, default to safechat.ch
|
||||||
|
var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch'
|
||||||
|
|
||||||
|
/// get user key
|
||||||
|
/** @internal key ist cached in k
|
||||||
|
@return key */
|
||||||
|
function key() {
|
||||||
|
if (k) return k
|
||||||
|
if (typeof localStorage.key == 'undefined') return null
|
||||||
|
return k = openpgp.key.readArmored(localStorage.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get own user name
|
||||||
|
/** get user name as user id of first public key */
|
||||||
|
function user() {
|
||||||
|
if (k || key()) return k.pub.keys[0].getUserIds()[0]
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create New User
|
||||||
|
function createuser(user, email, pwd) {
|
||||||
|
view.notice("generating keys")
|
||||||
|
openpgp.generateKey({
|
||||||
|
numBits: 4096,
|
||||||
|
userIds: [{name: user, email: email}],
|
||||||
|
passphrase: pwd
|
||||||
|
}).then(function(keyPair) {
|
||||||
|
view.success("keys generated")
|
||||||
|
localStorage.key = keyPair.privateKeyArmored
|
||||||
|
k = keyPair.key
|
||||||
|
}).catch(function(e) {
|
||||||
|
console.log(e)
|
||||||
|
view.fatal("generating key pairs failed")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// open private key with password
|
||||||
|
/** @return @c true if password matches */
|
||||||
|
function password(pwd) {
|
||||||
|
return (k || keys()) && k.keys[0].decrypt(pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt Message
|
||||||
|
function encrypt(message, targetkeys, done, failed) {
|
||||||
|
if (!k) return false
|
||||||
|
openpgp.encrypt({
|
||||||
|
publicKeys: targetkeys.keys.concat(k.keys),
|
||||||
|
privateKeys: k,
|
||||||
|
data: message,
|
||||||
|
armor: false
|
||||||
|
}).then(done).else(failed)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt Message
|
||||||
|
function decrypt(message, sourcekeys, done, failed) {
|
||||||
|
if (!k) return false
|
||||||
|
openpgp.decrypt({
|
||||||
|
privateKeys: k.keys,
|
||||||
|
publicKeys: sourcekeys.keys,
|
||||||
|
message: message
|
||||||
|
}).then(done).else(failed)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Own User Name
|
/// database that stores in indexed db
|
||||||
/** Get user name as user id of first public key */
|
function DataBase() {
|
||||||
function uid() {
|
|
||||||
if (k || key()) return k.pub.keys[0].getUserIds()[0]
|
function user(name, key) {
|
||||||
return null
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create New User
|
/// manage local copy of users
|
||||||
function createuser(user, email, pwd) {
|
function Users() {
|
||||||
|
|
||||||
|
var users = new Map()
|
||||||
|
|
||||||
|
function add(usr) {
|
||||||
|
if (!users[usr.name])
|
||||||
|
users[usr.name].valid = true
|
||||||
|
else
|
||||||
|
users[usr.name].valid = users[usr.name].valid && users[usr.name].key == usr.key
|
||||||
|
users[usr.name].key = usr.key
|
||||||
|
users[usr.name].online = usr.online
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// manage local copy of messages
|
||||||
|
function Messages() {
|
||||||
|
|
||||||
|
var messages = {};
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @class Communication client socket communication
|
||||||
|
/** @param view is of class SafeChat.View */
|
||||||
|
function Communication(controller) {
|
||||||
|
|
||||||
|
var socket = io.connect()
|
||||||
|
|
||||||
|
function broadcast(signal, data) {
|
||||||
|
console.log("<=snd "+signal)
|
||||||
|
socket.broadcast.emit(signal, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit(signal, data) {
|
||||||
|
console.log("<-snd "+signal)
|
||||||
|
socket.emit(signal, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
.on("login", controller.loggedin)
|
||||||
|
.on("fail", controller.fail)
|
||||||
|
.on("user", controller.user)
|
||||||
|
.on("users", controller.users)
|
||||||
|
.on("message", controller.message)
|
||||||
|
.on("messages", controller.messages)
|
||||||
|
.io
|
||||||
|
.on("connect", controller.connected)
|
||||||
|
.on("reconnect", controller.connected)
|
||||||
|
.on("disconnect", controller.disconnected)
|
||||||
|
.on("error", controller.disconnected)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @class View provides the glue to the GUI in the index.ejs file
|
||||||
|
/** View provides the following callbacks:
|
||||||
|
- status updates:
|
||||||
|
- @c notice(msg) to display information
|
||||||
|
- @c warning(msg)
|
||||||
|
- @c error(msg)
|
||||||
|
- @c fatal(msg) */
|
||||||
|
function View() {
|
||||||
|
|
||||||
|
var nexttimer = null
|
||||||
|
|
||||||
|
/// Padding for numbers in dates
|
||||||
|
function pad(n) {
|
||||||
|
return n<10 ? '0'+n : n
|
||||||
|
}
|
||||||
|
|
||||||
|
/// escape text to show in html @see htmldec
|
||||||
|
function htmlenc(html) {
|
||||||
|
return $('<div/>').text(html).html()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// decode html encoded text @see htmlenc
|
||||||
|
function htmldec(data) {
|
||||||
|
return $('<div/>').html(data).text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// alert user accoustically or by vibration
|
||||||
|
/** alert user, e.g. that a new message has arrived. */
|
||||||
|
function beep() {
|
||||||
|
if (navigator.vibrate) navigator.vibrate(1000)
|
||||||
|
(new Audio("sounds/beep.mp3")).play()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show fatal error
|
||||||
|
/** something completely failed, abort
|
||||||
|
@param msg */
|
||||||
|
function fatal(msg) {
|
||||||
|
if (nexttimer) clearTimeout(nexttimer)
|
||||||
|
if (msg) {
|
||||||
|
error(msg)
|
||||||
|
$('#fatal-msg').html(msg)
|
||||||
|
}
|
||||||
|
show('fatal')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show error messsage
|
||||||
|
/** shows an error message and logs to console.
|
||||||
|
@param data (optional) The error can be a string or any structure.
|
||||||
|
Strings are shown to the user, structures are logged only.
|
||||||
|
@param next (optional) next function to call */
|
||||||
|
function error(data, next) {
|
||||||
|
if (nexttimer) clearTimeout(nexttimer)
|
||||||
|
$("#status").hide()
|
||||||
|
$("#status").addClass("error")
|
||||||
|
$("#status").removeClass("notice")
|
||||||
|
$("#status").removeClass("success")
|
||||||
|
if (data) {
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
$("#status").html(data)
|
||||||
|
console.log("error: "+data)
|
||||||
|
} else {
|
||||||
|
$("#status").html('error')
|
||||||
|
console.log("error: "+JSON.stringify(data))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$("#status").html('error')
|
||||||
|
console.log("error")
|
||||||
|
}
|
||||||
|
$("#status").show()
|
||||||
|
if (next) nexttimer = setTimeout(function() {
|
||||||
|
nexttimer = null
|
||||||
|
next()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show notice messsage
|
||||||
|
/** shows an notice message and logs to console.
|
||||||
|
@param text (optional) The data is a string. */
|
||||||
|
function notice(text) {
|
||||||
|
$("#status").hide()
|
||||||
|
$("#status").addClass("notice")
|
||||||
|
$("#status").removeClass("error")
|
||||||
|
$("#status").removeClass("success")
|
||||||
|
if (text) {
|
||||||
|
$("#status").html(text)
|
||||||
|
console.log("notice: "+text)
|
||||||
|
} else {
|
||||||
|
$("#status").html('')
|
||||||
|
console.log("notice")
|
||||||
|
}
|
||||||
|
$("#status").show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show success messsage
|
||||||
|
/** shows an success message and logs to console.
|
||||||
|
@param text (optional) The data is a string. */
|
||||||
|
function success(text) {
|
||||||
|
$("#status").hide()
|
||||||
|
$("#status").addClass("success")
|
||||||
|
$("#status").removeClass("error")
|
||||||
|
$("#status").removeClass("notice")
|
||||||
|
if (text) {
|
||||||
|
$("#status").html(text)
|
||||||
|
console.log("success: "+text)
|
||||||
|
} else {
|
||||||
|
$("#status").html('')
|
||||||
|
console.log("success")
|
||||||
|
}
|
||||||
|
$("#status").show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show a specific screen given the element id
|
||||||
|
/** @param id html id to be shown.
|
||||||
|
@param msg (optional) the success message text */
|
||||||
|
function show(id, msg) {
|
||||||
|
console.log("state: "+id)
|
||||||
|
if (msg) success(msg) else $("#status").hide()
|
||||||
|
$("#main").children(":not(#"+id+")").hide()
|
||||||
|
$("#main #"+id).show()
|
||||||
|
$("#main #"+id+" form input:first-child").focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show server connected status
|
||||||
|
function connected() {
|
||||||
|
console.log("server connected")
|
||||||
|
$("#connectionstatus #bad").hide()
|
||||||
|
$("#connectionstatus #good").show()
|
||||||
|
success("server connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show server disconnected status
|
||||||
|
function disconnected() {
|
||||||
|
console.log("server disconnected")
|
||||||
|
$("#connectionstatus #good").hide()
|
||||||
|
$("#connectionstatus #bad").show()
|
||||||
|
error("server disconnected", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// toggle menu display
|
||||||
|
function togglemenu() {
|
||||||
|
$("#menu").toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFeature(id, query) {
|
||||||
|
if (query) $('#'+id+':before')
|
||||||
|
.css('color', 'green')
|
||||||
|
.css('content', '✔')
|
||||||
|
else $('#'+id+':before')
|
||||||
|
.css('color', 'red')
|
||||||
|
.css('content', '✘')
|
||||||
|
if (query) $('#'+id)
|
||||||
|
.css('color', 'green')
|
||||||
|
.css('text-decoration', 'line-through')
|
||||||
|
else $('#'+id)
|
||||||
|
.css('color', 'red')
|
||||||
|
.css('text-decoration', 'none')
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFeatures() {
|
||||||
|
$('ul.features').css('list-style-type', 'none')
|
||||||
|
checkFeature("localstorage", Storage)
|
||||||
|
checkFeature("indexeddb", window.indexedDB)
|
||||||
|
checkFeature("randomvalues", window.crypto.getRandomValues)
|
||||||
|
checkFeature("vibrate", navigator.vibrate)
|
||||||
|
checkFeature("filereader", window.FileReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DataTransfer() {
|
||||||
|
|
||||||
|
var reboottimer = null
|
||||||
|
var data = new DataTransfer()
|
||||||
|
|
||||||
|
/// download profile backup
|
||||||
|
function backup() {
|
||||||
|
var download = document.createElement('a')
|
||||||
|
download.href = 'data:attachment/text,'+encodeURI(JSON.stringify(localStorage))
|
||||||
|
download.target = '_blank'
|
||||||
|
var now = new Date()
|
||||||
|
download.download =
|
||||||
|
pad(now.getFullYear())+pad(now.getMonth()+1)+pad(now.getDate())+
|
||||||
|
"-"+userid()+"@"+hostname+".bak"
|
||||||
|
var clickEvent = new MouseEvent("click", {
|
||||||
|
"view": window,
|
||||||
|
"bubbles": true,
|
||||||
|
"cancelable": false
|
||||||
|
})
|
||||||
|
download.dispatchEvent(clickEvent)
|
||||||
|
togglemenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload Profile Backup
|
||||||
|
function restore(evt) {
|
||||||
|
for (var i=0, f; f=evt.target.files[i]; ++i) {
|
||||||
|
var file = f
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = function(evt) {
|
||||||
|
if (evt.target.error) return error("error reading file")
|
||||||
|
if (evt.target.readyState==0) return notice("waiting for data …")
|
||||||
|
if (evt.target.readyState==1) return notice("loading data …")
|
||||||
|
var parsed=JSON.parse(evt.target.result)
|
||||||
|
togglemenu()
|
||||||
|
localStorage.pubkey = parsed.pubkey
|
||||||
|
localStorage.privkey = parsed.privkey
|
||||||
|
success("backup is restored")
|
||||||
|
console.log("reboot after restore in 2s")
|
||||||
|
if (!reboottimer && reboot) reboottimer = setTimeout(function() {
|
||||||
|
reboottimer = null
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.FileReader) {
|
||||||
|
$('restore-menu-item').hide()
|
||||||
|
error("your browser does not support file upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @class Controller defines the programm flow
|
||||||
|
function Controller(view) {
|
||||||
|
|
||||||
|
var db = new Database()
|
||||||
|
var communication = new Communication(this)
|
||||||
|
var users = new Users()
|
||||||
|
|
||||||
|
function fail(msg) {
|
||||||
|
console.log('rcv-> fail('+msg+')')
|
||||||
|
error(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loggedin() {
|
||||||
|
console.log("rcv-> login")
|
||||||
|
success("login successful")
|
||||||
|
chat()
|
||||||
|
}
|
||||||
|
|
||||||
|
function user(usr) {
|
||||||
|
console.log("rcv-> user")
|
||||||
|
if (usr.exits) users.add(usr)
|
||||||
|
}
|
||||||
|
|
||||||
|
function users() {
|
||||||
|
console.log("rcv-> users")
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(msg) {
|
||||||
|
console.log("rcv-> message")
|
||||||
|
}
|
||||||
|
|
||||||
|
function messages(msgs) {
|
||||||
|
console.log("rcv-> messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connected = view.connected
|
||||||
|
this.reconnect = view.connected
|
||||||
|
this.disconnect = view.disconnected
|
||||||
|
this.error = view.disconnected
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function user(usr) {
|
||||||
|
if (usr.exists) db.adduser
|
||||||
|
}
|
||||||
|
|
||||||
|
function initBrowser() {
|
||||||
|
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
|
||||||
|
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction
|
||||||
|
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
|
||||||
|
navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate
|
||||||
|
return window.indexedDB && window.crypto.getRandomValues && Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
view.reboot = run
|
||||||
|
var compatible = initBrowser()
|
||||||
|
view.checkFeatures()
|
||||||
|
if (!compatible)
|
||||||
|
view.fatal("your browser is not supported")
|
||||||
|
else
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
return new Controller(new View())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var filecontent = new Array() ///< temporary storage for attachments
|
||||||
|
var reboottimer = null
|
||||||
|
|
||||||
|
function connectionstatus() {
|
||||||
|
if (socket.connected) connected() else disconnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure local groups
|
||||||
|
/** … */
|
||||||
|
function groups() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if password is set and matches the repeated password
|
||||||
|
/** Checks if both passwords are identical and valid and gives
|
||||||
|
feedback to the user.
|
||||||
|
|
||||||
|
Called when user edits the password fields.
|
||||||
|
|
||||||
|
Sets @ref username and checks @ref password - if both are well
|
||||||
|
defined, enables the submit button.
|
||||||
|
|
||||||
|
@param pwd The password.
|
||||||
|
@param pwd2 The repeated password. */
|
||||||
|
function checkpwd(pwd, pwd2) {
|
||||||
|
$("#register").submit(function(event) {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (pwd==pwd2) password=pwd
|
||||||
|
else password=null
|
||||||
|
if (!password||password.length<1) password=null
|
||||||
|
$("#createuser").prop("disabled", !(username && password))
|
||||||
|
if (password) {
|
||||||
|
if (username) success("user is ready to be created")
|
||||||
|
else notice("password matches, please chose a valid user name")
|
||||||
|
} else {
|
||||||
|
if (username) notice("passwords don't match")
|
||||||
|
else if ($('#user').val()) notice("user name is already in use")
|
||||||
|
else notice("please chose a user name")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the receiver of a message exists on server.
|
||||||
|
/** Calls checknewuser.php on server and enables the message submit
|
||||||
|
button if the receiver of the message exists on the server. */
|
||||||
|
function checkpartner(user) {
|
||||||
|
$("#chat").submit(function(event) {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
emit("user", uid(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create Local Public-/Private-Key Pair
|
||||||
|
/** Called if user has not yet his keys, just generates a new key pair. */
|
||||||
|
function createkeypair(user, pwd) {
|
||||||
notice("generating keys")
|
notice("generating keys")
|
||||||
openpgp.generateKey({
|
openpgp.generateKey({
|
||||||
numBits: 4096,
|
numBits: 4096,
|
||||||
userIds: [{name: user, email: email}],
|
userIds: [{name: user, email: user+'@'+hostname}],
|
||||||
passphrase: pwd
|
passphrase: pwd
|
||||||
}).then(function(keyPair) {
|
}).then(function(keyPair) {
|
||||||
success("keys generated")
|
success("keys generated")
|
||||||
localStorage.key = keyPair.privateKeyArmored
|
localStorage.pubkey = keyPair.publicKeyArmored
|
||||||
k = keyPair.key
|
localStorage.privkey = keyPair.privateKeyArmored
|
||||||
|
login()
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
error("generating key pairs failed")
|
error("generating key pairs failed")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function password(pwd) {
|
/// Get Own Public Key
|
||||||
return (k || keys()) && k.keys[0].decrypt(pwd)
|
/** @return public key object */
|
||||||
}
|
function publicKey() {
|
||||||
|
if (typeof localStorage.pubkey == 'undefined') {
|
||||||
/// Encrypt Message
|
if (typeof localStorage.pubKey == 'undefined') {
|
||||||
function encrypt(targetkeys, message, done, failed) {
|
return null
|
||||||
if (!k) return false
|
|
||||||
openpgp.encrypt({
|
|
||||||
publicKeys: targets.keys.concat(k.keys),
|
|
||||||
privateKeys: k,
|
|
||||||
data: message,
|
|
||||||
armor: false})
|
|
||||||
.then(done)
|
|
||||||
.else(failed)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt Message
|
|
||||||
function decrypt(message) {
|
|
||||||
if (!k) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var password = null; ///< password, only stored temporary, until reload
|
|
||||||
var username = null; ///< username, only used during registration
|
|
||||||
var filecontent = new Array(); ///< temporary storage for attachments
|
|
||||||
var socket = io.connect();
|
|
||||||
var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch';
|
|
||||||
|
|
||||||
/// Padding for numbers in dates
|
|
||||||
function pad(n) {
|
|
||||||
return n<10 ? '0'+n : n
|
|
||||||
}
|
|
||||||
|
|
||||||
function uid(name) {
|
|
||||||
return name+' <'+name+'@'+hostname+'>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert number of bytes to readable text
|
|
||||||
function size(num) {
|
|
||||||
if (num>0.6*1024) {
|
|
||||||
if (num>0.6*1024*1024) {
|
|
||||||
if (num>0.6*1024*1024*1024) {
|
|
||||||
if (num>0.6*1024*1024*1024*1024) {
|
|
||||||
return Math.round(num/1024/1024/1024/1024)+"TB";
|
|
||||||
} else {
|
|
||||||
return Math.round(num/1024/1024/1024)+"GB";
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Math.round(num/1024/1024)+"MB";
|
localStorage.pubkey = localStorage.pubKey
|
||||||
|
localStorage.removeItem(pubKey)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return Math.round(num/1024)+"kB";
|
|
||||||
}
|
}
|
||||||
} else {
|
return openpgp.key.readArmored(localStorage.pubkey)
|
||||||
return num+"B";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var reboottimer = null;
|
/// Get Own Private Key
|
||||||
/// Show error messsage
|
/** @return private key object */
|
||||||
/** Fades in an error message and logs to console.
|
function privateKey() {
|
||||||
@param data (optional) The error can be a string or any structure.
|
if (typeof localStorage.privkey == 'undefined') {
|
||||||
Strings are shown to the user, structures are logged only.
|
if (typeof localStorage.privKey == 'undefined') {
|
||||||
@param stay (optional) If not given as @c true, reloads page after 5s. */
|
return null
|
||||||
function error(data, stay) {
|
} else {
|
||||||
$("#status").hide();
|
localStorage.privkey = localStorage.privKey
|
||||||
$("#status").addClass("error")
|
localStorage.removeItem(privKey)
|
||||||
$("#status").removeClass("notice")
|
|
||||||
$("#status").removeClass("success")
|
|
||||||
if (data) {
|
|
||||||
if (typeof data == 'string') {
|
|
||||||
$("#status").html(data);
|
|
||||||
console.log("error: "+data);
|
|
||||||
} else {
|
|
||||||
$("#status").html('unknown error: '+JSON.stringify(data));
|
|
||||||
console.log("error: "+JSON.stringify(data));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$("#status").html('error');
|
|
||||||
console.log("error");
|
|
||||||
}
|
|
||||||
$("#status").show();
|
|
||||||
if (!stay) {
|
|
||||||
console.log("reboot in 5s");
|
|
||||||
console.log((new Error('stacktrace')).stack);
|
|
||||||
if (!reboottimer) reboottimer = setTimeout(function() {
|
|
||||||
reboottimer = null;
|
|
||||||
start();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show notice messsage
|
|
||||||
/** Fades in an notice message and logs to console.
|
|
||||||
@param text (optional) The data is a string. */
|
|
||||||
function notice(text) {
|
|
||||||
$("#status").hide()
|
|
||||||
$("#status").addClass("notice")
|
|
||||||
$("#status").removeClass("error")
|
|
||||||
$("#status").removeClass("success")
|
|
||||||
if (text) {
|
|
||||||
$("#status").html(text);
|
|
||||||
console.log("notice: "+text);
|
|
||||||
} else {
|
|
||||||
$("#status").html('');
|
|
||||||
console.log("notice");
|
|
||||||
}
|
|
||||||
$("#status").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show notice messsage
|
|
||||||
/** Fades in an success message and logs to console.
|
|
||||||
@param text (optional) The data is a string. */
|
|
||||||
function success(text) {
|
|
||||||
$("#status").hide();
|
|
||||||
$("#status").addClass("success")
|
|
||||||
$("#status").removeClass("error")
|
|
||||||
$("#status").removeClass("notice")
|
|
||||||
if (text) {
|
|
||||||
$("#status").html(text);
|
|
||||||
console.log("success: "+text);
|
|
||||||
} else {
|
|
||||||
$("#status").html('');
|
|
||||||
console.log("success");
|
|
||||||
}
|
|
||||||
$("#status").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show status message in the main screen area
|
|
||||||
/** @param id HTML id to be shown.
|
|
||||||
@param msg The success message text */
|
|
||||||
function status(id, msg) {
|
|
||||||
console.log("state: "+id);
|
|
||||||
if (msg) success(msg); else $("#status").hide();
|
|
||||||
$("#main").children(":not(#"+id+")").hide();
|
|
||||||
$("#main #"+id).show();
|
|
||||||
$("#main #"+id+" form input:first-child").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function emit(signal, data) {
|
|
||||||
console.log("<-snd "+signal);
|
|
||||||
socket.emit(signal, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connected() {
|
|
||||||
console.log("server connected");
|
|
||||||
$("#connectionstatus #bad").hide();
|
|
||||||
$("#connectionstatus #good").show();
|
|
||||||
success("server connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnected() {
|
|
||||||
console.log("server disconnected");
|
|
||||||
$("#connectionstatus #good").hide();
|
|
||||||
$("#connectionstatus #bad").show();
|
|
||||||
error("server disconnected", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectionstatus() {
|
|
||||||
if (socket.connected) connected(); else disconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
function htmlenc(html) {
|
|
||||||
return $('<div/>').text(html).html();
|
|
||||||
}
|
|
||||||
|
|
||||||
function htmldec(data) {
|
|
||||||
return $('<div/>').html(data).text();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alert user
|
|
||||||
/** Alert user, e.g. that a new message has arrived. */
|
|
||||||
function beep(user) {
|
|
||||||
if (user) success("message from "+htmlenc(user)+" received");
|
|
||||||
navigator.vibrate =
|
|
||||||
navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate;
|
|
||||||
if (navigator.vibrate) {
|
|
||||||
// vibration API supported
|
|
||||||
navigator.vibrate(1000);
|
|
||||||
}
|
|
||||||
(new Audio("sounds/A-Tone-His_Self-1266414414.mp3")).play();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle Menu Display
|
|
||||||
function togglemenu() {
|
|
||||||
$("#menu").toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download Profile Backup
|
|
||||||
function backup() {
|
|
||||||
var download = document.createElement('a');
|
|
||||||
download.href = 'data:attachment/text,'+encodeURI(JSON.stringify(localStorage));
|
|
||||||
download.target = '_blank';
|
|
||||||
var now = new Date();
|
|
||||||
download.download =
|
|
||||||
pad(now.getFullYear())+pad(now.getMonth()+1)+pad(now.getDate())+
|
|
||||||
"-"+userid()+"@"+hostname+".bak";
|
|
||||||
var clickEvent = new MouseEvent("click", {
|
|
||||||
"view": window,
|
|
||||||
"bubbles": true,
|
|
||||||
"cancelable": false
|
|
||||||
});
|
|
||||||
download.dispatchEvent(clickEvent);
|
|
||||||
togglemenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload Profile Backup
|
|
||||||
function restore(evt) {
|
|
||||||
if (!window.FileReader)
|
|
||||||
return error("your browser does not support file upload", true);
|
|
||||||
for (var i=0, f; f=evt.target.files[i]; ++i) {
|
|
||||||
var file = f;
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function(evt) {
|
|
||||||
if (evt.target.error) return error("error reading file", true);
|
|
||||||
if (evt.target.readyState==0) return notice("waiting for data …");
|
|
||||||
if (evt.target.readyState==1) return notice("loading data …");
|
|
||||||
var parsed=JSON.parse(evt.target.result);
|
|
||||||
togglemenu();
|
|
||||||
localStorage.pubkey = parsed.pubkey;
|
|
||||||
localStorage.privkey = parsed.privkey;
|
|
||||||
success("backup is restored");
|
|
||||||
console.log("reboot after restore in 2s");
|
|
||||||
if (!reboottimer) reboottimer = setTimeout(function() {
|
|
||||||
reboottimer = null;
|
|
||||||
start();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configure local groups
|
|
||||||
/** … */
|
|
||||||
function groups() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if password is set and matches the repeated password
|
|
||||||
/** Checks if both passwords are identical and valid and gives
|
|
||||||
feedback to the user.
|
|
||||||
|
|
||||||
Called when user edits the password fields.
|
|
||||||
|
|
||||||
Sets @ref username and checks @ref password - if both are well
|
|
||||||
defined, enables the submit button.
|
|
||||||
|
|
||||||
@param pwd The password.
|
|
||||||
@param pwd2 The repeated password. */
|
|
||||||
function checkpwd(pwd, pwd2) {
|
|
||||||
$("#register").submit(function(event) {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (pwd==pwd2) password=pwd;
|
|
||||||
else password=null;
|
|
||||||
if (!password||password.length<1) password=null;
|
|
||||||
$("#createuser").prop("disabled", !(username && password));
|
|
||||||
if (password) {
|
|
||||||
if (username) success("user is ready to be created");
|
|
||||||
else notice("password matches, please chose a valid user name");
|
|
||||||
} else {
|
|
||||||
if (username) notice("passwords don't match");
|
|
||||||
else if ($('#user').val()) notice("user name is already in use");
|
|
||||||
else notice("please chose a user name");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the receiver of a message exists on server.
|
|
||||||
/** Calls checknewuser.php on server and enables the message submit
|
|
||||||
button if the receiver of the message exists on the server. */
|
|
||||||
function checkpartner(user) {
|
|
||||||
$("#chat").submit(function(event) {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
emit("user", uid(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create Local Public-/Private-Key Pair
|
|
||||||
/** Called if user has not yet his keys, just generates a new key pair. */
|
|
||||||
function createkeypair(user, pwd) {
|
|
||||||
notice("generating keys");
|
|
||||||
openpgp.generateKey({
|
|
||||||
numBits: 4096,
|
|
||||||
userIds: [{name: user, email: user+'@'+hostname}],
|
|
||||||
passphrase: pwd
|
|
||||||
}).then(function(keyPair) {
|
|
||||||
success("keys generated");
|
|
||||||
localStorage.pubkey = keyPair.publicKeyArmored;
|
|
||||||
localStorage.privkey = keyPair.privateKeyArmored;
|
|
||||||
login();
|
|
||||||
}).catch(function(e) {
|
|
||||||
console.log(e)
|
|
||||||
error("generating key pairs failed")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get Own Public Key
|
|
||||||
/** @return public key object */
|
|
||||||
function publicKey() {
|
|
||||||
if (typeof localStorage.pubkey == 'undefined') {
|
|
||||||
if (typeof localStorage.pubKey == 'undefined') {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
localStorage.pubkey = localStorage.pubKey;
|
|
||||||
localStorage.removeItem(pubKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return openpgp.key.readArmored(localStorage.pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get Own Private Key
|
|
||||||
/** @return private key object */
|
|
||||||
function privateKey() {
|
|
||||||
if (typeof localStorage.privkey == 'undefined') {
|
|
||||||
if (typeof localStorage.privKey == 'undefined') {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
localStorage.privkey = localStorage.privKey;
|
|
||||||
localStorage.removeItem(privKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return openpgp.key.readArmored(localStorage.privkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get Own User Name
|
|
||||||
/** Get user name as user id of first public key */
|
|
||||||
function userid() {
|
|
||||||
if (!publicKey() ||
|
|
||||||
publicKey().keys.length < 1 ||
|
|
||||||
publicKey().keys[0].getUserIds().length < 1) return null
|
|
||||||
return publicKey().keys[0].getUserIds()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear Message Text And Attachments
|
|
||||||
/** Does not remove the receiver's name */
|
|
||||||
function clearmessage() {
|
|
||||||
$("#message").prop(":disabled", true);
|
|
||||||
filecontent = new Array();
|
|
||||||
$('#preview').empty();
|
|
||||||
$("#msg").val("");
|
|
||||||
$("#message").prop(":disabled", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function guessfilename(mimetype, user, date) {
|
|
||||||
if (!user) user = userid();
|
|
||||||
if (!date) date = new Date();
|
|
||||||
var ext = mimetype.replace(/.*\/(x-)?/i, "");
|
|
||||||
return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate())
|
|
||||||
+"-"+ext+"-"+user+"@"+hostname+'.'+ext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display Image Attachments
|
|
||||||
function attachments(files, id, from, date) {
|
|
||||||
if (files) files.forEach(function(file) {
|
|
||||||
console.log(file);
|
|
||||||
if (!file.name) file.name = guessfilename(file.type, from, date);
|
|
||||||
var a = document.createElement('a');
|
|
||||||
a.href = file.content;
|
|
||||||
a.download = file.name;
|
|
||||||
a.target = '_blank';
|
|
||||||
if (file.type.match('^image/')) {
|
|
||||||
var img = document.createElement('img');
|
|
||||||
img.title = file.name;
|
|
||||||
img.src = file.content;
|
|
||||||
a.appendChild(img);
|
|
||||||
} else if (file.type.match('^video/')) {
|
|
||||||
var video = document.createElement('video');
|
|
||||||
video.controls = true;
|
|
||||||
video.title = file.name;
|
|
||||||
video.src = file.content;
|
|
||||||
a.appendChild(video);
|
|
||||||
} else {
|
|
||||||
var img = document.createElement('img');
|
|
||||||
img.title = file.name;
|
|
||||||
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg";
|
|
||||||
a.appendChild(img);
|
|
||||||
}
|
|
||||||
$(id).append(a);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var recorder;
|
|
||||||
|
|
||||||
function done() {
|
|
||||||
if (recorder) {
|
|
||||||
recorder.stop();
|
|
||||||
recorder.recording(function(data) {
|
|
||||||
previewfile(data, "video/webm");
|
|
||||||
abort();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function abort() {
|
|
||||||
if (recorder) {
|
|
||||||
$("#videorecorder").hide();
|
|
||||||
recorder.release();
|
|
||||||
delete recorder; recorder = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record Video from builtin camera
|
|
||||||
function recordvideo() {
|
|
||||||
try {
|
|
||||||
abort();
|
|
||||||
$("#videorecorder").show();
|
|
||||||
recorder = new MediaStreamRecorder({
|
|
||||||
video: {
|
|
||||||
width: {ideal: 180},
|
|
||||||
height: {ideal: 160}
|
|
||||||
},
|
|
||||||
audio: true
|
|
||||||
});
|
|
||||||
recorder.on("ready", function() {
|
|
||||||
$("#videorecorder video").attr("src", recorder.preview());
|
|
||||||
$("#videorecorder video").css("width", 180);
|
|
||||||
$("#videorecorder video").css("height", 160);
|
|
||||||
$("#videorecorder video").attr("width", 180);
|
|
||||||
$("#videorecorder video").attr("height", 160);
|
|
||||||
recorder.start();
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
error("cannot access camera", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previewfile(content, type, name) {
|
|
||||||
if (!name) name = guessfilename(type);
|
|
||||||
if (type.match('^image/')) {
|
|
||||||
var img = document.createElement("img");
|
|
||||||
img.onload = function() { // resize image to maximum 400px
|
|
||||||
var MAX = 400;
|
|
||||||
var width = img.width;
|
|
||||||
var height = img.height;
|
|
||||||
if (width > MAX) {
|
|
||||||
height *= MAX / width;
|
|
||||||
width = MAX;
|
|
||||||
}
|
}
|
||||||
if (height > MAX) {
|
}
|
||||||
width *= MAX / height;
|
return openpgp.key.readArmored(localStorage.privkey)
|
||||||
height = MAX;
|
}
|
||||||
|
|
||||||
|
/// Get Own User Name
|
||||||
|
/** Get user name as user id of first public key */
|
||||||
|
function userid() {
|
||||||
|
if (!publicKey() ||
|
||||||
|
publicKey().keys.length < 1 ||
|
||||||
|
publicKey().keys[0].getUserIds().length < 1) return null
|
||||||
|
return publicKey().keys[0].getUserIds()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear Message Text And Attachments
|
||||||
|
/** Does not remove the receiver's name */
|
||||||
|
function clearmessage() {
|
||||||
|
$("#message").prop(":disabled", true)
|
||||||
|
filecontent = new Array()
|
||||||
|
$('#preview').empty()
|
||||||
|
$("#msg").val("")
|
||||||
|
$("#message").prop(":disabled", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessfilename(mimetype, user, date) {
|
||||||
|
if (!user) user = userid()
|
||||||
|
if (!date) date = new Date()
|
||||||
|
var ext = mimetype.replace(/.*\/(x-)?/i, "")
|
||||||
|
return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate())
|
||||||
|
+"-"+ext+"-"+user+"@"+hostname+'.'+ext
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display Image Attachments
|
||||||
|
function attachments(files, id, from, date) {
|
||||||
|
if (files) files.forEach(function(file) {
|
||||||
|
console.log(file)
|
||||||
|
if (!file.name) file.name = guessfilename(file.type, from, date)
|
||||||
|
var a = document.createElement('a')
|
||||||
|
a.href = file.content
|
||||||
|
a.download = file.name
|
||||||
|
a.target = '_blank'
|
||||||
|
if (file.type.match('^image/')) {
|
||||||
|
var img = document.createElement('img')
|
||||||
|
img.title = file.name
|
||||||
|
img.src = file.content
|
||||||
|
a.appendChild(img)
|
||||||
|
} else if (file.type.match('^video/')) {
|
||||||
|
var video = document.createElement('video')
|
||||||
|
video.controls = true
|
||||||
|
video.title = file.name
|
||||||
|
video.src = file.content
|
||||||
|
a.appendChild(video)
|
||||||
|
} else {
|
||||||
|
var img = document.createElement('img')
|
||||||
|
img.title = file.name
|
||||||
|
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"
|
||||||
|
a.appendChild(img)
|
||||||
}
|
}
|
||||||
var canvas = document.createElement("canvas");
|
$(id).append(a)
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
var ctx = canvas.getContext("2d");
|
|
||||||
ctx.drawImage(img, 0, 0, width, height);
|
|
||||||
img.onload = function() {
|
|
||||||
filecontent.push({name: name, type: type, content: img.src});
|
|
||||||
$("#preview").append(img);
|
|
||||||
success('image is ready to be sent');
|
|
||||||
}
|
|
||||||
img.src = canvas.toDataURL(file.type);
|
|
||||||
img.title = name+"\n"+size(img.src.length);
|
|
||||||
}
|
|
||||||
img.src=content;
|
|
||||||
} else if (type.match('^video/')) {
|
|
||||||
filecontent.push({name: name, type: type, content: content});
|
|
||||||
var video = document.createElement("video");
|
|
||||||
video.setAttribute("controls", "controls");
|
|
||||||
video.setAttribute("loop", "loop");
|
|
||||||
video.setAttribute("src", content);
|
|
||||||
video.setAttribute("title", name+"\n"+size(content.length));
|
|
||||||
$("#preview").append(video);
|
|
||||||
} else {
|
|
||||||
filecontent.push({name: name, type: type, content: content});
|
|
||||||
var img = document.createElement("img");
|
|
||||||
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg";
|
|
||||||
img.title = name+"\n"+size(content.length);
|
|
||||||
$("#preview").append(img);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload Attachment
|
|
||||||
/** Prepares attachment to be sent in a message. If the attachment is
|
|
||||||
an image, it resizes the image to 400px on the lager side.
|
|
||||||
|
|
||||||
By now, only images are supported.
|
|
||||||
|
|
||||||
Stores data in global variable @ref filecontent. */
|
|
||||||
function fileupload(evt) {
|
|
||||||
if (!window.FileReader)
|
|
||||||
return error("your browser does not support file upload", true);
|
|
||||||
for (var i=0, f; f=evt.target.files[i]; ++i) {
|
|
||||||
var file = f;
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function(evt) {
|
|
||||||
if (evt.target.error) return error("error reading file", true);
|
|
||||||
if (evt.target.readyState==0) return notice("waiting for data …");
|
|
||||||
if (evt.target.readyState==1) return notice("loading data …");
|
|
||||||
previewfile(evt.target.result, file.type, file.name);
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets Receiver's Name
|
|
||||||
/** Called when clicked on a receiver's name. Sets focus to the
|
|
||||||
message text field.
|
|
||||||
|
|
||||||
@param name The receiver's name. */
|
|
||||||
function setreceiver(name) {
|
|
||||||
$("#recv").val(name);
|
|
||||||
checkpartner(name);
|
|
||||||
$("#msg").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
var userMap = null;
|
|
||||||
function users(userlist) {
|
|
||||||
console.log("rcv-> users");
|
|
||||||
userMap = new Array();
|
|
||||||
$("#allusers").empty();
|
|
||||||
userlist.forEach(function(usr) {
|
|
||||||
userMap[usr.name] = usr.pubkey;
|
|
||||||
$("#allusers").append('<option value="'+htmlenc(usr.name)+'">')
|
|
||||||
$("#allusers").hide();
|
|
||||||
console.log(" user: "+usr.name);
|
|
||||||
});
|
|
||||||
localStorage.userMap = JSON.stringify(userMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fail(msg) {
|
|
||||||
console.log("rcv-> fail");
|
|
||||||
error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loggedin() {
|
|
||||||
console.log("rcv-> login");
|
|
||||||
success("login successful");
|
|
||||||
chat();
|
|
||||||
}
|
|
||||||
|
|
||||||
function user(usr) {
|
|
||||||
if (usr.exists) console.log("rcv-> user("+usr.name+")");
|
|
||||||
else console.log("rcv-> user("+usr.name+"): name is available");
|
|
||||||
if ($("#newuser").is(":visible") && usr.name==uid($('#user').val())) {
|
|
||||||
// same username as in the create user form
|
|
||||||
$("#createuser").prop("disabled", usr.exists); // todo: check password
|
|
||||||
if (!usr.exists) {
|
|
||||||
username = usr.name;
|
|
||||||
success("user name "+usr.name+" is available");
|
|
||||||
} else {
|
|
||||||
username = null;
|
|
||||||
error("user name "+usr.name+" is in use", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($("#chat").is(":visible") && usr.name==uid($("#recv").val())) { // same username as in receiver
|
|
||||||
$('#send').prop("disabled", !usr.exists);
|
|
||||||
$("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4");
|
|
||||||
$("label[for=send] img").css("filter", usr.exists?"alpha(opacity=100)":"alpha(opacity=40)");
|
|
||||||
if (usr.exists) success("recipient exists");
|
|
||||||
else error("unknown recipient", true);
|
|
||||||
}
|
|
||||||
if (userMap == null) {
|
|
||||||
if (localStorage.userMap) {
|
|
||||||
userMap = JSON.parse(localStorage.userMap);
|
|
||||||
} else {
|
|
||||||
userMap = new Array();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) {
|
|
||||||
userMap[usr.name] = usr.pubkey;
|
|
||||||
$("#allusers").append('option value="'+htmlenc(usr.name)+'"')
|
|
||||||
localStorage.userMap = JSON.stringify(userMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryuser(usr) {
|
|
||||||
console.log("query user: "+uid(usr));
|
|
||||||
socket.emit("user", uid(usr));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a user's public key.
|
|
||||||
/** The first time, gets it from the server, later from the cache. */
|
|
||||||
function getPublicKey(user) {
|
|
||||||
var deferredObject = $.Deferred();
|
|
||||||
if (userMap && userMap[user]) deferredObject.resolve(userMap[user]);
|
|
||||||
else deferredObject.reject("unknown user");
|
|
||||||
return deferredObject.promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Received a list of messages from server
|
|
||||||
function messages(msgs) {
|
|
||||||
console.log("rcv-> messages("+msgs.length+")");
|
|
||||||
if (!password || !privateKey())
|
|
||||||
return setTimeout(function() {emit("messages");}, 1000); // try again later
|
|
||||||
status("allmessages");
|
|
||||||
notice("load messages, please wait …");
|
|
||||||
msgs.forEach(function(msg) {message(msg, true);});
|
|
||||||
status("chat");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Received a message from server
|
|
||||||
function message(m, internal) {
|
|
||||||
if (!internal) console.log("rcv-> message("+m.user+")");
|
|
||||||
if (!password || !privateKey()) return;
|
|
||||||
var key=openpgp.key.readArmored(m.pubkey);
|
|
||||||
if (key.err) return error("key of sender unreadable", true);
|
|
||||||
var message = openpgp.message.readArmored(m.msg);
|
|
||||||
var privkey = privateKey().keys[0];
|
|
||||||
if (privkey.decrypt(password)) // prepare own key
|
|
||||||
openpgp.decrypt({
|
|
||||||
privateKeys: privkey,
|
|
||||||
publicKeys: key.keys,
|
|
||||||
message: message
|
|
||||||
}).then(function(msg) { // decryption succeded
|
|
||||||
// prepend message to list of messages
|
|
||||||
var message = JSON.parse(msg.text);
|
|
||||||
$("#msgs") // todo: check msg.signatures[0].valid
|
|
||||||
.prepend('<div id="id'+(m.id)+'" class="msg '+
|
|
||||||
(m.user==userid()?"me":"other")+
|
|
||||||
'"><div class="header">'+
|
|
||||||
'<span class="date">'+
|
|
||||||
(new Date(m.time)).toLocaleString()+
|
|
||||||
'</span><span class="sender">'+
|
|
||||||
'<a href="javascript:void(0)" '+
|
|
||||||
'onclick="setreceiver(this.innerHTML)">'+
|
|
||||||
htmlenc(m.user)+
|
|
||||||
'</a>'+(message.receiver?' → <a href="javascript:void(0)" '+
|
|
||||||
'onclick="setreceiver(this.innerHTML)">'
|
|
||||||
+htmlenc(message.receiver)+'</a>':"")+
|
|
||||||
'</span></div>'+
|
|
||||||
'<div class="text">'+
|
|
||||||
htmlenc(message.text)+
|
|
||||||
'</div></div><div class="clear"/>');
|
|
||||||
// show attachments
|
|
||||||
attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time));
|
|
||||||
// calculate and show emoticons
|
|
||||||
$('#id'+m.id).emoticonize();
|
|
||||||
if (!internal) beep(m.user);
|
|
||||||
}).catch(function(e) {
|
|
||||||
// not for me
|
|
||||||
success();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send Message To Server
|
|
||||||
/** User wants to send a message. Encrypt message with own private and
|
|
||||||
the receiver's public key, then send it to the server. */
|
|
||||||
function sendmessage(recv, txt) {
|
|
||||||
notice("1/3 preparing message …");
|
|
||||||
$("#message").prop(":disabled", true);
|
|
||||||
getPublicKey(recv) // get receiver's public key
|
|
||||||
.done(function(pk) {
|
|
||||||
var key=openpgp.key.readArmored(pk);
|
|
||||||
if (!pk||key.err) {
|
|
||||||
$("#message").prop(":disabled", false);
|
|
||||||
error("receiver's key not found", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var privkey = privateKey().keys[0];
|
|
||||||
privkey.decrypt(password); // get own private key ready
|
|
||||||
var message = JSON.stringify({receiver: recv, text: txt, files: filecontent});
|
|
||||||
notice("2/3 encrypting message …");
|
|
||||||
openpgp.encrypt({publicKeys: key.keys.concat(publicKey().keys),
|
|
||||||
privateKeys: privkey,
|
|
||||||
data: message,
|
|
||||||
armor: false})
|
|
||||||
.then(function(msg) { // message is encrypted
|
|
||||||
notice("3/3 sending message …");
|
|
||||||
emit("message", {user: userid(), content: msg});
|
|
||||||
clearmessage();
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
$("#message").prop(":disabled", false);
|
|
||||||
error("encryption of message failed", true);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.fail(function(e) {
|
|
||||||
$("#message").prop(":disabled", false);
|
|
||||||
error("user not found", true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check And Set Password
|
|
||||||
/** Check if given password matches to decrypt the private key. If so,
|
|
||||||
store it in global temporary variable @ref password and start the
|
|
||||||
chat. The password matches, when the private key can be decrypted.
|
|
||||||
|
|
||||||
@param pwd The password to check. */
|
|
||||||
function setpw(pwd) {
|
|
||||||
if (privateKey().keys[0].decrypt(pwd)) {
|
|
||||||
success("password matches");
|
|
||||||
$("#removeKey").hide();
|
|
||||||
password = pwd;
|
|
||||||
chat();
|
|
||||||
} else {
|
|
||||||
notice("password does not match");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Create Password Entry Field
|
var recorder
|
||||||
/** Asks user for password. When user starts to enter it, it is
|
|
||||||
permanentely checked in setpw(). As soon as the password matches,
|
|
||||||
setpw() continues automatically. No submit is required by the
|
|
||||||
user. */
|
|
||||||
function getpwd() {
|
|
||||||
if (password) return;
|
|
||||||
$("#removeKey").show();
|
|
||||||
status("getpwd");
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteUser() {
|
function done() {
|
||||||
var uid = userid();
|
if (recorder) {
|
||||||
localStorage.removeItem(pubkey);
|
recorder.stop()
|
||||||
localStorage.removeItem(privkey);
|
recorder.recording(function(data) {
|
||||||
error("user "+uid+" permanentely lost");
|
previewfile(data, "video/webm")
|
||||||
}
|
abort()
|
||||||
|
})
|
||||||
function removeKey() {
|
}
|
||||||
togglemenu();
|
|
||||||
$("#removeKey").hide();
|
|
||||||
status('forgotpassword');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main Chat Window
|
|
||||||
/** Gets chat widgets from server and displays them. Starts timer for
|
|
||||||
get() which polls for new messages. */
|
|
||||||
var firsttime = true;
|
|
||||||
function chat() {
|
|
||||||
if (!password) return getpwd();
|
|
||||||
status("chat");
|
|
||||||
if (firsttime && $('#msgs').is(':empty')) {
|
|
||||||
firsttime = false;
|
|
||||||
notice("getting previous messages, please wait …");
|
|
||||||
emit("messages");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Login User
|
function abort() {
|
||||||
/** This is not really a login, it is just some kind of validation.
|
if (recorder) {
|
||||||
The server does not care if a user is online or not, it is only
|
$("#videorecorder").hide()
|
||||||
interesting to the client to make sure, everything is fine. User
|
recorder.release()
|
||||||
is logged in the following way: User name and public key are sent
|
delete recorder recorder = null
|
||||||
to the server. If the user name exists on the server and the
|
}
|
||||||
public key is the same, the user is considered logged in, his
|
|
||||||
credentials seem to be valid. If user does not yet exits on
|
|
||||||
server, it is created now. If user exists, but public key is
|
|
||||||
different, then this is a complete failure, something went
|
|
||||||
terribly wrong. */
|
|
||||||
function login() {
|
|
||||||
$("#username").html(userid()+"@"+hostname);
|
|
||||||
emit("login", {name: userid(),
|
|
||||||
pubkey: localStorage.pubkey});
|
|
||||||
success("login sent to server");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get And Display Form To Create New User
|
|
||||||
/** Shows user creation form. On submit, a private key is generated in
|
|
||||||
createkeypair(), then login() creates the user. */
|
|
||||||
function newuser() {
|
|
||||||
status("newuser");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if local storage is available
|
|
||||||
function checkLocalStorage() {
|
|
||||||
var test = 'test';
|
|
||||||
try {
|
|
||||||
localStorage.setItem(test, test);
|
|
||||||
localStorage.removeItem(test);
|
|
||||||
return true;
|
|
||||||
} catch(e) {
|
|
||||||
status("nolocalstorage");
|
|
||||||
error("local storage not available");
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initial Function: Startup
|
/// Record Video from builtin camera
|
||||||
/** Decide whether to login or to create a new user */
|
function recordvideo() {
|
||||||
function start() {
|
|
||||||
$("#menu").hide();
|
|
||||||
//status("startup");
|
|
||||||
if (checkLocalStorage())
|
|
||||||
try {
|
try {
|
||||||
if (!userid()) {
|
abort()
|
||||||
newuser();
|
$("#videorecorder").show()
|
||||||
} else {
|
recorder = new MediaStreamRecorder({
|
||||||
login();
|
video: {
|
||||||
|
width: {ideal: 180},
|
||||||
|
height: {ideal: 160}
|
||||||
|
},
|
||||||
|
audio: true
|
||||||
|
})
|
||||||
|
recorder.on("ready", function() {
|
||||||
|
$("#videorecorder video").attr("src", recorder.preview())
|
||||||
|
$("#videorecorder video").css("width", 180)
|
||||||
|
$("#videorecorder video").css("height", 160)
|
||||||
|
$("#videorecorder video").attr("width", 180)
|
||||||
|
$("#videorecorder video").attr("height", 160)
|
||||||
|
recorder.start()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
error("cannot access camera", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewfile(content, type, name) {
|
||||||
|
if (!name) name = guessfilename(type)
|
||||||
|
if (type.match('^image/')) {
|
||||||
|
var img = document.createElement("img")
|
||||||
|
img.onload = function() { // resize image to maximum 400px
|
||||||
|
var MAX = 400
|
||||||
|
var width = img.width
|
||||||
|
var height = img.height
|
||||||
|
if (width > MAX) {
|
||||||
|
height *= MAX / width
|
||||||
|
width = MAX
|
||||||
|
}
|
||||||
|
if (height > MAX) {
|
||||||
|
width *= MAX / height
|
||||||
|
height = MAX
|
||||||
|
}
|
||||||
|
var canvas = document.createElement("canvas")
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
var ctx = canvas.getContext("2d")
|
||||||
|
ctx.drawImage(img, 0, 0, width, height)
|
||||||
|
img.onload = function() {
|
||||||
|
filecontent.push({name: name, type: type, content: img.src})
|
||||||
|
$("#preview").append(img)
|
||||||
|
success('image is ready to be sent')
|
||||||
|
}
|
||||||
|
img.src = canvas.toDataURL(file.type)
|
||||||
|
img.title = name+"\n"+size(img.src.length)
|
||||||
}
|
}
|
||||||
} catch (m) {
|
img.src=content
|
||||||
console.log(m.stack);
|
} else if (type.match('^video/')) {
|
||||||
error(m);
|
filecontent.push({name: name, type: type, content: content})
|
||||||
|
var video = document.createElement("video")
|
||||||
|
video.setAttribute("controls", "controls")
|
||||||
|
video.setAttribute("loop", "loop")
|
||||||
|
video.setAttribute("src", content)
|
||||||
|
video.setAttribute("title", name+"\n"+size(content.length))
|
||||||
|
$("#preview").append(video)
|
||||||
|
} else {
|
||||||
|
filecontent.push({name: name, type: type, content: content})
|
||||||
|
var img = document.createElement("img")
|
||||||
|
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"
|
||||||
|
img.title = name+"\n"+size(content.length)
|
||||||
|
$("#preview").append(img)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
/// Upload Attachment
|
||||||
/// On Load, Call @ref start
|
/** Prepares attachment to be sent in a message. If the attachment is
|
||||||
$(window.onbeforeunload = function() {
|
an image, it resizes the image to 400px on the lager side.
|
||||||
return "Are you sure you want to navigate away?";
|
|
||||||
});
|
By now, only images are supported.
|
||||||
/// Allow Running in Background on Android
|
|
||||||
document.addEventListener('deviceready', function () {
|
Stores data in global variable @ref filecontent. */
|
||||||
if (cordova && cordova.plugins.backgroundMode) {
|
function fileupload(evt) {
|
||||||
cordova.plugins.backgroundMode.enable();
|
if (!window.FileReader)
|
||||||
|
return error("your browser does not support file upload", true)
|
||||||
|
for (var i=0, f; f=evt.target.files[i]; ++i) {
|
||||||
|
var file = f
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = function(evt) {
|
||||||
|
if (evt.target.error) return error("error reading file", true)
|
||||||
|
if (evt.target.readyState==0) return notice("waiting for data …")
|
||||||
|
if (evt.target.readyState==1) return notice("loading data …")
|
||||||
|
previewfile(evt.target.result, file.type, file.name)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
}, false);
|
}
|
||||||
socket.io.on("connect", connected);
|
|
||||||
socket.io.on("reconnect", connected);
|
|
||||||
socket.io.on("disconnect", disconnected);
|
|
||||||
socket.io.on("error", disconnected);
|
|
||||||
socket.on("login", loggedin);
|
|
||||||
socket.on("fail", fail);
|
|
||||||
socket.on("user", user);
|
|
||||||
socket.on("users", users);
|
|
||||||
socket.on("message", message);
|
|
||||||
socket.on("messages", messages);
|
|
||||||
connectionstatus();
|
|
||||||
if (openpgp.initWorker("openpgp.worker.min.js"))
|
|
||||||
console.log("asynchronous openpgp enabled");
|
|
||||||
else
|
|
||||||
console.log("asynchronous openpgp failed");
|
|
||||||
emit('users');
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start Main Loop
|
/// Sets Receiver's Name
|
||||||
$(init);
|
/** Called when clicked on a receiver's name. Sets focus to the
|
||||||
|
message text field.
|
||||||
|
|
||||||
|
@param name The receiver's name. */
|
||||||
|
function setreceiver(name) {
|
||||||
|
$("#recv").val(name)
|
||||||
|
checkpartner(name)
|
||||||
|
$("#msg").focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
var userMap = null
|
||||||
|
function users(userlist) {
|
||||||
|
console.log("rcv-> users")
|
||||||
|
userMap = new Array()
|
||||||
|
$("#allusers").empty()
|
||||||
|
userlist.forEach(function(usr) {
|
||||||
|
userMap[usr.name] = usr.pubkey
|
||||||
|
$("#allusers").append('<option value="'+htmlenc(usr.name)+'">')
|
||||||
|
$("#allusers").hide()
|
||||||
|
console.log(" user: "+usr.name)
|
||||||
|
})
|
||||||
|
localStorage.userMap = JSON.stringify(userMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
function user(usr) {
|
||||||
|
if (usr.exists) console.log("rcv-> user("+usr.name+")")
|
||||||
|
else console.log("rcv-> user("+usr.name+"): name is available")
|
||||||
|
if ($("#newuser").is(":visible") && usr.name==uid($('#user').val())) {
|
||||||
|
// same username as in the create user form
|
||||||
|
$("#createuser").prop("disabled", usr.exists) // todo: check password
|
||||||
|
if (!usr.exists) {
|
||||||
|
username = usr.name
|
||||||
|
success("user name "+usr.name+" is available")
|
||||||
|
} else {
|
||||||
|
username = null
|
||||||
|
error("user name "+usr.name+" is in use", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($("#chat").is(":visible") && usr.name==uid($("#recv").val())) { // same username as in receiver
|
||||||
|
$('#send').prop("disabled", !usr.exists)
|
||||||
|
$("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4")
|
||||||
|
$("label[for=send] img").css("filter", usr.exists?"alpha(opacity=100)":"alpha(opacity=40)")
|
||||||
|
if (usr.exists) success("recipient exists")
|
||||||
|
else error("unknown recipient", true)
|
||||||
|
}
|
||||||
|
if (userMap == null) {
|
||||||
|
if (localStorage.userMap) {
|
||||||
|
userMap = JSON.parse(localStorage.userMap)
|
||||||
|
} else {
|
||||||
|
userMap = new Array()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) {
|
||||||
|
userMap[usr.name] = usr.pubkey
|
||||||
|
$("#allusers").append('option value="'+htmlenc(usr.name)+'"')
|
||||||
|
localStorage.userMap = JSON.stringify(userMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryuser(usr) {
|
||||||
|
console.log("query user: "+uid(usr))
|
||||||
|
socket.emit("user", uid(usr))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a user's public key.
|
||||||
|
/** The first time, gets it from the server, later from the cache. */
|
||||||
|
function getPublicKey(user) {
|
||||||
|
var deferredObject = $.Deferred()
|
||||||
|
if (userMap && userMap[user]) deferredObject.resolve(userMap[user])
|
||||||
|
else deferredObject.reject("unknown user")
|
||||||
|
return deferredObject.promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Received a list of messages from server
|
||||||
|
function messages(msgs) {
|
||||||
|
console.log("rcv-> messages("+msgs.length+")")
|
||||||
|
if (!password || !privateKey())
|
||||||
|
return setTimeout(function() {emit("messages")}, 1000) // try again later
|
||||||
|
status("allmessages")
|
||||||
|
notice("load messages, please wait …")
|
||||||
|
msgs.forEach(function(msg) {message(msg, true)})
|
||||||
|
status("chat")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Received a message from server
|
||||||
|
function message(m, internal) {
|
||||||
|
if (!internal) console.log("rcv-> message("+m.user+")")
|
||||||
|
if (!password || !privateKey()) return
|
||||||
|
var key=openpgp.key.readArmored(m.pubkey)
|
||||||
|
if (key.err) return error("key of sender unreadable", true)
|
||||||
|
var message = openpgp.message.readArmored(m.msg)
|
||||||
|
var privkey = privateKey().keys[0]
|
||||||
|
if (privkey.decrypt(password)) // prepare own key
|
||||||
|
openpgp.decrypt({
|
||||||
|
privateKeys: privkey,
|
||||||
|
publicKeys: key.keys,
|
||||||
|
message: message
|
||||||
|
}).then(function(msg) { // decryption succeded
|
||||||
|
openpgp.decrypt({
|
||||||
|
privateKeys: privkey,
|
||||||
|
publicKeys: key.keys,
|
||||||
|
message: message
|
||||||
|
}).then(function(msg) { // decryption succeded
|
||||||
|
// prepend message to list of messages
|
||||||
|
var message = JSON.parse(msg.text)
|
||||||
|
$("#msgs") // todo: check msg.signatures[0].valid
|
||||||
|
.prepend('<div id="id'+(m.id)+'" class="msg '+
|
||||||
|
(m.user==userid()?"me":"other")+
|
||||||
|
'"><div class="header">'+
|
||||||
|
'<span class="date">'+
|
||||||
|
(new Date(m.time)).toLocaleString()+
|
||||||
|
'</span><span class="sender">'+
|
||||||
|
'<a href="javascript:void(0)" '+
|
||||||
|
'onclick="setreceiver(this.innerHTML)">'+
|
||||||
|
htmlenc(m.user)+
|
||||||
|
'</a>'+(message.receiver?' → <a href="javascript:void(0)" '+
|
||||||
|
'onclick="setreceiver(this.innerHTML)">'
|
||||||
|
+htmlenc(message.receiver)+'</a>':"")+
|
||||||
|
'</span></div>'+
|
||||||
|
'<div class="text">'+
|
||||||
|
htmlenc(message.text)+
|
||||||
|
'</div></div><div class="clear"/>')
|
||||||
|
// show attachments
|
||||||
|
attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time))
|
||||||
|
// calculate and show emoticons
|
||||||
|
$('#id'+m.id).emoticonize()
|
||||||
|
if (!internal) beep(m.user)
|
||||||
|
}).catch(function(e) {
|
||||||
|
// not for me
|
||||||
|
success()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send Message To Server
|
||||||
|
/** User wants to send a message. Encrypt message with own private and
|
||||||
|
the receiver's public key, then send it to the server. */
|
||||||
|
function sendmessage(recv, txt) {
|
||||||
|
notice("1/3 preparing message …")
|
||||||
|
$("#message").prop(":disabled", true)
|
||||||
|
getPublicKey(recv) // get receiver's public key
|
||||||
|
.done(function(pk) {
|
||||||
|
var key=openpgp.key.readArmored(pk)
|
||||||
|
if (!pk||key.err) {
|
||||||
|
$("#message").prop(":disabled", false)
|
||||||
|
error("receiver's key not found", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var privkey = privateKey().keys[0]
|
||||||
|
privkey.decrypt(password) // get own private key ready
|
||||||
|
var message = JSON.stringify({receiver: recv, text: txt, files: filecontent})
|
||||||
|
notice("2/3 encrypting message …")
|
||||||
|
openpgp.encrypt({publicKeys: key.keys.concat(publicKey().keys),
|
||||||
|
privateKeys: privkey,
|
||||||
|
data: message,
|
||||||
|
armor: false})
|
||||||
|
.then(function(msg) { // message is encrypted
|
||||||
|
notice("3/3 sending message …")
|
||||||
|
emit("message", {user: userid(), content: msg})
|
||||||
|
clearmessage()
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
$("#message").prop(":disabled", false)
|
||||||
|
error("encryption of message failed", true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.fail(function(e) {
|
||||||
|
$("#message").prop(":disabled", false)
|
||||||
|
error("user not found", true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check And Set Password
|
||||||
|
/** Check if given password matches to decrypt the private key. If so,
|
||||||
|
store it in global temporary variable @ref password and start the
|
||||||
|
chat. The password matches, when the private key can be decrypted.
|
||||||
|
|
||||||
|
@param pwd The password to check. */
|
||||||
|
function setpw(pwd) {
|
||||||
|
if (privateKey().keys[0].decrypt(pwd)) {
|
||||||
|
success("password matches")
|
||||||
|
$("#removeKey").hide()
|
||||||
|
password = pwd
|
||||||
|
chat()
|
||||||
|
} else {
|
||||||
|
notice("password does not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create Password Entry Field
|
||||||
|
/** Asks user for password. When user starts to enter it, it is
|
||||||
|
permanentely checked in setpw(). As soon as the password matches,
|
||||||
|
setpw() continues automatically. No submit is required by the
|
||||||
|
user. */
|
||||||
|
function getpwd() {
|
||||||
|
if (password) return
|
||||||
|
$("#removeKey").show()
|
||||||
|
status("getpwd")
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser() {
|
||||||
|
var uid = userid()
|
||||||
|
localStorage.removeItem(pubkey)
|
||||||
|
localStorage.removeItem(privkey)
|
||||||
|
error("user "+uid+" permanentely lost")
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeKey() {
|
||||||
|
togglemenu()
|
||||||
|
$("#removeKey").hide()
|
||||||
|
status('forgotpassword')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main Chat Window
|
||||||
|
/** Gets chat widgets from server and displays them. Starts timer for
|
||||||
|
get() which polls for new messages. */
|
||||||
|
var firsttime = true
|
||||||
|
function chat() {
|
||||||
|
if (!password) return getpwd()
|
||||||
|
status("chat")
|
||||||
|
if (firsttime && $('#msgs').is(':empty')) {
|
||||||
|
firsttime = false
|
||||||
|
notice("getting previous messages, please wait …")
|
||||||
|
emit("messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login User
|
||||||
|
/** This is not really a login, it is just some kind of validation.
|
||||||
|
The server does not care if a user is online or not, it is only
|
||||||
|
interesting to the client to make sure, everything is fine. User
|
||||||
|
is logged in the following way: User name and public key are sent
|
||||||
|
to the server. If the user name exists on the server and the
|
||||||
|
public key is the same, the user is considered logged in, his
|
||||||
|
credentials seem to be valid. If user does not yet exits on
|
||||||
|
server, it is created now. If user exists, but public key is
|
||||||
|
different, then this is a complete failure, something went
|
||||||
|
terribly wrong. */
|
||||||
|
function login() {
|
||||||
|
$("#username").html(userid()+"@"+hostname)
|
||||||
|
emit("login", {name: userid(),
|
||||||
|
pubkey: localStorage.pubkey})
|
||||||
|
success("login sent to server")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get And Display Form To Create New User
|
||||||
|
/** Shows user creation form. On submit, a private key is generated in
|
||||||
|
createkeypair(), then login() creates the user. */
|
||||||
|
function newuser() {
|
||||||
|
status("newuser")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if local storage is available
|
||||||
|
function checkLocalStorage() {
|
||||||
|
var test = 'test'
|
||||||
|
try {
|
||||||
|
localStorage.setItem(test, test)
|
||||||
|
localStorage.removeItem(test)
|
||||||
|
return true
|
||||||
|
} catch(e) {
|
||||||
|
status("nolocalstorage")
|
||||||
|
error("local storage not available")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initial Function: Startup
|
||||||
|
/** Decide whether to login or to create a new user */
|
||||||
|
function start() {
|
||||||
|
$("#menu").hide()
|
||||||
|
//status("startup")
|
||||||
|
if (checkLocalStorage())
|
||||||
|
try {
|
||||||
|
if (!userid()) {
|
||||||
|
newuser()
|
||||||
|
} else {
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
} catch (m) {
|
||||||
|
console.log(m.stack)
|
||||||
|
error(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
/// On Load, Call @ref start
|
||||||
|
$(window.onbeforeunload = function() {
|
||||||
|
return "Are you sure you want to navigate away?"
|
||||||
|
})
|
||||||
|
/// Allow Running in Background on Android
|
||||||
|
document.addEventListener('deviceready', function () {
|
||||||
|
if (cordova && cordova.plugins.backgroundMode) {
|
||||||
|
cordova.plugins.backgroundMode.enable()
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
connectionstatus()
|
||||||
|
if (openpgp.initWorker("openpgp.worker.min.js"))
|
||||||
|
console.log("asynchronous openpgp enabled")
|
||||||
|
else
|
||||||
|
console.log("asynchronous openpgp failed")
|
||||||
|
emit('users')
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start Main Loop
|
||||||
|
$(init)
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<ul id="menu" style="display: none">
|
<ul id="menu" style="display: none">
|
||||||
<li onclick="backup()">Download Backup</li>
|
<li onclick="backup()">Download Backup</li>
|
||||||
<li class="toolbutton"><label for="restore">Restore Backup</label><input autocomplete="off" type="file" accept="*.bak" id="restore" /></li>
|
<li id="restore-menu-item" class="toolbutton"><label for="restore">Restore Backup</label><input autocomplete="off" type="file" accept="*.bak" id="restore" /></li>
|
||||||
<li id="groups" onclick="groups()">Edit Groups</li>
|
<li id="groups" onclick="groups()">Edit Groups</li>
|
||||||
<li id="removeKey" style="display: none" onclick="removeKey()">Password Forgotten</li>
|
<li id="removeKey" style="display: none" onclick="removeKey()">Password Forgotten</li>
|
||||||
<li id="android-download" href="safechat.apk"><a href="safechat.apk">Download Android-App</a></li>
|
<li id="android-download" href="safechat.apk"><a href="safechat.apk">Download Android-App</a></li>
|
||||||
@@ -182,6 +182,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Fatal: Abort -->
|
||||||
|
|
||||||
|
<div id="fatal">
|
||||||
|
<h2 id="fatal-msg">Failure</h2>
|
||||||
|
<p>The SafeChat has been aborted due to a fatal error.</p>
|
||||||
|
<p>There is a problem in your browser. Please try to reload. If the problem persists, please update your web browser or try SafeChat in another browser.</p>
|
||||||
|
<p>The following java script features are required:</p>
|
||||||
|
<ul class="features">
|
||||||
|
<li id="localstorage">Local Storage</li>
|
||||||
|
<li id="indexeddb">Indexed DB</li>
|
||||||
|
<li id="randomvalues">Cryptography: Random Values</li>
|
||||||
|
</ul>
|
||||||
|
<p>The following java script features are optional:</p>
|
||||||
|
<ul class="features">
|
||||||
|
<li id="vibrate">Vibration (vibrates when new message arrives)</li>
|
||||||
|
<li id="filereader">File Reader (required to restore backup)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Error: Missing JavaScript -->
|
<!-- Error: Missing JavaScript -->
|
||||||
<noscript>
|
<noscript>
|
||||||
<h2>JavasScript Required!</h2>
|
<h2>JavasScript Required!</h2>
|
||||||
@@ -193,12 +212,6 @@
|
|||||||
<p><a href="<%= package.documentation %>" target="_blank">more information</a></p>
|
<p><a href="<%= package.documentation %>" target="_blank">more information</a></p>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<!-- Error: Missing LocalStorage -->
|
|
||||||
<div id="nolocalstorage" style="display: none">
|
|
||||||
<p>No access to local storage. Please allow access to local
|
|
||||||
storage, i.e. do not block cookies.<p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notice: Setup Messages -->
|
<!-- Notice: Setup Messages -->
|
||||||
<div id="allmessages" style="display: none">
|
<div id="allmessages" style="display: none">
|
||||||
<p>Setting up all previous messages, please wait …</p>
|
<p>Setting up all previous messages, please wait …</p>
|
||||||
|
Reference in New Issue
Block a user