bot_twitter_ebooks 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +200 -0
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +168 -0
- data/Rakefile +2 -0
- data/bin/ebooks +454 -0
- data/bot_twitter_ebooks.gemspec +38 -0
- data/data/adjectives.txt +1466 -0
- data/data/nouns.txt +2193 -0
- data/lib/bot_twitter_ebooks.rb +22 -0
- data/lib/bot_twitter_ebooks/archive.rb +117 -0
- data/lib/bot_twitter_ebooks/bot.rb +481 -0
- data/lib/bot_twitter_ebooks/model.rb +336 -0
- data/lib/bot_twitter_ebooks/nlp.rb +195 -0
- data/lib/bot_twitter_ebooks/suffix.rb +104 -0
- data/lib/bot_twitter_ebooks/sync.rb +52 -0
- data/lib/bot_twitter_ebooks/version.rb +3 -0
- data/skeleton/Gemfile +4 -0
- data/skeleton/Procfile +1 -0
- data/skeleton/bots.rb +66 -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/elonmusk.json +12866 -0
- data/spec/data/elonmusk.model +2414 -0
- data/spec/memprof.rb +37 -0
- data/spec/model_spec.rb +88 -0
- data/spec/spec_helper.rb +6 -0
- metadata +310 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 01cb997f239d1bc64a15b0ccbc6b1e83d641c9fd
|
4
|
+
data.tar.gz: f09849a1882a75c3c32f4cd0c6811c6c40a4968f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e8ca65ed4046b3665a5d5675efa37643cafda354b953d935aa9e3ef2d2c4399f437d10deb337db8e6545e96afc3b48467be9c23e53b42c3786ca3b8997c4ef39
|
7
|
+
data.tar.gz: 8694e98af0cf9f66d627c62ae33206ea387cfae51e40a414eace37818fce90d70c673dea2f1f86c82607d6073052e3adedf2953f194556297e50eb0cffe3b6db
|
data/.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* text=auto
|
data/.gitignore
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
|
2
|
+
# Created by https://www.gitignore.io/api/git,vim,ruby,linux,macos,emacs,windows
|
3
|
+
|
4
|
+
### Emacs ###
|
5
|
+
# -*- mode: gitignore; -*-
|
6
|
+
*~
|
7
|
+
\#*\#
|
8
|
+
/.emacs.desktop
|
9
|
+
/.emacs.desktop.lock
|
10
|
+
*.elc
|
11
|
+
auto-save-list
|
12
|
+
tramp
|
13
|
+
.\#*
|
14
|
+
|
15
|
+
# Org-mode
|
16
|
+
.org-id-locations
|
17
|
+
*_archive
|
18
|
+
|
19
|
+
# flymake-mode
|
20
|
+
*_flymake.*
|
21
|
+
|
22
|
+
# eshell files
|
23
|
+
/eshell/history
|
24
|
+
/eshell/lastdir
|
25
|
+
|
26
|
+
# elpa packages
|
27
|
+
/elpa/
|
28
|
+
|
29
|
+
# reftex files
|
30
|
+
*.rel
|
31
|
+
|
32
|
+
# AUCTeX auto folder
|
33
|
+
/auto/
|
34
|
+
|
35
|
+
# cask packages
|
36
|
+
.cask/
|
37
|
+
dist/
|
38
|
+
|
39
|
+
# Flycheck
|
40
|
+
flycheck_*.el
|
41
|
+
|
42
|
+
# server auth directory
|
43
|
+
/server/
|
44
|
+
|
45
|
+
# projectiles files
|
46
|
+
.projectile
|
47
|
+
projectile-bookmarks.eld
|
48
|
+
|
49
|
+
# directory configuration
|
50
|
+
.dir-locals.el
|
51
|
+
|
52
|
+
# saveplace
|
53
|
+
places
|
54
|
+
|
55
|
+
# url cache
|
56
|
+
url/cache/
|
57
|
+
|
58
|
+
# cedet
|
59
|
+
ede-projects.el
|
60
|
+
|
61
|
+
# smex
|
62
|
+
smex-items
|
63
|
+
|
64
|
+
# company-statistics
|
65
|
+
company-statistics-cache.el
|
66
|
+
|
67
|
+
# anaconda-mode
|
68
|
+
anaconda-mode/
|
69
|
+
|
70
|
+
### Git ###
|
71
|
+
*.orig
|
72
|
+
|
73
|
+
### Linux ###
|
74
|
+
|
75
|
+
# temporary files which can be created if a process still has a handle open of a deleted file
|
76
|
+
.fuse_hidden*
|
77
|
+
|
78
|
+
# KDE directory preferences
|
79
|
+
.directory
|
80
|
+
|
81
|
+
# Linux trash folder which might appear on any partition or disk
|
82
|
+
.Trash-*
|
83
|
+
|
84
|
+
# .nfs files are created when an open file is removed but is still being accessed
|
85
|
+
.nfs*
|
86
|
+
|
87
|
+
### macOS ###
|
88
|
+
*.DS_Store
|
89
|
+
.AppleDouble
|
90
|
+
.LSOverride
|
91
|
+
|
92
|
+
# Icon must end with two \r
|
93
|
+
Icon
|
94
|
+
|
95
|
+
# Thumbnails
|
96
|
+
._*
|
97
|
+
|
98
|
+
# Files that might appear in the root of a volume
|
99
|
+
.DocumentRevisions-V100
|
100
|
+
.fseventsd
|
101
|
+
.Spotlight-V100
|
102
|
+
.TemporaryItems
|
103
|
+
.Trashes
|
104
|
+
.VolumeIcon.icns
|
105
|
+
.com.apple.timemachine.donotpresent
|
106
|
+
|
107
|
+
# Directories potentially created on remote AFP share
|
108
|
+
.AppleDB
|
109
|
+
.AppleDesktop
|
110
|
+
Network Trash Folder
|
111
|
+
Temporary Items
|
112
|
+
.apdisk
|
113
|
+
|
114
|
+
### Ruby ###
|
115
|
+
*.gem
|
116
|
+
*.rbc
|
117
|
+
/.config
|
118
|
+
/coverage/
|
119
|
+
/InstalledFiles
|
120
|
+
/pkg/
|
121
|
+
/spec/reports/
|
122
|
+
/spec/examples.txt
|
123
|
+
/test/tmp/
|
124
|
+
/test/version_tmp/
|
125
|
+
/tmp/
|
126
|
+
|
127
|
+
# Used by dotenv library to load environment variables.
|
128
|
+
.env
|
129
|
+
|
130
|
+
## Specific to RubyMotion:
|
131
|
+
.dat*
|
132
|
+
.repl_history
|
133
|
+
build/
|
134
|
+
*.bridgesupport
|
135
|
+
build-iPhoneOS/
|
136
|
+
build-iPhoneSimulator/
|
137
|
+
|
138
|
+
## Specific to RubyMotion (use of CocoaPods):
|
139
|
+
#
|
140
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
141
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
142
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
143
|
+
#
|
144
|
+
# vendor/Pods/
|
145
|
+
|
146
|
+
## Documentation cache and generated files:
|
147
|
+
/.yardoc/
|
148
|
+
/_yardoc/
|
149
|
+
/doc/
|
150
|
+
/rdoc/
|
151
|
+
|
152
|
+
## Environment normalization:
|
153
|
+
/.bundle/
|
154
|
+
/vendor/bundle
|
155
|
+
/lib/bundler/man/
|
156
|
+
|
157
|
+
# for a library or gem, you might want to ignore these files since the code is
|
158
|
+
# intended to run in multiple environments; otherwise, check them in:
|
159
|
+
Gemfile.lock
|
160
|
+
.ruby-version
|
161
|
+
.ruby-gemset
|
162
|
+
|
163
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
164
|
+
.rvmrc
|
165
|
+
|
166
|
+
### Vim ###
|
167
|
+
# swap
|
168
|
+
[._]*.s[a-v][a-z]
|
169
|
+
[._]*.sw[a-p]
|
170
|
+
[._]s[a-v][a-z]
|
171
|
+
[._]sw[a-p]
|
172
|
+
# session
|
173
|
+
Session.vim
|
174
|
+
# temporary
|
175
|
+
.netrwhist
|
176
|
+
# auto-generated tag files
|
177
|
+
tags
|
178
|
+
|
179
|
+
### Windows ###
|
180
|
+
# Windows thumbnail cache files
|
181
|
+
Thumbs.db
|
182
|
+
ehthumbs.db
|
183
|
+
ehthumbs_vista.db
|
184
|
+
|
185
|
+
# Folder config file
|
186
|
+
Desktop.ini
|
187
|
+
|
188
|
+
# Recycle Bin used on file shares
|
189
|
+
$RECYCLE.BIN/
|
190
|
+
|
191
|
+
# Windows Installer files
|
192
|
+
*.cab
|
193
|
+
*.msi
|
194
|
+
*.msm
|
195
|
+
*.msp
|
196
|
+
|
197
|
+
# Windows shortcuts
|
198
|
+
*.lnk
|
199
|
+
|
200
|
+
# End of https://www.gitignore.io/api/git,vim,ruby,linux,macos,emacs,windows
|
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,168 @@
|
|
1
|
+
# bot_twitter_ebooks
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/bot_twitter_ebooks.svg)](http://badge.fury.io/rb/bot_twitter_ebooks)
|
4
|
+
[![Build Status](https://travis-ci.org/astrolince/bot_twitter_ebooks.svg)](https://travis-ci.org/astrolince/bot_twitter_ebooks)
|
5
|
+
[![Dependency Status](https://gemnasium.com/astrolince/bot_twitter_ebooks.svg)](https://gemnasium.com/astrolince/bot_twitter_ebooks)
|
6
|
+
|
7
|
+
A framework for building interactive twitterbots which generate tweets based on pseudo-Markov texts models and respond to mentions/DMs/favs/rts. See [ebooks_example](https://github.com/astrolince/ebooks_example) for a fully-fledged bot definition.
|
8
|
+
|
9
|
+
## New in 3.0
|
10
|
+
|
11
|
+
- About 80% less memory and storage use for models
|
12
|
+
- Bots run in their own threads (no eventmachine), and startup is parallelized
|
13
|
+
- Bots start with `ebooks start`, and no longer die on unhandled exceptions
|
14
|
+
- `ebooks auth` command will create new access tokens, for running multiple bots
|
15
|
+
- `ebooks console` starts a ruby interpreter with bots loaded (see Ebooks::Bot.all)
|
16
|
+
- Replies are slightly rate-limited to prevent infinite bot convos
|
17
|
+
- Non-participating users in a mention chain will be dropped after a few tweets
|
18
|
+
- [API documentation](http://rdoc.info/github/astrolince/bot_twitter_ebooks) and tests
|
19
|
+
|
20
|
+
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.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Requires Ruby 2.1+. Ruby 2.4+ is recommended.
|
25
|
+
|
26
|
+
```bash
|
27
|
+
gem install bot_twitter_ebooks
|
28
|
+
```
|
29
|
+
|
30
|
+
## Setting up a bot
|
31
|
+
|
32
|
+
Run `ebooks new <reponame>` to generate a new repository containing a sample bots.rb file, which looks like this:
|
33
|
+
|
34
|
+
``` ruby
|
35
|
+
# This is an example bot definition with event handlers commented out
|
36
|
+
# You can define and instantiate as many bots as you like
|
37
|
+
|
38
|
+
class MyBot < Ebooks::Bot
|
39
|
+
# Configuration here applies to all MyBots
|
40
|
+
def configure
|
41
|
+
# Consumer details come from registering an app at https://dev.twitter.com/
|
42
|
+
# Once you have consumer details, use "ebooks auth" for new access tokens
|
43
|
+
self.consumer_key = '' # Your app consumer key
|
44
|
+
self.consumer_secret = '' # Your app consumer secret
|
45
|
+
|
46
|
+
# Users to block instead of interacting with
|
47
|
+
self.blacklist = ['tnietzschequote']
|
48
|
+
|
49
|
+
# Range in seconds to randomize delay when bot.delay is called
|
50
|
+
self.delay_range = 1..6
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_startup
|
54
|
+
scheduler.every '24h' do
|
55
|
+
# Tweet something every 24 hours
|
56
|
+
# See https://github.com/jmettraux/rufus-scheduler
|
57
|
+
# tweet("hi")
|
58
|
+
# pictweet("hi", "cuteselfie.jpg")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_message(dm)
|
63
|
+
# Reply to a DM
|
64
|
+
# Make sure to set your API permissions to "Read, Write and Access direct messages" or this won't work!
|
65
|
+
# reply(dm, "secret secrets")
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_follow(user)
|
69
|
+
# Follow a user back
|
70
|
+
# follow(user.screen_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def on_mention(tweet)
|
74
|
+
# Reply to a mention
|
75
|
+
# reply(tweet, "oh hullo")
|
76
|
+
end
|
77
|
+
|
78
|
+
def on_timeline(tweet)
|
79
|
+
# Reply to a tweet in the bot's timeline
|
80
|
+
# reply(tweet, "nice tweet")
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_favorite(user, tweet)
|
84
|
+
# Follow user who just favorited bot's tweet
|
85
|
+
# follow(user.screen_name)
|
86
|
+
end
|
87
|
+
|
88
|
+
def on_retweet(tweet)
|
89
|
+
# Follow user who just retweeted bot's tweet
|
90
|
+
# follow(tweet.user.screen_name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Make a MyBot and attach it to an account
|
95
|
+
MyBot.new("elonmusk_ebooks") do |bot|
|
96
|
+
bot.access_token = "" # Token connecting the app to this account
|
97
|
+
bot.access_token_secret = "" # Secret connecting the app to this account
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
`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.
|
102
|
+
|
103
|
+
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.
|
104
|
+
|
105
|
+
## Archiving accounts
|
106
|
+
|
107
|
+
bot_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 @elonmusk):
|
108
|
+
|
109
|
+
``` zsh
|
110
|
+
➜ ebooks archive elonmusk corpus/elonmusk.json
|
111
|
+
Currently 2584 tweets for elonmusk
|
112
|
+
Received 34 new tweets
|
113
|
+
```
|
114
|
+
|
115
|
+
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.
|
116
|
+
|
117
|
+
If you have full access to the account you can request your full Twitter archive in web settings and convert the tweets.csv to .json with the command: `ebooks jsonify tweets.csv`
|
118
|
+
|
119
|
+
## Text models
|
120
|
+
|
121
|
+
In order to use the included text modeling, you'll first need to preprocess your archive into a more efficient form:
|
122
|
+
|
123
|
+
``` zsh
|
124
|
+
➜ ebooks consume corpus/elonmusk.json
|
125
|
+
Reading json corpus from corpus/elonmusk.json
|
126
|
+
Removing commented lines and sorting mentions
|
127
|
+
Segmenting text into sentences
|
128
|
+
Tokenizing 987 statements and 1597 mentions
|
129
|
+
Ranking keywords
|
130
|
+
Corpus consumed to model/elonmusk.model
|
131
|
+
```
|
132
|
+
|
133
|
+
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.
|
134
|
+
|
135
|
+
Text files use newlines and full stops to seperate statements.
|
136
|
+
|
137
|
+
You can also consume multiple archives / plaintext files into a single model using `ebooks consume-all <model_name> <corpus_paths>`.
|
138
|
+
|
139
|
+
Once you have a model, the primary use is to produce statements and related responses to input, using a pseudo-Markov generator:
|
140
|
+
|
141
|
+
``` ruby
|
142
|
+
> model = Ebooks::Model.load("model/elonmusk.model")
|
143
|
+
> model.make_statement(140)
|
144
|
+
=> "Rainbows, unicorns and LA and seems to be working on the Falcon fireball investigation."
|
145
|
+
> model.make_response("Will you give free trips to Mars?", 130)
|
146
|
+
=> "Plan is to provide something special that only existing owners can give to friends and it is limited to 5 people."
|
147
|
+
```
|
148
|
+
|
149
|
+
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:
|
150
|
+
|
151
|
+
``` ruby
|
152
|
+
top100 = model.keywords.take(100)
|
153
|
+
tokens = Ebooks::NLP.tokenize(tweet.text)
|
154
|
+
|
155
|
+
if tokens.find { |t| top100.include?(t) }
|
156
|
+
favorite(tweet)
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
## Bot niceness
|
161
|
+
|
162
|
+
bot_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!
|
163
|
+
|
164
|
+
## License
|
165
|
+
|
166
|
+
bot_twitter_ebooks is [MIT licensed](https://github.com/astrolince/bot_twitter_ebooks/blob/master/LICENSE) and is a fork of [twitter_ebooks](https://github.com/mispy/twitter_ebooks).
|
167
|
+
|
168
|
+
Thanks to Jaiden Mispy ([@mispy](https://github.com/mispy)) and all the human beings/bots/star stuff affected by universe entropy that contribute to the project.
|
data/Rakefile
ADDED
data/bin/ebooks
ADDED
@@ -0,0 +1,454 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
require 'bot_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
|
+
APP_PATH = Dir.pwd # XXX do some recursive thing instead
|
16
|
+
HELP = OpenStruct.new
|
17
|
+
|
18
|
+
HELP.default = <<STR
|
19
|
+
Usage:
|
20
|
+
ebooks help <command>
|
21
|
+
|
22
|
+
ebooks new <reponame>
|
23
|
+
ebooks s[tart]
|
24
|
+
ebooks c[onsole]
|
25
|
+
ebooks auth
|
26
|
+
ebooks consume <corpus_path> [corpus_path2] [...]
|
27
|
+
ebooks consume-all <model_name> <corpus_path> [corpus_path2] [...]
|
28
|
+
ebooks append <model_name> <corpus_path>
|
29
|
+
ebooks gen <model_path> [input]
|
30
|
+
ebooks archive <username> [path]
|
31
|
+
ebooks sync <botname> [username]
|
32
|
+
ebooks tweet <model_path> <botname>
|
33
|
+
ebooks version
|
34
|
+
STR
|
35
|
+
|
36
|
+
def self.help(command=nil)
|
37
|
+
if command.nil?
|
38
|
+
log HELP.default
|
39
|
+
else
|
40
|
+
log HELP[command].gsub(/^ {4}/, '')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
HELP.new = <<-STR
|
45
|
+
Usage: ebooks new <reponame>
|
46
|
+
|
47
|
+
Creates a new skeleton repository defining a template bot in
|
48
|
+
the current working directory specified by <reponame>.
|
49
|
+
STR
|
50
|
+
|
51
|
+
def self.new(reponame)
|
52
|
+
if reponame.nil?
|
53
|
+
help :new
|
54
|
+
exit 1
|
55
|
+
end
|
56
|
+
|
57
|
+
path = "./#{reponame}"
|
58
|
+
|
59
|
+
if File.exists?(path)
|
60
|
+
log "#{path} already exists. Please remove if you want to recreate."
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
|
64
|
+
FileUtils.cp_r(Ebooks::SKELETON_PATH, path)
|
65
|
+
FileUtils.mv(File.join(path, 'gitignore'), File.join(path, '.gitignore'))
|
66
|
+
|
67
|
+
File.open(File.join(path, 'bots.rb'), 'w') do |f|
|
68
|
+
template = File.read(File.join(Ebooks::SKELETON_PATH, 'bots.rb'))
|
69
|
+
f.write(template.gsub("{{BOT_NAME}}", reponame))
|
70
|
+
end
|
71
|
+
|
72
|
+
File.open(File.join(path, 'Gemfile'), 'w') do |f|
|
73
|
+
template = File.read(File.join(Ebooks::SKELETON_PATH, 'Gemfile'))
|
74
|
+
f.write(template.gsub("{{RUBY_VERSION}}", RUBY_VERSION))
|
75
|
+
end
|
76
|
+
|
77
|
+
log "New twitter_ebooks app created at #{reponame}"
|
78
|
+
end
|
79
|
+
|
80
|
+
HELP.consume = <<-STR
|
81
|
+
Usage: ebooks consume <corpus_path> [corpus_path2] [...]
|
82
|
+
|
83
|
+
Processes some number of text files or json tweet corpuses
|
84
|
+
into usable models. These will be output at model/<corpus_name>.model
|
85
|
+
STR
|
86
|
+
|
87
|
+
def self.consume(pathes)
|
88
|
+
if pathes.empty?
|
89
|
+
help :consume
|
90
|
+
exit 1
|
91
|
+
end
|
92
|
+
|
93
|
+
pathes.each do |path|
|
94
|
+
filename = File.basename(path)
|
95
|
+
shortname = filename.split('.')[0..-2].join('.')
|
96
|
+
|
97
|
+
FileUtils.mkdir_p(File.join(APP_PATH, 'model'))
|
98
|
+
outpath = File.join(APP_PATH, 'model', "#{shortname}.model")
|
99
|
+
|
100
|
+
Ebooks::Model.consume(path).save(outpath)
|
101
|
+
log "Corpus consumed to #{outpath}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
HELP.consume_all = <<-STR
|
106
|
+
Usage: ebooks consume-all <model_name> <corpus_path> [corpus_path2] [...]
|
107
|
+
|
108
|
+
Processes some number of text files or json tweet corpuses
|
109
|
+
into one usable model. It will be output at model/<model_name>.model
|
110
|
+
STR
|
111
|
+
|
112
|
+
def self.consume_all(name, paths)
|
113
|
+
if paths.empty?
|
114
|
+
help :consume_all
|
115
|
+
exit 1
|
116
|
+
end
|
117
|
+
|
118
|
+
outpath = File.join(APP_PATH, 'model', "#{name}.model")
|
119
|
+
Ebooks::Model.consume_all(paths).save(outpath)
|
120
|
+
log "Corpuses consumed to #{outpath}"
|
121
|
+
end
|
122
|
+
|
123
|
+
HELP.append = <<-STR
|
124
|
+
Usage: ebooks append <model_name> <corpus_path>
|
125
|
+
|
126
|
+
Process then append the provided corpus to the model
|
127
|
+
instead of overwriting.
|
128
|
+
STR
|
129
|
+
|
130
|
+
def self.append(name, path)
|
131
|
+
if !name || !path
|
132
|
+
help :append
|
133
|
+
exit 1
|
134
|
+
end
|
135
|
+
|
136
|
+
Ebooks::Model.consume(path).append(File.join(APP_PATH,'model',"#{name}.model"))
|
137
|
+
log "Corpus appended to #{name}.model"
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
HELP.jsonify = <<-STR
|
142
|
+
Usage: ebooks jsonify <tweets.csv> [tweets.csv2] [...]
|
143
|
+
|
144
|
+
Takes a csv twitter archive and converts it to json.
|
145
|
+
STR
|
146
|
+
|
147
|
+
def self.jsonify(paths)
|
148
|
+
if paths.empty?
|
149
|
+
log usage
|
150
|
+
exit
|
151
|
+
end
|
152
|
+
|
153
|
+
paths.each do |path|
|
154
|
+
name = File.basename(path).split('.')[0]
|
155
|
+
new_path = name + ".json"
|
156
|
+
|
157
|
+
tweets = []
|
158
|
+
id = nil
|
159
|
+
if path.split('.')[-1] == "csv" #from twitter archive
|
160
|
+
csv_archive = CSV.read(path, :headers=>:first_row)
|
161
|
+
tweets = csv_archive.map do |tweet|
|
162
|
+
{ text: tweet['text'], id: tweet['tweet_id'].to_i }
|
163
|
+
end
|
164
|
+
else
|
165
|
+
File.read(path).split("\n").each do |l|
|
166
|
+
if l.start_with?('# ')
|
167
|
+
id = l.split('# ')[-1]
|
168
|
+
else
|
169
|
+
tweet = { text: l }
|
170
|
+
if id
|
171
|
+
tweet[:id] = id
|
172
|
+
id = nil
|
173
|
+
end
|
174
|
+
tweets << tweet
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
File.open(new_path, 'w') do |f|
|
180
|
+
log "Writing #{tweets.length} tweets to #{new_path}"
|
181
|
+
f.write(JSON.pretty_generate(tweets))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
HELP.gen = <<-STR
|
188
|
+
Usage: ebooks gen <model_path> [input]
|
189
|
+
|
190
|
+
Make a test tweet from the processed model at <model_path>.
|
191
|
+
Will respond to input if provided.
|
192
|
+
STR
|
193
|
+
|
194
|
+
def self.gen(model_path, input)
|
195
|
+
if model_path.nil?
|
196
|
+
help :gen
|
197
|
+
exit 1
|
198
|
+
end
|
199
|
+
|
200
|
+
model = Ebooks::Model.load(model_path)
|
201
|
+
if input && !input.empty?
|
202
|
+
puts "@cmd " + model.make_response(input, 135)
|
203
|
+
else
|
204
|
+
puts model.make_statement
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
HELP.archive = <<-STR
|
209
|
+
Usage: ebooks archive <username> [outpath]
|
210
|
+
|
211
|
+
Downloads a json corpus of the <username>'s tweets.
|
212
|
+
Output defaults to corpus/<username>.json
|
213
|
+
Due to API limitations, this can only receive up to ~3000 tweets
|
214
|
+
into the past.
|
215
|
+
|
216
|
+
The first time you run archive, you will need to enter the auth
|
217
|
+
details of some account to use for accessing the API. This info
|
218
|
+
will then be stored in ~/.ebooksrc for later use, and can be
|
219
|
+
modified there if needed.
|
220
|
+
STR
|
221
|
+
|
222
|
+
def self.archive(username, outpath=nil)
|
223
|
+
if username.nil?
|
224
|
+
help :archive
|
225
|
+
exit 1
|
226
|
+
end
|
227
|
+
|
228
|
+
Ebooks::Archive.new(username, outpath).sync
|
229
|
+
end
|
230
|
+
|
231
|
+
HELP.sync = <<-STR
|
232
|
+
Usage: ebooks sync <botname> <username>
|
233
|
+
|
234
|
+
Copies and flips <username>'s avatar and cover photo, uploading them to <botname>'s profile.
|
235
|
+
|
236
|
+
Stores saved avatar's and covers in image/.
|
237
|
+
|
238
|
+
STR
|
239
|
+
|
240
|
+
def self.sync(botname, username)
|
241
|
+
if botname.nil?
|
242
|
+
help :sync
|
243
|
+
exit 1
|
244
|
+
end
|
245
|
+
|
246
|
+
load File.join(APP_PATH, 'bots.rb')
|
247
|
+
Ebooks::Sync::run(botname, username)
|
248
|
+
end
|
249
|
+
|
250
|
+
HELP.tweet = <<-STR
|
251
|
+
Usage: ebooks tweet <model_path> <botname>
|
252
|
+
|
253
|
+
Sends a public tweet from the specified bot using text
|
254
|
+
from the processed model at <model_path>.
|
255
|
+
STR
|
256
|
+
|
257
|
+
def self.tweet(modelpath, botname)
|
258
|
+
if modelpath.nil? || botname.nil?
|
259
|
+
help :tweet
|
260
|
+
exit 1
|
261
|
+
end
|
262
|
+
|
263
|
+
load File.join(APP_PATH, 'bots.rb')
|
264
|
+
model = Ebooks::Model.load(modelpath)
|
265
|
+
statement = model.make_statement
|
266
|
+
bot = Ebooks::Bot.get(botname)
|
267
|
+
if bot.nil?
|
268
|
+
log "No such bot configured in bots.rb: #{botname}"
|
269
|
+
exit 1
|
270
|
+
end
|
271
|
+
bot.configure
|
272
|
+
bot.tweet(statement)
|
273
|
+
end
|
274
|
+
|
275
|
+
HELP.auth = <<-STR
|
276
|
+
Usage: ebooks auth
|
277
|
+
|
278
|
+
Authenticates your Twitter app for any account. By default, will
|
279
|
+
use the consumer key and secret from the first defined bot. You
|
280
|
+
can specify another by setting the CONSUMER_KEY and CONSUMER_SECRET
|
281
|
+
environment variables.
|
282
|
+
STR
|
283
|
+
|
284
|
+
def self.auth
|
285
|
+
consumer_key, consumer_secret = find_consumer
|
286
|
+
require 'oauth'
|
287
|
+
|
288
|
+
consumer = OAuth::Consumer.new(
|
289
|
+
consumer_key,
|
290
|
+
consumer_secret,
|
291
|
+
site: 'https://twitter.com/',
|
292
|
+
scheme: :header
|
293
|
+
)
|
294
|
+
|
295
|
+
request_token = consumer.get_request_token
|
296
|
+
auth_url = request_token.authorize_url()
|
297
|
+
|
298
|
+
pin = nil
|
299
|
+
loop do
|
300
|
+
log auth_url
|
301
|
+
|
302
|
+
log "Go to the above url and follow the prompts, then enter the PIN code here."
|
303
|
+
print "> "
|
304
|
+
|
305
|
+
pin = STDIN.gets.chomp
|
306
|
+
|
307
|
+
break unless pin.empty?
|
308
|
+
end
|
309
|
+
|
310
|
+
access_token = request_token.get_access_token(oauth_verifier: pin)
|
311
|
+
|
312
|
+
log "Account authorized successfully. Make sure to put these in your bots.rb!\n" +
|
313
|
+
" bot.access_token = \"#{access_token.token}\"\n" +
|
314
|
+
" bot.access_token_secret = \"#{access_token.secret}\""
|
315
|
+
end
|
316
|
+
|
317
|
+
HELP.console = <<-STR
|
318
|
+
Usage: ebooks c[onsole]
|
319
|
+
|
320
|
+
Starts an interactive ruby session with your bots loaded
|
321
|
+
and configured.
|
322
|
+
STR
|
323
|
+
|
324
|
+
def self.console
|
325
|
+
load_bots
|
326
|
+
require 'pry'; Ebooks.module_exec { pry }
|
327
|
+
end
|
328
|
+
|
329
|
+
HELP.version = <<-STR
|
330
|
+
Usage: ebooks version
|
331
|
+
|
332
|
+
Shows you twitter_ebooks' version number.
|
333
|
+
STR
|
334
|
+
|
335
|
+
def self.version
|
336
|
+
require File.expand_path('../../lib/twitter_ebooks/version', __FILE__)
|
337
|
+
log Ebooks::VERSION
|
338
|
+
end
|
339
|
+
|
340
|
+
HELP.start = <<-STR
|
341
|
+
Usage: ebooks s[tart] [botname]
|
342
|
+
|
343
|
+
Starts running bots. If botname is provided, only runs that bot.
|
344
|
+
STR
|
345
|
+
|
346
|
+
def self.start(botname=nil)
|
347
|
+
load_bots
|
348
|
+
|
349
|
+
if botname.nil?
|
350
|
+
bots = Ebooks::Bot.all
|
351
|
+
else
|
352
|
+
bots = Ebooks::Bot.all.select { |bot| bot.username == botname }
|
353
|
+
if bots.empty?
|
354
|
+
log "Couldn't find a defined bot for @#{botname}!"
|
355
|
+
exit 1
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
threads = []
|
360
|
+
bots.each do |bot|
|
361
|
+
threads << Thread.new { bot.prepare }
|
362
|
+
end
|
363
|
+
threads.each(&:join)
|
364
|
+
|
365
|
+
threads = []
|
366
|
+
bots.each do |bot|
|
367
|
+
threads << Thread.new do
|
368
|
+
loop do
|
369
|
+
begin
|
370
|
+
bot.start
|
371
|
+
rescue Exception => e
|
372
|
+
bot.log e.inspect
|
373
|
+
puts e.backtrace.map { |s| "\t"+s }.join("\n")
|
374
|
+
end
|
375
|
+
bot.log "Sleeping before reconnect"
|
376
|
+
sleep 60
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
threads.each(&:join)
|
381
|
+
end
|
382
|
+
|
383
|
+
# Non-command methods
|
384
|
+
|
385
|
+
def self.find_consumer
|
386
|
+
if ENV['CONSUMER_KEY'] && ENV['CONSUMER_SECRET']
|
387
|
+
log "Using consumer details from environment variables:\n" +
|
388
|
+
" consumer key: #{ENV['CONSUMER_KEY']}\n" +
|
389
|
+
" consumer secret: #{ENV['CONSUMER_SECRET']}"
|
390
|
+
return [ENV['CONSUMER_KEY'], ENV['CONSUMER_SECRET']]
|
391
|
+
end
|
392
|
+
|
393
|
+
load_bots
|
394
|
+
consumer_key = nil
|
395
|
+
consumer_secret = nil
|
396
|
+
Ebooks::Bot.all.each do |bot|
|
397
|
+
if bot.consumer_key && bot.consumer_secret
|
398
|
+
consumer_key = bot.consumer_key
|
399
|
+
consumer_secret = bot.consumer_secret
|
400
|
+
log "Using consumer details from @#{bot.username}:\n" +
|
401
|
+
" consumer key: #{bot.consumer_key}\n" +
|
402
|
+
" consumer secret: #{bot.consumer_secret}\n"
|
403
|
+
return consumer_key, consumer_secret
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
if consumer_key.nil? || consumer_secret.nil?
|
408
|
+
log "Couldn't find any consumer details to auth an account with.\n" +
|
409
|
+
"Please either configure a bot with consumer_key and consumer_secret\n" +
|
410
|
+
"or provide the CONSUMER_KEY and CONSUMER_SECRET environment variables."
|
411
|
+
exit 1
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def self.load_bots
|
416
|
+
load 'bots.rb'
|
417
|
+
|
418
|
+
if Ebooks::Bot.all.empty?
|
419
|
+
puts "Couldn't find any bots! Please make sure bots.rb instantiates at least one bot."
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def self.command(args)
|
424
|
+
if args.length == 0
|
425
|
+
help
|
426
|
+
exit 1
|
427
|
+
end
|
428
|
+
|
429
|
+
case args[0]
|
430
|
+
when "new" then new(args[1])
|
431
|
+
when "consume" then consume(args[1..-1])
|
432
|
+
when "consume-all" then consume_all(args[1], args[2..-1])
|
433
|
+
when "append" then append(args[1],args[2])
|
434
|
+
when "gen" then gen(args[1], args[2..-1].join(' '))
|
435
|
+
when "archive" then archive(args[1], args[2])
|
436
|
+
when "sync" then sync(args[1], args[2])
|
437
|
+
when "tweet" then tweet(args[1], args[2])
|
438
|
+
when "jsonify" then jsonify(args[1..-1])
|
439
|
+
when "auth" then auth
|
440
|
+
when "console" then console
|
441
|
+
when "c" then console
|
442
|
+
when "start" then start(args[1])
|
443
|
+
when "s" then start(args[1])
|
444
|
+
when "help" then help(args[1])
|
445
|
+
when "version" then version
|
446
|
+
else
|
447
|
+
log "No such command '#{args[0]}'"
|
448
|
+
help
|
449
|
+
exit 1
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
Ebooks::CLI.command(ARGV)
|