├── 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 | 
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
464 |
--------------------------------------------------------------------------------
/public/fonts/SquareSansThin.svg:
--------------------------------------------------------------------------------
1 |
470 |
--------------------------------------------------------------------------------