backchannel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "backchannel"
3
+ spec.version = "0.1.0"
4
+ spec.platform = Gem::Platform::RUBY
5
+ spec.license = "MIT"
6
+ spec.authors = ["John Pignata"]
7
+ spec.email = ["john@pignata.com"]
8
+ spec.summary = "Multicast-based peer-to-peer text chat"
9
+ spec.description = "Sample application from article posted on http://tx.pignata.com"
10
+ spec.files = `git ls-files`.split("\n")
11
+ spec.executables = %w(backchannel)
12
+ spec.require_paths = ["lib"]
13
+
14
+ spec.required_ruby_version = ">= 1.9.3"
15
+ end
data/bin/backchannel ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << "./lib"
4
+
5
+ if handle = ARGV[0]
6
+ trap(:INT) { abort }
7
+ require "backchannel"
8
+ Backchannel.start(handle)
9
+ else
10
+ abort "Usage: bcc <HANDLE>"
11
+ end
12
+
@@ -0,0 +1,12 @@
1
+ require_relative "backchannel/client"
2
+ require_relative "backchannel/window"
3
+ require_relative "backchannel/message"
4
+
5
+ class Backchannel
6
+ def self.start(handle)
7
+ client = Client.new(handle)
8
+ window = Window.new(client)
9
+
10
+ window.start
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ require "socket"
2
+ require "thread"
3
+ require "ipaddr"
4
+ require "securerandom"
5
+
6
+ class Client
7
+ MULTICAST_ADDR = "224.6.8.11"
8
+ BIND_ADDR = "0.0.0.0"
9
+ PORT = 6811
10
+
11
+ def initialize(handle)
12
+ @handle = handle
13
+ @client_id = SecureRandom.hex(5)
14
+ @listeners = []
15
+ end
16
+
17
+ def add_message_listener(listener)
18
+ listen unless listening?
19
+ @listeners << listener
20
+ end
21
+
22
+ def transmit(content)
23
+ message = Message.new(
24
+ "client_id" => @client_id,
25
+ "handle" => @handle,
26
+ "content" => content
27
+ )
28
+
29
+ socket.send(message.to_json, 0, MULTICAST_ADDR, PORT)
30
+ message
31
+ end
32
+
33
+ private
34
+
35
+ def listen
36
+ socket.bind(BIND_ADDR, PORT)
37
+
38
+ Thread.new do
39
+ loop do
40
+ attributes, _ = socket.recvfrom(1024)
41
+ message = Message.inflate(attributes)
42
+
43
+ unless message.client_id == @client_id
44
+ @listeners.each { |listener| listener.new_message(message) }
45
+ end
46
+ end
47
+ end
48
+
49
+ @listening = true
50
+ end
51
+
52
+ def listening?
53
+ @listening == true
54
+ end
55
+
56
+ def socket
57
+ @socket ||= UDPSocket.open.tap do |socket|
58
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, bind_address)
59
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 1)
60
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, 1)
61
+ end
62
+ end
63
+
64
+ def bind_address
65
+ IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ class Message
4
+ attr_reader :client_id, :handle, :content
5
+
6
+ def self.inflate(json)
7
+ attributes = JSON.parse(json)
8
+ new(attributes)
9
+ end
10
+
11
+ def initialize(attributes={})
12
+ @client_id = attributes.fetch("client_id")
13
+ @handle = attributes.fetch("handle")
14
+ @content = attributes.fetch("content")
15
+ end
16
+
17
+ def to_json
18
+ { client_id: client_id, handle: handle, content: content }.to_json
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ require "curses"
2
+
3
+ class Window
4
+ include Curses
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ @messages = []
9
+ end
10
+
11
+ def start
12
+ init_screen
13
+ start_color
14
+ init_pair(COLOR_WHITE, COLOR_BLACK, COLOR_WHITE)
15
+ redraw
16
+
17
+ @client.add_message_listener(self)
18
+
19
+ loop do
20
+ capture_input
21
+ end
22
+ end
23
+
24
+ def new_message(message)
25
+ @messages << message
26
+ redraw
27
+ end
28
+
29
+ private
30
+
31
+ def capture_input
32
+ content = getstr
33
+
34
+ if content.length > 0
35
+ message = @client.transmit(content)
36
+ new_message(message)
37
+ end
38
+ end
39
+
40
+ def redraw
41
+ draw_text_field
42
+ draw_messages
43
+ cursor_to_input_line
44
+ refresh
45
+ end
46
+
47
+ def draw_text_field
48
+ setpos(divider_line, 0)
49
+ attron(color_pair(COLOR_WHITE) | A_NORMAL) do
50
+ addstr(" [backchannel]" + " " * cols)
51
+ end
52
+
53
+ cursor_to_input_line
54
+ clrtoeol
55
+ end
56
+
57
+ def draw_messages
58
+ @messages.last(window_line_size).inject(0) do |line_number, message|
59
+ setpos(line_number, 0)
60
+ clrtoeol
61
+ addstr("<#{message.handle}> #{message.content}")
62
+ line_number + 1
63
+ end
64
+ end
65
+
66
+ def input_line
67
+ lines - 1
68
+ end
69
+
70
+ def divider_line
71
+ lines - 2
72
+ end
73
+
74
+ def window_line_size
75
+ lines - 2
76
+ end
77
+
78
+ def cursor_to_input_line
79
+ setpos(input_line, 0)
80
+ end
81
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: backchannel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Pignata
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-01 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Sample application from article posted on http://tx.pignata.com
15
+ email:
16
+ - john@pignata.com
17
+ executables:
18
+ - backchannel
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - backchannel.gemspec
23
+ - bin/backchannel
24
+ - lib/backchannel.rb
25
+ - lib/backchannel/client.rb
26
+ - lib/backchannel/message.rb
27
+ - lib/backchannel/window.rb
28
+ homepage:
29
+ licenses:
30
+ - MIT
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.9.3
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 1.8.24
50
+ signing_key:
51
+ specification_version: 3
52
+ summary: Multicast-based peer-to-peer text chat
53
+ test_files: []