├── .ruby-version ├── Gemfile ├── README.md ├── room_info.rb ├── Gemfile.lock ├── fibbage_client.rb ├── .gitignore ├── bot.rb └── jackbox_client.rb /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.5 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' do 2 | gem 'httparty', '~> 0.13' 3 | gem 'faye-websocket', '~> 0.11' 4 | 5 | group :development do 6 | gem 'byebug', '~> 5.0' 7 | gem 'pry', '~> 0.10' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | * Ruby 2.6+ 4 | * Bundler 2+ 5 | 6 | ## Setup 7 | 8 | `bundle install` 9 | 10 | ## Usage 11 | 12 | ```bash 13 | $ export JACKBOX_ROOMID=1234 14 | $ bundle exec ruby bot.rb 15 | ``` 16 | -------------------------------------------------------------------------------- /room_info.rb: -------------------------------------------------------------------------------- 1 | class RoomInfo 2 | attr_accessor :server, :apptag, :appid, :join_as, :room_id 3 | 4 | def initialize(json_str) 5 | json = JSON.parse(json_str) 6 | @server = json["server"] 7 | @apptag = json["apptag"] 8 | @appid = json["appid"] 9 | @join_as = json["joinAs"] 10 | @room_id = json["roomid"] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | byebug (5.0.0) 5 | columnize (= 0.9.0) 6 | coderay (1.1.2) 7 | columnize (0.9.0) 8 | eventmachine (1.2.7) 9 | faye-websocket (0.11.0) 10 | eventmachine (>= 0.12.0) 11 | websocket-driver (>= 0.5.1) 12 | httparty (0.17.3) 13 | mime-types (~> 3.0) 14 | multi_xml (>= 0.5.2) 15 | method_source (0.9.2) 16 | mime-types (3.3.1) 17 | mime-types-data (~> 3.2015) 18 | mime-types-data (3.2019.1009) 19 | multi_xml (0.6.0) 20 | pry (0.12.2) 21 | coderay (~> 1.1.0) 22 | method_source (~> 0.9.0) 23 | websocket-driver (0.7.3) 24 | websocket-extensions (>= 0.1.0) 25 | websocket-extensions (0.1.5) 26 | 27 | PLATFORMS 28 | ruby 29 | 30 | DEPENDENCIES 31 | byebug (~> 5.0)! 32 | faye-websocket (~> 0.11)! 33 | httparty (~> 0.13)! 34 | pry (~> 0.10)! 35 | 36 | BUNDLED WITH 37 | 2.0.2 38 | -------------------------------------------------------------------------------- /fibbage_client.rb: -------------------------------------------------------------------------------- 1 | class FibbageClient < JackboxClient 2 | 3 | def on_customer_blob_changed(blob) 4 | super(blob) 5 | 6 | if blob["state"] == "Gameplay_CategorySelection" && blob["isChoosing"] 7 | puts "Choosing category" 8 | sleep(2) 9 | choices = @room_blob["choices"] 10 | chosen_category = choices.index(choices.sample) 11 | send(create_action_packet(SEND_MESSAGE_TO_ROOM_OWNER, 12 | message: { 'chosenCategory' => chosen_category })) 13 | end 14 | if blob["state"] == "Gameplay_EnterLie" 15 | puts "Entering a lie" 16 | sleep(2) 17 | lie = @customer_blob["suggestions"].sample 18 | send(create_action_packet(SEND_MESSAGE_TO_ROOM_OWNER, 19 | message: { "lieEntered": lie, "usedSuggestion": false })) 20 | end 21 | if blob["state"] == "Gameplay_ChooseLie" && !blob["choosingDone"] && !blob["chosen"] 22 | puts "Choosing lie" 23 | sleep(2) 24 | choice = blob["choices"].sample 25 | send(create_action_packet(SEND_MESSAGE_TO_ROOM_OWNER, 26 | message: { choice: choice })) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Specific to RubyMotion (use of CocoaPods): 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | # vendor/Pods/ 34 | 35 | ## Documentation cache and generated files: 36 | /.yardoc/ 37 | /_yardoc/ 38 | /doc/ 39 | /rdoc/ 40 | 41 | ## Environment normalization: 42 | /.bundle/ 43 | /vendor/bundle 44 | /lib/bundler/man/ 45 | 46 | # for a library or gem, you might want to ignore these files since the code is 47 | # intended to run in multiple environments; otherwise, check them in: 48 | # Gemfile.lock 49 | # .ruby-version 50 | # .ruby-gemset 51 | 52 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 53 | .rvmrc 54 | 55 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 56 | # .rubocop-https?--* 57 | -------------------------------------------------------------------------------- /bot.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'securerandom' 3 | require_relative 'room_info' 4 | require_relative 'jackbox_client' 5 | require_relative 'fibbage_client' 6 | require 'bundler' 7 | Bundler.require 8 | 9 | class Bot 10 | 11 | ROOM_INFO_URL = "http://blobcast.jackboxgames.com/room/%{room_id}/?userId=%{user_id}" 12 | CLIENTS = { 13 | "fibbage" => FibbageClient 14 | } 15 | 16 | attr_accessor :room_id, :user_id, :logger 17 | 18 | def initialize(room_id) 19 | @room_id = room_id 20 | @logger = Logger.new(STDOUT) 21 | @logger.level = Logger::DEBUG 22 | @user_id = SecureRandom.uuid 23 | end 24 | 25 | def run! 26 | room_info = get_room_info 27 | web_socket_url = get_websocket_url(room_info) 28 | 29 | client = CLIENTS[room_info.apptag.to_s].new(room_info, user_id, web_socket_url) 30 | 31 | # here is our "main" bot thread 32 | EM.run { 33 | client.run! 34 | } 35 | end 36 | 37 | private 38 | def get_room_info 39 | response = HTTParty.get(ROOM_INFO_URL % { room_id: room_id, user_id: user_id }, logger: logger) 40 | RoomInfo.new(response.body) 41 | end 42 | 43 | def get_websocket_url(room_info) 44 | now = Time.now.utc 45 | response = HTTParty.get("http://#{room_info.server}:38202/socket.io/1/?t=#{now}", logger: logger) 46 | unknown = response.body.split(':').first 47 | "ws://#{room_info.server}:38202/socket.io/1/websocket/#{unknown}" 48 | end 49 | 50 | end 51 | 52 | room_id = ENV['JACKBOX_ROOMID'] 53 | Bot.new(room_id).run! 54 | -------------------------------------------------------------------------------- /jackbox_client.rb: -------------------------------------------------------------------------------- 1 | class JackboxClient 2 | attr_reader :room_info, :user_id, :ws_url, :ws, :room_state, :customer_state 3 | 4 | CONNECT = 0 5 | JOIN = 1 6 | PING = 2 7 | EVENT = 5 8 | EVENT_MESSAGE = "Event" 9 | ROOM_BLOB_CHANGED = "RoomBlobChanged" 10 | CUSTOMER_BLOB_CHANGED = "CustomerBlobChanged" 11 | ROOM_DESTROYED = "RoomDestroyed" 12 | SEND_MESSAGE_TO_ROOM_OWNER = "SendMessageToRoomOwner" 13 | RESULT_MESSAGE = "Result" 14 | JOIN_ROOM = "JoinRoom" 15 | 16 | def initialize(room_info, user_id, ws_url) 17 | @room_info = room_info 18 | @user_id = user_id 19 | @ws_url = ws_url 20 | @room_blob = "" 21 | @customer_blob = "" 22 | end 23 | 24 | def run! 25 | @ws = Faye::WebSocket::Client.new(ws_url) 26 | ws.on :message do |event| 27 | next unless event.data =~ /^\d+/ 28 | code, body = event.data.scan(/^(\d+):{2,3}(.*)$/).first 29 | code = code.to_i 30 | on_message(code, body) 31 | end 32 | ws.on :close do |event| 33 | @ws = nil 34 | end 35 | end 36 | 37 | protected 38 | 39 | def join_room 40 | send(create_action_packet("JoinRoom", 41 | name: "RANDO", 42 | "joinType" => room_info.join_as)) 43 | end 44 | 45 | def on_message(code, body) 46 | case code 47 | when PING 48 | puts "Got ping" 49 | send_pong 50 | when JOIN 51 | join_room 52 | when EVENT 53 | args = JSON.parse(body)["args"] 54 | args.each { |arg| on_data_received(arg) } 55 | end 56 | end 57 | 58 | def send(packet) 59 | msg = { 60 | name: "msg", 61 | args: [packet] 62 | }.to_json 63 | puts "Sending #{msg}" 64 | ws.send("5:::#{msg}") 65 | end 66 | 67 | def create_action_packet(action, data) 68 | { 69 | "type" => "Action", 70 | "appId" => room_info.appid, 71 | "userId" => user_id, 72 | "roomId" => room_info.room_id, 73 | "action" => action 74 | }.merge(data) 75 | end 76 | 77 | private 78 | 79 | def on_data_received(data) 80 | puts "on_data_received: #{data}" 81 | on_event_received(data) if data["type"] == EVENT_MESSAGE 82 | on_result_received(data) if data["type"] == RESULT_MESSAGE 83 | end 84 | 85 | def on_event_received(data) 86 | event = data["event"] 87 | on_customer_blob_changed(data["blob"]) if event == CUSTOMER_BLOB_CHANGED 88 | on_room_blob_changed(data["blob"]) if event == ROOM_BLOB_CHANGED 89 | EventMachine::stop_event_loop if event == ROOM_DESTROYED 90 | end 91 | 92 | def on_result_received(data) 93 | puts "on_result_received: #{data}" 94 | action = data["action"] 95 | on_joined if action == JOIN_ROOM && data["success"] && data["initial"] 96 | end 97 | 98 | def on_customer_blob_changed(blob) 99 | @customer_blob = blob 100 | end 101 | 102 | def on_room_blob_changed(blob) 103 | @room_blob = blob 104 | end 105 | 106 | def on_joined 107 | end 108 | 109 | def send_pong 110 | puts "Sending pong" 111 | ws.send("2::") 112 | end 113 | end 114 | --------------------------------------------------------------------------------