123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- 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 <link>, where link is a YouTube, Soundcloud, Archive.org, Monstercat, Spotify, or Bandcamp link, and !sotd retrieves the latest SOTDs, !sotd <username> gets the last SOTD by that user, and !sotdstats gets the top users of sotdbot. Also, !allmysotd <page> 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 = "<tr><td><a href=\"#{row[2]}\">#{row[1]}</a></td><td>#{row[3]}</td></tr>"
- 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 <link> <title>"
- 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
|