From b2209ff868308b7a9a90cf4bec49a6fa41ab4f14 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 24 Jul 2012 00:22:23 +0200 Subject: [PATCH] Moved to websockets. --- Gemfile | 3 + .../_application_controller.js.coffee | 33 ++- .../_dashboard/activity_stream.js.coffee | 34 +-- .../app/controllers/_dashboard/rss.js.coffee | 28 +-- .../controllers/agent_ticket_view.js.coffee | 2 +- .../app/controllers/getting_started.js.coffee | 17 +- .../app/controllers/login.js.coffee | 26 ++- .../app/controllers/logout.js.coffee | 7 +- .../app/controllers/navigation.js.coffee | 200 +++++++----------- app/assets/javascripts/app/index.js.coffee | 50 ++--- app/assets/javascripts/app/lib/auth.js.coffee | 21 +- .../app/lib/bootstrap-transition.js | 61 ++++++ .../javascripts/app/lib/websocket.js.coffee | 114 ++++++++++ .../javascripts/app/views/error.jst.eco | 17 ++ .../javascripts/app/views/navigation.jst.eco | 12 +- app/controllers/activity_controller.rb | 50 +---- app/controllers/application_controller.rb | 47 +--- app/controllers/recent_viewed_controller.rb | 30 +-- app/controllers/rss_controller.rb | 30 +-- app/models/history.rb | 97 ++++++++- app/models/ticket.rb | 39 ++-- app/models/user.rb | 56 ++++- config/environment.rb | 5 +- lib/rss.rb | 31 +++ lib/web_socket.rb | 175 +++++++++++++++ script/websocket-server.rb | 86 ++++++++ 26 files changed, 868 insertions(+), 403 deletions(-) create mode 100644 app/assets/javascripts/app/lib/bootstrap-transition.js create mode 100644 app/assets/javascripts/app/lib/websocket.js.coffee create mode 100644 app/assets/javascripts/app/views/error.jst.eco create mode 100644 lib/rss.rb create mode 100644 lib/web_socket.rb create mode 100644 script/websocket-server.rb diff --git a/Gemfile b/Gemfile index 3fe9f1091..e1ac40aa9 100644 --- a/Gemfile +++ b/Gemfile @@ -58,3 +58,6 @@ gem 'simple-rss' # To use debugger # gem 'ruby-debug' +# event machine +gem 'eventmachine' +gem 'em-websocket' diff --git a/app/assets/javascripts/app/controllers/_application_controller.js.coffee b/app/assets/javascripts/app/controllers/_application_controller.js.coffee index fa835db0d..291daf466 100644 --- a/app/assets/javascripts/app/controllers/_application_controller.js.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller.js.coffee @@ -2,7 +2,8 @@ class App.Controller extends Spine.Controller # add @title methode to set title title: (name) -> - $('html head title').html( Config.product_name + ' - ' + Ti(name) ) +# $('html head title').html( Config.product_name + ' - ' + Ti(name) ) + document.title = Config.product_name + ' - ' + Ti(name) # add @notify methode to create notification notify: (data) -> @@ -545,6 +546,13 @@ class App.Controller extends Spine.Controller return newInstance + clearInterval: (interval_id) => + # check global var + if !@intervalID + @intervalID = {} + + clearInterval( @intervalID[interval_id] ) if @intervalID[interval_id] + interval: (action, interval, interval_id) => # check global var @@ -778,11 +786,14 @@ class App.ControllerModal extends App.Controller super(options) modalShow: (params) => - @el.modal({ + defaults = { backdrop: true, keyboard: true, - show: true - }) + show: true, + } + data = $.extend({}, defaults, params) + @el.modal(data) + @el.bind('hidden', => # navigate back to home page @@ -805,3 +816,17 @@ class App.ControllerModal extends App.Controller submit: (e) => e.preventDefault() @log 'You need to implement your own "submit" method!' + +class App.ErrorModal extends App.ControllerModal + constructor: -> + super + @render() + + render: -> + @html App.view('error')( + message: @message + ) + @modalShow( + backdrop: false, + keyboard: false, + ) diff --git a/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee b/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee index 4df1ce12e..9ec2ac570 100644 --- a/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee +++ b/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee @@ -9,25 +9,29 @@ class App.DashboardActivityStream extends App.Controller @items = [] # refresh list ever 140 sec. - @interval( @fetch, 1400000, 'dashboard_activity_stream' ) - +# @interval( @fetch, 1400000, 'dashboard_activity_stream' ) + @fetch() + Spine.bind 'activity_stream_rebuild', (data) => + @log 'a_stream', data + @fetch() + fetch: => # use cache of first page - if window.LastRefresh[ 'dashboard_activity_stream' ] - @render( window.LastRefresh[ 'dashboard_activity_stream' ] ) + if window.LastRefresh[ 'activity_stream' ] + @load( window.LastRefresh[ 'activity_stream' ] ) - # get data - App.Com.ajax( - id: 'dashoard_activity_stream', - type: 'GET', - url: '/activity_stream', - data: { - limit: @limit, - } - processData: true, - success: @load - ) +# # get data +# App.Com.ajax( +# id: 'dashoard_activity_stream', +# type: 'GET', +# url: '/activity_stream', +# data: { +# limit: @limit, +# } +# processData: true, +# success: @load +# ) load: (data) => items = data.activity_stream diff --git a/app/assets/javascripts/app/controllers/_dashboard/rss.js.coffee b/app/assets/javascripts/app/controllers/_dashboard/rss.js.coffee index 3b5ec613c..111cdaf07 100644 --- a/app/assets/javascripts/app/controllers/_dashboard/rss.js.coffee +++ b/app/assets/javascripts/app/controllers/_dashboard/rss.js.coffee @@ -3,36 +3,18 @@ $ = jQuery.sub() class App.DashboardRss extends App.Controller constructor: -> super - - - # refresh list ever 600 sec. - @interval( @fetch, 6000000, 'dashboard_rss' ) - fetch: => + # refresh list ever 600 sec. + Spine.bind 'rss_rebuild', (data) => + @load(data) # use cache of first page if window.LastRefresh[ 'dashboard_rss' ] - @render( window.LastRefresh[ 'dashboard_rss' ] ) - - # get data - App.Com.ajax( - id: 'dashboard_rss', - type: 'GET', - url: '/rss_fetch', - data: { - limit: @limit, - url: @url, - } - processData: true, - success: @load - ) + @load( window.LastRefresh[ 'dashboard_rss' ] ) load: (data) => items = data.items || [] - - # set cache - window.LastRefresh[ 'dashboard_rss' ] = items - + @head = data.head || '?' @render(items) render: (items) -> diff --git a/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee index 45a29b77d..fa592c37a 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee @@ -271,7 +271,7 @@ class Index extends App.Controller @tickets = [] # rebuild navbar with updated ticket count of overviews - Spine.trigger 'navupdate_remote' + App.WebSocket.send( event: 'navupdate_ticket_overview' ) # fetch overview data again @fetch() diff --git a/app/assets/javascripts/app/controllers/getting_started.js.coffee b/app/assets/javascripts/app/controllers/getting_started.js.coffee index 185aad2d2..cd2c78c8a 100644 --- a/app/assets/javascripts/app/controllers/getting_started.js.coffee +++ b/app/assets/javascripts/app/controllers/getting_started.js.coffee @@ -9,13 +9,12 @@ class Index extends App.Controller constructor: -> super - + # set title @title 'Get Started' @navupdate '#get_started' - + @master_user = 0 - # @render() @fetch() @@ -43,7 +42,7 @@ class Index extends App.Controller ) render: -> - + # check authentication, redirect to login if master user already exists if !@master_user && !@authenticate() @navigate '#login' @@ -59,7 +58,7 @@ class Index extends App.Controller submit: (e) -> e.preventDefault() @params = @formParam(e.target) - + # if no login is given, use emails as fallback if !@params.login && @params.email @params.login = @params.email @@ -102,28 +101,24 @@ class Index extends App.Controller # rerender page @render() - # error: => # @modalHide() ) - relogin: (data, status, xhr) => @log 'login:success', data # login check App.Auth.loginCheck() - + # add notify Spine.trigger 'notify:removeall' # @notify # type: 'success', # msg: 'Thanks for joining. Email sent to "' + @params.email + '". Please verify your email address.' - + @el.find('.master_user').fadeOut('slow', => @el.find('.agent_user').fadeIn() ) - - Config.Routes['getting_started'] = Index diff --git a/app/assets/javascripts/app/controllers/login.js.coffee b/app/assets/javascripts/app/controllers/login.js.coffee index 4c15d2319..2a20aa0e4 100644 --- a/app/assets/javascripts/app/controllers/login.js.coffee +++ b/app/assets/javascripts/app/controllers/login.js.coffee @@ -3,7 +3,7 @@ $ = jQuery.sub() class Index extends App.Controller events: 'submit #login': 'login', - + constructor: -> super @title 'Sign in' @@ -37,12 +37,12 @@ class Index extends App.Controller for key, provider of auth_provider_all if Config[provider.config] is true || Config[provider.config] is "true" auth_providers.push provider - + @html App.view('login')( item: data, auth_providers: auth_providers, ) - + # set focus if !$(@el).find('[name="username"]').val() $(@el).find('[name="username"]').focus() @@ -55,17 +55,17 @@ class Index extends App.Controller login: (e) -> e.preventDefault() params = @formParam(e.target) - + # remember username @username = params['username'] - + # session create with login/password App.Auth.login( data: params, success: @success error: @error, ) - + success: (data, status, xhr) => @log 'login:success', data @@ -85,10 +85,14 @@ class Index extends App.Controller for key, value of data.default_collections App[key].refresh( value, options: { clear: true } ) + # rebuild navbar with user data Spine.trigger 'navrebuild', data.session - # rebuild navbar with updated ticket count of overviews - Spine.trigger 'navupdate_remote' + # update websocked auth info + App.WebSocket.auth() + + # rebuild navbar with ticket overview counter + App.WebSocket.send( event: 'navupdate_ticket_overview' ) # add notify Spine.trigger 'notify:removeall' @@ -100,7 +104,7 @@ class Index extends App.Controller # redirect to # if window.Config['requested_url'] isnt '' @navigate window.Config['requested_url'] - + # reset window.Config['requested_url'] = '' else @@ -108,14 +112,14 @@ class Index extends App.Controller error: (xhr, statusText, error) => console.log 'login:error' - + # add notify Spine.trigger 'notify:removeall' Spine.trigger 'notify', { type: 'error', msg: T('Wrong Username and Password combination.'), } - + # rerender login page @render( username: @username diff --git a/app/assets/javascripts/app/controllers/logout.js.coffee b/app/assets/javascripts/app/controllers/logout.js.coffee index ef6ef6d98..e3ff8aa7e 100644 --- a/app/assets/javascripts/app/controllers/logout.js.coffee +++ b/app/assets/javascripts/app/controllers/logout.js.coffee @@ -4,11 +4,10 @@ class Index extends Spine.Controller constructor: -> super - @signout() - + signout: -> - + # remove remote session App.Auth.logout() @@ -17,7 +16,7 @@ class Index extends Spine.Controller window.Session = {} @log 'Session', window.Session Spine.trigger 'navrebuild' - + # redirect to login @navigate 'login' diff --git a/app/assets/javascripts/app/controllers/navigation.js.coffee b/app/assets/javascripts/app/controllers/navigation.js.coffee index f71c179f2..dcf281cf1 100644 --- a/app/assets/javascripts/app/controllers/navigation.js.coffee +++ b/app/assets/javascripts/app/controllers/navigation.js.coffee @@ -5,36 +5,47 @@ class App.Navigation extends App.Controller super @log 'nav...' @render() - - sync_ticket_overview = => - @interval( @ticket_overview, 30000, 'nav_ticket_overview' ) - sync_recent_viewed = => - @interval( @recent_viewed, 40000, 'nav_recent_viewed' ) - + # update selected item Spine.bind 'navupdate', (data) => @update(arguments[0]) - + + # rebuild nav bar with given user data Spine.bind 'navrebuild', (user) => @log 'navbarrebuild', user @render(user) - Spine.bind 'navupdate_remote', (user) => - @log 'navupdate_remote' - @delay( sync_ticket_overview, 500 ) - @delay( sync_recent_viewed, 1000 ) - - # rerender if new overview data is there - @delay( sync_ticket_overview, 800 ) - @delay( sync_recent_viewed, 1000 ) - + # rebuild ticket overview data + Spine.bind 'navupdate_ticket_overview', (data) => + @ticket_overview_build(data) + + # rebuild recent viewd data + Spine.bind 'update_recent_viewed', (data) => + @recent_viewed_build(data) + render: (user) -> nav_left = @getItems( navbar: Config.NavBar ) nav_right = @getItems( navbar: Config.NavBarRight ) + # get open tabs to repopen on rerender + open_tab = {} + @el.find('.open').children('a').each( (i,d) => + href = $(d).attr('href') + open_tab[href] = true + ) + + # get active tabs to reactivate on rerender + active_tab = {} + @el.find('.active').children('a').each( (i,d) => + href = $(d).attr('href') + active_tab[href] = true + ) + @html App.view('navigation')( navbar_left: nav_left, navbar_right: nav_right, + open_tab: open_tab, + active_tab: active_tab, user: user, ) @@ -119,118 +130,65 @@ class App.Navigation extends App.Controller update: (url) => @el.find('li').removeClass('active') -# if url isnt '#' @el.find("[href=\"#{url}\"]").parents('li').addClass('active') -# @el.find("[href*=\"#{url}\"]").parents('li').addClass('active') - # get data - ticket_overview: => + ticket_overview_build: (data) => - # do no load and rerender if sub-menu is open - open = @el.find('.open').val() - if open isnt undefined - return - - # do no load and rerender if user is not logged in - if !window.Session['id'] - return + # remove old views + for key of Config.NavBar + if Config.NavBar[key].parent is '#ticket/view' + delete Config.NavBar[key] - # only of lod request is already done + # add new views + for item in data + Config.NavBar['TicketOverview' + item.url] = { + prio: item.prio, + parent: '#ticket/view', + name: item.name, + count: item.count, + target: '#ticket/view/' + item.url, + role: ['Agent'], + } - if !@req_overview - @req_overview = App.Com.ajax( - id: 'navbar_ticket_overviews', - type: 'GET', - url: '/ticket_overviews', - data: {}, - processData: true, - success: (data, status, xhr) => - - # remove old views - for key of Config.NavBar - if Config.NavBar[key].parent is '#ticket/view' - delete Config.NavBar[key] - - # add new views - for item in data - Config.NavBar['TicketOverview' + item.url] = { - prio: item.prio, - parent: '#ticket/view', - name: item.name, - count: item.count, - target: '#ticket/view/' + item.url, - role: ['Agent'], - } - - # rebuild navbar - Spine.trigger 'navrebuild', window.Session + # rebuild navbar + Spine.trigger 'navrebuild', window.Session - # reset ajax call - @req_overview = undefined - ) + recent_viewed_build: (data) => - # get data - recent_viewed: => + items = data.recent_viewed - # do no load and rerender if sub-menu is open - open = @el.find('.open').val() - if open isnt undefined - return - - # do no load and rerender if user is not logged in - if !window.Session['id'] - return + # load user collection + @loadCollection( type: 'User', data: data.users ) - # only of lod request is already done - if !@req_recent_viewed - @req_recent_viewed = App.Com.ajax( - id: 'navbar_recent_viewed', - type: 'GET', - url: '/recent_viewed', - data: { - limit: 5, - } - processData: true, - success: (data, status, xhr) => - - items = data.recent_viewed - - # load user collection - @loadCollection( type: 'User', data: data.users ) - - # load ticket collection - @loadCollection( type: 'Ticket', data: data.tickets ) - - # remove old views - for key of Config.NavBarRight - if Config.NavBarRight[key].parent is '#current_user' - part = Config.NavBarRight[key].target.split '::' - if part is 'RecendViewed' - delete Config.NavBarRight[key] - - # add new views - prio = 5000 - for item in items - divider = false - navheader = false - if prio is 5000 - divider = true - navheader = 'Recent Viewed' - ticket = App.Ticket.find(item.o_id) - prio++ - Config.NavBarRight['RecendViewed::' + ticket.id] = { - prio: prio, - parent: '#current_user', - name: item.history_object.name + ' (' + ticket.title + ')', - target: '#ticket/zoom/' + ticket.id, - role: ['Agent'], - divider: divider, - navheader: navheader - } - - # rebuild navbar - Spine.trigger 'navrebuild', window.Session + # load ticket collection + @loadCollection( type: 'Ticket', data: data.tickets ) - # reset ajax call - @req_recent_viewed = undefined - ) + # remove old views + for key of Config.NavBarRight + if Config.NavBarRight[key].parent is '#current_user' + part = key.split '::' + if part[0] is 'RecendViewed' + delete Config.NavBarRight[key] + + # add new views + prio = 8000 + for item in items + divider = false + navheader = false + if prio is 8000 + divider = true + navheader = 'Recent Viewed' + ticket = App.Ticket.find(item.o_id) + prio++ + Config.NavBarRight['RecendViewed::' + ticket.id + '-' + prio ] = { + prio: prio, + parent: '#current_user', + name: item.history_object.name + ' (' + ticket.title + ')', + target: '#ticket/zoom/' + ticket.id, + role: ['Agent'], + divider: divider, + navheader: navheader + } + + # rebuild navbar + Spine.trigger 'navrebuild', window.Session \ No newline at end of file diff --git a/app/assets/javascripts/app/index.js.coffee b/app/assets/javascripts/app/index.js.coffee index 88d8ac55b..b9edf83cc 100644 --- a/app/assets/javascripts/app/index.js.coffee +++ b/app/assets/javascripts/app/index.js.coffee @@ -11,6 +11,7 @@ #= require ./lib/bootstrap-popover.js #= require ./lib/bootstrap-modal.js #= require ./lib/bootstrap-tab.js +#= require ./lib/bootstrap-transition.js #= require ./lib/underscore.coffee #= require ./lib/ba-linkify.js @@ -23,6 +24,7 @@ #not_used= require_tree ./lib #= require_self #= require ./lib/ajax.js.coffee +#= require ./lib/websocket.js.coffee #= require ./lib/auth.js.coffee #= require ./lib/i18n.js.coffee #= require_tree ./models @@ -57,13 +59,15 @@ Config2.set( 'a', 123) console.log '1112222', Config2.get( 'a') ### - class App.Run extends Spine.Controller constructor: -> super - @log 'RUN app'#, @ + @log 'RUN app' @el = $('#app') + # create web socket connection + App.WebSocket.connect() + # init of i18n new App.i18n @@ -84,27 +88,6 @@ class App.Run extends Spine.Controller window.Session['UISelection'] = @getSelected() + '' ) -# @ws = new WebSocket("ws://localhost:3001/"); - - # Set event handlers. -# @ws.onopen = -> -# console.log("onopen") - -# @ws.onmessage = (e) -> - # e.data contains received string. -# console.log("onmessage: " + e.data) -# eval e.data - -# Spine.bind 'ws:send', (data) => -# @log 'ws:send', data -# @ws.send(data); - -# @ws.onclose = -> -# console.log("onclose") - -# @ws.onerror = -> -# console.log("onerror") - getSelected: -> text = ''; if window.getSelection @@ -125,24 +108,35 @@ class App.Content extends Spine.Controller for route, callback of Config.Routes do (route, callback) => @route(route, (params) -> + + # remember current controller Config['ActiveController'] = route - Spine.trigger( 'ws:send', JSON.stringify( { action: 'active_controller', controller: route, params: params } ) ) + + # send current controller + params_only = {} + for i of params + if typeof params[i] isnt 'object' + params_only[i] = params[i] + App.WebSocket.send( + action: 'active_controller', + controller: route, + params: params_only, + ) # unbind in controller area @el.unbind() @el.undelegate() - + # remove waypoints $('footer').waypoint('remove') - + params.el = @el - params.auth = @auth new callback( params ) # scroll to top # window.scrollTo(0,0) ) - Spine.Route.setup() + Spine.Route.setup() window.App = App \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/auth.js.coffee b/app/assets/javascripts/app/lib/auth.js.coffee index 5296767ab..b93ca7cc8 100644 --- a/app/assets/javascripts/app/lib/auth.js.coffee +++ b/app/assets/javascripts/app/lib/auth.js.coffee @@ -34,6 +34,9 @@ class App.Auth # empty session window.Session = {} + # update websocked auth info + App.WebSocket.auth() + # rebuild navbar with new navbar items Spine.trigger 'navrebuild' @@ -50,6 +53,9 @@ class App.Auth # store user data for key, value of data.session window.Session[key] = value + + # update websocked auth info + App.WebSocket.auth() # refresh/load default collections for key, value of data.default_collections @@ -61,12 +67,14 @@ class App.Auth # rebuild navbar with updated ticket count of overviews Spine.trigger 'navupdate_remote' - error: (xhr, statusText, error) => console.log 'loginCheck:error'#, error, statusText, xhr.statusCode - + # empty session window.Session = {} + + # update websocked auth info + App.WebSocket.auth() ) @logout: -> @@ -75,4 +83,13 @@ class App.Auth id: 'logout', type: 'DELETE', url: '/signout', + success: => + + # update websocked auth info + App.WebSocket.auth() + + error: (xhr, statusText, error) => + + # update websocked auth info + App.WebSocket.auth() ) \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/bootstrap-transition.js b/app/assets/javascripts/app/lib/bootstrap-transition.js new file mode 100644 index 000000000..534182622 --- /dev/null +++ b/app/assets/javascripts/app/lib/bootstrap-transition.js @@ -0,0 +1,61 @@ +/* =================================================== + * bootstrap-transition.js v2.0.4 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + $(function () { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd' + , 'msTransition' : 'MSTransitionEnd' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.jQuery); \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/websocket.js.coffee b/app/assets/javascripts/app/lib/websocket.js.coffee new file mode 100644 index 000000000..41b8dbe87 --- /dev/null +++ b/app/assets/javascripts/app/lib/websocket.js.coffee @@ -0,0 +1,114 @@ +$ = jQuery.sub() + +class App.WebSocket + _instance = undefined # Must be declared here to force the closure on the class + @connect: (args) -> # Must be a static method + if _instance == undefined + _instance ?= new _Singleton + _instance + + @send: (args) -> # Must be a static method + @connect() + _instance.send(args) + + @auth: (args) -> # Must be a static method + @connect() + _instance.auth(args) + +# The actual Singleton class +class _Singleton extends Spine.Controller + queue: [] + + constructor: (@args) -> + @connect() + + send: (data) => + console.log 'ws:send trying', data, @ws, @ws.readyState + + # A value of 0 indicates that the connection has not yet been established. + # A value of 1 indicates that the connection is established and communication is possible. + # A value of 2 indicates that the connection is going through the closing handshake. + # A value of 3 indicates that the connection has been closed or could not be opened. + if @ws.readyState is 0 + @queue.push data + else + console.log( 'ws:send', data ) + string = JSON.stringify( data ) + @ws.send(string) + + auth: (data) => + + # logon websocket + data = { + action: 'login', + session: window.Session + } + @send(data) + + close: => + @ws.close() + + connect: => +# console.log '------------ws connect....--------------' + + if !window.WebSocket + @error = new App.ErrorModal( + message: 'Sorry, no websocket support!' + ) + return + + @ws = new window.WebSocket( "ws://" + window.location.hostname + ":6042/" ) + + # Set event handlers. + @ws.onopen = => + console.log( "onopen" ) + + # close error message if exists + if @error + @error.modalHide() + @error = undefined + + @auth() + + # empty queue + for item in @queue + console.log( 'ws:send queue', item ) + @send(item) + @queue = [] + + @ws.onmessage = (e) -> + pipe = JSON.parse( e.data ) +# console.log( "ws:onmessage", pipe ) + + # go through all blocks + for item in pipe + + # fill collection + if item['collection'] + console.log( "ws:onmessage collection:" + item['collection'] ) + window.LastRefresh[ item['collection'] ] = item['data'] + + # fire event + if item['event'] + console.log( "ws:onmessage event:" + item['event'] ) + Spine.trigger( item['event'], item['data'] ) + + # bind to send messages + Spine.bind 'ws:send', (data) => + @send(data) + + @ws.onclose = (e) => + console.log( "onclose", e ) + + # show error message + if !@error + @error = new App.ErrorModal( + message: 'No connection to websocket, trying to reconnect...' + ) + + # try reconnect after 5 sec. + @delay @connect, 5000 + + @ws.onerror = -> + console.log( "onerror" ) + diff --git a/app/assets/javascripts/app/views/error.jst.eco b/app/assets/javascripts/app/views/error.jst.eco new file mode 100644 index 000000000..dd82e6cca --- /dev/null +++ b/app/assets/javascripts/app/views/error.jst.eco @@ -0,0 +1,17 @@ + + + diff --git a/app/assets/javascripts/app/views/navigation.jst.eco b/app/assets/javascripts/app/views/navigation.jst.eco index 028c8cd89..6d18fc057 100644 --- a/app/assets/javascripts/app/views/navigation.jst.eco +++ b/app/assets/javascripts/app/views/navigation.jst.eco @@ -5,7 +5,7 @@ @@ -32,7 +32,7 @@ diff --git a/app/controllers/activity_controller.rb b/app/controllers/activity_controller.rb index f94a8148f..0ebb49df2 100644 --- a/app/controllers/activity_controller.rb +++ b/app/controllers/activity_controller.rb @@ -3,56 +3,10 @@ class ActivityController < ApplicationController # GET /activity_stream def activity_stream - activity_stream = History.activity_stream(current_user, params[:limit]) - - # get related users - users = {} - tickets = [] - articles = [] - activity_stream.each {|item| - - # load article ids - if item['history_object'] == 'Ticket' - ticket = Ticket.find( item['o_id'] ).attributes - tickets.push ticket - - # load users - if !users[ ticket['owner_id'] ] - users[ ticket['owner_id'] ] = user_data_full( ticket['owner_id'] ) - end - if !users[ ticket['customer_id'] ] - users[ ticket['customer_id'] ] = user_data_full( ticket['customer_id'] ) - end - end - if item['history_object'] == 'Ticket::Article' - article = Ticket::Article.find( item['o_id'] ).attributes - if !article['subject'] || article['subject'] == '' - article['subject'] = Ticket.find( article['ticket_id'] ).title - end - articles.push article - - # load users - if !users[ article['created_by_id'] ] - users[ article['created_by_id'] ] = user_data_full( article['created_by_id'] ) - end - end - if item['history_object'] == 'User' - users[ item['o_id'] ] = user_data_full( item['o_id'] ) - end - - # load users - if !users[ item['created_by_id'] ] - users[ item['created_by_id'] ] = user_data_full( item['created_by_id'] ) - end - } + activity_stream = History.activity_stream_fulldata(current_user, params[:limit]) # return result - render :json => { - :activity_stream => activity_stream, - :tickets => tickets, - :articles => articles, - :users => users, - } + render :json => activity_stream end end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1aceb9e50..47111007c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -161,52 +161,7 @@ class ApplicationController < ActionController::Base end def user_data_full (user_id) - - # get user - user = User.find_fulldata(user_id) - - # do not show password - user['password'] = '' - - - # TEMP: compat. reasons - user['preferences'] = {} if user['preferences'] == nil - - items = [] - if user['preferences'][:tickets_open].to_i > 0 - item = { - :url => '', - :name => 'open', - :count => user['preferences'][:tickets_open] || 0, - :title => 'Open Tickets', - :class => 'user-tickets', - :data => 'open' - } - items.push item - end - if user['preferences'][:tickets_closed].to_i > 0 - item = { - :url => '', - :name => 'closed', - :count => user['preferences'][:tickets_closed] || 0, - :title => 'Closed Tickets', - :class => 'user-tickets', - :data => 'closed' - } - items.push item - end - - # show linked topics and items - if items.count > 0 - topic = { - :title => 'Tickets', - :items => items, - } - user['links'] = [] - user['links'].push topic - end - - return user + user = User.user_data_full(user_id) end end diff --git a/app/controllers/recent_viewed_controller.rb b/app/controllers/recent_viewed_controller.rb index 8d2a944d2..d20ebf151 100644 --- a/app/controllers/recent_viewed_controller.rb +++ b/app/controllers/recent_viewed_controller.rb @@ -3,36 +3,10 @@ class RecentViewedController < ApplicationController # GET /recent_viewed def recent_viewed - recent_viewed = History.recent_viewed(current_user) - - # get related users - users = {} - tickets = [] - recent_viewed.each {|item| - - # load article ids -# if item.history_object == 'Ticket' - tickets.push Ticket.find( item['o_id'] ).attributes -# end -# if item.history_object 'Ticket::Article' -# tickets.push Ticket::Article.find(item.o_id) -# end -# if item.history_object 'User' -# tickets.push User.find(item.o_id) -# end - - # load users - if !users[ item['created_by_id'] ] - users[ item['created_by_id'] ] = user_data_full( item['created_by_id'] ) - end - } + recent_viewed = History.recent_viewed_fulldata(current_user) # return result - render :json => { - :recent_viewed => recent_viewed, - :tickets => tickets, - :users => users, - } + render :json => recent_viewed end end \ No newline at end of file diff --git a/app/controllers/rss_controller.rb b/app/controllers/rss_controller.rb index c0f76eb81..d50db079b 100644 --- a/app/controllers/rss_controller.rb +++ b/app/controllers/rss_controller.rb @@ -3,33 +3,9 @@ class RssController < ApplicationController # GET /rss_fetch def fetch - url = params[:url] - limit = params[:limit] || 10 - - cache_key = 'rss::' + url - items = Rails.cache.read( cache_key ) - if !items - response = Net::HTTP.get_response( URI.parse(url) ) - if response.code.to_s != '200' - render :json => { :message => "failed to fetch #{url}, code: #{response.code}"}, :status => :unprocessable_entity - return - end - rss = SimpleRSS.parse response.body - items = [] - fetched = 0 - rss.items.each { |item| - record = { - :id => item.id, - :title => item.title, - :summary => item.summary, - :link => item.link, - :published => item.published - } - items.push record - fetched += 1 - break item if fetched == limit.to_i - } - Rails.cache.write( cache_key, items, :expires_in => 4.hours ) + items = RSS.fetch(params[:url], params[:limit]) + if items == nil + render :json => { :message => "failed to fetch #{ params[:url] }", :status => :unprocessable_entity } end render :json => { :items => items } end diff --git a/app/models/history.rb b/app/models/history.rb index 79057f5fc..1025652f7 100644 --- a/app/models/history.rb +++ b/app/models/history.rb @@ -136,14 +136,65 @@ class History < ActiveRecord::Base end return datas end + def self.activity_stream_fulldata(user, limit = 10) + activity_stream = History.activity_stream( user, limit ) + + # get related users + users = {} + tickets = [] + articles = [] + activity_stream.each {|item| + + # load article ids + if item['history_object'] == 'Ticket' + ticket = Ticket.find( item['o_id'] ).attributes + tickets.push ticket + + # load users + if !users[ ticket['owner_id'] ] + users[ ticket['owner_id'] ] = User.user_data_full( ticket['owner_id'] ) + end + if !users[ ticket['customer_id'] ] + users[ ticket['customer_id'] ] = User.user_data_full( ticket['customer_id'] ) + end + end + if item['history_object'] == 'Ticket::Article' + article = Ticket::Article.find( item['o_id'] ).attributes + if !article['subject'] || article['subject'] == '' + article['subject'] = Ticket.find( article['ticket_id'] ).title + end + articles.push article + + # load users + if !users[ article['created_by_id'] ] + users[ article['created_by_id'] ] = User.user_data_full( article['created_by_id'] ) + end + end + if item['history_object'] == 'User' + users[ item['o_id'] ] = User.user_data_full( item['o_id'] ) + end + + # load users + if !users[ item['created_by_id'] ] + users[ item['created_by_id'] ] = User.user_data_full( item['created_by_id'] ) + end + } + + return { + :activity_stream => activity_stream, + :tickets => tickets, + :articles => articles, + :users => users, + } + end def self.recent_viewed(user) # g = Group.where( :active => true ).joins(:users).where( 'users.id' => user.id ) - stream = History.select("distinct(histories.o_id), created_by_id, history_attribute_id, history_type_id, history_object_id, value_from, value_to"). + stream = History.select("distinct(o_id), created_by_id, history_type_id, history_object_id, created_at"). where( :history_object_id => History::Object.where( :name => 'Ticket').first.id ). - where( :history_type_id => History::Type.where( :name => ['viewed']) ). + where( :history_type_id => History::Type.where( :name => ['viewed'] ) ). where( :created_by_id => user.id ). - order('created_at DESC, id DESC'). + order('created_at DESC, id ASC'). limit(10) datas = [] stream.each do |item| @@ -153,9 +204,49 @@ class History < ActiveRecord::Base datas.push data # item['history_attribute'] = item.history_attribute end +# puts 'pppppppppp' +# puts datas.inspect return datas end + def self.recent_viewed_fulldata(user) + recent_viewed = History.recent_viewed(user) + + # get related users + users = {} + tickets = [] + recent_viewed.each {|item| + + # load article ids +# if item.history_object == 'Ticket' + ticket = Ticket.find( item['o_id'] ).attributes + tickets.push ticket +# end +# if item.history_object 'Ticket::Article' +# tickets.push Ticket::Article.find(item.o_id) +# end +# if item.history_object 'User' +# tickets.push User.find(item.o_id) +# end + + # load users + if !users[ ticket['owner_id'] ] + users[ ticket['owner_id'] ] = User.user_data_full( ticket['owner_id'] ) + end + if !users[ ticket['created_by_id'] ] + users[ ticket['created_by_id'] ] = User.user_data_full( ticket['created_by_id'] ) + end + if !users[ item['created_by_id'] ] + users[ item['created_by_id'] ] = User.user_data_full( item['created_by_id'] ) + end + } + return { + :recent_viewed => recent_viewed, + :tickets => tickets, + :users => users, + } + end + private def check_type puts '--------------' diff --git a/app/models/ticket.rb b/app/models/ticket.rb index b0697bd15..6cbe3fb9a 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -123,7 +123,6 @@ class Ticket < ActiveRecord::Base return subject end - # Ticket.overview( # :view => 'some_view_url', # :current_user_id => 123, @@ -142,7 +141,7 @@ class Ticket < ActiveRecord::Base overview.condition[item] = 'current_user.id' end } - + # remember selected view if data[:view] && data[:view] == overview.meta[:url] overview_selected = overview @@ -192,10 +191,10 @@ class Ticket < ActiveRecord::Base # get count count = Ticket.where( :group_id => group_ids ).where( overview.condition ).count() - + # get meta info all = overview.meta - + # push to result data result.push all.merge( { :count => count } ) } @@ -261,7 +260,7 @@ class Ticket < ActiveRecord::Base end end def destroy_dependencies - + # delete history History.history_destroy( 'Ticket', self.id ) @@ -293,14 +292,14 @@ class Ticket < ActiveRecord::Base belongs_to :ticket_article_type, :class_name => 'Ticket::Article::Type' belongs_to :ticket_article_sender, :class_name => 'Ticket::Article::Sender' belongs_to :created_by, :class_name => 'User' - + private def fillup - + # if sender is customer, do not change anything sender = Ticket::Article::Sender.where( :id => self.ticket_article_sender_id ).first return if sender == nil || sender['name'] == 'Customer' - + type = Ticket::Article::Type.where( :id => self.ticket_article_type_id ).first ticket = Ticket.find(self.ticket_id) @@ -337,10 +336,10 @@ class Ticket < ActiveRecord::Base end end def attachment_check - + # do nothing if no attachment exists return 1 if self['attachments'] == nil - + # store attachments article_store = [] self.attachments.each do |attachment| @@ -363,9 +362,9 @@ class Ticket < ActiveRecord::Base type = Ticket::Article::Type.where( :id => self.ticket_article_type_id ).first ticket = Ticket.find(self.ticket_id) - + # if sender is agent or system - + # create tweet if type['name'] == 'twitter direct-message' || type['name'] == 'twitter status' a = Channel::Twitter2.new @@ -381,7 +380,7 @@ class Ticket < ActiveRecord::Base self.message_id = message.id self.save end - + # post facebook comment if type['name'] == 'facebook' a = Channel::Facebook.new @@ -393,13 +392,13 @@ class Ticket < ActiveRecord::Base } ) end - + # send email if type['name'] == 'email' - + # build subject subject = ticket.subject_build(self.subject) - + # send email a = Channel::IMAP.new message = a.send( @@ -414,7 +413,7 @@ class Ticket < ActiveRecord::Base :attachments => self.attachments } ) - + # store mail plain Store.add( :object => 'Ticket::Article::Mail', @@ -428,12 +427,12 @@ class Ticket < ActiveRecord::Base class Flag < ActiveRecord::Base end - + class Sender < ActiveRecord::Base end - + class Type < ActiveRecord::Base end end -end +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 55915da5b..ca14d2c69 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -142,14 +142,14 @@ Your #{config.product_name} Team end def self.password_reset_via_token(token,password) - + # check token token = Token.check( :action => 'PasswordReset', :name => token ) return if !token - + # reset password token.user.update_attributes( :password => password ) - + # delete token token.delete token.save @@ -176,7 +176,7 @@ Your #{config.product_name} Team :username => authorization[:username] } end - + # set roles roles = [] user.roles.select('id, name').where( :active => true ).each { |role| @@ -206,6 +206,54 @@ Your #{config.product_name} Team return data end + + def self.user_data_full (user_id) + + # get user + user = User.find_fulldata(user_id) + + # do not show password + user['password'] = '' + + # TEMP: compat. reasons + user['preferences'] = {} if user['preferences'] == nil + + items = [] + if user['preferences'][:tickets_open].to_i > 0 + item = { + :url => '', + :name => 'open', + :count => user['preferences'][:tickets_open] || 0, + :title => 'Open Tickets', + :class => 'user-tickets', + :data => 'open' + } + items.push item + end + if user['preferences'][:tickets_closed].to_i > 0 + item = { + :url => '', + :name => 'closed', + :count => user['preferences'][:tickets_closed] || 0, + :title => 'Closed Tickets', + :class => 'user-tickets', + :data => 'closed' + } + items.push item + end + + # show linked topics and items + if items.count > 0 + topic = { + :title => 'Tickets', + :items => items, + } + user['links'] = [] + user['links'].push topic + end + + return user + end # update all users geo data def self.geo_update_all diff --git a/config/environment.rb b/config/environment.rb index 265657054..80ed9c829 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -13,8 +13,11 @@ require 'google_oauth2_database' # load notification factory (replace all tags) require 'notification_factory' -# load gmaps lookup +# load lib require 'gmaps' +require 'rss' + +require 'web_socket' # Initialize the rails application Zammad::Application.initialize! diff --git a/lib/rss.rb b/lib/rss.rb new file mode 100644 index 000000000..ac9f2c823 --- /dev/null +++ b/lib/rss.rb @@ -0,0 +1,31 @@ +module RSS + def self.fetch(url, limit = 10) + cache_key = 'rss::' + url + items = Rails.cache.read( cache_key ) + if !items + puts 'fetch rss...' + response = Net::HTTP.get_response( URI.parse(url) ) + if response.code.to_s != '200' + return + end + rss = SimpleRSS.parse response.body + items = [] + fetched = 0 + rss.items.each { |item| + record = { + :id => item.id, + :title => item.title, + :summary => item.summary, + :link => item.link, + :published => item.published + } + items.push record + fetched += 1 + break item if fetched == limit.to_i + } + Rails.cache.write( cache_key, items, :expires_in => 4.hours ) + end + + return items + end +end \ No newline at end of file diff --git a/lib/web_socket.rb b/lib/web_socket.rb new file mode 100644 index 000000000..f13cb3c30 --- /dev/null +++ b/lib/web_socket.rb @@ -0,0 +1,175 @@ +require 'json' + +module Session + @path = '/tmp/websocket' + + def self.create( client_id, session ) + path = @path + '/' + client_id.to_s + FileUtils.mkpath path + File.open( path + '/session', 'w' ) { |file| + user = { :id => session['id'] } + file.puts Marshal.dump(user) + } + end + + def self.get( client_id ) + session_file = @path + '/' + client_id.to_s + '/session' + data = nil + return if !File.exist? session_file + File.open( session_file, 'r' ) { |file| + all = '' + while line = file.gets + all = all + line + end + begin + data = Marshal.load( all ) + rescue + return + end + } + return data + end + + def self.transaction( client_id, data ) + filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + if File::exists?( filename ) + filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-1' + if File::exists?( filename ) + filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-2' + if File::exists?( filename ) + filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-3' + if File::exists?( filename ) + filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-4' + end + end + end + end + File.open( filename, 'w' ) { |file| + file.puts data.to_json + } + return true + end + + def self.jobs + state_client_ids = {} + while true + client_ids = self.sessions + client_ids.each { |client_id| + + if !state_client_ids[client_id] + state_client_ids[client_id] = {} + end + + # get current user + user_session = Session.get( client_id ) + next if !user_session + next if !user_session[:id] + user = User.find( user_session[:id] ) + + # overviews + result = Ticket.overview( + :current_user_id => user.id, + ) + if state_client_ids[client_id][:overview] != result + state_client_ids[client_id][:overview] = result + + # send update to browser + Session.transaction( client_id, { + :action => 'load', + :data => result, + :event => 'navupdate_ticket_overview', + }) + end + + # recent viewed + recent_viewed = History.recent_viewed(user) + if state_client_ids[client_id][:recent_viewed] != recent_viewed + state_client_ids[client_id][:recent_viewed] = recent_viewed + + # tickets and users + recent_viewed = History.recent_viewed_fulldata(user) + + # send update to browser + Session.transaction( client_id, { + :action => 'load', + :data => recent_viewed, + :event => 'update_recent_viewed', + }) + end + + # activity stream + activity_stream = History.activity_stream(user) + if state_client_ids[client_id][:activity_stream] != activity_stream + state_client_ids[client_id][:activity_stream] = activity_stream + + activity_stream = History.activity_stream_fulldata(user) + + # send update to browser + Session.transaction( client_id, { + :event => 'activity_stream_rebuild', + :collection => 'activity_stream', + :data => activity_stream, + }) + end + + # rss view + rss_items = RSS.fetch( 'http://www.heise.de/newsticker/heise-atom.xml', 8 ) + if state_client_ids[client_id][:rss_items] != rss_items + state_client_ids[client_id][:rss_items] = rss_items + + # send update to browser + Session.transaction( client_id, { + :event => 'rss_rebuild', + :collection => 'dashboard_rss', + :data => { + head: 'Heise ATOM', + items: rss_items, + }, + }) + end + sleep 1 + } + end + end + + def self.sessions + path = @path + '/' + data = [] + Dir.foreach( path ) do |entry| + if entry != '.' && entry != '..' + data.push entry + end + end + return data + end + + def self.queue( client_id ) + path = @path + '/' + client_id.to_s + '/' + data = [] + Dir.foreach( path ) do |entry| + if /^transaction/.match( entry ) + data.push Session.queue_file( path + entry ) + end + end + return data + end + + def self.queue_file( filename ) + data = nil + File.open( filename, 'r' ) { |file| + all = '' + while line = file.gets + all = all + line + end + data = JSON.parse( all ) + } + File.delete( filename ) + return data + end + + def self.destory( client_id ) + path = @path + '/' + client_id.to_s + FileUtils.rm_rf path + end + +end diff --git a/script/websocket-server.rb b/script/websocket-server.rb new file mode 100644 index 000000000..0b616162e --- /dev/null +++ b/script/websocket-server.rb @@ -0,0 +1,86 @@ +$LOAD_PATH << './lib' +require 'rubygems' +require 'eventmachine' +require 'em-websocket' +require 'json' +require 'fileutils' +require 'web_socket' +require 'optparse' + +# Look for -o with argument, and -I and -D boolean arguments +options = { + :p => 6042, + :b => '0.0.0.0', +} +OptionParser.new do |opts| + opts.banner = "Usage: websocket-server.rb [options]" + + opts.on("-p", "--port [OPT]", "port of websocket server") do |p| + options[:p] = p + end + opts.on("-b", "--bind [OPT]", "bind address") do |b| + options[:b] = b + end +end.parse! + +puts "Starting websocket server on #{ options[:b] }:#{ options[:p] }" + +@clients = {} +EventMachine.run { + EventMachine::WebSocket.start( :host => options[:b], :port => options[:p] ) do |ws| + + # register client connection + ws.onopen { + client_id = ws.object_id + puts 'Client ' + client_id.to_s + ' connected' + + if !@clients.include? client_id + @clients[client_id] = { + :websocket => ws, + } + end + } + + # unregister client connection + ws.onclose { + client_id = ws.object_id + puts 'Client ' + client_id.to_s + ' disconnected' + + if @clients.include? client_id + @clients.delete client_id + end + Session.destory( client_id ) + } + + # manage messages + ws.onmessage { |msg| + + client_id = ws.object_id + puts 'From Client ' + client_id.to_s + ' received message: ' + msg + data = JSON.parse(msg) + + # get session + if data['action'] == 'login' + @clients[client_id][:session] = data['session'] + Session.create( client_id, data['session'] ) + end + } + end + + EventMachine.add_periodic_timer(0.4) { +# puts "loop" + @clients.each { |client_id, client| +# puts 'checking client...' + client_id.to_s + begin + queue = Session.queue( client_id ) + if queue && queue[0] + puts "send to #{client_id} " + queue.inspect + client[:websocket].send( queue.to_json ) + end + rescue + puts 'problem' + end + } + } + +}