wabot 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ffe9be4b97b0465e99c58023ae6e95738d3303cecafe11837c005d2e3cdfadae
4
+ data.tar.gz: 25861a984ae565f07f83e5db58389f5eb78274761b2912e4107e8dbe9ea6e598
5
+ SHA512:
6
+ metadata.gz: a6bf9f916fb6b428d4ab78322fa87fdc494a96c8a49c2144b2b6dd81bcdd7a9304920a3809e6acadcaeb05488fad0579f4bb425199f4e8f2ceaa0a079db18932
7
+ data.tar.gz: 40a6a9425863422b93f275b5d3f49b526bbcff28977ed863e613aea3923e0f78c7c824630ae98d84fabcc578a85002bb25af7ff75022f63cc1e0a76b5f7c4fab
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # WaBot (Ruby)
2
+
3
+ A simple personal WhatsApp Web bot using Ruby, Selenium, and WhatsApp Web. No WhatsApp Business API required.
4
+
5
+ Features:
6
+ - Register/login local users (credentials stored locally, password hashed with bcrypt)
7
+ - Persist WhatsApp Web session per user via Chrome profile directory
8
+ - CLI to login to WhatsApp Web (QR) and send messages
9
+
10
+ ## Prerequisites
11
+ - Ruby 3.0+
12
+ - Google Chrome installed
13
+
14
+ ## Setup
15
+ ```
16
+ cd wabot
17
+ bundle install
18
+ ```
19
+
20
+ Note: This project uses Selenium Manager (built into selenium-webdriver >= 4.11) to automatically manage the browser driver. You do NOT need the `webdrivers` gem.
21
+
22
+ ## Usage
23
+ 1) Register a local user:
24
+ ```
25
+ ruby bin/wabot register -u alice -p secret123
26
+ ```
27
+
28
+ 2) Login as that local user:
29
+ ```
30
+ ruby bin/wabot login -u alice -p secret123
31
+ ```
32
+
33
+ 3) Log in to WhatsApp Web (scan QR once). A Chrome window will open:
34
+ ```
35
+ ruby bin/wabot wa_login
36
+ ```
37
+ Wait until you see your chat list.
38
+
39
+ 4) Send a message (reuses the saved WhatsApp Web session for the current user):
40
+ ```
41
+ ruby bin/wabot send -t "+1234567890" -m "Hello from WaBot!"
42
+ ```
43
+
44
+ Notes:
45
+ - Phone number must be in international format including country code, e.g., +14155552671
46
+ - Each local user gets a separate Chrome profile under `profiles/<username>` to persist WhatsApp login
47
+ - Selenium Manager will handle downloading a compatible chromedriver automatically
48
+
49
+ ## Security
50
+ - Local user passwords are hashed with bcrypt and stored in `storage/users.json`
51
+ - The current CLI login session is stored in `storage/session.json`
52
+ - Your WhatsApp Web cookies/tokens live inside `profiles/<username>`; keep this folder private
53
+
54
+ ## Troubleshooting
55
+ - If WhatsApp Web UI changes and selectors break, update selectors in `lib/wabot/bot.rb`
56
+ - If Chrome fails to start in headless on macOS, try without `--headless` (default)
57
+ - Clear a user's WhatsApp session by deleting `profiles/<username>` (you'll need to scan QR again)
58
+
59
+ ## Gem usage (Ruby API)
60
+
61
+ You can use this as a library in your own Ruby code. Build and install the gem locally:
62
+
63
+ ```
64
+ cd wabot
65
+ gem build wabot.gemspec
66
+ gem install ./wabot-0.1.0.gem
67
+ ```
68
+
69
+ Then, in your Ruby app:
70
+
71
+ ```ruby
72
+ require "wabot"
73
+
74
+ # First-time only: open a visible window and log in (scan QR)
75
+ WaBot.login(username: "alice", headless: false)
76
+
77
+ # Later: open a session and send multiple messages inside a block
78
+ WaBot.session(username: "alice", headless: true) do |bot|
79
+ bot.send_message(phone_number: "+14155552671", message: "Hello from WaBot")
80
+ bot.send_message(phone_number: "+14155552672", message: "Second message")
81
+ end
82
+ ```
83
+
84
+ Using with Bundler (Gemfile):
85
+
86
+ ```
87
+ gem "wabot", "~> 0.1.0"
88
+ ```
89
+
90
+ Then in code:
91
+
92
+ ```ruby
93
+ require "wabot"
94
+ ```
95
+
96
+ CLI when installed via gem:
97
+
98
+ ```
99
+ wabot login -u alice
100
+ wabot send -t "+1234567890" -m "Hello from WaBot" --headless
101
+ ```
102
+
103
+ Notes:
104
+ - Library defaults to storing Chrome profiles under `~/.wabot/profiles/<username>` so your session persists across uses.
105
+ - If you prefer a visible browser for debugging, set `headless: false` in `WaBot.session(...)`.
data/bin/wabot ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "wabot/cli"
5
+
6
+ WaBot::CLI.start(ARGV)
data/lib/wabot/bot.rb ADDED
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "selenium-webdriver"
4
+ require "fileutils"
5
+ require "uri"
6
+
7
+ module WaBot
8
+ class Bot
9
+ BASE_URL = "https://web.whatsapp.com"
10
+
11
+ def initialize(username:, base_dir: File.expand_path("~/.wabot"), headless: false)
12
+ @username = username
13
+ @base_dir = base_dir
14
+ @profiles_dir = File.join(@base_dir, "profiles")
15
+ FileUtils.mkdir_p(@profiles_dir)
16
+ @user_profile_dir = File.join(@profiles_dir, username)
17
+ FileUtils.mkdir_p(@user_profile_dir)
18
+ @driver = nil
19
+ @headless = headless
20
+ end
21
+
22
+ def start
23
+ opts = Selenium::WebDriver::Chrome::Options.new
24
+ opts.add_argument("--user-data-dir=#{@user_profile_dir}")
25
+ opts.add_argument("--no-sandbox")
26
+ opts.add_argument("--disable-dev-shm-usage")
27
+ opts.add_argument("--window-size=1280,900")
28
+ opts.add_argument("--headless=new") if @headless
29
+
30
+ @driver = Selenium::WebDriver.for(:chrome, options: opts)
31
+ @driver.navigate.to(BASE_URL)
32
+ end
33
+
34
+ def ensure_logged_in(timeout: 90)
35
+ wait = Selenium::WebDriver::Wait.new(timeout: timeout)
36
+ begin
37
+ wait.until do
38
+ begin
39
+ @driver.find_element(css: "div[data-testid='chat-list']")
40
+ rescue Selenium::WebDriver::Error::NoSuchElementError
41
+ begin
42
+ @driver.find_element(css: "div[aria-label='Chat list']")
43
+ rescue Selenium::WebDriver::Error::NoSuchElementError
44
+ nil
45
+ end
46
+ end
47
+ end
48
+ true
49
+ rescue Selenium::WebDriver::Error::TimeoutError
50
+ false
51
+ end
52
+ end
53
+
54
+ def send_message(phone_number:, message:)
55
+ ensure_driver!
56
+ # WhatsApp expects digits only (no '+', spaces, hyphens)
57
+ digits_phone = phone_number.to_s.gsub(/\D/, "")
58
+ encoded_text = URI.encode_www_form_component(message)
59
+ chat_url = "#{BASE_URL}/send?phone=#{digits_phone}&text=#{encoded_text}"
60
+ @driver.navigate.to(chat_url)
61
+
62
+ wait = Selenium::WebDriver::Wait.new(timeout: 60)
63
+ begin
64
+ # Wait until the message box (preferred) or a send button is visible
65
+ wait.until { !!(find_message_box || find_send_button) }
66
+
67
+ # Always attempt to type the message explicitly to avoid relying on URL prefill
68
+ box = find_message_box
69
+ if box
70
+ box.click
71
+ sleep 0.15
72
+ # Select-all + delete to clear any previous text
73
+ modifier = (RUBY_PLATFORM =~ /darwin/i ? :command : :control)
74
+ box.send_keys([modifier, 'a'])
75
+ box.send_keys(:backspace)
76
+ sleep 0.05
77
+ box.send_keys(message)
78
+ sleep 0.1
79
+ end
80
+
81
+ # Prefer clicking the send button if present
82
+ if (btn = find_send_button)
83
+ begin
84
+ @driver.execute_script("arguments[0].click();", btn)
85
+ rescue
86
+ btn.click
87
+ end
88
+ else
89
+ # If no button, try Enter/Return inside the composer
90
+ if box
91
+ box.send_keys(:enter)
92
+ sleep 0.2
93
+ unless composer_text.to_s.strip.empty?
94
+ box.send_keys(:return)
95
+ end
96
+ else
97
+ # As a last resort, press Enter/Return on body
98
+ body = @driver.find_element(tag_name: "body")
99
+ body.send_keys(:enter)
100
+ sleep 0.2
101
+ # Try :return if still not sent
102
+ if composer_text && !composer_text.to_s.strip.empty?
103
+ body.send_keys(:return)
104
+ end
105
+ end
106
+ end
107
+
108
+ # Primary verification: new outgoing bubble with the same text appears
109
+ begin
110
+ Selenium::WebDriver::Wait.new(timeout: 8).until do
111
+ message_sent?(message)
112
+ end
113
+ return true
114
+ rescue Selenium::WebDriver::Error::TimeoutError
115
+ # Secondary verification: composer cleared
116
+ begin
117
+ Selenium::WebDriver::Wait.new(timeout: 3).until do
118
+ current = composer_text
119
+ current.nil? || current.strip.empty?
120
+ end
121
+ return true
122
+ rescue Selenium::WebDriver::Error::TimeoutError
123
+ return false
124
+ end
125
+ end
126
+ rescue Selenium::WebDriver::Error::TimeoutError
127
+ # Timed out waiting; try body Enter
128
+ body = @driver.find_element(tag_name: "body")
129
+ body.send_keys(:enter)
130
+ true
131
+ rescue => e
132
+ warn "Failed to send message: #{e.message}"
133
+ false
134
+ end
135
+ end
136
+
137
+ # Convenience alias
138
+ def send_to(phone_number, text)
139
+ send_message(phone_number: phone_number, message: text)
140
+ end
141
+
142
+ def close
143
+ @driver&.quit
144
+ @driver = nil
145
+ end
146
+
147
+ private
148
+
149
+ def ensure_driver!
150
+ raise "Driver not started. Call start first." unless @driver
151
+ end
152
+
153
+ def find_message_box
154
+ # Try several known selectors WhatsApp uses for the main composer textbox
155
+ candidates = [
156
+ "div[data-testid='conversation-compose-box-input']",
157
+ "div[contenteditable='true'][data-tab='10']",
158
+ "div[contenteditable='true'][data-tab='6']",
159
+ "div[role='textbox'][contenteditable='true']",
160
+ "div[aria-label='Type a message']",
161
+ "div[title='Type a message']"
162
+ ]
163
+ candidates.each do |css|
164
+ begin
165
+ el = @driver.find_element(css: css)
166
+ return el if el.displayed?
167
+ rescue Selenium::WebDriver::Error::NoSuchElementError
168
+ end
169
+ end
170
+ nil
171
+ end
172
+
173
+ def find_send_button
174
+ # Try multiple variants for the send button
175
+ css_candidates = [
176
+ "button[data-testid='compose-btn-send']",
177
+ "button[aria-label='Send']",
178
+ "button[aria-label='Send message']"
179
+ ]
180
+ css_candidates.each do |css|
181
+ begin
182
+ el = @driver.find_element(css: css)
183
+ return el if el.displayed?
184
+ rescue Selenium::WebDriver::Error::NoSuchElementError
185
+ end
186
+ end
187
+ # Older UI: span icon
188
+ begin
189
+ span = @driver.find_element(css: "span[data-icon='send']")
190
+ return span.find_element(xpath: "./ancestor::button")
191
+ rescue Selenium::WebDriver::Error::NoSuchElementError
192
+ end
193
+ nil
194
+ end
195
+
196
+ def composer_text
197
+ el = find_message_box
198
+ return nil unless el
199
+ el.text
200
+ rescue
201
+ nil
202
+ end
203
+
204
+ def message_sent?(text)
205
+ begin
206
+ # Look for outgoing message bubbles containing our text
207
+ nodes = @driver.find_elements(css: "div.message-out span.selectable-text, div.message-out span[dir='auto']")
208
+ nodes.any? { |n| n.text.to_s.strip == text.to_s.strip }
209
+ rescue
210
+ false
211
+ end
212
+ end
213
+ end
214
+ end
data/lib/wabot/cli.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "colorize"
5
+ require_relative "version"
6
+ require_relative "user_store"
7
+ require_relative "session_store"
8
+ require_relative "bot"
9
+
10
+ module WaBot
11
+ class CLI < Thor
12
+ desc "register", "Register a new local user"
13
+ method_option :username, aliases: "-u", type: :string, required: true, desc: "Username"
14
+ method_option :password, aliases: "-p", type: :string, required: true, desc: "Password"
15
+ def register
16
+ store = UserStore.new
17
+ begin
18
+ store.register(options[:username], options[:password])
19
+ puts "User registered: #{options[:username]}".green
20
+ rescue => e
21
+ puts "Error: #{e.message}".red
22
+ exit 1
23
+ end
24
+ end
25
+
26
+ desc "login", "Login as a local user"
27
+ method_option :username, aliases: "-u", type: :string, required: true
28
+ method_option :password, aliases: "-p", type: :string, required: true
29
+ def login
30
+ store = UserStore.new
31
+ unless store.authenticate(options[:username], options[:password])
32
+ puts "Invalid username or password".red
33
+ exit 1
34
+ end
35
+ session = SessionStore.new
36
+ session.login(options[:username])
37
+ puts "Logged in as #{options[:username]}".green
38
+ end
39
+
40
+ desc "logout", "Logout current local user"
41
+ def logout
42
+ session = SessionStore.new
43
+ if session.logged_in?
44
+ user = session.current_user
45
+ session.logout
46
+ puts "Logged out: #{user}".yellow
47
+ else
48
+ puts "No user is currently logged in".yellow
49
+ end
50
+ end
51
+
52
+ desc "wa_login", "Open WhatsApp Web to log in (QR) for the current user"
53
+ method_option :headless, type: :boolean, default: false, desc: "Run Chrome in headless mode"
54
+ def wa_login
55
+ session = SessionStore.new
56
+ unless session.logged_in?
57
+ puts "Please login as a local user first (cli login)".red
58
+ exit 1
59
+ end
60
+
61
+ bot = Bot.new(username: session.current_user, headless: options[:headless])
62
+ begin
63
+ bot.start
64
+ puts "A Chrome window has opened. Scan the QR code with your phone to login to WhatsApp Web.".cyan
65
+ if bot.ensure_logged_in(timeout: 180)
66
+ puts "WhatsApp Web login successful!".green
67
+ else
68
+ puts "Timed out waiting for WhatsApp Web login.".red
69
+ end
70
+ ensure
71
+ bot.close
72
+ end
73
+ end
74
+
75
+ desc "send", "Send a WhatsApp message"
76
+ method_option :to, aliases: "-t", type: :string, required: true, desc: "Phone number in international format, e.g. +1234567890"
77
+ method_option :message, aliases: "-m", type: :string, required: true, desc: "Message text"
78
+ method_option :headless, type: :boolean, default: false
79
+ method_option :keep_open, type: :boolean, default: false, desc: "Keep the browser open after sending (debug)"
80
+ def send
81
+ session = SessionStore.new
82
+ unless session.logged_in?
83
+ puts "Please login as a local user first (cli login)".red
84
+ exit 1
85
+ end
86
+
87
+ bot = Bot.new(username: session.current_user, headless: options[:headless])
88
+ begin
89
+ bot.start
90
+ unless bot.ensure_logged_in(timeout: 180)
91
+ puts "Not logged into WhatsApp Web. Please run `wa_login` to scan the QR code first.".red
92
+ exit 1
93
+ end
94
+ if bot.send_message(phone_number: options[:to], message: options[:message])
95
+ puts "Message sent to #{options[:to]}".green
96
+ else
97
+ puts "Failed to send message".red
98
+ exit 1 unless options[:keep_open]
99
+ end
100
+ if options[:keep_open]
101
+ puts "Keeping browser open. Press Enter to quit...".yellow
102
+ STDIN.gets
103
+ end
104
+ ensure
105
+ bot.close unless options[:keep_open]
106
+ end
107
+ end
108
+
109
+ desc "version", "Print version"
110
+ def version
111
+ puts WaBot::VERSION
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "time"
6
+
7
+ module WaBot
8
+ class SessionStore
9
+ def initialize(base_dir: File.expand_path("~/.wabot"))
10
+ @storage_dir = File.join(base_dir, "storage")
11
+ FileUtils.mkdir_p(@storage_dir)
12
+ @session_file = File.join(@storage_dir, "session.json")
13
+ initialize_file
14
+ end
15
+
16
+ def login(username)
17
+ data = { "current_user" => username, "logged_in_at" => Time.now.utc.iso8601 }
18
+ File.write(@session_file, JSON.pretty_generate(data))
19
+ true
20
+ end
21
+
22
+ def current_user
23
+ data = JSON.parse(File.read(@session_file))
24
+ data["current_user"]
25
+ rescue
26
+ nil
27
+ end
28
+
29
+ def logged_in?
30
+ !current_user.nil?
31
+ end
32
+
33
+ def logout
34
+ File.write(@session_file, JSON.pretty_generate({ "current_user" => nil }))
35
+ true
36
+ end
37
+
38
+ private
39
+
40
+ def initialize_file
41
+ unless File.exist?(@session_file)
42
+ File.write(@session_file, JSON.pretty_generate({ "current_user" => nil }))
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "bcrypt"
5
+ require "fileutils"
6
+
7
+ module WaBot
8
+ class UserStore
9
+ def initialize(base_dir: File.expand_path("~/.wabot"))
10
+ @storage_dir = File.join(base_dir, "storage")
11
+ FileUtils.mkdir_p(@storage_dir)
12
+ @users_file = File.join(@storage_dir, "users.json")
13
+ initialize_file
14
+ end
15
+
16
+ def register(username, password)
17
+ raise "Username is required" if username.to_s.strip.empty?
18
+ raise "Password is required" if password.to_s.strip.empty?
19
+
20
+ users = read_users
21
+ raise "User already exists" if users.any? { |u| u["username"] == username }
22
+
23
+ password_hash = BCrypt::Password.create(password)
24
+ users << { "username" => username, "password_hash" => password_hash }
25
+ write_users(users)
26
+ true
27
+ end
28
+
29
+ def authenticate(username, password)
30
+ users = read_users
31
+ user = users.find { |u| u["username"] == username }
32
+ return false unless user
33
+
34
+ begin
35
+ BCrypt::Password.new(user["password_hash"]) == password
36
+ rescue
37
+ false
38
+ end
39
+ end
40
+
41
+ def users
42
+ read_users
43
+ end
44
+
45
+ private
46
+
47
+ def initialize_file
48
+ unless File.exist?(@users_file)
49
+ File.write(@users_file, JSON.pretty_generate({ users: [] }))
50
+ end
51
+ end
52
+
53
+ def read_users
54
+ data = JSON.parse(File.read(@users_file))
55
+ data["users"] || []
56
+ rescue
57
+ []
58
+ end
59
+
60
+ def write_users(users)
61
+ File.write(@users_file, JSON.pretty_generate({ users: users }))
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaBot
4
+ VERSION = "0.1.0"
5
+ end
data/lib/wabot.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "wabot/version"
4
+ require_relative "wabot/bot"
5
+
6
+ module WaBot
7
+ # Open a WhatsApp Web browser session for a given username and yield the bot.
8
+ # Ensures the browser is closed after the block even if an error occurs.
9
+ #
10
+ # Example:
11
+ # WaBot.session(username: "alice") do |bot|
12
+ # bot.send_message(phone_number: "+14155552671", message: "Hello")
13
+ # bot.send_message(phone_number: "+14155552672", message: "Hi again")
14
+ # end
15
+ #
16
+ # Options:
17
+ # - headless: run Chrome headless (default: false)
18
+ # - timeout: seconds to wait for WhatsApp Web chat list (default: 180)
19
+ # - base_dir: base directory for profiles/ and storage/ (default: ~/.wabot)
20
+ # - require_login: if true, waits for chat list; if false, skips the check (default: true)
21
+ def self.session(username:, headless: false, timeout: 180, base_dir: File.expand_path("~/.wabot"), require_login: true)
22
+ bot = Bot.new(username: username, headless: headless, base_dir: base_dir)
23
+ begin
24
+ bot.start
25
+ if require_login
26
+ ok = bot.ensure_logged_in(timeout: timeout)
27
+ unless ok
28
+ raise "Not logged into WhatsApp Web for user '#{username}'. Run wa_login or open a non-headless session to scan QR."
29
+ end
30
+ end
31
+ yield bot
32
+ ensure
33
+ bot.close
34
+ end
35
+ end
36
+
37
+ # Convenience: open a session, send one message, and close.
38
+ def self.send_message(username:, to:, message:, headless: false, timeout: 180, base_dir: File.expand_path("~/.wabot"))
39
+ session(username: username, headless: headless, timeout: timeout, base_dir: base_dir) do |bot|
40
+ bot.send_message(phone_number: to, message: message)
41
+ end
42
+ end
43
+
44
+ # Open a visible Chrome window and wait for QR login (up to timeout seconds).
45
+ # Returns true if chat list detected, false otherwise.
46
+ def self.login(username:, timeout: 180, base_dir: File.expand_path("~/.wabot"), headless: false)
47
+ bot = Bot.new(username: username, headless: headless, base_dir: base_dir)
48
+ begin
49
+ bot.start
50
+ bot.ensure_logged_in(timeout: timeout)
51
+ ensure
52
+ bot.close
53
+ end
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wabot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vikas Kumar
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: selenium-webdriver
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '4.11'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '4.11'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bcrypt
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: colorize
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.1'
68
+ description: Automate WhatsApp Web from Ruby using Selenium. Provides a CLI and a
69
+ Ruby API with block-based session handling.
70
+ email:
71
+ - vikas_kr@live.com
72
+ executables:
73
+ - wabot
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - README.md
78
+ - bin/wabot
79
+ - lib/wabot.rb
80
+ - lib/wabot/bot.rb
81
+ - lib/wabot/cli.rb
82
+ - lib/wabot/session_store.rb
83
+ - lib/wabot/user_store.rb
84
+ - lib/wabot/version.rb
85
+ homepage: https://github.com/vikas-0/whatsapp_bot_ruby
86
+ licenses:
87
+ - MIT
88
+ metadata: {}
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.7.1
104
+ specification_version: 4
105
+ summary: 'WaBot: Personal WhatsApp Web automation with CLI and block-based API'
106
+ test_files: []