export default class ContentController{ /* (1) Construct default attributes * ---------------------------------------------------------*/ constructor(){ /* (1) Websocket re-connection policy */ this.attempt = { max: 5, count: 1, _default_timeout: 500, // in ms get timeout(){ // return timeout + increment it for next time return (this._default_timeout/2) * Math.pow(2, this.count++); }, // same without incrementing (for LOGS) get raw_timeout(){ return (this._default_timeout/2) * Math.pow(2, this.count); } }; /* (2) Manage updates in connection status (ONLINE - OFFLINE) */ window.addEventListener('online', function(){ DEBUG_MOD && console.log(`[NETWORK] online`); DEBUG_MOD && console.log(` + Try to connect`); // wait 500ms for net to be really online setTimeout( this.ws_connect.bind(this), 500); }.bind(this)); window.addEventListener('offline', function(){ DEBUG_MOD && console.log(`[NETWORK] offline`); gs.get.connection = 0; // connection status bar DEBUG_MOD && console.log(` + stop resync-strategy`); csock.onclose = function(){}; // stop propagating recursive ws_connect() }); } /* (2) Channel bindings * ---------------------------------------------------------*/ get cid(){ return gs.get.channel.current; } get cbuf(){ return gs.get.channel._buffer; } /* (3) Room ID binding * ---------------------------------------------------------*/ get rid(){ return gs.get.room.text.current; } get rbuf(){ return gs.get.room._buffer.text; } get messages(){ return this.rbuf.messages; } get members(){ return this.rbuf.members; } // current user data get uid(){ return gs.get.auth.user.uid; } get ubuf(){ return gs.get.auth.user; } /* (5) User getter * * @user_id User id * * @return user User data * ---------------------------------------------------------*/ user(user_id=null){ /* (1) Error: if invalid user_id */ if( isNaN(user_id) ) return {}; /* (2) Error: unknown user */ if( this.cbuf.users == null || this.cbuf.users.length < 1 ) return {}; /* (3) return user data */ for( let u of this.cbuf.users ) if( u.uid === user_id ) return u; /* (4) Error */ return {}; } /* (6) Change username * * @username New username ---------------------------------------------------------*/ change_username(username=null){ /* (1) Error: if invalid user_id */ if( typeof username !== 'string' ) return false; /* (2) Error: unknown user */ if( this.uid == null ) return false; /* (3) Call api UPDATE */ api.call(`PUT /user/${this.uid}`, { username: username }, function(rs){ gs.get.popup.hide(); // manage error if( rs.error !== 0 ) return; // update global username let tmp_user = auth.user; tmp_user.username = username; auth.user = tmp_user; }.bind(this), auth.token); /* (4) Error */ return true; } /* (7) Change password * * @password New password * ---------------------------------------------------------*/ change_password(password=null){ /* (1) Error: if invalid user_id */ if( typeof password !== 'string' ) return false; /* (2) Error: unknown user */ if( this.uid == null ) return false; /* (3) Call api UPDATE */ api.call(`PUT /user/${this.uid}`, { password: password }, () => gs.get.popup.hide(), auth.token); /* (4) Error */ return true; } /* (8) Send message * ---------------------------------------------------------*/ send_message(_msg=null){ /* (1) Manage invalid _msg */ if( typeof _msg !== 'string' || _msg.length <= 0 ) return true; /* (2) Send message */ window.csock.send({ buffer: { rid: this.rid, mid: null, message: _msg }}); return true; } /* (9) Websocket connection / reconnection * ---------------------------------------------------------*/ ws_connect(){ gs.get.connection = 1; // 1. Close websocket if exists if( window.csock instanceof wscd ){ csock.onclose = function(){}; // stop propagating recursive ws_connect() csock.close(); } // 2. Create new connection window.csock = new wscd(`wss://ws.douscord.xdrm.io/channel/${this.cid}`, { token: auth.token }); // 3. Bind events csock.onconnected = () => { DEBUG_MOD && console.log('[WS] connected'); DEBUG_MOD && console.log('[WS] pop stack', csock.stack.map( v => v.buffer ) ); // show connection status gs.get.connection = 2; // reset attempt count gs.get.content.attempt.count = 1; setTimeout( () => { gs.get.connection = null; }, 1500); }; csock.onreceive = gs.get.content.ws_handler.bind({ event: 'receive', class: this }); csock.onclose = gs.get.content.ws_handler.bind({ event: 'close', class: this }); // 4. Start communication csock.bind(); // 5. Send RID (useful when reconnecting, but a doublon at first connection) csock.send({ buffer: { rid: gs.get.room.text.current } }); DEBUG_MOD && console.log('[WS] connecting'); DEBUG_MOD && console.log(` + start resync-strategy`); DEBUG_MOD && console.log(`[WS] push stack`, { rid: gs.get.room.text.current }); } /* (10) Websocket connection manager * * @this.event Event type : * 'close' -> socket closed * 'receive' -> received message * ---------------------------------------------------------*/ ws_handler(_response){ DEBUG_MOD && console.groupCollapsed(`[WS] on${this.event}`); /* (1) Manage error */ if( this.event === null ) return; /* (2) CLOSE event -> reconnect ---------------------------------------------------------*/ if( this.event === 'close' ){ // 1. update connection status bar gs.get.connection = 0; // 2. Do nothing if offline (online trigger will do the job when online again) if( !navigator.onLine ){ DEBUG_MOD && console.log(' + network offline, wait for online trigger'); return DEBUG_MOD && console.groupEnd(); } // 3. if max attempt exceeded -> logout user if( this.class.attempt.count > this.class.attempt.max ){ DEBUG_MOD && console.log(' + max resync offset reached'); DEBUG_MOD && console.log(' + logout'); auth.token = null; gs.get.refresh(); return DEBUG_MOD && console.groupEnd(); } // 4. Try to reconnect DEBUG_MOD && console.log(` + resync (${this.class.attempt.count}/${this.class.attempt.max}) in ${this.class.attempt.raw_timeout}ms`); setTimeout(this.class.ws_connect.bind(this.class), this.class.attempt.timeout); return DEBUG_MOD && console.groupEnd(); } /* (3) RECEIVE event ---------------------------------------------------------*/ if( this.event === 'receive' ){ /* (1) Communication error -> reconnect in 500ms */ if( typeof _response !== 'object' ){ DEBUG_MOD && console.log(` + invalid response: resync in 500ms`); setTimeout(this.class.ws_connect.bind(this.class), 500); return DEBUG_MOD && console.groupEnd(); } /* (2) If message update -> update interface model */ this.class.ws_to_model(_response); return DEBUG_MOD && console.groupEnd(); } } /* (11) MAIN UPDATER * ---------------------------------------------------------*/ ws_to_model(_dat){ DEBUG_MOD && console.group(`room`); /* (1) Manage rooms DELETE ---------------------------------------------------------*/ /* (1) Extract ids */ let room_ids = Object.keys(_dat.room).map( (v) => parseInt(v) ); let current_list = gs.get.room; /* (2) Manage DELETED rooms */ for( let t in current_list ){ for( let ri in current_list[t].list ){ // if existing room is not in received keys -> has been deleted let to_remove = room_ids.indexOf(current_list[t].list[ri].id) < 0; // delete room from interface DEBUG_MOD && to_remove && console.log(` + #${current_list[t].list[ri].id} dropped`); ( to_remove ) && current_list[t].list.splice(ri,1); } } /* (2) Manage rooms CREATE + UPDATE ---------------------------------------------------------*/ for( let ri of room_ids ){ // 1. Extract room data let room = _dat.room[ri]; // 2. if room data is null -> ignore if( room === null ) continue; // 3. Manage room 'type' room.type = (room.type === 0) ? 'text' : 'voice'; // 4. Check whether room already exists in interface let existing_index = -1; main_loop: for( let t in current_list ){ for( let r in current_list[t].list ){ if( current_list[t].list[r].id === ri ){ existing_index = r; break main_loop; } } } // 5. Create room if( existing_index < 0 ){ DEBUG_MOD && console.log(` + #${ri} [${room.type}] room created`); gs.get.room.dump([{ rid: ri, name: room.name, messages: room.messages, members: room.members, type: room.type }], true); continue; } // 5. Update room current_list[room.type].list[existing_index].name = room.name; current_list[room.type].list[existing_index].members = room.members; // 6. We are done if VOICE room if( room.type === 'voice' ) continue; // 7. Push new messages DEBUG_MOD && ( room.messages.length > 0 ) && console.log(` + #${ri} has ${room.messages.length} new message(s)`); for( let m of room.messages ){ current_list[room.type].list[existing_index].messages.push({ uid: m.uid, mid: m.mid, msg: m.content, ts: m.ts }); } // 8. Notification API -> if not current channel if( room.messages.length > 0 && ri !== gs.get.content.rid ){ let title = `Room #${room.name}`; let body = `${room.messages.length} new messages`; new Notification(title, { body: body }); } // 9. If not self on current channel -> notification sound if( room.messages.length > 0 && room.messages[0].uid !== auth.user.uid && ri === gs.get.content.rid ) AudioManager.pop(); } DEBUG_MOD && console.groupEnd(); DEBUG_MOD && console.group(`channel`); DEBUG_MOD && ( _dat.channels.add.length > 0 ) && console.log(` + ${_dat.channels.add.length} new`); DEBUG_MOD && ( _dat.channels.rem.length > 0 ) && console.log(` + ${_dat.channels.rem.length} dropped`); DEBUG_MOD && ( _dat.channels.upd.length > 0 ) && console.log(` + ${_dat.channels.upd.length} updated`); /* (3) Manage channels DELETE ---------------------------------------------------------*/ for( let c of _dat.channels.rem ){ for( let ci in gs.get.channel.list ){ // 1. Local copy channel data let channel = gs.get.channel.list[ci]; // 2. If id matches -> REMOVE if( channel.id === c.id ){ // 2.1. If remove CURRENT channel -> nav to channel 1 if( channel.id === gs.get.content.cid ) gs.get.channel.nav(1); // 2.2. Delete channel DEBUG_MOD && console.log(` + #${channel.id} dropped`); gs.get.channel.list.splice(ci, 1); } } } /* (4) Manage channels CREATE ---------------------------------------------------------*/ for( let c of _dat.channels.add ){ DEBUG_MOD && console.log(` + #${c.id} [/${c.link}] created`); gs.get.channel.dump([{ id: parseInt(c.id), label: c.name, link: c.link }], true); } /* (5) Manage channels UPDATE ---------------------------------------------------------*/ for( let c of _dat.channels.upd ){ for( let ci in gs.get.channel.list ){ // 1. Local copy channel data let channel = gs.get.channel.list[ci]; // 2. If id matches -> UPDATE if( channel.id === c.id ){ DEBUG_MOD && console.log(` + #${c.id} updated`); gs.get.channel.list[ci].label = c.name; gs.get.channel.list[ci].link = c.link; } } } DEBUG_MOD && console.groupEnd(); DEBUG_MOD && console.group('users'); let userset = gs.get.content.cbuf.users; /* (6) Manage users DELETE ---------------------------------------------------------*/ for( let u of _dat.users.rem ){ for( let ui in userset ){ // 1. Local copy user data let user = userset[ui]; // 2. If id matches -> REMOVE if( user.uid === u.id ){ DEBUG_MOD && console.log(` + '${user.username}' dropped`); userset.splice(ui, 1); } } } /* (7) Manage users CREATE ---------------------------------------------------------*/ for( let u of _dat.users.add ){ DEBUG_MOD && console.log(` + '${u.name}' created`); userset.push({ uid: parseInt(u.id), username: u.name }); } /* (8) Manage users UPDATE ---------------------------------------------------------*/ for( let u of _dat.users.upd ){ for( let ui in userset ){ // 1. Local copy user data let user = userset[ui]; // 2. If id matches -> UPDATE if( user.uid === u.id ){ DEBUG_MOD && console.log(` + '${user.username}' renamed '${u.name}'`); userset[ui].username = u.name; } } } DEBUG_MOD && console.groupEnd(); } }