class Pizza
Constants
- MULTI_WORD_PIZZAS
- REJECT_PLACE_PATTERN
Public Class Methods
Source
# File plugins/pizza.rb, line 119 def self.clean_place_title(title) text = title.to_s.strip return "" if text.empty? if text.match?(/\A(?:home|menu|about us)\s*\|\s*/i) text = text.split(/\s*\|\s*/, 2).last.to_s else text = text.split(/\s*\|\s*/).first.to_s end text = text.sub(/\s+in\s+.+$/i, "").strip text.sub(/\s+-\s+.+$/, "").strip end
Source
# File plugins/pizza.rb, line 194 def self.extract_best_place_from_ddg_html(html, town:) doc = Nokogiri::HTML(html.to_s) candidates = doc.css(".result").filter_map do |result| title = result.at_css(".result__a")&.text&.strip href = Duck.clean_url(result.at_css(".result__a")&.[]("href")) place_candidate(title, href, town: town) end candidates.max_by { |candidate| candidate[:score] }&.dig(:name) end
Source
# File plugins/pizza.rb, line 154 def self.extract_name_from_facebook_title(title) if (match = title.match(/-\s*(.+?)\s*\|\s*Facebook\z/i)) return clean_place_title(match[1]) end if (match = title.match(/\A(.+?)\s*\|\s*.+\s+-\s*Facebook\z/i)) return clean_place_title(match[1]) end if (match = title.match(/\A(?:home\s*\|\s*)?(.+?)\s*\|\s*Facebook\z/i)) return clean_place_title(match[1]) end nil end
Source
# File plugins/pizza.rb, line 205 def self.extract_place_from_ddg_html(html, town:) extract_best_place_from_ddg_html(html, town: town) end
Source
# File plugins/pizza.rb, line 133 def self.extract_place_name(text) text = text.to_s.strip return nil if text.empty? || text == "No results found" text = text.sub(/\s*\(https?:\/\/[^)]+\)\z/, "").strip if (match = text.match(/\A(.+?)\s+(?:went viral|is the most viral|viral on|on TikTok)/i)) candidate = clean_place_title(match[1]) return candidate if valid_place_name?(candidate) end snippet = text if (match = snippet.match(/\A(.+?)\s+is\s+(?:a|the)\s+/i)) candidate = clean_place_title(match[1]) return candidate if valid_place_name?(candidate) end fallback = clean_place_title(snippet.split(/[.(]/).first.to_s) valid_place_name?(fallback) ? fallback : nil end
Source
# File plugins/pizza.rb, line 226 def self.extract_toppings(text) if (match = text.match(/toppings?:\s*([^.]+)/i)) return match[1].strip end if (match = text.match(/topped with\s+([^.]+)/i)) return match[1].strip end if (match = text.match(/with\s+([a-z ,]+(?:and\s+[a-z]+)?)\./i)) candidate = match[1].strip return candidate if candidate.match?(/\b(pepperoni|ham|bacon|mushroom|olive|cheese|capsicum|pineapple)\b/i) end nil end
Source
# File plugins/pizza.rb, line 209 def self.find_place(town, fetch:) place_search_queries(town).each do |query| if query.include?("viral") viral_text = Duck.search(query, fetch: fetch) viral_place = extract_place_name(viral_text) return [viral_place, :viral] if viral_place else html = fetch.call(query) place = extract_best_place_from_ddg_html(html, town: town) source = query.include?("facebook") ? :social : :local return [place, source] if place end end nil end
Source
# File plugins/pizza.rb, line 270 def self.format_announcement(place, details, source: :viral) "Found #{details[:name]} at #{place}#{source_tag(source)}: #{details[:price]}, est. #{details[:duration]}, toppings: #{details[:toppings]}" end
Source
# File plugins/pizza.rb, line 287 def self.lookup(town, pizza, fetch:) place_info = find_place(town, fetch: fetch) return "Could not find a pizza place in #{town}" unless place_info place, source = place_info pizza_result = Duck.search(pizza_query(place, town, pizza), fetch: fetch) details = parse_pizza_details(pizza_result, pizza_name: pizza) return menu_fallback_message(place, pizza, pizza_result, source: source) unless details format_announcement(place, details, source: source) rescue StandardError "Could not find a pizza place in #{town}" end
Source
# File plugins/pizza.rb, line 57 def self.parse_args(text) text = text.to_s.strip return { error: usage_message } if text.empty? if text.match?(%r{\A["']}) unless (match = text.match(%r{\A["'](.+?)["']\s+(.+)\z})) return { error: usage_message } end town = match[1].strip pizza = match[2].strip return { error: usage_message } if town.empty? || pizza.empty? return { town: town, pizza: pizza } end words = text.split(/\s+/) return { error: usage_message } if words.size < 2 joined = words.join(" ").downcase MULTI_WORD_PIZZAS.sort_by { |term| -term.length }.each do |term| next unless joined.end_with?(term) next if joined == term term_words = term.split.size return { town: words[0...-term_words].join(" "), pizza: words.last(term_words).join(" ") } end { town: words[0...-1].join(" "), pizza: words.last } end
Source
# File plugins/pizza.rb, line 243 def self.parse_pizza_details(text, pizza_name:) text = text.to_s.strip return nil if text.empty? || text == "No results found" price = text[/\$[\d,]+(?:\.\d{1,2})?(?:\s*-\s*\$[\d,]+(?:\.\d{1,2})?)?/] duration_match = text.match(/(\d+\s*-\s*\d+|\d+)\s*(?:min(?:ute)?s?)/i) duration = duration_match ? duration_match[0].strip : nil toppings = extract_toppings(text) return nil unless price || duration || toppings { name: pizza_name, price: price || "unknown", duration: duration || "unknown", toppings: toppings || "unknown" } end
Source
# File plugins/pizza.rb, line 104 def self.pizza_query(place, town, pizza) "#{place} #{town} #{pizza} pizza price menu toppings delivery" end
Source
# File plugins/pizza.rb, line 170 def self.place_candidate(title, href, town:) title = title.to_s.strip href = href.to_s return nil if title.empty? name = if href.include?("facebook.com") extract_name_from_facebook_title(title) else clean_place_title(title) end return nil if name.nil? || name.empty? return nil unless valid_place_name?(name) return nil unless name.match?(/pizza|pizzeria/i) score = 0 score += 100 if href.include?("facebook.com") score += 80 if href.include?("google.com/maps") score += 60 if name.match?(/pizza|pizzeria/i) score += 30 if town.to_s.downcase.split.any? { |word| name.downcase.include?(word) } score += 20 if href.match?(/pizza|pizzeria/i) { name: name, score: score } end
Source
# File plugins/pizza.rb, line 95 def self.place_search_queries(town) [ viral_place_query(town), "#{town} pizza facebook", "pizzeria #{town} queensland", "pizza restaurant #{town} queensland" ] end
Source
# File plugins/pizza.rb, line 262 def self.source_tag(source) case source when :local then " (local pick)" when :social then " (social pick)" else "" end end
Source
# File plugins/pizza.rb, line 53 def self.usage_message 'Usage: !pizza <town> <pizza> — quote multi-word towns, e.g. !pizza "elgin vale" meat lovers' end
Source
# File plugins/pizza.rb, line 108 def self.valid_place_name?(name) text = name.to_s.strip return false if text.empty? || text.length > 80 return false if text == "No results found" return false if text.match?(REJECT_PLACE_PATTERN) return false if text.match?(/#\w+|\d+\s*views|watch the latest videos/i) return false if text.match?(/\A[a-z]+\s+pizza\z/i) true end
Public Instance Methods
Source
# File plugins/pizza.rb, line 310 def execute(m, args_text) parsed = self.class.parse_args(args_text) if parsed[:error] m.reply(parsed[:error]) return end safe_reply(m, lookup(parsed[:town], parsed[:pizza])) end
Source
# File plugins/pizza.rb, line 305 def fetch_html(query) url = "#{Duck::DDG_HTML_URL}?q=#{CGI.escape(query)}" URI.parse(url).open("User-Agent" => "Mozilla/5.0").read end
Source
# File plugins/pizza.rb, line 301 def lookup(town, pizza, fetch: method(:fetch_html)) self.class.lookup(town, pizza, fetch: fetch) end