A SOTDBot for Tilde.Town's IRC.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
9.4 KiB

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