whatup 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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