├── .gitignore ├── Capfile ├── config ├── config.yml.example └── deploy.rb ├── public ├── fontawesome-webfont.woff └── font-awesome.min.css ├── VSPL-LICENSE.txt ├── Gemfile ├── oauth_generator.rb ├── keywords_blacklist.txt ├── views ├── tweets.erb └── layout.erb ├── Gemfile.lock ├── bot ├── keywords_whitelist.txt ├── retweeter.rb ├── server.rb ├── tweet_presenter.rb └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .DS_Store 3 | config/config.yml 4 | *.json 5 | stuff 6 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | load 'deploy' 2 | load 'config/deploy' # remove this line to skip loading any of the default tasks 3 | -------------------------------------------------------------------------------- /config/config.yml.example: -------------------------------------------------------------------------------- 1 | consumer_key: xxx 2 | consumer_secret: xxx 3 | oauth_token: xxx 4 | oauth_token_secret: xxx 5 | -------------------------------------------------------------------------------- /public/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mackuba/rails-retweeter-bot/HEAD/public/fontawesome-webfont.woff -------------------------------------------------------------------------------- /VSPL-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jakub Suder 2 | 3 | You can modify, distribute and use this software for any purpose without any 4 | restrictions as long as you keep this copyright notice intact. The software is 5 | provided without any warranty. 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # fix for "ArgumentError: invalid byte sequence in US-ASCII" during bundle install (?) 2 | Encoding.default_internal = Encoding.default_external = Encoding::UTF_8 3 | 4 | source "http://rubygems.org" 5 | 6 | # TODO: update to 5.x 7 | gem 'twitter', '~> 4.4' 8 | gem 'oauth' 9 | gem 'multi_json' 10 | 11 | group :development do 12 | gem 'capistrano', '~> 2.13' 13 | gem 'rvm-capistrano' 14 | gem 'sinatra' 15 | gem 'sinatra-reloader' 16 | end 17 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/capistrano' 2 | 3 | set :application, "rails_bot" 4 | set :repository, "git@github.com:mackuba/rails-retweeter-bot.git" 5 | set :scm, :git 6 | set :keep_releases, 5 7 | set :use_sudo, false 8 | set :deploy_to, "/var/www/rails_bot" 9 | set :deploy_via, :remote_cache 10 | 11 | server "zermatt", :app, :web, :db, :primary => true 12 | 13 | after 'deploy:update_code', 'deploy:symlink_config' 14 | 15 | namespace :deploy do 16 | task :symlink_config do 17 | run "ln -s #{shared_path}/config/config.yml #{release_path}/config/config.yml" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /oauth_generator.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'oauth' 3 | require 'yaml' 4 | 5 | config = YAML.load(File.read('config/config.yml')) 6 | 7 | oauth = OAuth::Consumer.new( 8 | config['consumer_key'], 9 | config['consumer_secret'], 10 | :site => 'https://twitter.com', 11 | :request_token_path => '/oauth/request_token', 12 | :access_token_path => '/oauth/access_token', 13 | :authorize_path => '/oauth/authorize' 14 | ) 15 | 16 | rt = oauth.get_request_token 17 | request_token = rt.token 18 | request_secret = rt.secret 19 | 20 | puts "Request token => #{request_token}" 21 | puts "Request secret => #{request_secret}" 22 | puts "Authentication URL => #{rt.authorize_url} [OPEN THIS]" 23 | 24 | print "Provide the PIN that Twitter gave you here: " 25 | pin = gets.chomp 26 | 27 | at = rt.get_access_token(oauth_verifier: pin) 28 | access_token = at.token 29 | access_secret = at.secret 30 | 31 | puts "Access token => #{at.token}" 32 | puts "Access secret => #{at.secret}" 33 | -------------------------------------------------------------------------------- /keywords_blacklist.txt: -------------------------------------------------------------------------------- 1 | abuse 2 | america\w* 3 | anarch\w+ 4 | apple 5 | arrest\w* 6 | asshole 7 | bank\w* 8 | capitalis\w+ 9 | censor\w* 10 | civil\w* 11 | coc 12 | communis\w+ 13 | conduct 14 | cops? 15 | countr(y|ies) 16 | crimes? 17 | DEA 18 | democra\w+ 19 | dick\w* 20 | drones? 21 | drugs? 22 | ecto 23 | elixir(lang)? 24 | employ\w* 25 | erlang 26 | fanatic\w* 27 | fascis\w+ 28 | fbi 29 | femini\w+ 30 | (de)?fraud\w* 31 | fuck\w* 32 | gay 33 | golang 34 | gov\w* 35 | harass\w* 36 | haskell 37 | hex 38 | humanit\w+ 39 | (de)?humani[sz]\w+ 40 | iOS 41 | iPhone 42 | IRS 43 | journ(alis[tm]|o)s? 44 | (in)?justic\w+ 45 | law\w* 46 | (il)?legal\w* 47 | liberal\w* 48 | libert\w+ 49 | (fe)?male 50 | masculin\w+ 51 | meritocr\w+ 52 | misog\w* 53 | nationali\w+ 54 | nazi\w* 55 | NSA 56 | obama 57 | officers? 58 | parent\w* 59 | patriarchy 60 | phoenix 61 | polic\w+ 62 | politic\w+ 63 | power 64 | priviliged? 65 | racis\w+ 66 | rape 67 | regime 68 | religi\w+ 69 | republic\w* 70 | rust 71 | scala 72 | senat\w* 73 | sex\w* 74 | soci\w+ 75 | suprem\w+ 76 | swift 77 | terror\w* 78 | testif\w+ 79 | testim\w+ 80 | tortur\w+ 81 | toxic\w* 82 | trump 83 | TSA 84 | uber 85 | united\sstates 86 | USA? 87 | vim 88 | violen\w+ 89 | war 90 | weapon\w* 91 | wom[ae]n 92 | xcode 93 | xenoph\w+ 94 | -------------------------------------------------------------------------------- /views/tweets.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if user_data %> 3 | Followers count: <%= user_data.followers_count %> | 4 | Tweets count: <%= user_data.statuses_count %> | 5 | Awesomeness threshold: <%= awesomeness_threshold(user_data) %> | 6 | <% end %> 7 | 8 | Matched tweets: <%= tweets.select(&:interesting?).length %>/<%= tweets.length %> | 9 | Sort by: time / score 10 |
11 | 12 |<%= highlight(t.expanded_text) %>
28 | 29 |30 | <%= t.activity_count %> reaction(s) = 31 | <%= t.retweet_count %> retweet(s) + 32 | <%= t.favorite_count %> favorite(s) / 33 | <%= t.created_at %> / 34 | link 35 | <% if t.retweeted? %> 36 | / 37 | <% end %> 38 |
39 |26 |
41 | 42 | 43 |12 | 13 | 14 | But not this: 15 | 16 |Thanks @steveklabnik for reminding me about this article. Every programmer should read it: kalzumeus.com/2010/06/17/fal…
— Aaron Patterson (@tenderlove) August 31, 2012
17 | 18 | (sorry, Aaron...) 19 | 20 | What I need is someone that would follow all these people, read all their tweets and retweet only what seems important. This bot is my attempt at creating such filter. 21 | 22 | ## How it works 23 | 24 | The basic idea was that the best tweets get retweeted a lot, so I made the bot select tweets with a high number of retweets. Adding favorites improved things further, because a lot of tweets get many favorites but not many retweets (especially some useful but not funny tweets from [@ruby_news](https://twitter.com/ruby_news) or [@rubyflow](https://twitter.com/rubyflow) - the funny ones get retweeted the most). I've ignored the tweets of people from outside the selected group retweeted by people in the group, because almost all of them were off topic. 25 | 26 | Now I had most of the interesting tweets marked to be retweeted, but most of the top tweets were still not relevant - funny tweets about random things, tweets about politics, current news, Apple, Microsoft, startups, religion, etc. So then I've added a keyword whitelist - I went through the top tweets and I've prepared a list of keywords that would only match the tweets I'd like to see retweeted. Later I've also added a blacklist in order to ignore tweets that matched some "good" words, but also included some "bad" words too (a blacklist match trumps a whitelist match). (Note: I'm using the words good/bad/whitelist/blacklist only to refer to being relevant or irrelevant to what I want to see the bot tweet about - I realize that a lot of blacklisted topics are important, I simply want this bot to focus only on purely technical content, since that's easiest to judge algorithmically.) 27 | 28 | I've also made the minimum number of retweets+favorites depend on the author - those with a high number of followers get much more retweets on average, so a post with 30 retweets by [@spastorino](https://twitter.com/spastorino) (3871 followers) will usually be more interesting than a post with 30 retweets by [@dhh](https://twitter.com/dhh) (72141 followers). 29 | 30 | The end result is that even though some good tweets are ignored and some off topic tweets get retweeted, the filter works surprisingly well in most cases. It should retweet about 4 tweets per day on average, which sounds like an acceptable number. 31 | 32 | ## How to use 33 | 34 | First, you need a Twitter account to tweet from. You'll want a fresh account only for that purpose. The bot selects the tweets to retweet from what it sees on its home timeline, so the list of people to be observed and retweeted is simply the bot account's "following" list. Log in as the bot's account and follow any people that you'd like to see retweeted. 35 | 36 | Next, clone the repository and run `bundle install`. Create a `config.yml` file based on the example file. Go to Twitter's developer site and register an application there (it doesn't technically have to be owned by the bot), then copy the first two keys to the config. Then use the `oauth_generator.rb` script to get the other two keys (here you need to sign in with the bot's account!). 37 | 38 | Then you can use any of these: 39 | 40 | ### ./bot retweet 41 | 42 | Loads 200 last tweets from the timeline (ignoring retweets) and retweets the ones that match the filter and weren't retweeted yet. 200 last tweets includes tweets from the last 2 days on average in @rails_bot's case (I think most tweets will get most of their retweets and favorites in the first 2 days anyway). 43 | 44 | ### ./bot fetch data.json 45 | 46 | Downloads last 3 months of tweets from each of the followed people and saves them as a JSON file. 47 | 48 | ### ./bot cached data.json 49 | 50 | Starts a web UI that you can use to tweak the filter and see which tweets get marked. Uses the JSON file created above. This is useful for tweaking whitelists, blacklists, followed user list etc. - you can easily test which tweets are matched and which aren't without having to redownload the data constantly. This is important because there are pretty strict usage limits in Twitter's API, and if you load too much within an hour, you will be blocked until the next hour. 51 | 52 | ### ./bot live 53 | 54 | Starts a web UI, downloading tweets on demand. This is useful for checking new profiles to see if it's worth adding them to the list. 55 | 56 | ## How to customize the bot 57 | 58 | To make the bot useful for you, apart from the followed user list you will probably need to tweak some of its parameters like keywords and threshold values to make it focus on the specific content you're interested in. 59 | 60 | ### Edit keywords that will not be retweeted in 'keywords_blacklist.txt' 61 | 62 | These are the words that you DO NOT want included in any of your retweets. If any of those words and expressions match, the tweet will not be retweeted regardless of what else is in it, who tweeted it or how popular it is. Use the original list as an example of how the patterns should look and prepare a list that will work for you. 63 | 64 | The file is basically a list of Ruby regular expression patterns, each on a separate line, without any opening and closing symbols. As you can see from the original list, it can include things like alternative or optional parts, wildcards etc. Also, a word break symbol is automatically added at the beginning and end of the pattern, so they only match full words, not parts of words. 65 | 66 | Two additional exceptions: a pattern in quotes (`"FOO"`) means that the pattern is case-sensitive, and a pattern in slashes (`/foo/`) means that it will be used as is without adding the word break symbols. 67 | 68 | ### Edit words that will be retweeted in 'keywords_whitelist.txt' 69 | 70 | The list of words in this text file are the words that you DO want included in your retweets. Matching a word from this list is a requirement, i.e. a tweet will only be considered at all if it matches something on this list. Technically the list is built in the same way as the blacklist. Again, use the example list from @rails_bot to prepare a list of words that are relevant for you and that you want included in your retweets. 71 | 72 | ### Change retweet threshold in 'tweet_presenter.rb' 73 | 74 | If you look in `tweet_presenter.rb` and find the `user_awesomeness_threshold` function, you can customize the threshold values that determine which retweets get retweeted. The way the threshold works is that if the number of retweets + the number of favorites on a given tweet is greater than or equal to a threshold (which depends on the number of followers its author has), then that tweet will be retweeted. 75 | 76 | The function currently uses a pretty strange expression that was simply created by trial and error to fit the specific user list that @rails_bot follows, but it might not work for other lists, so you might need to change not only the specific values, but also the whole function. E.g. you could use a simpler function like `user.followers_count / 1000 + 3` - then for a person with 6000 followers their tweets will need to have at least 9 retweets+favorites to be considered for retweeting (as long as they match the whitelist and don't match the blacklist), and a person with almost no followers will still need at least 3. Do some experiments and see what works for you - make sure that both popular and relatively unknown people don't get retweeted regardless of what they say or not retweeted at all. 77 | 78 | ## Credits 79 | 80 | Created by [Kuba Suder](http://mackuba.eu) ([@kuba_suder](https://twitter.com/kuba_suder)), licensed under VSPL ([Very Simple Public License](https://github.com/mackuba/rails-retweeter-bot/blob/master/VSPL-LICENSE.txt)). 81 | -------------------------------------------------------------------------------- /public/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 3.0.2 3 | * the iconic font designed for use with Twitter Bootstrap 4 | * ------------------------------------------------------- 5 | * The full suite of pictographic icons, examples, and documentation 6 | * can be found at: http://fortawesome.github.com/Font-Awesome/ 7 | * 8 | * License 9 | * ------------------------------------------------------- 10 | * - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL 11 | * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - 12 | * http://opensource.org/licenses/mit-license.html 13 | * - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ 14 | * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: 15 | * "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 16 | 17 | * Contact 18 | * ------------------------------------------------------- 19 | * Email: dave@davegandy.com 20 | * Twitter: http://twitter.com/fortaweso_me 21 | * Work: Lead Product Designer @ http://kyruus.com 22 | */ 23 | 24 | @font-face{ 25 | font-family: 'FontAwesome'; 26 | src: url('/fontawesome-webfont.woff?v=3.0.1') format('woff'); 27 | font-weight:normal; 28 | font-style:normal; 29 | } 30 | 31 | [class^="icon-"],[class*=" icon-"]{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0 0;background-repeat:repeat;margin-top:0}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:none}[class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none}a [class^="icon-"],a [class*=" icon-"]{display:inline-block}.icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em}.btn [class^="icon-"],.nav [class^="icon-"],.btn [class*=" icon-"],.nav [class*=" icon-"]{display:inline}.btn [class^="icon-"].icon-large,.nav [class^="icon-"].icon-large,.btn [class*=" icon-"].icon-large,.nav [class*=" icon-"].icon-large{line-height:.9em}.btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block}.nav-tabs [class^="icon-"],.nav-pills [class^="icon-"],.nav-tabs [class*=" icon-"],.nav-pills [class*=" icon-"],.nav-tabs [class^="icon-"].icon-large,.nav-pills [class^="icon-"].icon-large,.nav-tabs [class*=" icon-"].icon-large,.nav-pills [class*=" icon-"].icon-large{line-height:.9em}li [class^="icon-"],.nav li [class^="icon-"],li [class*=" icon-"],.nav li [class*=" icon-"]{display:inline-block;width:1.25em;text-align:center}li [class^="icon-"].icon-large,.nav li [class^="icon-"].icon-large,li [class*=" icon-"].icon-large,.nav li [class*=" icon-"].icon-large{width:1.5625em}ul.icons{list-style-type:none;text-indent:-0.75em}ul.icons li [class^="icon-"],ul.icons li [class*=" icon-"]{width:.75em}.icon-muted{color:#eee}.icon-border{border:solid 1px #eee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.icon-2x{font-size:2em}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.icon-3x{font-size:3em}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.icon-4x{font-size:4em}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.pull-right{float:right}.pull-left{float:left}[class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.3em}[class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.3em}.btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.18em}.btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{line-height:.8em}.btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.25em}.btn.btn-large [class^="icon-"],.btn.btn-large [class*=" icon-"]{margin-top:0}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.05em}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x{margin-right:.2em}.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-left:.2em}.icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}@-moz-document url-prefix(){.icon-spin{height:.9em}.btn .icon-spin{height:auto}.icon-spin.icon-large{height:1.25em}.btn .icon-spin.icon-large{height:.75em}}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-repeat:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-share-alt:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope-alt:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-undo:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-reply:before{content:"\f112"}.icon-github-alt:before{content:"\f113"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"} --------------------------------------------------------------------------------Just saw a poo that looked like a starfish.
— Aaron Patterson (@tenderlove) October 15, 2012