class Joke
Constants
- FUNNIES_BASE_URL
- FUNNIES_BOILERPLATE_PATTERNS
- JOKE_SOURCES
- LEGACY_SEEN_PATH
- MAX_SEEN_IDS
- NO_JOKE_MESSAGE
- SEEN_KEY
- SEEN_PLUGIN
- USER_AGENT
- WINDOW_SECONDS
Public Class Methods
Source
# File plugins/joke.rb, line 244 def self.complete_joke_post(post, fetch:) return post unless post[:body].to_s.strip.empty? page = fetch.call(post[:url]) parse_funnies_page(page, post) rescue StandardError post end
Source
# File plugins/joke.rb, line 117 def self.extract_description_body(description, title:) text = Nokogiri::HTML.fragment(description.to_s).text.gsub(/\s+/, " ").strip title_text = title.to_s.strip return "" if text.empty? return "" if text.casecmp?(title_text) if text.start_with?(title_text) remainder = text[title_text.length..].strip remainder.sub(/\A[:\-—]\s*/, "").strip else text end end
Source
# File plugins/joke.rb, line 207 def self.extract_funnies_body(content_node) return "" unless content_node parts = [] content_node.css("p").each do |paragraph| next if funnies_boilerplate_paragraph?(paragraph) text = paragraph.text.to_s.gsub(/\s+/, " ").strip next if text.empty? || funnies_boilerplate_text?(text) parts << text end content_node.css("ul:not(.social-icons) > li").each do |item| text = item.text.to_s.gsub(/\s+/, " ").strip next if text.empty? || funnies_boilerplate_text?(text) parts << text end parts.join(" ") end
Source
# File plugins/joke.rb, line 60 def self.feed_urls JOKE_SOURCES.map { |source| source[:url] } end
Source
# File plugins/joke.rb, line 282 def self.fetch_source_posts(source, fetch:, now: Time.now) case source[:type] when :funnies_index html = fetch.call(source[:url]) recent_posts(parse_funnies_index(html, source: source, now: now), now: now) else feed = fetch.call(source[:url]) recent_posts(parse_feed(feed, source: source), now: now) end rescue StandardError [] end
Source
# File plugins/joke.rb, line 262 def self.format_joke(post) title = strip_urls(post[:title]) body = strip_urls(post[:body]) body.empty? ? title : "#{title} — #{body}" end
Source
# File plugins/joke.rb, line 151 def self.funnies_absolute_url(href, base: FUNNIES_BASE_URL) path = href.to_s.strip return path if path.match?(/\Ahttps?:\/\//i) URI.join(base, path).to_s end
Source
# File plugins/joke.rb, line 200 def self.funnies_boilerplate_paragraph?(node) classes = node["class"].to_s.split return true if (%w[trail more-link] & classes).any? funnies_boilerplate_text?(node.text) end
Source
# File plugins/joke.rb, line 193 def self.funnies_boilerplate_text?(text) normalized = text.to_s.gsub(/\s+/, " ").strip return true if normalized.empty? FUNNIES_BOILERPLATE_PATTERNS.any? { |pattern| normalized.match?(pattern) } end
Source
# File plugins/joke.rb, line 158 def self.funnies_post_id(href) path = funnies_absolute_url(href) File.basename(URI.parse(path).path, ".*") end
Source
# File plugins/joke.rb, line 143 def self.joke_page_path?(href) path = href.to_s.strip return false if path.empty? || path.start_with?("#", "mailto:") return false if path.match?(/\Ahttps?:\/\//i) && !path.start_with?(FUNNIES_BASE_URL) path.match?(/\.(?:html?|php)\z/i) end
Source
# File plugins/joke.rb, line 308 def self.legacy_seen_path @legacy_seen_path || LEGACY_SEEN_PATH end
Source
# File plugins/joke.rb, line 312 def self.legacy_seen_path=(path) @legacy_seen_path = path end
Source
# File plugins/joke.rb, line 332 def self.load_seen_ids migrate_legacy_seen_json! Array(RabbotDb.get_plugin_json(plugin: SEEN_PLUGIN, key: SEEN_KEY, default: [])).map(&:to_s) end
Source
# File plugins/joke.rb, line 316 def self.migrate_legacy_seen_json!(path: legacy_seen_path) return unless File.file?(path) existing = RabbotDb.get_plugin_json(plugin: SEEN_PLUGIN, key: SEEN_KEY, default: []) return unless existing.empty? data = JSON.parse(File.read(path)) ids = data.is_a?(Array) ? data : data.fetch("seen_ids", []) save_seen_ids(ids) backup = "#{path}.migrated" File.rename(path, backup) unless File.exist?(backup) rescue JSON::ParserError, StandardError nil end
Source
# File plugins/joke.rb, line 344 def initialize(*args) super @seen_ids = self.class.load_seen_ids end
Calls superclass method
Source
# File plugins/joke.rb, line 81 def self.normalize_wordpress_item(item, source:) title = rss_element_text(item, "title") return nil if title.empty? return nil if title.match?(/\A\[(?:removed|deleted)\]\z/i) raw_id = rss_element_text(item, "guid") id = wordpress_post_id(raw_id) id = rss_element_text(item, "link") if id.empty? return nil if id.empty? link = rss_element_text(item, "link") pub_date = rss_element_text(item, "pubDate") created_utc = parse_rss_timestamp(pub_date) description = rss_element_text(item, "description") body = extract_description_body(description, title: title) { id: id, title: title, body: body, created_utc: created_utc, url: link, source: source[:name], region: source[:region] } end
Source
# File plugins/joke.rb, line 64 def self.parse_feed(body, source:) text = body.to_s.lstrip return [] if text.empty? return [] unless text[0] == "<" parse_rss(text, source: source) end
Source
# File plugins/joke.rb, line 163 def self.parse_funnies_index(html, source:, now: Time.now) doc = Nokogiri::HTML(html.to_s) posts = [] seen_ids = {} doc.css("a[href]").each do |anchor| href = anchor["href"] next unless joke_page_path?(href) title = anchor.text.to_s.gsub(/\s+/, " ").strip next if title.empty? id = funnies_post_id(href) next if seen_ids[id] seen_ids[id] = true posts << { id: id, title: title.sub(/\AUpdated:\s*/i, "").strip, body: "", created_utc: now.to_i, url: funnies_absolute_url(href), source: source[:name], region: source[:region] } end posts end
Source
# File plugins/joke.rb, line 231 def self.parse_funnies_page(html, stub) doc = Nokogiri::HTML(html.to_s) content = doc.at_css(".item.content") || doc.at_css("div.content") || doc title = content.css("h1").map { |heading| heading.text.to_s.strip } .reject { |text| text.casecmp?("Funny Shit") } .first title = stub[:title].to_s.strip if title.to_s.empty? body = extract_funnies_body(content) stub.merge(title: title, body: body) end
Source
# File plugins/joke.rb, line 72 def self.parse_rss(xml, source:) doc = REXML::Document.new(xml) REXML::XPath.match(doc, "//*[local-name()='item']").filter_map do |item| normalize_wordpress_item(item, source: source) end rescue REXML::ParseException, StandardError [] end
Source
# File plugins/joke.rb, line 131 def self.parse_rss_timestamp(timestamp) return 0 if timestamp.to_s.strip.empty? Time.parse(timestamp).to_i rescue ArgumentError 0 end
Source
# File plugins/joke.rb, line 295 def self.pick_joke(seen_ids:, fetch:, now: Time.now, rng: Random) posts = JOKE_SOURCES.flat_map do |source| fetch_source_posts(source, fetch: fetch, now: now) end joke = select_joke(posts, seen_ids: seen_ids, rng: rng) return { error: NO_JOKE_MESSAGE } unless joke joke = complete_joke_post(joke, fetch: fetch) { text: format_joke(joke), id: joke[:id] } rescue StandardError { error: NO_JOKE_MESSAGE } end
Source
# File plugins/joke.rb, line 253 def self.recent_posts(posts, now: Time.now, window_seconds: WINDOW_SECONDS) cutoff = now.to_i - window_seconds posts.select { |post| post[:created_utc] >= cutoff } end
Source
# File plugins/joke.rb, line 139 def self.rss_element_text(entry, name) REXML::XPath.first(entry, ".//*[local-name()='#{name}']")&.text.to_s.strip end
Source
# File plugins/joke.rb, line 337 def self.save_seen_ids(seen_ids) ids = seen_ids.map(&:to_s).uniq ids = ids.last(MAX_SEEN_IDS) if ids.length > MAX_SEEN_IDS RabbotDb.set_plugin_json(plugin: SEEN_PLUGIN, key: SEEN_KEY, value: ids) end
Source
# File plugins/joke.rb, line 274 def self.select_joke(posts, seen_ids:, rng: Random) pool = unseen_posts(posts, seen_ids: seen_ids) pool = posts if pool.empty? && !posts.empty? queensland = pool.select { |post| post[:region] == :queensland } pool = queensland unless queensland.empty? pool.sample(random: rng) end
Source
# File plugins/joke.rb, line 258 def self.strip_urls(text) text.to_s.gsub(%r{https?://\S+}i, "").gsub(/\s+/, " ").strip end
Source
# File plugins/joke.rb, line 269 def self.unseen_posts(posts, seen_ids:) seen = seen_ids.map(&:to_s) posts.reject { |post| seen.include?(post[:id].to_s) } end
Source
# File plugins/joke.rb, line 108 def self.wordpress_post_id(raw_id) raw = raw_id.to_s.strip if (match = raw.match(/[?&]p=([^&]+)/)) match[1] else raw end end
Public Instance Methods
Source
# File plugins/joke.rb, line 357 def execute(m, _match = nil) result = pick_joke if result[:error] flood_safe_reply(m, result[:error]) return end @seen_ids << result[:id] unless @seen_ids.include?(result[:id]) self.class.save_seen_ids(@seen_ids) flood_safe_reply(m, result[:text]) end
Source
# File plugins/joke.rb, line 353 def fetch_feed(url) RabbotHttp.fetch(url, user_agent: USER_AGENT) end
Source
# File plugins/joke.rb, line 349 def pick_joke(fetch: method(:fetch_feed), now: Time.now, rng: Random) self.class.pick_joke(seen_ids: @seen_ids, fetch: fetch, now: now, rng: rng) end