Class: TwBot
- Inherits:
-
Object
- Object
- TwBot
- Defined in:
- twbot3.rb
Overview
A class for X (formerly Twitter) bot framework combined with OAuth token manager.
Defined Under Namespace
Classes: IncompleteConfigError, MessageFormatError
Constant Summary collapse
- WAIT_SECONDS =
Time to wait for opening config file if it is locked.
2
Class Method Summary collapse
-
.parse_boolean(str) ⇒ Object
If the specified string is “true” or “false” (case insensitive), returns that boolean value.
-
.remove_reply(str) ⇒ Object
Separates reply string (“@USERNAME”) into “@ USERNAME” to avoid unintended replies.
-
.save_log(log_file, logmsg) ⇒ void
Saves the logs stored in the `logmsg` variable to the file, and clears them from `logmsg` variable.
-
.truncate_to_length(max_length, source, footer = "") ⇒ String
Truncate the end of the string if it is longer than max_length.
-
.validate_message(obj) ⇒ Object
Converts values from user-defined “load_post” method into HTTP request.
Instance Method Summary collapse
-
#access_token_via_browser(username) ⇒ Object
Get OAuth token (via browser).
-
#action(mode, block) ⇒ void
private
Parses CUI command (see #cui_menu) and acts with it.
-
#auth_http(info = ) ⇒ OAuth::AccessToken
Returns access token (an instance of `OAuth::AccessToken`) for Twitter API with specified app and user.
-
#consumer ⇒ Object
private
Provides consumer token (token to represent an app registered to Twitter) by reading the key and the secret from the config info.
-
#cui_menu(&block) ⇒ void
Defines a bot with CUI menu.
-
#cui_menu_error ⇒ void
private
Displays error message of #cui_menu method to `$stderr`.
-
#dialog_add_user(username, reload = false, update_default = false) ⇒ Object
Add a new user.
- #dialog_authenticate_default_user ⇒ Object
-
#initialize(config_file, log_file = nil, list = '', preserve_config = false, no_post = false) ⇒ void
constructor
Constructor.
-
#load_messages(&block) ⇒ void
Runs the code under the context of the instance itself, and store returned values to the config file as messages to be posted.
-
#post_messages(post_count = 1, retries = 0, user = , list = @list) ⇒ Object
Post messages loaded by #load_message (or equivalent command under #cui_menu) to Twitter.
-
#run(&block) ⇒ void
Runs the code under the context of the instance itself, and saves the changed config to the file.
- #save_config ⇒ Object
-
#set_consumer(key, secret, site = 'https://api.twitter.com', authorize_path = '/oauth/authenticate') ⇒ void
private
Set up consumer token (token to represent an app registered to Twitter) by specifying the key and the secret.
- #set_default_user(username) ⇒ Object
-
#update_from_list(info = ) ⇒ Object
update.
-
#user_registered?(user) ⇒ Boolean
check the user is registered in the config file returns true if and only if registered with OAuth token.
Constructor Details
#initialize(config_file, log_file = nil, list = '', preserve_config = false, no_post = false) ⇒ void
Constructor.
Usage(1)
`TwBot.new(config_file, [log_file[, list[, preserve_config[, no_post]]]])`
Usage(2)
`TwBot.new(config_file, [log_file: …[, list: …[, preserve_config: …[, no_post: …]]]])`
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'twbot3.rb', line 105 def initialize(config_file, log_file = nil, list = '', preserve_config = false, no_post = false) @log_file = log_file begin if log_file.kind_of?(Hash) # If arguments are specified by a Hash list = log_file.fetch(:list, "") preserve_config = log_file.fetch(:preserve_config, false) no_post = log_file.fetch(:no_post, false) log_file = log_file.fetch(:log_file, nil) end wait = WAIT_SECONDS if config_file == nil raise "Configuration file is required" end @config_file = config_file @@config_file_obj ||= {} if @@config_file_obj[@config_file] && !(@@config_file_obj[@config_file].closed?) @@config_file_obj[@config_file].rewind @config = YAML.load(@@config_file_obj[@config_file].read) else if File.exist?(config_file) @@config_file_obj[@config_file] = open(config_file, "r+b") until @@config_file_obj[@config_file].flock(File::LOCK_EX | File::LOCK_NB) sleep 1 wait -= 1 raise "Configuration file is locked" if wait < 0 end @config = YAML.load(@@config_file_obj[@config_file].read) else $stderr.puts "Warning: Configuration file \"#{@config_file}\" not found: newly created." @@config_file_obj[@config_file] = open(config_file, "a+b") until @@config_file_obj[@config_file].flock(File::LOCK_EX | File::LOCK_NB) sleep 1 wait -= 1 raise "Configuration file is locked" if wait < 0 end end end @config = {} unless @config.kind_of?(Hash) @list = "data/#{list}" @preserve_config_default = preserve_config @preserve_config = preserve_config @no_post = no_post @logmsg = "" rescue Exception => e TwBot.save_log(@log_file, "<Error> "+e.twbot_errorlog_format+"\n") return end end |
Class Method Details
.parse_boolean(str) ⇒ Object
If the specified string is “true” or “false” (case insensitive), returns that boolean value. Otherwise raises an exception.
689 690 691 692 693 694 695 696 697 698 |
# File 'twbot3.rb', line 689 def self.parse_boolean(str) case str when /\Atrue\z/i true when /\Afalse\z/i false else raise ArgumentError, "Value is neither of 'true' nor 'false'" end end |
.remove_reply(str) ⇒ Object
Separates reply string (“@USERNAME”) into “@ USERNAME” to avoid unintended replies. If a block is given, “@USERNAME” is separated if the result of the block is true.
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 |
# File 'twbot3.rb', line 655 def self.remove_reply(str) result = str.dup result.gsub!(/(@|@)([0-9A-Z_a-z]+)/) do |x| at_mark = $1 user_id = $2 if block_given? (yield(user_id) ? "#{at_mark} #{user_id}" : x) else "#{at_mark} #{user_id}" end end result.gsub!(/#/){ |x| "# " } result end |
.save_log(log_file, logmsg) ⇒ void
This method returns an undefined value.
Saves the logs stored in the `logmsg` variable to the file, and clears them from `logmsg` variable.
75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'twbot3.rb', line 75 def self.save_log(log_file, logmsg) return unless log_file begin open(log_file, "a") do |f| f.puts logmsg logmsg.replace("") end rescue Exception => e $stderr.puts e.twbot_errorlog_format end end |
.truncate_to_length(max_length, source, footer = "") ⇒ String
Truncate the end of the string if it is longer than max_length. It can be used to limit the message length to be posted to Twitter.
682 683 684 685 |
# File 'twbot3.rb', line 682 def self.truncate_to_length(max_length, source, = "") return source if source.length <= max_length "#{source[0, max_length - .length]}#{}" end |
.validate_message(obj) ⇒ Object
Converts values from user-defined “load_post” method into HTTP request. Returns nil if the value is invalid.
703 704 705 706 707 708 709 710 711 712 713 714 715 |
# File 'twbot3.rb', line 703 def self.(obj) case obj when String {'text' => obj} when Array return nil if obj.size != 2 {'text' => obj[0], 'reply' => {'in_reply_to_tweet_id' => obj[1].to_s}} when Hash obj else nil end end |
Instance Method Details
#access_token_via_browser(username) ⇒ Object
Get OAuth token (via browser)
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 |
# File 'twbot3.rb', line 718 def access_token_via_browser(username) # reference: https://shibason.hatenadiary.org/entry/20090802/1249204953 (in Japanese) request_token = consumer.get_request_token puts <<-OUT ============================================================ To retrieve OAuth token of user "#{username}": (1) Log in Twitter with a browser for user "#{username}". (2) Access the URL below with same browser: #{request_token.} (3) Check the application name is the one you registered, and if so, click "Allow" link in the browser. (4) Input the shown number (PIN number). To cancel, input nothing and press enter key. ============================================================ OUT pin_number = nil begin print "PIN number > " pin_number = STDIN.gets.chomp end until pin_number && pin_number =~ /\A\d*\z/ return nil if pin_number == "" token = request_token.token token_secret = request_token.secret hash = { :oauth_token => token, :oauth_token_secret => token_secret } request_token = OAuth::RequestToken.from_hash(consumer, hash) # Get access token access_token = request_token.get_access_token(:oauth_verifier => pin_number) end |
#action(mode, block) ⇒ void (private)
This method returns an undefined value.
Parses CUI command (see #cui_menu) and acts with it.
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'twbot3.rb', line 248 def action(mode, block) @last_run_mode = mode begin case mode when "init" dialog_authenticate_default_user when /\Aadd(?:=([0-9A-Z_a-z]+))?\z/ dialog_add_user($1, false) when /\Arefresh(?:=([0-9A-Z_a-z]+))?\z/ dialog_add_user($1, true) when /\Adefault(?:=([0-9A-Z_a-z]+))?\z/ set_default_user($1) when /\Arun(?:\=(.*?))?\z/ @optstr = $1 run(&block) when /\Aload(?:\=(.*?))?\z/ @optstr = $1 (&block) when /\Apost(?:=(\d+)(?:,(\d+))?)?\z/ # post messages from the list post_count = ($1 ? $1.to_i : 1) retries = ($2 ? $2.to_i : 0) (post_count, retries) when /\Aconsumer=/ values = $'.split(',') if values.size < 2 || values.size > 4 @logmsg << "Error: Consumer key and secret must be specified as \"consumer=[KEY],[SECRET](,[SITE](,[AUTH_PATH]))\"" @preserve_config = true save_config return else set_consumer(*values) end else @logmsg << "Error: Invalid mode" @preserve_config = true save_config return end rescue Exception => e @logmsg << "<Error>"+e.twbot_errorlog_format+"\n" @preserve_config = true save_config end end |
#auth_http(user = @config["login/"]) ⇒ OAuth::AccessToken #auth_http(user: u, reload: r, browser: b) ⇒ OAuth::AccessToken
Returns access token (an instance of `OAuth::AccessToken`) for Twitter API with specified app and user.
HTTP access with registered OAuth token can be done like:
auth_http.get(path...)
auth_http.post(path...)
auth_http(user).get(path...)
See www.rubydoc.info/gems/oauth/OAuth/AccessToken for details.
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 |
# File 'twbot3.rb', line 598 def auth_http(info = @config["login/"]) # parse parameters case info when String # If the parameter is given by a string, # It is treated as the user name user = info reload = false browser = false when Hash user = info.fetch(:user, @config["login/"]) reload = info.fetch(:reload, false) browser = info.fetch(:browser, false) else errmsg = "A String (user name) or Hash (parameters) is required as the argument (#{info.class} given)" if info == nil errmsg << "\n* Perhaps you have not finished authentication. Try '#{$0} init' to register the default user." end raise ArgumentError, errmsg end # creates an instance of AccessToken user_key = "users/#{user}" @config[user_key] ||= {} if reload || !(user_registered?(user)) # if token is not stored, or the library user choosed not to use stored token, # retrieves it with xAuth or browser if browser # with browser access_token = access_token_via_browser(user) else raise IncompleteConfigError, "Access token for the user @#{user} is not registered." end return nil if access_token == nil # Store the result to @config @config[user_key]["token"] = access_token.token @config[user_key]["secret"] = access_token.secret # return the access token access_token else # if token is stored, creates access token with it OAuth::AccessToken.new(consumer, @config[user_key]["token"], @config[user_key]["secret"]) end end |
#consumer ⇒ Object (private)
Returns Provides consumer token (token to represent an app registered to Twitter) by reading the key and the secret from the config info.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'twbot3.rb', line 30 def consumer if defined?(@consumer) return @consumer end if @config.include?('consumer_key/') && @config.include?('consumer_secret/') && @config.include?('site/') && @config.include?('authorize_path/') @consumer = OAuth::Consumer.new( @config['consumer_key/'], @config['consumer_secret/'], :site => @config['site/'], :authorize_path => @config['authorize_path/'], :debug_output => false ) else raise IncompleteConfigError, "Consumer key and/or secret is not written in the config file. Please run \"#{$0} consumer=[KEY],[SECRET]\"." end end |
#cui_menu(&block) ⇒ void
This method returns an undefined value.
Defines a bot with CUI menu. With this setup,
-
`ruby [ProgramName] init` authenticates the user account who posts messages (or other accesses to Twitter).
-
`ruby [ProgramName] run` just runs the code given as the block. See #run.
-
`ruby [ProgramName] load` generates the messages to be posted. See #load_messages.
-
`ruby [ProgramName] post` posts one generated message to Twitter (and remove it from the list).
For other uses, see the output of #cui_menu_error (or, equivalently, run `ruby [ProgramName]`).
When you use TwBot instance, it is recommended to use this method (or #cui_menu or #load_messages) rather than directly calling the method (`twbot.method_name`), since the config file and log file are saved even when an error is occurred.
230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'twbot3.rb', line 230 def (&block) if ARGV.empty? return end ARGV.each do |mode| $stderr.puts "Running mode '#{mode}'..." @logmsg << "\n[cui_menu:mode=#{mode}]" action(mode, block) end end |
#cui_menu_error ⇒ void (private)
This method returns an undefined value.
Displays error message of #cui_menu method to `$stderr`.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'twbot3.rb', line 168 def $stderr.puts <<-BUF Usage: #{$0} [modes...] 'modes' should be one of the followings: - init: Initializes the configuration file by an authenticated user. (Browser needed) - consumer=KEY,SECRET[,SITE][,PATH]: Set the key and the secret of the Twitter app you use. SITE (default: https://api.twitter.com) and PATH (default: /oauth/authenticate) are set for Twitter by default; you can usually omit. - add[=USER]: Adds an authenticated user to the configuration file. (Browser needed) - refresh[=USER]: Same as "add[=USER]", but always tries authentication even if the USER is in the configuration file. - default[=USER]: Set the default authenticated user as USER. - run[=OPTSTR]: Runs specified code. OPTSTR is given as the variable @optstr in the code. - load[=OPTSTR]: Runs specified code as a Twitter bot definition; the returned values (must be an array) are stored as messages into the configuration file. OPTSTR is given as the variable @optstr in the code. - post[=COUNT]: Posts messages stored by "load" mode. Example: #{$0} init #{$0} add #{$0} add=h_hiro_ #{$0} run #{$0} load #{$0} post=10 BUF end |
#dialog_add_user(username, reload = false, update_default = false) ⇒ Object
Add a new user. If 'reload' is specified true, a token will be re-retrieved.
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'twbot3.rb', line 391 def dialog_add_user(username, reload = false, update_default = false) consumer # Check existence of consumer token until username print "User name >" username = STDIN.gets.chomp return if username.empty? redo unless username =~ /\A[0-9A-Z_a-z]+\z/ end if !reload && user_registered?(username) puts "The user \"#{username}\" is already registered." return end auth = auth_http(:user => username, :reload => reload, :browser => true) if auth != nil puts "User \"#{username}\" is successfully registered." if update_default || @config["login/"] == nil @config["login/"] = username puts "Default user is set to @#{username}." end end save_config end |
#dialog_authenticate_default_user ⇒ Object
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'twbot3.rb', line 428 def dialog_authenticate_default_user if @config["login/"] # If default login user is already registered # (updating from twbot.rb 0.1*) puts <<-OUT ============================================================ Here I help you retrieve OAuth token of user "#{@config['login/']}". Please prepare a browser to retrieve OAuth tokens. ============================================================ OUT dialog_add_user(@config["login/"], true) else # Otherwise puts <<-OUT ============================================================ Here I help you register your bot account to the setting file. Please prepare a browser to retrieve OAuth tokens. Input the screen name of your bot account. ============================================================ OUT dialog_add_user(nil, true) end end |
#load_messages(&block) ⇒ void
This method returns an undefined value.
Runs the code under the context of the instance itself, and store returned values to the config file as messages to be posted. Also, it saves the changed config to the file.
Consider using #cui_menu instead if you use this method only via CUI commands.
When you use TwBot instance, it is recommended to use this method (or #cui_menu or #run) rather than directly calling the method (`twbot.method_name`), since the config file and log file are saved even when an error is occurred.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'twbot3.rb', line 343 def (&block) @config[@list] ||= [] begin if block new_updates = instance_eval(&block) else new_updates = load_data end new_updates.each do |m| if TwBot.(m) == nil raise MessageFormatError, "Invalid object as a message is contained: #{m.inspect}" end end rescue Exception => e @logmsg << "<Error> "+e.twbot_errorlog_format+"\n" @preserve_config = true else @config[@list].concat new_updates end save_config end |
#post_messages(post_count = 1, retries = 0, user = , list = @list) ⇒ Object
Post messages loaded by #load_message (or equivalent command under #cui_menu) to Twitter.
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'twbot3.rb', line 372 def (post_count = 1, retries = 0, user = @config["login/"], list = @list) while post_count > 0 begin break if update_from_list(:user => user, :list => list, :duplicated => @config['duplicated/']) == nil rescue Exception => e @logmsg << "<Error in updating> #{e}\n"+e.twbot_errorlog_format+"\n" retries -= 1 break if retries < 0 redo end post_count -= 1 end save_config end |
#run(&block) ⇒ void
This method returns an undefined value.
Runs the code under the context of the instance itself, and saves the changed config to the file.
Consider using #cui_menu instead if you use this method only via CUI commands.
Different from #load_messages, no message is registered as messages to be posted. This method is convenient to use Twitter API but not posting messages.
When you use TwBot instance, it is recommended to use this method (or #cui_menu or #load_messages) rather than directly calling the method (`twbot.method_name`), since the config file and log file are saved even when an error is occurred.
twbot = TwBot.new("config.yml", "error.log")
twbot.run do
# This `auth_http` is equivalent to `twbot.auth_http`,
# since the block is run as the context of the instance `twbot`
json_src = auth_http.get("/1.1/account/verify_credentials.json").body
data = JSON.load(json_src)
puts "My name is #{data['screen_name']}."
end
317 318 319 320 |
# File 'twbot3.rb', line 317 def run(&block) instance_eval(&block) save_config end |
#save_config ⇒ Object
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 |
# File 'twbot3.rb', line 455 def save_config unless @preserve_config new_yaml = YAML.dump(@config) @@config_file_obj[@config_file].rewind @@config_file_obj[@config_file].truncate(0) @@config_file_obj[@config_file].print new_yaml end @preserve_config = @preserve_config_default # output log @logmsg = "[#{Time.now}]#{@last_run_mode ? '(mode='+@last_run_mode+')' : ''}#{@logmsg}" $stderr.puts @logmsg TwBot.save_log(@log_file, @logmsg) end |
#set_consumer(key, secret, site = 'https://api.twitter.com', authorize_path = '/oauth/authenticate') ⇒ void (private)
This method returns an undefined value.
Set up consumer token (token to represent an app registered to Twitter) by specifying the key and the secret.
59 60 61 62 63 64 65 |
# File 'twbot3.rb', line 59 def set_consumer(key, secret, site = 'https://api.twitter.com', = '/oauth/authenticate') @config['consumer_key/'] = key @config['consumer_secret/'] = secret @config['site/'] = site @config['authorize_path/'] = save_config end |
#set_default_user(username) ⇒ Object
417 418 419 420 421 422 423 424 425 426 |
# File 'twbot3.rb', line 417 def set_default_user(username) @config["login/"] ||= nil if @config["login/"] print "Current default user is @#{@config["login/"]}." end unless username print "Input new default user name." end dialog_add_user(username, false, true) end |
#update_from_list(info = ) ⇒ Object
update
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 |
# File 'twbot3.rb', line 472 def update_from_list(info = @config["login/"]) # parse parameters case info when String # If the parameter is given by a string, # It is treated as the user name user = info list = @list duplicated = "ignore" when Hash user = info.fetch(:user, @config["login/"]) list = info.fetch(:list, @list) duplicated = info.fetch(:duplicated, @config['duplicated/']).to_s duplicated = "ignore" if duplicated == "" else raise ArgumentError, "A String (user name) or Hash (parameters) is required as the argument (#{info.class} given)" end # post messages auth = auth_http(user) trial = 0 while true trial += 1 # prepare the message if @config[list].empty? = "(error: No message remains)" $stderr.puts @logmsg << return nil end = @config[list].first request = TwBot.() raise MessageFormatError, .inspect if request == nil if request['text'].empty? # If empty string is specified @config[list].shift @logmsg << "(skipped: An empty string specified)" return false end request['text'].force_encoding("utf-8") # send request if @no_post result = "[]" # dummy json else post_status = auth.post("/2/tweets", JSON.dump(request), {"User-Agent" => "twbot3rb", "Content-Type" => "application/json"}) result = post_status.body end # Check the result json_result = nil begin json_result = JSON.load(result) rescue json_result = nil end if !json_result || (!json_result.include?("data")) || (!json_result["data"].include?("text")) # if failed if json_result && json_result.include?("detail") && json_result["detail"] == "You are not allowed to create a Tweet with duplicate content." # if duplicated = "(error: The message \"#{request['text']}\" is not posted because a duplicated message is tried to be posted)" $stderr.puts @logmsg << case duplicated when "seek" tmp = @config[list].shift @config[list].push tmp when "discard" @config[list].shift trial -= 1 when "cancel" return false when "ignore" @config[list].shift return false end else # if another reason raise RuntimeError, "Posting a message has failed - JSON data is:\n#{result}" end else # if succeeded # renew lists @config[list].shift # outputing / writing log $stderr.puts "[Updated!#{@no_post ? '(no_post)' : ''}] #{result}" @logmsg << "(A message has been posted)" return result end return false if trial >= @config[@list].size end end |
#user_registered?(user) ⇒ Boolean
check the user is registered in the config file returns true if and only if registered with OAuth token
575 576 577 578 |
# File 'twbot3.rb', line 575 def user_registered?(user) user_key = "users/#{user}" @config[user_key] && @config[user_key]["token"] && @config[user_key]["secret"] end |