├── db └── .gitignore ├── log └── .gitignore ├── .ruby-version ├── .ruby_version ├── pid └── .gitignore ├── views ├── index.haml ├── clouds.haml ├── suggestion │ ├── _timeago.haml │ ├── _table_row.haml │ ├── _bubble_set.haml │ ├── _cluster_table_row.haml │ ├── _bubble.haml │ ├── hacker_mode.haml │ ├── _table_set.haml │ ├── index.haml │ └── _cluster_set.haml ├── clouds_svg.haml ├── links.haml ├── coffeescripts │ └── showbot.coffee ├── layout.haml └── ..tmp.css ├── config.ru ├── images ├── link.acorn └── vote_sprite.acorn ├── public ├── images │ ├── link.png │ ├── hr_gap.png │ ├── main_bg.png │ ├── close_quote.png │ ├── open_quote.png │ ├── vote_sprite.png │ └── expand_arrow_sprite.png ├── css │ ├── .svn │ │ ├── all-wcprops │ │ └── entries │ ├── text.css │ ├── reset.css │ ├── tipsy.css │ └── 960.css ├── js │ ├── word_cloud.js │ ├── jquery.timeago.js │ ├── modernizr.custom.js │ ├── jquery.tipsy.js │ └── d3.layout.cloud.js └── shows.json ├── Procfile ├── Procfile.local ├── .gitignore ├── console ├── sass └── bourbon │ ├── lib │ ├── bourbon │ │ ├── sass_extensions.rb │ │ └── sass_extensions │ │ │ ├── functions │ │ │ └── compact.rb │ │ │ └── functions.rb │ └── bourbon.rb │ ├── css3 │ ├── _appearance.scss │ ├── _border-image.scss │ ├── _box-sizing.scss │ ├── _inline-block.scss │ ├── _background-size.scss │ ├── _box-shadow.scss │ ├── _transform.scss │ ├── _radial-gradient.scss │ ├── _linear-gradient.scss │ ├── _columns.scss │ ├── _flex-box.scss │ ├── _border-radius.scss │ ├── _background-image.scss │ ├── _transition.scss │ └── _animation.scss │ ├── addons │ ├── _font-family.scss │ ├── _position.scss │ ├── _html5-input-types.scss │ ├── _timing-functions.scss │ └── _button.scss │ ├── functions │ ├── _tint-shade.scss │ ├── _grid-width.scss │ ├── _radial-gradient.scss │ ├── _linear-gradient.scss │ ├── _golden-ratio.scss │ └── _deprecated-webkit-gradient.scss │ └── _bourbon.scss ├── lib ├── models │ ├── show.rb │ ├── idftracker.rb │ ├── api_key.rb │ ├── vote.rb │ ├── cluster.rb │ ├── shows.rb │ ├── link.rb │ ├── ical_cache.rb │ ├── wordcount.rb │ └── suggestion.rb └── cinch │ └── plugins │ ├── suggestions.rb │ ├── admin.rb │ ├── links.rb │ ├── commands.rb │ ├── shoutcast.rb │ ├── schedule.rb │ ├── twitter.rb │ └── fivebyfun.rb ├── .powrc ├── config ├── schedule.rb └── backup.rb ├── showbot_irc.rb ├── scripts └── shoutcast_test.rb ├── license ├── Gemfile ├── environment.rb ├── cinchize.yml ├── Rakefile ├── test └── showbot_web_test.rb ├── locales └── en.yml ├── readme.md ├── Gemfile.lock └── showbot_web.rb /db/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-1.9.2-p320 2 | -------------------------------------------------------------------------------- /.ruby_version: -------------------------------------------------------------------------------- 1 | rvm 1.9.2-p320 2 | -------------------------------------------------------------------------------- /pid/.gitignore: -------------------------------------------------------------------------------- 1 | *.pid 2 | *.log 3 | -------------------------------------------------------------------------------- /views/index.haml: -------------------------------------------------------------------------------- 1 | = haml :'suggestion/index', locals: locals 2 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './showbot_web' 2 | 3 | run ShowbotWeb.new 4 | -------------------------------------------------------------------------------- /images/link.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/images/link.acorn -------------------------------------------------------------------------------- /public/images/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/link.png -------------------------------------------------------------------------------- /images/vote_sprite.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/images/vote_sprite.acorn -------------------------------------------------------------------------------- /public/images/hr_gap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/hr_gap.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec thin start -p $PORT -e production 2 | irc: ruby showbot_irc.rb network_live 3 | -------------------------------------------------------------------------------- /public/images/main_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/main_bg.png -------------------------------------------------------------------------------- /public/css/.svn/all-wcprops: -------------------------------------------------------------------------------- 1 | K 25 2 | svn:wc:ra_dav:version-url 3 | V 25 4 | /svn/!svn/ver/4/trunk/css 5 | END 6 | -------------------------------------------------------------------------------- /public/images/close_quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/close_quote.png -------------------------------------------------------------------------------- /public/images/open_quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/open_quote.png -------------------------------------------------------------------------------- /public/images/vote_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/vote_sprite.png -------------------------------------------------------------------------------- /Procfile.local: -------------------------------------------------------------------------------- 1 | web: bundle exec thin start -p $DEVELOPMENT_PORT -e development 2 | irc: ruby showbot_irc.rb network_test 3 | -------------------------------------------------------------------------------- /public/images/expand_arrow_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutewinter/Showbot/HEAD/public/images/expand_arrow_sprite.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /password.yml 2 | /readme.html 3 | /.sass-cache/ 4 | .DS_Store 5 | 6 | /development.db 7 | .sass-cache 8 | 9 | /tmp/ 10 | /.env -------------------------------------------------------------------------------- /console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'irb' 4 | require 'irb/completion' 5 | require 'irbtools/more' 6 | require 'environment' 7 | 8 | IRB.start 9 | -------------------------------------------------------------------------------- /views/clouds.haml: -------------------------------------------------------------------------------- 1 | -if cloud_data.empty? 2 | %p=t('views.clouds.cloud_data_empty') 3 | -else 4 | #cloud-data{data: {:'cloud-data' => cloud_json(cloud_data)}} 5 | -------------------------------------------------------------------------------- /sass/bourbon/lib/bourbon/sass_extensions.rb: -------------------------------------------------------------------------------- 1 | module Bourbon::SassExtensions 2 | end 3 | 4 | require "sass" 5 | 6 | require File.join(File.dirname(__FILE__), "/sass_extensions/functions") 7 | -------------------------------------------------------------------------------- /views/suggestion/_timeago.haml: -------------------------------------------------------------------------------- 1 | - time_string = datetime.strftime('%-d %b, %Y %-I:%M%P EST') 2 | %abbr.timeago{title: datetime, 'data-epoch-time' => datetime.strftime('%s')}= time_string 3 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_appearance.scss: -------------------------------------------------------------------------------- 1 | @mixin appearance ($value) { 2 | -webkit-appearance: $value; 3 | -moz-appearance: $value; 4 | -ms-appearance: $value; 5 | -o-appearance: $value; 6 | appearance: $value; 7 | } 8 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_border-image.scss: -------------------------------------------------------------------------------- 1 | @mixin border-image ($image) { 2 | -webkit-border-image: $image; 3 | -moz-border-image: $image; 4 | -ms-border-image: $image; 5 | -o-border-image: $image; 6 | border-image: $image; 7 | } 8 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_box-sizing.scss: -------------------------------------------------------------------------------- 1 | @mixin box-sizing ($box) { 2 | // content-box | border-box | inherit 3 | -webkit-box-sizing: $box; 4 | -moz-box-sizing: $box; 5 | -ms-box-sizing: $box; 6 | -o-box-sizing: $box; 7 | box-sizing: $box; 8 | } 9 | -------------------------------------------------------------------------------- /sass/bourbon/addons/_font-family.scss: -------------------------------------------------------------------------------- 1 | $georgia: Georgia, Cambria, "Times New Roman", Times, serif; 2 | $helvetica: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | $lucida-grande: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif; 4 | $verdana: Verdana, Geneva, sans-serif; 5 | -------------------------------------------------------------------------------- /lib/models/show.rb: -------------------------------------------------------------------------------- 1 | # A class to hold the data for a Show, woah 2 | 3 | class Show 4 | attr_reader :title, :url, :rss 5 | 6 | def initialize(json_hash) 7 | @title = json_hash["title"] 8 | @url = json_hash["url"] 9 | @rss = json_hash["rss"] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /views/suggestion/_table_row.haml: -------------------------------------------------------------------------------- 1 | %td.votes=link_and_vote_count(suggestion, request.ip) 2 | %td.title=h suggestion.title 3 | %td.user=h suggestion.user || t("views.suggestion._table_row.User") 4 | %td.time 5 | = haml :'suggestion/_timeago', :locals => {datetime: suggestion.created_at} 6 | -------------------------------------------------------------------------------- /sass/bourbon/functions/_tint-shade.scss: -------------------------------------------------------------------------------- 1 | // Add percentage of white to a color 2 | @function tint($color, $percent){ 3 | @return mix(white, $color, $percent); 4 | } 5 | 6 | // Add percentage of black to a color 7 | @function shade($color, $percent){ 8 | @return mix(black, $color, $percent); 9 | } 10 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_inline-block.scss: -------------------------------------------------------------------------------- 1 | // Legacy support for inline-block in IE7 (maybe IE6) 2 | @mixin inline-block { 3 | display: -moz-inline-box; 4 | -moz-box-orient: vertical; 5 | display: inline-block; 6 | vertical-align: baseline; 7 | zoom: 1; 8 | *display: inline; 9 | *vertical-align: auto; 10 | } 11 | -------------------------------------------------------------------------------- /.powrc: -------------------------------------------------------------------------------- 1 | if [ -f "$rvm_path/scripts/rvm" ] && [ -f ".ruby_version" ]; then 2 | source "$rvm_path/scripts/rvm" 3 | 4 | if [ -f ".rvmrc" ]; then 5 | source ".rvmrc" 6 | fi 7 | 8 | if [ -f ".ruby-version" ]; then 9 | rvm use `cat .ruby-version` 10 | fi 11 | 12 | if [ -f ".ruby-gemset" ]; then 13 | rvm gemset use --create `cat .ruby-gemset` 14 | fi 15 | fi 16 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | # schedule_production.rb 2 | # 3 | # Whenever gem's crontab schedule for Showbot in production 4 | 5 | set :environment, "production" 6 | 7 | set :output, '/var/log/showbot/crontab.log' 8 | 9 | # Fixed for Rack 10 | job_type :rake, "cd :path && RACK_ENV=:environment bundle exec rake :task --silent :output" 11 | 12 | every 1.day, :at => '3:00am' do 13 | rake 'backup:run' 14 | end 15 | -------------------------------------------------------------------------------- /sass/bourbon/lib/bourbon.rb: -------------------------------------------------------------------------------- 1 | module Bourbon 2 | if defined?(Rails) 3 | class Engine < ::Rails::Engine 4 | require 'bourbon/engine' 5 | end 6 | 7 | module Rails 8 | class Railtie < ::Rails::Railtie 9 | rake_tasks do 10 | load "tasks/install.rake" 11 | end 12 | end 13 | end 14 | end 15 | end 16 | 17 | require File.join(File.dirname(__FILE__), "/bourbon/sass_extensions") 18 | -------------------------------------------------------------------------------- /sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb: -------------------------------------------------------------------------------- 1 | # Compact function pulled from compass 2 | module Bourbon::SassExtensions::Functions::Compact 3 | 4 | def compact(*args) 5 | sep = :comma 6 | if args.size == 1 && args.first.is_a?(Sass::Script::List) 7 | args = args.first.value 8 | sep = args.first.separator 9 | end 10 | Sass::Script::List.new(args.reject{|a| !a.to_bool}, sep) 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /public/css/text.css: -------------------------------------------------------------------------------- 1 | body{font:13px/1.5 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif}pre,code{font-family:'DejaVu Sans Mono',Monaco,Consolas,monospace}hr{border:0 #ccc solid;border-top-width:1px;clear:both;height:0}h1{font-size:25px}h2{font-size:23px}h3{font-size:21px}h4{font-size:19px}h5{font-size:17px}h6{font-size:15px}ol{list-style:decimal}ul{list-style:disc}li{margin-left:30px}p,dl,hr,h1,h2,h3,h4,h5,h6,ol,ul,pre,table,address,fieldset,figure{margin-bottom:20px} -------------------------------------------------------------------------------- /sass/bourbon/lib/bourbon/sass_extensions/functions.rb: -------------------------------------------------------------------------------- 1 | module Bourbon::SassExtensions::Functions 2 | end 3 | 4 | require File.join(File.dirname(__FILE__), "/functions/compact") 5 | 6 | module Sass::Script::Functions 7 | include Bourbon::SassExtensions::Functions::Compact 8 | end 9 | 10 | # Wierd that this has to be re-included to pick up sub-modules. Ruby bug? 11 | class Sass::Script::Functions::EvaluationContext 12 | include Sass::Script::Functions 13 | end 14 | -------------------------------------------------------------------------------- /views/suggestion/_bubble_set.haml: -------------------------------------------------------------------------------- 1 | - suggestion_sets.each do |suggestion_set| 2 | .clear 3 | = suggestion_set_hr(suggestion_set) 4 | - suggestions = suggestion_set.suggestions 5 | %ol 6 | - col = 0 7 | - suggestions.each do |suggestion| 8 | %li.bubble.grid_4 9 | = haml :'suggestion/_bubble', :locals => {suggestion: suggestion} 10 | - if col >= 2 11 | .clear 12 | - col = 0 13 | - else 14 | - col += 1 15 | -------------------------------------------------------------------------------- /views/clouds_svg.haml: -------------------------------------------------------------------------------- 1 | -if cloud_index >= cloud_data.count 2 | %p=t(:out_of_bounds_begin, :scope => [:views, :clouds_svg], :cloud_index => cloud_index) + t(:cloud_data_count, :scope => [:views, :clouds_svgf], :count => cloud_data.count :cloud_data_count => cloud_data.count) + t(:out_of_bounds_end, :scope => [:views, :clouds_svg], :cloud_index => cloud_index) + t('views.clouds_svg.out_of_bounds_end') 3 | -else 4 | #cloud-data{data: {:'cloud-data' => cloud_json(cloud_data[cloud_index, 1])}} 5 | -------------------------------------------------------------------------------- /views/suggestion/_cluster_table_row.haml: -------------------------------------------------------------------------------- 1 | - if cluster_top 2 | %td.cluster-votes=suggestion.total_for_cluster 3 | - else 4 | %td 5 | %td.votes=link_and_vote_count(suggestion, request.ip) 6 | %td.title 7 | =h suggestion.title 8 | - if suggestion.top_of_cluster? and suggestion.cluster_id 9 | %span.cluster-arrow 10 | %td.user=h suggestion.user || t("views.suggestion._table_row.User") 11 | %td.time 12 | = haml :'suggestion/_timeago', :locals => {datetime: suggestion.created_at} 13 | -------------------------------------------------------------------------------- /lib/models/idftracker.rb: -------------------------------------------------------------------------------- 1 | # idftracker.rb 2 | # 3 | # Model that tracks the information used to calculate IDF 4 | 5 | require 'dm-core' 6 | require 'dm-validations' 7 | require 'dm-timestamps' 8 | require 'dm-aggregates' 9 | 10 | class IdfTracker 11 | include DataMapper::Resource 12 | 13 | property :id, Serial 14 | property :last_suggestion_time, DateTime 15 | property :last_suggestion_show, String 16 | property :document_count, Integer, :default => 0 17 | end 18 | -------------------------------------------------------------------------------- /sass/bourbon/functions/_grid-width.scss: -------------------------------------------------------------------------------- 1 | @function grid-width($n) { 2 | @return $n * $gw-column + ($n - 1) * $gw-gutter; 3 | } 4 | 5 | // The $gw-column and $gw-gutter variables must be defined in your base stylesheet to properly use the grid-width function. 6 | // 7 | // $gw-column: 100px; // Column Width 8 | // $gw-gutter: 40px; // Gutter Width 9 | // 10 | // div { 11 | // width: grid-width(4); // returns 520px; 12 | // margin-left: $gw-gutter; // returns 40px; 13 | // } 14 | -------------------------------------------------------------------------------- /views/suggestion/_bubble.haml: -------------------------------------------------------------------------------- 1 | .hover.show=h Shows.find_show_title(suggestion.show) if suggestion.show 2 | .qstart “ 3 | .title_container 4 | .votes_container 5 | .votes=link_and_vote_count(suggestion, request.ip) 6 | .subtitle=t(:subtitle, :scope => [:views, :suggestion, :_bubble], :count => suggestion.votes_counter) 7 | .title=h suggestion.title 8 | .qend ” 9 | = haml :'suggestion/_timeago', :locals => {datetime: suggestion.created_at} 10 | .arrow_user 11 | .user=h suggestion.user 12 | %span.arrow 13 | -------------------------------------------------------------------------------- /showbot_irc.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'environment') 2 | 3 | require 'optparse' 4 | require 'cinchize' 5 | 6 | 7 | # Required to parse the cinchize.yml file properly 8 | YAML::ENGINE.yamler = 'syck' 9 | 10 | Options = { 11 | :ontop => true, 12 | :system => false, 13 | :local_config => File.join(Dir.pwd, 'cinchize.yml'), 14 | :system_config => '/etc/cinchize.yml', 15 | :action => :start, 16 | } 17 | 18 | options = Options.dup 19 | 20 | daemon = Cinchize::Cinchize.new *Cinchize.config(options, ARGV.first) 21 | daemon.send options[:action] 22 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_background-size.scss: -------------------------------------------------------------------------------- 1 | @mixin background-size ($length-1, 2 | $length-2: false, $length-3: false, 3 | $length-4: false, $length-5: false, 4 | $length-6: false, $length-7: false, 5 | $length-8: false, $length-9: false) 6 | { 7 | $full: compact($length-1, $length-2, $length-3, $length-4, 8 | $length-5, $length-6, $length-7, $length-8, $length-9); 9 | 10 | -webkit-background-size: $full; 11 | -moz-background-size: $full; 12 | -ms-background-size: $full; 13 | -o-background-size: $full; 14 | background-size: $full; 15 | } 16 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_box-shadow.scss: -------------------------------------------------------------------------------- 1 | // Box-Shadow Mixin Requires Sass v3.1.1+ 2 | @mixin box-shadow ($shadow-1, 3 | $shadow-2: false, $shadow-3: false, 4 | $shadow-4: false, $shadow-5: false, 5 | $shadow-6: false, $shadow-7: false, 6 | $shadow-8: false, $shadow-9: false) 7 | { 8 | $full: compact($shadow-1, $shadow-2, $shadow-3, $shadow-4, 9 | $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9); 10 | 11 | -webkit-box-shadow: $full; 12 | -moz-box-shadow: $full; 13 | -ms-box-shadow: $full; 14 | -o-box-shadow: $full; 15 | box-shadow: $full; 16 | } 17 | -------------------------------------------------------------------------------- /sass/bourbon/functions/_radial-gradient.scss: -------------------------------------------------------------------------------- 1 | // This function is required and used by the background-image mixin. 2 | @function radial-gradient($pos, $shape-size, 3 | $G1, $G2, 4 | $G3: false, $G4: false, 5 | $G5: false, $G6: false, 6 | $G7: false, $G8: false, 7 | $G9: false, $G10: false) { 8 | 9 | $type: radial; 10 | $gradient: compact($pos, $shape-size, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 11 | $type-gradient: append($type, $gradient, comma); 12 | 13 | @return $type-gradient; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /views/suggestion/hacker_mode.haml: -------------------------------------------------------------------------------- 1 | =t('views.suggestion.hacker_mode.welcome') 2 | - all_suggestions = suggestion_sets.map{|s| s.suggestions}.flatten 3 | =t(:suggestions, :scope => [:views, :suggestion, :hacker_mode], :count => all_suggestions.count) 4 | ="\n" 5 | - suggestion_sets.each do |suggestion_set| 6 | = show_title_for_slug(suggestion_set.slug) 7 | - suggestions = suggestion_set.suggestions 8 | - suggestions.each do |suggestion| 9 | ="#{suggestion.votes_counter} vote#{suggestion.votes.count == 1 ? '' : 's'}, #{suggestion.title}, #{suggestion.user}, #{suggestion.created_at.strftime('%m/%d/%Y %l:%M:%S%p')}" 10 | ="\n" 11 | -------------------------------------------------------------------------------- /lib/models/api_key.rb: -------------------------------------------------------------------------------- 1 | # DataMapper model for API keys for Showbot 2 | 3 | require 'dm-core' 4 | require 'dm-validations' 5 | require 'dm-timestamps' 6 | 7 | require 'securerandom' 8 | 9 | class ApiKey 10 | include DataMapper::Resource 11 | 12 | property :id, Serial 13 | property :value, String, :length => 16 14 | property :app_name, String, :length => 100 15 | 16 | # Create a random key automatically when making a new ApiKey 17 | before :create do 18 | # Must truncate since urlsafe_base64 doesn't generate the exact length 19 | self.value = SecureRandom.urlsafe_base64(16)[0...16] 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/models/vote.rb: -------------------------------------------------------------------------------- 1 | # vote.rb 2 | # 3 | # Model that contains a single vote for a user attached to a suggestion. 4 | 5 | require 'dm-core' 6 | require 'dm-validations' 7 | require 'dm-types' 8 | require 'dm-is-counter_cacheable' 9 | 10 | 11 | class Vote 12 | 13 | include DataMapper::Resource 14 | is :counter_cacheable 15 | 16 | property :id, Serial 17 | property :user_ip, IPAddress, :required => true, :default => '0.0.0.0', index: :user_ip_and_suggestion_id 18 | property :suggestion_id, Integer, index: [:suggestion_id, :user_ip_and_suggestion_id] 19 | 20 | # Assocations 21 | belongs_to :suggestion 22 | counter_cacheable :suggestion 23 | 24 | end 25 | -------------------------------------------------------------------------------- /sass/bourbon/addons/_position.scss: -------------------------------------------------------------------------------- 1 | @mixin position ($position: relative, $coordinates: 0 0 0 0) { 2 | 3 | @if type-of($position) == list { 4 | $coordinates: $position; 5 | $position: relative; 6 | } 7 | 8 | $top: nth($coordinates, 1); 9 | $right: nth($coordinates, 2); 10 | $bottom: nth($coordinates, 3); 11 | $left: nth($coordinates, 4); 12 | 13 | position: $position; 14 | 15 | @if not(unitless($top)) { 16 | top: $top; 17 | } 18 | 19 | @if not(unitless($right)) { 20 | right: $right; 21 | } 22 | 23 | @if not(unitless($bottom)) { 24 | bottom: $bottom; 25 | } 26 | 27 | @if not(unitless($left)) { 28 | left: $left; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_transform.scss: -------------------------------------------------------------------------------- 1 | @mixin transform($property: none) { 2 | // none | 3 | -webkit-transform: $property; 4 | -moz-transform: $property; 5 | -ms-transform: $property; 6 | -o-transform: $property; 7 | transform: $property; 8 | } 9 | 10 | @mixin transform-origin($axes: 50%) { 11 | // x-axis - left | center | right | length | % 12 | // y-axis - top | center | bottom | length | % 13 | // z-axis - length 14 | -webkit-transform-origin: $axes; 15 | -moz-transform-origin: $axes; 16 | -ms-transform-origin: $axes; 17 | -o-transform-origin: $axes; 18 | transform-origin: $axes; 19 | } 20 | -------------------------------------------------------------------------------- /public/css/.svn/entries: -------------------------------------------------------------------------------- 1 | 10 2 | 3 | dir 4 | 4 5 | https://flexigrid.googlecode.com/svn/trunk/css 6 | https://flexigrid.googlecode.com/svn 7 | 8 | 9 | 10 | 2010-03-23T20:40:31.514417Z 11 | 4 12 | eric.caron 13 | 14 | 15 | svn:special svn:externals svn:needs-lock 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | a2bb0b7c-ce36-0410-bce9-01c9dc5a8ffe 28 | 29 | flexigrid 30 | dir 31 | 32 | 33 | 34 | delete 35 | 36 | flexigrid.css 37 | file 38 | 39 | 40 | 41 | add 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | copied 56 | https://flexigrid.googlecode.com/svn/trunk/css/flexigrid/flexigrid.css 57 | 4 58 | 59 | images 60 | dir 61 | 62 | -------------------------------------------------------------------------------- /scripts/shoutcast_test.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'net/http' 3 | 4 | SHOUTCAST_URI = URI("http://5by5.fm/") 5 | HEADERS = { 6 | "Icy-MetaData" => '1' 7 | } 8 | 9 | 10 | # Fetches the show title from the live stream defined by URI 11 | def fetch_show 12 | @last_update = Time.now 13 | 14 | http = Net::HTTP.new(SHOUTCAST_URI.host, SHOUTCAST_URI.port) 15 | 16 | 17 | chunk_count = 0 18 | chunk_limit = 20 # Limit chunks to prevent lockups 19 | http.get(SHOUTCAST_URI.path, HEADERS) do |chunk| 20 | chunk_count += 1 21 | if chunk =~ /StreamTitle='(.+?)';/ 22 | p chunk 23 | return $1 24 | break; 25 | elsif chunk_count > chunk_limit 26 | return nil 27 | end 28 | end 29 | end 30 | 31 | puts fetch_show 32 | -------------------------------------------------------------------------------- /views/suggestion/_table_set.haml: -------------------------------------------------------------------------------- 1 | - suggestion_sets.each do |suggestion_set| 2 | - suggestions = suggestion_set.suggestions 3 | .clear 4 | = suggestion_set_hr(suggestion_set) 5 | .suggestions_table 6 | .total="#{suggestions.count} Title#{suggestions.count == 1 ? '' : 's'}" 7 | %table.sortable.zebra-striped 8 | %thead 9 | %tr 10 | %th.votes.header= t('views.suggestion._table_set.Votes') 11 | %th.header= t('views.suggestion._table_set.Title') 12 | %th.header= t('views.suggestion._table_set.User') 13 | %th.header= t('views.suggestion._table_set.When') 14 | %tbody 15 | - suggestions.each do |suggestion| 16 | %tr 17 | = haml :'suggestion/_table_row', :locals => {suggestion: suggestion} 18 | -------------------------------------------------------------------------------- /public/css/reset.css: -------------------------------------------------------------------------------- 1 | a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,dialog,div,dl,dt,em,embed,fieldset,figcaption,figure,font,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,meter,nav,object,ol,output,p,pre,progress,q,rp,rt,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video,xmp{border:0;font-size:100%;margin:0;padding:0}html,body{height:100%}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}b,strong{font-weight:bold}img{color:transparent;font-size:0;vertical-align:middle;-ms-interpolation-mode:bicubic}li{display:list-item}table{border-collapse:collapse;border-spacing:0}th,td,caption{font-weight:normal;vertical-align:top;text-align:left}svg{overflow:hidden} -------------------------------------------------------------------------------- /sass/bourbon/functions/_linear-gradient.scss: -------------------------------------------------------------------------------- 1 | @function linear-gradient($pos: top, $G1: false, $G2: false, 2 | $G3: false, $G4: false, 3 | $G5: false, $G6: false, 4 | $G7: false, $G8: false, 5 | $G9: false, $G10: false) { 6 | 7 | // Detect what type of value exists in $pos 8 | $pos-type: type-of(nth($pos, 1)); 9 | 10 | // If $pos is missing from mixin, reassign vars and add default position 11 | @if ($pos-type == color) or (nth($pos, 1) == "transparent") { 12 | $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; 13 | $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; 14 | $pos: top; // Default position 15 | } 16 | 17 | $type: linear; 18 | $gradient: compact($pos, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 19 | $type-gradient: append($type, $gradient, comma); 20 | 21 | @return $type-gradient; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /sass/bourbon/_bourbon.scss: -------------------------------------------------------------------------------- 1 | // Custom Functions 2 | @import "functions/deprecated-webkit-gradient"; 3 | @import "functions/golden-ratio"; 4 | @import "functions/grid-width"; 5 | @import "functions/tint-shade"; 6 | @import "functions/linear-gradient"; 7 | @import "functions/radial-gradient"; 8 | 9 | // CSS3 Mixins 10 | @import "css3/animation"; 11 | @import "css3/appearance"; 12 | @import "css3/background-image"; 13 | @import "css3/background-size"; 14 | @import "css3/border-image"; 15 | @import "css3/border-radius"; 16 | @import "css3/box-shadow"; 17 | @import "css3/box-sizing"; 18 | @import "css3/columns"; 19 | @import "css3/flex-box"; 20 | @import "css3/inline-block"; 21 | @import "css3/linear-gradient"; 22 | @import "css3/radial-gradient"; 23 | @import "css3/transform"; 24 | @import "css3/transition"; 25 | 26 | // Addons & other mixins 27 | @import "addons/button"; 28 | @import "addons/font-family"; 29 | @import "addons/html5-input-types"; 30 | @import "addons/position"; 31 | @import "addons/timing-functions"; 32 | -------------------------------------------------------------------------------- /sass/bourbon/functions/_golden-ratio.scss: -------------------------------------------------------------------------------- 1 | @function golden-ratio($value, $increment) { 2 | @if $increment > 0 { 3 | @for $i from 1 through $increment { 4 | $value: ($value * 1.618); 5 | } 6 | } 7 | 8 | @if $increment < 0 { 9 | $increment: abs($increment); 10 | @for $i from 1 through $increment { 11 | $value: ($value / 1.618); 12 | } 13 | } 14 | 15 | @return $value; 16 | } 17 | 18 | // div { 19 | // Increment Up GR with positive value 20 | // font-size: golden-ratio(14px, 1); // returns: 22.652px 21 | // 22 | // Increment Down GR with negative value 23 | // font-size: golden-ratio(14px, -1); // returns: 8.653px 24 | // 25 | // Can be used with ceil(round up) or floor(round down) 26 | // font-size: floor( golden-ratio(14px, 1) ); // returns: 22px 27 | // font-size: ceil( golden-ratio(14px, 1) ); // returns: 23px 28 | // } 29 | // 30 | // modularscale.com 31 | // goldenratiocalculator.com 32 | -------------------------------------------------------------------------------- /lib/models/cluster.rb: -------------------------------------------------------------------------------- 1 | 2 | # cluster.rb 3 | # 4 | # Model that contains a set of similar title suggestions, determined by 5 | # string metrics. 6 | 7 | require 'open-uri' 8 | require 'json' 9 | 10 | require 'dm-core' 11 | require 'dm-validations' 12 | require 'dm-timestamps' 13 | require 'dm-aggregates' 14 | 15 | class Cluster 16 | include DataMapper::Resource 17 | 18 | property :id, Serial 19 | 20 | # Associations 21 | has n, :suggestions, :order => [:votes_counter.desc, :created_at.desc] 22 | has 1, :top_suggestion, :model => 'Suggestion', :order => [:votes_counter.desc, :created_at.asc] 23 | 24 | # ------------------ 25 | # Methods 26 | # ------------------ 27 | 28 | def total_votes 29 | self.suggestions.all.map(&:votes_counter).inject(0, :+) 30 | end 31 | 32 | def created_at 33 | self.top_suggestion.created_at 34 | end 35 | 36 | # ------------------ 37 | # Class Methods 38 | # ------------------ 39 | 40 | def self.recent(days_ago = 1) 41 | from = DateTime.now - days_ago 42 | all(:created_at.gt => from).all(:order => [:created_at.desc]) 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Jeremy Mack, Pile of Turtles, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/cinch/plugins/suggestions.rb: -------------------------------------------------------------------------------- 1 | # All suggestions welcome. 2 | 3 | module Cinch 4 | module Plugins 5 | class Suggestions 6 | include Cinch::Plugin 7 | 8 | match "help suggest", :method => :command_help # !help suggest 9 | match /(?:suggest|s) (.+)/i, :method => :command_suggest # !suggest Great Title Here 10 | 11 | 12 | # Show help for the suggestions module 13 | def command_help(m) 14 | m.user.send "Usage: !suggest Sweet Show Title" 15 | end 16 | 17 | # Add the user's suggestion to the database 18 | def command_suggest(m, title) 19 | if title.empty? 20 | command_help(m) 21 | else 22 | new_suggestion = Suggestion.create( 23 | :title => title, 24 | :user => m.user.nick 25 | ) 26 | 27 | if new_suggestion.saved? 28 | m.user.send "Added title suggestion \"#{new_suggestion.title}\"" 29 | else 30 | new_suggestion.errors.each do |e| 31 | m.user.send e.first 32 | end 33 | end 34 | end 35 | 36 | end 37 | 38 | end 39 | end 40 | end 41 | 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Regular 4 | gem 'tzinfo' 5 | gem 'tzinfo-data' 6 | gem 'i18n' 7 | gem 'rake' 8 | 9 | # Backups 10 | gem 'backup' 11 | gem 'fog', '~> 1.9.0' 12 | gem 'whenever', require: false 13 | gem 'net-ssh', ['>= 2.3.0', '<= 2.5.2'] 14 | gem 'excon', '~> 0.17.0' 15 | gem 'mail', '~> 2.5.0' 16 | 17 | # Web 18 | gem 'sinatra' 19 | gem 'sinatra-reloader' 20 | gem 'thin' 21 | gem 'haml' 22 | gem 'sass' 23 | gem 'coffee-script' 24 | gem 'execjs' 25 | gem 'therubyracer' 26 | 27 | # Showbot Specific 28 | gem 'cinch' 29 | gem 'cinchize' 30 | gem 'chronic' 31 | gem 'chronic_duration' 32 | gem 'ri_cal' 33 | gem 'twitter' 34 | gem 'cinch-identify' 35 | gem 'stopwords', '0.2' 36 | 37 | # Data Mapper 38 | gem 'data_mapper' 39 | gem 'dm-core' 40 | gem 'dm-timestamps' 41 | gem 'dm-validations' 42 | gem 'dm-aggregates' 43 | gem 'dm-migrations' 44 | gem 'dm-mysql-adapter' 45 | gem 'dm-types' 46 | gem 'dm-is-counter_cacheable' 47 | 48 | # Development Gems 49 | group :development do 50 | gem 'dm-sqlite-adapter' 51 | gem 'foreman' 52 | gem 'rb-fsevent' 53 | gem 'irbtools-more' 54 | end 55 | 56 | group :test do 57 | gem 'rack-test', require: false 58 | end 59 | 60 | 61 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_radial-gradient.scss: -------------------------------------------------------------------------------- 1 | // Requires Sass 3.1+ 2 | @mixin radial-gradient($pos, $shape-size, 3 | $G1, $G2, 4 | $G3: false, $G4: false, 5 | $G5: false, $G6: false, 6 | $G7: false, $G8: false, 7 | $G9: false, $G10: false) { 8 | 9 | $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 10 | 11 | background-color: nth($G1, 1); 12 | background-image: deprecated-webkit-gradient(radial, $full); // Safari <= 5.0 13 | background-image: -webkit-radial-gradient($pos, $shape-size, $full); 14 | background-image: -moz-radial-gradient($pos, $shape-size, $full); 15 | background-image: -ms-radial-gradient($pos, $shape-size, $full); 16 | background-image: -o-radial-gradient($pos, $shape-size, $full); 17 | background-image: unquote("radial-gradient(#{$pos}, #{$shape-size}, #{$full})"); 18 | } 19 | 20 | // Usage: Gradient position and shape-size are required. Color stops are optional. 21 | // @include radial-gradient(50% 50%, circle cover, #1e5799, #efefef); 22 | // @include radial-gradient(50% 50%, circle cover, #eee 10%, #1e5799 30%, #efefef); 23 | -------------------------------------------------------------------------------- /sass/bourbon/functions/_deprecated-webkit-gradient.scss: -------------------------------------------------------------------------------- 1 | // Render Deprecated Webkit Gradient - Linear || Radial 2 | //************************************************************************// 3 | @function deprecated-webkit-gradient($type, $full) { 4 | $gradient-list: (); 5 | $gradient: false; 6 | $full-length: length($full); 7 | $percentage: false; 8 | $gradient-type: $type; 9 | 10 | @for $i from 1 through $full-length { 11 | $gradient: nth($full, $i); 12 | 13 | @if length($gradient) == 2 { 14 | $color-stop: color-stop(nth($gradient, 2), nth($gradient, 1)); 15 | $gradient-list: join($gradient-list, $color-stop, comma); 16 | } 17 | @else { 18 | @if $i == $full-length { 19 | $percentage: 100%; 20 | } 21 | @else { 22 | $percentage: ($i - 1) * (100 / ($full-length - 1)) + "%"; 23 | } 24 | $color-stop: color-stop(unquote($percentage), $gradient); 25 | $gradient-list: join($gradient-list, $color-stop, comma); 26 | } 27 | } 28 | 29 | @if $type == radial { 30 | $gradient: -webkit-gradient(radial, center center, 0, center center, 460, $gradient-list); 31 | } 32 | @else if $type == linear { 33 | $gradient: -webkit-gradient(linear, left top, left bottom, $gradient-list); 34 | } 35 | @return $gradient; 36 | } 37 | -------------------------------------------------------------------------------- /views/links.haml: -------------------------------------------------------------------------------- 1 | -if @links.count > 0 2 | %h2.subtitle.grid_12=t(:links_count, :scope => [:views, :links], :count => @links.count) 3 | .irc_help=t('views.links.irc_help') 4 | %hr 5 | .clear 6 | %ol#links 7 | - col = 0 8 | - last_link = nil 9 | -@links.each do |link| 10 | - if (last_link and last_link.show != link.show) or last_link.nil? 11 | - col = 0 12 | .clear 13 | - if link.show.nil? 14 | %h2.show_break=t('views.links.unknown_show') 15 | - else 16 | %h2.show_break= Shows.find_show_title(link.show) 17 | .clear 18 | %li.link.grid_6 19 | .show=Shows.find_show_title(link.show) 20 | .link_box 21 | - if link.title 22 | .title=h link.title 23 | .uri_wrapper 24 | %a{:href => external_link(link.uri), :rel => 'nofollow', :class => link.title ? 'uri' : 'big uri'}=h truncate_string(link.uri.to_s, 100) 25 | .bottom 26 | %abbr.timeago{:title => link.created_at}= link.created_at.strftime("%-m/%-d/%Y at %-I:%M%P %Z") 27 | .user=h link.user 28 | - last_link = link 29 | - if col >= 1 30 | .clear 31 | - col = 0 32 | - else 33 | - col += 1 34 | .clear 35 | -else 36 | %h2.subtitle.grid_12=t('views.links.zero') 37 | .irc_help=t('views.links.irc_help') 38 | -------------------------------------------------------------------------------- /config/backup.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Backup 3 | # Generated Template 4 | # 5 | # For more information: 6 | # 7 | # View the Git repository at https://github.com/meskyanichi/backup 8 | # View the Wiki/Documentation at https://github.com/meskyanichi/backup/wiki 9 | # View the issue log at https://github.com/meskyanichi/backup/issues 10 | # 11 | # When you're finished configuring this configuration file, 12 | # you can run it from the command line by issuing the following command: 13 | # 14 | # $ backup perform -t my_backup [-c ] 15 | 16 | Backup::Model.new(:showbot_backup, 'Showbot Backup') do 17 | 18 | ## 19 | # MySQL [Database] 20 | # 21 | database MySQL do |db| 22 | db.name = ENV['BOT_DATABASE_NAME'] 23 | db.username = ENV['BOT_DATABASE_USER'] 24 | db.password = ENV['BOT_DATABASE_PASSWORD'] 25 | db.host = ENV['BOT_DATABASE_HOST'] 26 | db.port = ENV['BOT_DATABASE_PORT'] 27 | db.additional_options = ENV['BOT_DATABASE_OPTS'] 28 | end 29 | 30 | compress_with Gzip do |compression| 31 | compression.best = true 32 | end 33 | 34 | store_with S3 do |s3| 35 | s3.access_key_id = ENV['BOT_S3_ACCESS_KEY'] 36 | s3.secret_access_key = ENV['BOT_S3_SECRET_KEY'] 37 | s3.region = ENV['BOT_S3_REGION'] 38 | s3.bucket = ENV['BOT_S3_BUCKET'] 39 | s3.path = ENV['BOT_S3_PATH'] 40 | s3.keep = ENV['BOT_S3_KEEP'] 41 | end 42 | 43 | end 44 | 45 | -------------------------------------------------------------------------------- /sass/bourbon/addons/_html5-input-types.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Generate a variable ($all-text-inputs) with a list of all html5 3 | // input types that have a text-based input, excluding textarea. 4 | // http://diveintohtml5.org/forms.html 5 | //************************************************************************// 6 | $inputs-list: 'input[type="email"]', 7 | 'input[type="number"]', 8 | 'input[type="password"]', 9 | 'input[type="search"]', 10 | 'input[type="tel"]', 11 | 'input[type="text"]', 12 | 'input[type="url"]', 13 | 14 | // Webkit & Gecko may change the display of these in the future 15 | 'input[type="color"]', 16 | 'input[type="date"]', 17 | 'input[type="datetime"]', 18 | 'input[type="datetime-local"]', 19 | 'input[type="month"]', 20 | 'input[type="time"]', 21 | 'input[type="week"]'; 22 | 23 | $unquoted-inputs-list: (); 24 | 25 | @each $input-type in $inputs-list { 26 | $unquoted-inputs-list: append($unquoted-inputs-list, unquote($input-type), comma); 27 | } 28 | 29 | $all-text-inputs: $unquoted-inputs-list; 30 | 31 | // You must use interpolation on the variable: 32 | // #{$all-text-inputs} 33 | //************************************************************************// 34 | // #{$all-text-inputs}, textarea { 35 | // border: 1px solid red; 36 | // } 37 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_linear-gradient.scss: -------------------------------------------------------------------------------- 1 | @mixin linear-gradient($pos, $G1, $G2: false, 2 | $G3: false, $G4: false, 3 | $G5: false, $G6: false, 4 | $G7: false, $G8: false, 5 | $G9: false, $G10: false) { 6 | // Detect what type of value exists in $pos 7 | $pos-type: type-of(nth($pos, 1)); 8 | 9 | // If $pos is missing from mixin, reassign vars and add default position 10 | @if ($pos-type == color) or (nth($pos, 1) == "transparent") { 11 | $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; 12 | $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; 13 | $pos: top; // Default position 14 | } 15 | 16 | $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 17 | 18 | background-color: nth($G1, 1); 19 | background-image: deprecated-webkit-gradient(linear, $full); // Safari <= 5.0 20 | background-image: -webkit-linear-gradient($pos, $full); // Safari 5.1+, Chrome 21 | background-image: -moz-linear-gradient($pos, $full); 22 | background-image: -ms-linear-gradient($pos, $full); 23 | background-image: -o-linear-gradient($pos, $full); 24 | background-image: unquote("linear-gradient(#{$pos}, #{$full})"); 25 | } 26 | 27 | 28 | // Usage: Gradient position is optional, default is top. Position can be a degree. Color stops are optional as well. 29 | // @include linear-gradient(#1e5799, #2989d8); 30 | // @include linear-gradient(top, #1e5799 0%, #2989d8 50%); 31 | // @include linear-gradient(50deg, rgba(10, 10, 10, 0.5) 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%); 32 | -------------------------------------------------------------------------------- /lib/cinch/plugins/admin.rb: -------------------------------------------------------------------------------- 1 | # Admin commands for the bot 2 | 3 | module Cinch 4 | module Plugins 5 | class Admin 6 | include Cinch::Plugin 7 | 8 | timer 300, :method => :fix_name 9 | 10 | match %r{(?:exit|quit) (.+)}, :method => :command_exit 11 | 12 | def initialize(*args) 13 | super 14 | @admin_password = config[:bot_admin_password] 15 | 16 | if @admin_password.nil? or @admin_password.strip.empty? 17 | # Generate an admim key since one wasn't found 18 | @admin_password ||= (0...8).map{65.+(rand(25)).chr}.join 19 | puts "Admin key is #{@admin_password}" 20 | end 21 | end 22 | 23 | # Admin command that tells the bot to exit 24 | # !exit @admin_password 25 | def command_exit(m, password) 26 | if password == @admin_password 27 | m.user.send "#{shared[:Bot_Nick]} is shutting down. Good bye :(" 28 | 29 | Process.exit 30 | else 31 | puts "Wrong admin password (#{password}), should be #{@admin_password}" 32 | end 33 | end 34 | 35 | # Called every 5 minutes to attempt to fix the bots name. 36 | # This can happen if the bot gets disconnected and reconnects before 37 | # the last bot as been kicked from the IRC server. 38 | def fix_name 39 | if @bot.nick == "#{shared[:Bot_Nick]}" 40 | puts "Nick is fine, no change necessary." 41 | else 42 | puts "Fixing nickname." 43 | @bot.nick = "#{shared[:Bot_Nick]}" 44 | end 45 | end 46 | 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /environment.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'dm-core' 4 | require 'dm-timestamps' 5 | require 'dm-validations' 6 | require 'dm-aggregates' 7 | require 'dm-migrations' 8 | require 'haml' 9 | require 'sass' 10 | require 'i18n' 11 | 12 | require 'sinatra' unless defined?(Sinatra) 13 | 14 | LIVE_URL = ENV['DATA_JSON_URL'] 15 | 16 | configure do 17 | Dir[File.join(Dir.pwd, 'locales', '*.yml')].each {|file| I18n.load_path << file } 18 | # Make sure only available locales are used. This will be the default in the 19 | # future but we need this to silence a deprecation warning 20 | I18n.config.enforce_available_locales = true 21 | I18n.config.default_locale = ENV['SHOWBOT_LOCALE'] 22 | I18n.config.locale = I18n.config.default_locale 23 | 24 | # load models 25 | Dir.glob("#{File.dirname(__FILE__)}/lib/models/*.rb").sort.each { |lib| require lib } 26 | 27 | def t(*args) 28 | # Just a simple alias 29 | I18n.t(*args) 30 | end 31 | 32 | end 33 | 34 | configure(:production, :development) do 35 | DataMapper::Logger.new(STDOUT, :debug) if settings.development? 36 | 37 | current_directory = File.expand_path(File.dirname(__FILE__)) 38 | sqlite_file = File.join(current_directory, 'db', "#{Sinatra::Base.environment}.db") 39 | DataMapper.setup(:default, (ENV["SHOWBOT_DATABASE_URL"] || 40 | "sqlite3:///#{sqlite_file}")) 41 | DataMapper.finalize 42 | end 43 | 44 | configure :test do 45 | puts 'Test configuration in use' 46 | DataMapper.setup(:default, "sqlite3::memory:") 47 | DataMapper.auto_migrate! 48 | DataMapper.finalize 49 | end 50 | -------------------------------------------------------------------------------- /views/suggestion/index.haml: -------------------------------------------------------------------------------- 1 | - suggestions = suggestion_sets.map{|s| s.suggestions}.flatten 2 | -if suggestions.count > 0 3 | %h2.subtitle=t(:subtitle, :scope => [:views, :suggestion, :index], :count => suggestions.count) 4 | .irc_help=t('views.suggestion.index.irc_help') 5 | 6 | %hr 7 | 8 | .view_mode.grid_12 9 | %ul.segmented_controls 10 | %li#bubble{:class => view_mode == 'bubbles' ? 'selected' : ''} 11 | %a{href: '/titles?view_mode=bubbles'}=t('views.suggestion.index.view_mode.Bubbles') 12 | %li#table{:class => view_mode == 'tables' ? 'selected' : ''} 13 | %a{href: '/titles?view_mode=tables'}=t('views.suggestion.index.view_mode.Tables') 14 | %li#clusters{:class => view_mode == 'clusters' ? 'selected' : ''} 15 | %a{href: '/titles?view_mode=clusters'}=t('views.suggestion.index.view_mode.Clusters') 16 | %li#hacker{:class => view_mode == 'hacker' ? 'selected' : ''} 17 | %a{href: '/titles?view_mode=hacker'}=t('views.suggestion.index.view_mode.Hacker') 18 | 19 | .clear 20 | #titles 21 | - if view_mode == 'bubbles' 22 | = haml :'suggestion/_bubble_set', :locals => {suggestion_sets: suggestion_sets} 23 | - elsif view_mode == 'tables' 24 | = haml :'suggestion/_table_set', :locals => {suggestion_sets: suggestion_sets} 25 | - elsif view_mode == 'clusters' 26 | = haml :'suggestion/_cluster_set', :locals => {suggestion_sets: suggestion_sets} 27 | - else 28 | = haml :'suggestion/_table_set', :locals => {suggestion_sets: suggestion_sets} 29 | .clear 30 | -else 31 | %h2.subtitle.grid_12=t(:subtitle, :scope => [:views, :suggestion, :index], :count => suggestions.count) 32 | .irc_help=t('views.suggestion.index.irc_help') 33 | -------------------------------------------------------------------------------- /views/suggestion/_cluster_set.haml: -------------------------------------------------------------------------------- 1 | - suggestion_sets.each do |suggestion_set| 2 | - suggestions = suggestion_set.suggestions 3 | .clear 4 | = suggestion_set_hr(suggestion_set) 5 | .suggestions_table 6 | - group_count = suggestions.map { |s| s.cluster_id }.uniq.reject { |s| s == nil }.count + suggestions.count { |s| s.cluster_id == nil } 7 | .total=t(:titles, :scope => [:views, :suggestion, :_cluster_set], :count => suggestions.count) + t(:groups, :scope => [:views, :suggestion, :_cluster_set], :count => group_count) 8 | %table.sortable 9 | %thead 10 | %tr 11 | %th.votes.header{:colspan => 2}=t('views.suggestion._table_set.Votes') 12 | %th.header= t('views.suggestion._table_set.Title') 13 | %th.header= t('views.suggestion._table_set.User') 14 | %th.header= t('views.suggestion._table_set.When') 15 | %tbody 16 | - suggestions.each do |suggestion| 17 | - if suggestion.in_cluster? 18 | - if suggestion.top_of_cluster? 19 | %tr{:class => "cluster-top", :id => "cluster-#{suggestion.cluster_id}"} 20 | = haml :'suggestion/_cluster_table_row', :locals => {suggestion: suggestion, cluster_top: true} 21 | - suggestion.cluster.suggestions.each do |child_suggestion| 22 | - if child_suggestion.id != suggestion.id 23 | %tr{:class => "expand-child child-cluster-#{child_suggestion.cluster_id}"} 24 | = haml :'suggestion/_cluster_table_row', :locals => {suggestion: child_suggestion, cluster_top: false} 25 | - else 26 | %tr 27 | = haml :'suggestion/_cluster_table_row', :locals => {suggestion: suggestion, cluster_top: true} 28 | -------------------------------------------------------------------------------- /sass/bourbon/addons/_timing-functions.scss: -------------------------------------------------------------------------------- 1 | // CSS cubic-bezier timing functions. Timing functions courtesy of jquery.easie (github.com/jaukia/easie) 2 | // Timing functions are the same as demo'ed here: http://jqueryui.com/demos/effect/easing.html 3 | 4 | // EASE IN 5 | $ease-in-quad: cubic-bezier(0.550, 0.085, 0.680, 0.530); 6 | $ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190); 7 | $ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220); 8 | $ease-in-quint: cubic-bezier(0.755, 0.050, 0.855, 0.060); 9 | $ease-in-sine: cubic-bezier(0.470, 0.000, 0.745, 0.715); 10 | $ease-in-expo: cubic-bezier(0.950, 0.050, 0.795, 0.035); 11 | $ease-in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335); 12 | $ease-in-back: cubic-bezier(0.600, -0.280, 0.735, 0.045); 13 | 14 | // EASE OUT 15 | $ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940); 16 | $ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1.000); 17 | $ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000); 18 | $ease-out-quint: cubic-bezier(0.230, 1.000, 0.320, 1.000); 19 | $ease-out-sine: cubic-bezier(0.390, 0.575, 0.565, 1.000); 20 | $ease-out-expo: cubic-bezier(0.190, 1.000, 0.220, 1.000); 21 | $ease-out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000); 22 | $ease-out-back: cubic-bezier(0.175, 0.885, 0.320, 1.275); 23 | 24 | // EASE IN OUT 25 | $ease-in-out-quad: cubic-bezier(0.455, 0.030, 0.515, 0.955); 26 | $ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1.000); 27 | $ease-in-out-quart: cubic-bezier(0.770, 0.000, 0.175, 1.000); 28 | $ease-in-out-quint: cubic-bezier(0.860, 0.000, 0.070, 1.000); 29 | $ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950); 30 | $ease-in-out-expo: cubic-bezier(1.000, 0.000, 0.000, 1.000); 31 | $ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860); 32 | $ease-in-out-back: cubic-bezier(0.680, -0.550, 0.265, 1.550); 33 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_columns.scss: -------------------------------------------------------------------------------- 1 | @mixin columns($arg: auto) { 2 | // || 3 | -webkit-columns: $arg; 4 | -moz-columns: $arg; 5 | columns: $arg; 6 | } 7 | 8 | @mixin column-count($int: auto) { 9 | // auto || integer 10 | -webkit-column-count: $int; 11 | -moz-column-count: $int; 12 | column-count: $int; 13 | } 14 | 15 | @mixin column-gap($length: normal) { 16 | // normal || length 17 | -webkit-column-gap: $length; 18 | -moz-column-gap: $length; 19 | column-gap: $length; 20 | } 21 | 22 | @mixin column-fill($arg: auto) { 23 | // auto || length 24 | -webkit-columns-fill: $arg; 25 | -moz-columns-fill: $arg; 26 | columns-fill: $arg; 27 | } 28 | 29 | @mixin column-rule($arg) { 30 | // || || 31 | -webkit-column-rule: $arg; 32 | -moz-column-rule: $arg; 33 | column-rule: $arg; 34 | } 35 | 36 | @mixin column-rule-color($color) { 37 | -webkit-column-rule-color: $color; 38 | -moz-column-rule-color: $color; 39 | column-rule-color: $color; 40 | } 41 | 42 | @mixin column-rule-style($style: none) { 43 | // none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid 44 | -webkit-column-rule-style: $style; 45 | -moz-column-rule-style: $style; 46 | column-rule-style: $style; 47 | } 48 | 49 | @mixin column-rule-width ($width: none) { 50 | -webkit-column-rule-width: $width; 51 | -moz-column-rule-width: $width; 52 | column-rule-width: $width; 53 | } 54 | 55 | @mixin column-span($arg: none) { 56 | // none || all 57 | -webkit-column-span: $arg; 58 | -moz-column-span: $arg; 59 | column-span: $arg; 60 | } 61 | 62 | @mixin column-width($length: auto) { 63 | // auto || length 64 | -webkit-column-width: $length; 65 | -moz-column-width: $length; 66 | column-width: $length; 67 | } 68 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_flex-box.scss: -------------------------------------------------------------------------------- 1 | // CSS3 Flexible Box Model and property defaults 2 | 3 | // Custom shorthand notation for flexbox 4 | @mixin box($orient: inline-axis, $pack: start, $align: stretch) { 5 | @include display-box; 6 | @include box-orient($orient); 7 | @include box-pack($pack); 8 | @include box-align($align); 9 | } 10 | 11 | @mixin display-box { 12 | display: -webkit-box; 13 | display: -moz-box; 14 | display: box; 15 | } 16 | 17 | @mixin box-orient($orient: inline-axis) { 18 | // horizontal|vertical|inline-axis|block-axis|inherit 19 | -webkit-box-orient: $orient; 20 | -moz-box-orient: $orient; 21 | box-orient: $orient; 22 | } 23 | 24 | @mixin box-pack($pack: start) { 25 | // start|end|center|justify 26 | -webkit-box-pack: $pack; 27 | -moz-box-pack: $pack; 28 | box-pack: $pack; 29 | } 30 | 31 | @mixin box-align($align: stretch) { 32 | // start|end|center|baseline|stretch 33 | -webkit-box-align: $align; 34 | -moz-box-align: $align; 35 | box-align: $align; 36 | } 37 | 38 | @mixin box-direction($direction: normal) { 39 | // normal|reverse|inherit 40 | -webkit-box-direction: $direction; 41 | -moz-box-direction: $direction; 42 | box-direction: $direction; 43 | } 44 | @mixin box-lines($lines: single) { 45 | // single|multiple 46 | -webkit-box-lines: $lines; 47 | -moz-box-lines: $lines; 48 | box-lines: $lines; 49 | } 50 | 51 | @mixin box-ordinal-group($integer: 1) { 52 | -webkit-box-ordinal-group: $integer; 53 | -moz-box-ordinal-group: $integer; 54 | box-ordinal-group: $integer; 55 | } 56 | 57 | @mixin box-flex($value: 0.0) { 58 | -webkit-box-flex: $value; 59 | -moz-box-flex: $value; 60 | box-flex: $value; 61 | } 62 | 63 | @mixin box-flex-group($integer: 1) { 64 | -webkit-box-flex-group: $integer; 65 | -moz-box-flex-group: $integer; 66 | box-flex-group: $integer; 67 | } 68 | -------------------------------------------------------------------------------- /lib/cinch/plugins/links.rb: -------------------------------------------------------------------------------- 1 | # links.rb 2 | # 3 | # Cinch plugin to gather links from IRC and put them in a database 4 | # 5 | # Gotta link 'em all 6 | 7 | require 'addressable/uri' 8 | 9 | module Cinch 10 | module Plugins 11 | class Links 12 | include Cinch::Plugin 13 | 14 | match /(?:link|l) (.+)/i, :method => :command_link # !link http://audacious_thunderbolt.org/islate 15 | match "links", :method => :command_links # !links Show where the user can go to see links 16 | match "help link", :method => :command_help # !help link 17 | 18 | 19 | # Show help for the suggestions module 20 | def command_help(m) 21 | m.user.send "Suggest a relevant link for the current show." 22 | m.user.send " Usage: !link http://audacious_thunderbolt.org/islate" 23 | end 24 | 25 | # Add the user's link to the database 26 | def command_link(m, uri_string) 27 | if uri_string.empty? 28 | command_help(m) 29 | else 30 | # Verify this is a valid URI 31 | uri = Addressable::URI::parse(uri_string) 32 | 33 | if uri.scheme.nil? 34 | # No scheme for URI, parse it again with http in front 35 | uri = Addressable::URI.parse("http://#{uri.to_s}") 36 | end 37 | 38 | new_link = Link.create( 39 | :uri => uri, 40 | :user => m.user.nick 41 | ) 42 | 43 | if new_link.saved? 44 | m.user.send "Added link suggestion #{new_link.uri}" 45 | else 46 | new_link.errors.each do |e| 47 | m.user.send e.first 48 | end 49 | end 50 | end 51 | 52 | end 53 | 54 | # Tell them where to find the lovely links 55 | def command_links(m) 56 | m.user.send "Go to #{shared[:Sinatra_Url]}" + "/links to see the link suggestions." 57 | end 58 | 59 | end 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_border-radius.scss: -------------------------------------------------------------------------------- 1 | @mixin border-radius ($radii) { 2 | -webkit-border-radius: $radii; 3 | -moz-border-radius: $radii; 4 | -ms-border-radius: $radii; 5 | -o-border-radius: $radii; 6 | border-radius: $radii; 7 | } 8 | 9 | @mixin border-top-left-radius($radii) { 10 | -webkit-border-top-left-radius: $radii; 11 | -moz-border-top-left-radius: $radii; 12 | -ms-border-top-left-radius: $radii; 13 | -o-border-top-left-radius: $radii; 14 | border-top-left-radius: $radii; 15 | } 16 | 17 | @mixin border-top-right-radius($radii) { 18 | -webkit-border-top-right-radius: $radii; 19 | -moz-border-top-right-radius: $radii; 20 | -ms-border-top-right-radius: $radii; 21 | -o-border-top-right-radius: $radii; 22 | border-top-right-radius: $radii; 23 | } 24 | 25 | @mixin border-bottom-left-radius($radii) { 26 | -webkit-border-bottom-left-radius: $radii; 27 | -moz-border-bottom-left-radius: $radii; 28 | -ms-border-bottom-left-radius: $radii; 29 | -o-border-bottom-left-radius: $radii; 30 | border-bottom-left-radius: $radii; 31 | } 32 | 33 | @mixin border-bottom-right-radius($radii) { 34 | -webkit-border-bottom-right-radius: $radii; 35 | -moz-border-bottom-right-radius: $radii; 36 | -ms-border-bottom-right-radius: $radii; 37 | -o-border-bottom-right-radius: $radii; 38 | border-bottom-right-radius: $radii; 39 | } 40 | 41 | @mixin border-top-radius($radii) { 42 | @include border-top-left-radius($radii); 43 | @include border-top-right-radius($radii); 44 | } 45 | 46 | @mixin border-right-radius($radii) { 47 | @include border-top-right-radius($radii); 48 | @include border-bottom-right-radius($radii); 49 | } 50 | 51 | @mixin border-bottom-radius($radii) { 52 | @include border-bottom-left-radius($radii); 53 | @include border-bottom-right-radius($radii); 54 | } 55 | 56 | @mixin border-left-radius($radii) { 57 | @include border-top-left-radius($radii); 58 | @include border-bottom-left-radius($radii); 59 | } 60 | -------------------------------------------------------------------------------- /cinchize.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Plugins used for all configs 3 | plugins: &plugins 4 | plugins: 5 | - 6 | class: "Cinch::Plugins::Admin" 7 | options: 8 | :bot_admin_password: "test" 9 | - 10 | class: "Cinch::Plugins::Commands" 11 | - 12 | class: "Cinch::Plugins::FiveByFun" 13 | - 14 | class: "Cinch::Plugins::Identify" 15 | options: 16 | :password: "test" # Enter your password here 17 | :type: :nickserv # Most servers use nickserv, so setting this as the sane default. 18 | - 19 | class: "Cinch::Plugins::Links" 20 | - 21 | class: "Cinch::Plugins::Schedule" 22 | options: 23 | :ical_uri: "http://www.example.com/basic.ics" 24 | - 25 | class: "Cinch::Plugins::Suggestions" 26 | - 27 | class: "Cinch::Plugins::Shoutcast" 28 | options: 29 | :shoutcast_uri: "http://www.example.com/" 30 | - 31 | class: "Cinch::Plugins::Twitter" 32 | options: 33 | :channel: "#test" 34 | :twitter_user: "test" 35 | :twitter_consumer_key: "test" 36 | :twitter_consumer_secret: "test" 37 | :twitter_access_token: "test" 38 | :twitter_access_token_secret: "test" 39 | 40 | # The base configuration, Freenode in the example configuration. 41 | network_base: &network_base 42 | <<: *plugins 43 | server: irc.freenode.net 44 | port: 6667 45 | 46 | options: 47 | log_output: true 48 | dir_mode: normal 49 | dir: "pid" 50 | servers: 51 | network_test: 52 | <<: *network_base 53 | nick: botname_test 54 | channels: 55 | - "#cinch-bots" 56 | shared: 57 | Bot_Nick: "botname" 58 | Backup_Bot_Nick: "botname-backup" 59 | Live_Url: "example.com/live" 60 | Sinatra_Url: "http://www.example.com" 61 | Twitter_User: "ExampleUser" 62 | network_live: 63 | <<: *network_base 64 | nick: botname 65 | channels: 66 | - "#cinch-bots" 67 | shared: 68 | Bot_Nick: "botname" 69 | Backup_Bot_Nick: "botname-backup" 70 | Live_Url: "example.com/live" 71 | Sinatra_Url: "http://www.example.com" 72 | Twitter_User: "ExampleUser" -------------------------------------------------------------------------------- /lib/models/shows.rb: -------------------------------------------------------------------------------- 1 | # Class to hold all of the shows and some sweet helper methods 2 | 3 | require 'json' 4 | 5 | SHOWS_JSON = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "public", "shows.json")) 6 | 7 | require File.expand_path(File.join(File.dirname(__FILE__), "show")) 8 | 9 | class Shows 10 | 11 | # The array of shows loaded from the SHOWS_JSON file 12 | def self.shows 13 | if not defined? @@shows_array 14 | # Define the static @@shows_array variable if it doesn't exist 15 | @@shows_array = [] 16 | show_hashes = JSON.parse(File.open(SHOWS_JSON).read)["shows"] 17 | show_hashes.each do |show_hash| 18 | @@shows_array.push Show.new(show_hash) 19 | end 20 | end 21 | 22 | @@shows_array 23 | end 24 | 25 | # Find a show by keyword (slug or part of the title) 26 | def self.find_show(keyword) 27 | if keyword 28 | self.shows.each do |show| 29 | if show.url.downcase == keyword.downcase 30 | return show 31 | elsif show.title.downcase.include? keyword.downcase 32 | return show 33 | end 34 | end 35 | end 36 | 37 | return nil # No show found, return nil 38 | end 39 | 40 | # Find a show title by keyword, or returns the keyword if the show doesn't exist 41 | def self.find_show_title(keyword) 42 | show = self.find_show(keyword) 43 | if show 44 | return show.title 45 | else 46 | return keyword 47 | end 48 | end 49 | 50 | # Get the live show slug 51 | def self.fetch_live_show_slug 52 | slug = nil 53 | 54 | begin 55 | live_hash = JSON.parse(open(LIVE_URL).read) 56 | if live_hash and live_hash.has_key?("live") and live_hash["live"] 57 | # Show is live, read show name 58 | broadcast = live_hash["broadcast"] if live_hash.has_key? "broadcast" 59 | slug = broadcast["slug"] if broadcast.has_key? "slug" 60 | end 61 | rescue OpenURI::HTTPError 62 | puts "Error: #{LIVE_URL} looks to be down." 63 | end 64 | 65 | return slug 66 | end 67 | 68 | # Returns the show object for the live show 69 | def self.fetch_live_show 70 | self.find_show(fetch_live_show_slug) 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/cinch/plugins/commands.rb: -------------------------------------------------------------------------------- 1 | # Commands that didn't belong anywhere else 2 | 3 | require 'chronic_duration' 4 | 5 | module Cinch 6 | module Plugins 7 | class Commands 8 | include Cinch::Plugin 9 | 10 | match %r{(help|commands)$}, :method => :command_help # !help 11 | match %r{(about|showbot)}, :method => :command_about # !about 12 | match "uptime", :method => :command_uptime # !uptime 13 | 14 | def initialize(*args) 15 | super 16 | @start_time = Time.now 17 | end 18 | 19 | # Show help for the suggestions module 20 | def command_help(m) 21 | m.user.send [ 22 | "!next - When's the next live show?", 23 | "!schedule - What shows are being recorded live in the next seven days?", 24 | "!suggest - Be heard. Suggest a title for the live show.", 25 | "!link - Know the link for that? Suggest it and make the show better.", 26 | "!current - What's playing on #{shared[:Live_Url]}? I've got you covered.", 27 | "!last_status - The last tweet by @#{shared[:Twitter_User].join(", @")} delievered to you in IRC. Sweet.", 28 | "!about - Was #{shared[:Bot_Nick]} coded or did it spontaniously come into existence?", 29 | "!help - Uh, this.", 30 | ].join("\n") 31 | end 32 | 33 | # Show information about showbot 34 | def command_about(m) 35 | m.user.send "Showbot was created by Jeremy Mack (@mutewinter) and some awesome contributors on github. The project page is located at https://github.com/mutewinter/Showbot" 36 | m.user.send "Type !help for a list of showbot's commands" 37 | end 38 | 39 | # Tell them where to find the lovely suggestions 40 | def command_uptime(m) 41 | date_string = @start_time.strftime("%-m/%-d/%Y") 42 | time_string = @start_time.strftime("%-I:%M%P") 43 | seconds_running = (Time.now - @start_time).to_i 44 | m.user.send "#{shared[:Bot_Nick]} has been running for " + 45 | "#{ChronicDuration.output(seconds_running, :format => :long)} " + 46 | "since #{date_string} at #{time_string}" 47 | end 48 | 49 | end 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /public/css/tipsy.css: -------------------------------------------------------------------------------- 1 | .tipsy { font-size: 10px; position: absolute; padding: 5px; z-index: 100000; } 2 | .tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; } 3 | 4 | /* Rounded corners */ 5 | .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; } 6 | 7 | /* Uncomment for shadow */ 8 | /*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/ 9 | 10 | .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; } 11 | 12 | /* Rules to colour arrows */ 13 | .tipsy-arrow-n { border-bottom-color: #000; } 14 | .tipsy-arrow-s { border-top-color: #000; } 15 | .tipsy-arrow-e { border-left-color: #000; } 16 | .tipsy-arrow-w { border-right-color: #000; } 17 | 18 | .tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; } 19 | .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} 20 | .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} 21 | .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } 22 | .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } 23 | .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } 24 | .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; } 25 | .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; } 26 | -------------------------------------------------------------------------------- /lib/cinch/plugins/shoutcast.rb: -------------------------------------------------------------------------------- 1 | 2 | # Get the title of a Shoutcast stream 3 | 4 | require 'uri' 5 | require 'net/http' 6 | 7 | module Cinch 8 | module Plugins 9 | class Shoutcast 10 | include Cinch::Plugin 11 | 12 | HEADERS = { 13 | "Icy-MetaData" => '1' 14 | } 15 | 16 | match %r{(current|live|nowplaying)}, :method => :command_current # !current 17 | 18 | def initialize(*args) 19 | super 20 | 21 | @shoutcast_uri = URI(config[:shoutcast_uri]) 22 | @last_update = Time.now 23 | @shoutcast_show = parse_shoutcast_stream 24 | end 25 | 26 | # ========================= 27 | # Commands 28 | # ========================= 29 | 30 | def command_current(m) 31 | if (Time.now - @last_update) > 60 32 | # Data older than 60 seconds, refresh it 33 | @shoutcast_show = parse_shoutcast_stream 34 | end 35 | 36 | live_show = Shows.fetch_live_show 37 | if @shoutcast_show 38 | m.user.send "#{@shoutcast_show} is streaming on #{shared[:Live_Url]}" 39 | elsif live_show 40 | m.user.send "#{live_show.title} is live right now!" 41 | else 42 | m.user.send "Failed to get stream info, #{shared[:Live_Url]} may be down. I'm sorry." 43 | end 44 | end 45 | 46 | # ========================= 47 | # Helper Methods 48 | # ========================= 49 | 50 | # Fetches the show title from the live stream defined by URI 51 | def parse_shoutcast_stream 52 | @last_update = Time.now 53 | 54 | http = Net::HTTP.new(@shoutcast_uri.host, @shoutcast_uri.port) 55 | 56 | chunk_count = 0 57 | chunk_limit = 20 # Limit chunks to prevent lockups 58 | begin 59 | http.get(@shoutcast_uri.path, HEADERS) do |chunk| 60 | chunk_count += 1 61 | if chunk =~ /StreamTitle='(.+?)';/ 62 | return $1 63 | break; 64 | elsif chunk_count > chunk_limit 65 | return nil 66 | end 67 | end 68 | rescue Exception => e 69 | puts "Shoucast stream parse failed with message:\n" 70 | puts e.message 71 | end 72 | 73 | # Just in case we get an HTTP error 74 | return nil 75 | end 76 | 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require 'bundler/setup' 3 | 4 | 5 | namespace :test do 6 | Rake::TestTask.new do |t| 7 | t.libs << "test" 8 | t.test_files = FileList['test/*test.rb'] 9 | t.verbose = true 10 | end 11 | end 12 | 13 | # ----------------------- 14 | # Database 15 | # ----------------------- 16 | 17 | namespace :db do 18 | desc 'Auto-migrate the database (destroys data)' 19 | task :migrate => :environment do 20 | puts "This will destroy all data in the _#{ENV['RACK_ENV']}_ database, continue? (yN)" 21 | if STDIN::gets.strip.downcase == 'y' 22 | DataMapper.auto_migrate! 23 | else 24 | puts "Okay, exiting." 25 | end 26 | end 27 | 28 | desc 'Auto-upgrade the database (preserves data)' 29 | task :upgrade => :environment do 30 | DataMapper.auto_upgrade! 31 | end 32 | 33 | desc 'Seed the database with some fake title suggestions' 34 | task :seed => :environment do 35 | 5.times do |n| 36 | Suggestion.create( 37 | :title => "Test #{Time.now.to_i} #{n}", 38 | :show => 'b2w', 39 | :user => 'derp' 40 | ) 41 | end 42 | end 43 | end 44 | 45 | task :environment do 46 | require File.join(File.dirname(__FILE__), 'environment') 47 | end 48 | 49 | namespace :backup do 50 | task :run do 51 | `backup perform -t showbot_backup -c './config/backup.rb'` 52 | end 53 | end 54 | 55 | namespace :foreman do 56 | desc 'Export foreman upstart config for Showbot' 57 | task :export do 58 | sh 'sudo foreman export upstart /etc/init -a showbot -p 5000 -u deploy' 59 | end 60 | end 61 | 62 | # ----------------------- 63 | # SASS 64 | # ----------------------- 65 | 66 | namespace :sass do 67 | desc 'Watches and compiles sass to proper directory' 68 | task :watch => :environment do 69 | sh 'sass --watch -r ./sass/bourbon/lib/bourbon.rb sass/showbot.scss:public/css/showbot.css' 70 | end 71 | end 72 | 73 | # ----------------------- 74 | # Api Key Tasks 75 | # ----------------------- 76 | 77 | namespace :api_key do 78 | desc 'Generates an Api Key for Showbot and prints it out to the console.' 79 | task :generate => :environment do 80 | print "Application Name (required): " 81 | app_name = STDIN::gets.chomp.strip 82 | key = ApiKey.create(app_name: app_name) 83 | if app_name 84 | puts "Here's the Api key for #{app_name}: #{key.value}" 85 | else 86 | puts "App name required to make a key. We gotta keep track of this stuff." 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /public/js/word_cloud.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | WebFontConfig = { 3 | google: { families: [ 'Goudy+Bookletter+1911::latin' ] }, 4 | fontactive: function(fontFamily, FontDescription) { make_all_clouds(); } 5 | }; 6 | (function() { 7 | var wf = document.createElement('script'); 8 | wf.src = ('https:' == document.location.protocol ? 'https' : 'http') + 9 | '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js'; 10 | wf.type = 'text/javascript'; 11 | wf.async = 'true'; 12 | var s = document.getElementsByTagName('script')[0]; 13 | s.parentNode.insertBefore(wf, s); 14 | })(); 15 | 16 | var fill = d3.scale.category20b(); 17 | 18 | var w = 960, 19 | h = 600; 20 | 21 | var fontSize; 22 | 23 | var layout = d3.layout.cloud() 24 | .size([w, h]) 25 | .rotate(function() { return (Math.random() < 0.85) ? 0 : 90; }) 26 | .font("\"Goudy Bookletter 1911\"") 27 | .fontSize(function(d) { return fontSize(+d.value); }) 28 | .text(function(d) { return d.key; }) 29 | .spiral("archimedean") 30 | .on("end", draw); 31 | 32 | function draw(words, bounds) { 33 | scale = bounds ? Math.min( 34 | w / Math.abs(bounds[1].x - w / 2), 35 | w / Math.abs(bounds[0].x - w / 2), 36 | h / Math.abs(bounds[1].y - h / 2), 37 | h / Math.abs(bounds[0].y - h / 2)) / 2 : 1; 38 | d3.select("#content") 39 | .append("svg") 40 | .attr("width", w) 41 | .attr("height", h) 42 | .append("g") 43 | .attr("transform", "translate(" + [w >> 1, h >> 1] + ")scale(" + scale + ")") 44 | .selectAll("text") 45 | .data(words) 46 | .enter().append("text") 47 | .style("font-size", function(d) { return d.size + "px"; }) 48 | .style("font-family", "\"Goudy Bookletter 1911\"") 49 | .style("fill", function(d, i) { return fill(i); }) 50 | .attr("text-anchor", "middle") 51 | .attr("transform", function(d) { 52 | return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; 53 | }) 54 | .text(function(d) { return d.text; }); 55 | } 56 | 57 | function make_cloud(show, data) { 58 | d3.select("#content").append("h2") 59 | .text(show).attr("class", "show_break"); 60 | layout.words(data).start(); 61 | }; 62 | 63 | fontSize = d3.scale.linear().range([5, 55]); 64 | 65 | function make_all_clouds() { 66 | cloudData = $('#cloud-data').data('cloud-data'); 67 | jQuery.each(cloudData, function(i, cloudDatum) { 68 | make_cloud(cloudDatum.title, cloudDatum.data); 69 | }); 70 | }; 71 | }); 72 | -------------------------------------------------------------------------------- /lib/models/link.rb: -------------------------------------------------------------------------------- 1 | # link.rb 2 | # 3 | # DataMapper model for Link 4 | 5 | require 'dm-core' 6 | require 'dm-validations' 7 | require 'dm-timestamps' 8 | require 'dm-types' 9 | require 'open-uri' 10 | require 'openssl' 11 | 12 | class Link 13 | include DataMapper::Resource 14 | 15 | property :id, Serial 16 | property :uri, URI 17 | property :title, String, :length => 100 18 | property :user, String 19 | property :show, String 20 | property :created_at, DateTime 21 | property :updated_at, DateTime 22 | 23 | validates_presence_of :uri 24 | 25 | validates_with_method :check_link_uniqueness 26 | 27 | # ===================== 28 | # Before Create 29 | # ===================== 30 | before :create, :set_live_show 31 | 32 | def set_live_show 33 | # Only fetch show from website if it wasn't set previously. 34 | if !self.show 35 | self.show = Shows.fetch_live_show_slug 36 | end 37 | 38 | true # Since this hook shouldn't keep the link from saving 39 | end 40 | 41 | # ===================== 42 | # After Create 43 | # ===================== 44 | 45 | # Fetch the page title on a new thread so we don't block while requesting it 46 | after :create do |link| 47 | fetch_page_title(link) 48 | end 49 | 50 | def fetch_page_title(link) 51 | Thread.new(link) do 52 | begin 53 | link.update(:title => open(link.uri, :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE).read.match(/(.*?)<\/title>?/im)[1]) 54 | rescue URI::InvalidURIError 55 | STDOUT::puts "Failed to fetch title for #{link.uri}." 56 | end 57 | end 58 | end 59 | 60 | # ===================== 61 | # Validations 62 | # ===================== 63 | 64 | # Verifies that link hasn't been entered in the last 30 minutes 65 | def check_link_uniqueness 66 | if self.uri 67 | Link.minutes_ago(30).each do |link| 68 | # Don't check uniqueness against itself 69 | if link.id != self.id and link.uri == self.uri 70 | return [false, "Darn, #{link.user} beat you to #{link.uri}."] 71 | end 72 | end 73 | else 74 | return true 75 | end 76 | return true 77 | end 78 | 79 | # ===================== 80 | # Class Methods 81 | # ===================== 82 | 83 | def self.recent(days_ago = 1) 84 | from = DateTime.now - days_ago 85 | all(:created_at.gt => from).all(:order => [:created_at.desc]) 86 | end 87 | 88 | def self.minutes_ago(minutes) 89 | if minutes 90 | time_ago = Time.now - (60 * minutes) 91 | all(:created_at.gt => time_ago).all(:order => [:created_at.desc]) 92 | end 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /lib/models/ical_cache.rb: -------------------------------------------------------------------------------- 1 | # Wrapper class for ri_cal that caches the data 2 | # and handles some advanced queries 3 | 4 | require "ri_cal" 5 | require "open-uri" 6 | require 'tzinfo' 7 | 8 | class ICalCache 9 | 10 | # Takes the target iCal url as an argument 11 | def initialize(ical_url) 12 | @ical_url = ical_url 13 | refresh 14 | end 15 | 16 | # Refreshes the cache 17 | def refresh 18 | begin 19 | @cache = RiCal.parse(open(@ical_url)) 20 | rescue OpenURI::HTTPError 21 | puts "#{Time.now} ERROR: Failed to fetch calendar data. OpenURI::HTTPError" 22 | end 23 | end 24 | 25 | 26 | # Return the next iCal event after the current time 27 | def next_event(keyword = nil) 28 | nearest_event = nil 29 | nearest_seconds_until = nil 30 | 31 | upcoming_events.each do |event| 32 | # Grab the next occurrence for the event 33 | event = (event.occurrences({:starting => DateTime.now, :count => 1})).first 34 | 35 | if event and event.start_time > DateTime.now 36 | seconds_until = ((event.start_time - DateTime.now) * 24 * 60 * 60).to_i 37 | if keyword and event.summary.strip.downcase.include? keyword.downcase 38 | if !nearest_seconds_until 39 | nearest_seconds_until = seconds_until 40 | nearest_event = event 41 | elsif seconds_until < nearest_seconds_until 42 | nearest_seconds_until = seconds_until 43 | nearest_event = event 44 | end 45 | elsif !keyword 46 | if !nearest_seconds_until 47 | nearest_seconds_until = seconds_until 48 | nearest_event = event 49 | elsif seconds_until < nearest_seconds_until 50 | nearest_seconds_until = seconds_until 51 | nearest_event = event 52 | end 53 | end 54 | end 55 | end 56 | 57 | nearest_event 58 | end 59 | 60 | # Return an array of iCal events for today onward 61 | def upcoming_events 62 | events = [] 63 | 64 | @cache.first.events.each do |event| 65 | # Grab the next occurrence for the event 66 | event = (event.occurrences({:starting => Date.today, :count => 1})).first 67 | 68 | if event 69 | skip = false 70 | events.reject do |e| 71 | if e.uid == event.uid 72 | if e.last_modified < event.last_modified 73 | # Remove old event if same UID and older modified time 74 | true 75 | else 76 | # Don't add the new event because it was modified longer ago than current 77 | skip = true 78 | end 79 | else 80 | false 81 | end 82 | end 83 | 84 | events << event if not skip 85 | end 86 | end 87 | events 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /views/coffeescripts/showbot.coffee: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(-> 2 | # Timeago and Tipsy 3 | $("abbr.timeago").timeago().show().timeago().tipsy( 4 | gravity: 'w' 5 | fade: true 6 | ) 7 | 8 | # Modernizr 9 | if (Modernizr.touch) 10 | # Remove hover events for Touch devices since they screw up rendering 11 | $(".hover").removeClass("hover") 12 | 13 | # Show the heart since it starts off hidden 14 | $('.heart').show() 15 | 16 | # Table Sorting 17 | $("table.sortable").tablesorter( 18 | textExtraction: table_text_extraction 19 | sortList: [[0,1]] 20 | ) 21 | 22 | # Setup Votes 23 | setup_voting() 24 | ) 25 | 26 | 27 | # Extract text from cells that aren't normal 28 | # 29 | # Returns a String of text that represents the cell for sorting purposes. 30 | table_text_extraction = (element) -> 31 | $element = $(element) 32 | text = $element.html() 33 | 34 | # If this is a date column, extract the text for sorting 35 | if $element.find('abbr').length 36 | text = $element.find('abbr').data('epoch-time') 37 | # Extract vote count value if this is a vote column 38 | # Note: This is also required for sorting to continue working via the 39 | # trigger('update') after a vote is cast 40 | else if $element.find('.vote_count').length 41 | text = $element.find('.vote_count').html() 42 | 43 | text 44 | 45 | setup_voting = -> 46 | $('a.vote_up').live('click', (e) -> 47 | e.preventDefault() 48 | $link = $(@) 49 | $vote_count = $link.siblings('.vote_count').first() 50 | 51 | # Do nothing if already marked as voted 52 | if $vote_count.hasClass('voted') 53 | return 54 | else 55 | $vote_count.addClass('voted') 56 | 57 | id = $link.data('id') 58 | 59 | $.get("/titles/#{id}/vote_up", (response) -> 60 | if response? 61 | $vote_arrow = $link.find('.vote_arrow') 62 | $vote_arrow.addClass('launch') 63 | # Wait for the launch animation to finish 64 | setTimeout( 65 | -> $vote_arrow.remove() 66 | 800 # 0.2 seconds less than animation due to hide 67 | ) 68 | vote_amount = parseInt(response.votes) 69 | if isNaN(vote_amount) 70 | $vote_count.addClass('error') 71 | else 72 | $vote_count.text(vote_amount) 73 | 74 | if response.cluster_top 75 | $link.closest('tr').children('.cluster-votes').text(response.cluster_votes) 76 | else 77 | $link.closest('tr').siblings('#cluster-' + response.cluster_id).children('.cluster-votes').text(response.cluster_votes) 78 | 79 | # Update the sort cache so the table will sort based on the new vote 80 | # value 81 | $link.parents('table').trigger('update') 82 | , "json").error(-> 83 | $vote_count.removeClass('voted') 84 | $vote_count.addClass('error') 85 | ) 86 | ) 87 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_background-image.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Background-image property for adding multiple background images with 3 | // gradients, or for stringing multiple gradients together. 4 | //************************************************************************// 5 | /*@import "../functions/linear-gradient";*/ 6 | /*@import "../functions/radial-gradient";*/ 7 | 8 | @mixin background-image( 9 | $image-1 , $image-2: false, 10 | $image-3: false, $image-4: false, 11 | $image-5: false, $image-6: false, 12 | $image-7: false, $image-8: false, 13 | $image-9: false, $image-10: false 14 | ) { 15 | $images: compact($image-1, $image-2, 16 | $image-3, $image-4, 17 | $image-5, $image-6, 18 | $image-7, $image-8, 19 | $image-9, $image-10); 20 | 21 | background-image: add-prefix($images, webkit); 22 | background-image: add-prefix($images, moz); 23 | background-image: add-prefix($images, ms); 24 | background-image: add-prefix($images, o); 25 | background-image: add-prefix($images); 26 | } 27 | 28 | 29 | @function add-prefix($images, $vendor: false) { 30 | $images-prefixed: (); 31 | 32 | @for $i from 1 through length($images) { 33 | $type: type-of(nth($images, $i)); // Get type of variable - List or String 34 | 35 | // If variable is a list - Gradient 36 | @if $type == list { 37 | $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) 38 | $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) 39 | 40 | $gradient: render-gradients($gradient-args, $gradient-type, $vendor); 41 | $images-prefixed: append($images-prefixed, $gradient, comma); 42 | } 43 | 44 | // If variable is a string - Image 45 | @else if $type == string { 46 | $images-prefixed: join($images-prefixed, nth($images, $i), comma); 47 | } 48 | } 49 | @return $images-prefixed; 50 | } 51 | 52 | 53 | @function render-gradients($gradients, $gradient-type, $vendor: false) { 54 | $vendor-gradients: false; 55 | @if $vendor { 56 | $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient($gradients); 57 | } 58 | 59 | @else if $vendor == false { 60 | $vendor-gradients: "#{$gradient-type}-gradient(#{$gradients})"; 61 | $vendor-gradients: unquote($vendor-gradients); 62 | } 63 | @return $vendor-gradients; 64 | } 65 | 66 | //Examples: 67 | //@include background-image(linear-gradient(top, orange, red)); 68 | //@include background-image(radial-gradient(50% 50%, cover circle, orange, red)); 69 | //@include background-image(url("/images/a.png"), linear-gradient(orange, red)); 70 | //@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png")); 71 | //@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red); 72 | -------------------------------------------------------------------------------- /lib/cinch/plugins/schedule.rb: -------------------------------------------------------------------------------- 1 | # Query the schedule from an iCal store 2 | 3 | require 'chronic_duration' 4 | require './lib/models/shows' 5 | require './lib/models/ical_cache' 6 | 7 | module Cinch 8 | module Plugins 9 | class Schedule 10 | include Cinch::Plugin 11 | 12 | timer 600, :method => :refresh_calendar 13 | 14 | match /next\s?(.*)/i, :method => :command_next # !next 15 | match /schedule\s?(.*)/i, :method => :command_schedule # !schedule 16 | 17 | def initialize(*args) 18 | super 19 | # This is a terrible hack to get access to a folder in the project directory 20 | # TODO find a better way 21 | @calendar = ICalCache.new config[:ical_uri] 22 | # Get the inital data for the calendar 23 | refresh_calendar 24 | end 25 | 26 | # Refreshes calendar data 27 | def refresh_calendar 28 | puts "Refreshing calendar data" 29 | @calendar.refresh 30 | end 31 | 32 | # Replies to the user with information about the next show 33 | # !next b2w -> The next Back to Work is in 3 hours 30 minutes (6/2/2011) 34 | def command_next(m, show_keyword) 35 | show = Shows.find_show(show_keyword) if show_keyword and !show_keyword.strip.empty? 36 | 37 | if show 38 | next_event = @calendar.next_event(show.title) 39 | else 40 | next_event = @calendar.next_event 41 | end 42 | 43 | if next_event 44 | tz = TZInfo::Timezone.get('America/New_York') 45 | time = tz.utc_to_local(next_event.start_time) 46 | 47 | date_string = time.strftime("%A, %-m/%-d/%Y") 48 | time_string = time.strftime("%-I:%M%P EST") 49 | nearest_seconds_until = ((next_event.start_time - DateTime.now) * 24 * 60 * 60).to_i 50 | if show 51 | m.user.send "The next #{next_event.summary} is in #{ChronicDuration.output(nearest_seconds_until, :format => :long)} (#{time_string} on #{date_string})" 52 | else 53 | m.user.send "Next show is #{next_event.summary} in #{ChronicDuration.output(nearest_seconds_until, :format => :long)} (#{time_string} on #{date_string})" 54 | end 55 | else 56 | m.user.send "No upcoming show found for #{show.title}" 57 | end 58 | 59 | end 60 | 61 | # Replies with the schedule for the next 7 days of shows 62 | def command_schedule(m, command) 63 | upcoming_events = @calendar.upcoming_events 64 | 65 | if command == "refresh" 66 | refresh_calendar 67 | elsif upcoming_events.length > 0 68 | m.user.send "#{upcoming_events.length} upcoming show#{upcoming_events.length > 1 ? "s" : ""}" 69 | upcoming_events.sort{|e1, e2| e1.start_time <=> e2.start_time}.each do |event| 70 | tz = TZInfo::Timezone.get('America/New_York') 71 | time = tz.utc_to_local(event.start_time) 72 | date_string = time.strftime("%A, %-m/%-d/%Y") 73 | time_string = time.strftime("%-I:%M%P EST") 74 | m.user.send " #{event.summary} on #{date_string} at #{time_string}" 75 | end 76 | end 77 | 78 | end 79 | 80 | end 81 | end 82 | end 83 | 84 | -------------------------------------------------------------------------------- /test/showbot_web_test.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | 3 | require './environment' 4 | require './showbot_web' 5 | require 'test/unit' 6 | require 'rack/test' 7 | 8 | 9 | class ShowbotWebTest < Test::Unit::TestCase 10 | include Rack::Test::Methods 11 | 12 | def app 13 | ShowbotWeb 14 | end 15 | 16 | # ------------------ 17 | # Setup and Teardown 18 | # ------------------ 19 | 20 | class << self 21 | def startup 22 | end 23 | 24 | def shutdown 25 | end 26 | end 27 | 28 | # ------------------ 29 | # Success Cases 30 | # ------------------ 31 | 32 | def test_home_page_works 33 | get '/' 34 | last_response.ok? 35 | assert last_response.body =~ /showbot/ 36 | end 37 | 38 | def test_title_suggestion_api 39 | title = valid_title 40 | user = valid_user 41 | 42 | post '/suggestions/new', params = {api_key: api_key.value, title: title, user: user} 43 | 44 | assert_last_response_valid 45 | assert_equal json_response['suggestion']['title'], title 46 | assert_equal json_response['suggestion']['user'], user 47 | end 48 | 49 | # ------------------ 50 | # Error Cases 51 | # ------------------ 52 | 53 | def test_invalid_api_key 54 | invalid_key = 'invalid_key' 55 | post '/suggestions/new', params = {api_key: invalid_key} 56 | 57 | assert_equal last_response.status, 404 58 | 59 | assert_equal json_response['error'], "Invalid Api Key #{invalid_key}" 60 | end 61 | 62 | def test_too_long_title_api 63 | error_message = "That suggestion was too long. Showbot is sorry. Think title, not transcript." 64 | 65 | post '/suggestions/new', params = {api_key: api_key.value, title: ('x'*41), user: valid_user} 66 | 67 | assert_equal json_response['error'], error_message 68 | end 69 | 70 | def test_duplicate_title 71 | title = "Same Title" 72 | first_user = valid_user 73 | error_message = "Darn, #{first_user} beat you to \"#{title}\"." 74 | 75 | post '/suggestions/new', params = {api_key: api_key.value, title: title, user: first_user} 76 | post '/suggestions/new', params = {api_key: api_key.value, title: title, user: valid_user} 77 | 78 | assert_equal json_response['error'], error_message 79 | end 80 | 81 | def test_missing_user 82 | post '/suggestions/new', params = {api_key: api_key.value, title: valid_title} 83 | assert_equal json_response['error'], 'Missing / Invalid User' 84 | end 85 | 86 | def test_title_user 87 | post '/suggestions/new', params = {api_key: api_key.value, user: valid_user} 88 | assert_equal json_response['error'], 'Missing / Invalid Title' 89 | end 90 | 91 | # ------------------ 92 | # Helpers 93 | # ------------------ 94 | 95 | def api_key 96 | @@api_key ||= ApiKey.create(app_name: 'Test App') 97 | end 98 | 99 | def json_response 100 | @json ||= JSON.parse(last_response.body) 101 | end 102 | 103 | def valid_user 104 | user = SecureRandom.urlsafe_base64(10) 105 | end 106 | 107 | def valid_title 108 | title = SecureRandom.urlsafe_base64(20) 109 | end 110 | 111 | def assert_last_response_valid 112 | assert last_response.ok?, "Last response returned error status #{last_response.status}\nBody:#{last_response.body}" 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | messages: 3 | models: 4 | suggestion: That suggestion was too long. Bot is sorry. Think title, not transcript. 5 | views: 6 | clouds: 7 | cloud_data_empty: "No episodes found in that time frame." 8 | clouds_svg: 9 | out_of_bounds_begin: "The index of %cloud_index is out of bounds. There " 10 | cloud_data_count: 11 | one: "is %cloud_data_count cloud" 12 | other: "are %cloud_data_count clouds" 13 | out_of_bounds_end: " on the specified date." 14 | layout: 15 | title: "Showbot" 16 | meta_description: "Showbot displays title suggestions from users listening to the live http://5by5.tv podcasts. It gathers these suggestions from the #5by5 IRC channel." 17 | meta_author: "Jeremy Mack" 18 | header: "Showbot" 19 | heart: "<<span>3</span>" 20 | link_titles: "Titles" 21 | link_links: "Links" 22 | friendly: 23 | message: "Showbot loves you." 24 | alt: "Not like that." 25 | exist: 26 | message: "Why" 27 | para_1: "The jackals in the 5by5 Chat think they know what the title " 28 | para_2: "of the show should be. Showbot passes no judgements. Showbot collects" 29 | para_3: " the title suggestions from IRC and shows them here. Showbot is here to help." 30 | who: 31 | message: "Who" 32 | para_1: "Showbot was created by Jeremy Mack, <a href=\"http://twitter.com/mutewinter\">@mutewinter</a>." 33 | para_2: "Showbot also receives love from coders in the 5by5 community on GitHub." 34 | where: 35 | message: "Where" 36 | para_1: "<a href=\"http://github.com/mutewinter/Showbot\">Showbot on GitHub</a>" 37 | para_2: "<a href=\"http://5by5.tv/chat\">Showbot in IRC</a>" 38 | para_3: "<a href=\"http://5by5.tv\">5by5.tv</a>" 39 | links: 40 | links_count: 41 | one: "1 Link Suggestion in the last 24 hours" 42 | other: "%links.count Link Suggestions in the last 24 hours" 43 | irc_help: In the <a href='http://5by5.tv/chat'>5by5 chat room</a>? Type <span class='code'>!link http://example.com</span> to suggest a link. 44 | unknown_show: "Show Not Listed" 45 | zero: "No link suggestions yet" 46 | suggestion: 47 | index: 48 | subtitle: 49 | zero: "No title suggestions yet" 50 | one: "%{count} Title suggestion in the last 24 hours" 51 | other: "%{count} Title suggestions in the last 24 hours" 52 | irc_help: In the <a href='http://5by5.tv/chat'>5by5 chat room</a>? Type <span class='code'>!suggest Wacky Title</span> to suggest a title. 53 | view_mode: 54 | Bubbles: "Bubbles" 55 | Tables: "Tables" 56 | Clusters: "Clusters" 57 | Hacker: "Hacker" 58 | _table_set: 59 | Votes: "Votes" 60 | Title: "Title" 61 | User: "User" 62 | When: "When" 63 | _table_row: 64 | User: "No User" 65 | _cluster_set: 66 | titles: 67 | one: "%{count} Title " 68 | other: "%{count} Titles " 69 | groups: 70 | one: "in %{count} Group" 71 | other: "in %{count} Groups" 72 | Votes: "Votes" 73 | Title: "Title" 74 | User: "User" 75 | When: "When" 76 | _cluster_table_row: 77 | User: "No User" 78 | _bubble: 79 | subtitle: 80 | one: "Vote" 81 | other: "Votes" 82 | hacker_mode: 83 | welcome: Welcome to Hacker Mode. 84 | suggestions: 85 | zero: "No Title Suggestion in the last 24 hours." 86 | one: "%{count} Title Suggestion in the last 24 hours." 87 | other: "%{count} Title Suggestions in the last 24 hours." -------------------------------------------------------------------------------- /lib/cinch/plugins/twitter.rb: -------------------------------------------------------------------------------- 1 | # A Cinch plugin for broadcasting Twitter updates to an IRC channel 2 | 3 | require 'chronic_duration' 4 | require 'twitter' 5 | 6 | module Cinch 7 | module Plugins 8 | class Twitter 9 | include Cinch::Plugin 10 | 11 | timer 60, :method => :send_last_status 12 | 13 | match "last_status", :method => :command_last_status 14 | 15 | Client = ::Twitter::REST::Client.new do |c| 16 | c.consumer_key = config[:twitter_consumer_key] 17 | c.consumer_secret = config[:twitter_consumer_secret] 18 | c.access_token = config[:twitter_access_token] 19 | c.access_token_secret = config[:twitter_access_token_secret] 20 | end 21 | 22 | def initialize(*args) 23 | super 24 | 25 | @user = config[:twitter_user] 26 | @channel = config[:channel] 27 | @channel_test = config[:channel_test] 28 | @twitter_user = nil 29 | @status = {} 30 | @last_sent_id = {} 31 | @user.each do |z| 32 | @last_sent_id[z] = nil 33 | @status[z] = nil 34 | end 35 | end 36 | 37 | def response_from_laststatus(status) 38 | if status 39 | created_at = status.created_at.to_datetime 40 | seconds_ago = (Time.now - created_at.to_time).to_i 41 | relative_time = ChronicDuration.output(seconds_ago, :format => :long) 42 | 43 | return "@#{@twitter_laststatus_user}: #{status.text} (#{relative_time} ago)" 44 | end 45 | end 46 | 47 | # Send the last status for the TWITTER_USER to the user who requested it 48 | def command_last_status(m) 49 | @user.each do |z| 50 | begin 51 | status = Client.user_timeline(z).first 52 | @twitter_laststatus_user = z 53 | m.user.send response_from_laststatus(status) 54 | rescue ::Twitter::Error::ServiceUnavailable 55 | m.user.send "Oops, looks like Twitter's whale failed. Try again in a minute." 56 | end 57 | end 58 | end 59 | 60 | def send_last_status 61 | @user.each do |z| 62 | begin 63 | @status[z] = Client.user_timeline(z).first 64 | rescue ::Twitter::Error::ServiceUnavailable 65 | puts "Error: Twitter is over capacity." 66 | return 67 | end 68 | 69 | if @last_sent_id[z].nil? 70 | # Skip the first message from TWITTER_USER so we don't spam every 71 | # time the bot reconnects 72 | @last_sent_id[z] = @status[z].id 73 | 74 | elsif @last_sent_id[z] != @status[z].id 75 | 76 | if @status[z].in_reply_to_status_id or @status[z].in_reply_to_screen_name 77 | # Don't show replies 78 | next 79 | end 80 | 81 | @last_sent_id[z] = @status[z].id 82 | status = @status[z] 83 | @twitter_user = z 84 | 85 | if @bot.nick =~ /_test$/ 86 | Channel(@channel_test).send response_from_status(status) 87 | else 88 | Channel(@channel).send response_from_status(status) 89 | end 90 | end 91 | end 92 | 93 | def response_from_status(status) 94 | if status 95 | created_at = status.created_at.to_datetime 96 | seconds_ago = (Time.now - created_at.to_time).to_i 97 | relative_time = ChronicDuration.output(seconds_ago, :format => :long) 98 | 99 | return "@#{@twitter_user}: #{status.text} (#{relative_time} ago)" 100 | end 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /sass/bourbon/css3/_transition.scss: -------------------------------------------------------------------------------- 1 | // Shorthand mixin. Supports multiple parentheses-deliminated values for each variable. 2 | // Example: @include transition (all, 2.0s, ease-in-out); 3 | // @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s)); 4 | // @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s)); 5 | 6 | @mixin transition ($property: all, $duration: 0.15s, $timing-function: ease-out, $delay: 0) { 7 | 8 | // Detect # of args passed into each variable 9 | $length-of-property: length($property); 10 | $length-of-duration: length($duration); 11 | $length-of-timing-function: length($timing-function); 12 | $length-of-delay: length($delay); 13 | 14 | @if $length-of-property > 1 { 15 | @include transition-property(zip($property)); } 16 | @else { 17 | @include transition-property( $property); 18 | } 19 | 20 | @if $length-of-duration > 1 { 21 | @include transition-duration(zip($duration)); } 22 | @else { 23 | @include transition-duration( $duration); 24 | } 25 | 26 | @if $length-of-timing-function > 1 { 27 | @include transition-timing-function(zip($timing-function)); } 28 | @else { 29 | @include transition-timing-function( $timing-function); 30 | } 31 | 32 | @if $length-of-delay > 1 { 33 | @include transition-delay(zip($delay)); } 34 | @else { 35 | @include transition-delay( $delay); 36 | } 37 | } 38 | 39 | 40 | @mixin transition-property ($prop-1: all, 41 | $prop-2: false, $prop-3: false, 42 | $prop-4: false, $prop-5: false, 43 | $prop-6: false, $prop-7: false, 44 | $prop-8: false, $prop-9: false) 45 | { 46 | $full: compact($prop-1, $prop-2, $prop-3, $prop-4, $prop-5, 47 | $prop-6, $prop-7, $prop-8, $prop-9); 48 | 49 | -webkit-transition-property: $full; 50 | -moz-transition-property: $full; 51 | -ms-transition-property: $full; 52 | -o-transition-property: $full; 53 | transition-property: $full; 54 | } 55 | 56 | @mixin transition-duration ($time-1: 0, 57 | $time-2: false, $time-3: false, 58 | $time-4: false, $time-5: false, 59 | $time-6: false, $time-7: false, 60 | $time-8: false, $time-9: false) 61 | { 62 | $full: compact($time-1, $time-2, $time-3, $time-4, $time-5, 63 | $time-6, $time-7, $time-8, $time-9); 64 | 65 | -webkit-transition-duration: $full; 66 | -moz-transition-duration: $full; 67 | -ms-transition-duration: $full; 68 | -o-transition-duration: $full; 69 | transition-duration: $full; 70 | } 71 | 72 | @mixin transition-timing-function ($motion-1: ease, 73 | $motion-2: false, $motion-3: false, 74 | $motion-4: false, $motion-5: false, 75 | $motion-6: false, $motion-7: false, 76 | $motion-8: false, $motion-9: false) 77 | { 78 | $full: compact($motion-1, $motion-2, $motion-3, $motion-4, $motion-5, 79 | $motion-6, $motion-7, $motion-8, $motion-9); 80 | 81 | // ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() 82 | -webkit-transition-timing-function: $full; 83 | -moz-transition-timing-function: $full; 84 | -ms-transition-timing-function: $full; 85 | -o-transition-timing-function: $full; 86 | transition-timing-function: $full; 87 | } 88 | 89 | @mixin transition-delay ($time-1: 0, 90 | $time-2: false, $time-3: false, 91 | $time-4: false, $time-5: false, 92 | $time-6: false, $time-7: false, 93 | $time-8: false, $time-9: false) 94 | { 95 | $full: compact($time-1, $time-2, $time-3, $time-4, $time-5, 96 | $time-6, $time-7, $time-8, $time-9); 97 | 98 | -webkit-transition-delay: $full; 99 | -moz-transition-delay: $full; 100 | -ms-transition-delay: $full; 101 | -o-transition-delay: $full; 102 | transition-delay: $full; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /public/shows.json: -------------------------------------------------------------------------------- 1 | {"shows": [ 2 | {"rss":"http://feeds.feedburner.com/5by5-afterdark" , "url": "afterdark" , "title": "After Dark"} , 3 | {"rss":"http://feeds.feedburner.com/back2work" , "url": "b2w" , "title": "Back to Work"} , 4 | {"rss":"http://feeds.feedburner.com/bigwebshow" , "url": "bigwebshow" , "title": "The Big Web Show"} , 5 | {"rss":"http://feeds.feedburner.com/brieflyawesome" , "url": "brieflyawesome" , "title": "Briefly Awesome"} , 6 | {"rss":"http://feeds.feedburner.com/buildanalyze" , "url": "buildanalyze" , "title": "Build and Analyze"} , 7 | {"rss":"http://feeds.feedburner.com/contenttalks" , "url": "contenttalks" , "title": "Content Talks"} , 8 | {"rss":"http://feeds.feedburner.com/dailyedition" , "url": "dailyedition" , "title": "The Daily Edition"} , 9 | {"rss":"http://feeds.feedburner.com/5by5-devshow" , "url": "devshow" , "title": "The Dev Show"} , 10 | {"rss":"http://feeds.feedburner.com/founderstalk" , "url": "founderstalk" , "title": "Founders Talk"} , 11 | {"rss":"http://feeds.feedburner.com/hypercritical" , "url": "hypercritical" , "title": "Hypercritical"} , 12 | {"rss":"http://feeds.feedburner.com/letsmakemistakes" , "url": "mistakes" , "title": "Let's Make Mistakes"} , 13 | {"rss":"http://feeds.feedburner.com/PaleoPodcast" , "url": "paleo" , "title": "Latest in Paleo"} , 14 | {"rss":"http://feeds.feedburner.com/thepipelineshow" , "url": "pipeline" , "title": "The Pipeline"} , 15 | {"rss":"http://feeds.feedburner.com/5by5-superhero" , "url": "superhero" , "title": "Internet Superhero"} , 16 | {"rss":"http://feeds.feedburner.com/thetalkshow" , "url": "talkshow" , "title": "The Talk Show"} , 17 | {"rss":"http://feeds.feedburner.com/criticalpath" , "url": "criticalpath" , "title": "The Critical Path"} , 18 | {"rss":"http://feeds.feedburner.com/macpowerusers" , "url": "mpu" , "title": "Mac Power Users"} , 19 | {"rss":"http://feeds.feedburner.com/TheWebAhead" , "url": "webahead" , "title": "The Web Ahead"} , 20 | {"rss":"http://feeds.feedburner.com/TheCocktailNapkinVideos" , "url": "tcn" , "title": "The Cocktail Napkin"} , 21 | {"rss":"http://feeds.feedburner.com/GeekFriday" , "url": "geekfriday" , "title": "Geek Friday"} , 22 | {"rss":"http://feeds.feedburner.com/IhnatkoAlmanac" , "url": "ia" , "title": "The Ihnatko Almanac"} , 23 | {"rss":"http://feeds.feedburner.com/incomparablepodcast" , "url": "incomparable" , "title": "The Incomparable"} , 24 | {"rss":"http://feeds.feedburner.com/5by5-specials" , "url": "specials" , "title": "Specials"} , 25 | {"rss":"http://feeds.feedburner.com/5by5-amplified" , "url": "amplified" , "title": "Amplified"} , 26 | {"rss":"http://feeds.feedburner.com/thebbpodcast" , "url": "bb" , "title": "The B&B Podcast"} , 27 | {"rss":"http://feeds.feedburner.com/5by5-inbeta" , "url": "inbeta" , "title": "In Beta"} , 28 | {"rss":"http://feeds.feedburner.com/TheNickel" , "url": "nickel" , "title": "The Nickel"} , 29 | {"rss":"http://feeds.5by5.tv/frequency" , "url": "frequency" , "title": "The Frequency"} , 30 | {"rss":"http://feeds.5by5.tv/screentime" , "url": "screentime" , "title": "Screen Time"} , 31 | {"rss":"http://feeds.5by5.tv/otn" , "url": "otn" , "title": "Old Tech New"} , 32 | {"rss":"http://feeds.5by5.tv/crossover" , "url": "crossover" , "title": "The Crossover"} , 33 | {"rss":"http://feeds.5by5.tv/quit" , "url": "quit" , "title": "Quit!"} 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html 3 | %head 4 | %meta{:charset => "utf-8"} 5 | %meta{:'http-equiv' => "X-UA-Compatible", :content => "IE=edge,chrome=1"} 6 | 7 | %title=t('views.layout.title') + " #{'| ' + @title if @title}" 8 | %meta{:content => t('views.layout.meta_description'), :name => "description"} 9 | %meta{:content => t('views.layout.meta_author'), :name => "author"} 10 | %link(rel="stylesheet" href="/css/reset.css") 11 | %link(rel="stylesheet" href="/css/text.css") 12 | %link(rel="stylesheet" href="/css/960.css") 13 | %link(rel="stylesheet" href="/css/tipsy.css") 14 | %link(rel="stylesheet" href="/css/showbot.css?v=5") 15 | / Modernizer 16 | %script{:type => "text/javascript", :src => "/js/modernizr.custom.js"} 17 | %body 18 | #wrap.container_12 19 | -if development? 20 | .development 21 | .banner DEVELOPMENT MODE 22 | .banner_push 23 | #header 24 | %h1.logo 25 | %a{:href => '/'}=t('views.layout.header') 26 | -# display:none so the heart doesn't briefly show 27 | .heart{:style => 'display:none;'}=t('views.layout.heart') 28 | %nav 29 | %ul 30 | %li 31 | %a{:href => '/titles'}=t('views.layout.link_titles') 32 | %li 33 | %a{:href => '/links'}=t('views.layout.link_links') 34 | .clear 35 | %hr.push 36 | 37 | #content 38 | =yield 39 | 40 | .footer_push 41 | .container_12 42 | %abbr#bot_love.grid_12{:title => t('views.layout.friendly.alt')}=t('views.layout.friendly.message') 43 | #footer 44 | .container_12 45 | %ul 46 | %li.grid_3.push_2 47 | %h2= t('views.layout.exist.message') 48 | %p=t('views.layout.exist.para_1') + | 49 | t('views.layout.exist.para_2') + | 50 | t('views.layout.exist.para_3') | 51 | %li.grid_3.push_2 52 | %h2=t('views.layout.who.message') 53 | %p=t('views.layout.who.para_1') 54 | %p=t('views.layout.who.para_2') 55 | %li.grid_3.push_2 56 | %h2=t('views.layout.where.message') 57 | %p=t('views.layout.where.para_1') 58 | %p=t('views.layout.where.para_2') 59 | %p=t('views.layout.where.para_3') 60 | 61 | / Javascript at the bottom for fast page loading 62 | / Grab Google CDN's jQuery. fall back to local if necessary 63 | %script{:type => "text/javascript", :src => "http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"} 64 | %script{:type => "text/javascript", :src => "http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"} 65 | :javascript 66 | !window.jQuery && document.write('<script src="/js/jquery.min.js"><\/script>') 67 | / JQuery Plugins 68 | %script{:type => "text/javascript", :src => "/js/jquery.timeago.js"} 69 | %script{:type => "text/javascript", :src => "/js/jquery.tablesorter.min.js"} 70 | %script{:type => "text/javascript", :src => "/js/jquery.tipsy.js"} 71 | %script{:type => "text/javascript", :src => "/js/d3.v3.min.js"} 72 | %script{:type => "text/javascript", :src => "/js/d3.layout.cloud.js"} 73 | %script{:type => "text/javascript", :src => "/js/word_cloud.js"} 74 | / Main JS 75 | %script{:type => "text/javascript", :src => "/js/showbot.js"} 76 | :javascript 77 | var _gauges = _gauges || []; 78 | (function() { 79 | var t = document.createElement('script'); 80 | t.type = 'text/javascript'; 81 | t.async = true; 82 | t.id = 'gauges-tracker'; 83 | t.setAttribute('data-site-id', '50db683e613f5d7e99000002'); 84 | t.src = '//secure.gaug.es/track.js'; 85 | var s = document.getElementsByTagName('script')[0]; 86 | s.parentNode.insertBefore(t, s); 87 | })(); 88 | :javascript 89 | var _gaq = _gaq || []; 90 | _gaq.push(['_setAccount', 'UA-24163893-1']); 91 | _gaq.push(['_trackPageview']); 92 | 93 | (function() { 94 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 95 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 96 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 97 | })(); 98 | :javascript 99 | $(function() { 100 | $('tr.cluster-top').children('td.title') 101 | .css("cursor","pointer") 102 | .attr("title","Click to expand/collapse") 103 | .click(function(){ 104 | $(this).parent().siblings('.child-'+this.parentElement.id).toggle(); 105 | $(this).find('.cluster-arrow').toggleClass('expanded-arrow') 106 | }); 107 | $('tr[class^="expand-child"]').hide().children('td'); 108 | }); 109 | -------------------------------------------------------------------------------- /public/js/jquery.timeago.js: -------------------------------------------------------------------------------- 1 | /* 2 | * timeago: a jQuery plugin, version: 0.9.3 (2011-01-21) 3 | * @requires jQuery v1.2.3 or later 4 | * 5 | * Timeago is a jQuery plugin that makes it easy to support automatically 6 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). 7 | * 8 | * For usage and examples, visit: 9 | * http://timeago.yarp.com/ 10 | * 11 | * Licensed under the MIT: 12 | * http://www.opensource.org/licenses/mit-license.php 13 | * 14 | * Copyright (c) 2008-2011, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org) 15 | */ 16 | (function($) { 17 | $.timeago = function(timestamp) { 18 | if (timestamp instanceof Date) { 19 | return inWords(timestamp); 20 | } else if (typeof timestamp === "string") { 21 | return inWords($.timeago.parse(timestamp)); 22 | } else { 23 | return inWords($.timeago.datetime(timestamp)); 24 | } 25 | }; 26 | var $t = $.timeago; 27 | 28 | $.extend($.timeago, { 29 | settings: { 30 | refreshMillis: 60000, 31 | allowFuture: false, 32 | strings: { 33 | prefixAgo: null, 34 | prefixFromNow: null, 35 | suffixAgo: "ago", 36 | suffixFromNow: "from now", 37 | seconds: "less than a minute", 38 | minute: "about a minute", 39 | minutes: "%d minutes", 40 | hour: "about an hour", 41 | hours: "about %d hours", 42 | day: "a day", 43 | days: "%d days", 44 | month: "about a month", 45 | months: "%d months", 46 | year: "about a year", 47 | years: "%d years", 48 | numbers: [] 49 | } 50 | }, 51 | inWords: function(distanceMillis) { 52 | var $l = this.settings.strings; 53 | var prefix = $l.prefixAgo; 54 | var suffix = $l.suffixAgo; 55 | if (this.settings.allowFuture) { 56 | if (distanceMillis < 0) { 57 | prefix = $l.prefixFromNow; 58 | suffix = $l.suffixFromNow; 59 | } 60 | distanceMillis = Math.abs(distanceMillis); 61 | } 62 | 63 | var seconds = distanceMillis / 1000; 64 | var minutes = seconds / 60; 65 | var hours = minutes / 60; 66 | var days = hours / 24; 67 | var years = days / 365; 68 | 69 | function substitute(stringOrFunction, number) { 70 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; 71 | var value = ($l.numbers && $l.numbers[number]) || number; 72 | return string.replace(/%d/i, value); 73 | } 74 | 75 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || 76 | seconds < 90 && substitute($l.minute, 1) || 77 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) || 78 | minutes < 90 && substitute($l.hour, 1) || 79 | hours < 24 && substitute($l.hours, Math.round(hours)) || 80 | hours < 48 && substitute($l.day, 1) || 81 | days < 30 && substitute($l.days, Math.floor(days)) || 82 | days < 60 && substitute($l.month, 1) || 83 | days < 365 && substitute($l.months, Math.floor(days / 30)) || 84 | years < 2 && substitute($l.year, 1) || 85 | substitute($l.years, Math.floor(years)); 86 | 87 | return $.trim([prefix, words, suffix].join(" ")); 88 | }, 89 | parse: function(iso8601) { 90 | var s = $.trim(iso8601); 91 | s = s.replace(/\.\d\d\d+/,""); // remove milliseconds 92 | s = s.replace(/-/,"/").replace(/-/,"/"); 93 | s = s.replace(/T/," ").replace(/Z/," UTC"); 94 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 95 | return new Date(s); 96 | }, 97 | datetime: function(elem) { 98 | // jQuery's `is()` doesn't play well with HTML5 in IE 99 | var isTime = $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); 100 | var iso8601 = isTime ? $(elem).attr("datetime") : $(elem).attr("title"); 101 | return $t.parse(iso8601); 102 | } 103 | }); 104 | 105 | $.fn.timeago = function() { 106 | var self = this; 107 | self.each(refresh); 108 | 109 | var $s = $t.settings; 110 | if ($s.refreshMillis > 0) { 111 | setInterval(function() { self.each(refresh); }, $s.refreshMillis); 112 | } 113 | return self; 114 | }; 115 | 116 | function refresh() { 117 | var data = prepareData(this); 118 | if (!isNaN(data.datetime)) { 119 | $(this).text(inWords(data.datetime)); 120 | } 121 | return this; 122 | } 123 | 124 | function prepareData(element) { 125 | element = $(element); 126 | if (!element.data("timeago")) { 127 | element.data("timeago", { datetime: $t.datetime(element) }); 128 | var text = $.trim(element.text()); 129 | if (text.length > 0) { 130 | element.attr("title", text); 131 | } 132 | } 133 | return element.data("timeago"); 134 | } 135 | 136 | function inWords(date) { 137 | return $t.inWords(distance(date)); 138 | } 139 | 140 | function distance(date) { 141 | return (new Date().getTime() - date.getTime()); 142 | } 143 | 144 | // fix for IE6 suckage 145 | document.createElement("abbr"); 146 | document.createElement("time"); 147 | }(jQuery)); 148 | -------------------------------------------------------------------------------- /public/css/960.css: -------------------------------------------------------------------------------- 1 | body{min-width:960px}.container_12,.container_16{margin-left:auto;margin-right:auto;width:960px}.grid_1,.grid_2,.grid_3,.grid_4,.grid_5,.grid_6,.grid_7,.grid_8,.grid_9,.grid_10,.grid_11,.grid_12,.grid_13,.grid_14,.grid_15,.grid_16{display:inline;float:left;margin-left:10px;margin-right:10px}.push_1,.pull_1,.push_2,.pull_2,.push_3,.pull_3,.push_4,.pull_4,.push_5,.pull_5,.push_6,.pull_6,.push_7,.pull_7,.push_8,.pull_8,.push_9,.pull_9,.push_10,.pull_10,.push_11,.pull_11,.push_12,.pull_12,.push_13,.pull_13,.push_14,.pull_14,.push_15,.pull_15{position:relative}.container_12 .grid_3,.container_16 .grid_4{width:220px}.container_12 .grid_6,.container_16 .grid_8{width:460px}.container_12 .grid_9,.container_16 .grid_12{width:700px}.container_12 .grid_12,.container_16 .grid_16{width:940px}.alpha{margin-left:0}.omega{margin-right:0}.container_12 .grid_1{width:60px}.container_12 .grid_2{width:140px}.container_12 .grid_4{width:300px}.container_12 .grid_5{width:380px}.container_12 .grid_7{width:540px}.container_12 .grid_8{width:620px}.container_12 .grid_10{width:780px}.container_12 .grid_11{width:860px}.container_16 .grid_1{width:40px}.container_16 .grid_2{width:100px}.container_16 .grid_3{width:160px}.container_16 .grid_5{width:280px}.container_16 .grid_6{width:340px}.container_16 .grid_7{width:400px}.container_16 .grid_9{width:520px}.container_16 .grid_10{width:580px}.container_16 .grid_11{width:640px}.container_16 .grid_13{width:760px}.container_16 .grid_14{width:820px}.container_16 .grid_15{width:880px}.container_12 .prefix_3,.container_16 .prefix_4{padding-left:240px}.container_12 .prefix_6,.container_16 .prefix_8{padding-left:480px}.container_12 .prefix_9,.container_16 .prefix_12{padding-left:720px}.container_12 .prefix_1{padding-left:80px}.container_12 .prefix_2{padding-left:160px}.container_12 .prefix_4{padding-left:320px}.container_12 .prefix_5{padding-left:400px}.container_12 .prefix_7{padding-left:560px}.container_12 .prefix_8{padding-left:640px}.container_12 .prefix_10{padding-left:800px}.container_12 .prefix_11{padding-left:880px}.container_16 .prefix_1{padding-left:60px}.container_16 .prefix_2{padding-left:120px}.container_16 .prefix_3{padding-left:180px}.container_16 .prefix_5{padding-left:300px}.container_16 .prefix_6{padding-left:360px}.container_16 .prefix_7{padding-left:420px}.container_16 .prefix_9{padding-left:540px}.container_16 .prefix_10{padding-left:600px}.container_16 .prefix_11{padding-left:660px}.container_16 .prefix_13{padding-left:780px}.container_16 .prefix_14{padding-left:840px}.container_16 .prefix_15{padding-left:900px}.container_12 .suffix_3,.container_16 .suffix_4{padding-right:240px}.container_12 .suffix_6,.container_16 .suffix_8{padding-right:480px}.container_12 .suffix_9,.container_16 .suffix_12{padding-right:720px}.container_12 .suffix_1{padding-right:80px}.container_12 .suffix_2{padding-right:160px}.container_12 .suffix_4{padding-right:320px}.container_12 .suffix_5{padding-right:400px}.container_12 .suffix_7{padding-right:560px}.container_12 .suffix_8{padding-right:640px}.container_12 .suffix_10{padding-right:800px}.container_12 .suffix_11{padding-right:880px}.container_16 .suffix_1{padding-right:60px}.container_16 .suffix_2{padding-right:120px}.container_16 .suffix_3{padding-right:180px}.container_16 .suffix_5{padding-right:300px}.container_16 .suffix_6{padding-right:360px}.container_16 .suffix_7{padding-right:420px}.container_16 .suffix_9{padding-right:540px}.container_16 .suffix_10{padding-right:600px}.container_16 .suffix_11{padding-right:660px}.container_16 .suffix_13{padding-right:780px}.container_16 .suffix_14{padding-right:840px}.container_16 .suffix_15{padding-right:900px}.container_12 .push_3,.container_16 .push_4{left:240px}.container_12 .push_6,.container_16 .push_8{left:480px}.container_12 .push_9,.container_16 .push_12{left:720px}.container_12 .push_1{left:80px}.container_12 .push_2{left:160px}.container_12 .push_4{left:320px}.container_12 .push_5{left:400px}.container_12 .push_7{left:560px}.container_12 .push_8{left:640px}.container_12 .push_10{left:800px}.container_12 .push_11{left:880px}.container_16 .push_1{left:60px}.container_16 .push_2{left:120px}.container_16 .push_3{left:180px}.container_16 .push_5{left:300px}.container_16 .push_6{left:360px}.container_16 .push_7{left:420px}.container_16 .push_9{left:540px}.container_16 .push_10{left:600px}.container_16 .push_11{left:660px}.container_16 .push_13{left:780px}.container_16 .push_14{left:840px}.container_16 .push_15{left:900px}.container_12 .pull_3,.container_16 .pull_4{left:-240px}.container_12 .pull_6,.container_16 .pull_8{left:-480px}.container_12 .pull_9,.container_16 .pull_12{left:-720px}.container_12 .pull_1{left:-80px}.container_12 .pull_2{left:-160px}.container_12 .pull_4{left:-320px}.container_12 .pull_5{left:-400px}.container_12 .pull_7{left:-560px}.container_12 .pull_8{left:-640px}.container_12 .pull_10{left:-800px}.container_12 .pull_11{left:-880px}.container_16 .pull_1{left:-60px}.container_16 .pull_2{left:-120px}.container_16 .pull_3{left:-180px}.container_16 .pull_5{left:-300px}.container_16 .pull_6{left:-360px}.container_16 .pull_7{left:-420px}.container_16 .pull_9{left:-540px}.container_16 .pull_10{left:-600px}.container_16 .pull_11{left:-660px}.container_16 .pull_13{left:-780px}.container_16 .pull_14{left:-840px}.container_16 .pull_15{left:-900px}.clear{clear:both;display:block;overflow:hidden;visibility:hidden;width:0;height:0}.clearfix:before,.clearfix:after,.container_12:before,.container_12:after,.container_16:before,.container_16:after{content:'.';display:block;overflow:hidden;visibility:hidden;font-size:0;line-height:0;width:0;height:0}.clearfix:after,.container_12:after,.container_16:after{clear:both}.clearfix,.container_12,.container_16{zoom:1} -------------------------------------------------------------------------------- /sass/bourbon/css3/_animation.scss: -------------------------------------------------------------------------------- 1 | // http://www.w3.org/TR/css3-animations/#the-animation-name-property- 2 | // Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties. 3 | 4 | @mixin animation-name ($name-1, 5 | $name-2: false, $name-3: false, 6 | $name-4: false, $name-5: false, 7 | $name-6: false, $name-7: false, 8 | $name-8: false, $name-9: false) 9 | { 10 | $full: compact($name-1, $name-2, $name-3, $name-4, 11 | $name-5, $name-6, $name-7, $name-8, $name-9); 12 | 13 | -webkit-animation-name: $full; 14 | -moz-animation-name: $full; 15 | animation-name: $full; 16 | } 17 | 18 | 19 | @mixin animation-duration ($time-1: 0, 20 | $time-2: false, $time-3: false, 21 | $time-4: false, $time-5: false, 22 | $time-6: false, $time-7: false, 23 | $time-8: false, $time-9: false) 24 | { 25 | $full: compact($time-1, $time-2, $time-3, $time-4, 26 | $time-5, $time-6, $time-7, $time-8, $time-9); 27 | 28 | -webkit-animation-duration: $full; 29 | -moz-animation-duration: $full; 30 | animation-duration: $full; 31 | } 32 | 33 | 34 | @mixin animation-timing-function ($motion-1: ease, 35 | // ease | linear | ease-in | ease-out | ease-in-out 36 | $motion-2: false, $motion-3: false, 37 | $motion-4: false, $motion-5: false, 38 | $motion-6: false, $motion-7: false, 39 | $motion-8: false, $motion-9: false) 40 | { 41 | $full: compact($motion-1, $motion-2, $motion-3, $motion-4, 42 | $motion-5, $motion-6, $motion-7, $motion-8, $motion-9); 43 | 44 | -webkit-animation-timing-function: $full; 45 | -moz-animation-timing-function: $full; 46 | animation-timing-function: $full; 47 | } 48 | 49 | 50 | @mixin animation-iteration-count ($value-1: 1, 51 | // infinite | <number> 52 | $value-2: false, $value-3: false, 53 | $value-4: false, $value-5: false, 54 | $value-6: false, $value-7: false, 55 | $value-8: false, $value-9: false) 56 | { 57 | $full: compact($value-1, $value-2, $value-3, $value-4, 58 | $value-5, $value-6, $value-7, $value-8, $value-9); 59 | 60 | -webkit-animation-iteration-count: $full; 61 | -moz-animation-iteration-count: $full; 62 | animation-iteration-count: $full; 63 | } 64 | 65 | 66 | @mixin animation-direction ($direction-1: normal, 67 | // normal | alternate 68 | $direction-2: false, $direction-3: false, 69 | $direction-4: false, $direction-5: false, 70 | $direction-6: false, $direction-7: false, 71 | $direction-8: false, $direction-9: false) 72 | { 73 | $full: compact($direction-1, $direction-2, $direction-3, $direction-4, 74 | $direction-5, $direction-6, $direction-7, $direction-8, $direction-9); 75 | 76 | -webkit-animation-direction: $full; 77 | -moz-animation-direction: $full; 78 | animation-direction: $full; 79 | } 80 | 81 | 82 | @mixin animation-play-state ($state-1: running, 83 | // running | paused 84 | $state-2: false, $state-3: false, 85 | $state-4: false, $state-5: false, 86 | $state-6: false, $state-7: false, 87 | $state-8: false, $state-9: false) 88 | { 89 | $full: compact($state-1, $state-2, $state-3, $state-4, 90 | $state-5, $state-6, $state-7, $state-8, $state-9); 91 | 92 | -webkit-animation-play-state: $full; 93 | -moz-animation-play-state: $full; 94 | animation-play-state: $full; 95 | } 96 | 97 | 98 | @mixin animation-delay ($time-1: 0, 99 | $time-2: false, $time-3: false, 100 | $time-4: false, $time-5: false, 101 | $time-6: false, $time-7: false, 102 | $time-8: false, $time-9: false) 103 | { 104 | $full: compact($time-1, $time-2, $time-3, $time-4, 105 | $time-5, $time-6, $time-7, $time-8, $time-9); 106 | 107 | -webkit-animation-delay: $full; 108 | -moz-animation-delay: $full; 109 | animation-delay: $full; 110 | } 111 | 112 | 113 | @mixin animation-fill-mode ($mode-1: none, 114 | // http://goo.gl/l6ckm 115 | // none | forwards | backwards | both 116 | $mode-2: false, $mode-3: false, 117 | $mode-4: false, $mode-5: false, 118 | $mode-6: false, $mode-7: false, 119 | $mode-8: false, $mode-9: false) 120 | { 121 | $full: compact($mode-1, $mode-2, $mode-3, $mode-4, 122 | $mode-5, $mode-6, $mode-7, $mode-8, $mode-9); 123 | 124 | -webkit-animation-fill-mode: $full; 125 | -moz-animation-fill-mode: $full; 126 | animation-fill-mode: $full; 127 | } 128 | 129 | 130 | // Shorthand for a basic animation. Supports multiple parentheses-deliminated values for each variable. 131 | // Example: @include animation-basic((slideup, fadein), (1.0s, 2.0s), ease-in); 132 | @mixin animation-basic ($name, $time: 0, $motion: ease) { 133 | $length-of-name: length($name); 134 | $length-of-time: length($time); 135 | $length-of-motion: length($motion); 136 | 137 | @if $length-of-name > 1 { 138 | @include animation-name(zip($name)); 139 | } @else { 140 | @include animation-name( $name); 141 | } 142 | 143 | @if $length-of-time > 1 { 144 | @include animation-duration(zip($time)); 145 | } @else { 146 | @include animation-duration( $time); 147 | } 148 | 149 | @if $length-of-motion > 1 { 150 | @include animation-timing-function(zip($motion)); 151 | } @else { 152 | @include animation-timing-function( $motion); 153 | } 154 | } 155 | 156 | // Official animation shorthand property. Needs more work to actually be useful. 157 | @mixin animation ($name, $duration, $timing-function, $delay, $iteration-count, $direction) { 158 | -webkit-animation: $name $duration $timing-function $delay $iteration-count $direction; 159 | -moz-animation: $name $duration $timing-function $delay $iteration-count $direction; 160 | animation: $name $duration $timing-function $delay $iteration-count $direction; 161 | } 162 | -------------------------------------------------------------------------------- /lib/models/wordcount.rb: -------------------------------------------------------------------------------- 1 | # wordcount.rb 2 | # 3 | # Model that tracks the number of times a word appears across all shows. 4 | 5 | require 'dm-core' 6 | require 'dm-validations' 7 | require 'dm-timestamps' 8 | require 'dm-aggregates' 9 | require 'stopwords' 10 | 11 | class WordCount 12 | include DataMapper::Resource 13 | 14 | property :id, Serial 15 | property :word, String, :unique => true, :required => true 16 | property :frequency, Integer, :default => 0 17 | 18 | def add_one 19 | self.frequency = self.frequency + 1 20 | self.save 21 | end 22 | 23 | def idf 24 | WordCount.count_document_frequency if IdfTracker.count == 0 25 | 26 | Math.log( IdfTracker.first.document_count.to_f / self.frequency ) 27 | end 28 | 29 | # ------------------ 30 | # Class Methods 31 | # ------------------ 32 | 33 | def self.count_document_frequency 34 | if IdfTracker.count == 0 35 | suggestion_sets = Suggestion.all(:order => [:created_at.desc]).group_by_show 36 | IdfTracker.create( last_suggestion_time: Suggestion.last.created_at, last_suggestion_show: Suggestion.last.show ) 37 | IdfTracker.first.update(document_count: suggestion_sets.count) 38 | else 39 | # get shows since last check - make sure that the first titles aren't from the same show as the last one counted 40 | from = IdfTracker.first.last_suggestion_time 41 | suggestion_sets = Suggestion.all(:created_at.gt => from, :order => [:created_at.desc]).group_by_show 42 | return if suggestion_sets.count == 0 43 | 44 | first_new_suggestion = Suggestion.first(:created_at.gt => from) 45 | if IdfTracker.first.last_suggestion_show == first_new_suggestion.show and 46 | (IdfTracker.first.last_suggestion_time - first_new_suggestion.created_at) > 0.75 47 | IdfTracker.first.update(document_count: IdfTracker.first.document_count + suggestion_sets.count - 1, # already counted first episode of new set 48 | last_suggestion_show: Suggestion.last(:created_at.gt => from).show, last_suggestion_time: Suggestion.last(:created_at.gt => from).created_at) 49 | else 50 | IdfTracker.first.update(document_count: IdfTracker.first.document_count + suggestion_sets.count, 51 | last_suggestion_show: Suggestion.last(:created_at.gt => from).show, last_suggestion_time: Suggestion.last(:created_at.gt => from).created_at) 52 | end 53 | end 54 | 55 | my_stop_words = WordCount.stop_words 56 | 57 | suggestion_sets.each do |set| 58 | word_list = [] 59 | set.suggestions.each { |suggestion| word_list += tokenize(suggestion.title.downcase) } 60 | word_list = word_list.uniq - my_stop_words 61 | word_list.each { |word| WordCount.first_or_create(:word => word).add_one } 62 | end 63 | end 64 | 65 | def self.stop_words 66 | sw = Stopwords::STOP_WORDS 67 | sw.push("'s", "n't", "'ll", "'re", "'d", "'ve", "'m", ".", "!", "?", ",", "*", "...", "(", ")", "&", "``", "''", ":") 68 | end 69 | 70 | def self.generate_clouds(suggestion_sets) 71 | WordCount.count_document_frequency 72 | 73 | all_clouds_data = Array.new 74 | stop_words = WordCount.stop_words 75 | 76 | suggestion_sets.each do |set| 77 | set_counts = Hash.new(0) 78 | set_tfidf = Hash.new(0) 79 | 80 | set.suggestions.each do |suggestion| 81 | words = tokenize(suggestion.title.downcase) - stop_words 82 | words.each { |w| set_counts[w] += 1 } 83 | end 84 | 85 | set_counts.each { |w, tf| set_tfidf[w] = tf * WordCount.first(word: w).idf } 86 | 87 | max_val = set_tfidf.values.max 88 | this_cloud_data = Array.new 89 | set_tfidf.sort_by {|k, v| -v}.each { |k, v| this_cloud_data.push( {key: k, value: v / max_val} ) } 90 | all_clouds_data.push( {show: set.suggestions.first.show, time: set.suggestions.first.created_at, data: this_cloud_data[0..249]} ) 91 | end 92 | 93 | all_clouds_data 94 | end 95 | end 96 | 97 | def tokenize(str) 98 | # Normalize all whitespace 99 | str.gsub!(/\s+/, ' ') 100 | 101 | # Fix curly quotes 102 | trans = { "\u2018" => "`", 103 | "\u2019" => "'", 104 | "\u201c" => "``", 105 | "\u201d" => "''", 106 | } 107 | trans_re = trans.keys.join('') 108 | str.gsub!(/([#{trans_re}])/) { " " + trans[$1] + " " } 109 | 110 | # Attempt to get correct directional quotes 111 | str.gsub!(/\"\b/, ' `` ') 112 | str.gsub!(/\b\"/, " '' ") 113 | str.gsub!(/\"(?=\s)/, " '' ") 114 | str.gsub!(/\"/, ' `` ') 115 | 116 | # Isolate all ellipses 117 | str.gsub!(/\.\.\./, ' ... ') 118 | 119 | # Isolate any embedded punctuation chars 120 | str.gsub!(/([,;:\@\#\$\%&])/, ' \1 ') 121 | 122 | # Assume sentence tokenization has been done first, so split FINAL periods only 123 | str.gsub!(/ ([^.]) \. ([\]\)\}\>\"\']*) [ \t]* $ /x, '\1 .\2') 124 | 125 | # hHowever, we may as well split ALL question marks and exclamation points, 126 | # since they shouldn't have the abbrev-marker ambiguity problem 127 | str.gsub!(/([?!])/, ' \1 ') 128 | 129 | # parentheses, brackets, etc. 130 | str.gsub!(/([\]\[\(\)\{\}\<\>])/, ' \1 ') 131 | 132 | str.gsub!(/(-{2,})/, ' \1 ') 133 | 134 | # Add a space to the beginning and end of each line, to reduce 135 | # necessary number of regexps below 136 | 137 | str.gsub!(/$/, ' '); 138 | str.gsub!(/^/, ' '); 139 | 140 | # possessive or close-single-quote 141 | str.gsub!(/\([^\']\)\' /, '\1 \' ') 142 | 143 | # as in it's, I'm, we'd 144 | str.gsub!(/\'([smd]) /i, ' \'\1 ') 145 | 146 | str.gsub!(/\'(ll|re|ve) /i, ' \'\1 ') 147 | str.gsub!(/n\'t /i, ' n\'t ') 148 | 149 | str.gsub!(/ (can)(not) /i, ' \1 \2 ') 150 | str.gsub!(/ (d\')(ye) /i, ' \1 \2 ') 151 | str.gsub!(/ (gim)(me) /i, ' \1 \2 ') 152 | str.gsub!(/ (gon)(na) /i, ' \1 \2 ') 153 | str.gsub!(/ (got)(ta) /i, ' \1 \2 ') 154 | str.gsub!(/ (lem)(me) /i, ' \1 \2 ') 155 | str.gsub!(/ (more)(\'n) /i, ' \1 \2 ') 156 | str.gsub!(/ (\'t)(is|was) /i, ' \1 \2 ') 157 | str.gsub!(/ (wan)(na) /i, ' \1 \2 ') 158 | 159 | str.split(' ') 160 | end 161 | -------------------------------------------------------------------------------- /public/js/modernizr.custom.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.0.6 (Custom Build) | MIT & BSD 2 | * Contains: touch | iepp | cssclasses | teststyles | prefixes | load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function z(a,b){return!!~(""+a).indexOf(b)}function y(a,b){return typeof a===b}function x(a,b){return w(n.join(a+";")+(b||""))}function w(a){k.cssText=a}var d="2.0.6",e={},f=!0,g=b.documentElement,h=b.head||b.getElementsByTagName("head")[0],i="modernizr",j=b.createElement(i),k=j.style,l,m=Object.prototype.toString,n=" -webkit- -moz- -o- -ms- -khtml- ".split(" "),o={},p={},q={},r=[],s=function(a,c,d,e){var f,h,j,k=b.createElement("div");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:i+(d+1),k.appendChild(j);f=["­","<style>",a,"</style>"].join(""),k.id=i,k.innerHTML+=f,g.appendChild(k),h=c(k,a),k.parentNode.removeChild(k);return!!h},t,u={}.hasOwnProperty,v;!y(u,c)&&!y(u.call,c)?v=function(a,b){return u.call(a,b)}:v=function(a,b){return b in a&&y(a.constructor.prototype[b],c)};var A=function(c,d){var f=c.join(""),g=d.length;s(f,function(c,d){var f=b.styleSheets[b.styleSheets.length-1],h=f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"",i=c.childNodes,j={};while(g--)j[i[g].id]=i[g];e.touch="ontouchstart"in a||j.touch.offsetTop===9},g,d)}([,["@media (",n.join("touch-enabled),("),i,")","{#touch{top:9px;position:absolute}}"].join("")],[,"touch"]);o.touch=function(){return e.touch};for(var B in o)v(o,B)&&(t=B.toLowerCase(),e[t]=o[B](),r.push((e[t]?"":"no-")+t));w(""),j=l=null,a.attachEvent&&function(){var a=b.createElement("div");a.innerHTML="<elem></elem>";return a.childNodes.length!==1}()&&function(a,b){function s(a){var b=-1;while(++b<g)a.createElement(f[b])}a.iepp=a.iepp||{};var d=a.iepp,e=d.html5elements||"abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",f=e.split("|"),g=f.length,h=new RegExp("(^|\\s)("+e+")","gi"),i=new RegExp("<(/*)("+e+")","gi"),j=/^\s*[\{\}]\s*$/,k=new RegExp("(^|[^\\n]*?\\s)("+e+")([^\\n]*)({[\\n\\w\\W]*?})","gi"),l=b.createDocumentFragment(),m=b.documentElement,n=m.firstChild,o=b.createElement("body"),p=b.createElement("style"),q=/print|all/,r;d.getCSS=function(a,b){if(a+""===c)return"";var e=-1,f=a.length,g,h=[];while(++e<f){g=a[e];if(g.disabled)continue;b=g.media||b,q.test(b)&&h.push(d.getCSS(g.imports,b),g.cssText),b="all"}return h.join("")},d.parseCSS=function(a){var b=[],c;while((c=k.exec(a))!=null)b.push(((j.exec(c[1])?"\n":c[1])+c[2]+c[3]).replace(h,"$1.iepp_$2")+c[4]);return b.join("\n")},d.writeHTML=function(){var a=-1;r=r||b.body;while(++a<g){var c=b.getElementsByTagName(f[a]),d=c.length,e=-1;while(++e<d)c[e].className.indexOf("iepp_")<0&&(c[e].className+=" iepp_"+f[a])}l.appendChild(r),m.appendChild(o),o.className=r.className,o.id=r.id,o.innerHTML=r.innerHTML.replace(i,"<$1font")},d._beforePrint=function(){p.styleSheet.cssText=d.parseCSS(d.getCSS(b.styleSheets,"all")),d.writeHTML()},d.restoreHTML=function(){o.innerHTML="",m.removeChild(o),m.appendChild(r)},d._afterPrint=function(){d.restoreHTML(),p.styleSheet.cssText=""},s(b),s(l);d.disablePP||(n.insertBefore(p,n.firstChild),p.media="print",p.className="iepp-printshim",a.attachEvent("onbeforeprint",d._beforePrint),a.attachEvent("onafterprint",d._afterPrint))}(a,b),e._version=d,e._prefixes=n,e.testStyles=s,g.className=g.className.replace(/\bno-js\b/,"")+(f?" js "+r.join(" "):"");return e}(this,this.document),function(a,b,c){function k(a){return!a||a=="loaded"||a=="complete"}function j(){var a=1,b=-1;while(p.length- ++b)if(p[b].s&&!(a=p[b].r))break;a&&g()}function i(a){var c=b.createElement("script"),d;c.src=a.s,c.onreadystatechange=c.onload=function(){!d&&k(c.readyState)&&(d=1,j(),c.onload=c.onreadystatechange=null)},m(function(){d||(d=1,j())},H.errorTimeout),a.e?c.onload():n.parentNode.insertBefore(c,n)}function h(a){var c=b.createElement("link"),d;c.href=a.s,c.rel="stylesheet",c.type="text/css";if(!a.e&&(w||r)){var e=function(a){m(function(){if(!d)try{a.sheet.cssRules.length?(d=1,j()):e(a)}catch(b){b.code==1e3||b.message=="security"||b.message=="denied"?(d=1,m(function(){j()},0)):e(a)}},0)};e(c)}else c.onload=function(){d||(d=1,m(function(){j()},0))},a.e&&c.onload();m(function(){d||(d=1,j())},H.errorTimeout),!a.e&&n.parentNode.insertBefore(c,n)}function g(){var a=p.shift();q=1,a?a.t?m(function(){a.t=="c"?h(a):i(a)},0):(a(),j()):q=0}function f(a,c,d,e,f,h){function i(){!o&&k(l.readyState)&&(r.r=o=1,!q&&j(),l.onload=l.onreadystatechange=null,m(function(){u.removeChild(l)},0))}var l=b.createElement(a),o=0,r={t:d,s:c,e:h};l.src=l.data=c,!s&&(l.style.display="none"),l.width=l.height="0",a!="object"&&(l.type=d),l.onload=l.onreadystatechange=i,a=="img"?l.onerror=i:a=="script"&&(l.onerror=function(){r.e=r.r=1,g()}),p.splice(e,0,r),u.insertBefore(l,s?null:n),m(function(){o||(u.removeChild(l),r.r=r.e=o=1,j())},H.errorTimeout)}function e(a,b,c){var d=b=="c"?z:y;q=0,b=b||"j",C(a)?f(d,a,b,this.i++,l,c):(p.splice(this.i++,0,a),p.length==1&&g());return this}function d(){var a=H;a.loader={load:e,i:0};return a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=r&&!s,u=s?l:n.parentNode,v=a.opera&&o.call(a.opera)=="[object Opera]",w="webkitAppearance"in l.style,x=w&&"async"in b.createElement("script"),y=r?"object":v||x?"img":"script",z=w?"img":y,A=Array.isArray||function(a){return o.call(a)=="[object Array]"},B=function(a){return Object(a)===a},C=function(a){return typeof a=="string"},D=function(a){return o.call(a)=="[object Function]"},E=[],F={},G,H;H=function(a){function f(a){var b=a.split("!"),c=E.length,d=b.pop(),e=b.length,f={url:d,origUrl:d,prefixes:b},g,h;for(h=0;h<e;h++)g=F[b[h]],g&&(f=g(f));for(h=0;h<c;h++)f=E[h](f);return f}function e(a,b,e,g,h){var i=f(a),j=i.autoCallback;if(!i.bypass){b&&(b=D(b)?b:b[a]||b[g]||b[a.split("/").pop().split("?")[0]]);if(i.instead)return i.instead(a,b,e,g,h);e.load(i.url,i.forceCSS||!i.forceJS&&/css$/.test(i.url)?"c":c,i.noexec),(D(b)||D(j))&&e.load(function(){d(),b&&b(i.origUrl,h,g),j&&j(i.origUrl,h,g)})}}function b(a,b){function c(a){if(C(a))e(a,h,b,0,d);else if(B(a))for(i in a)a.hasOwnProperty(i)&&e(a[i],h,b,i,d)}var d=!!a.test,f=d?a.yep:a.nope,g=a.load||a.both,h=a.callback,i;c(f),c(g),a.complete&&b.load(a.complete)}var g,h,i=this.yepnope.loader;if(C(a))e(a,0,i,0);else if(A(a))for(g=0;g<a.length;g++)h=a[g],C(h)?e(h,0,i,0):A(h)?H(h):B(h)&&b(h,i);else B(a)&&b(a,i)},H.addPrefix=function(a,b){F[a]=b},H.addFilter=function(a){E.push(a)},H.errorTimeout=1e4,b.readyState==null&&b.addEventListener&&(b.readyState="loading",b.addEventListener("DOMContentLoaded",G=function(){b.removeEventListener("DOMContentLoaded",G,0),b.readyState="complete"},0)),a.yepnope=d()}(this,this.document),Modernizr.load=function(){yepnope.apply(window,[].slice.call(arguments,0))}; -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Showbot 2 | 3 | **Dead as of 2013-10-16, the main project is now located at [rikai/Showbot](https://github.com/rikai/Showbot)** 4 | 5 | A sweet IRC bot with a **web interface** for [5by5](http://5by5.tv). 6 | Built on [cinch](https://github.com/cinchrb/cinch) and [sinatra](http://www.sinatrarb.com/). 7 | 8 | [The Creation of Showbot](http://pileofturtles.com/2011/07/showbot/) 9 | 10 | ## IRC Commands 11 | 12 | * !next - When's the next live show? 13 | * !schedule - What shows are being recorded live in the next seven days? 14 | * !suggest - Be heard. Suggest a title for the live show. 15 | * !link - Know the link for that? Suggest it and make the show better. 16 | * !current - What's playing on 5by5.tv/live? I've got you covered. 17 | * !last_status - The last tweet by @5by5, delivered to you in IRC. Sweet. 18 | * !about - Was showbot coded or did it spontaneously come into existence? 19 | * !help - Uh, this. 20 | 21 | ## Setup and Customization 22 | 23 | ### Prerequisites 24 | 25 | * [RVM with Ruby 1.9.2 or Greater](https://rvm.io/) 26 | * [Bundler](http://gembundler.com/) 27 | 28 | ### Setup 29 | 30 | These commands will get you setup to run Showbot. 31 | 32 | * `git clone https://github.com/mutewinter/Showbot.git` 33 | * `cd Showbot` 34 | * `bundle` 35 | * `foreman run rake db:migrate` 36 | 37 | Finally you need to setup your `.env` file in the root of the project. At the 38 | bare minimum you'll need the following: 39 | 40 | ``` 41 | # Bot lib folder 42 | RUBYLIB=./lib 43 | 44 | # Set this if you want to use a language other than english. 45 | # You will also need to create a corresponding .yml file in the locales folder. 46 | SHOWBOT_LOCALE=en 47 | 48 | # Foreman stuff 49 | ## Production port 50 | PORT=80 51 | ## Development port 52 | DEVELOPMENT_PORT=5000 53 | 54 | # Point this to the url of data.json, if you have one 55 | SHOWBOT_DATABASE_URL=your_info_here 56 | 57 | # For backup.rb 58 | BOT_DATABASE_NAME=your_info_here 59 | BOT_DATABASE_USER=your_info_here 60 | BOT_DATABASE_PASSWORD=your_info_here 61 | BOT_DATABASE_HOST=your_info_here 62 | BOT_DATABASE_PORT=your_info_here 63 | BOT_DATABASE_OPTS=your_info_here 64 | S3_ACCESS_KEY_ID=your_info_here 65 | S3_SECRET_ACCESS_KEY=your_info_here 66 | S3_REIGON=your_info_here 67 | S3_BUCKET=your_info_here 68 | S3_PATH=your_info_here 69 | S3_KEEP=your_info_here 70 | 71 | # Point this to the url of data.json, if you have one 72 | DATA_JSON_URL=your_info_here 73 | ``` 74 | 75 | ### Configuring IRC 76 | 77 | * Customize [`cinchize.yml`][cinchize] for your IRC channel. 78 | * Update [`fix_name`][fix_name] to match your bot's name. 79 | 80 | [cinchize]: https://github.com/mutewinter/Showbot/blob/master/cinchize.yml 81 | [fix_name]: https://github.com/mutewinter/Showbot/blob/master/lib/cinch/plugins/showbot_admin.rb#L54 82 | 83 | ### data.json 84 | 85 | `data.json` is a file served specifically by 5by5.tv that indicates which 86 | show is live. It is currently integrated with title suggestions so that the 87 | currently playing show is fetched and saved in the database when a title is 88 | suggested. 89 | 90 | If you want to remove the functionality provided by `data.json`, you will need 91 | to start by removing the [before create hook][hook] in `suggestion.rb`. To avoid 92 | the "Show Not Listed" message you'll want to remove the suggestion set break in 93 | [`_table_set.haml`][table_set] and [`_bubble_set.haml`][bubble_set]. 94 | 95 | [hook]: https://github.com/mutewinter/Showbot/blob/master/lib/models/suggestion.rb#L45 96 | [table_set]: https://github.com/mutewinter/Showbot/blob/master/views/suggestion/_table_set.haml#L4 97 | [bubble_set]: https://github.com/mutewinter/Showbot/blob/master/views/suggestion/_bubble_set.haml#L3 98 | 99 | ### Modifying the CSS 100 | 101 | Modifying [`showbot.scss`][showbot_scss] requires that you start the `rake sass:watch` 102 | command. While this command is running, `public/showbot.css` will be 103 | overwritten with any changes that are made in `showbot.scss`. This annoying 104 | setup is necessary due to [Bourbon](https://github.com/thoughtbot/bourbon) not 105 | working well outside of the Rails asset pipeline. 106 | 107 | [showbot_scss]: https://github.com/mutewinter/Showbot/blob/master/sass/showbot.scss 108 | 109 | ### Data.json 110 | 111 | 5by5.tv uses a JSON API to provide details about which show is live. When a 112 | title is suggested, this JSON API is queried and the show that the title was 113 | suggested during is attached to the suggestion. 114 | 115 | The format of 5by5.tv's JSON is as follows: 116 | 117 | ``` 118 | { 119 | 'live': true, 120 | 'broadcast': { 121 | 'slug': 'show_slug_here' 122 | } 123 | } 124 | ``` 125 | 126 | The `'show_slug_here'` is the value read and attached to the title suggestion. 127 | 128 | ### Launching Showbot 129 | 130 | **Website and the IRC Bot** 131 | 132 | ``` 133 | $ bundle exec foreman start -f Procfile.local 134 | ``` 135 | 136 | **Just the Website** 137 | 138 | ``` 139 | $ bundle exec foreman start web -f Procfile.local 140 | ``` 141 | 142 | **Just the IRC Bot** 143 | 144 | ``` 145 | $ bundle exec foreman start irc -f Procfile.local 146 | ``` 147 | 148 | ## Special Thanks 149 | 150 | * Special thanks to Rikai for reverse-engineering the setup steps for someone 151 | setting up Showbot from scratch. 152 | * To [gouwens](https://github.com/gouwens) for implementing the clustered 153 | view. 154 | 155 | ## License 156 | 157 | The MIT License 158 | 159 | Copyright (c) 2011 Jeremy Mack, Pile of Turtles, LLC 160 | 161 | Permission is hereby granted, free of charge, to any person obtaining a copy 162 | of this software and associated documentation files (the "Software"), to deal 163 | in the Software without restriction, including without limitation the rights 164 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 165 | copies of the Software, and to permit persons to whom the Software is 166 | furnished to do so, subject to the following conditions: 167 | 168 | The above copyright notice and this permission notice shall be included in 169 | all copies or substantial portions of the Software. 170 | 171 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 172 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 173 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 174 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 175 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 176 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 177 | THE SOFTWARE. 178 | 179 | _sekret analytics_ ![shhh](https://d2weczhvl823v0.cloudfront.net/mutewinter/Showbot/trend.png) 180 | -------------------------------------------------------------------------------- /lib/models/suggestion.rb: -------------------------------------------------------------------------------- 1 | # suggestion.rb 2 | # 3 | # Model that contains a title suggestion. These are created from the IRC chat 4 | # bot via !suggest 5 | 6 | require 'open-uri' 7 | require 'json' 8 | 9 | require 'dm-core' 10 | require 'dm-validations' 11 | require 'dm-timestamps' 12 | require 'dm-aggregates' 13 | 14 | class Suggestion 15 | include DataMapper::Resource 16 | 17 | property :id, Serial 18 | property :title, String, :length => 40, 19 | :message => I18n.t('messages.models.suggestion') 20 | property :user, String 21 | property :show, String 22 | property :created_at, DateTime, index: true 23 | property :updated_at, DateTime 24 | 25 | validates_presence_of :title 26 | validates_with_method :title, :check_title_uniqueness, :if => :new? 27 | 28 | # Assocations 29 | has n, :votes 30 | belongs_to :cluster, :required => false 31 | 32 | # ------------------ 33 | # Before Save 34 | # ------------------ 35 | 36 | before :save, :fix_title 37 | 38 | # Remove quotes from the title before saving 39 | def fix_title 40 | # Remove quotes if the user thought they needed them 41 | self.title = self.title.gsub(/^(?:'|")(.*)(?:'|")$/, '\1') 42 | end 43 | 44 | before :create, :check_cluster 45 | 46 | def check_cluster 47 | Suggestion.minutes_ago(30).each do |suggestion| 48 | if suggestion.id != self.id and lev_sim(suggestion) > 0.7 49 | if suggestion.in_cluster? 50 | suggestion.cluster.suggestions << self 51 | self.cluster = suggestion.cluster 52 | suggestion.cluster.save 53 | else 54 | cluster = Cluster.create 55 | cluster.suggestions << suggestion 56 | cluster.suggestions << self 57 | self.cluster = cluster 58 | cluster.save 59 | end 60 | return true 61 | end 62 | end 63 | true 64 | end 65 | 66 | before :create, :set_live_show 67 | 68 | def set_live_show 69 | # Only fetch show from website if it wasn't set previously. 70 | if !self.show 71 | self.show = Shows.fetch_live_show_slug 72 | end 73 | 74 | true 75 | end 76 | 77 | # after :save, :debug_cluster_id 78 | 79 | # def debug_cluster_id 80 | # $stderr.puts "After save, #{self.title}'s cluster_id is #{self.cluster_id}" 81 | # end 82 | 83 | # ------------------ 84 | # Validations 85 | # ------------------ 86 | 87 | # Verifies that title hasn't been entered in the last 30 minutes 88 | def check_title_uniqueness 89 | if self.title 90 | Suggestion.minutes_ago(30).each do |suggestion| 91 | if suggestion.title.downcase == self.title.downcase 92 | return [false, "Darn, #{suggestion.user} beat you to \"#{suggestion.title}\"."] 93 | end 94 | end 95 | else 96 | return true 97 | end 98 | return true 99 | end 100 | 101 | # ------------------ 102 | # Class Methods 103 | # ------------------ 104 | 105 | def self.recent(days_ago = 1) 106 | from = DateTime.now - days_ago 107 | all(:created_at.gt => from).all(:order => [:created_at.desc]) 108 | end 109 | 110 | def self.minutes_ago(minutes) 111 | if minutes 112 | time_ago = Time.now - (60 * minutes) 113 | all(:created_at.gt => time_ago).all(:order => [:created_at.desc]) 114 | end 115 | end 116 | 117 | # Group suggestions by show slug 118 | # 119 | # Returns an array of SuggestionSets (see the bottom of this file) 120 | def self.group_by_show 121 | suggestion_sets = [] 122 | last_show = nil 123 | last_time = nil 124 | split_interval = 0.75 # 18 hours - for creating a new set if same show runs 2 days in a row without another in between 125 | 126 | all.each do |suggestion| 127 | if suggestion_sets.empty? or last_show != suggestion.show or (last_time - suggestion.created_at) > split_interval 128 | suggestion_sets << SuggestionSet.new(suggestion.show) 129 | end 130 | 131 | last_show = suggestion.show 132 | last_time = suggestion.created_at 133 | 134 | # Always add the show to the last group 135 | suggestion_sets.last.add suggestion 136 | end 137 | 138 | suggestion_sets 139 | end 140 | 141 | # ------------------ 142 | # Helper Methods 143 | # ------------------ 144 | 145 | # Voting 146 | 147 | # Add a vote to the votes assocation for the user's IP 148 | # 149 | # Returns true if successful and false if the user has already voted. 150 | def vote_up(user_ip) 151 | if user_already_voted?(user_ip) 152 | false 153 | else 154 | if self.votes.create(:user_ip => user_ip) 155 | true 156 | else 157 | false 158 | end 159 | end 160 | end 161 | 162 | # Determine if a user has already voted on this suggestion from this IP address. 163 | # 164 | # Returns true if user has not voted on this suggestion. 165 | def user_already_voted?(user_ip) 166 | self.votes.all(:user_ip => user_ip).count > 0 167 | end 168 | 169 | def to_s 170 | "#{self.title} by #{self.user}" 171 | end 172 | 173 | def update_votes_counter_cache 174 | vote_count = self.votes.count 175 | if vote_count != self.votes_counter 176 | puts "Fixing missmatched count (#{vote_count}/#{self.votes_counter} for #{self})" 177 | self.update(:votes_counter => self.votes.count) 178 | end 179 | end 180 | 181 | # Clustering 182 | 183 | def lev_sim(other_suggestion) 184 | distance = levenshtein(self.title.downcase, 185 | other_suggestion.title.downcase) 186 | 187 | 1.0 - distance.to_f / [self.title.length, other_suggestion.title.length].max 188 | end 189 | 190 | def in_cluster? 191 | !!self.cluster_id 192 | end 193 | 194 | def top_of_cluster? 195 | if self.in_cluster? 196 | self.id == self.cluster.top_suggestion.id 197 | else 198 | true # would be the top if it were in a cluster by itself 199 | end 200 | end 201 | 202 | def total_for_cluster 203 | self.in_cluster? ? self.cluster.total_votes : self.votes.count 204 | end 205 | 206 | end 207 | 208 | 209 | # Class to hold sets of suggestions for the group_by_show method of Suggestion 210 | class SuggestionSet 211 | def initialize(slug = nil) 212 | @slug = slug 213 | @suggestions = [] 214 | super 215 | end 216 | 217 | def add(show) 218 | @suggestions << show 219 | end 220 | 221 | def to_s 222 | string = "" 223 | string << "Show #@slug\n" 224 | string << " Titles:\n" 225 | string << @suggestions.map{|s| " #{s.title}"}.join("\n") 226 | string 227 | end 228 | 229 | attr_accessor :slug, :suggestions 230 | end 231 | 232 | # https://github.com/threedaymonk/text/blob/master/lib/text/levenshtein.rb 233 | def levenshtein(str1, str2) 234 | ar1 = str1.split(//) 235 | ar2 = str2.split(//) 236 | 237 | d = (0..ar2.length).to_a 238 | x = nil 239 | 240 | ar1.length.times do |i| 241 | e = i + 1 242 | ar2.length.times do |j| 243 | cost = (ar1[i] == ar2[j]) ? 0 : 1; 244 | x = [ d[j+1] + 1, e + 1, d[j] + cost].min 245 | d[j] = e 246 | e = x 247 | end 248 | d[ar2.length] = x 249 | end 250 | 251 | x 252 | end 253 | -------------------------------------------------------------------------------- /sass/bourbon/addons/_button.scss: -------------------------------------------------------------------------------- 1 | @mixin button ($style: simple, $base-color: #4294f0) { 2 | 3 | @if type-of($style) == color { 4 | $base-color: $style; 5 | $style: simple; 6 | } 7 | 8 | @if $style == simple { 9 | @include simple($base-color); 10 | } 11 | 12 | @else if $style == shiny { 13 | @include shiny($base-color); 14 | } 15 | 16 | @else if $style == pill { 17 | @include pill($base-color); 18 | } 19 | } 20 | 21 | @mixin simple ($base-color) { 22 | $stop-gradient: adjust-color($base-color, $saturation: 9%, $lightness: -11%); 23 | $border: adjust-color($base-color, $saturation: 9%, $lightness: -14%); 24 | $color: hsl(0, 0, 100%); 25 | $inset-shadow: adjust-color($base-color, $saturation: -8%, $lightness: 15%); 26 | $text-shadow: adjust-color($base-color, $saturation: 15%, $lightness: -18%); 27 | 28 | @if lightness($base-color) > 70% { 29 | $color: hsl(0, 0, 20%); 30 | $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); 31 | } 32 | 33 | border: 1px solid $border; 34 | @include border-radius (3px); 35 | @include box-shadow (inset 0 1px 0 0 $inset-shadow); 36 | color: $color; 37 | display: inline; 38 | font-size: 11px; 39 | font-weight: bold; 40 | @include linear-gradient ($base-color, $stop-gradient); 41 | padding: 6px 18px 7px; 42 | text-shadow: 0 1px 0 $text-shadow; 43 | -webkit-background-clip: padding-box; 44 | 45 | &:hover { 46 | $base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%); 47 | $stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%); 48 | $inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%); 49 | 50 | @include box-shadow (inset 0 1px 0 0 $inset-shadow-hover); 51 | cursor: pointer; 52 | @include linear-gradient ($base-color-hover, $stop-gradient-hover); 53 | } 54 | 55 | &:active { 56 | $border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%); 57 | $inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%); 58 | 59 | border: 1px solid $border-active; 60 | @include box-shadow (inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee); 61 | } 62 | } 63 | 64 | @mixin shiny($base-color) { 65 | $second-stop: adjust-color($base-color, $red: -56, $green: -50, $blue: -33); 66 | $third-stop: adjust-color($base-color, $red: -86, $green: -75, $blue: -48); 67 | $fourth-stop: adjust-color($base-color, $red: -79, $green: -70, $blue: -46); 68 | $border: adjust-color($base-color, $red: -117, $green: -111, $blue: -81); 69 | $border-bottom: adjust-color($base-color, $red: -126, $green: -127, $blue: -122); 70 | $color: hsl(0, 0, 100%); 71 | $inset-shadow: adjust-color($base-color, $red: 37, $green: 29, $blue: 12); 72 | $text-shadow: adjust-color($base-color, $red: -140, $green: -141, $blue: -114); 73 | 74 | @if lightness($base-color) > 70% { 75 | $color: hsl(0, 0, 20%); 76 | $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); 77 | } 78 | 79 | @include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%); 80 | border: 1px solid $border; 81 | border-bottom: 1px solid $border-bottom; 82 | @include border-radius(5px); 83 | @include box-shadow(inset 0 1px 0 0 $inset-shadow); 84 | color: $color; 85 | display: inline; 86 | font-size: 14px; 87 | font-weight: bold; 88 | padding: 7px 20px 8px; 89 | text-decoration: none; 90 | text-align: center; 91 | text-shadow: 0 -1px 1px $text-shadow; 92 | 93 | &:hover { 94 | $first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18); 95 | $second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51); 96 | $third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66); 97 | $fourth-stop-hover: adjust-color($base-color, $red: -86, $green: -80, $blue: -63); 98 | 99 | @include linear-gradient(top, $first-stop-hover 0%, $second-stop-hover 50%, $third-stop-hover 50%, $fourth-stop-hover 100%); 100 | cursor: pointer; 101 | } 102 | 103 | &:active { 104 | $inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122); 105 | 106 | @include box-shadow(inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff); 107 | } 108 | } 109 | 110 | @mixin pill($base-color) { 111 | $stop-gradient: adjust-color($base-color, $hue: 8, $saturation: 14%, $lightness: -10%); 112 | $border-top: adjust-color($base-color, $hue: -1, $saturation: -30%, $lightness: -15%); 113 | $border-sides: adjust-color($base-color, $hue: 4, $saturation: -21%, $lightness: -21%); 114 | $border-bottom: adjust-color($base-color, $hue: 8, $saturation: -11%, $lightness: -26%); 115 | $color: hsl(0, 0, 100%); 116 | $inset-shadow: adjust-color($base-color, $hue: -1, $saturation: -1%, $lightness: 7%); 117 | $text-shadow: adjust-color($base-color, $hue: 5, $saturation: -19%, $lightness: -15%); 118 | 119 | @if lightness($base-color) > 70% { 120 | $color: hsl(0, 0, 20%); 121 | $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); 122 | } 123 | 124 | @include linear-gradient ($base-color, $stop-gradient); 125 | border: 1px solid $border-top; 126 | border-color: $border-top $border-sides $border-bottom; 127 | @include border-radius(16px); 128 | @include box-shadow(inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3); 129 | color: $color; 130 | display: inline; 131 | font-size: 11px; 132 | font-weight: normal; 133 | line-height: 1; 134 | padding: 3px 16px 5px; 135 | text-align: center; 136 | text-shadow: 0 -1px 1px $text-shadow; 137 | -webkit-background-clip: padding-box; 138 | 139 | &:hover { 140 | $base-color-hover: adjust-color($base-color, $lightness: -4.5%); 141 | $stop-gradient-hover: adjust-color($base-color, $hue: 8, $saturation: -4%, $lightness: -15.5%); 142 | $border-top: adjust-color($base-color, $hue: -1, $saturation: -17%, $lightness: -21%); 143 | $border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%); 144 | $border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%); 145 | $inset-shadow-hover: adjust-color($base-color, $saturation: -1%, $lightness: 3%); 146 | $text-shadow-hover: adjust-color($base-color, $hue: 5, $saturation: -5%, $lightness: -22%); 147 | 148 | @include linear-gradient ($base-color-hover, $stop-gradient-hover); 149 | border: 1px solid $border-top; 150 | border-color: $border-top $border-sides $border-bottom; 151 | @include box-shadow(inset 0 1px 0 0 $inset-shadow-hover); 152 | cursor: pointer; 153 | text-shadow: 0 -1px 1px $text-shadow-hover; 154 | -webkit-background-clip: padding-box; 155 | } 156 | 157 | &:active { 158 | $active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%); 159 | $border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%); 160 | $border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%); 161 | $inset-shadow-active: adjust-color($base-color, $hue: 9, $saturation: 2%, $lightness: -21.5%); 162 | $text-shadow-active: adjust-color($base-color, $hue: 5, $saturation: -12%, $lightness: -21.5%); 163 | 164 | background: $active-color; 165 | border: 1px solid $border-active; 166 | border-bottom: 1px solid $border-bottom-active; 167 | @include box-shadow(inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff); 168 | text-shadow: 0 -1px 1px $text-shadow-active; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (3.2.16) 5 | i18n (~> 0.6, >= 0.6.4) 6 | multi_json (~> 1.0) 7 | addressable (2.3.5) 8 | alias (0.2.3) 9 | atomic (1.1.14) 10 | awesome_print (1.2.0) 11 | backports (3.4.1) 12 | backup (3.4.0) 13 | open4 (~> 1.3.0) 14 | thor (>= 0.15.4, < 2) 15 | bcrypt-ruby (3.1.2) 16 | binding.repl (0.9.0) 17 | toml (~> 0.0.3) 18 | blankslate (2.1.2.4) 19 | bond (0.5.0) 20 | boson (1.2.4) 21 | boson-more (0.2.2) 22 | boson (>= 1.2.0) 23 | buftok (0.2.0) 24 | builder (3.2.2) 25 | chronic (0.10.2) 26 | chronic_duration (0.10.2) 27 | numerizer (~> 0.1.1) 28 | cinch (2.0.12) 29 | cinch-identify (1.5.1) 30 | cinch (~> 2.0) 31 | cinchize (0.4.2) 32 | cinch (>= 2.0.1) 33 | daemons 34 | clipboard (1.0.5) 35 | coderay (1.1.0) 36 | coffee-script (2.2.0) 37 | coffee-script-source 38 | execjs 39 | coffee-script-source (1.7.0) 40 | daemons (1.1.9) 41 | data_mapper (1.2.0) 42 | dm-aggregates (~> 1.2.0) 43 | dm-constraints (~> 1.2.0) 44 | dm-core (~> 1.2.0) 45 | dm-migrations (~> 1.2.0) 46 | dm-serializer (~> 1.2.0) 47 | dm-timestamps (~> 1.2.0) 48 | dm-transactions (~> 1.2.0) 49 | dm-types (~> 1.2.0) 50 | dm-validations (~> 1.2.0) 51 | data_objects (0.10.13) 52 | addressable (~> 2.1) 53 | descendants_tracker (0.0.3) 54 | dm-aggregates (1.2.0) 55 | dm-core (~> 1.2.0) 56 | dm-constraints (1.2.0) 57 | dm-core (~> 1.2.0) 58 | dm-core (1.2.1) 59 | addressable (~> 2.3) 60 | dm-do-adapter (1.2.0) 61 | data_objects (~> 0.10.6) 62 | dm-core (~> 1.2.0) 63 | dm-is-counter_cacheable (0.1.1) 64 | dm-core (~> 1.0) 65 | dm-migrations (1.2.0) 66 | dm-core (~> 1.2.0) 67 | dm-mysql-adapter (1.2.0) 68 | dm-do-adapter (~> 1.2.0) 69 | do_mysql (~> 0.10.6) 70 | dm-serializer (1.2.2) 71 | dm-core (~> 1.2.0) 72 | fastercsv (~> 1.5) 73 | json (~> 1.6) 74 | json_pure (~> 1.6) 75 | multi_json (~> 1.0) 76 | dm-sqlite-adapter (1.2.0) 77 | dm-do-adapter (~> 1.2.0) 78 | do_sqlite3 (~> 0.10.6) 79 | dm-timestamps (1.2.0) 80 | dm-core (~> 1.2.0) 81 | dm-transactions (1.2.0) 82 | dm-core (~> 1.2.0) 83 | dm-types (1.2.2) 84 | bcrypt-ruby (~> 3.0) 85 | dm-core (~> 1.2.0) 86 | fastercsv (~> 1.5) 87 | json (~> 1.6) 88 | multi_json (~> 1.0) 89 | stringex (~> 1.4) 90 | uuidtools (~> 2.1) 91 | dm-validations (1.2.0) 92 | dm-core (~> 1.2.0) 93 | do_mysql (0.10.13) 94 | data_objects (= 0.10.13) 95 | do_sqlite3 (0.10.13) 96 | data_objects (= 0.10.13) 97 | dotenv (0.9.0) 98 | equalizer (0.0.9) 99 | eventmachine (1.0.3) 100 | every_day_irb (1.5.1) 101 | excon (0.17.0) 102 | execjs (2.0.2) 103 | fancy_irb (0.7.3) 104 | paint (>= 0.8.1) 105 | unicode-display_width (>= 0.1.1) 106 | faraday (0.9.0) 107 | multipart-post (>= 1.2, < 3) 108 | fastercsv (1.5.5) 109 | ffi (1.9.3) 110 | fog (1.9.0) 111 | builder 112 | excon (~> 0.14) 113 | formatador (~> 0.2.0) 114 | mime-types 115 | multi_json (~> 1.0) 116 | net-scp (~> 1.0.4) 117 | net-ssh (>= 2.1.3) 118 | nokogiri (~> 1.5.0) 119 | ruby-hmac 120 | foreman (0.63.0) 121 | dotenv (>= 0.7) 122 | thor (>= 0.13.6) 123 | formatador (0.2.4) 124 | g (1.7.2) 125 | haml (4.0.5) 126 | tilt 127 | hirb (0.7.1) 128 | i18n (0.6.9) 129 | interactive_editor (0.0.10) 130 | spoon (>= 0.0.1) 131 | irbtools (1.5.1) 132 | alias (~> 0.2.3) 133 | awesome_print (~> 1.2.0) 134 | binding.repl (>= 0.7.0) 135 | boson (~> 1.2.4) 136 | boson-more (~> 0.2.2) 137 | clipboard (~> 1.0.5) 138 | coderay (~> 1.1.0) 139 | every_day_irb (>= 1.5.1) 140 | fancy_irb (>= 0.7.3) 141 | g (>= 1.7.2) 142 | hirb (~> 0.7.1) 143 | interactive_editor (>= 0.0.10) 144 | method_locator (>= 0.0.4) 145 | method_source (>= 0.8.2) 146 | methodfinder (>= 1.2.5) 147 | ori (~> 0.1.0) 148 | paint (>= 0.8.6) 149 | wirb (>= 1.0.2) 150 | zucker (>= 13.1) 151 | irbtools-more (1.5.2) 152 | bond (~> 0.5) 153 | irbtools (~> 1.5) 154 | looksee (~> 2.0) 155 | json (1.8.1) 156 | json_pure (1.8.1) 157 | libv8 (3.11.8.17) 158 | looksee (2.0.0) 159 | mail (2.5.4) 160 | mime-types (~> 1.16) 161 | treetop (~> 1.4.8) 162 | memoizable (0.4.0) 163 | thread_safe (~> 0.1.3) 164 | method_locator (0.0.4) 165 | method_source (0.8.2) 166 | methodfinder (2.0.0) 167 | mime-types (1.25.1) 168 | multi_json (1.8.4) 169 | multipart-post (2.0.0) 170 | naught (1.0.0) 171 | net-scp (1.0.4) 172 | net-ssh (>= 1.99.1) 173 | net-ssh (2.5.2) 174 | nokogiri (1.5.11) 175 | numerizer (0.1.1) 176 | open4 (1.3.0) 177 | ori (0.1.0) 178 | paint (0.8.7) 179 | parslet (1.5.0) 180 | blankslate (~> 2.0) 181 | polyglot (0.3.3) 182 | rack (1.5.2) 183 | rack-protection (1.5.2) 184 | rack 185 | rack-test (0.6.2) 186 | rack (>= 1.0) 187 | rake (10.1.1) 188 | rb-fsevent (0.9.4) 189 | ref (1.0.5) 190 | ri_cal (0.8.8) 191 | ruby-hmac (0.4.0) 192 | sass (3.2.14) 193 | simple_oauth (0.2.0) 194 | sinatra (1.4.4) 195 | rack (~> 1.4) 196 | rack-protection (~> 1.4) 197 | tilt (~> 1.3, >= 1.3.4) 198 | sinatra-contrib (1.4.2) 199 | backports (>= 2.0) 200 | multi_json 201 | rack-protection 202 | rack-test 203 | sinatra (~> 1.4.0) 204 | tilt (~> 1.3) 205 | sinatra-reloader (1.0) 206 | sinatra-contrib 207 | spoon (0.0.4) 208 | ffi 209 | stopwords (0.2) 210 | stringex (1.5.1) 211 | therubyracer (0.11.4) 212 | libv8 (~> 3.11.8.12) 213 | ref 214 | thin (1.6.1) 215 | daemons (>= 1.0.9) 216 | eventmachine (>= 1.0.0) 217 | rack (>= 1.0.0) 218 | thor (0.18.1) 219 | thread_safe (0.1.3) 220 | atomic 221 | tilt (1.4.1) 222 | toml (0.0.4) 223 | parslet 224 | treetop (1.4.15) 225 | polyglot 226 | polyglot (>= 0.3.1) 227 | twitter (5.6.0) 228 | addressable (~> 2.3) 229 | buftok (~> 0.2.0) 230 | descendants_tracker (~> 0.0.3) 231 | equalizer (~> 0.0.9) 232 | faraday (~> 0.9.0) 233 | http (~> 0.5.0) 234 | http_parser.rb (~> 0.6.0) 235 | json (~> 1.8) 236 | memoizable (~> 0.4.0) 237 | naught (~> 1.0) 238 | simple_oauth (~> 0.2.0) 239 | tzinfo (1.1.0) 240 | thread_safe (~> 0.1) 241 | tzinfo-data (1.2013.9) 242 | tzinfo (>= 1.0.0) 243 | unicode-display_width (0.1.1) 244 | uuidtools (2.1.4) 245 | whenever (0.9.0) 246 | activesupport (>= 2.3.4) 247 | chronic (>= 0.6.3) 248 | wirb (1.0.3) 249 | paint (~> 0.8) 250 | zucker (13.1) 251 | 252 | PLATFORMS 253 | ruby 254 | 255 | DEPENDENCIES 256 | backup 257 | chronic 258 | chronic_duration 259 | cinch 260 | cinch-identify 261 | cinchize 262 | coffee-script 263 | data_mapper 264 | dm-aggregates 265 | dm-core 266 | dm-is-counter_cacheable 267 | dm-migrations 268 | dm-mysql-adapter 269 | dm-sqlite-adapter 270 | dm-timestamps 271 | dm-types 272 | dm-validations 273 | excon (~> 0.17.0) 274 | execjs 275 | fog (~> 1.9.0) 276 | foreman 277 | haml 278 | i18n 279 | irbtools-more 280 | mail (~> 2.5.0) 281 | net-ssh (>= 2.3.0, <= 2.5.2) 282 | rack-test 283 | rake 284 | rb-fsevent 285 | ri_cal 286 | sass 287 | sinatra 288 | sinatra-reloader 289 | stopwords (= 0.2) 290 | therubyracer 291 | thin 292 | twitter 293 | tzinfo 294 | tzinfo-data 295 | whenever 296 | -------------------------------------------------------------------------------- /showbot_web.rb: -------------------------------------------------------------------------------- 1 | # showbot_web.rb 2 | # The web front-end for showbot 3 | 4 | # Gems 5 | require 'bundler/setup' 6 | require 'coffee_script' 7 | require 'sinatra' unless defined?(Sinatra) 8 | require "sinatra/reloader" if development? 9 | 10 | require 'json' 11 | 12 | require File.join(File.dirname(__FILE__), 'environment') 13 | 14 | 15 | SHOWS_JSON = File.expand_path(File.join(File.dirname(__FILE__), "public", "shows.json")) unless defined? SHOWS_JSON 16 | SAMPLE_TITLES_JSON = File.expand_path("hypercritical.json") 17 | 18 | class ShowbotWeb < Sinatra::Base 19 | configure do 20 | set :public_folder, "#{File.dirname(__FILE__)}/public" 21 | set :views, "#{File.dirname(__FILE__)}/views" 22 | set :shows, Shows.new(SHOWS_JSON) 23 | end 24 | 25 | configure(:production, :development) do 26 | enable :logging 27 | end 28 | 29 | configure :development do 30 | register Sinatra::Reloader 31 | end 32 | 33 | # ------------------ 34 | # Pages 35 | # ------------------ 36 | 37 | # CoffeeScript 38 | get '/js/showbot.js' do 39 | coffee :'coffeescripts/showbot' 40 | end 41 | 42 | get '/' do 43 | @title = "Home" 44 | suggestion_sets = Suggestion.recent.group_by_show 45 | view_mode = params[:view_mode] || 'tables' 46 | haml :index, :locals => {suggestion_sets: suggestion_sets, :view_mode => view_mode} 47 | end 48 | 49 | get '/titles' do 50 | @title = "Title Suggestions in the last 24 hours" 51 | view_mode = params[:view_mode] || 'tables' 52 | suggestion_sets = Suggestion.recent.group_by_show 53 | if view_mode == 'hacker' 54 | content_type 'text/plain' 55 | haml :'suggestion/hacker_mode', :locals => {suggestion_sets: suggestion_sets, :view_mode => view_mode}, :layout => false 56 | else 57 | haml :'suggestion/index', :locals => {suggestion_sets: suggestion_sets, :view_mode => view_mode} 58 | end 59 | end 60 | 61 | get '/links' do 62 | @title = "Suggested Links in the last 24 hours" 63 | @links = Link.recent.all(:order => [:created_at.desc]) 64 | haml :links 65 | end 66 | 67 | get '/all' do 68 | suggestion_sets = Suggestion.all(:order => [:created_at.desc]).group_by_show 69 | content_type 'text/plain' 70 | haml :'suggestion/hacker_mode', :locals => {suggestion_sets: suggestion_sets}, :layout => false 71 | end 72 | 73 | get '/titles/:id/vote_up' do 74 | content_type :json 75 | # Only allow XHR requests for voting 76 | if request.xhr? 77 | suggestion = Suggestion.get(params[:id]) 78 | cluster_top = suggestion.top_of_cluster? # figure out if top before adding new vote 79 | suggestion.vote_up(request.ip) 80 | {votes: suggestion.votes.count.to_s, 81 | cluster_top: cluster_top, 82 | cluster_id: suggestion.cluster_id, 83 | cluster_votes: suggestion.total_for_cluster}.to_json 84 | else 85 | redirect '/' 86 | end 87 | end 88 | 89 | # Word cloud generation 90 | 91 | get '/clouds_between/:days_a/:days_b' do 92 | days_ago = [params[:days_a].to_i, params[:days_b].to_i].sort 93 | suggestion_sets = Suggestion.all(:created_at => ( (DateTime.now - days_ago[1])..(DateTime.now - days_ago[0]) ), :order => [:created_at.desc]).group_by_show 94 | haml :'clouds', :locals => { cloud_data: WordCount.generate_clouds(suggestion_sets) } 95 | end 96 | 97 | get '/cloud_svg/:year/:month/:day/:index' do 98 | the_date = DateTime.new(params[:year].to_i, params[:month].to_i, params[:day].to_i) 99 | bracketed_suggestion_sets = Suggestion.all(:created_at => ( (the_date - 1)..(the_date + 2) ), :order => [:created_at.desc]).group_by_show 100 | suggestion_sets = bracketed_suggestion_sets.select { |set| set.suggestions[0].created_at.to_date == the_date.to_date } 101 | 102 | haml :'clouds_svg', :locals => { cloud_data: WordCount.generate_clouds(suggestion_sets), cloud_index: params[:index].to_i } 103 | end 104 | 105 | get '/num_clouds_on_date/:year/:month/:day' do 106 | content_type :json 107 | 108 | the_date = DateTime.new(params[:year].to_i, params[:month].to_i, params[:day].to_i) 109 | bracketed_suggestion_sets = Suggestion.all(:created_at => ( (the_date - 1)..(the_date + 2) ), :order => [:created_at.desc]).group_by_show 110 | 111 | { num_clouds: bracketed_suggestion_sets.select { |set| set.suggestions[0].created_at.to_date == the_date.to_date }.count }.to_json 112 | end 113 | 114 | # ------------------ 115 | # API 116 | # ------------------ 117 | 118 | # Creates a title suggestion based on a POST request with valid 119 | # title and user parameters. 120 | # 121 | # title - String less than 40 characters. 122 | # user - String username to use. 123 | # api_key - Your API key. 124 | # 125 | # Examples 126 | # 127 | # Context: Posting a valid show title with a valid user. 128 | # POST /suggestions/new 129 | # params: { 130 | # title: 'Omg Title', 131 | # user: 'mrman', 132 | # api_key: 'keyhere' 133 | # } 134 | # 135 | # Response: 136 | # { 137 | # suggestion: { 138 | # user: 'mrman', 139 | # title: 'Omg Title' 140 | # } 141 | # } 142 | # 143 | # Context: Posting a show title that's too long. 144 | # POST /suggestions/new 145 | # params: { 146 | # title: 'Super freaking long title that will make showbot cry.', 147 | # user: 'badman', 148 | # api_key: 'keyhere' 149 | # } 150 | # 151 | # Response: 152 | # { 153 | # error: 'That suggestion was too long. Showbot is sorry. Think title, 154 | # not transcript.' 155 | # } 156 | # 157 | # Context: The same title suggested seconds later. 158 | # POST /suggestions/new 159 | # params: { 160 | # title: 'Same Title', 161 | # user: 'slowpoke', 162 | # api_key: 'keyhere' 163 | # } 164 | # 165 | # Response: 166 | # { 167 | # error: 'Darn, fastman beat you to "Same Title".' 168 | # } 169 | # 170 | # Returns a JSON response with the original suggestion and an error 171 | # message if one was generated. 172 | post '/suggestions/new' do 173 | content_type :json 174 | 175 | api_key = params[:api_key] 176 | response = nil 177 | if api_key and ApiKey.first(value: api_key) 178 | title = params[:title] 179 | user = params[:user] 180 | if title && user 181 | suggestion = Suggestion.create( 182 | title: title, 183 | user: user 184 | ) 185 | 186 | if suggestion.saved? 187 | response = { 188 | suggestion: { 189 | title: title, 190 | user: user 191 | } 192 | } 193 | else 194 | response = { 195 | error: suggestion.errors.first.first 196 | } 197 | end 198 | else 199 | if !title 200 | response = { 201 | error: 'Missing / Invalid Title' 202 | } 203 | else 204 | response = { 205 | error: 'Missing / Invalid User' 206 | } 207 | end 208 | end 209 | end 210 | 211 | if response 212 | response.to_json 213 | else 214 | halt 404, { 215 | error: "Invalid Api Key #{api_key}" 216 | }.to_json 217 | end 218 | end 219 | 220 | # ------------------ 221 | # Helpers 222 | # ------------------ 223 | 224 | helpers do 225 | include Rack::Utils 226 | alias_method :h, :escape_html 227 | 228 | def external_link(link) 229 | /^http/.match(link) ? link : "http://#{link}" 230 | end 231 | 232 | # Returns a string truncated in the middle 233 | # Note: Rounds max_length down to nearest even number 234 | def truncate_string(string, max_length) 235 | if string.length > max_length 236 | # +/- 2 is to account for the elipse in the middle 237 | "#{string[0..(max_length/2)-2]}...#{string[-(max_length/2)+2..-1]}" 238 | else 239 | string 240 | end 241 | end 242 | 243 | def show_title_for_slug(slug) 244 | text = "Show Not Listed" 245 | if slug 246 | text = Shows.find_show_title(slug) 247 | end 248 | text 249 | end 250 | 251 | def suggestion_set_hr(suggestion_set) 252 | "<h2 class='show_break'>#{show_title_for_slug(suggestion_set.slug)}</h2>" 253 | end 254 | 255 | def link_to_vote_up(suggestion) 256 | html = '' 257 | # onclick returns false to keep from allowing 258 | html << "<a href='#' class='vote_up' onclick='return false;' data-id='#{suggestion.id}'>" 259 | html << "<span class='vote_arrow'/>" 260 | html << "</a>" 261 | end 262 | 263 | def link_and_vote_count(suggestion, user_ip) 264 | html = '' 265 | extra_classes = [] 266 | if suggestion.user_already_voted?(user_ip) 267 | extra_classes << 'voted' 268 | else 269 | html << link_to_vote_up(suggestion) 270 | end 271 | html << "<span class='vote_count #{extra_classes.join(',')}'>#{suggestion.votes_counter}</span>" 272 | end 273 | 274 | def development? 275 | settings.development? 276 | end 277 | 278 | def cloud_json(cloud_data) 279 | return if cloud_data.nil? 280 | 281 | cloud_data.map do |d| 282 | { 283 | title: "#{show_title_for_slug(d[:show])} #{d[:time].to_date.to_s}", 284 | data: d[:data] 285 | } 286 | end.to_json 287 | end 288 | 289 | end # helpers 290 | 291 | end 292 | -------------------------------------------------------------------------------- /public/js/jquery.tipsy.js: -------------------------------------------------------------------------------- 1 | // tipsy, facebook style tooltips for jquery 2 | // version 1.0.0a 3 | // (c) 2008-2010 jason frame [jason@onehackoranother.com] 4 | // released under the MIT license 5 | 6 | (function($) { 7 | 8 | function maybeCall(thing, ctx) { 9 | return (typeof thing == 'function') ? (thing.call(ctx)) : thing; 10 | }; 11 | 12 | function Tipsy(element, options) { 13 | this.$element = $(element); 14 | this.options = options; 15 | this.enabled = true; 16 | this.fixTitle(); 17 | }; 18 | 19 | Tipsy.prototype = { 20 | show: function() { 21 | var title = this.getTitle(); 22 | if (title && this.enabled) { 23 | var $tip = this.tip(); 24 | 25 | $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); 26 | $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity 27 | $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); 28 | 29 | var pos = $.extend({}, this.$element.offset(), { 30 | width: this.$element[0].offsetWidth, 31 | height: this.$element[0].offsetHeight 32 | }); 33 | 34 | var actualWidth = $tip[0].offsetWidth, 35 | actualHeight = $tip[0].offsetHeight, 36 | gravity = maybeCall(this.options.gravity, this.$element[0]); 37 | 38 | var tp; 39 | switch (gravity.charAt(0)) { 40 | case 'n': 41 | tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; 42 | break; 43 | case 's': 44 | tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; 45 | break; 46 | case 'e': 47 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}; 48 | break; 49 | case 'w': 50 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}; 51 | break; 52 | } 53 | 54 | if (gravity.length == 2) { 55 | if (gravity.charAt(1) == 'w') { 56 | tp.left = pos.left + pos.width / 2 - 15; 57 | } else { 58 | tp.left = pos.left + pos.width / 2 - actualWidth + 15; 59 | } 60 | } 61 | 62 | $tip.css(tp).addClass('tipsy-' + gravity); 63 | $tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0); 64 | if (this.options.className) { 65 | $tip.addClass(maybeCall(this.options.className, this.$element[0])); 66 | } 67 | 68 | if (this.options.fade) { 69 | $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}); 70 | } else { 71 | $tip.css({visibility: 'visible', opacity: this.options.opacity}); 72 | } 73 | } 74 | }, 75 | 76 | hide: function() { 77 | if (this.options.fade) { 78 | this.tip().stop().fadeOut(function() { $(this).remove(); }); 79 | } else { 80 | this.tip().remove(); 81 | } 82 | }, 83 | 84 | fixTitle: function() { 85 | var $e = this.$element; 86 | if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') { 87 | $e.attr('original-title', $e.attr('title') || '').removeAttr('title'); 88 | } 89 | }, 90 | 91 | getTitle: function() { 92 | var title, $e = this.$element, o = this.options; 93 | this.fixTitle(); 94 | var title, o = this.options; 95 | if (typeof o.title == 'string') { 96 | title = $e.attr(o.title == 'title' ? 'original-title' : o.title); 97 | } else if (typeof o.title == 'function') { 98 | title = o.title.call($e[0]); 99 | } 100 | title = ('' + title).replace(/(^\s*|\s*$)/, ""); 101 | return title || o.fallback; 102 | }, 103 | 104 | tip: function() { 105 | if (!this.$tip) { 106 | this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>'); 107 | } 108 | return this.$tip; 109 | }, 110 | 111 | validate: function() { 112 | if (!this.$element[0].parentNode) { 113 | this.hide(); 114 | this.$element = null; 115 | this.options = null; 116 | } 117 | }, 118 | 119 | enable: function() { this.enabled = true; }, 120 | disable: function() { this.enabled = false; }, 121 | toggleEnabled: function() { this.enabled = !this.enabled; } 122 | }; 123 | 124 | $.fn.tipsy = function(options) { 125 | 126 | if (options === true) { 127 | return this.data('tipsy'); 128 | } else if (typeof options == 'string') { 129 | var tipsy = this.data('tipsy'); 130 | if (tipsy) tipsy[options](); 131 | return this; 132 | } 133 | 134 | options = $.extend({}, $.fn.tipsy.defaults, options); 135 | 136 | function get(ele) { 137 | var tipsy = $.data(ele, 'tipsy'); 138 | if (!tipsy) { 139 | tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); 140 | $.data(ele, 'tipsy', tipsy); 141 | } 142 | return tipsy; 143 | } 144 | 145 | function enter() { 146 | var tipsy = get(this); 147 | tipsy.hoverState = 'in'; 148 | if (options.delayIn == 0) { 149 | tipsy.show(); 150 | } else { 151 | tipsy.fixTitle(); 152 | setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn); 153 | } 154 | }; 155 | 156 | function leave() { 157 | var tipsy = get(this); 158 | tipsy.hoverState = 'out'; 159 | if (options.delayOut == 0) { 160 | tipsy.hide(); 161 | } else { 162 | setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut); 163 | } 164 | }; 165 | 166 | if (!options.live) this.each(function() { get(this); }); 167 | 168 | if (options.trigger != 'manual') { 169 | var binder = options.live ? 'live' : 'bind', 170 | eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus', 171 | eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'; 172 | this[binder](eventIn, enter)[binder](eventOut, leave); 173 | } 174 | 175 | return this; 176 | 177 | }; 178 | 179 | $.fn.tipsy.defaults = { 180 | className: null, 181 | delayIn: 0, 182 | delayOut: 0, 183 | fade: false, 184 | fallback: '', 185 | gravity: 'n', 186 | html: false, 187 | live: false, 188 | offset: 0, 189 | opacity: 0.8, 190 | title: 'title', 191 | trigger: 'hover' 192 | }; 193 | 194 | // Overwrite this method to provide options on a per-element basis. 195 | // For example, you could store the gravity in a 'tipsy-gravity' attribute: 196 | // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); 197 | // (remember - do not modify 'options' in place!) 198 | $.fn.tipsy.elementOptions = function(ele, options) { 199 | return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; 200 | }; 201 | 202 | $.fn.tipsy.autoNS = function() { 203 | return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n'; 204 | }; 205 | 206 | $.fn.tipsy.autoWE = function() { 207 | return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w'; 208 | }; 209 | 210 | /** 211 | * yields a closure of the supplied parameters, producing a function that takes 212 | * no arguments and is suitable for use as an autogravity function like so: 213 | * 214 | * @param margin (int) - distance from the viewable region edge that an 215 | * element should be before setting its tooltip's gravity to be away 216 | * from that edge. 217 | * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer 218 | * if there are no viewable region edges effecting the tooltip's 219 | * gravity. It will try to vary from this minimally, for example, 220 | * if 'sw' is preferred and an element is near the right viewable 221 | * region edge, but not the top edge, it will set the gravity for 222 | * that element's tooltip to be 'se', preserving the southern 223 | * component. 224 | */ 225 | $.fn.tipsy.autoBounds = function(margin, prefer) { 226 | return function() { 227 | var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, 228 | boundTop = $(document).scrollTop() + margin, 229 | boundLeft = $(document).scrollLeft() + margin, 230 | $this = $(this); 231 | 232 | if ($this.offset().top < boundTop) dir.ns = 'n'; 233 | if ($this.offset().left < boundLeft) dir.ew = 'w'; 234 | if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e'; 235 | if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's'; 236 | 237 | return dir.ns + (dir.ew ? dir.ew : ''); 238 | } 239 | }; 240 | 241 | })(jQuery); 242 | -------------------------------------------------------------------------------- /lib/cinch/plugins/fivebyfun.rb: -------------------------------------------------------------------------------- 1 | # The stuff that makes showbot so nice to be around. 2 | 3 | module Cinch 4 | module Plugins 5 | class FiveByFun 6 | include Cinch::Plugin 7 | 8 | # The cast 9 | match /(dan|danbenjamin)/i, :method => :command_dan 10 | match /(haddie|haddiebird)/i, :method => :command_haddie 11 | match /(merlin|mann)/i, :method => :command_merlin 12 | match /(sandy|sandwich|adam)/i, :method => :command_sandy 13 | match /(jsir|siracusa|jsiracusa)/i, :method => :command_jsir 14 | match /marco/i, :method => :command_marco 15 | match /gruber/i, :method => :command_gruber 16 | match /robertevans/i, :method => :command_robert_evans 17 | match /faith/i, :method => :command_faith 18 | match /(mike|monteiro)/i, :method => :command_mike 19 | match /(roderick)/i, :method => :command_roderick 20 | # The characters 21 | match /drphil/i, :method => :command_drphil 22 | match /(vanhoet|neckbeard)/i, :method => :command_van_hoet 23 | match /paleo/i, :method => :command_paleo 24 | match /glen/i, :method => :command_glengarry 25 | match /usa/i, :method => :command_usa 26 | match /turd/i, :method => :command_turd 27 | match /resp/i, :method => :command_responsibility 28 | match /(lebowski|dude)/i, :method => :command_lebowski 29 | match /(texas)/i, :method => :command_texas 30 | match /(ferris|bueller)/i, :method => :command_bueller 31 | match /bluetoot/i, :method => :command_bluetoot 32 | match /sodastream/i, :method => :command_sodastream 33 | match /(aviator|future)/i, :method => :command_aviator 34 | # The etc. 35 | match /lemongrab/i, :method => :command_lemongrab 36 | match /peppermint_butler/i, :method => :command_peppermint 37 | match /(eight_ball|8ball)/i, :method => :command_eight_ball 38 | match /(quit)/i, :method => :command_quit_show 39 | 40 | def command_glengarry(m) 41 | m.reply ["You got leads. Mitch and Murray paid good money. Get their names to sell them.", 42 | "I'm here from downtown. I'm here from Mitch and Murray.", 43 | "Get them to sign on the line which is dotted!", 44 | "Attention. Do I have your attention? Interest. Are you interested? I know you are 'cause it's fuck or walk. You close or you hit the bricks. Decision. Have you made your decision for Christ? And action.", 45 | "Oh yeah, I used to be a salesman. It's a tough racket.", 46 | "Second prize is a set of steak knives. Third prize is you're fired.", 47 | "Coffee's for closers only." 48 | ].sample 49 | end 50 | 51 | def command_usa(m) 52 | m.reply "USA! USA! USA! USA! USA! USA!" 53 | end 54 | 55 | def command_turd(m) 56 | m.reply "You can't. Polish. A turd." 57 | end 58 | 59 | def command_responsibility(m) 60 | m.reply ["Have you ever had a single moment's thought about my responsibilities?", 61 | "Have you ever thought for a single solitary moment about my responsibilities to my employers?", 62 | "Has it ever occurred to you that I have agreed to look after the Overlook Hotel until May the First?", 63 | "Does it matter to you at all that the owners have placed their complete confidence and trust in me, and that I have signed a letter of agreement", 64 | "A CONTRACT!", 65 | "Do you have the slightest idea what a moral and ethical principle is?", 66 | "Has it ever occurred to you what would happen to my future if I were to fail to live up to my responsibilities?", 67 | ].sample 68 | end 69 | 70 | def command_lebowski(m) 71 | m.reply ["That's your name, Dude!", 72 | "I see you rolled your way into the semis. Dios mio, man.", 73 | "Uh, uh, papers, um, just papers, uh, you know, uh, my papers, business papers." 74 | ].sample 75 | end 76 | 77 | def command_texas(m) 78 | m.reply "The stars at night are big and bright..." 79 | end 80 | 81 | def command_bluetoot(m) 82 | m.reply ["Hi! Can I aks you a queshion?", 83 | "Before you answer, Hi!" 84 | ].sample 85 | end 86 | 87 | def command_sodastream(m) 88 | m.reply "pshhh pshhh pshhh HONNNNKKKK HONNNNKKKK HONNNNKKKK" 89 | end 90 | 91 | def command_bueller(m) 92 | m.reply ["Nine times?", 93 | "Nine times.", 94 | "ok I'll go, I'll go, I'll go, I'll go, I'll go." 95 | ].sample 96 | end 97 | 98 | def command_aviator(m) 99 | m.reply ["The way of the future.", 100 | "Come in with the milk. Come in with the milk. Come in with the milk." 101 | ].sample 102 | end 103 | 104 | def command_roderick(m) 105 | m.reply ["I demand satisfaction!", 106 | "I agree to nothing!", 107 | "Supertrain will fix all of this.", 108 | "Keep moving and get out of the way", 109 | "Hitler"].sample 110 | end 111 | 112 | def command_drphil(m) 113 | m.reply ["There's a genie for that.", 114 | "Everything's a bear.", 115 | "A beret will be fine.", 116 | "If you want to find the treasure you gotta buy the chest!", 117 | "You don't win at tennis by buying a bowling ball.", 118 | "If you live in a tree, don't be surprised that you're living with monkeys.", 119 | "Crush the Bunny.", 120 | "Doesn't matter how many Fords you buy, they're never gonna be a Dodge. You can repaint the Ford but... let's go to a break.", 121 | "You're not gonna get Black Lung from an excel spreadsheet.", 122 | "I'm not gonna euthanize this dog, I'm just gonna put it over here where I can't see it.", 123 | "Failure is the equivalent of existential sit-ups."].sample 124 | end 125 | 126 | def command_merlin(m) 127 | m.reply ["SO angry.", 128 | "Don't be creepy.", 129 | "Go ahead, caller.", 130 | "Is this what people tune in for?", 131 | "I love you.", 132 | "Recursion. Which is also known as recursion.", 133 | "...Cleric...", 134 | "I gotta go bust a tinkie." 135 | ].sample 136 | end 137 | 138 | def command_sandy(m) 139 | m.reply "He's great." 140 | end 141 | 142 | def command_paleo(m) 143 | m.reply ["You wouldn't be tired.", 144 | "Your insulin wouldn't be spiking.", 145 | "Elk.", 146 | "No glutens."].sample 147 | end 148 | 149 | def command_jsir(m) 150 | m.reply ["perl -le '$n=10; $min=5; $max=15; $, = \" \"; print map { int(rand($max-$min))+$min } 1..$n'", 151 | "perl -le '$i=3; $u += ($_<<8*$i--) for \"127.0.0.1\" =~ /(\d+)/g; print $u'", 152 | "perl -MAlgorithm::Permute -le '$l = [1,2,3,4,5]; $p = Algorithm::Permute->new($l); print @r while @r = $p->next'", 153 | "perl -lne '(1x$_) !~ /^1?$|^(11+?)\\1+$/ && print \"$_ is prime\"'", 154 | "perl -ple 's/^[ \\t]+|[ \\t]+$//g'"].sample 155 | end 156 | 157 | def command_gruber(m) 158 | m.reply '...' 159 | min_sleep = 3 160 | max_sleep = 10 161 | sleep(rand(max_sleep-min_sleep) + min_sleep) 162 | m.reply "I don't know." 163 | end 164 | 165 | def command_lemongrab(m) 166 | m.reply ["This castle is in...unacceptable...condition! UNACCEPTABLE!!!", 167 | "All of you. Dungeon. Seven years. No trials.", 168 | "Three. Hours. Dungeon.", 169 | "Oooonnne MILLION YEARS DUNGEON!!!", 170 | "Who did...the thing?!", 171 | "Yes, of course.", 172 | "Ahhhhh....HAHAHAHAHAHAHAHA!", 173 | "I determine what is early and what is late, Mr. Peppermint.", 174 | "Me too. I'm excited, too."].sample 175 | end 176 | 177 | def command_peppermint(m) 178 | m.reply ["Hey man, calm down! It's just a prank, man! For laughs!", 179 | "I'm excited by this meal I made!"].sample 180 | end 181 | 182 | def command_eight_ball(m) 183 | m.reply ["It is certain", 184 | "It is decidedly so", 185 | "Without a doubt", 186 | "Yes - definitely", 187 | "You may rely on it", 188 | "As I see it, yes", 189 | "Most likely", 190 | "Outlook good", 191 | "Signs point to yes", 192 | "Yes", 193 | "Reply hazy, try again", 194 | "Ask again later", 195 | "Better not tell you now", 196 | "Cannot predict now", 197 | "Concentrate and ask again", 198 | "Don't count on it", 199 | "My reply is no", 200 | "My sources say no", 201 | "Outlook not so good", 202 | "Very doubtful"].sample 203 | end 204 | 205 | def command_quit_show(m) 206 | m.reply "Call in to Quit! Live at (512) 518-5714. Leave a Voicemail at (512) 222-8141." 207 | end 208 | 209 | def command_dan(m) 210 | m.reply [ "That's fine for Merlin.", 211 | "Big week. Huge week.", 212 | "It's your show.", 213 | "Go ahead caller.", 214 | "Keeping you up, Haddie?"].sample 215 | end 216 | 217 | def command_haddie(m) 218 | m.reply [ "That's meat, I know it.", 219 | "Oh, it's fabulous.", 220 | "No, no, no one's screwing a hole", 221 | "It's a sensation", 222 | "Eww, the cat is weird", 223 | "We have to worry about space germs too. Ughhhhhhhhh! Why?!?!" 224 | ].sample 225 | end 226 | 227 | def command_robert_evans(m) 228 | m.reply "You bet your ass I was." 229 | end 230 | 231 | def command_van_hoet(m) 232 | m.reply "Erm, so." 233 | end 234 | 235 | def command_marco(m) 236 | m.reply ["Please don't email me.", 237 | "I shouldn't have said that.", 238 | "Braaaaands"].sample 239 | end 240 | 241 | def command_faith(m) 242 | m.reply [ "Don't tweet me.", 243 | "Don't be mean."].sample 244 | end 245 | 246 | def command_mike(m) 247 | m.reply "F*ck you. Pay me." 248 | end 249 | 250 | end 251 | end 252 | end 253 | 254 | -------------------------------------------------------------------------------- /views/..tmp.css: -------------------------------------------------------------------------------- 1 | /* 2 | * File: demo_table_jui.css 3 | * CVS: $Id$ 4 | * Description: CSS descriptions for DataTables demo pages 5 | * Author: Allan Jardine 6 | * Created: Tue May 12 06:47:22 BST 2009 7 | * Modified: $Date$ by $Author$ 8 | * Language: CSS 9 | * Project: DataTables 10 | * 11 | * Copyright 2009 Allan Jardine. All Rights Reserved. 12 | * 13 | * *************************************************************************** 14 | * DESCRIPTION 15 | * 16 | * The styles given here are suitable for the demos that are used with the standard DataTables 17 | * distribution (see www.datatables.net). You will most likely wish to modify these styles to 18 | * meet the layout requirements of your site. 19 | * 20 | * Common issues: 21 | * 'full_numbers' pagination - I use an extra selector on the body tag to ensure that there is 22 | * no conflict between the two pagination types. If you want to use full_numbers pagination 23 | * ensure that you either have "example_alt_pagination" as a body class name, or better yet, 24 | * modify that selector. 25 | * Note that the path used for Images is relative. All images are by default located in 26 | * ../images/ - relative to this CSS file. 27 | */ 28 | 29 | 30 | /* 31 | * jQuery UI specific styling 32 | */ 33 | 34 | .paging_two_button .ui-button { 35 | float: left; 36 | cursor: pointer; 37 | * cursor: hand; 38 | } 39 | 40 | .paging_full_numbers .ui-button { 41 | padding: 2px 6px; 42 | margin: 0; 43 | cursor: pointer; 44 | * cursor: hand; 45 | } 46 | 47 | .dataTables_paginate .ui-button { 48 | margin-right: -0.1em !important; 49 | } 50 | 51 | .paging_full_numbers { 52 | width: 350px !important; 53 | } 54 | 55 | .dataTables_wrapper .ui-toolbar { 56 | padding: 5px; 57 | } 58 | 59 | .dataTables_paginate { 60 | width: auto; 61 | } 62 | 63 | .dataTables_info { 64 | padding-top: 3px; 65 | } 66 | 67 | table.display thead th { 68 | padding: 3px 0px 3px 10px; 69 | cursor: pointer; 70 | * cursor: hand; 71 | } 72 | 73 | div.dataTables_wrapper .ui-widget-header { 74 | font-weight: normal; 75 | } 76 | 77 | 78 | /* 79 | * Sort arrow icon positioning 80 | */ 81 | table.display thead th div.DataTables_sort_wrapper { 82 | position: relative; 83 | padding-right: 20px; 84 | padding-right: 20px; 85 | } 86 | 87 | table.display thead th div.DataTables_sort_wrapper span { 88 | position: absolute; 89 | top: 50%; 90 | margin-top: -8px; 91 | right: 0; 92 | } 93 | 94 | 95 | 96 | 97 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 98 | * 99 | * Everything below this line is the same as demo_table.css. This file is 100 | * required for 'cleanliness' of the markup 101 | * 102 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 103 | 104 | 105 | 106 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 107 | * DataTables features 108 | */ 109 | 110 | .dataTables_wrapper { 111 | position: relative; 112 | min-height: 302px; 113 | _height: 302px; 114 | clear: both; 115 | } 116 | 117 | .dataTables_processing { 118 | position: absolute; 119 | top: 0px; 120 | left: 50%; 121 | width: 250px; 122 | margin-left: -125px; 123 | border: 1px solid #ddd; 124 | text-align: center; 125 | color: #999; 126 | font-size: 11px; 127 | padding: 2px 0; 128 | } 129 | 130 | .dataTables_length { 131 | width: 40%; 132 | float: left; 133 | } 134 | 135 | .dataTables_filter { 136 | width: 50%; 137 | float: right; 138 | text-align: right; 139 | } 140 | 141 | .dataTables_info { 142 | width: 50%; 143 | float: left; 144 | } 145 | 146 | .dataTables_paginate { 147 | float: right; 148 | text-align: right; 149 | } 150 | 151 | /* Pagination nested */ 152 | .paginate_disabled_previous, .paginate_enabled_previous, .paginate_disabled_next, .paginate_enabled_next { 153 | height: 19px; 154 | width: 19px; 155 | margin-left: 3px; 156 | float: left; 157 | } 158 | 159 | .paginate_disabled_previous { 160 | background-image: url('../images/back_disabled.jpg'); 161 | } 162 | 163 | .paginate_enabled_previous { 164 | background-image: url('../images/back_enabled.jpg'); 165 | } 166 | 167 | .paginate_disabled_next { 168 | background-image: url('../images/forward_disabled.jpg'); 169 | } 170 | 171 | .paginate_enabled_next { 172 | background-image: url('../images/forward_enabled.jpg'); 173 | } 174 | 175 | 176 | 177 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 178 | * DataTables display 179 | */ 180 | table.display { 181 | margin: 0 auto; 182 | width: 100%; 183 | clear: both; 184 | border-collapse: collapse; 185 | } 186 | 187 | table.display tfoot th { 188 | padding: 3px 0px 3px 10px; 189 | font-weight: bold; 190 | font-weight: normal; 191 | } 192 | 193 | table.display tr.heading2 td { 194 | border-bottom: 1px solid #aaa; 195 | } 196 | 197 | table.display td { 198 | padding: 3px 10px; 199 | } 200 | 201 | table.display td.center { 202 | text-align: center; 203 | } 204 | 205 | 206 | 207 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 208 | * DataTables sorting 209 | */ 210 | 211 | .sorting_asc { 212 | background: url('../images/sort_asc.png') no-repeat center right; 213 | } 214 | 215 | .sorting_desc { 216 | background: url('../images/sort_desc.png') no-repeat center right; 217 | } 218 | 219 | .sorting { 220 | background: url('../images/sort_both.png') no-repeat center right; 221 | } 222 | 223 | .sorting_asc_disabled { 224 | background: url('../images/sort_asc_disabled.png') no-repeat center right; 225 | } 226 | 227 | .sorting_desc_disabled { 228 | background: url('../images/sort_desc_disabled.png') no-repeat center right; 229 | } 230 | 231 | 232 | 233 | 234 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 235 | * DataTables row classes 236 | */ 237 | table.display tr.odd.gradeA { 238 | background-color: #ddffdd; 239 | } 240 | 241 | table.display tr.even.gradeA { 242 | background-color: #eeffee; 243 | } 244 | 245 | 246 | 247 | 248 | table.display tr.odd.gradeA { 249 | background-color: #ddffdd; 250 | } 251 | 252 | table.display tr.even.gradeA { 253 | background-color: #eeffee; 254 | } 255 | 256 | table.display tr.odd.gradeC { 257 | background-color: #ddddff; 258 | } 259 | 260 | table.display tr.even.gradeC { 261 | background-color: #eeeeff; 262 | } 263 | 264 | table.display tr.odd.gradeX { 265 | background-color: #ffdddd; 266 | } 267 | 268 | table.display tr.even.gradeX { 269 | background-color: #ffeeee; 270 | } 271 | 272 | table.display tr.odd.gradeU { 273 | background-color: #ddd; 274 | } 275 | 276 | table.display tr.even.gradeU { 277 | background-color: #eee; 278 | } 279 | 280 | 281 | tr.odd { 282 | background-color: #E2E4FF; 283 | } 284 | 285 | tr.even { 286 | background-color: white; 287 | } 288 | 289 | 290 | 291 | 292 | 293 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 294 | * Misc 295 | */ 296 | .dataTables_scroll { 297 | clear: both; 298 | } 299 | 300 | .top, .bottom { 301 | padding: 15px; 302 | background-color: #F5F5F5; 303 | border: 1px solid #CCCCCC; 304 | } 305 | 306 | .top .dataTables_info { 307 | float: none; 308 | } 309 | 310 | .clear { 311 | clear: both; 312 | } 313 | 314 | .dataTables_empty { 315 | text-align: center; 316 | } 317 | 318 | tfoot input { 319 | margin: 0.5em 0; 320 | width: 100%; 321 | color: #444; 322 | } 323 | 324 | tfoot input.search_init { 325 | color: #999; 326 | } 327 | 328 | td.group { 329 | background-color: #d1cfd0; 330 | border-bottom: 2px solid #A19B9E; 331 | border-top: 2px solid #A19B9E; 332 | } 333 | 334 | td.details { 335 | background-color: #d1cfd0; 336 | border: 2px solid #A19B9E; 337 | } 338 | 339 | 340 | .example_alt_pagination div.dataTables_info { 341 | width: 40%; 342 | } 343 | 344 | .paging_full_numbers span.paginate_button, 345 | .paging_full_numbers span.paginate_active { 346 | border: 1px solid #aaa; 347 | -webkit-border-radius: 5px; 348 | -moz-border-radius: 5px; 349 | padding: 2px 5px; 350 | margin: 0 3px; 351 | cursor: pointer; 352 | *cursor: hand; 353 | } 354 | 355 | .paging_full_numbers span.paginate_button { 356 | background-color: #ddd; 357 | } 358 | 359 | .paging_full_numbers span.paginate_button:hover { 360 | background-color: #ccc; 361 | } 362 | 363 | .paging_full_numbers span.paginate_active { 364 | background-color: #99B3FF; 365 | } 366 | 367 | table.display tr.even.row_selected td { 368 | background-color: #B0BED9; 369 | } 370 | 371 | table.display tr.odd.row_selected td { 372 | background-color: #9FAFD1; 373 | } 374 | 375 | 376 | /* 377 | * Sorting classes for columns 378 | */ 379 | /* For the standard odd/even */ 380 | tr.odd td.sorting_1 { 381 | background-color: #D3D6FF; 382 | } 383 | 384 | tr.odd td.sorting_2 { 385 | background-color: #DADCFF; 386 | } 387 | 388 | tr.odd td.sorting_3 { 389 | background-color: #E0E2FF; 390 | } 391 | 392 | tr.even td.sorting_1 { 393 | background-color: #EAEBFF; 394 | } 395 | 396 | tr.even td.sorting_2 { 397 | background-color: #F2F3FF; 398 | } 399 | 400 | tr.even td.sorting_3 { 401 | background-color: #F9F9FF; 402 | } 403 | 404 | 405 | /* For the Conditional-CSS grading rows */ 406 | /* 407 | Colour calculations (based off the main row colours) 408 | Level 1: 409 | dd > c4 410 | ee > d5 411 | Level 2: 412 | dd > d1 413 | ee > e2 414 | */ 415 | tr.odd.gradeA td.sorting_1 { 416 | background-color: #c4ffc4; 417 | } 418 | 419 | tr.odd.gradeA td.sorting_2 { 420 | background-color: #d1ffd1; 421 | } 422 | 423 | tr.odd.gradeA td.sorting_3 { 424 | background-color: #d1ffd1; 425 | } 426 | 427 | tr.even.gradeA td.sorting_1 { 428 | background-color: #d5ffd5; 429 | } 430 | 431 | tr.even.gradeA td.sorting_2 { 432 | background-color: #e2ffe2; 433 | } 434 | 435 | tr.even.gradeA td.sorting_3 { 436 | background-color: #e2ffe2; 437 | } 438 | 439 | tr.odd.gradeC td.sorting_1 { 440 | background-color: #c4c4ff; 441 | } 442 | 443 | tr.odd.gradeC td.sorting_2 { 444 | background-color: #d1d1ff; 445 | } 446 | 447 | tr.odd.gradeC td.sorting_3 { 448 | background-color: #d1d1ff; 449 | } 450 | 451 | tr.even.gradeC td.sorting_1 { 452 | background-color: #d5d5ff; 453 | } 454 | 455 | tr.even.gradeC td.sorting_2 { 456 | background-color: #e2e2ff; 457 | } 458 | 459 | tr.even.gradeC td.sorting_3 { 460 | background-color: #e2e2ff; 461 | } 462 | 463 | tr.odd.gradeX td.sorting_1 { 464 | background-color: #ffc4c4; 465 | } 466 | 467 | tr.odd.gradeX td.sorting_2 { 468 | background-color: #ffd1d1; 469 | } 470 | 471 | tr.odd.gradeX td.sorting_3 { 472 | background-color: #ffd1d1; 473 | } 474 | 475 | tr.even.gradeX td.sorting_1 { 476 | background-color: #ffd5d5; 477 | } 478 | 479 | tr.even.gradeX td.sorting_2 { 480 | background-color: #ffe2e2; 481 | } 482 | 483 | tr.even.gradeX td.sorting_3 { 484 | background-color: #ffe2e2; 485 | } 486 | 487 | tr.odd.gradeU td.sorting_1 { 488 | background-color: #c4c4c4; 489 | } 490 | 491 | tr.odd.gradeU td.sorting_2 { 492 | background-color: #d1d1d1; 493 | } 494 | 495 | tr.odd.gradeU td.sorting_3 { 496 | background-color: #d1d1d1; 497 | } 498 | 499 | tr.even.gradeU td.sorting_1 { 500 | background-color: #d5d5d5; 501 | } 502 | 503 | tr.even.gradeU td.sorting_2 { 504 | background-color: #e2e2e2; 505 | } 506 | 507 | tr.even.gradeU td.sorting_3 { 508 | background-color: #e2e2e2; 509 | } 510 | 511 | 512 | /* 513 | * Row highlighting example 514 | */ 515 | .ex_highlight #example tbody tr.even:hover, #example tbody tr.even td.highlighted { 516 | background-color: #ECFFB3; 517 | } 518 | 519 | .ex_highlight #example tbody tr.odd:hover, #example tbody tr.odd td.highlighted { 520 | background-color: #E6FF99; 521 | } 522 | -------------------------------------------------------------------------------- /public/js/d3.layout.cloud.js: -------------------------------------------------------------------------------- 1 | // Word cloud layout by Jason Davies, http://www.jasondavies.com/word-cloud/ 2 | // Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf 3 | (function(exports) { 4 | function cloud() { 5 | var size = [256, 256], 6 | text = cloudText, 7 | font = cloudFont, 8 | fontSize = cloudFontSize, 9 | fontStyle = cloudFontNormal, 10 | fontWeight = cloudFontNormal, 11 | rotate = cloudRotate, 12 | padding = cloudPadding, 13 | spiral = archimedeanSpiral, 14 | words = [], 15 | timeInterval = Infinity, 16 | event = d3.dispatch("word", "end"), 17 | timer = null, 18 | cloud = {}; 19 | 20 | cloud.start = function() { 21 | var board = zeroArray((size[0] >> 5) * size[1]), 22 | bounds = null, 23 | n = words.length, 24 | i = -1, 25 | tags = [], 26 | data = words.map(function(d, i) { 27 | d.text = text.call(this, d, i); 28 | d.font = font.call(this, d, i); 29 | d.style = fontStyle.call(this, d, i); 30 | d.weight = fontWeight.call(this, d, i); 31 | d.rotate = rotate.call(this, d, i); 32 | d.size = ~~fontSize.call(this, d, i); 33 | d.padding = cloudPadding.call(this, d, i); 34 | return d; 35 | }).sort(function(a, b) { return b.size - a.size; }); 36 | 37 | if (timer) clearInterval(timer); 38 | timer = setInterval(step, 0); 39 | step(); 40 | 41 | return cloud; 42 | 43 | function step() { 44 | var start = +new Date, 45 | d; 46 | while (+new Date - start < timeInterval && ++i < n && timer) { 47 | d = data[i]; 48 | d.x = (size[0] * (Math.random() + 1.5)) >> 2; 49 | d.y = (size[1] * (Math.random() + 3.5)) >> 3; 50 | cloudSprite(d, data, i); 51 | if (place(board, d, bounds)) { 52 | tags.push(d); 53 | event.word(d); 54 | if (bounds) cloudBounds(bounds, d); 55 | else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}]; 56 | // Temporary hack 57 | d.x -= size[0] >> 1; 58 | d.y -= size[1] >> 1; 59 | } 60 | } 61 | if (i >= n) { 62 | cloud.stop(); 63 | event.end(tags, bounds); 64 | } 65 | } 66 | } 67 | 68 | cloud.stop = function() { 69 | if (timer) { 70 | clearInterval(timer); 71 | timer = null; 72 | } 73 | return cloud; 74 | }; 75 | 76 | cloud.timeInterval = function(x) { 77 | if (!arguments.length) return timeInterval; 78 | timeInterval = x == null ? Infinity : x; 79 | return cloud; 80 | }; 81 | 82 | function place(board, tag, bounds) { 83 | var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}], 84 | startX = tag.x, 85 | startY = tag.y, 86 | maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), 87 | s = spiral(size), 88 | dt = Math.random() < .5 ? 1 : -1, 89 | t = -dt, 90 | dxdy, 91 | dx, 92 | dy; 93 | 94 | while (dxdy = s(t += dt)) { 95 | dx = ~~dxdy[0]; 96 | dy = ~~dxdy[1]; 97 | 98 | if (Math.min(dx, dy) > maxDelta) break; 99 | 100 | tag.x = startX + dx; 101 | tag.y = startY + dy; 102 | 103 | if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || 104 | tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue; 105 | // TODO only check for collisions within current bounds. 106 | if (!bounds || !cloudCollide(tag, board, size[0])) { 107 | if (!bounds || collideRects(tag, bounds)) { 108 | var sprite = tag.sprite, 109 | w = tag.width >> 5, 110 | sw = size[0] >> 5, 111 | lx = tag.x - (w << 4), 112 | sx = lx & 0x7f, 113 | msx = 32 - sx, 114 | h = tag.y1 - tag.y0, 115 | x = (tag.y + tag.y0) * sw + (lx >> 5), 116 | last; 117 | for (var j = 0; j < h; j++) { 118 | last = 0; 119 | for (var i = 0; i <= w; i++) { 120 | board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); 121 | } 122 | x += sw; 123 | } 124 | delete tag.sprite; 125 | return true; 126 | } 127 | } 128 | } 129 | return false; 130 | } 131 | 132 | cloud.words = function(x) { 133 | if (!arguments.length) return words; 134 | words = x; 135 | return cloud; 136 | }; 137 | 138 | cloud.size = function(x) { 139 | if (!arguments.length) return size; 140 | size = [+x[0], +x[1]]; 141 | return cloud; 142 | }; 143 | 144 | cloud.font = function(x) { 145 | if (!arguments.length) return font; 146 | font = d3.functor(x); 147 | return cloud; 148 | }; 149 | 150 | cloud.fontStyle = function(x) { 151 | if (!arguments.length) return fontStyle; 152 | fontStyle = d3.functor(x); 153 | return cloud; 154 | }; 155 | 156 | cloud.fontWeight = function(x) { 157 | if (!arguments.length) return fontWeight; 158 | fontWeight = d3.functor(x); 159 | return cloud; 160 | }; 161 | 162 | cloud.rotate = function(x) { 163 | if (!arguments.length) return rotate; 164 | rotate = d3.functor(x); 165 | return cloud; 166 | }; 167 | 168 | cloud.text = function(x) { 169 | if (!arguments.length) return text; 170 | text = d3.functor(x); 171 | return cloud; 172 | }; 173 | 174 | cloud.spiral = function(x) { 175 | if (!arguments.length) return spiral; 176 | spiral = spirals[x + ""] || x; 177 | return cloud; 178 | }; 179 | 180 | cloud.fontSize = function(x) { 181 | if (!arguments.length) return fontSize; 182 | fontSize = d3.functor(x); 183 | return cloud; 184 | }; 185 | 186 | cloud.padding = function(x) { 187 | if (!arguments.length) return padding; 188 | padding = d3.functor(x); 189 | return cloud; 190 | }; 191 | 192 | return d3.rebind(cloud, event, "on"); 193 | } 194 | 195 | function cloudText(d) { 196 | return d.text; 197 | } 198 | 199 | function cloudFont() { 200 | return "serif"; 201 | } 202 | 203 | function cloudFontNormal() { 204 | return "normal"; 205 | } 206 | 207 | function cloudFontSize(d) { 208 | return Math.sqrt(d.value); 209 | } 210 | 211 | function cloudRotate() { 212 | return (~~(Math.random() * 6) - 3) * 30; 213 | } 214 | 215 | function cloudPadding() { 216 | return 1; 217 | } 218 | 219 | // Fetches a monochrome sprite bitmap for the specified text. 220 | // Load in batches for speed. 221 | function cloudSprite(d, data, di) { 222 | if (d.sprite) return; 223 | c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); 224 | var x = 0, 225 | y = 0, 226 | maxh = 0, 227 | n = data.length; 228 | di--; 229 | while (++di < n) { 230 | d = data[di]; 231 | c.save(); 232 | c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font; 233 | var w = c.measureText(d.text + "m").width * ratio, 234 | h = d.size << 1; 235 | if (d.rotate) { 236 | var sr = Math.sin(d.rotate * cloudRadians), 237 | cr = Math.cos(d.rotate * cloudRadians), 238 | wcr = w * cr, 239 | wsr = w * sr, 240 | hcr = h * cr, 241 | hsr = h * sr; 242 | w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5; 243 | h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); 244 | } else { 245 | w = (w + 0x1f) >> 5 << 5; 246 | } 247 | if (h > maxh) maxh = h; 248 | if (x + w >= (cw << 5)) { 249 | x = 0; 250 | y += maxh; 251 | maxh = 0; 252 | } 253 | if (y + h >= ch) break; 254 | c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); 255 | if (d.rotate) c.rotate(d.rotate * cloudRadians); 256 | c.fillText(d.text, 0, 0); 257 | c.restore(); 258 | d.width = w; 259 | d.height = h; 260 | d.xoff = x; 261 | d.yoff = y; 262 | d.x1 = w >> 1; 263 | d.y1 = h >> 1; 264 | d.x0 = -d.x1; 265 | d.y0 = -d.y1; 266 | x += w; 267 | } 268 | var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, 269 | sprite = []; 270 | while (--di >= 0) { 271 | d = data[di]; 272 | var w = d.width, 273 | w32 = w >> 5, 274 | h = d.y1 - d.y0, 275 | p = d.padding; 276 | // Zero the buffer 277 | for (var i = 0; i < h * w32; i++) sprite[i] = 0; 278 | x = d.xoff; 279 | if (x == null) return; 280 | y = d.yoff; 281 | var seen = 0, 282 | seenRow = -1; 283 | for (var j = 0; j < h; j++) { 284 | for (var i = 0; i < w; i++) { 285 | var k = w32 * j + (i >> 5), 286 | m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0; 287 | if (p) { 288 | if (j) sprite[k - w32] |= m; 289 | if (j < w - 1) sprite[k + w32] |= m; 290 | m |= (m << 1) | (m >> 1); 291 | } 292 | sprite[k] |= m; 293 | seen |= m; 294 | } 295 | if (seen) seenRow = j; 296 | else { 297 | d.y0++; 298 | h--; 299 | j--; 300 | y++; 301 | } 302 | } 303 | d.y1 = d.y0 + seenRow; 304 | d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); 305 | } 306 | } 307 | 308 | // Use mask-based collision detection. 309 | function cloudCollide(tag, board, sw) { 310 | sw >>= 5; 311 | var sprite = tag.sprite, 312 | w = tag.width >> 5, 313 | lx = tag.x - (w << 4), 314 | sx = lx & 0x7f, 315 | msx = 32 - sx, 316 | h = tag.y1 - tag.y0, 317 | x = (tag.y + tag.y0) * sw + (lx >> 5), 318 | last; 319 | for (var j = 0; j < h; j++) { 320 | last = 0; 321 | for (var i = 0; i <= w; i++) { 322 | if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) 323 | & board[x + i]) return true; 324 | } 325 | x += sw; 326 | } 327 | return false; 328 | } 329 | 330 | function cloudBounds(bounds, d) { 331 | var b0 = bounds[0], 332 | b1 = bounds[1]; 333 | if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0; 334 | if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0; 335 | if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1; 336 | if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1; 337 | } 338 | 339 | function collideRects(a, b) { 340 | return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y; 341 | } 342 | 343 | function archimedeanSpiral(size) { 344 | var e = size[0] / size[1]; 345 | return function(t) { 346 | return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)]; 347 | }; 348 | } 349 | 350 | function rectangularSpiral(size) { 351 | var dy = 4, 352 | dx = dy * size[0] / size[1], 353 | x = 0, 354 | y = 0; 355 | return function(t) { 356 | var sign = t < 0 ? -1 : 1; 357 | // See triangular numbers: T_n = n * (n + 1) / 2. 358 | switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) { 359 | case 0: x += dx; break; 360 | case 1: y += dy; break; 361 | case 2: x -= dx; break; 362 | default: y -= dy; break; 363 | } 364 | return [x, y]; 365 | }; 366 | } 367 | 368 | // TODO reuse arrays? 369 | function zeroArray(n) { 370 | var a = [], 371 | i = -1; 372 | while (++i < n) a[i] = 0; 373 | return a; 374 | } 375 | 376 | var cloudRadians = Math.PI / 180, 377 | cw = 1 << 11 >> 5, 378 | ch = 1 << 11, 379 | canvas, 380 | ratio = 1; 381 | 382 | if (typeof document !== "undefined") { 383 | canvas = document.createElement("canvas"); 384 | canvas.width = 1; 385 | canvas.height = 1; 386 | ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2); 387 | canvas.width = (cw << 5) / ratio; 388 | canvas.height = ch / ratio; 389 | } else { 390 | // node-canvas support 391 | var Canvas = require("canvas"); 392 | canvas = new Canvas(cw << 5, ch); 393 | } 394 | 395 | var c = canvas.getContext("2d"), 396 | spirals = { 397 | archimedean: archimedeanSpiral, 398 | rectangular: rectangularSpiral 399 | }; 400 | c.fillStyle = "red"; 401 | c.textAlign = "center"; 402 | 403 | exports.cloud = cloud; 404 | })(typeof exports === "undefined" ? d3.layout || (d3.layout = {}) : exports); 405 | --------------------------------------------------------------------------------