#!/usr/bin/env ruby # encoding: utf-8 require 'twitter_ebooks' require 'ostruct' require 'fileutils' module Ebooks::Util def pretty_exception(e) end end module Ebooks::CLI APP_PATH = Dir.pwd # XXX do some recursive thing instead HELP = OpenStruct.new HELP.default = < ebooks new ebooks s[tart] ebooks c[onsole] ebooks auth ebooks consume [corpus_path2] [...] ebooks consume-all [corpus_path2] [...] ebooks append ebooks gen [input] ebooks archive [path] ebooks tweet ebooks version STR def self.help(command=nil) if command.nil? log HELP.default else log HELP[command].gsub(/^ {4}/, '') end end HELP.new = <<-STR Usage: ebooks new Creates a new skeleton repository defining a template bot in the current working directory specified by . STR def self.new(reponame) if reponame.nil? help :new exit 1 end path = "./#{reponame}" if File.exists?(path) log "#{path} already exists. Please remove if you want to recreate." exit 1 end FileUtils.cp_r(Ebooks::SKELETON_PATH, path) FileUtils.mv(File.join(path, 'gitignore'), File.join(path, '.gitignore')) File.open(File.join(path, 'bots.rb'), 'w') do |f| template = File.read(File.join(Ebooks::SKELETON_PATH, 'bots.rb')) f.write(template.gsub("{{BOT_NAME}}", reponame)) end File.open(File.join(path, 'Gemfile'), 'w') do |f| template = File.read(File.join(Ebooks::SKELETON_PATH, 'Gemfile')) f.write(template.gsub("{{RUBY_VERSION}}", RUBY_VERSION)) end log "New twitter_ebooks app created at #{reponame}" end HELP.consume = <<-STR Usage: ebooks consume [corpus_path2] [...] Processes some number of text files or json tweet corpuses into usable models. These will be output at model/.model STR def self.consume(pathes) if pathes.empty? help :consume exit 1 end pathes.each do |path| filename = File.basename(path) shortname = filename.split('.')[0..-2].join('.') FileUtils.mkdir_p(File.join(APP_PATH, 'model')) outpath = File.join(APP_PATH, 'model', "#{shortname}.model") Ebooks::Model.consume(path).save(outpath) log "Corpus consumed to #{outpath}" end end HELP.consume_all = <<-STR Usage: ebooks consume-all [corpus_path2] [...] Processes some number of text files or json tweet corpuses into one usable model. It will be output at model/.model STR def self.consume_all(name, paths) if paths.empty? help :consume_all exit 1 end outpath = File.join(APP_PATH, 'model', "#{name}.model") Ebooks::Model.consume_all(paths).save(outpath) log "Corpuses consumed to #{outpath}" end HELP.append = <<-STR Usage: ebooks append Process then append the provided corpus to the model instead of overwriting. STR def self.append(name, path) if !name || !path help :append exit 1 end Ebooks::Model.consume(path).append(File.join(APP_PATH,'model',"#{name}.model")) log "Corpus appended to #{name}.model" end HELP.jsonify = <<-STR Usage: ebooks jsonify [tweets.csv2] [...] Takes a csv twitter archive and converts it to json. STR def self.jsonify(paths) if paths.empty? log usage exit end paths.each do |path| name = File.basename(path).split('.')[0] new_path = name + ".json" tweets = [] id = nil if path.split('.')[-1] == "csv" #from twitter archive csv_archive = CSV.read(path, :headers=>:first_row) tweets = csv_archive.map do |tweet| { text: tweet['text'], id: tweet['tweet_id'] } end else File.read(path).split("\n").each do |l| if l.start_with?('# ') id = l.split('# ')[-1] else tweet = { text: l } if id tweet[:id] = id id = nil end tweets << tweet end end end File.open(new_path, 'w') do |f| log "Writing #{tweets.length} tweets to #{new_path}" f.write(JSON.pretty_generate(tweets)) end end end HELP.gen = <<-STR Usage: ebooks gen [input] Make a test tweet from the processed model at . Will respond to input if provided. STR def self.gen(model_path, input) if model_path.nil? help :gen exit 1 end model = Ebooks::Model.load(model_path) if input && !input.empty? puts "@cmd " + model.make_response(input, 135) else puts model.make_statement end end HELP.archive = <<-STR Usage: ebooks archive [outpath] Downloads a json corpus of the 's tweets. Output defaults to corpus/.json Due to API limitations, this can only receive up to ~3000 tweets into the past. The first time you run archive, you will need to enter the auth details of some account to use for accessing the API. This info will then be stored in ~/.ebooksrc for later use, and can be modified there if needed. STR def self.archive(username, outpath=nil) if username.nil? help :archive exit 1 end Ebooks::Archive.new(username, outpath).sync end HELP.tweet = <<-STR Usage: ebooks tweet Sends a public tweet from the specified bot using text from the processed model at . STR def self.tweet(modelpath, botname) if modelpath.nil? || botname.nil? help :tweet exit 1 end load File.join(APP_PATH, 'bots.rb') model = Ebooks::Model.load(modelpath) statement = model.make_statement bot = Ebooks::Bot.get(botname) bot.configure bot.tweet(statement) end HELP.auth = <<-STR Usage: ebooks auth Authenticates your Twitter app for any account. By default, will use the consumer key and secret from the first defined bot. You can specify another by setting the CONSUMER_KEY and CONSUMER_SECRET environment variables. STR def self.auth consumer_key, consumer_secret = find_consumer require 'oauth' consumer = OAuth::Consumer.new( consumer_key, consumer_secret, site: 'https://twitter.com/', scheme: :header ) request_token = consumer.get_request_token auth_url = request_token.authorize_url() pin = nil loop do log auth_url log "Go to the above url and follow the prompts, then enter the PIN code here." print "> " pin = STDIN.gets.chomp break unless pin.empty? end access_token = request_token.get_access_token(oauth_verifier: pin) log "Account authorized successfully. Make sure to put these in your bots.rb!\n" + " access token: #{access_token.token}\n" + " access token secret: #{access_token.secret}" end HELP.console = <<-STR Usage: ebooks c[onsole] Starts an interactive ruby session with your bots loaded and configured. STR def self.console load_bots require 'pry'; Ebooks.module_exec { pry } end HELP.version = <<-STR Usage: ebooks version Shows you twitter_ebooks' version number. STR def self.version require File.expand_path('../../lib/twitter_ebooks/version', __FILE__) log Ebooks::VERSION end HELP.start = <<-STR Usage: ebooks s[tart] [botname] Starts running bots. If botname is provided, only runs that bot. STR def self.start(botname=nil) load_bots if botname.nil? bots = Ebooks::Bot.all else bots = Ebooks::Bot.all.select { |bot| bot.username == botname } if bots.empty? log "Couldn't find a defined bot for @#{botname}!" exit 1 end end threads = [] bots.each do |bot| threads << Thread.new { bot.prepare } end threads.each(&:join) threads = [] bots.each do |bot| threads << Thread.new do loop do begin bot.start rescue Exception => e bot.log e.inspect puts e.backtrace.map { |s| "\t"+s }.join("\n") end bot.log "Sleeping before reconnect" sleep 60 end end end threads.each(&:join) end # Non-command methods def self.find_consumer if ENV['CONSUMER_KEY'] && ENV['CONSUMER_SECRET'] log "Using consumer details from environment variables:\n" + " consumer key: #{ENV['CONSUMER_KEY']}\n" + " consumer secret: #{ENV['CONSUMER_SECRET']}" return [ENV['CONSUMER_KEY'], ENV['CONSUMER_SECRET']] end load_bots consumer_key = nil consumer_secret = nil Ebooks::Bot.all.each do |bot| if bot.consumer_key && bot.consumer_secret consumer_key = bot.consumer_key consumer_secret = bot.consumer_secret log "Using consumer details from @#{bot.username}:\n" + " consumer key: #{bot.consumer_key}\n" + " consumer secret: #{bot.consumer_secret}\n" return consumer_key, consumer_secret end end if consumer_key.nil? || consumer_secret.nil? log "Couldn't find any consumer details to auth an account with.\n" + "Please either configure a bot with consumer_key and consumer_secret\n" + "or provide the CONSUMER_KEY and CONSUMER_SECRET environment variables." exit 1 end end def self.load_bots load 'bots.rb' if Ebooks::Bot.all.empty? puts "Couldn't find any bots! Please make sure bots.rb instantiates at least one bot." end end def self.command(args) if args.length == 0 help exit 1 end case args[0] when "new" then new(args[1]) when "consume" then consume(args[1..-1]) when "consume-all" then consume_all(args[1], args[2..-1]) when "append" then append(args[1],args[2]) when "gen" then gen(args[1], args[2..-1].join(' ')) when "archive" then archive(args[1], args[2]) when "tweet" then tweet(args[1], args[2]) when "jsonify" then jsonify(args[1..-1]) when "auth" then auth when "console" then console when "c" then console when "start" then start(args[1]) when "s" then start(args[1]) when "help" then help(args[1]) when "version" then version else log "No such command '#{args[0]}'" help exit 1 end end end Ebooks::CLI.command(ARGV)