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.

main.rb 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. require 'uri'
  2. require 'open-uri'
  3. require 'open_uri_redirections'
  4. require 'nokogiri'
  5. require 'cinch'
  6. require 'sqlite3'
  7. require 'time-lord'
  8. require 'json'
  9. require 'mastodon'
  10. require 'yt'
  11. require 'yaml'
  12. # require 'bitly'
  13. # Bitly.use_api_version_3
  14. $db = SQLite3::Database.new "sotd.db"
  15. rows = $db.execute <<-SQL
  16. CREATE TABLE IF NOT EXISTS sotd (
  17. id INTEGER PRIMARY KEY,
  18. username varchar(32),
  19. nick varchar(32),
  20. link TEXT,
  21. display TEXT,
  22. created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  23. );
  24. SQL
  25. $config = YAML::load_file('/home/caff/code/sotdbot/config/config.yml')
  26. Yt.configure do |config|
  27. config.api_key = $config['youtube']['api_key']
  28. end
  29. SotdHelp = Class.new do
  30. include Cinch::Plugin
  31. match "sotdhelp"
  32. match "rollcall"
  33. match("sotdbot: help", {
  34. :use_prefix => false
  35. })
  36. def execute(m)
  37. 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."
  38. end
  39. end
  40. module Invisibleify
  41. def self.invisibleify(str)
  42. str.gsub(/(..?)(.+)/, "\\1\u{200C}\\2")
  43. end
  44. end
  45. module SotdParse
  46. def self.parse(link)
  47. @display = ""
  48. if !link.host || !link.port then
  49. return 1
  50. elsif /youtu(?:be)?\.(?:be|com)/i =~ link.host then
  51. ytl = Yt::URL.new link.to_s
  52. @display = ytl.resource.title
  53. elsif /bandcamp\.com/ =~ link.host then
  54. @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe))
  55. @name = @doc.css(".trackTitle")[0].content.strip
  56. @artist = @doc.css("[itemprop='byArtist']")[0].content.strip
  57. @display = "#{@artist} - #{@name}"
  58. elsif /soundcloud\.com/ =~ link.host then
  59. @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe))
  60. @display = @doc.css("[itemprop='name']")[0].content.gsub(/[\n\t\s]+/, ' ').strip
  61. # Parsing Archive.org is temporarily broken :( Sorry
  62. elsif /archive\.org/ =~ link.host then
  63. return 2
  64. # @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe), nil, "UTF-8")
  65. # @display = @doc.css(".playing .ttl")[0].content.strip
  66. # # @artist = @doc.css(".details-metadata [itemprop='creator']")[0].content.strip
  67. # # @title = @doc.css(".details-metadata [itemprop='name']")[0].content.strip
  68. # # @display = "#{@artist} - #{@title}"
  69. elsif /monstercat\.com/ =~ link.host and /release\/.+/ =~ link.path then
  70. @release = /release\/([A-Za-z0-9]+)/.match(link.path).captures[0]
  71. @data = JSON.parse(open("https://connect.monstercat.com/api/catalog/release/#{@release}", :allow_redirections => :safe).read)
  72. @display = "#{@data["renderedArtists"]} - #{@data["title"]}"
  73. elsif /vimeo\.com/ =~ link.host then
  74. @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe))
  75. @display = @doc.css("meta[property='og:title']")[0]["content"]
  76. elsif /open.spotify.com/ =~ link.host then
  77. @doc = Nokogiri::HTML(open(link.to_s, :allow_redirections => :safe))
  78. @title = @doc.css(".media-bd h1")[0].content.strip
  79. @artist = @doc.css(".media-bd h2 a")[0].content.strip
  80. @display = "#{@artist} - #{@title}"
  81. else
  82. return 2
  83. end
  84. return @display
  85. end
  86. end
  87. SotdTest = Class.new do
  88. include Cinch::Plugin
  89. extend SotdParse
  90. match /sotdtest (.+)/
  91. def execute(m, link)
  92. @link = URI.parse(link)
  93. @display = SotdParse.parse(@link)
  94. if @display == 2 then
  95. m.reply "No valid parser found."
  96. elsif @display == 1 then
  97. m.reply "Invalid link."
  98. else
  99. m.reply "Link parsed to this display: #{@display}"
  100. end
  101. end
  102. end
  103. RegenHTML = Class.new do
  104. include Cinch::Plugin
  105. match "regenhtml"
  106. def execute(m)
  107. if m.user.user == "~caff" then
  108. @rows = $db.execute "SELECT sotd.username, sotd.display, sotd.link, sotd.created_at FROM sotd ORDER BY sotd.username DESC, sotd.created_at DESC"
  109. @rows.each do |row|
  110. filename = "/home/caff/public_html/sotd/.partials/#{row[0]}.mustache"
  111. html = "<tr><td><a href=\"#{row[2]}\">#{row[1]}</a></td><td>#{row[3]}</td></tr>"
  112. File.open(filename, "a") { |f| f.write(html) }
  113. end
  114. end
  115. end
  116. end
  117. SotdUpdate = Class.new do
  118. include Cinch::Plugin
  119. extend SotdParse
  120. match /supdate ([^ ]+)(?: (.+))?/
  121. match /sotd (https?:\/\/[^ ]+)(?: (.+))?/
  122. match /sotdupdate ([^ ]+)(?: (.+))?/
  123. def execute(m, link, desc)
  124. @link = URI.parse(link)
  125. unless desc
  126. @display = SotdParse.parse(@link)
  127. else
  128. @display = desc
  129. end
  130. if @display == 2 then
  131. 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>"
  132. elsif @display == 1 then
  133. m.reply "This doesn't appear to be a valid link. Sorry!"
  134. elsif !@display.is_a? String then
  135. m.reply "Something seems to have gone wrong. Let ~caff know."
  136. else
  137. rows = $db.execute "insert into sotd ( username, nick, link, display ) VALUES ( ?, ?, ?, ? )", [m.user.user, m.user.nick, @link.to_s, @display]
  138. m.reply "Done! Your song of the day has been updated to #{@display}"
  139. # bitly = Bitly.new($config['bitly']['login'], $config['bitly']['api_key'])
  140. # shortlink = bitly.shorten(@link.to_s, :history => 1)
  141. client = Mastodon::REST::Client.new(base_url: 'https://tiny.tilde.website', bearer_token: $config['mastodon']['access_token'])
  142. client.create_status("#{m.user.user} has updated their #SOTD: #{@display} < #{@link.to_s} >")
  143. end
  144. end
  145. end
  146. class SotdStats
  147. include Cinch::Plugin
  148. extend Invisibleify
  149. # match "sotdstats"
  150. match /sotdstats/
  151. def execute(m)
  152. rows = $db.execute <<-SQL
  153. SELECT username, COUNT(username) FROM sotd
  154. GROUP BY username
  155. ORDER BY COUNT(username) DESC
  156. LIMIT 10;
  157. SQL
  158. totalcount = $db.execute <<-SQL
  159. SELECT COUNT(id) FROM sotd;
  160. SQL
  161. rep = rows.map { |r| "#{Invisibleify.invisibleify(r[0])}: #{r[1]}" }
  162. m.reply "Top SOTD users: #{ rep * ", " }"
  163. m.reply "Total SOTD count: #{totalcount[0][0]}"
  164. end
  165. end
  166. class SotdGet
  167. include Cinch::Plugin
  168. extend Invisibleify
  169. match "sotd"
  170. match /sotd ([a-zA-Z0-9]+)/
  171. match /listsotd/
  172. def execute(m, username = "")
  173. if username.to_s.empty? then
  174. if m.channel? and m.channel.name == "#tildetown" then
  175. 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"
  176. else
  177. rows = $db.execute <<-SQL
  178. SELECT sotd.username, sotd.display, sotd.link, sotd.created_at
  179. FROM sotd sotd
  180. INNER JOIN (
  181. SELECT MAX(created_at) created_at, username
  182. FROM sotd
  183. WHERE created_at > DATETIME('now', '-2 days')
  184. GROUP BY username
  185. ) AS s1
  186. ON sotd.username = s1.username
  187. AND sotd.created_at = s1.created_at
  188. ORDER BY sotd.created_at DESC
  189. SQL
  190. rows.each do |row|
  191. time = DateTime.parse(row[3]).to_time
  192. period = TimeLord::Period.new(time, Time.now).to_words
  193. m.reply "#{Invisibleify.invisibleify(row[0])}: #{row[1]} <#{row[2]}> (#{period})"
  194. sleep 0.25
  195. end
  196. end
  197. else
  198. rows = $db.execute <<-SQL
  199. SELECT username, display, link, created_at
  200. FROM sotd
  201. WHERE username='#{username}' OR username='~#{username}'
  202. ORDER BY created_at DESC
  203. LIMIT 1;
  204. SQL
  205. rows.each do |row|
  206. time = DateTime.parse(row[3]).to_time
  207. period = TimeLord::Period.new(time, Time.now).to_words
  208. m.reply "#{Invisibleify.invisibleify(row[0])}: #{row[1]} <#{row[2]}> (#{period})"
  209. sleep 0.25
  210. end
  211. end
  212. end
  213. end
  214. class SotdAll
  215. include Cinch::Plugin
  216. match /allmysotd(?: (\d+))?/
  217. def execute(m, page = 0)
  218. if m.channel? then
  219. return m.reply "Please message this command to me directly as it is quite verbose. Try /msg sotdbot !allmysotd <page>"
  220. end
  221. page ||= 1
  222. page = [page.to_i - 1, 0].max
  223. pagelength = 10.to_f
  224. usercount = $db.execute "SELECT COUNT(*) FROM SOTD WHERE username='#{m.user.user}' LIMIT 1;"
  225. count = usercount[0][0]
  226. @totalpages = count / pagelength
  227. @totalpages = @totalpages.ceil
  228. if count < (page.to_i * pagelength) then
  229. return m.reply "No more pages."
  230. else
  231. rows = $db.execute <<-SQL
  232. SELECT display, link, created_at
  233. FROM sotd
  234. WHERE username='#{m.user.user}'
  235. ORDER BY created_at DESC
  236. LIMIT #{pagelength}
  237. OFFSET #{pagelength * page};
  238. SQL
  239. rows.each do |row|
  240. time = DateTime.parse(row[2]).to_time
  241. period = TimeLord::Period.new(time, Time.now).to_words
  242. m.reply "#{row[0]} <#{row[1]}> (#{period})"
  243. sleep 0.25
  244. end
  245. m.reply("Page #{page + 1} of #{@totalpages} (#{count} songs total)")
  246. end
  247. end
  248. end
  249. bot = Cinch::Bot.new do
  250. configure do |c|
  251. c.server = "localhost"
  252. if ARGV.include? "--debug" then
  253. c.channels = ["#sotd"]
  254. else
  255. c.channels = ["#tildetown", "#bots", "#sotd", "#music"]
  256. end
  257. c.nick = "sotdbot"
  258. c.realname = "sotdbot"
  259. c.user = "sotdbot"
  260. c.plugins.prefix = "!"
  261. c.plugins.plugins = [
  262. SotdHelp, SotdUpdate, SotdStats, SotdGet, SotdTest, SotdAll, RegenHTML
  263. ]
  264. end
  265. end
  266. bot.start