/ * ! @ f i l e
@ id $Id$
This is the main application as it is fully run in the user ' s browser .
@ dot
digraph X {
start [ URL = "\ref start()" ] ;
newuser [ URL = "\ref newuser()" ] ;
login [ URL = "\ref login()" ] ;
createkeypair [ URL = "\ref createkeypair()" ] ;
chat [ URL = "\ref chat()" ] ;
getpwd [ URL = "\ref getpwd()" ] ;
setpw [ URL = "\ref setpw()" ] ;
get [ URL = "\ref get()" ] ;
sendmessage [ URL = "\ref sendmessage()" ] ;
start - > newuser [ label = "if no keys exist" ] ;
start - > login [ label = "if keys exist" ] ;
newuser - > createkeypair [ label = "on submit" ] ;
createkeypair - > "openpgp.generateKey" ;
"openpgp.generateKey" - > login [ label = "keys generated in local store" ] ;
login - > chat [ label = "user is valid on server" ] ;
chat - > getpwd [ label = "password not yet entered" ] ;
getpwd - > setpw [ label = "on input" ] ;
setpw - > chat [ label = "password is valid" ] ;
chat - > chat [ label = "remain in chat" ] ;
chat - > get [ label = "start timer" ] ;
get - > get [ label = "restart timer" ] ;
chat - > sendmessage [ label = "on submit" ] ;
sendmessage - > chat [ label = "remain in chat" ] ;
}
@ enddot
* /
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
function SafeChat ( ) {
/// Create UID from a name by appending an E-Mail
function uid ( name ) {
return name + ' <' + mail ( name ) + '>'
}
function mail ( name ) {
var hostname = window . location . hostname != 'localhost' ? window . location . hostname : 'safechat.ch'
return name + '@' + hostname
}
//==============================================================================
/// @class Crypto cryptographic functions
/** @param view is of class SafeChat.View */
function Crypto ( controller ) {
/// cache client's key from local strorage
var k = null
/// detect hostname, default to safechat.ch
/// get user key
/ * * @ i n t e r n a l k e y i s t c a c h e d i n k
@ return key * /
this . key = function ( ) {
if ( k ) return k // cached key
if ( typeof localStorage . privkey === 'undefined' ) return null
return k = openpgp . key . readArmored ( localStorage . privkey )
}
/// get own user name
/** get user name as user id of first public key */
this . user = function ( ) {
if ( k || key ( ) ) return k . pub . keys [ 0 ] . getUserIds ( ) [ 0 ]
return null
}
/// 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
}
//------------------------------------------------------------------------------
if ( openpgp . initWorker ( "openpgp.worker.min.js" ) )
console . log ( "asynchronous openpgp enabled" )
else
console . log ( "asynchronous openpgp failed" )
}
//==============================================================================
/// database that stores in indexed db
function DataBase ( ) {
function user ( name , key ) {
}
}
//==============================================================================
/// manage local copy of users
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 , next ) {
console . log ( "<-snd " + signal )
socket . emit ( signal , data , next )
}
this . lookup = function ( usr , next ) {
emit ( 'user' , usr , next )
}
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
/ * * V i e w p r o v i d e s t h e f o l l o w i n g c a l l b a c k s :
- 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
/ * * s o m e t h i n g c o m p l e t e l y f a i l e d , a b o r t
@ param msg * /
function fatal ( msg ) {
if ( nexttimer ) clearTimeout ( nexttimer )
if ( msg ) {
error ( msg )
$ ( '#fatal-msg' ) . html ( msg )
}
show ( 'fatal' )
}
/// show error messsage
/ * * s h o w s a n e r r o r m e s s a g e a n d l o g s t o c o n s o l e .
@ 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
/ * * s h o w s a n n o t i c e m e s s a g e a n d l o g s t o c o n s o l e .
@ 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
/ * * s h o w s a n s u c c e s s m e s s a g e a n d l o g s t o c o n s o l e .
@ 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
/ * * @ p a r a m i d h t m l i d t o b e s h o w n .
@ 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 )
. css ( 'color' , 'green' )
. prepend ( '<span>✔</span>' )
else $ ( '#' + id )
. css ( 'color' , 'red' )
. prepend ( '<span>✘</span>' )
}
this . checkFeatures = function ( ) {
$ ( '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 )
}
/// @name create new user
/// @{
this . newuser = function ( ) {
show ( 'newuser' )
}
var user = null
var pwd = false
function invalid ( usr ) {
return ! user || ! user . exists && user . name . length < 3
}
this . available = function ( usr ) {
user = usr
console . log ( "props:" , invalid ( user ) || ! pwd )
$ ( "#createuser" ) . prop ( ":disabled" , invalid ( user ) || ! pwd )
if ( user . length == 0 )
notice ( "please chose a user name" )
else if ( user . length < 3 )
notice ( "please chose a longer user name" )
else if ( user . exists )
notice ( "user name is already in use" )
else if ( ! pwd )
notice ( "please chose a password" )
else
success ( "user is ready to be created" )
}
this . passwords = function ( pwd1 , pwd2 ) {
pwd = pwd1 == pwd2 && pwd1 . length > 5
console . log ( "props:" , invalid ( user ) || ! pwd )
$ ( "#createuser" ) . prop ( ":disabled" , invalid ( user ) || ! pwd )
if ( pwd1 . length == 0 )
notice ( 'please chose a password' )
else if ( pwd1 . length < 6 )
notice ( 'please chose a longer password' )
else if ( pwd1 != pwd2 )
notice ( "passwords don't match" )
else if ( invalid ( user ) )
notice ( "please chose a user name" )
else
success ( "user is ready to be created" )
}
/// @}
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 crypto = new Crypto ( this )
var communication = new Communication ( this )
var users = new Users ( )
/// @name error handling
/// @{
this . fatal = view . fatal
this . error = view . error
this . warning = view . warning
this . notice = view . notice
/// @}
/// @name signals from server
/// @{
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
// @}
/// @name signals from view
/// @{
/// @name new user registration
/// @{
this . lookup = function ( usr ) {
if ( usr . length > 2 ) communication . lookup ( uid ( usr ) , function ( res ) {
view . available ( res )
} )
}
this . checkpasswords = view . passwords
this . createuser = function ( name , pwd ) {
crypto . createuser ( name , name + '@' + hostname , pwd ) . then ( function ( ) {
if ( ! crypto . password ( pwd ) )
fatal ( "private key decryption failed" )
else
chat ( )
} )
}
/// @}
/// @}
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
}
var newuser = view . newuser
function chat ( ) {
}
function password ( ) {
}
function login ( ) {
if ( ! crypto . key ( ) ) newuser ( ) ; else password ( ) ;
}
this . run = function ( ) {
login ( )
}
this . start = function ( ) {
view . reboot = this . run
var compatible = initBrowser ( )
view . checkFeatures ( )
if ( ! compatible )
view . fatal ( "your browser is not supported" )
else
this . 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
/ * * C h e c k s i f b o t h p a s s w o r d s a r e i d e n t i c a l a n d v a l i d a n d g i v e s
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.
/ * * C a l l s c h e c k n e w u s e r . p h p o n s e r v e r a n d e n a b l e s t h e m e s s a g e s u b m i t
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
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 )
}
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
/ * * P r e p a r e s a t t a c h m e n t t o b e s e n t i n a m e s s a g e . I f t h e a t t a c h m e n t i s
an image , it resizes the image to 400 px 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
/ * * C a l l e d w h e n c l i c k e d o n a r e c e i v e r ' s n a m e . S e t s f o c u s t o t h e
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
show ( "allmessages" )
notice ( "load messages, please wait …" )
msgs . forEach ( function ( msg ) { message ( msg , true ) } )
show ( "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
/ * * U s e r w a n t s t o s e n d a m e s s a g e . E n c r y p t m e s s a g e w i t h o w n p r i v a t e a n d
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
/ * * C h e c k i f g i v e n p a s s w o r d m a t c h e s t o d e c r y p t t h e p r i v a t e k e y . I f s o ,
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
/ * * A s k s u s e r f o r p a s s w o r d . W h e n u s e r s t a r t s t o e n t e r i t , i t i s
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 ( )
show ( "getpwd" )
}
function deleteUser ( ) {
var uid = userid ( )
localStorage . removeItem ( pubkey )
localStorage . removeItem ( privkey )
error ( "user " + uid + " permanentely lost" )
}
function removeKey ( ) {
togglemenu ( )
$ ( "#removeKey" ) . hide ( )
show ( 'forgotpassword' )
}
/// Main Chat Window
/ * * G e t s c h a t w i d g e t s f r o m s e r v e r a n d d i s p l a y s t h e m . S t a r t s t i m e r f o r
get ( ) which polls for new messages . * /
var firsttime = true
function chat ( ) {
if ( ! password ) return getpwd ( )
show ( "chat" )
if ( firsttime && $ ( '#msgs' ) . is ( ':empty' ) ) {
firsttime = false
notice ( "getting previous messages, please wait …" )
emit ( "messages" )
}
}
/// Login User
/ * * T h i s i s n o t r e a l l y a l o g i n , i t i s j u s t s o m e k i n d o f v a l i d a t i o n .
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
/ * * S h o w s u s e r c r e a t i o n f o r m . O n s u b m i t , a p r i v a t e k e y i s g e n e r a t e d i n
createkeypair ( ) , then login ( ) creates the user . * /
function newuser ( ) {
show ( "newuser" )
}
/// Check if local storage is available
function checkLocalStorage ( ) {
var test = 'test'
try {
localStorage . setItem ( test , test )
localStorage . removeItem ( test )
return true
} catch ( e ) {
show ( "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 ( )
//show("startup")
if ( checkLocalStorage ( ) )
try {
if ( ! userid ( ) ) {
newuser ( )
} else {
login ( )
}
} catch ( m ) {
console . log ( m . stack )
error ( m )
}
}
var safechat = new SafeChat ( )
function init ( ) {
safechat . start ( )
}
function old ( ) {
/// 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 )