twitter_ebooks_poll 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +167 -0
- data/Rakefile +2 -0
- data/bin/ebooks +449 -0
- data/data/adjectives.txt +1466 -0
- data/data/nouns.txt +2193 -0
- data/lib/twitter_ebooks/archive.rb +116 -0
- data/lib/twitter_ebooks/bot.rb +521 -0
- data/lib/twitter_ebooks/model.rb +336 -0
- data/lib/twitter_ebooks/nlp.rb +195 -0
- data/lib/twitter_ebooks/suffix.rb +104 -0
- data/lib/twitter_ebooks/sync.rb +52 -0
- data/lib/twitter_ebooks/version.rb +3 -0
- data/lib/twitter_ebooks.rb +22 -0
- data/skeleton/Gemfile +4 -0
- data/skeleton/Procfile +1 -0
- data/skeleton/bots.rb +65 -0
- data/skeleton/corpus/.gitignore +0 -0
- data/skeleton/gitignore +1 -0
- data/skeleton/image/.gitignore +0 -0
- data/skeleton/model/.gitignore +0 -0
- data/skeleton/stopwords.txt +843 -0
- data/spec/bot_spec.rb +216 -0
- data/spec/data/0xabad1dea.json +203945 -0
- data/spec/data/0xabad1dea.model +6158 -1
- data/spec/memprof.rb +37 -0
- data/spec/model_spec.rb +88 -0
- data/spec/spec_helper.rb +6 -0
- data/twitter_ebooks.gemspec +37 -0
- metadata +309 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f58b5801ce8261adf450070f37923c493ed945cb
|
4
|
+
data.tar.gz: f29a8c97eaf6b52022954e411e74522ec306b7d0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3513c69c90999452b08e9b65206ac4c04b0a7dacd5c54fb7a71c9b320aab6f4f12730a9874ff714d4cb6c2b962c017d4f76200425af9d51bce9f1a34e1190f51
|
7
|
+
data.tar.gz: d4459320f200b00272603b72fcfb5d82dfc1feb2553a3107c4f51ff86646a7ffecde6877fc37fb5ae0e74346e33ad77dc236c42ffd76586450385f8bf55127f3
|
data/.gitattributes
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jaiden Mispy
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
|
2
|
+
## Fork
|
3
|
+
|
4
|
+
No one I know had fun in changing to the shiny new Twitter Account Actitvity API, since this means to run your own web server with all consequences. So I started hacking into the code to change the streaming API to polling the home and mentions timeline.
|
5
|
+
|
6
|
+
# twitter\_ebooks\_poll
|
7
|
+
|
8
|
+
[![Gem Version](https://badge.fury.io/rb/twitter_ebooks_poll.svg)](http://badge.fury.io/rb/twitter_ebooks_poll)
|
9
|
+
[![Build Status](https://travis-ci.org/grindcrank/twitter_ebooks.svg)](https://travis-ci.org/grindcrank/twitter_ebooks)
|
10
|
+
|
11
|
+
A framework for building interactive twitterbots which respond to mentions/DMs. See [ebooks_example](https://github.com/mispy/ebooks_example) for a fully-fledged bot definition.
|
12
|
+
|
13
|
+
## New in 3.2
|
14
|
+
|
15
|
+
(There was no changelog for 3.1, but the version nevertheless existed.)
|
16
|
+
|
17
|
+
- Uses home and mentions polling instead of streams.
|
18
|
+
|
19
|
+
## New in 3.0
|
20
|
+
|
21
|
+
- About 80% less memory and storage use for models
|
22
|
+
- Bots run in their own threads (no eventmachine), and startup is parallelized
|
23
|
+
- Bots start with `ebooks start`, and no longer die on unhandled exceptions
|
24
|
+
- `ebooks auth` command will create new access tokens, for running multiple bots
|
25
|
+
- `ebooks console` starts a ruby interpreter with bots loaded (see Ebooks::Bot.all)
|
26
|
+
- Replies are slightly rate-limited to prevent infinite bot convos
|
27
|
+
- Non-participating users in a mention chain will be dropped after a few tweets
|
28
|
+
- [API documentation](http://rdoc.info/github/mispy/twitter_ebooks) and tests
|
29
|
+
|
30
|
+
Note that 3.0 is not backwards compatible with 2.x, so upgrade carefully! In particular, **make sure to regenerate your models** since the storage format changed.
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
Requires Ruby 2.1+. Ruby 2.3+ is recommended.
|
35
|
+
|
36
|
+
```bash
|
37
|
+
gem install twitter_ebooks
|
38
|
+
```
|
39
|
+
|
40
|
+
## Setting up a bot
|
41
|
+
|
42
|
+
Run `ebooks new <reponame>` to generate a new repository containing a sample bots.rb file, which looks like this:
|
43
|
+
|
44
|
+
``` ruby
|
45
|
+
# This is an example bot definition with event handlers commented out
|
46
|
+
# You can define and instantiate as many bots as you like
|
47
|
+
|
48
|
+
class MyBot < Ebooks::Bot
|
49
|
+
# Configuration here applies to all MyBots
|
50
|
+
def configure
|
51
|
+
# Consumer details come from registering an app at https://dev.twitter.com/
|
52
|
+
# Once you have consumer details, use "ebooks auth" for new access tokens
|
53
|
+
self.consumer_key = "" # Your app consumer key
|
54
|
+
self.consumer_secret = "" # Your app consumer secret
|
55
|
+
|
56
|
+
# Users to block instead of interacting with
|
57
|
+
self.blacklist = ['tnietzschequote']
|
58
|
+
|
59
|
+
# Range in seconds to randomize delay when bot.delay is called
|
60
|
+
self.delay_range = 1..6
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_startup
|
64
|
+
scheduler.every '24h' do
|
65
|
+
# Tweet something every 24 hours
|
66
|
+
# See https://github.com/jmettraux/rufus-scheduler
|
67
|
+
# tweet("hi")
|
68
|
+
# pictweet("hi", "cuteselfie.jpg")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_message(dm)
|
73
|
+
# Reply to a DM
|
74
|
+
# reply(dm, "secret secrets")
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_follow(user)
|
78
|
+
# Follow a user back
|
79
|
+
# follow(user.screen_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def on_mention(tweet)
|
83
|
+
# Reply to a mention
|
84
|
+
# reply(tweet, meta(tweet).reply_prefix + "oh hullo")
|
85
|
+
end
|
86
|
+
|
87
|
+
def on_timeline(tweet)
|
88
|
+
# Reply to a tweet in the bot's timeline
|
89
|
+
# reply(tweet, meta(tweet).reply_prefix + "nice tweet")
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_favorite(user, tweet)
|
93
|
+
# Follow user who just favorited bot's tweet
|
94
|
+
# follow(user.screen_name)
|
95
|
+
end
|
96
|
+
|
97
|
+
def on_retweet(tweet)
|
98
|
+
# Follow user who just retweeted bot's tweet
|
99
|
+
# follow(tweet.user.screen_name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Make a MyBot and attach it to an account
|
104
|
+
MyBot.new("abby_ebooks") do |bot|
|
105
|
+
bot.access_token = "" # Token connecting the app to this account
|
106
|
+
bot.access_token_secret = "" # Secret connecting the app to this account
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
`ebooks start` will run all defined bots in their own threads. The easiest way to run bots in a semi-permanent fashion is with [Heroku](https://www.heroku.com); just make an app, push the bot repository to it, enable a worker process in the web interface and it ought to chug along merrily forever.
|
111
|
+
|
112
|
+
The underlying streaming and REST clients from the [twitter gem](https://github.com/sferik/twitter) can be accessed at `bot.stream` and `bot.twitter` respectively.
|
113
|
+
|
114
|
+
## Archiving accounts
|
115
|
+
|
116
|
+
twitter\_ebooks comes with a syncing tool to download and then incrementally update a local json archive of a user's tweets (in this case, my good friend @0xabad1dea):
|
117
|
+
|
118
|
+
``` zsh
|
119
|
+
➜ ebooks archive 0xabad1dea corpus/0xabad1dea.json
|
120
|
+
Currently 20209 tweets for 0xabad1dea
|
121
|
+
Received 67 new tweets
|
122
|
+
```
|
123
|
+
|
124
|
+
The first time you'll run this, it'll ask for auth details to connect with. Due to API limitations, for users with high numbers of tweets it may not be possible to get their entire history in the initial download. However, so long as you run it frequently enough you can maintain a perfect copy indefinitely into the future.
|
125
|
+
|
126
|
+
## Text models
|
127
|
+
|
128
|
+
In order to use the included text modeling, you'll first need to preprocess your archive into a more efficient form:
|
129
|
+
|
130
|
+
``` zsh
|
131
|
+
➜ ebooks consume corpus/0xabad1dea.json
|
132
|
+
Reading json corpus from corpus/0xabad1dea.json
|
133
|
+
Removing commented lines and sorting mentions
|
134
|
+
Segmenting text into sentences
|
135
|
+
Tokenizing 7075 statements and 17947 mentions
|
136
|
+
Ranking keywords
|
137
|
+
Corpus consumed to model/0xabad1dea.model
|
138
|
+
```
|
139
|
+
|
140
|
+
Notably, this works with both json tweet archives and plaintext files (based on file extension), so you can make a model out of any kind of text.
|
141
|
+
|
142
|
+
Text files use newlines and full stops to seperate statements.
|
143
|
+
|
144
|
+
Once you have a model, the primary use is to produce statements and related responses to input, using a pseudo-Markov generator:
|
145
|
+
|
146
|
+
``` ruby
|
147
|
+
> model = Ebooks::Model.load("model/0xabad1dea.model")
|
148
|
+
> model.make_statement(140)
|
149
|
+
=> "My Terrible Netbook may be the kind of person who buys Starbucks, but this Rackspace vuln is pretty straight up a backdoor"
|
150
|
+
> model.make_response("The NSA is coming!", 130)
|
151
|
+
=> "Hey - someone who claims to be an NSA conspiracy"
|
152
|
+
```
|
153
|
+
|
154
|
+
The secondary function is the "interesting keywords" list. For example, I use this to determine whether a bot wants to fav/retweet/reply to something in its timeline:
|
155
|
+
|
156
|
+
``` ruby
|
157
|
+
top100 = model.keywords.take(100)
|
158
|
+
tokens = Ebooks::NLP.tokenize(tweet.text)
|
159
|
+
|
160
|
+
if tokens.find { |t| top100.include?(t) }
|
161
|
+
favorite(tweet)
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
## Bot niceness
|
166
|
+
|
167
|
+
twitter_ebooks will drop bystanders from mentions for you and avoid infinite bot conversations, but it won't prevent you from doing a lot of other spammy things. Make sure your bot is a good and polite citizen!
|
data/Rakefile
ADDED
data/bin/ebooks
ADDED
@@ -0,0 +1,449 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
require 'twitter_ebooks'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module Ebooks::Util
|
9
|
+
def pretty_exception(e)
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Ebooks::CLI
|
15
|
+
$LOAD_PATH.unshift(Dir.pwd)
|
16
|
+
puts $LOAD_PATH
|
17
|
+
HELP = OpenStruct.new
|
18
|
+
|
19
|
+
HELP.default = <<STR
|
20
|
+
Usage:
|
21
|
+
ebooks help <command>
|
22
|
+
|
23
|
+
ebooks new <reponame>
|
24
|
+
ebooks s[tart]
|
25
|
+
ebooks c[onsole]
|
26
|
+
ebooks auth
|
27
|
+
ebooks consume <corpus_path> [corpus_path2] [...]
|
28
|
+
ebooks consume-all <model_name> <corpus_path> [corpus_path2] [...]
|
29
|
+
ebooks append <model_name> <corpus_path>
|
30
|
+
ebooks gen <model_path> [input]
|
31
|
+
ebooks archive <username> [path]
|
32
|
+
ebooks sync <botname> [username]
|
33
|
+
ebooks tweet <model_path> <botname>
|
34
|
+
ebooks version
|
35
|
+
STR
|
36
|
+
|
37
|
+
def self.help(command=nil)
|
38
|
+
if command.nil?
|
39
|
+
log HELP.default
|
40
|
+
else
|
41
|
+
log HELP[command].gsub(/^ {4}/, '')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
HELP.new = <<-STR
|
46
|
+
Usage: ebooks new <reponame>
|
47
|
+
|
48
|
+
Creates a new skeleton repository defining a template bot in
|
49
|
+
the current working directory specified by <reponame>.
|
50
|
+
STR
|
51
|
+
|
52
|
+
def self.new(reponame)
|
53
|
+
if reponame.nil?
|
54
|
+
help :new
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
path = "./#{reponame}"
|
59
|
+
|
60
|
+
if File.exists?(path)
|
61
|
+
log "#{path} already exists. Please remove if you want to recreate."
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
FileUtils.cp_r(Ebooks::SKELETON_PATH, path)
|
66
|
+
FileUtils.mv(File.join(path, 'gitignore'), File.join(path, '.gitignore'))
|
67
|
+
|
68
|
+
File.open(File.join(path, 'bots.rb'), 'w') do |f|
|
69
|
+
template = File.read(File.join(Ebooks::SKELETON_PATH, 'bots.rb'))
|
70
|
+
f.write(template.gsub("{{BOT_NAME}}", reponame))
|
71
|
+
end
|
72
|
+
|
73
|
+
File.open(File.join(path, 'Gemfile'), 'w') do |f|
|
74
|
+
template = File.read(File.join(Ebooks::SKELETON_PATH, 'Gemfile'))
|
75
|
+
f.write(template.gsub("{{RUBY_VERSION}}", RUBY_VERSION))
|
76
|
+
end
|
77
|
+
|
78
|
+
log "New twitter_ebooks app created at #{reponame}"
|
79
|
+
end
|
80
|
+
|
81
|
+
HELP.consume = <<-STR
|
82
|
+
Usage: ebooks consume <corpus_path> [corpus_path2] [...]
|
83
|
+
|
84
|
+
Processes some number of text files or json tweet corpuses
|
85
|
+
into usable models. These will be output at model/<corpus_name>.model
|
86
|
+
STR
|
87
|
+
|
88
|
+
def self.consume(pathes)
|
89
|
+
if pathes.empty?
|
90
|
+
help :consume
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
|
94
|
+
pathes.each do |path|
|
95
|
+
filename = File.basename(path)
|
96
|
+
shortname = filename.split('.')[0..-2].join('.')
|
97
|
+
|
98
|
+
FileUtils.mkdir_p(File.join(APP_PATH, 'model'))
|
99
|
+
outpath = File.join(APP_PATH, 'model', "#{shortname}.model")
|
100
|
+
|
101
|
+
Ebooks::Model.consume(path).save(outpath)
|
102
|
+
log "Corpus consumed to #{outpath}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
HELP.consume_all = <<-STR
|
107
|
+
Usage: ebooks consume-all <model_name> <corpus_path> [corpus_path2] [...]
|
108
|
+
|
109
|
+
Processes some number of text files or json tweet corpuses
|
110
|
+
into one usable model. It will be output at model/<model_name>.model
|
111
|
+
STR
|
112
|
+
|
113
|
+
def self.consume_all(name, paths)
|
114
|
+
if paths.empty?
|
115
|
+
help :consume_all
|
116
|
+
exit 1
|
117
|
+
end
|
118
|
+
|
119
|
+
outpath = File.join(APP_PATH, 'model', "#{name}.model")
|
120
|
+
Ebooks::Model.consume_all(paths).save(outpath)
|
121
|
+
log "Corpuses consumed to #{outpath}"
|
122
|
+
end
|
123
|
+
|
124
|
+
HELP.append = <<-STR
|
125
|
+
Usage: ebooks append <model_name> <corpus_path>
|
126
|
+
|
127
|
+
Process then append the provided corpus to the model
|
128
|
+
instead of overwriting.
|
129
|
+
STR
|
130
|
+
|
131
|
+
def self.append(name, path)
|
132
|
+
if !name || !path
|
133
|
+
help :append
|
134
|
+
exit 1
|
135
|
+
end
|
136
|
+
|
137
|
+
Ebooks::Model.consume(path).append(File.join(APP_PATH,'model',"#{name}.model"))
|
138
|
+
log "Corpus appended to #{name}.model"
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
HELP.jsonify = <<-STR
|
143
|
+
Usage: ebooks jsonify <tweets.csv> [tweets.csv2] [...]
|
144
|
+
|
145
|
+
Takes a csv twitter archive and converts it to json.
|
146
|
+
STR
|
147
|
+
|
148
|
+
def self.jsonify(paths)
|
149
|
+
if paths.empty?
|
150
|
+
log usage
|
151
|
+
exit
|
152
|
+
end
|
153
|
+
|
154
|
+
paths.each do |path|
|
155
|
+
name = File.basename(path).split('.')[0]
|
156
|
+
new_path = name + ".json"
|
157
|
+
|
158
|
+
tweets = []
|
159
|
+
id = nil
|
160
|
+
if path.split('.')[-1] == "csv" #from twitter archive
|
161
|
+
csv_archive = CSV.read(path, :headers=>:first_row)
|
162
|
+
tweets = csv_archive.map do |tweet|
|
163
|
+
{ text: tweet['text'], id: tweet['tweet_id'] }
|
164
|
+
end
|
165
|
+
else
|
166
|
+
File.read(path).split("\n").each do |l|
|
167
|
+
if l.start_with?('# ')
|
168
|
+
id = l.split('# ')[-1]
|
169
|
+
else
|
170
|
+
tweet = { text: l }
|
171
|
+
if id
|
172
|
+
tweet[:id] = id
|
173
|
+
id = nil
|
174
|
+
end
|
175
|
+
tweets << tweet
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
File.open(new_path, 'w') do |f|
|
181
|
+
log "Writing #{tweets.length} tweets to #{new_path}"
|
182
|
+
f.write(JSON.pretty_generate(tweets))
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
HELP.gen = <<-STR
|
189
|
+
Usage: ebooks gen <model_path> [input]
|
190
|
+
|
191
|
+
Make a test tweet from the processed model at <model_path>.
|
192
|
+
Will respond to input if provided.
|
193
|
+
STR
|
194
|
+
|
195
|
+
def self.gen(model_path, input)
|
196
|
+
if model_path.nil?
|
197
|
+
help :gen
|
198
|
+
exit 1
|
199
|
+
end
|
200
|
+
|
201
|
+
model = Ebooks::Model.load(model_path)
|
202
|
+
if input && !input.empty?
|
203
|
+
puts "@cmd " + model.make_response(input, 135)
|
204
|
+
else
|
205
|
+
puts model.make_statement
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
HELP.archive = <<-STR
|
210
|
+
Usage: ebooks archive <username> [outpath]
|
211
|
+
|
212
|
+
Downloads a json corpus of the <username>'s tweets.
|
213
|
+
Output defaults to corpus/<username>.json
|
214
|
+
Due to API limitations, this can only receive up to ~3000 tweets
|
215
|
+
into the past.
|
216
|
+
|
217
|
+
The first time you run archive, you will need to enter the auth
|
218
|
+
details of some account to use for accessing the API. This info
|
219
|
+
will then be stored in ~/.ebooksrc for later use, and can be
|
220
|
+
modified there if needed.
|
221
|
+
STR
|
222
|
+
|
223
|
+
def self.archive(username, outpath=nil)
|
224
|
+
if username.nil?
|
225
|
+
help :archive
|
226
|
+
exit 1
|
227
|
+
end
|
228
|
+
|
229
|
+
Ebooks::Archive.new(username, outpath).sync
|
230
|
+
end
|
231
|
+
|
232
|
+
HELP.sync = <<-STR
|
233
|
+
Usage: ebooks sync <botname> <username>
|
234
|
+
|
235
|
+
Copies and flips <username>'s avatar and cover photo, uploading them to <botname>'s profile.
|
236
|
+
|
237
|
+
Stores saved avatar's and covers in image/.
|
238
|
+
|
239
|
+
STR
|
240
|
+
|
241
|
+
def self.sync(botname, username)
|
242
|
+
if botname.nil?
|
243
|
+
help :sync
|
244
|
+
exit 1
|
245
|
+
end
|
246
|
+
|
247
|
+
load File.join(APP_PATH, 'bots.rb')
|
248
|
+
Ebooks::Sync::run(botname, username)
|
249
|
+
end
|
250
|
+
|
251
|
+
HELP.tweet = <<-STR
|
252
|
+
Usage: ebooks tweet <model_path> <botname>
|
253
|
+
|
254
|
+
Sends a public tweet from the specified bot using text
|
255
|
+
from the processed model at <model_path>.
|
256
|
+
STR
|
257
|
+
|
258
|
+
def self.tweet(modelpath, botname)
|
259
|
+
if modelpath.nil? || botname.nil?
|
260
|
+
help :tweet
|
261
|
+
exit 1
|
262
|
+
end
|
263
|
+
|
264
|
+
load File.join(APP_PATH, 'bots.rb')
|
265
|
+
model = Ebooks::Model.load(modelpath)
|
266
|
+
statement = model.make_statement
|
267
|
+
bot = Ebooks::Bot.get(botname)
|
268
|
+
if bot.nil?
|
269
|
+
log "No such bot configured in bots.rb: #{botname}"
|
270
|
+
exit 1
|
271
|
+
end
|
272
|
+
bot.configure
|
273
|
+
bot.tweet(statement)
|
274
|
+
end
|
275
|
+
|
276
|
+
HELP.auth = <<-STR
|
277
|
+
Usage: ebooks auth
|
278
|
+
|
279
|
+
Authenticates your Twitter app for any account. By default, will
|
280
|
+
use the consumer key and secret from the first defined bot. You
|
281
|
+
can specify another by setting the CONSUMER_KEY and CONSUMER_SECRET
|
282
|
+
environment variables.
|
283
|
+
STR
|
284
|
+
|
285
|
+
def self.auth
|
286
|
+
consumer_key, consumer_secret = find_consumer
|
287
|
+
require 'oauth'
|
288
|
+
|
289
|
+
consumer = OAuth::Consumer.new(
|
290
|
+
consumer_key,
|
291
|
+
consumer_secret,
|
292
|
+
site: 'https://twitter.com/',
|
293
|
+
scheme: :header
|
294
|
+
)
|
295
|
+
|
296
|
+
request_token = consumer.get_request_token
|
297
|
+
auth_url = request_token.authorize_url()
|
298
|
+
|
299
|
+
pin = nil
|
300
|
+
loop do
|
301
|
+
log auth_url
|
302
|
+
|
303
|
+
log "Go to the above url and follow the prompts, then enter the PIN code here."
|
304
|
+
print "> "
|
305
|
+
|
306
|
+
pin = STDIN.gets.chomp
|
307
|
+
|
308
|
+
break unless pin.empty?
|
309
|
+
end
|
310
|
+
|
311
|
+
access_token = request_token.get_access_token(oauth_verifier: pin)
|
312
|
+
|
313
|
+
log "Account authorized successfully. Make sure to put these in your bots.rb!\n" +
|
314
|
+
" bot.access_token = \"#{access_token.token}\"\n" +
|
315
|
+
" bot.access_token_secret = \"#{access_token.secret}\""
|
316
|
+
end
|
317
|
+
|
318
|
+
HELP.console = <<-STR
|
319
|
+
Usage: ebooks c[onsole]
|
320
|
+
|
321
|
+
Starts an interactive ruby session with your bots loaded
|
322
|
+
and configured.
|
323
|
+
STR
|
324
|
+
|
325
|
+
def self.console
|
326
|
+
load_bots
|
327
|
+
require 'pry'; Ebooks.module_exec { pry }
|
328
|
+
end
|
329
|
+
|
330
|
+
HELP.version = <<-STR
|
331
|
+
Usage: ebooks version
|
332
|
+
|
333
|
+
Shows you twitter_ebooks' version number.
|
334
|
+
STR
|
335
|
+
|
336
|
+
def self.version
|
337
|
+
require File.expand_path('../../lib/twitter_ebooks/version', __FILE__)
|
338
|
+
log Ebooks::VERSION
|
339
|
+
end
|
340
|
+
|
341
|
+
HELP.start = <<-STR
|
342
|
+
Usage: ebooks s[tart] [botname]
|
343
|
+
|
344
|
+
Starts running bots. If botname is provided, only runs that bot.
|
345
|
+
STR
|
346
|
+
|
347
|
+
def self.start(botname=nil)
|
348
|
+
load_bots
|
349
|
+
|
350
|
+
if botname.nil?
|
351
|
+
bots = Ebooks::Bot.all
|
352
|
+
else
|
353
|
+
bots = Ebooks::Bot.all.select { |bot| bot.username == botname }
|
354
|
+
if bots.empty?
|
355
|
+
log "Couldn't find a defined bot for @#{botname}!"
|
356
|
+
exit 1
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
threads = []
|
361
|
+
bots.each do |bot|
|
362
|
+
threads << Thread.new { bot.prepare }
|
363
|
+
end
|
364
|
+
threads.each(&:join)
|
365
|
+
|
366
|
+
threads = []
|
367
|
+
bots.each do |bot|
|
368
|
+
threads << Thread.new do
|
369
|
+
bot.start
|
370
|
+
loop do
|
371
|
+
sleep 60
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
threads.each(&:join)
|
376
|
+
end
|
377
|
+
|
378
|
+
# Non-command methods
|
379
|
+
|
380
|
+
def self.find_consumer
|
381
|
+
if ENV['CONSUMER_KEY'] && ENV['CONSUMER_SECRET']
|
382
|
+
log "Using consumer details from environment variables:\n" +
|
383
|
+
" consumer key: #{ENV['CONSUMER_KEY']}\n" +
|
384
|
+
" consumer secret: #{ENV['CONSUMER_SECRET']}"
|
385
|
+
return [ENV['CONSUMER_KEY'], ENV['CONSUMER_SECRET']]
|
386
|
+
end
|
387
|
+
|
388
|
+
load_bots
|
389
|
+
consumer_key = nil
|
390
|
+
consumer_secret = nil
|
391
|
+
Ebooks::Bot.all.each do |bot|
|
392
|
+
if bot.consumer_key && bot.consumer_secret
|
393
|
+
consumer_key = bot.consumer_key
|
394
|
+
consumer_secret = bot.consumer_secret
|
395
|
+
log "Using consumer details from @#{bot.username}:\n" +
|
396
|
+
" consumer key: #{bot.consumer_key}\n" +
|
397
|
+
" consumer secret: #{bot.consumer_secret}\n"
|
398
|
+
return consumer_key, consumer_secret
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
if consumer_key.nil? || consumer_secret.nil?
|
403
|
+
log "Couldn't find any consumer details to auth an account with.\n" +
|
404
|
+
"Please either configure a bot with consumer_key and consumer_secret\n" +
|
405
|
+
"or provide the CONSUMER_KEY and CONSUMER_SECRET environment variables."
|
406
|
+
exit 1
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.load_bots
|
411
|
+
load 'bots.rb'
|
412
|
+
|
413
|
+
if Ebooks::Bot.all.empty?
|
414
|
+
puts "Couldn't find any bots! Please make sure bots.rb instantiates at least one bot."
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def self.command(args)
|
419
|
+
if args.length == 0
|
420
|
+
help
|
421
|
+
exit 1
|
422
|
+
end
|
423
|
+
|
424
|
+
case args[0]
|
425
|
+
when "new" then new(args[1])
|
426
|
+
when "consume" then consume(args[1..-1])
|
427
|
+
when "consume-all" then consume_all(args[1], args[2..-1])
|
428
|
+
when "append" then append(args[1],args[2])
|
429
|
+
when "gen" then gen(args[1], args[2..-1].join(' '))
|
430
|
+
when "archive" then archive(args[1], args[2])
|
431
|
+
when "sync" then sync(args[1], args[2])
|
432
|
+
when "tweet" then tweet(args[1], args[2])
|
433
|
+
when "jsonify" then jsonify(args[1..-1])
|
434
|
+
when "auth" then auth
|
435
|
+
when "console" then console
|
436
|
+
when "c" then console
|
437
|
+
when "start" then start(args[1])
|
438
|
+
when "s" then start(args[1])
|
439
|
+
when "help" then help(args[1])
|
440
|
+
when "version" then version
|
441
|
+
else
|
442
|
+
log "No such command '#{args[0]}'"
|
443
|
+
help
|
444
|
+
exit 1
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
Ebooks::CLI.command(ARGV)
|