├── public ├── css │ ├── font.css │ └── style.css ├── favicon.ico ├── fonts │ ├── SquareSansLight.eot │ ├── SquareSansLight.ttf │ ├── SquareSansThin.eot │ ├── SquareSansThin.ttf │ ├── SquareSansThin.woff │ ├── SquareSansLight.woff │ ├── SquareSansLight.svg │ └── SquareSansThin.svg └── js │ ├── object_with_handlers.js │ ├── util.js │ ├── status_box.js │ ├── contacts_box.js │ ├── register_box.js │ ├── video_box.js │ ├── session.js │ ├── index.js │ ├── remote.js │ └── more.js ├── config └── routes.rb ├── Gemfile ├── .rvmrc ├── app ├── actions │ ├── index_action.rb │ └── session_action.rb └── views │ └── index.haml ├── config.ru ├── Gemfile.lock ├── application.rb ├── lib └── model │ └── session.rb └── README.md /public/css/font.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/SquareSansLight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/fonts/SquareSansLight.eot -------------------------------------------------------------------------------- /public/fonts/SquareSansLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/fonts/SquareSansLight.ttf -------------------------------------------------------------------------------- /public/fonts/SquareSansThin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/fonts/SquareSansThin.eot -------------------------------------------------------------------------------- /public/fonts/SquareSansThin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/fonts/SquareSansThin.ttf -------------------------------------------------------------------------------- /public/fonts/SquareSansThin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/fonts/SquareSansThin.woff -------------------------------------------------------------------------------- /public/fonts/SquareSansLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osdrv/callme/HEAD/public/fonts/SquareSansLight.woff -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # Check out https://github.com/joshbuddy/http_router for more information on HttpRouter 2 | HttpRouter.new do 3 | add( '/' ).to( IndexAction ) 4 | add( '/session' ).to( SessionAction ) 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'cramp' 4 | 5 | gem 'thin' 6 | 7 | gem 'http_router' 8 | 9 | gem 'async-rack' 10 | 11 | gem "haml", "~> 3.1.7" 12 | 13 | gem "em-hiredis", "~> 0.1.1" 14 | 15 | gem "uuid", "~> 2.3.6" 16 | 17 | gem 'json' 18 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | environment_id="ruby-1.9.3-p194@callme" 4 | 5 | if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \ 6 | && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]] ; then 7 | \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id" 8 | else 9 | rvm --create "$environment_id" 10 | fi 11 | -------------------------------------------------------------------------------- /app/actions/index_action.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class IndexAction < Cramp::Action 4 | 5 | def app 6 | Callme::Application 7 | end 8 | 9 | def reload_template 10 | @@template = Haml::Engine.new( File.read( app.root( 'app/views/index.haml' ) ) ) 11 | end 12 | 13 | def start 14 | reload_template unless app.env == 'production' && @@template.nil? 15 | render @@template.render 16 | finish 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './application' 2 | Callme::Application.initialize! 3 | 4 | # Development middlewares 5 | if Callme::Application.env == 'development' 6 | use AsyncRack::CommonLogger 7 | 8 | # Enable code reloading on every request 9 | use Rack::Reloader, 0 10 | 11 | # Serve assets from /public 12 | use Rack::Static, :urls => ["/js", "/css", "/favicon.ico", "/fonts"], :root => Callme::Application.root(:public) 13 | end 14 | 15 | # Running thin : 16 | # 17 | # bundle exec thin --max-persistent-conns 1024 --timeout 0 -R config.ru start 18 | # 19 | # Vebose mode : 20 | # 21 | # Very useful when you want to view all the data being sent/received by thin 22 | # 23 | # bundle exec thin --max-persistent-conns 1024 --timeout 0 -V -R config.ru start 24 | # 25 | run Callme::Application.routes 26 | -------------------------------------------------------------------------------- /public/js/object_with_handlers.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | var ObjectWithHandlers = new Class({ 4 | 5 | initialize: function() { 6 | this.handlers = {}; 7 | }, 8 | 9 | registerHandler: function( event, cb ) { 10 | this.handlers[ event ] = this.handlers[ event ] || new Array(); 11 | this.handlers[ event ].push( cb ); 12 | }, 13 | 14 | callHandlersFor: function( event ) { 15 | var _arguments = Array.prototype.slice.call( arguments ); 16 | _arguments.shift(); 17 | if ( this.handlers[ event ] !== undefined ) { 18 | this.handlers[ event ].each( function( el ) { 19 | el.apply( el, _arguments ); 20 | } ); 21 | } 22 | } 23 | }); 24 | 25 | W.ObjectWithHandlers = ObjectWithHandlers; 26 | } )( window ); -------------------------------------------------------------------------------- /public/js/util.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | W.tmpl = function( str_tmpl, obj ) { 4 | res = str_tmpl; 5 | Object.each( obj, function( v, k ) { 6 | var re = new RegExp( '#{' + k + '}', 'g' ); 7 | res = res.replace( re, v ); 8 | } ); 9 | res = res.replace( /\#\{.+?\}/g, '' ); 10 | 11 | return res; 12 | } 13 | 14 | W.is_empty = function( v ) { 15 | return v === undefined || v === null; 16 | } 17 | 18 | W.$w = function( str ) { 19 | return str.split( /\s+/ ); 20 | } 21 | 22 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 23 | 24 | W.URL = W.URL || W.webkitURL; 25 | 26 | W.RTCPeerConnection = W.mozRTCPeerConnection || W.webkitRTCPeerConnection; 27 | 28 | } )( window ); -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activesupport (3.0.17) 5 | async-rack (0.5.1) 6 | rack (~> 1.1) 7 | cramp (0.15.1) 8 | activesupport (~> 3.0.9) 9 | eventmachine (~> 1.0.0.beta.3) 10 | rack (~> 1.3.2) 11 | thor (~> 0.14.6) 12 | daemons (1.1.9) 13 | em-hiredis (0.1.1) 14 | hiredis (~> 0.4.0) 15 | eventmachine (1.0.0) 16 | haml (3.1.7) 17 | hiredis (0.4.5) 18 | http_router (0.11.0) 19 | rack (>= 1.0.0) 20 | url_mount (~> 0.2.1) 21 | macaddr (1.6.1) 22 | systemu (~> 2.5.0) 23 | rack (1.3.6) 24 | systemu (2.5.2) 25 | thin (1.5.0) 26 | daemons (>= 1.0.9) 27 | eventmachine (>= 0.12.6) 28 | rack (>= 1.0.0) 29 | thor (0.14.6) 30 | url_mount (0.2.1) 31 | rack 32 | uuid (2.3.6) 33 | macaddr (~> 1.0) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | async-rack 40 | cramp 41 | em-hiredis (~> 0.1.1) 42 | haml (~> 3.1.7) 43 | http_router 44 | thin 45 | uuid (~> 2.3.6) 46 | -------------------------------------------------------------------------------- /public/js/status_box.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | StatusBox = new Class({ 4 | 5 | Extends: ObjectWithHandlers, 6 | 7 | known_statuses: [ 'online', 'offline', 'connecting' ], 8 | 9 | initialize: function( element_id ) { 10 | this.parent(); 11 | this.element = $( element_id ); 12 | this.user_name = this.element.getElements( '.name' ); 13 | this.status_marker = this.element.getElements( '.user_status' ); 14 | this.status_text = this.element.getElements( '.user_status .text' ); 15 | }, 16 | 17 | setStatus: function( status ) { 18 | var self = this; 19 | this.callHandlersFor( 'status', status ); 20 | this.known_statuses.each( function( klass ) { 21 | self.status_marker.removeClass( klass ); 22 | } ); 23 | this.status_marker.addClass( status ); 24 | this.status_text.set( 'html', status ); 25 | }, 26 | 27 | setName: function( name ) { 28 | this.callHandlersFor( 'name', name ); 29 | this.user_name.set( 'html', name ); 30 | } 31 | 32 | }); 33 | 34 | W.StatusBox = StatusBox; 35 | 36 | } )( window ); -------------------------------------------------------------------------------- /application.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "rubygems" 4 | require "bundler" 5 | require 'em-hiredis' 6 | require 'json' 7 | 8 | module Callme 9 | class Application 10 | 11 | def self.root(path = nil) 12 | @_root ||= File.expand_path(File.dirname(__FILE__)) 13 | path ? File.join(@_root, path.to_s) : @_root 14 | end 15 | 16 | def self.env 17 | @_env ||= ENV['RACK_ENV'] || 'development' 18 | end 19 | 20 | def self.routes 21 | @_routes ||= eval(File.read('./config/routes.rb')) 22 | end 23 | 24 | def self.scripts 25 | @_scripts ||= %w(mt more object_with_handlers session remote register_box status_box contacts_box util video_box index) 26 | end 27 | 28 | def self.redis 29 | if @_redis.nil? 30 | @_redis = EM::Hiredis.connect 31 | @_redis.callback do 32 | @_redis.del( Session.table_name ) 33 | end 34 | end 35 | @_redis 36 | end 37 | 38 | # Initialize the application 39 | def self.initialize! 40 | Cramp::Websocket.backend = :thin 41 | end 42 | 43 | end 44 | end 45 | 46 | Bundler.require(:default, Callme::Application.env) 47 | 48 | # Preload application classes 49 | %w(lib app).each do |loc| 50 | Dir["./#{loc}/**/*.rb"].each {|f| require f} 51 | end 52 | -------------------------------------------------------------------------------- /app/views/index.haml: -------------------------------------------------------------------------------- 1 | - # encoding: utf-8 2 | !!!5 3 | 4 | %head 5 | %title CallMe App 6 | %meta{ :charset => "utf-8" } 7 | %link{ :rel => 'shortcut icon', :href => '/favicon.ico' } 8 | %link{ :href => '/css/font.css', :rel => 'stylesheet' } 9 | %link{ :href => '/css/style.css', :rel => 'stylesheet' } 10 | - Callme::Application.scripts.each do |script| 11 | %script{ :src => "js/#{script}.js", :type => 'text/javascript' } 12 | 13 | %body 14 | #page.page 15 | #hello.hello 16 | %h1 CallMe App 17 | HTML5 p2p video chat application 18 | #user.user.hide 19 | .user_name 20 | %span.name Anonimous 21 | .user_status.offline 22 | ● 23 | %span.text offline 24 | #contacts.contacts 25 | %h4 Contacts online: 26 | %ul 27 | %li Waiting for data… 28 | 29 | #video.video.offline 30 | #paired.paired 31 | #self.self 32 | 33 | #register.register.hide 34 | %h4 Registration 35 | %label Username: 36 | %input#register_name{ :type => 'text', :name => 'name', :size => 30, :required => true } 37 | %br/ 38 | %button#register_submit Connect 39 | 40 | #sorry.sorry.hide 41 | %h2.red Ooops, something is broken ((( 42 | We are very sorry. It seems connection error has fired. 43 | %br/ 44 | Please try reload this page a little bit later. 45 | .footer.ghost 46 | © 47 | %a{ :href => "http://whitebox.io" } Whitebox.io -------------------------------------------------------------------------------- /lib/model/session.rb: -------------------------------------------------------------------------------- 1 | class Session 2 | 3 | attr_accessor :uuid 4 | attr_accessor :user_data 5 | 6 | def initialize( uuid, user_data ) 7 | self.uuid = uuid 8 | self.user_data = user_data 9 | end 10 | 11 | def save( &blk ) 12 | begin 13 | save!( blk ) 14 | return true 15 | rescue Exception => e 16 | return false 17 | end 18 | end 19 | 20 | def save!( &blk ) 21 | self.class.connection.hset( 22 | self.class.table_name, 23 | uuid, 24 | ( user_data.to_json rescue '' ) 25 | ).callback &blk 26 | end 27 | 28 | def destroy! 29 | self.class.connection.hdel( self.class.table_name, uuid ) 30 | end 31 | 32 | # TODO: some private logic 33 | def private? 34 | false 35 | end 36 | 37 | def to_json 38 | { 39 | :uuid => uuid, 40 | :user_data => user_data 41 | }.to_json 42 | end 43 | 44 | class << self 45 | 46 | def connection 47 | Callme::Application.redis 48 | end 49 | 50 | def table_name 51 | self.to_s 52 | end 53 | 54 | def create 55 | self.new( UUID.new.generate.to_s, nil ) 56 | end 57 | 58 | def findAll( ids, &blk ) 59 | connection.hmget( table_name, *ids ).callback do |data| 60 | res = [] 61 | data.each_index do |idx| 62 | next unless !data[ idx ].empty? && data[ idx ] != 'null' 63 | res.push self.new( ids[ idx ], JSON.parse( data[ idx ] ) ) 64 | end 65 | 66 | blk.call( res ) 67 | end 68 | 69 | end 70 | 71 | def find( uuid, &blk ) 72 | connection.hget( table_name, uuid ).callback do |data| 73 | return if data.nil? 74 | blk.call( self.new( uuid, data ) ) 75 | end 76 | end 77 | 78 | end 79 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #CallMe App 2 | 3 | ## Introduction 4 | 5 | CallMe App is a simple p2p video call web application built on top of [WebRTC](http://www.webrtc.org/). 6 | 7 | ## Features 8 | 9 | ![image](http://i.4pcbr.com/i/3c/1tJ7Qj.png) 10 | 11 | You can call yourself! ))) 12 | 13 | No flash, no complicated video systems. Just pure javascript. 14 | 15 | No video server is required. Just remote session exchange coordination. 16 | 17 | ## Requirements 18 | 19 | Current version requires Chrome 23 or later. 20 | 21 | ## Backend 22 | 23 | CallMe App uses [cramp](http://cramp.in) backend with WebSockets as primary transport and [redis](http://redis.io) as user session storage. 24 | 25 | ## Starting your own server 26 | 27 | ```bash 28 | git clone https://github.com/4pcbr/callme.git 29 | cd callme 30 | bundle install 31 | bundle exec thin --max-persistent-conns 1024 --timeout 0 -R config.ru -p 8080 start 32 | ``` 33 | 34 | You have to launch redis-server as well to get websockets working. 35 | 36 | ## Usage 37 | 38 | After launching server instance launch your google chrome and open http://localhost:8000 39 | 40 | You need 2 clients online minimum to make a calls. 41 | 42 | ## TODO 43 | 44 | #### Functional TODO 45 | 46 | * hangup button 47 | * notify callee if partner was disconnected 48 | * conference calls 49 | * call request 50 | * decline call if user is ringing 51 | * rooms, invitations 52 | * authorization with fb && google 53 | * user profiles 54 | 55 | #### Tech TODO 56 | 57 | * JQUnit all around 58 | * move all pub-sub actions to redis 59 | * some mvc for frontend 60 | 61 | ## Code contribution 62 | 63 | If you want to contribute this proj follow those simple steps: 64 | 65 | 1. Clone this repository 66 | 2. Make new feature branch 67 | 3. Make your changes 68 | 4. Make a pull request 69 | 70 | ## Feature contribution 71 | 72 | 1. Open an issue with subject 73 | 2. Write simple description of your feature 74 | 3. We will take it on free-for-all discussion -------------------------------------------------------------------------------- /public/js/contacts_box.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | var ContactsBox = new Class({ 4 | 5 | Extends: ObjectWithHandlers, 6 | 7 | defaults: { 8 | template: "
  • #{name}
  • " 9 | }, 10 | 11 | initialize: function( element_id, options ) { 12 | this.parent(); 13 | var self = this; 14 | this.options = Object.merge( this.defaults, options ); 15 | this.element = $( element_id ); 16 | this.list = this.element.getElement( 'ul' ); 17 | this.list.addEvent( 'click:relay(a)', function( e, t ) { 18 | e.preventDefault(); 19 | var uuid = t.get( 'href' ).replace( '#', '' ); 20 | self.callHandlersFor.call( self, 'contact.selected', uuid ); 21 | } ); 22 | }, 23 | 24 | proceed: function( data ) { 25 | if ( data !== undefined && data !== null ) { 26 | switch ( data.status ) { 27 | case 'refresh': 28 | this.clearList(); 29 | this.setContactList( data.sessions ); 30 | break; 31 | } 32 | } 33 | }, 34 | 35 | clearList: function() { 36 | this.list.empty(); 37 | }, 38 | 39 | setContactList: function( sessions ) { 40 | var self = this; 41 | sessions.each( function( session ) { 42 | // try { 43 | data = JSON.parse( session ); 44 | if ( !is_empty( data ) && !is_empty( data.uuid ) && !is_empty( data.user_data.name ) ) { 45 | self.addContact({ status: 'online', uuid: data.uuid, name: data.user_data.name }); 46 | } 47 | // } catch ( e ) { 48 | // console.log( 'ContactsBox.setContactList: Malformed data given' ); 49 | // } 50 | } ) 51 | }, 52 | 53 | addContact: function( contact ) { 54 | var li = tmpl( this.options.template, contact ); 55 | this.list.set( 'html', this.list.get( 'html' ) + li ); 56 | } 57 | 58 | 59 | }); 60 | 61 | W.ContactsBox = ContactsBox; 62 | 63 | } )( window ); -------------------------------------------------------------------------------- /public/js/register_box.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | RegisterBox = new Class({ 4 | 5 | Extends: ObjectWithHandlers, 6 | 7 | initialize: function( element_id ) { 8 | this.parent(); 9 | this.element = $( element_id ); 10 | this.form_fields = this.element.getElements( 'input, textarea' ); 11 | this.submit_button = this.element.getElement( 'button' ); 12 | this._bindElements(); 13 | }, 14 | 15 | isInputCorrect: function() { 16 | res = true; 17 | this.form_fields.each( function( element ) { 18 | element.removeClass( 'wrong' ); 19 | var check = true; 20 | if ( element.getProperty( 'required' ) && !element.get( "value" ) ) { 21 | check = false; 22 | element.addClass( 'wrong' ); 23 | window.setTimeout( function() { 24 | element.removeClass( 'wrong' ); 25 | }, 1000 ); 26 | } 27 | res &= check; 28 | } ); 29 | 30 | return res; 31 | }, 32 | 33 | getInput: function() { 34 | res = {}; 35 | this.form_fields.each( function( element ) { 36 | res[ element.getProperty( 'name' ) ] = element.get( "value" ); 37 | } ); 38 | 39 | return res; 40 | }, 41 | 42 | focus: function() { 43 | this.form_fields[ 0 ].focus(); 44 | }, 45 | 46 | submit: function () { 47 | if ( this.isInputCorrect() ) { 48 | this.callHandlersFor( 'register', this.getInput() ); 49 | } 50 | }, 51 | 52 | _bindElements: function() { 53 | var self = this; 54 | if ( this.submit_button !== null ) { 55 | this.submit_button.addEvent( 'click', function() { 56 | self.submit.call( self ); 57 | } ); 58 | } 59 | this.form_fields.each( function( element ) { 60 | element.addEvent( 'keypress', function( e ) { 61 | if ( e.key !== undefined && e.key == 'enter' ) { 62 | self.submit.call( self ); 63 | } 64 | } ); 65 | } ); 66 | } 67 | }); 68 | 69 | W.RegisterBox = RegisterBox; 70 | 71 | } )( window ); -------------------------------------------------------------------------------- /public/js/video_box.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | var VIDEO_TMPL = '', 4 | SELF_VIDEO_SIZE = { w: 240, h: 180 }, 5 | PAIRED_VIDEO_SIZE = { w: 640, h: 480 }; 6 | 7 | var VideoBox = new Class({ 8 | 9 | Extends: ObjectWithHandlers, 10 | 11 | initialize: function( videos ) { 12 | this.parent(); 13 | this.self_video = $( videos.self ); 14 | this.paired_video = $( videos.paired ); 15 | this.self_stream = null; 16 | this.self_stream_url = null; 17 | this.initUserMedia(); 18 | }, 19 | 20 | initUserMedia: function() { 21 | var self = this; 22 | navigator.getUserMedia({ video: true, audio: true }, function( stream ) { 23 | self.self_stream_url = W.URL.createObjectURL( stream ); 24 | self.self_video.set( 'html', tmpl( VIDEO_TMPL, { 25 | width: SELF_VIDEO_SIZE.w, 26 | height: SELF_VIDEO_SIZE.h, 27 | src: self.self_stream_url 28 | } ) ); 29 | self.self_stream = stream; 30 | self.callHandlersFor( 'inited', self.self_stream ); 31 | }, function( error ) { 32 | self.callHandlersFor( 'error', error ); 33 | }); 34 | }, 35 | 36 | playSelfVideo: function( src ) { 37 | this.stopVideo( this.self_video ); 38 | this.startVideo( this.self_video, { 39 | width: SELF_VIDEO_SIZE.w, 40 | height: SELF_VIDEO_SIZE.h, 41 | src: src 42 | } ); 43 | }, 44 | 45 | stopSelfVideo: function() { 46 | this.stopVideo( this.self_video ); 47 | }, 48 | 49 | playPairedVideo: function( src ) { 50 | this.stopVideo( this.paired_video ); 51 | this.startVideo( this.paired_video, { 52 | width: PAIRED_VIDEO_SIZE.w, 53 | height: PAIRED_VIDEO_SIZE.h, 54 | src: src 55 | } ); 56 | }, 57 | 58 | stopPairedVideo: function() { 59 | this.stopVideo( this.paired_video ); 60 | }, 61 | 62 | startVideo: function( video, params ) { 63 | video.set( 'html', tmpl( VIDEO_TMPL, params ) ); 64 | }, 65 | 66 | stopVideo: function( video ) { 67 | var video_element = video.getElement( 'video' ); 68 | if ( video_element !== null ) { 69 | video_element.stop(); 70 | video_element.set( 'src', '' ); 71 | } 72 | video.empty(); 73 | } 74 | }); 75 | 76 | W.VideoBox = VideoBox; 77 | } )( window ); -------------------------------------------------------------------------------- /app/actions/session_action.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class SessionAction < Cramp::Action 4 | 5 | @@_connections = {} 6 | 7 | self.transport = :websocket 8 | 9 | on_start :create_redis, :create_session 10 | on_finish :close_session 11 | on_data :message_received 12 | 13 | def create_redis 14 | Callme::Application.redis 15 | end 16 | 17 | def close_redis 18 | Callme::Application.redis.close_connection 19 | end 20 | 21 | def create_session 22 | @session = Session.create 23 | @session.save! do 24 | @@_connections[ @session.uuid ] = self 25 | response :action => :session, :status => :created, :uuid => @session.uuid 26 | end 27 | end 28 | 29 | def close_session 30 | @@_connections.delete( @session.uuid ) 31 | @session.destroy! 32 | refresh_contact_list 33 | end 34 | 35 | def refresh_contact_list 36 | Session.findAll( @@_connections.keys ) do |items| 37 | @@_connections.each_pair do |uuid, connection| 38 | connection.response :action => :contacts, :status => :refresh, :sessions => items.reject { |sess| 39 | sess.uuid == uuid || sess.private? 40 | }.map(&:to_json) 41 | end 42 | end 43 | end 44 | 45 | def message_received( data ) 46 | begin 47 | message = JSON.parse( data ) 48 | p message 49 | case message[ 'action' ] 50 | when 'session' 51 | save_session!( message ) 52 | when 'peer' 53 | peer_with!( message[ 'receiver' ], message[ 'session' ] ) 54 | when 'confirm' 55 | confirm_to!( message[ 'receiver' ], message[ 'session' ] ) 56 | when 'candidate' 57 | offer_candidate!( message[ 'candidate' ] ) 58 | end 59 | rescue Exception => e 60 | p e 61 | end 62 | end 63 | 64 | def offer_candidate!( candidate ) 65 | @@_connections.keys.reject { |key| 66 | key == @session.uuid 67 | }.each do |key| 68 | @@_connections[ key ].response :action => :remote, :status => :candidate, :callee => @session.to_json, :candidate => candidate 69 | end 70 | end 71 | 72 | def save_session!( message ) 73 | @session.user_data = message[ 'user_data' ] 74 | @session.save! do 75 | refresh_contact_list 76 | end 77 | end 78 | 79 | def peer_with!( receiver_uuid, session ) 80 | receiver = @@_connections[ receiver_uuid ] 81 | return if receiver_uuid.nil? || receiver.nil? || session.nil? 82 | receiver.response :action => :remote, :status => :offer, :callee => @session.to_json, :session => session 83 | end 84 | 85 | def confirm_to!( receiver_uuid, session ) 86 | receiver = @@_connections[ receiver_uuid ] 87 | return if receiver_uuid.nil? || receiver.nil? || session.nil? 88 | receiver.response :action => :remote, :status => :confirm, :callee => @session.to_json, :session => session 89 | end 90 | 91 | def response( data ) 92 | render data.to_json.force_encoding('utf-8') 93 | end 94 | 95 | protected 96 | 97 | # https://github.com/lifo/cramp/pull/39 98 | # пиздец, блядь 99 | def websockets_protocol_10? 100 | [7, 8, 9, 10, 13].include?(@env['HTTP_SEC_WEBSOCKET_VERSION'].to_i) 101 | end 102 | 103 | end -------------------------------------------------------------------------------- /public/js/session.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | defaults = { 4 | host: 'localhost', 5 | port: '8080', 6 | protocols: [ 'soap', 'xmpp' ], 7 | url: '/echo' 8 | } 9 | 10 | var Session = new Class({ 11 | 12 | Extends: ObjectWithHandlers, 13 | 14 | initialize: function( options ) { 15 | this.parent(); 16 | this.options = Object.merge( defaults, options ); 17 | this.SESSID = null; 18 | this.connection = null; 19 | this.user_data = null; 20 | 21 | var self = this; 22 | 23 | this.registerHandler( "close", function() { 24 | if ( self.connection !== null ) 25 | self.connection.close(); 26 | } ); 27 | }, 28 | 29 | cfg: function( k, v ) { 30 | if ( v === undefined ) { 31 | return this.options[ k ]; 32 | } else { 33 | this.options[ k ] = v; 34 | } 35 | }, 36 | 37 | getConnectionUrl: function() { 38 | return 'ws://' + this.options.host + ':' + this.options.port + this.options.url; 39 | }, 40 | 41 | bindConnection: function() { 42 | if ( this.connection === null ) { 43 | throw 'Session is not started yet. Call session.start() first.'; 44 | } 45 | var self = this, 46 | connection = this.connection; 47 | [ 'onopen', 'error', 'onmessage', 'onclose' ].each( function( event_kind ) { 48 | connection[ event_kind ] = function() { 49 | console.log( "Fired: ", event_kind, arguments ); 50 | var args = Array.prototype.slice.call( arguments ); 51 | args.unshift( event_kind.replace( /^on/, '' ) ); 52 | self.callHandlersFor.apply( self, args ); 53 | } 54 | } ); 55 | }, 56 | 57 | start: function() { 58 | this.connection = new WebSocket( this.getConnectionUrl(), this.options.protocols ); 59 | this.bindConnection(); 60 | }, 61 | 62 | setUserData: function( data ) { 63 | this.user_data = data; 64 | }, 65 | 66 | getUserData: function() { 67 | return this.user_data; 68 | }, 69 | 70 | _finalizeRegister: function() { 71 | this.connection.send( 72 | JSON.encode( { action: 'session', uuid: this.SESSID, user_data: this.getUserData() } ) 73 | ); 74 | }, 75 | 76 | proceed: function( data ) { 77 | if ( data !== undefined && data !== null ) { 78 | switch ( data.status ) { 79 | case 'created': 80 | this.SESSID = data.uuid; 81 | this.callHandlersFor( 'session.created' ); 82 | this._finalizeRegister(); 83 | case 'confirmed': 84 | this.callHandlersFor( 'session.confirmed' ); 85 | } 86 | } 87 | }, 88 | 89 | connectWith: function( uuid, sess_descr ) { 90 | this.connection.send( JSON.encode({ 91 | action: 'peer', 92 | uuid: this.SESSID, 93 | receiver: uuid, 94 | session: sess_descr 95 | })); 96 | }, 97 | 98 | answerTo: function( uuid, sess_descr ) { 99 | this.connection.send( JSON.encode({ 100 | action: 'confirm', 101 | uuid: this.SESSID, 102 | receiver: uuid, 103 | session: sess_descr 104 | })); 105 | }, 106 | 107 | offerCandidate: function( candidate ) { 108 | this.connection.send( JSON.encode({ 109 | action: 'candidate', 110 | uuid: this.SESSID, 111 | candidate: candidate 112 | })); 113 | } 114 | }); 115 | 116 | W.Session = Session; 117 | 118 | } )( window ); -------------------------------------------------------------------------------- /public/js/index.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | document.addEventListener( 'DOMContentLoaded', function() { 3 | 4 | var uri = new URI(), 5 | session = new Session({ 6 | host: uri.get( 'host' ), 7 | port: uri.get( 'port' ), 8 | url: '/session' 9 | }), 10 | remote = new Remote( session, {} ), 11 | register_box = new RegisterBox( 'register' ), 12 | user_box = new StatusBox( 'user' ), 13 | contacts_box = new ContactsBox( 'contacts' ), 14 | video_box = new VideoBox( { self: 'self', paired: 'paired' } ), 15 | sorry_plate = $( 'sorry' ), 16 | page = $( 'page' ), 17 | interval = 250, 18 | reconnects = 0, 19 | MAX_RECONNECTS = 10, 20 | RECONNECT_TIMEOUT = 1000; 21 | 22 | 23 | 24 | remote.registerHandler( 'stun.icecandidate', function( event ) { 25 | console.log( 'stun.ice_candidate', arguments ); 26 | } ); 27 | 28 | remote.registerHandler( 'stun.addstream', function( event ) { 29 | console.log( 'stun.addstream', arguments ); 30 | video_box.playPairedVideo( URL.createObjectURL( event.stream ) ); 31 | } ); 32 | 33 | remote.registerHandler( 'stun.removestream', function( event ) { 34 | console.log( 'stun.removestream', arguments ); 35 | video_box.stopPairedVideo(); 36 | } ); 37 | 38 | remote.registerHandler( 'stun.error', function() { 39 | console.log( 'stun.error', arguments ); 40 | } ); 41 | 42 | remote.registerHandler( 'sdp.init', function() { 43 | console.log( 'sdp.init', arguments ); 44 | } ); 45 | 46 | 47 | contacts_box.registerHandler( 'contact.selected', function( uuid ) { 48 | remote.callTo( uuid ); 49 | } ); 50 | 51 | 52 | session.registerHandler( 'open', function() { 53 | user_box.setStatus( 'connecting' ); 54 | } ); 55 | 56 | session.registerHandler( 'session.confirmed', function() { 57 | user_box.setStatus( 'online' ); 58 | } ); 59 | 60 | session.registerHandler( 'close', function() { 61 | user_box.setStatus( 'offline' ); 62 | if ( reconnects <= MAX_RECONNECTS ) { 63 | window.setTimeout( function() { 64 | reconnects++; 65 | session.start(); 66 | }, RECONNECT_TIMEOUT ); 67 | } else { 68 | page.removeClass( 'unblur' ).addClass( 'blur' ); 69 | sorry_plate.removeClass( 'hide' ).addClass( 'appear' ); 70 | } 71 | } ); 72 | 73 | session.registerHandler( 'message', function( message ) { 74 | reconnects = 0; 75 | // try { 76 | var data = JSON.decode( message.data ); 77 | 78 | switch ( data.action ) { 79 | case 'session': 80 | session.proceed( data ); 81 | break; 82 | case 'contacts': 83 | contacts_box.proceed( data ); 84 | break; 85 | case 'remote': 86 | remote.proceed( data ); 87 | break; 88 | } 89 | // } catch ( e ) { 90 | // console.log( e ); 91 | // console.log( 'Inconsistent message received' ); 92 | // } 93 | // console.log( ); 94 | } ); 95 | 96 | 97 | video_box.registerHandler( 'inited', function( stream ) { 98 | remote.setStream( stream ); 99 | } ); 100 | 101 | 102 | register_box.registerHandler( 'register', function( values ) { 103 | session.setUserData( values ); 104 | register_box.element.removeClass( 'appear' ).addClass( 'right-top' ).addClass( 'hide' ); 105 | window.setTimeout( function() { 106 | page.removeClass( 'blur' ).addClass( 'unblur' ); 107 | user_box.setName( values.name ); 108 | session.start(); 109 | }, 2 * interval ); 110 | } ); 111 | 112 | 113 | var ix = 1; 114 | page.addClass( 'blur' ); 115 | [ 'user', 'register' ].each( function( box_id ) { 116 | window.setTimeout( function() { 117 | $( box_id ).removeClass( 'hide' ).addClass( 'appear' ); 118 | if ( box_id == 'register' ) { 119 | window.setTimeout( function() { 120 | register_box.focus(); 121 | }, interval ); 122 | } 123 | }, interval * ( ++ix ) ); 124 | } ); 125 | 126 | }, false ); 127 | } )( window ); -------------------------------------------------------------------------------- /public/js/remote.js: -------------------------------------------------------------------------------- 1 | ;( function( W ) { 2 | 3 | var mediaSettings = { 4 | mandatory: { 5 | OfferToReceiveAudio: true, 6 | OfferToReceiveVideo: true 7 | } 8 | }; 9 | 10 | var Remote = new Class({ 11 | 12 | Extends: ObjectWithHandlers, 13 | 14 | defaults: { 15 | stun: { iceServers: [ { url: "stun:stun.l.google.com:19302" } ] } 16 | }, 17 | 18 | initialize: function( session, options ) { 19 | this.parent(); 20 | var self = this; 21 | this.session = session; 22 | this.options = Object.merge( this.defaults, options ); 23 | this.peer_connection = null; 24 | 25 | this.registerHandler( 'stun.icecandidate', function( event ) { 26 | if ( event.candidate ) { 27 | self.offerCandidate( { 28 | type: 'candidate', 29 | label: event.candidate.sdpMLineIndex, 30 | id: event.candidate.sdpMid, 31 | candidate: event.candidate.candidate 32 | } ); 33 | } 34 | } ); 35 | }, 36 | 37 | createPeerConn: function() { 38 | var self = this; 39 | try { 40 | this.peer_connection = new RTCPeerConnection( this.options.stun ); 41 | $w( 'onicecandidate onconnecting onopen onaddstream onremovestream' ).each( function( event ) { 42 | ( function( event_kind ) { 43 | self.peer_connection[ event_kind ] = function() { 44 | var args = Array.prototype.slice.call( arguments ); 45 | args.unshift( "stun." + event_kind.replace( /^on/, '' ) ); 46 | self.callHandlersFor.apply( self, args ); 47 | } 48 | } )( event ); 49 | } ); 50 | } catch ( e ) { 51 | this.callHandlersFor( "stun.error", e ); 52 | } 53 | }, 54 | 55 | setStream: function( local_stream ) { 56 | this.local_stream = local_stream; 57 | this.callHandlersFor( "stream.add", local_stream ); 58 | }, 59 | 60 | callTo: function( receiver ) { 61 | var self = this; 62 | if ( this.peer_connection === null ) 63 | this.createPeerConn(); 64 | this.peer_connection.addStream( this.local_stream ); 65 | var call = this.peer_connection.createOffer( 66 | function ( sess_descr ) { 67 | sess_descr.sdp = self._preferOpus( sess_descr.sdp ); 68 | self.peer_connection.setLocalDescription( sess_descr ); 69 | self.session.connectWith( receiver, sess_descr ); 70 | }, 71 | null, 72 | mediaSettings 73 | ); 74 | }, 75 | 76 | answer: function( callee, remote_session ) { 77 | var self = this; 78 | if ( this.peer_connection === null ) 79 | this.createPeerConn(); 80 | this.peer_connection.addStream( this.local_stream ); 81 | this.peer_connection.setRemoteDescription( 82 | new RTCSessionDescription( remote_session ) 83 | ); 84 | this.peer_connection.createAnswer( 85 | function ( sess_descr ) { 86 | sess_descr.sdp = self._preferOpus( sess_descr.sdp ); 87 | self.peer_connection.setLocalDescription( sess_descr ); 88 | self.session.answerTo( callee, sess_descr ); 89 | }, 90 | null, 91 | mediaSettings 92 | ); 93 | this.callHandlersFor( 'remote.offered', callee, remote_session ); 94 | }, 95 | 96 | letsRock: function( callee, remote_session ) { 97 | if ( this.peer_connection === null ) 98 | this.createPeerConn(); 99 | console.log( callee, remote_session ); 100 | console.log( this.peer_connection ); 101 | this.peer_connection.setRemoteDescription( 102 | new RTCSessionDescription( remote_session ) 103 | ); 104 | this.callHandlersFor( 'remote.confirmed', callee, remote_session ); 105 | }, 106 | 107 | offerCandidate: function( candidate ) { 108 | this.session.offerCandidate( candidate ); 109 | }, 110 | 111 | proceed: function( data ) { 112 | switch ( data.status ) { 113 | case 'offer': 114 | try { 115 | var callee = JSON.parse( data.callee ); 116 | } catch ( e ) { 117 | console.warn( 'Malformed data received.' ); 118 | console.warn( e ); 119 | return; 120 | } 121 | this.answer( this._getCallee( data.callee ).uuid, data.session ); 122 | break; 123 | case 'confirm': 124 | this.letsRock( this._getCallee( data.callee ), data.session ); 125 | break; 126 | case 'candidate': 127 | this.addCandidate( data.candidate ); 128 | break; 129 | case 'hangup': 130 | this.hangup(); 131 | break; 132 | } 133 | }, 134 | 135 | _getCallee: function( data ) { 136 | try { 137 | var callee = JSON.parse( data ); 138 | } catch ( e ) { 139 | console.error( 'Malformed data received.' ); 140 | console.error( e ); 141 | 142 | return null; 143 | } 144 | 145 | return callee; 146 | }, 147 | 148 | // TODO: implement it 149 | hangup: function() { 150 | console.log( 'hanged up!' ); 151 | this.callHandlersFor( 'remote.hanged_up' ); 152 | }, 153 | 154 | addCandidate: function( data ) { 155 | var candidate = new RTCIceCandidate( { sdpMLineIndex: data.label, candidate: data.candidate } ); 156 | this.peer_connection.addIceCandidate( candidate ); 157 | }, 158 | 159 | // copypasted from https://apprtc.appspot.com/ 160 | 161 | _preferOpus: function( sdp ) { 162 | var sdpLines = sdp.split( '\r\n' ); 163 | // Search for m line. 164 | for ( var i = 0; i < sdpLines.length; i++ ) { 165 | if ( sdpLines[i].search( 'm=audio' ) !== -1 ) { 166 | var mLineIndex = i; 167 | break; 168 | } 169 | } 170 | if ( mLineIndex === null ) 171 | return sdp; 172 | // If Opus is available, set it as the default in m line. 173 | for ( var i = 0; i < sdpLines.length; i++ ) { 174 | if ( sdpLines[i].search( 'opus/48000' ) !== -1 ) { 175 | var opusPayload = this._extractSdp( sdpLines[ i ], /:(\d+) opus\/48000/i ); 176 | if ( opusPayload ) { 177 | sdpLines[ mLineIndex ] = this._defaultCodec( sdpLines[ mLineIndex ], opusPayload ); 178 | } 179 | break; 180 | } 181 | } 182 | 183 | // Remove CN in m line and sdp. 184 | sdpLines = this._removeCN( sdpLines, mLineIndex ); 185 | 186 | sdp = sdpLines.join( '\r\n' ); 187 | 188 | return sdp; 189 | }, 190 | 191 | _extractSdp: function( sdpLine, pattern ) { 192 | var result = sdpLine.match( pattern ); 193 | return ( result && result.length == 2 ) ? result[ 1 ]: null; 194 | }, 195 | 196 | _defaultCodec: function( mLine, payload ) { 197 | var elements = mLine.split( ' ' ), 198 | newLine = new Array(), 199 | index = 0; 200 | for ( var i = 0; i < elements.length; i++ ) { 201 | if ( index === 3 ) // Format of media starts from the fourth. 202 | newLine[ index++ ] = payload; // Put target payload to the first. 203 | if ( elements[ i ] !== payload ) { 204 | newLine[ index++ ] = elements[ i ]; 205 | } 206 | } 207 | 208 | return newLine.join( ' ' ); 209 | }, 210 | 211 | _removeCN: function(sdpLines, mLineIndex) { 212 | var mLineElements = sdpLines[mLineIndex].split(' '); 213 | // Scan from end for the convenience of removing an item. 214 | for ( var i = sdpLines.length - 1; i >= 0; i-- ) { 215 | var payload = this._extractSdp( sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i ); 216 | if ( payload ) { 217 | var cnPos = mLineElements.indexOf( payload ); 218 | if ( cnPos !== -1 ) { 219 | // Remove CN payload from m line. 220 | mLineElements.splice( cnPos, 1 ); 221 | } 222 | // Remove CN line in sdp 223 | sdpLines.splice( i, 1 ); 224 | } 225 | } 226 | sdpLines[ mLineIndex ] = mLineElements.join( ' ' ); 227 | 228 | return sdpLines; 229 | } 230 | 231 | }); 232 | 233 | W.Remote = Remote; 234 | 235 | } )( window ); -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Square Sans Cyrillic Light'; 3 | src: url('/fonts/SquareSansLight.eot'); 4 | src: url('/fonts/SquareSansLight.eot?') format('embedded-opentype'), 5 | url('/fonts/SquareSansLight.woff') format('woff'), 6 | url('/fonts/SquareSansLight.ttf') format('opentype'), 7 | url('/fonts/SquareSansLight.svg') format('svg'); 8 | font-weight: 100; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: 'Square Sans Cyrillic Thin'; 14 | src: url('/fonts/SquareSansThin.eot'); 15 | src: url('/fonts/SquareSansThin.eot?') format('embedded-opentype'), 16 | url('/fonts/SquareSansThin.woff') format('woff'), 17 | url('/fonts/SquareSansThin.ttf') format('opentype'), 18 | url('/fonts/SquareSansThin.svg') format('svg'); 19 | font-weight: 100; 20 | font-style: normal; 21 | } 22 | 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | overflow: hidden; 27 | } 28 | 29 | body, button, input { font-family:"Square Sans Cyrillic Thin", Calibri, 'Trebuchet MS', sans-serif; font-size-adjust:0.495; font-weight:200; font-style:normal; } 30 | 31 | ::selection { 32 | background: #f984e5; 33 | color: #FFF; 34 | } 35 | ::-moz-selection { 36 | background: #f984e5; 37 | color: #FFF; 38 | } 39 | 40 | .hide { 41 | visibility: hidden !important; 42 | opacity: 0 !important; 43 | -webkit-transition: all 200ms linear; 44 | -moz-transition: all 200ms linear; 45 | -o-transition: all 200ms linear; 46 | -ms-transition: all 200ms linear; 47 | transition: all 200ms linear; 48 | } 49 | 50 | .video { 51 | margin-top: 50px; 52 | position: relative; 53 | width: 100%; 54 | padding: 2px; 55 | } 56 | 57 | .video .paired { 58 | position: absolute; 59 | background-color: lightgrey; 60 | border-radius: 10px; 61 | z-index: 1; 62 | left: 50%; 63 | margin-left: -320px; 64 | width: 652px; 65 | height: 492px; 66 | } 67 | 68 | .video video { 69 | margin: 4px; 70 | } 71 | 72 | .video .paired video { 73 | margin: 6px; 74 | } 75 | 76 | .video .self { 77 | position: absolute; 78 | background-color: orange; 79 | border-radius: 5px; 80 | z-index: 15; 81 | left: 50%; 82 | margin-top: 350px; 83 | margin-left: -400px; 84 | width: 248px; 85 | height: 188px; 86 | } 87 | 88 | .appear { 89 | visibility: visible !important; 90 | opacity: 1 !important; 91 | -webkit-transition: opacity 200ms linear, visibility 200ms linear; 92 | -moz-transition: opacity 200ms linear, visibility 200ms linear; 93 | -o-transition: opacity 200ms linear, visibility 200ms linear; 94 | -ms-transition: opacity 200ms linear, visibility 200ms linear; 95 | transition: opacity 200ms linear, visibility 200ms linear; 96 | } 97 | 98 | .orange { 99 | color: orange; 100 | } 101 | 102 | .red { 103 | color: red; 104 | } 105 | 106 | .page .hello { 107 | text-align: center; 108 | font-size: 18px; 109 | } 110 | 111 | .register { 112 | position: absolute; 113 | left: 50%; 114 | top: 50%; 115 | width: 300px; 116 | height: 130px; 117 | margin-left: -150px; 118 | margin-top: -75px; 119 | border: 2px solid lightblue; 120 | text-align: center; 121 | border-radius: 10px; 122 | background-color: #f0fFf0; 123 | } 124 | 125 | .register h4 { 126 | margin-bottom: 5px; 127 | } 128 | 129 | .register input, .page .register button { 130 | font-size: 18px; 131 | } 132 | 133 | .register input { 134 | border: 2px solid grey; 135 | border-radius: 2px; 136 | outline-style: none; 137 | -webkit-transition: border-color 150ms ease-in-out; 138 | -moz-transition: border-color 150ms ease-in-out; 139 | -o-transition: border-color 150ms ease-in-out; 140 | -ms-transition: border-color 150ms ease-in-out; 141 | transition: border-color 150ms ease-in-out; 142 | } 143 | 144 | .register input:focus { 145 | border-color: orange; 146 | -webkit-transition: border-color 150ms ease-in-out; 147 | -moz-transition: border-color 150ms ease-in-out; 148 | -o-transition: border-color 150ms ease-in-out; 149 | -ms-transition: border-color 150ms ease-in-out; 150 | transition: border-color 150ms ease-in-out; 151 | } 152 | 153 | .register input.wrong { 154 | border-color: red; 155 | background-color: lightpink; 156 | -webkit-transition: border-color 150ms ease-in-out; 157 | -moz-transition: border-color 150ms ease-in-out; 158 | -o-transition: border-color 150ms ease-in-out; 159 | -ms-transition: border-color 150ms ease-in-out; 160 | transition: border-color 150ms ease-in-out; 161 | } 162 | 163 | .page .user { 164 | position: absolute; 165 | right: 0; 166 | top: 0; 167 | width: 150px; 168 | } 169 | 170 | .page .user .user_status.offline { 171 | color: red; 172 | } 173 | 174 | .page .user .user_name { 175 | color: lightblue; 176 | font-weight: bold; 177 | font-size: 24px; 178 | } 179 | 180 | .page .user .user_status.online { 181 | color: lightgreen; 182 | } 183 | 184 | .page .user .user_status.connecting { 185 | color: lightgrey; 186 | } 187 | 188 | .contacts { 189 | position: absolute; 190 | right: 0; 191 | top: 200px; 192 | width: 150px; 193 | } 194 | 195 | .contacts h4 { 196 | margin-bottom: 5px; 197 | } 198 | 199 | .contacts ul { 200 | margin-left: 0; 201 | padding-left: 0; 202 | margin-top: 5px; 203 | } 204 | 205 | .contacts ul li { 206 | list-style-type: none; 207 | } 208 | 209 | .contacts ul li.disappear { 210 | margin-left: 100%; 211 | -webkit-transition: all 250ms ease-out; 212 | -moz-transition: all 250ms ease-out; 213 | -o-transition: all 250ms ease-out; 214 | -ms-transition: all 250ms ease-out; 215 | transition: all 250ms ease-out; 216 | } 217 | 218 | .contacts ul li.appear { 219 | margin-left: 0; 220 | -webkit-transition: all 250ms ease-out; 221 | -moz-transition: all 250ms ease-out; 222 | -o-transition: all 250ms ease-out; 223 | -ms-transition: all 250ms ease-out; 224 | transition: all 250ms ease-out; 225 | } 226 | 227 | .contacts ul li a { 228 | text-decoration: none; 229 | font-weight: bold; 230 | color: #000; 231 | } 232 | 233 | .contacts ul li a:hover { 234 | background-color: orange; 235 | color: #FFF; 236 | } 237 | 238 | .contacts ul li.online span { 239 | color: orange; 240 | } 241 | 242 | .contacts ul li.offline span { 243 | color: red; 244 | } 245 | 246 | .sorry { 247 | text-align: center; 248 | padding: 5px; 249 | border: 2px solid red; 250 | background-color: #fadadd; 251 | position: absolute; 252 | width: 500px; 253 | height: 130px; 254 | border-radius: 10px; 255 | z-index: 15; 256 | margin-left: -250px; 257 | margin-top: -65px; 258 | left: 50%; 259 | top: 50%; 260 | } 261 | 262 | .footer { 263 | text-align: center; 264 | position: absolute; 265 | bottom: 0; 266 | width: 100%; 267 | } 268 | 269 | .footer a { 270 | text-decoration: none; 271 | } 272 | 273 | .ghost a { 274 | color: #000; 275 | text-decoration: none; 276 | text-shadow: none; 277 | -webkit-transition: text-shadow 250ms ease-in-out; 278 | -moz-transition: text-shadow 250ms ease-in-out; 279 | -o-transition: text-shadow 250ms ease-in-out; 280 | -ms-transition: text-shadow 250ms ease-in-out; 281 | transition: text-shadow 250ms ease-in-out; 282 | } 283 | 284 | .ghost a:hover { 285 | text-shadow: #0000FF 0 0 20px; 286 | -webkit-transition: text-shadow 250ms ease-in-out; 287 | -moz-transition: text-shadow 250ms ease-in-out; 288 | -o-transition: text-shadow 250ms ease-in-out; 289 | -ms-transition: text-shadow 250ms ease-in-out; 290 | transition: text-shadow 250ms ease-in-out; 291 | } 292 | 293 | .right-top { 294 | left: 100%; 295 | top: 0; 296 | -webkit-transition: all 750ms ease-in-out; 297 | -moz-transition: all 750ms ease-in-out; 298 | -o-transition: all 750ms ease-in-out; 299 | -ms-transition: all 750ms ease-in-out; 300 | transition: all 750ms ease-in-out; 301 | } 302 | 303 | .page.blur { 304 | -webkit-filter: blur(2px); 305 | filter: blur(3px); 306 | -webkit-transition: all 250ms ease-in-out; 307 | -moz-transition: all 250ms ease-in-out; 308 | -o-transition: all 250ms ease-in-out; 309 | -ms-transition: all 250ms ease-in-out; 310 | transition: all 250ms ease-in-out; 311 | } 312 | 313 | .page.unblur { 314 | -webkit-filter: none; 315 | filter: none; 316 | -webkit-transition: all 250ms ease-in-out; 317 | -moz-transition: all 250ms ease-in-out; 318 | -o-transition: all 250ms ease-in-out; 319 | -ms-transition: all 250ms ease-in-out; 320 | transition: all 250ms ease-in-out; 321 | } 322 | -------------------------------------------------------------------------------- /public/js/more.js: -------------------------------------------------------------------------------- 1 | // MooTools: the javascript framework. 2 | // Load this file's selection again by visiting: http://mootools.net/more/d77f91c8b0f1ea2e9e18d4f20963c591 3 | // Or build this file again with packager using: packager build More/URI More/URI.Relative More/Keyboard More/Keyboard.Extras 4 | /* 5 | --- 6 | copyrights: 7 | - [MooTools](http://mootools.net) 8 | 9 | licenses: 10 | - [MIT License](http://mootools.net/license.txt) 11 | ... 12 | */ 13 | MooTools.More={version:"1.4.0.1",build:"a4244edf2aa97ac8a196fc96082dd35af1abab87"};String.implement({parseQueryString:function(d,a){if(d==null){d=true; 14 | }if(a==null){a=true;}var c=this.split(/[&;]/),b={};if(!c.length){return b;}c.each(function(i){var e=i.indexOf("=")+1,g=e?i.substr(e):"",f=e?i.substr(0,e-1).match(/([^\]\[]+|(\B)(?=\]))/g):[i],h=b; 15 | if(!f){return;}if(a){g=decodeURIComponent(g);}f.each(function(k,j){if(d){k=decodeURIComponent(k);}var l=h[k];if(j0){c.pop(); 22 | }else{if(f!="."){c.push(f);}}});return c.join("/")+"/";},combine:function(c){return c.value||c.scheme+"://"+(c.user?c.user+(c.password?":"+c.password:"")+"@":"")+(c.host||"")+(c.port&&c.port!=this.schemes[c.scheme]?":"+c.port:"")+(c.directory||"/")+(c.file||"")+(c.query?"?"+c.query:"")+(c.fragment?"#"+c.fragment:""); 23 | },set:function(d,f,e){if(d=="value"){var c=f.match(a.regs.scheme);if(c){c=c[1];}if(c&&this.schemes[c.toLowerCase()]==null){this.parsed={scheme:c,value:f}; 24 | }else{this.parsed=this.parse(f,(e||this).parsed)||(c?{scheme:c,value:f}:{value:f});}}else{if(d=="data"){this.setData(f);}else{this.parsed[d]=f;}}return this; 25 | },get:function(c,d){switch(c){case"value":return this.combine(this.parsed,d?d.parsed:false);case"data":return this.getData();}return this.parsed[c]||""; 26 | },go:function(){document.location.href=this.toString();},toURI:function(){return this;},getData:function(e,d){var c=this.get(d||"query");if(!(c||c===0)){return e?null:{}; 27 | }var f=c.parseQueryString();return e?f[e]:f;},setData:function(c,f,d){if(typeof c=="string"){var e=this.getData();e[arguments[0]]=arguments[1];c=e;}else{if(f){c=Object.merge(this.getData(),c); 28 | }}return this.set(d||"query",Object.toQueryString(c));},clearData:function(c){return this.set(c||"query","");},toString:b,valueOf:b});a.regs={endSlash:/\/$/,scheme:/^(\w+):/,directoryDot:/\.\/|\.$/}; 29 | a.base=new a(Array.from(document.getElements("base[href]",true)).getLast(),{base:document.location});String.implement({toURI:function(c){return new a(this,c); 30 | }});})();Class.refactor=function(b,a){Object.each(a,function(e,d){var c=b.prototype[d];c=(c&&c.$origin)||c||function(){};b.implement(d,(typeof e=="function")?function(){var f=this.previous; 31 | this.previous=c;var g=e.apply(this,arguments);this.previous=f;return g;}:e);});return b;};URI=Class.refactor(URI,{combine:function(f,e){if(!e||f.scheme!=e.scheme||f.host!=e.host||f.port!=e.port){return this.previous.apply(this,arguments); 32 | }var a=f.file+(f.query?"?"+f.query:"")+(f.fragment?"#"+f.fragment:"");if(!e.directory){return(f.directory||(f.file?"":"./"))+a;}var d=e.directory.split("/"),c=f.directory.split("/"),g="",h; 33 | var b=0;for(h=0;h 2 | 3 | 10 | 11 | 12 | 13 | 15 | 18 | 21 | 25 | 28 | 29 | 31 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 45 | 47 | 49 | 51 | 54 | 55 | 58 | 61 | 62 | 64 | 65 | 66 | 67 | 69 | 72 | 73 | 75 | 77 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 88 | 89 | 91 | 93 | 95 | 97 | 100 | 102 | 104 | 105 | 107 | 108 | 110 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 122 | 124 | 126 | 128 | 130 | 132 | 134 | 136 | 137 | 139 | 141 | 142 | 145 | 147 | 149 | 151 | 153 | 154 | 156 | 158 | 160 | 161 | 163 | 165 | 167 | 168 | 171 | 172 | 175 | 177 | 178 | 179 | 181 | 183 | 185 | 188 | 189 | 192 | 194 | 195 | 198 | 199 | 201 | 202 | 204 | 205 | 207 | 209 | 212 | 214 | 215 | 217 | 219 | 220 | 222 | 224 | 225 | 227 | 228 | 231 | 233 | 235 | 237 | 239 | 241 | 243 | 245 | 247 | 248 | 250 | 252 | 253 | 255 | 257 | 259 | 261 | 263 | 265 | 267 | 269 | 271 | 273 | 275 | 278 | 280 | 282 | 285 | 287 | 288 | 290 | 292 | 295 | 297 | 299 | 301 | 303 | 305 | 307 | 308 | 310 | 311 | 313 | 315 | 316 | 318 | 320 | 322 | 324 | 326 | 328 | 330 | 332 | 334 | 336 | 338 | 341 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 367 | 368 | 369 | 370 | 372 | 374 | 376 | 378 | 380 | 381 | 382 | 383 | 384 | 386 | 387 | 388 | 394 | 395 | 397 | 399 | 400 | 402 | 404 | 405 | 406 | 408 | 409 | 410 | 414 | 417 | 420 | 423 | 424 | 425 | 428 | 431 | 434 | 437 | 438 | 439 | 440 | 442 | 444 | 447 | 450 | 451 | 452 | 455 | !"#$%&'()*+,-./0123456789:;<>? 456 | @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ 457 | `abcdefghijklmnopqrstuvwxyz|{}~ 458 | €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ 459 |  ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ 460 | ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞß 461 | àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ 462 | 463 | 464 | -------------------------------------------------------------------------------- /public/fonts/SquareSansThin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 14 | 16 | 19 | 22 | 26 | 29 | 30 | 32 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 46 | 48 | 50 | 52 | 55 | 56 | 59 | 62 | 63 | 65 | 66 | 67 | 68 | 71 | 74 | 76 | 78 | 80 | 82 | 83 | 84 | 86 | 87 | 88 | 89 | 91 | 92 | 94 | 96 | 98 | 100 | 102 | 104 | 106 | 107 | 109 | 111 | 113 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 125 | 127 | 129 | 131 | 133 | 135 | 137 | 139 | 140 | 142 | 144 | 145 | 147 | 149 | 151 | 153 | 155 | 156 | 158 | 160 | 162 | 163 | 165 | 167 | 169 | 170 | 172 | 173 | 176 | 178 | 179 | 181 | 183 | 185 | 187 | 190 | 191 | 194 | 196 | 197 | 200 | 201 | 203 | 204 | 206 | 207 | 209 | 211 | 214 | 217 | 218 | 220 | 222 | 224 | 226 | 228 | 229 | 231 | 233 | 236 | 238 | 240 | 243 | 245 | 247 | 249 | 251 | 253 | 254 | 256 | 258 | 259 | 261 | 263 | 265 | 267 | 269 | 271 | 273 | 275 | 277 | 279 | 281 | 283 | 285 | 287 | 290 | 292 | 293 | 295 | 297 | 300 | 302 | 304 | 306 | 308 | 310 | 312 | 313 | 315 | 316 | 318 | 320 | 321 | 322 | 324 | 326 | 328 | 330 | 332 | 334 | 336 | 338 | 340 | 342 | 345 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 371 | 372 | 373 | 374 | 376 | 378 | 380 | 382 | 384 | 385 | 386 | 387 | 388 | 390 | 391 | 392 | 397 | 398 | 400 | 402 | 403 | 405 | 407 | 408 | 409 | 411 | 412 | 413 | 418 | 422 | 426 | 429 | 430 | 431 | 434 | 437 | 440 | 442 | 443 | 445 | 446 | 448 | 450 | 453 | 456 | 457 | 458 | 461 | !"#$%&'()*+,-./0123456789:;<>? 462 | @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ 463 | `abcdefghijklmnopqrstuvwxyz|{}~ 464 | €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ 465 |  ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ 466 | ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞß 467 | àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ 468 | 469 | 470 | --------------------------------------------------------------------------------