yaic 0.1.0 → 0.2.0

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/ralph-qa.md +101 -0
  3. data/.claude/ralph/bin/dump-pid.sh +3 -0
  4. data/.claude/ralph/bin/kill-claude +6 -0
  5. data/.claude/ralph/bin/start-ralph +44 -0
  6. data/.claude/ralph/bin/stop-hook.sh +9 -0
  7. data/.claude/ralph/bin/stop-ralph +17 -0
  8. data/.claude/ralph/prompt.md +218 -0
  9. data/.claude/settings.json +26 -0
  10. data/.gitmodules +3 -0
  11. data/CLAUDE.md +65 -0
  12. data/README.md +106 -17
  13. data/Rakefile +8 -0
  14. data/devenv.nix +1 -0
  15. data/docs/agents/data-model.md +150 -0
  16. data/docs/agents/ralph/features/01-message-parsing.md.done +160 -0
  17. data/docs/agents/ralph/features/01-tcpsocket-refactor.md.done +109 -0
  18. data/docs/agents/ralph/features/02-connection-socket.md.done +138 -0
  19. data/docs/agents/ralph/features/02-simplified-client-api.md.done +306 -0
  20. data/docs/agents/ralph/features/03-registration.md.done +147 -0
  21. data/docs/agents/ralph/features/04-ping-pong.md.done +109 -0
  22. data/docs/agents/ralph/features/05-event-system.md.done +167 -0
  23. data/docs/agents/ralph/features/06-privmsg-notice.md.done +163 -0
  24. data/docs/agents/ralph/features/07-join-part.md.done +190 -0
  25. data/docs/agents/ralph/features/08-quit.md.done +118 -0
  26. data/docs/agents/ralph/features/09-nick-change.md.done +109 -0
  27. data/docs/agents/ralph/features/10-topic.md.done +145 -0
  28. data/docs/agents/ralph/features/11-kick.md.done +122 -0
  29. data/docs/agents/ralph/features/12-names.md.done +124 -0
  30. data/docs/agents/ralph/features/13-mode.md.done +174 -0
  31. data/docs/agents/ralph/features/14-who-whois.md.done +188 -0
  32. data/docs/agents/ralph/features/15-client-api.md.done +180 -0
  33. data/docs/agents/ralph/features/16-ssl-test-infrastructure.md.done +50 -0
  34. data/docs/agents/ralph/features/17-github-actions-ci.md.done +70 -0
  35. data/docs/agents/ralph/features/18-brakeman-security-scanning.md.done +67 -0
  36. data/docs/agents/ralph/features/19-fix-qa.md.done +73 -0
  37. data/docs/agents/ralph/features/20-test-optimization.md.done +70 -0
  38. data/docs/agents/ralph/features/21-test-parallelization.md.done +56 -0
  39. data/docs/agents/ralph/features/22-wait-until-pattern.md.done +90 -0
  40. data/docs/agents/ralph/features/23-ping-test-optimization.md.done +46 -0
  41. data/docs/agents/ralph/features/24-blocking-who-whois.md.done +159 -0
  42. data/docs/agents/ralph/features/25-verbose-mode.md.done +166 -0
  43. data/docs/agents/ralph/plans/test-optimization-plan.md +172 -0
  44. data/docs/agents/ralph/progress.md +731 -0
  45. data/docs/agents/todo.md +5 -0
  46. data/lib/yaic/channel.rb +22 -0
  47. data/lib/yaic/client.rb +821 -0
  48. data/lib/yaic/event.rb +35 -0
  49. data/lib/yaic/message.rb +119 -0
  50. data/lib/yaic/registration.rb +17 -0
  51. data/lib/yaic/socket.rb +120 -0
  52. data/lib/yaic/source.rb +39 -0
  53. data/lib/yaic/version.rb +1 -1
  54. data/lib/yaic/who_result.rb +17 -0
  55. data/lib/yaic/whois_result.rb +20 -0
  56. data/lib/yaic.rb +13 -1
  57. metadata +51 -1
data/lib/yaic/event.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaic
4
+ class Event
5
+ attr_reader :type, :message
6
+
7
+ def initialize(type:, message: nil, **attributes)
8
+ @type = type
9
+ @message = message
10
+ @attributes = attributes
11
+ end
12
+
13
+ def [](key)
14
+ @attributes[key]
15
+ end
16
+
17
+ def to_h
18
+ @attributes.dup
19
+ end
20
+
21
+ private
22
+
23
+ def respond_to_missing?(method_name, include_private = false)
24
+ @attributes.key?(method_name) || super
25
+ end
26
+
27
+ def method_missing(method_name, *args)
28
+ if @attributes.key?(method_name) && args.empty?
29
+ @attributes[method_name]
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Yaic
6
+ class Message
7
+ attr_reader :tags, :source, :command, :params, :raw
8
+
9
+ def initialize(tags: {}, source: nil, command: nil, params: [])
10
+ @tags = tags
11
+ @source = source
12
+ @command = command
13
+ @params = params
14
+ @raw = nil
15
+ end
16
+
17
+ def self.parse(str)
18
+ return nil if str.nil? || str.empty?
19
+
20
+ str = str.b.force_encoding("UTF-8")
21
+ str = str.encode("UTF-8", "ISO-8859-1", invalid: :replace, undef: :replace) unless str.valid_encoding?
22
+
23
+ stripped = str.chomp("\r\n").chomp("\n")
24
+ return nil if stripped.empty?
25
+
26
+ scanner = StringScanner.new(stripped)
27
+
28
+ tags = parse_tags(scanner)
29
+ source = parse_source(scanner)
30
+ command = parse_command(scanner)
31
+ params = parse_params(scanner)
32
+
33
+ msg = new(tags: tags, source: source, command: command, params: params)
34
+ msg.instance_variable_set(:@raw, str)
35
+ msg
36
+ end
37
+
38
+ def to_s
39
+ parts = []
40
+ parts << @command
41
+
42
+ @params.each_with_index do |param, idx|
43
+ is_last = idx == @params.length - 1
44
+ parts << if is_last && needs_trailing_prefix?(param, @params.length)
45
+ ":#{param}"
46
+ else
47
+ param
48
+ end
49
+ end
50
+
51
+ "#{parts.join(" ")}\r\n"
52
+ end
53
+
54
+ private
55
+
56
+ def needs_trailing_prefix?(param, param_count)
57
+ param.empty? || param.include?(" ") || param.start_with?(":") || param_count > 1
58
+ end
59
+
60
+ class << self
61
+ private
62
+
63
+ def parse_tags(scanner)
64
+ tags = {}
65
+
66
+ if scanner.peek(1) == "@"
67
+ scanner.getch
68
+ tag_str = scanner.scan(/[^ ]+/)
69
+ skip_spaces(scanner)
70
+
71
+ tag_str&.split(";")&.each do |tag|
72
+ key, value = tag.split("=", 2)
73
+ tags[key] = value || ""
74
+ end
75
+ end
76
+
77
+ tags
78
+ end
79
+
80
+ def parse_source(scanner)
81
+ return nil unless scanner.peek(1) == ":"
82
+
83
+ scanner.getch
84
+ source_str = scanner.scan(/[^ ]+/)
85
+ skip_spaces(scanner)
86
+
87
+ Source.parse(source_str)
88
+ end
89
+
90
+ def parse_command(scanner)
91
+ cmd = scanner.scan(/[A-Za-z]+|\d{3}/)
92
+ skip_spaces(scanner)
93
+ cmd
94
+ end
95
+
96
+ def parse_params(scanner)
97
+ params = []
98
+
99
+ until scanner.eos?
100
+ if scanner.peek(1) == ":"
101
+ scanner.getch
102
+ params << scanner.rest
103
+ break
104
+ else
105
+ param = scanner.scan(/[^ ]+/)
106
+ params << param if param
107
+ skip_spaces(scanner)
108
+ end
109
+ end
110
+
111
+ params
112
+ end
113
+
114
+ def skip_spaces(scanner)
115
+ scanner.skip(/ +/)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaic
4
+ module Registration
5
+ def self.pass_message(password)
6
+ Message.new(command: "PASS", params: [password])
7
+ end
8
+
9
+ def self.nick_message(nickname)
10
+ Message.new(command: "NICK", params: [nickname])
11
+ end
12
+
13
+ def self.user_message(username, realname)
14
+ Message.new(command: "USER", params: [username, "0", "*", realname])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "openssl"
5
+
6
+ module Yaic
7
+ class Socket
8
+ def state
9
+ @monitor.synchronize { @state }
10
+ end
11
+
12
+ def initialize(host, port, ssl: false, verify_mode: nil, connect_timeout: 30)
13
+ @host = host
14
+ @port = port
15
+ @ssl = ssl
16
+ @verify_mode = verify_mode || OpenSSL::SSL::VERIFY_NONE
17
+ @connect_timeout = connect_timeout
18
+
19
+ @socket = nil
20
+ @read_buffer = String.new(encoding: Encoding::ASCII_8BIT)
21
+ @write_queue = []
22
+ @state = :disconnected
23
+ @monitor = Monitor.new
24
+ end
25
+
26
+ def connect
27
+ tcp_socket = TCPSocket.new(@host, @port, connect_timeout: @connect_timeout)
28
+ tcp_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
29
+
30
+ @monitor.synchronize do
31
+ @socket = @ssl ? wrap_ssl(tcp_socket) : tcp_socket
32
+ @state = :connecting
33
+ end
34
+ rescue Errno::ETIMEDOUT, Errno::EHOSTUNREACH, Errno::ECONNREFUSED, IO::TimeoutError, SocketError => e
35
+ raise Yaic::ConnectionError, e.message
36
+ end
37
+
38
+ def disconnect
39
+ @monitor.synchronize do
40
+ return if @state == :disconnected
41
+
42
+ begin
43
+ @socket&.close
44
+ rescue
45
+ nil
46
+ end
47
+ @socket = nil
48
+ @read_buffer.clear
49
+ @write_queue.clear
50
+ @state = :disconnected
51
+ end
52
+ end
53
+
54
+ def read
55
+ @monitor.synchronize do
56
+ return nil if @socket.nil?
57
+
58
+ begin
59
+ data = @socket.read_nonblock(4096)
60
+ buffer_data(data)
61
+ extract_message
62
+ rescue IO::WaitReadable
63
+ extract_message
64
+ rescue IOError, Errno::ECONNRESET
65
+ nil
66
+ end
67
+ end
68
+ end
69
+
70
+ def write(message)
71
+ @monitor.synchronize do
72
+ return if @socket.nil?
73
+
74
+ message = message.dup
75
+ message << "\r\n" unless message.end_with?("\r\n", "\n")
76
+
77
+ begin
78
+ @socket.write_nonblock(message)
79
+ rescue IO::WaitWritable
80
+ @write_queue << message
81
+ flush_write_queue
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def wrap_ssl(tcp_socket)
89
+ context = OpenSSL::SSL::SSLContext.new
90
+ context.verify_mode = @verify_mode
91
+
92
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, context)
93
+ ssl_socket.hostname = @host
94
+ ssl_socket.sync = true
95
+ ssl_socket.connect
96
+ ssl_socket
97
+ end
98
+
99
+ def buffer_data(data)
100
+ @read_buffer << data
101
+ end
102
+
103
+ def extract_message
104
+ if (idx = @read_buffer.index("\n"))
105
+ @read_buffer.slice!(0..idx)
106
+ end
107
+ end
108
+
109
+ def flush_write_queue
110
+ while @write_queue.any?
111
+ begin
112
+ @socket.write_nonblock(@write_queue.first)
113
+ @write_queue.shift
114
+ rescue IO::WaitWritable
115
+ break
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaic
4
+ class Source
5
+ attr_reader :nick, :user, :host, :raw
6
+
7
+ def initialize(nick: nil, user: nil, host: nil, raw: nil)
8
+ @nick = nick
9
+ @user = user
10
+ @host = host
11
+ @raw = raw
12
+ end
13
+
14
+ def self.parse(str)
15
+ return nil if str.nil? || str.empty?
16
+
17
+ nick = nil
18
+ user = nil
19
+ host = nil
20
+
21
+ if str.include?("!")
22
+ nick, rest = str.split("!", 2)
23
+ if rest.include?("@")
24
+ user, host = rest.split("@", 2)
25
+ else
26
+ user = rest
27
+ end
28
+ elsif str.include?("@")
29
+ nick, host = str.split("@", 2)
30
+ elsif str.include?(".")
31
+ host = str
32
+ else
33
+ nick = str
34
+ end
35
+
36
+ new(nick: nick, user: user, host: host, raw: str)
37
+ end
38
+ end
39
+ end
data/lib/yaic/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yaic
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaic
4
+ class WhoResult
5
+ attr_reader :channel, :user, :host, :server, :nick, :away, :realname
6
+
7
+ def initialize(channel:, user:, host:, server:, nick:, away:, realname:)
8
+ @channel = channel
9
+ @user = user
10
+ @host = host
11
+ @server = server
12
+ @nick = nick
13
+ @away = away
14
+ @realname = realname
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaic
4
+ class WhoisResult
5
+ attr_accessor :nick, :user, :host, :realname, :channels, :server, :idle, :signon, :account, :away
6
+
7
+ def initialize(nick:)
8
+ @nick = nick
9
+ @user = nil
10
+ @host = nil
11
+ @realname = nil
12
+ @channels = []
13
+ @server = nil
14
+ @idle = nil
15
+ @signon = nil
16
+ @account = nil
17
+ @away = nil
18
+ end
19
+ end
20
+ end
data/lib/yaic.rb CHANGED
@@ -1,8 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
4
+
3
5
  require_relative "yaic/version"
6
+ require_relative "yaic/source"
7
+ require_relative "yaic/message"
8
+ require_relative "yaic/socket"
9
+ require_relative "yaic/registration"
10
+ require_relative "yaic/event"
11
+ require_relative "yaic/channel"
12
+ require_relative "yaic/who_result"
13
+ require_relative "yaic/whois_result"
14
+ require_relative "yaic/client"
4
15
 
5
16
  module Yaic
6
17
  class Error < StandardError; end
7
- # Your code goes here...
18
+ class TimeoutError < Error; end
19
+ class ConnectionError < Error; end
8
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joé Dupuis
@@ -15,17 +15,67 @@ executables: []
15
15
  extensions: []
16
16
  extra_rdoc_files: []
17
17
  files:
18
+ - ".claude/agents/ralph-qa.md"
19
+ - ".claude/ralph/bin/dump-pid.sh"
20
+ - ".claude/ralph/bin/kill-claude"
21
+ - ".claude/ralph/bin/start-ralph"
22
+ - ".claude/ralph/bin/stop-hook.sh"
23
+ - ".claude/ralph/bin/stop-ralph"
24
+ - ".claude/ralph/prompt.md"
25
+ - ".claude/settings.json"
18
26
  - ".dockerignore"
27
+ - ".gitmodules"
19
28
  - ".ruby-version"
20
29
  - CHANGELOG.md
30
+ - CLAUDE.md
21
31
  - LICENSE.txt
22
32
  - README.md
23
33
  - Rakefile
24
34
  - devenv.lock
25
35
  - devenv.nix
26
36
  - devenv.yaml
37
+ - docs/agents/data-model.md
38
+ - docs/agents/ralph/features/01-message-parsing.md.done
39
+ - docs/agents/ralph/features/01-tcpsocket-refactor.md.done
40
+ - docs/agents/ralph/features/02-connection-socket.md.done
41
+ - docs/agents/ralph/features/02-simplified-client-api.md.done
42
+ - docs/agents/ralph/features/03-registration.md.done
43
+ - docs/agents/ralph/features/04-ping-pong.md.done
44
+ - docs/agents/ralph/features/05-event-system.md.done
45
+ - docs/agents/ralph/features/06-privmsg-notice.md.done
46
+ - docs/agents/ralph/features/07-join-part.md.done
47
+ - docs/agents/ralph/features/08-quit.md.done
48
+ - docs/agents/ralph/features/09-nick-change.md.done
49
+ - docs/agents/ralph/features/10-topic.md.done
50
+ - docs/agents/ralph/features/11-kick.md.done
51
+ - docs/agents/ralph/features/12-names.md.done
52
+ - docs/agents/ralph/features/13-mode.md.done
53
+ - docs/agents/ralph/features/14-who-whois.md.done
54
+ - docs/agents/ralph/features/15-client-api.md.done
55
+ - docs/agents/ralph/features/16-ssl-test-infrastructure.md.done
56
+ - docs/agents/ralph/features/17-github-actions-ci.md.done
57
+ - docs/agents/ralph/features/18-brakeman-security-scanning.md.done
58
+ - docs/agents/ralph/features/19-fix-qa.md.done
59
+ - docs/agents/ralph/features/20-test-optimization.md.done
60
+ - docs/agents/ralph/features/21-test-parallelization.md.done
61
+ - docs/agents/ralph/features/22-wait-until-pattern.md.done
62
+ - docs/agents/ralph/features/23-ping-test-optimization.md.done
63
+ - docs/agents/ralph/features/24-blocking-who-whois.md.done
64
+ - docs/agents/ralph/features/25-verbose-mode.md.done
65
+ - docs/agents/ralph/plans/test-optimization-plan.md
66
+ - docs/agents/ralph/progress.md
67
+ - docs/agents/todo.md
27
68
  - lib/yaic.rb
69
+ - lib/yaic/channel.rb
70
+ - lib/yaic/client.rb
71
+ - lib/yaic/event.rb
72
+ - lib/yaic/message.rb
73
+ - lib/yaic/registration.rb
74
+ - lib/yaic/socket.rb
75
+ - lib/yaic/source.rb
28
76
  - lib/yaic/version.rb
77
+ - lib/yaic/who_result.rb
78
+ - lib/yaic/whois_result.rb
29
79
  - sig/yaic.rbs
30
80
  homepage: https://github.com/JoeDupuis/yaic
31
81
  licenses: