require 'uri' require 'open-uri' require 'open_uri_redirections' require 'nokogiri' require 'cinch' require 'sqlite3' require 'time-lord' require 'json' require 'mastodon' require 'yt' require 'yaml' # require 'bitly' # Bitly.use_api_version_3 $db = SQLite3::Database.new "sotd.db" rows = $db.execute <<-SQL CREATE TABLE IF NOT EXISTS sotd ( id INTEGER PRIMARY KEY, username varchar(32), nick varchar(32), link TEXT, display TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); SQL $config = YAML::load_file('/home/caff/code/sotdbot/config/config.yml') Yt.configure do |config| config.api_key = $config['youtube']['api_key'] end SotdHelp = Class.new do include Cinch::Plugin match "sotdhelp" match "rollcall" match("sotdbot: help", { :use_prefix => false }) def execute(m) m.reply "SOTDBot keeps track of your songs of the day. You can update your SOTD by running !supdate , where link is a YouTube, Soundcloud, Archive.org, Monstercat, Spotify, or Bandcamp link, and !sotd retrieves the latest SOTDs, !sotd gets the last SOTD by that user, and !sotdstats gets the top users of sotdbot. Also, !allmysotd will retrieve your previous songs of the day, but must be messaged to sotdbot directly." end end module Invisibleify def self.invisibleify(str) str.gsub(/(..?)(.+)/, "\\1\u{200C}\\2") end end module SotdParse def self.parse(link) @display = "" if !link.host || !link.port then return 1 elsif /youtu(?:be)?\.(?:be|com)/i =~ link.host then ytl = Yt::URL.new link.to_s @display = ytl.resource.title elsif /bandcamp\.com/ =~ link.host then @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe)) @name = @doc.css(".trackTitle")[0].content.strip @artist = @doc.css("[itemprop='byArtist']")[0].content.strip @display = "#{@artist} - #{@name}" elsif /soundcloud\.com/ =~ link.host then @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe)) @display = @doc.css("[itemprop='name']")[0].content.gsub(/[\n\t\s]+/, ' ').strip # Parsing Archive.org is temporarily broken :( Sorry elsif /archive\.org/ =~ link.host then return 2 # @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe), nil, "UTF-8") # @display = @doc.css(".playing .ttl")[0].content.strip # # @artist = @doc.css(".details-metadata [itemprop='creator']")[0].content.strip # # @title = @doc.css(".details-metadata [itemprop='name']")[0].content.strip # # @display = "#{@artist} - #{@title}" elsif /monstercat\.com/ =~ link.host and /release\/.+/ =~ link.path then @release = /release\/([A-Za-z0-9]+)/.match(link.path).captures[0] @data = JSON.parse(open("https://connect.monstercat.com/api/catalog/release/#{@release}", :allow_redirections => :safe).read) @display = "#{@data["renderedArtists"]} - #{@data["title"]}" elsif /vimeo\.com/ =~ link.host then @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe)) @display = @doc.css("meta[property='og:title']")[0]["content"] elsif /open.spotify.com/ =~ link.host then @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe)) @title = @doc.css(".media-bd h1")[0].content.strip @artist = @doc.css(".media-bd h2 a")[0].content.strip @display = "#{@artist} - #{@title}" else return 2 end return @display end end SotdTest = Class.new do include Cinch::Plugin extend SotdParse match /sotdtest (.+)/ def execute(m, link) @link = URI.parse(link) @display = SotdParse.parse(@link) if @display == 2 then m.reply "No valid parser found." elsif @display == 1 then m.reply "Invalid link." else m.reply "Link parsed to this display: #{@display}" end end end RegenHTML = Class.new do include Cinch::Plugin match "regenhtml" def execute(m) if m.user.user == "~caff" then @rows = $db.execute "SELECT sotd.username, sotd.display, sotd.link, sotd.created_at FROM sotd ORDER BY sotd.username DESC, sotd.created_at DESC" @rows.each do |row| filename = "/home/caff/public_html/sotd/.partials/#{row[0]}.mustache" html = "#{row[1]}#{row[3]}" File.open(filename, "a") { |f| f.write(html) } end end end end SotdUpdate = Class.new do include Cinch::Plugin extend SotdParse match /supdate ([^ ]+)(?: (.+))?/ match /sotd (https?:\/\/[^ ]+)(?: (.+))?/ match /sotdupdate ([^ ]+)(?: (.+))?/ def execute(m, link, desc) @link = URI.parse(link) unless desc @display = SotdParse.parse(@link) else @display = desc end if @display == 2 then m.reply "This doesn't look like a supported link. Let ~caff know if this should be added, or rerun SOTD adding a title to this link. !supdate " elsif @display == 1 then m.reply "This doesn't appear to be a valid link. Sorry!" elsif !@display.is_a? String then m.reply "Something seems to have gone wrong. Let ~caff know." else rows = $db.execute "insert into sotd ( username, nick, link, display ) VALUES ( ?, ?, ?, ? )", [m.user.user, m.user.nick, @link.to_s, @display] m.reply "Done! Your song of the day has been updated to #{@display}" # bitly = Bitly.new($config['bitly']['login'], $config['bitly']['api_key']) # shortlink = bitly.shorten(@link.to_s, :history => 1) client = Mastodon::REST::Client.new(base_url: 'https://tiny.tilde.website', bearer_token: $config['mastodon']['access_token']) client.create_status("#{m.user.user} has updated their #SOTD: #{@display} < #{@link.to_s} >") end end end class SotdStats include Cinch::Plugin extend Invisibleify # match "sotdstats" match /sotdstats/ def execute(m) rows = $db.execute <<-SQL SELECT username, COUNT(username) FROM sotd GROUP BY username ORDER BY COUNT(username) DESC LIMIT 10; SQL totalcount = $db.execute <<-SQL SELECT COUNT(id) FROM sotd; SQL rep = rows.map { |r| "#{Invisibleify.invisibleify(r[0])}: #{r[1]}" } m.reply "Top SOTD users: #{ rep * ", " }" m.reply "Total SOTD count: #{totalcount[0][0]}" end end class SotdGet include Cinch::Plugin extend Invisibleify match "sotd" match /sotd ([a-zA-Z0-9]+)/ match /listsotd/ def execute(m, username = "") if username.to_s.empty? then if m.channel? and m.channel.name == "#tildetown" then m.reply "Check out http://tilde.town/~severak/town_radio.html, or run this command in another channel such as #music, #bots, or #sotd for a list of current SOTDs" else rows = $db.execute <<-SQL SELECT sotd.username, sotd.display, sotd.link, sotd.created_at FROM sotd sotd INNER JOIN ( SELECT MAX(created_at) created_at, username FROM sotd WHERE created_at > DATETIME('now', '-2 days') GROUP BY username ) AS s1 ON sotd.username = s1.username AND sotd.created_at = s1.created_at ORDER BY sotd.created_at DESC SQL rows.each do |row| time = DateTime.parse(row[3]).to_time period = TimeLord::Period.new(time, Time.now).to_words m.reply "#{Invisibleify.invisibleify(row[0])}: #{row[1]} <#{row[2]}> (#{period})" sleep 0.25 end end else rows = $db.execute <<-SQL SELECT username, display, link, created_at FROM sotd WHERE username='#{username}' OR username='~#{username}' ORDER BY created_at DESC LIMIT 1; SQL rows.each do |row| time = DateTime.parse(row[3]).to_time period = TimeLord::Period.new(time, Time.now).to_words m.reply "#{Invisibleify.invisibleify(row[0])}: #{row[1]} <#{row[2]}> (#{period})" sleep 0.25 end end end end class SotdAll include Cinch::Plugin match /allmysotd(?: (\d+))?/ def execute(m, page = 0) if m.channel? then return m.reply "Please message this command to me directly as it is quite verbose. Try /msg sotdbot !allmysotd <page>" end page ||= 1 page = [page.to_i - 1, 0].max pagelength = 10.to_f usercount = $db.execute "SELECT COUNT(*) FROM SOTD WHERE username='#{m.user.user}' LIMIT 1;" count = usercount[0][0] @totalpages = count / pagelength @totalpages = @totalpages.ceil if count < (page.to_i * pagelength) then return m.reply "No more pages." else rows = $db.execute <<-SQL SELECT display, link, created_at FROM sotd WHERE username='#{m.user.user}' ORDER BY created_at DESC LIMIT #{pagelength} OFFSET #{pagelength * page}; SQL rows.each do |row| time = DateTime.parse(row[2]).to_time period = TimeLord::Period.new(time, Time.now).to_words m.reply "#{row[0]} <#{row[1]}> (#{period})" sleep 0.25 end m.reply("Page #{page + 1} of #{@totalpages} (#{count} songs total)") end end end bot = Cinch::Bot.new do configure do |c| c.server = "localhost" if ARGV.include? "--debug" then c.channels = ["#sotd"] else c.channels = ["#tildetown", "#bots", "#sotd", "#music"] end c.nick = "sotdbot" c.realname = "sotdbot" c.user = "sotdbot" c.plugins.prefix = "!" c.plugins.plugins = [ SotdHelp, SotdUpdate, SotdStats, SotdGet, SotdTest, SotdAll, RegenHTML ] end end bot.start