class Recipe
Constants
- ADD_DRAFT_KEY
- AGENT_MODEL
- ARCHIVE_EXTENSIONS
- FLOOD_SAFE
- IMPORT_COOLDOWN_SEC
- IMPORT_STATE_KEY
- INVALID_VERBS
- MAX_FILE_BYTES
- MAX_LIST
- PLUGIN_NAME
- RATE_LIMIT_KEY
- RECIPES_KEY
- SHOW_BODY_MAX_BYTES
- STYLE_TITLE
- TEXT_EXTENSIONS
Public Class Methods
Source
# File plugins/recipe.rb, line 679 def self.actor_owns_recipe?(actor_nick, recipe) Normalize.user_key(actor_nick) == Normalize.user_key(recipe["nick"]) end
Source
# File plugins/recipe.rb, line 274 def self.add_recipe(network:, channel:, nick:, title:, ingredients:, instructions:, source:, tags: [], store: RabbotDb, received_at: nil) recipe = { "id" => generate_id, "nick" => Normalize.nick(nick), "title" => title.to_s.strip, "ingredients" => Array(ingredients).map(&:to_s).map(&:strip).reject(&:empty?), "instructions" => instructions.to_s.strip, "tags" => Array(tags).map(&:to_s).map(&:strip).reject(&:empty?), "source" => source.to_s.strip, "added_at" => received_at || store.iso_time } recipes = load_recipes(network: network, channel: channel, store: store) recipes << recipe save_recipes(network: network, channel: channel, recipes: recipes, store: store) recipe end
Source
# File plugins/recipe.rb, line 792 def self.add_recipe_from_last_history(network:, channel:, actor_nick:, store: RabbotDb, received_at: nil) plan = RecipeHistory.plan_add_from_last_history(network: network, channel: channel, store: store) return plan[:error] if plan[:error] recipe = add_recipe( network: network, channel: channel, nick: actor_nick, title: plan[:title], ingredients: parse_ingredients(plan[:ingredients]), instructions: plan[:instructions], source: RecipeHistory.source_label(plan[:source_nick]), store: store, received_at: received_at || store.iso_time ) format_show_report(recipe) end
Source
# File plugins/recipe.rb, line 669 def self.admin_allowed?(network:, channel:, nick:, is_op:, store: RabbotDb) store.user_allowed_at_level?( network: network, channel: channel, nick: nick, is_op: is_op, minimum: RabbotDb::LEVEL_ADMIN ) end
Source
# File plugins/recipe.rb, line 117 def self.archive_filename?(filename) name = filename.to_s.downcase ARCHIVE_EXTENSIONS.any? { |ext| name.end_with?(ext) } end
Source
# File plugins/recipe.rb, line 144 def self.archive_type(path) name = path.to_s.downcase return :zip if name.end_with?(".zip") return :tar if name.end_with?(".tar.gz", ".tgz", ".tar") nil end
Source
# File plugins/recipe.rb, line 207 def self.build_extract_prompt(filename:, content:) AiGuard.guard_prompt( <<~TEXT.strip Extract cookbook recipes from the following file. Reply with JSON only in this shape: {"recipes":[{"title":"...","ingredients":["..."],"instructions":"...","tags":["..."]}]} Use an empty recipes array if none are found. Do not include markdown fences. File: #{filename} --- #{content.to_s.strip} TEXT ) end
Source
# File plugins/recipe.rb, line 383 def self.clear_add_draft(network:, channel:, nick:, store: RabbotDb) drafts = Hash(store.get_plugin_json( plugin: PLUGIN_NAME, key: ADD_DRAFT_KEY, network: network, channel: channel, default: {} )) drafts.delete(Normalize.user_key(nick)) store.set_plugin_json( plugin: PLUGIN_NAME, key: ADD_DRAFT_KEY, network: network, channel: channel, value: drafts ) end
Source
# File plugins/recipe.rb, line 346 def self.clear_import_state(network:, channel:, dcc_id:, store: RabbotDb) save_import_state(network: network, channel: channel, dcc_id: dcc_id, state: nil, store: store) end
Source
# File plugins/recipe.rb, line 444 def self.complete_add_recipe(network:, channel:, nick:, instructions:, store: RabbotDb, received_at: nil) draft = load_add_draft(network: network, channel: channel, nick: nick, store: store) return "No recipe in progress — use !recipe add <title> first." unless draft return "No ingredients yet — use !recipe ingreds <list> first." if draft["ingredients"].to_s.strip.empty? recipe = add_recipe( network: network, channel: channel, nick: nick, title: draft["title"], ingredients: parse_ingredients(draft["ingredients"]), instructions: instructions, source: "manual", store: store, received_at: received_at || store.iso_time ) clear_add_draft(network: network, channel: channel, nick: nick, store: store) "Saved #{recipe['id']} — #{recipe['title']}." end
Source
# File plugins/recipe.rb, line 297 def self.delete_recipe(network:, channel:, id:, store: RabbotDb) recipes = load_recipes(network: network, channel: channel, store: store) recipe = recipes.find { |item| item["id"].to_s.downcase == id.to_s.strip.downcase } return "Unknown recipe id #{id}." unless recipe recipes.reject! { |item| item["id"] == recipe["id"] } save_recipes(network: network, channel: channel, recipes: recipes, store: store) "Deleted #{recipe['id']} (#{recipe['title']})." end
Source
# File plugins/recipe.rb, line 242 def self.extract_recipes_from_file(content, filename:, runner: AiAgent.method(:default_runner), model: AGENT_MODEL) prompt = build_extract_prompt(filename: filename, content: content) result = AiAgent.run_agent(prompt, model: model, runner: runner) return [] unless result.success? parse_agent_recipes(result.text) end
Source
# File plugins/recipe.rb, line 291 def self.find_recipe(network:, channel:, id:, store: RabbotDb) key = id.to_s.strip.downcase load_recipes(network: network, channel: channel, store: store) .find { |recipe| recipe["id"].to_s.downcase == key } end
Source
# File plugins/recipe.rb, line 772 def self.format_add_ingreds_report(title:, ingredients:) lines = [ IrcFormat.report_header(tag: "RECIPE", title: "new recipe (2/3)", style: STYLE_TITLE), "#{title} — ingredients saved", ingredients.to_s, IrcFormat.report_footer("send !recipe method <steps>") ] lines.join("\n") end
Source
# File plugins/recipe.rb, line 753 def self.format_add_start_report(title:, ingredients:) lines = [ IrcFormat.report_header(tag: "RECIPE", title: "new recipe (1/2)", style: STYLE_TITLE), "#{title} — ingredients saved", ingredients.to_s, IrcFormat.report_footer("send !recipe method <steps>") ] lines.join("\n") end
Source
# File plugins/recipe.rb, line 763 def self.format_add_title_report(title:) lines = [ IrcFormat.report_header(tag: "RECIPE", title: "new recipe (1/3)", style: STYLE_TITLE), "#{title} — title saved", IrcFormat.report_footer("send !recipe ingreds <list>") ] lines.join("\n") end
Source
# File plugins/recipe.rb, line 782 def self.format_import_report(dcc_entry:, result:) lines = [ IrcFormat.report_header(tag: "RECIPE", title: "archive import", style: STYLE_TITLE), "#{dcc_entry['filename']} — #{dcc_entry['id']}", result[:message].to_s, IrcFormat.report_footer("#{result[:remaining]} remaining") ] lines.join("\n") end
Source
# File plugins/recipe.rb, line 716 def self.format_ingredient_lines(ingredients, max_bytes: SHOW_BODY_MAX_BYTES) lines = wrap_items(ingredients, max_bytes: max_bytes, separator: ", ") lines.empty? ? ["No ingredients listed."] : lines end
Source
# File plugins/recipe.rb, line 721 def self.format_instruction_lines(instructions, max_bytes: SHOW_BODY_MAX_BYTES) lines = wrap_text(instructions, max_bytes: max_bytes) lines.empty? ? ["No instructions listed."] : lines end
Source
# File plugins/recipe.rb, line 726 def self.format_list_report(recipes:, title: "cookbook") lines = [IrcFormat.report_header(tag: "RECIPE", title: title, style: STYLE_TITLE)] if recipes.empty? lines << "No recipes saved yet." else recipes.last(MAX_LIST).each { |recipe| lines << format_recipe_line(recipe) } end lines << IrcFormat.report_footer("#{recipes.length} items") lines.join("\n") end
Source
# File plugins/recipe.rb, line 683 def self.format_recipe_line(recipe) tags = Array(recipe["tags"]).empty? ? "" : " · #{Array(recipe['tags']).join(', ')}" "#{recipe['id']} — #{recipe['title']} (#{recipe['nick']})#{tags}" end
Source
# File plugins/recipe.rb, line 748 def self.format_search_report(query:, recipes:) title = query.to_s.strip.empty? ? "recent recipes" : "search — #{query}" format_list_report(recipes: recipes, title: title) end
Source
# File plugins/recipe.rb, line 737 def self.format_show_report(recipe, max_bytes: SHOW_BODY_MAX_BYTES) lines = [ IrcFormat.report_header(tag: "RECIPE", title: recipe["title"], style: STYLE_TITLE), "#{recipe['id']} — #{recipe['nick']} — #{recipe['source']}" ] lines.concat(format_ingredient_lines(recipe["ingredients"], max_bytes: max_bytes)) lines.concat(format_instruction_lines(recipe["instructions"], max_bytes: max_bytes)) lines << IrcFormat.report_footer("recipe") lines.join("\n") end
Source
# File plugins/recipe.rb, line 270 def self.generate_id SecureRandom.hex(4) end
Source
# File plugins/recipe.rb, line 496 def self.import_cooldown_message(cooldown_sec: IMPORT_COOLDOWN_SEC) "Slow down — wait #{cooldown_sec}s before importing another archive file." end
Source
# File plugins/recipe.rb, line 506 def self.import_from_archive(network:, channel:, actor_nick:, dcc_entry:, path:, store: RabbotDb, runner: AiAgent.method(:default_runner), model: AGENT_MODEL, now: Time.now, cooldown_sec: IMPORT_COOLDOWN_SEC) unless archive_filename?(dcc_entry["filename"]) return { processed: 0, imported: 0, remaining: 0, rate_limited: false, message: "That DCC file is not a supported archive (zip/tar.gz)." } end unless File.file?(path) return { processed: 0, imported: 0, remaining: 0, rate_limited: false, message: "Archive file is no longer available." } end if import_rate_limited?(network: network, channel: channel, nick: actor_nick, store: store, now: now, cooldown_sec: cooldown_sec) return { processed: 0, imported: 0, remaining: pending_members(path: path, dcc_id: dcc_entry["id"], network: network, channel: channel, store: store).length, rate_limited: true, message: import_cooldown_message(cooldown_sec: cooldown_sec) } end remaining = pending_members(path: path, dcc_id: dcc_entry["id"], network: network, channel: channel, store: store) if remaining.empty? clear_import_state(network: network, channel: channel, dcc_id: dcc_entry["id"], store: store) return { processed: 0, imported: 0, remaining: 0, rate_limited: false, message: "No text recipe files found in that archive." } end member = remaining.first content = read_archive_member(path, member) unless content && !content.strip.empty? remaining = remaining[1..] || [] save_import_state( network: network, channel: channel, dcc_id: dcc_entry["id"], state: { "remaining" => remaining, "source" => dcc_entry["filename"] }, store: store ) return { processed: 1, imported: 0, remaining: remaining.length, rate_limited: false, message: "Skipped empty file #{member}." } end extracted = extract_recipes_from_file(content, filename: member, runner: runner, model: model) imported = extracted.map do |item| add_recipe( network: network, channel: channel, nick: actor_nick, title: item["title"], ingredients: item["ingredients"], instructions: item["instructions"], tags: item["tags"], source: "#{dcc_entry['filename']}:#{member}", store: store, received_at: store.iso_time(now) ) end remaining = remaining[1..] || [] if remaining.empty? clear_import_state(network: network, channel: channel, dcc_id: dcc_entry["id"], store: store) else save_import_state( network: network, channel: channel, dcc_id: dcc_entry["id"], state: { "remaining" => remaining, "source" => dcc_entry["filename"] }, store: store ) end record_import_request(network: network, channel: channel, nick: actor_nick, store: store, now: now) titles = imported.map { |recipe| recipe["title"] } summary = if titles.empty? "No recipes found in #{member}." else "Imported #{titles.length} from #{member}: #{titles.join(', ')}." end summary += " #{remaining.length} file(s) left — run !recipe import #{dcc_entry['id']} again." unless remaining.empty? { processed: 1, imported: imported.length, remaining: remaining.length, rate_limited: false, message: summary } end
Source
# File plugins/recipe.rb, line 464 def self.import_rate_limited?(network:, channel:, nick:, store: RabbotDb, now: Time.now, cooldown_sec: IMPORT_COOLDOWN_SEC) limits = Hash(store.get_plugin_json( plugin: PLUGIN_NAME, key: RATE_LIMIT_KEY, network: network, channel: channel, default: {} )) last_at = store.parse_time(limits[Normalize.user_key(nick)]) return false unless last_at (now - last_at) < cooldown_sec end
Source
# File plugins/recipe.rb, line 322 def self.import_state_key(dcc_id) "#{IMPORT_STATE_KEY}:#{dcc_id}" end
Source
# File plugins/recipe.rb, line 133 def self.list_archive_entries(path) case archive_type(path) when :zip list_zip_entries(path) when :tar list_tar_entries(path) else [] end end
Source
# File plugins/recipe.rb, line 160 def self.list_tar_entries(path) entries = [] open_tar_stream(path) do |tar| tar.each do |entry| next unless entry.file? entries << { name: entry.full_name.to_s, size: entry.header.size.to_i } end end entries rescue StandardError [] end
Source
# File plugins/recipe.rb, line 152 def self.list_zip_entries(path) RecipeArchiveZip.each_entry(path).map do |name, body, _crc| { name: name, size: body.bytesize } end rescue StandardError [] end
Source
# File plugins/recipe.rb, line 350 def self.load_add_draft(network:, channel:, nick:, store: RabbotDb) drafts = Hash(store.get_plugin_json( plugin: PLUGIN_NAME, key: ADD_DRAFT_KEY, network: network, channel: channel, default: {} )) draft = drafts[Normalize.user_key(nick)] draft.is_a?(Hash) && !draft["title"].to_s.strip.empty? ? draft : nil end
Source
# File plugins/recipe.rb, line 326 def self.load_import_state(network:, channel:, dcc_id:, store: RabbotDb) store.get_plugin_json( plugin: PLUGIN_NAME, key: import_state_key(dcc_id), network: network, channel: channel, default: nil ) end
Source
# File plugins/recipe.rb, line 250 def self.load_recipes(network:, channel:, store: RabbotDb) Array(store.get_plugin_json( plugin: PLUGIN_NAME, key: RECIPES_KEY, network: network, channel: channel, default: [] )) end
Source
# File plugins/recipe.rb, line 810 def self.manage_command(network:, channel:, args:, actor_nick:, is_op:, storage_root: Dcc::STORAGE_ROOT, store: RabbotDb, runner: AiAgent.method(:default_runner), now: Time.now) parsed = parse_command(args) return parsed[:error] if parsed[:error] case parsed[:action] when :list recipes = load_recipes(network: network, channel: channel, store: store) format_list_report(recipes: recipes) when :search recipes = search_recipes(network: network, channel: channel, query: parsed[:query], store: store) format_search_report(query: parsed[:query], recipes: recipes) when :show recipe = find_recipe(network: network, channel: channel, id: parsed[:id], store: store) return "Unknown recipe id #{parsed[:id]}." unless recipe format_show_report(recipe) when :add recipe = add_recipe( network: network, channel: channel, nick: actor_nick, title: parsed[:title], ingredients: parse_ingredients(parsed[:ingredients]), instructions: parsed[:instructions], source: "manual", store: store, received_at: store.iso_time(now) ) "Saved #{recipe['id']} — #{recipe['title']}." when :addlast add_recipe_from_last_history( network: network, channel: channel, actor_nick: actor_nick, store: store, received_at: store.iso_time(now) ) when :add_start start_add_recipe( network: network, channel: channel, nick: actor_nick, title: parsed[:title], ingredients: parsed[:ingredients], store: store ) when :add_title start_add_recipe_title( network: network, channel: channel, nick: actor_nick, title: parsed[:title], store: store ) when :add_ingreds set_add_recipe_ingredients( network: network, channel: channel, nick: actor_nick, ingredients: parsed[:ingredients], store: store ) when :add_method complete_add_recipe( network: network, channel: channel, nick: actor_nick, instructions: parsed[:instructions], store: store, received_at: store.iso_time(now) ) when :del recipe = find_recipe(network: network, channel: channel, id: parsed[:id], store: store) return "Unknown recipe id #{parsed[:id]}." unless recipe unless admin_allowed?(network: network, channel: channel, nick: actor_nick, is_op: is_op, store: store) || actor_owns_recipe?(actor_nick, recipe) return "You cannot delete that recipe." end delete_recipe(network: network, channel: channel, id: parsed[:id], store: store) when :import entry = Dcc.find_file(network: network, id: parsed[:dcc_id], store: store) return "Unknown DCC file id #{parsed[:dcc_id]}." unless entry unless admin_allowed?(network: network, channel: channel, nick: actor_nick, is_op: is_op, store: store) || actor_owns_recipe?(actor_nick, { "nick" => entry["nick"] }) return "You cannot import from that DCC file." end path = Dcc.absolute_path(entry, storage_root: storage_root) result = import_from_archive( network: network, channel: channel, actor_nick: actor_nick, dcc_entry: entry, path: path, store: store, runner: runner, now: now ) format_import_report(dcc_entry: entry, result: result) end end
Source
# File plugins/recipe.rb, line 174 def self.open_tar_stream(path) if path.to_s.downcase.end_with?(".tar") File.open(path, "rb") { |io| yield Gem::Package::TarReader.new(io) } else Zlib::GzipReader.open(path) { |gz| yield Gem::Package::TarReader.new(gz) } end end
Source
# File plugins/recipe.rb, line 221 def self.parse_agent_recipes(text) payload = JSON.parse(text.to_s) recipes = payload["recipes"] || payload recipes = [recipes] if recipes.is_a?(Hash) Array(recipes).filter_map do |item| next unless item.is_a?(Hash) title = item["title"].to_s.strip next if title.empty? { "title" => title, "ingredients" => Array(item["ingredients"]).map(&:to_s).map(&:strip).reject(&:empty?), "instructions" => item["instructions"].to_s.strip, "tags" => Array(item["tags"]).map(&:to_s).map(&:strip).reject(&:empty?) } end rescue JSON::ParserError [] end
Source
# File plugins/recipe.rb, line 602 def self.parse_command(text) stripped = text.to_s.strip return { action: :list } if stripped.empty? return { action: :list } if stripped.casecmp?("list") if (verb = stripped.split(/\s+/, 2).first) && INVALID_VERBS.include?(verb.downcase) return { error: usage_message } end if (match = stripped.match(/\Asearch\s+(.+)\z/i)) return { action: :search, query: match[1].strip } end if (match = stripped.match(/\Ashow\s+(\S+)\z/i)) return { action: :show, id: match[1] } end if (match = stripped.match(/\Adel(?:ete)?\s+(\S+)\z/i)) return { action: :del, id: match[1] } end return { action: :addlast } if stripped.casecmp?("addlast") if (match = stripped.match(/\Aadd\s+method\s+(.+)\z/i)) instructions = match[1].strip return { error: usage_message } if instructions.empty? return { action: :add_method, instructions: instructions } end if (match = stripped.match(/\Amethod\s+(.+)\z/i)) instructions = match[1].strip return { error: usage_message } if instructions.empty? return { action: :add_method, instructions: instructions } end if (match = stripped.match(/\Aingreds\s+(.+)\z/i)) ingredients = match[1].strip return { error: usage_message } if ingredients.empty? return { action: :add_ingreds, ingredients: ingredients } end if (match = stripped.match(/\Aadd\s+(.+)\z/i)) parts = match[1].split("|", 3).map(&:strip) if parts.length == 3 && parts.all? { |part| !part.empty? } return { action: :add, title: parts[0], ingredients: parts[1], instructions: parts[2] } end if parts.length == 2 && parts.all? { |part| !part.empty? } return { action: :add_start, title: parts[0], ingredients: parts[1] } end if parts.length == 1 && !parts[0].empty? return { action: :add_title, title: parts[0] } end return { error: usage_message } end if (match = stripped.match(/\Aimport\s+(\S+)\z/i)) return { action: :import, dcc_id: match[1] } end return { error: usage_message } if stripped.match?(/\A(?:show|del|add|import|method|ingreds)\z/i) { action: :search, query: stripped } end
Source
# File plugins/recipe.rb, line 401 def self.parse_ingredients(text) text.to_s.split(/,|\band\b/i).map(&:strip).reject(&:empty?) end
Source
# File plugins/recipe.rb, line 500 def self.pending_members(path:, dcc_id:, network:, channel:, store: RabbotDb) state = load_import_state(network: network, channel: channel, dcc_id: dcc_id, store: store) members = state && state["remaining"].is_a?(Array) ? state["remaining"] : text_members(path) members end
Source
# File plugins/recipe.rb, line 182 def self.read_archive_member(path, member_name) case archive_type(path) when :zip RecipeArchiveZip.read_member(path, member_name) when :tar body = nil open_tar_stream(path) do |tar| tar.each do |entry| next unless entry.file? && entry.full_name.to_s == member_name data = entry.read body = data.bytesize <= MAX_FILE_BYTES ? data : data.byteslice(0, MAX_FILE_BYTES) break end end body end rescue StandardError nil end
Source
# File plugins/recipe.rb, line 478 def self.record_import_request(network:, channel:, nick:, store: RabbotDb, now: Time.now) limits = Hash(store.get_plugin_json( plugin: PLUGIN_NAME, key: RATE_LIMIT_KEY, network: network, channel: channel, default: {} )) limits[Normalize.user_key(nick)] = store.iso_time(now) store.set_plugin_json( plugin: PLUGIN_NAME, key: RATE_LIMIT_KEY, network: network, channel: channel, value: limits ) end
Source
# File plugins/recipe.rb, line 362 def self.save_add_draft(network:, channel:, nick:, title:, ingredients:, store: RabbotDb) drafts = Hash(store.get_plugin_json( plugin: PLUGIN_NAME, key: ADD_DRAFT_KEY, network: network, channel: channel, default: {} )) drafts[Normalize.user_key(nick)] = { "title" => title.to_s.strip, "ingredients" => ingredients.to_s.strip } store.set_plugin_json( plugin: PLUGIN_NAME, key: ADD_DRAFT_KEY, network: network, channel: channel, value: drafts ) end
Source
# File plugins/recipe.rb, line 336 def self.save_import_state(network:, channel:, dcc_id:, state:, store: RabbotDb) store.set_plugin_json( plugin: PLUGIN_NAME, key: import_state_key(dcc_id), network: network, channel: channel, value: state ) end
Source
# File plugins/recipe.rb, line 260 def self.save_recipes(network:, channel:, recipes:, store: RabbotDb) store.set_plugin_json( plugin: PLUGIN_NAME, key: RECIPES_KEY, network: network, channel: channel, value: recipes ) end
Source
# File plugins/recipe.rb, line 307 def self.search_recipes(network:, channel:, query:, store: RabbotDb) needle = query.to_s.strip.downcase return load_recipes(network: network, channel: channel, store: store).last(MAX_LIST) if needle.empty? load_recipes(network: network, channel: channel, store: store).select do |recipe| haystack = [ recipe["title"], recipe["instructions"], Array(recipe["ingredients"]).join(" "), Array(recipe["tags"]).join(" ") ].join(" ").downcase haystack.include?(needle) end end
Source
# File plugins/recipe.rb, line 429 def self.set_add_recipe_ingredients(network:, channel:, nick:, ingredients:, store: RabbotDb) draft = load_add_draft(network: network, channel: channel, nick: nick, store: store) return "No recipe in progress — use !recipe add <title> first." unless draft save_add_draft( network: network, channel: channel, nick: nick, title: draft["title"], ingredients: ingredients, store: store ) format_add_ingreds_report(title: draft["title"], ingredients: ingredients) end
Source
# File plugins/recipe.rb, line 405 def self.start_add_recipe(network:, channel:, nick:, title:, ingredients:, store: RabbotDb) save_add_draft( network: network, channel: channel, nick: nick, title: title, ingredients: ingredients, store: store ) format_add_start_report(title: title, ingredients: ingredients) end
Source
# File plugins/recipe.rb, line 417 def self.start_add_recipe_title(network:, channel:, nick:, title:, store: RabbotDb) save_add_draft( network: network, channel: channel, nick: nick, title: title, ingredients: "", store: store ) format_add_title_report(title: title) end
Source
# File plugins/recipe.rb, line 122 def self.text_member?(name) base = File.basename(name.to_s) return false if base.start_with?(".") return false if base.include?("__MACOSX") ext = File.extname(base).downcase return true if TEXT_EXTENSIONS.include?(ext) ext.empty? && base.match?(/recipe|ingredient|cook/i) end
Source
# File plugins/recipe.rb, line 203 def self.text_members(path) list_archive_entries(path).map { |entry| entry[:name] }.select { |name| text_member?(name) } end
Source
# File plugins/recipe.rb, line 113 def self.usage_message "Usage: !recipe [list|search <q>|<q>|show <id>|add [<title>|ingredients|steps]|addlast|ingreds <list>|method <steps>|del <id>|import <dcc_id>]" end
Source
# File plugins/recipe.rb, line 692 def self.wrap_items(items, max_bytes: SHOW_BODY_MAX_BYTES, separator: ", ") list = Array(items).map(&:to_s).map(&:strip).reject(&:empty?) return [] if list.empty? lines = [] current = +"" list.each do |item| candidate = current.empty? ? item : "#{current}#{separator}#{item}" if candidate.bytesize > max_bytes && !current.empty? lines << current current = item.dup elsif candidate.bytesize > max_bytes lines.concat(wrap_text(item, max_bytes: max_bytes)) current = +"" else current = candidate end end lines << current unless current.empty? lines end
Source
# File plugins/recipe.rb, line 688 def self.wrap_text(text, max_bytes: SHOW_BODY_MAX_BYTES) FLOOD_SAFE.chunk_text(text.to_s.strip, max_bytes: max_bytes).reject(&:empty?) end
Public Instance Methods
Source
# File plugins/recipe.rb, line 914 def execute(m, args = nil) unless m.channel m.reply "Use !recipe in a channel" return end parsed = self.class.parse_command(args) if parsed[:action] == :import return themed_flood_safe_reply(m, AiAccess.denial_message) unless AiAccess.user_allowed?( channel: m.channel, user: m.user, network: bot.config.server, nick: m.user.nick ) end reply = self.class.manage_command( network: bot.config.server, channel: m.channel.name, args: args, actor_nick: m.user.nick, is_op: m.channel.opped?(m.user) ) themed_flood_safe_reply(m, reply) end