whatup 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 205d1b22572393ab8a0fb12bc09edd48576ac740ab484e76e10511afaf080550
4
- data.tar.gz: 26648b7a519d2eaeb15149793933d840965c045c6bf911acd6608a365dfa17f8
3
+ metadata.gz: 391ddb29c708ee7cf37769dc7aa3723d65e420fe08a268862bff6ec7ff5e659f
4
+ data.tar.gz: b7e349838c88b72f3fbb04258afa114b124f460af40b99d64654bb5b941c04f3
5
5
  SHA512:
6
- metadata.gz: 5c0dd6fa2a58e09dd1443527e1c58102060f1f1d5fa224c29772e6a86ee89a2c216c4bf43b22aef85a5a3d18b7ccead298860533487e3ec23e7c2e379ec7f956
7
- data.tar.gz: 8ed6007535ff1054232ad3e53e6d26eb758e2ebb7d379fa2046bab4601880309156fc45949e67a3e3c9e3136df6006a499fb7bebb2f5eb138c811e6fe1137f25
6
+ metadata.gz: 6f8458ee475df4aa40d81cd3c126cea7d0153ba3ab2c3d56ec7ff92e130ae1b2765b5594a7a780584498a74d2808779d7024e412d724acad919665b8b339acf3
7
+ data.tar.gz: e84e5379f28eec45311b4ab0aeea6ec67b45dadea2240f06f3c7dcc33a4094784ad081e6fc1bffa8e57d0c4cbc30248c7a55e663923d95e73b1de89698355316
data/.gitignore CHANGED
@@ -11,7 +11,7 @@
11
11
  .rspec_status
12
12
 
13
13
  # vagrant
14
- vagrant/.vagrant
14
+ vagrant/*
15
15
  **/*.log
16
16
 
17
17
  # Since this is a gem, don't include the lock file
data/.rubocop.yml CHANGED
@@ -3,10 +3,32 @@
3
3
  AllCops:
4
4
  TargetRubyVersion: 2.4 # Modern Ruby
5
5
 
6
+ # attr_reader *%i[a b c]
7
+ Lint/UnneededSplatExpansion:
8
+ Enabled: false
9
+
10
+ # Needed for the server pid
11
+ Style/GlobalVars:
12
+ Exclude:
13
+ - 'spec/system_spec.rb'
14
+
15
+ # Aruba uses modifiable strings
16
+ Style/FrozenStringLiteralComment:
17
+ Exclude:
18
+ - 'spec/system_spec.rb'
19
+
6
20
  # The default is a bit restrictive
7
21
  Metrics/AbcSize:
8
22
  Max: 30
9
23
 
24
+ # The default is a bit restrictive
25
+ Metrics/ClassLength:
26
+ Max: 145
27
+
28
+ # <<~OUTPUT.gsub /^\s+/, ''
29
+ Lint/AmbiguousRegexpLiteral:
30
+ Enabled: false
31
+
10
32
  Metrics/BlockLength:
11
33
  Exclude:
12
34
  - 'whatup.gemspec'
data/README.md CHANGED
@@ -6,6 +6,8 @@ whatup is a simple server-based instant messaging application
6
6
 
7
7
  Check it out on [rubygems.org](https://rubygems.org/gems/whatup).
8
8
 
9
+ [![Build Status](https://travis-ci.com/jethrodaniel/whatup.svg?branch=dev)](https://travis-ci.com/jethrodaniel/whatup)
10
+
9
11
  ## Installation
10
12
 
11
13
  Assuming you have Ruby 2.4 or greater installed,
data/Rakefile CHANGED
@@ -3,6 +3,6 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ RSpec::Core::RakeTask.new :spec
7
7
 
8
8
  task default: :spec
Binary file
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ require 'whatup/client/client'
6
+
7
+ module Whatup
8
+ module CLI
9
+ # Any methods of class `Whatup::CLI::Interactive` that rely on instance
10
+ # variables should be included here
11
+ COMMANDS = %i[room list exit].freeze
12
+
13
+ require 'whatup/cli/commands/interactive/setup'
14
+
15
+ # Interactive client commands that are available after connecting
16
+ #
17
+ # This class is run on the server
18
+ class Interactive < Thor
19
+ prepend InteractiveSetup
20
+
21
+ attr_accessor *%i[server current_user]
22
+
23
+ no_commands do
24
+ # Checks whether a given input string is a valid command
25
+ def self.command? msg
26
+ cmds, opts = parse_input(msg)
27
+ parsed_args = new(cmds, opts).args
28
+ commands.key?(parsed_args.first) || parsed_args.first == 'help'
29
+ end
30
+
31
+ # Parses a client's input into a format suitable for Thor commands
32
+ #
33
+ # @param [String] msg - the client's message
34
+ def self.parse_input msg
35
+ # Split user input at the first "option"
36
+ cmds, opts = msg&.split /-|--/, 2
37
+
38
+ # Then split each by whitespace
39
+ cmds = cmds&.split(/\s+/)
40
+ opts = opts&.split(/\s+/)
41
+
42
+ # `Whatup::CLI::Interactive.new(cmds, opts)` expects arrays, and
43
+ # a final options hash
44
+ cmds = [] if cmds.nil?
45
+ opts = [] if opts.nil?
46
+
47
+ [cmds, opts]
48
+ end
49
+ end
50
+
51
+ # Don't show app name in command help, i.e, instead of
52
+ # `app command desc`, use `command desc`
53
+ def self.banner task, _namespace = false, subcommand = false
54
+ task.formatted_usage(self, false, subcommand).to_s
55
+ end
56
+
57
+ desc 'list', 'Show all connected clients'
58
+ def list
59
+ say 'All connected clients:'
60
+ @server.clients.each { |c| say c.status }
61
+ end
62
+
63
+ desc 'room [NAME]', 'Create and enter chatroom [NAME]'
64
+ def room name
65
+ if room = @server.rooms.select { |r| r.name == name }&.first
66
+ @current_user.puts <<~MSG
67
+ Entering #{room.name}... enjoy your stay!
68
+
69
+ Type `.exit` to exit this chat room.
70
+
71
+ Currently in this room:
72
+ #{room.clients.map do |client|
73
+ "- #{client.name}\n"
74
+ end.join}
75
+ MSG
76
+ @current_user.room = room
77
+
78
+ room.broadcast except: @current_user do
79
+ <<~MSG
80
+ #{@current_user.name} has arrived! Play nice, kids.
81
+ MSG
82
+ end
83
+
84
+ room.clients << @current_user
85
+ return
86
+ end
87
+
88
+ room = @server.new_room! name: name, clients: [@current_user]
89
+
90
+ @current_user.puts <<~MSG
91
+ Created and entered #{room.name}... invite some people or something!
92
+
93
+ Type `.exit` to exit this chat room.
94
+ MSG
95
+ end
96
+
97
+ # desc 'dm [NAME]', 'Send a direct message to [NAME]'
98
+ # def dm name
99
+ # client = @server.find_client_by name: name
100
+
101
+ # if client.nil?
102
+ # @current_user.puts "No client named `#{name}` found!"
103
+ # return
104
+ # end
105
+
106
+ # client.send_message
107
+ # end
108
+
109
+ desc 'exit', 'Closes your connection with the server'
110
+ def exit
111
+ @current_user.exit!
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatup
4
+ module CLI
5
+ # Implements a `before` hook to set the correct instance variables
6
+ # before any command methods.
7
+ #
8
+ # This is needed, since Thor creates another cli class instance when it is
9
+ # called with `invoke`, and we need to reassign any variables to the new
10
+ # cli instance.
11
+ #
12
+ # TODO: grab commands dynamically
13
+ module InteractiveSetup
14
+ Whatup::CLI::COMMANDS.each do |cmd|
15
+ define_method cmd do |*args|
16
+ cli = instance_variable_get(:@_initializer).last[:shell].base
17
+ @server = cli.server
18
+ @current_user = cli.current_user
19
+ super *args
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -20,6 +20,7 @@ module Whatup
20
20
 
21
21
  @socket = TCPSocket.open @dest[:ip], @dest[:port]
22
22
 
23
+ puts 'Please enter your username to establish a connection...'
23
24
  @request = request!
24
25
  @response = listen!
25
26
 
@@ -29,12 +30,13 @@ module Whatup
29
30
  exit
30
31
  end
31
32
 
33
+ private
34
+
35
+ # Loop and send all input to the server
32
36
  def request!
33
- puts 'Please enter your username to establish a connection...'
34
37
  Thread.new do
35
38
  loop do
36
- print '> '
37
- input = $stdin.gets&.chomp
39
+ input = Readline.readline '~> ', true
38
40
  exit if input.nil?
39
41
  @socket.puts input
40
42
  end
@@ -44,11 +46,15 @@ module Whatup
44
46
  @socket.close
45
47
  end
46
48
 
49
+ # Continually listen to the server, and print anything received
47
50
  def listen!
48
51
  Thread.new do
49
52
  loop do
50
53
  response = @socket.gets&.chomp
51
- puts response unless response.nil?
54
+
55
+ exit if response == 'END'
56
+
57
+ puts response # unless response.nil?
52
58
  end
53
59
  end
54
60
  rescue IOError => e
@@ -4,20 +4,52 @@ module Whatup
4
4
  module Server
5
5
  class Client
6
6
  attr_reader :id, :name
7
- attr_accessor :socket
7
+ attr_accessor *%i[socket room]
8
8
 
9
9
  def initialize id:, name:, socket:
10
10
  @id = id
11
11
  @name = name
12
12
  @socket = socket
13
+ @room = nil
13
14
  end
14
15
 
15
16
  def puts msg
16
- @socket.puts msg
17
+ socket.puts msg
17
18
  end
18
19
 
19
20
  def gets
20
- @socket.gets
21
+ socket.gets
22
+ end
23
+
24
+ def input!
25
+ loop while (msg = gets).blank?
26
+ msg.chomp
27
+ end
28
+
29
+ def room?
30
+ !room.nil?
31
+ end
32
+ alias chatting? room?
33
+
34
+ def status
35
+ "#{name}" \
36
+ "#{chatting? ? " (#{@room.name})" : ''}"
37
+ end
38
+
39
+ def broadcast msg
40
+ @room.clients.reject { |c| c == self }
41
+ .each { |c| c.puts "#{name}> #{msg}" }
42
+ end
43
+
44
+ def leave_room!
45
+ broadcast 'LEFT'
46
+ room.drop_client! self
47
+ @room = nil
48
+ end
49
+
50
+ def exit!
51
+ puts "END\n"
52
+ Thread.kill Thread.current
21
53
  end
22
54
  end
23
55
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatup
4
+ module Server
5
+ class Room
6
+ attr_accessor *%i[name clients]
7
+
8
+ def initialize name:, clients:
9
+ @name = name
10
+ @clients = clients
11
+
12
+ @clients.each { |c| c.room = self }
13
+ end
14
+
15
+ def drop_client! client
16
+ @clients = @clients.reject { |c| c == client }
17
+ end
18
+
19
+ def broadcast except: nil
20
+ clients = except \
21
+ ? @clients.reject { |c| c == except }
22
+ : @clients
23
+
24
+ clients.each { |c| c.puts yield }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,8 +2,13 @@
2
2
 
3
3
  require 'socket'
4
4
  require 'fileutils'
5
+ require 'securerandom'
6
+
7
+ require 'active_support/core_ext/object/blank'
5
8
 
6
9
  require 'whatup/server/client'
10
+ require 'whatup/server/room'
11
+ require 'whatup/cli/commands/interactive/interactive'
7
12
 
8
13
  module Whatup
9
14
  module Server
@@ -12,18 +17,26 @@ module Whatup
12
17
 
13
18
  Client = Whatup::Server::Client
14
19
 
20
+ # Used by the interactive client cli
21
+ attr_reader *%i[ip port address clients max_id pid pid_file rooms]
22
+
15
23
  def initialize port:
16
24
  @ip = 'localhost'
17
25
  @port = port
18
26
  @address = "#{@ip}:#{@port}"
19
27
 
20
28
  @clients = []
29
+ @rooms = []
21
30
  @max_id = 1
22
31
 
23
32
  @pid = Process.pid
24
33
  @pid_file = "#{Dir.home}/.whatup.pid"
25
34
  end
26
35
 
36
+ # Starts the server.
37
+ #
38
+ # The server continuously loops, and handle each new client in a separate
39
+ # thread.
27
40
  def start
28
41
  say "Starting a server with PID:#{@pid} @ #{@address} ... \n", :green
29
42
 
@@ -33,34 +46,136 @@ module Whatup
33
46
 
34
47
  # Listen for connections, then accept each in a separate thread
35
48
  loop do
36
- Thread.new @socket.accept do |client|
37
- handle_client client
49
+ Thread.new(@socket.accept) do |client|
50
+ case handle_client client
51
+ when :exit
52
+ client.puts 'bye!'
53
+ Thread.kill Thread.current
54
+ end
38
55
  end
39
56
  end
40
- rescue SignalException
57
+ rescue SignalException # In case of ^c
41
58
  kill
42
59
  end
43
60
 
61
+ def find_client_by name:
62
+ @clients.select { |c| c.name == name }&.first
63
+ end
64
+
65
+ def new_room! clients: [], name:
66
+ room = Room.new name: name, clients: clients
67
+ @rooms << room
68
+ room
69
+ end
70
+
44
71
  private
45
72
 
73
+ # Receives a new client, then continuously gets input from that client
74
+ #
75
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
46
76
  def handle_client client
77
+ client = create_new_client_if_not_existing! client
78
+
79
+ # Loop forever to maintain the connection
80
+ loop do
81
+ handle_chatting(client) if client.chatting?
82
+
83
+ # Wait until we get a valid command. This takes as long as the client
84
+ # takes.
85
+ msg = client.input! unless Whatup::CLI::Interactive.command?(msg)
86
+
87
+ puts "#{client.name}> #{msg}"
88
+
89
+ # Initialize a new cli class using the initial command and options,
90
+ # and then set any instance variables, since Thor will create a new
91
+ # class instance when it's invoked.
92
+ cmds, opts = Whatup::CLI::Interactive.parse_input msg
93
+ cli = Whatup::CLI::Interactive.new(cmds, opts).tap do |c|
94
+ c.server = self
95
+ c.current_user = client
96
+ end
97
+
98
+ begin
99
+ # Send the output to the client
100
+ redirect stdin: client.socket, stdout: client.socket do
101
+ # Invoke the cli using the provided commands and options.
102
+
103
+ # This _should_ achieve the same effect as
104
+ # `Whatup::CLI::Interactive.start(args)`, but allows us to set
105
+ # instance variables on the cli class.
106
+ cli.invoke cli.args.first, cli.args[1..cli.args.size - 1]
107
+ end
108
+ rescue RuntimeError,
109
+ Thor::InvocationError,
110
+ Thor::UndefinedCommandError => e
111
+ puts e.message
112
+ client.puts 'Invalid input or unknown command'
113
+ rescue ArgumentError => e
114
+ puts e.message
115
+ client.puts e.message
116
+ end
117
+ msg = nil # rubocop:disable Lint/UselessAssignment
118
+ end
119
+ end
120
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
121
+
122
+ def handle_chatting client
123
+ loop do
124
+ input = client.input!
125
+ room = client.room
126
+ puts "#{client.name}> #{input}"
127
+ if input == '.exit'
128
+ client.leave_room!
129
+ break
130
+ end
131
+ room.broadcast except: client do
132
+ "#{client.name}> #{input}"
133
+ end
134
+ end
135
+ end
136
+
137
+ # Receives a username from a client, then creates a new client unless a
138
+ # client with that username already exists.
139
+ #
140
+ # If no username is provided (i.e, blank), it assigns a random, anonymous
141
+ # username in the format `ANON-xxx`, where `xxx` is a random number upto
142
+ # 100, left-padded with zeros.
143
+ def create_new_client_if_not_existing! client
144
+ name = client.gets.chomp
145
+ rand_num = SecureRandom.random_number(100).to_s.rjust 3, '0'
146
+ name = name == '' ? "ANON-#{rand_num}" : name
147
+
148
+ if @clients.any? { |c| c.name == name }
149
+ client.puts 'That name is taken! Goodbye.'
150
+ client.exit!
151
+ end
152
+
47
153
  @clients << client = Client.new(
48
- id: @max_id += 1,
49
- name: client.gets.chomp,
154
+ id: new_client_id,
155
+ name: name,
50
156
  socket: client
51
157
  )
52
158
 
53
159
  puts "#{client.name} just showed up!"
54
160
  client.puts "Hello, #{client.name}!"
161
+ client
162
+ end
55
163
 
56
- loop do
57
- msg = client.gets&.chomp
58
- puts "#{client.name}> #{msg}" unless msg.nil? || msg == ''
164
+ # @return A new, unique client identification number
165
+ def new_client_id
166
+ @max_id += 1
167
+ end
59
168
 
60
- @clients.reject { |c| c.id == client.id }.each do |c|
61
- c.puts "\n#{client.name}> #{msg}" unless msg.nil? || msg == ''
62
- end
63
- end
169
+ # Reroutes stdin and stdout inside a block
170
+ def redirect stdin: $stdin, stdout: $stdout
171
+ original_stdin = $stdin
172
+ original_stdout = $stdout
173
+ $stdin = stdin
174
+ $stdout = stdout
175
+ yield
176
+ ensure
177
+ $stdin = original_stdin
178
+ $stdout = original_stdout
64
179
  end
65
180
 
66
181
  def exit_if_pid_exists!
@@ -92,6 +207,7 @@ module Whatup
92
207
  def kill
93
208
  say "Killing the server with PID:#{Process.pid} ...", :red
94
209
  FileUtils.rm_rf @pid_file
210
+ exit
95
211
  end
96
212
  end
97
213
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whatup
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.4'
5
5
  end
data/whatup.gemspec CHANGED
@@ -56,7 +56,9 @@ Gem::Specification.new do |spec|
56
56
  'rake' => '~> 12.3.2',
57
57
  'rspec' => '~> 3.8',
58
58
  'yard' => '~> 0.9.18',
59
- 'rubocop' => '~> 0.65.0'
59
+ 'rubocop' => '~> 0.65.0',
60
+ 'aruba' => '~> 0.14.9',
61
+ 'activesupport' => '~> 5.2'
60
62
  }.each do |gem, version|
61
63
  spec.add_development_dependency gem, version
62
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whatup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Delk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-23 00:00:00.000000000 Z
11
+ date: 2019-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -108,6 +108,34 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 0.65.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: aruba
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.14.9
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.14.9
125
+ - !ruby/object:Gem::Dependency
126
+ name: activesupport
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '5.2'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '5.2'
111
139
  - !ruby/object:Gem::Dependency
112
140
  name: thor
113
141
  requirement: !ruby/object:Gem::Requirement
@@ -151,15 +179,19 @@ files:
151
179
  - bin/console
152
180
  - bin/setup
153
181
  - docs/hw/assignment.pdf
182
+ - docs/hw/framework.pdf
154
183
  - docs/hw/inital_design.md
155
184
  - docs/installing_ruby.md
156
185
  - exe/whatup
157
186
  - lib/whatup.rb
158
187
  - lib/whatup/cli/cli.rb
159
188
  - lib/whatup/cli/commands/client.rb
189
+ - lib/whatup/cli/commands/interactive/interactive.rb
190
+ - lib/whatup/cli/commands/interactive/setup.rb
160
191
  - lib/whatup/cli/commands/server.rb
161
192
  - lib/whatup/client/client.rb
162
193
  - lib/whatup/server/client.rb
194
+ - lib/whatup/server/room.rb
163
195
  - lib/whatup/server/server.rb
164
196
  - lib/whatup/version.rb
165
197
  - whatup.gemspec