From 9005c0fba5f222069edeeb4b6d247662065d4e48 Mon Sep 17 00:00:00 2001 From: mio Date: Mon, 14 Mar 2022 06:57:49 +0000 Subject: [PATCH] Rewrite module in Lua --- .gitignore | 11 +- README.md | 38 ++- examples/ramenkan.config.lua | 500 +++++++++++++++++++++++++++ examples/ramenkan.lua | 26 ++ examples/sample.servers.lua | 24 ++ itte.lua | 638 +++++++++++++++++++++++++++++++++++ itte.py | 185 ---------- itteutil.lua | 252 ++++++++++++++ ramenkan.py | 191 ----------- ramenkan/config.sample.yml | 27 -- ramenkan/dishes.yml | 143 -------- ramenkan/links.yml | 70 ---- ramenkan/misc.yml | 28 -- ramenkan/photos.yml | 44 --- 14 files changed, 1470 insertions(+), 707 deletions(-) create mode 100644 examples/ramenkan.config.lua create mode 100644 examples/ramenkan.lua create mode 100644 examples/sample.servers.lua create mode 100644 itte.lua delete mode 100644 itte.py create mode 100644 itteutil.lua delete mode 100755 ramenkan.py delete mode 100644 ramenkan/config.sample.yml delete mode 100644 ramenkan/dishes.yml delete mode 100644 ramenkan/links.yml delete mode 100644 ramenkan/misc.yml delete mode 100644 ramenkan/photos.yml diff --git a/.gitignore b/.gitignore index ae67476..fc161b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ -__pycache__/ -*.py[cod] -*$py.class -*.swp - -nohup.out -*.config.yml -*.log +*.servers.lua +examples/itte*.lua +!sample.servers.lua diff --git a/README.md b/README.md index 3d8c380..fb1540e 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,39 @@ # itte -A very basic Python IRC bot script. +Mini IRC bot module in Lua. + + +Currently supported: + +- Authentication via SASL (plain) or Nickserv +- Joining multiple servers +- Config reload + + +## Requirements + +- [Lua 5.x](https://www.lua.org/) +- [luasocket](https://w3.impa.br/~diego/software/luasocket/) +- [luasec](https://github.com/brunoos/luasec) + + +## Usage + +See the bot files in the `examples/` directory for usage notes. ## Example: ramenkan -- Install dependencies: `pip install Mastodon.py pyyaml` +- Install Lua and other dependencies, e.g. for Alpine: + `apk add lua-socket lua-sec` -- Copy the `ramenkan/config.sample.yml` as `ramenkan/default.config.yml` and - change the settings as applicable. +- Copy the `itte*.lua` files to the `examples/` directory. Copy the + `examples/sample.servers.lua` as `ramenkan.servers.lua` and change the server + settings as applicable. -- Run: - - ``` - chmod +x ramenkan.py - nohup python3 ramenkan.py >/dev/null 2>&1 & - ``` +- Run: `nohup lua /path/to/examples/ramenkan.lua >/dev/null 2>&1 &` ## License -BSD +BSD-3.0 diff --git a/examples/ramenkan.config.lua b/examples/ramenkan.config.lua new file mode 100644 index 0000000..8764cce --- /dev/null +++ b/examples/ramenkan.config.lua @@ -0,0 +1,500 @@ +-- --------------------------------------------------------------------------- +-- Ramen data +-- --------------------------------------------------------------------------- + +-- [[ +-- This is sample data and could be split into another file. +-- ]] + +local ramen = {} + +ramen.noodle_broth_types = { + "thick noodles in rich", + "medium-thick noodles in mild", + "thin noodles in light", +} + +ramen.noodle_shapes = { + "curly", "flat", "straight", +} + +ramen.broths = { + "miso", "spicy miso", "soy sauce", "grilled soy sauce and Rishiri kelp", + "salt", "salt and soy sauce", +} + +-- niboshi: dried little sardines +-- katsuoboshi, konbu: skipjack tuna, kelp +-- gyokai: anchovies/shrimp/squid/seaweed +-- miso and truffle: ramen by chef Ryu Takahashi +ramen.broths_meat = { + "Kobe beef bone", "tonkotsu", "pork bone and chicken", + "pork bone and niboshi", "pork bone, chicken and seafood", + "pork bone, chicken, tuna and kelp", "pork bone and soy sauce", "chicken", + "chicken, katsuoboshi and konbu", "fish", "gyokai", "gyokai tonkotsu", + "roasted tuna", "miso and truffle", "milk", "curry and milk", +} + +-- wagyu: Japanese beef known for its higher fat percentage +-- kamaboko/surimi: fish paste +-- narutomaki: fish paste with a swirling pattern +-- kakuni: cubed braised pork +-- chashu: Japanese-style barbecued pork +ramen.meats = { + "Kobe beef fillet", "Kobe beef sirloin", "wagyu", "teppanyaki beef", + "beef shank", "chicken breast", "chicken meatballs", "teppanyaki chicken", + "crab", "crispy duck", "filet mignon", "kamaboko", "narutomaki", "kakuni", + "chashu", "pork confit", "pork cutlet", "pork belly", "pork shoulder", + "shrimp", "sea urchin", "scallops", "squid", +} + +-- beni shoga: pickled ginger +-- menma: fermented bamboo shoots +-- kikurage: a type of fungi, known in some cultures as "wood ear" +-- karashi takana: spicy pickled mustard greens +-- negi: Welsh onion +ramen.toppings = { + "bean sprouts", "chili flakes", "sweet corn", "grated garlic", + "roasted garlic chips", "beni shoga", "bamboo shoots", "menma", + "dried mushrooms", "shiitake mushrooms", "kikurage", "karashi takana", + "negi", "white onions", "spring onions", "sichuan peppers", "nori", "wakame", + "sauerkraut", "sesame seeds", "scallions", "boiled spinach", +} + +-- ajitama: egg marinated in soy sauce +ramen.toppings_egg = { + "egg yolk", "seasoned boiled egg", "ajitama", +} + + +ramen.sauce_verbs = { + "garnished", "sprinkled", "sprinkled lightly", "sprinkled liberally", + "drizzled", "drizzled lightly", "drizzled artfully", "brimming", +} + +ramen.spice_verbs = { + "seasoned", "sprinkled", "sprinkled lightly", "sprinkled liberally", +} + +-- rayu: chili oil +-- mayu: garlic oil +-- dashi: fish/seaweed stock +-- ponzu: citrus sauce +-- shichimi: spice blend +ramen.sauces = { + "butter", "rayu", "chili sauce", "mayu", "spicy garlic and ginger oil", + "sesame oil", "dashi", "ponzu", +} + +ramen.spices = { + "black pepper", "white pepper", "chili peppers", "shichimi", +} + +ramen.tapas = { + "edamame tossed in sea salt", "tofu nuggets", "picked daikon", + "cucumber salad", "seaweed salad", +} + +ramen.tapas_meat = { + "gyoza", "karaage", "tako wasabi", "takoyaki", "corn and shrimp salad", + "crab salad", "beef enoki rolls", +} + +ramen.sets = { + "Gunma konnyaku ramen in soy sauce broth", + "miso ramen with vegetable broth, cabbage and bean sprouts", + "ramen with shiitake mushrooms, kombu and sesame oil", + "ramen with soybean pork slices, kikurage, bamboo shoots and scallions", + "barley miso ramen with roasted vegetables", +} + +ramen.sets_meat = { + "Korean kimchi ramyeon with tteok", + "Vancouver cold bonito beer broth ramen with egg whites", + "Hakata tonkotsu ramen with pickled mustard greens", + "Hakodate shio ramen with kelp, crab, shrimp, sea urchin and scallops", + "Hakodate shrimp ramen with a hard-boiled egg and white onions", + "Kagoshima tonkotsu ramen with chashu, dried sardines, kelp and scallions", + "Kitakata niboshi-tonkotsu ramen with chashu, naruto and spring onions", + "Kumamoto tonkotsu ramen with chashu, raw eggs, scallions and garlic chips", + "Muroran curry ramen with chashu, wakame and bean sprouts", + "Nagasaki champon with fried pork, scallions and seasonal greens", + "Osaka Kobe beef sirloin ramen with a soft-boiled egg and arugula", + "Sapporo miso ramen with scallops, sweet corn and bean sprouts", + "Tokyo ramen in chicken broth with pork, menma, a boiled egg and nori", + "Wakayama shoyu-tonkotsu ramen with pork slices, naruto and scallions", + "Yokohama ramen with chashu, spinach, a soft-boiled egg and negi", +} + + +-- Links to photos and resources +ramen.links = { + -- Photos of ramen vending machines + -- Ticket machines + "https://c2.staticflickr.com/6/5077/5902028877_7d8c65b23f_b.jpg", + "https://c1.staticflickr.com/3/2455/5777431350_36e147e719_z.jpg", + "https://i.pinimg.com/originals/72/08/1e/72081e68ff0989d54fd24a86fcac6f2b.jpg", + "https://i.pinimg.com/originals/9f/74/92/9f7492b7c456ac7ba350498a50c74b23.jpg", + "https://c1.staticflickr.com/9/8157/7105010129_cd4b0d7e27_c.jpg", + "https://c1.staticflickr.com/8/7600/27418644850_2c70342734_c.jpg", + "https://c3.staticflickr.com/3/2049/2229742287_8df3e823a9_z.jpg", + "https://c1.staticflickr.com/1/101/308523474_4295a27326_z.jpg", + "https://media-cdn.tripadvisor.com/media/photo-s/0b/b6/27/f6/ramen-shop-menu-banner.jpg", + "https://abroadabroad2011.files.wordpress.com/2013/03/sam_0040.jpg", + "https://c1.staticflickr.com/6/5753/22803130417_ba86ebb4fa_c.jpg", + "https://c1.staticflickr.com/6/5077/5902028877_7d8c65b23f_z.jpg", + "https://c1.staticflickr.com/8/7504/15210528573_2612010fc3_c.jpg", + "https://s3-media4.fl.yelpcdn.com/bphoto/iUHJczm_--xNqWEUzDS69Q/o.jpg", + "https://thewholeworldornothing.com/wp-content/uploads/2017/01/vending-machine-waiter.jpg", + "https://thebrunchingbooth.files.wordpress.com/2014/07/img_42681.jpg", + "https://c1.staticflickr.com/8/7426/16587852342_0cbfa4a63c_c.jpg", + "https://c1.staticflickr.com/9/8422/7535359482_b5537bbde0_c.jpg", + "https://c1.staticflickr.com/5/4037/4530351457_8666286b56_z.jpg", + "https://c1.staticflickr.com/1/97/244236130_0c22679f97_z.jpg", + "https://c1.staticflickr.com/4/3479/3465112347_1be2868ff0_z.jpg", + "https://1.bp.blogspot.com/_W-pG7tUmJk0/TDQ-J_vz9sI/AAAAAAAACac/xYjAvl8ZGOY/s1600/shinjuku_ramen_02.jpg", + "https://thefoodieflight.files.wordpress.com/2015/10/img_3749.jpg", + "https://static1.squarespace.com/static/57ad3b51c534a528e26a2e93/t/57f91e3be6f2e18cf1d11c82/1476472671797/", + "https://c1.staticflickr.com/1/166/417127513_e063ccf80a_z.jpg", + -- Instant cup noodles + "http://www.toxel.com/wp-content/uploads/2009/06/vendingmachine04.jpg", + "https://c1.staticflickr.com/3/2233/2347915323_9e9e2264a3_z.jpg", + "https://c1.staticflickr.com/4/3818/10186967173_237d38b86f_c.jpg", + "https://c1.staticflickr.com/8/7221/7175446233_076264a2b4_c.jpg", + "https://c7.alamy.com/comp/BCDJRG/a-cup-noodle-vending-machine-instant-ramen-museum-osaka-japan-1-december-BCDJRG.jpg", + -- Ramen can + "https://c1.staticflickr.com/4/3625/3374051074_641b371d23_z.jpg", + "https://c4.staticflickr.com/4/3214/2911821453_78c6178f54_z.jpg", + "https://smallbiztrends.com/wp-content/uploads/2015/06/P1020817-728x485.jpg", + "https://c1.staticflickr.com/9/8440/7745533776_7b9990be82_c.jpg", + "https://c1.staticflickr.com/3/2268/4507516049_e7aa354489_z.jpg", + -- Various ramen-related links + -- Places and shops + "Ramen, Soul Food Staple - " .. + "https://www3.nhk.or.jp/nhkworld/en/food/articles/51.html", + "Shin-Yokohama Ramen Museum, Yokohama - " .. + "http://www.raumen.co.jp/english/", + "Ramen Street, Tokyo - " .. + "https://en.wikipedia.org/wiki/Ramen_street", + "Sapporo Ramen Republic - " .. + "http://www.sapporo-esta.jp/ramen", + "Ramen DB, a directory of ramen shops in Japan - " .. + "https://ramendb.supleks.jp/", + "Ramen Adventures, a ramen shop review blog - " .. + "http://www.ramenadventures.com/", + -- Recipes + "15 Fantastic Ramen Recipes - " .. + "https://www3.nhk.or.jp/nhkworld/en/food/articles/131.html", + "Chef Rika's Miso Ramen - " .. + "https://www3.nhk.or.jp/nhkworld/en/ondemand/video/9999434/", + "Chef Rika's Shoyu Ramen - " .. + "https://www3.nhk.or.jp/nhkworld/en/ondemand/video/9999299/", + -- Instant noodles + "Momofuku Ando, inventor of instant ramen (1958) - " .. + "https://www.nissin.com/en_jp/about/founder/", + "Instant cup noodles (1971) - " .. + "https://en.wikipedia.org/wiki/Cup_Noodles", + "Instant noodles as emergency food in disaster relief - " .. + "https://instantnoodles.org/en/activities/support.html", + "Samyang, the first Korean instant ramen (1963) - " .. + "https://en.wikipedia.org/wiki/Samyang_ramen", + "The Ramen Rater, an instant ramen blog - " .. + "https://www.theramenrater.com/", + -- Ramen news + "Japan's leading ramen chain Ippudo goes vegan - " .. + "https://asia.nikkei.com/Business/Food-Beverage/Japan-s-leading-ramen-chain-Ippudo-goes-vegan", + -- Ramen in pop culture + "Ramen Manga, comic strips in Japanese about ramen - " .. + "https://www.ramenbank.com/manga/", + "Flower Boy Ramen Shop, romantic comedy drama (2011) - " .. + "https://en.wikipedia.org/wiki/Flower_Boy_Ramen_Shop", + "The Ramen Girl, romantic comedy film (2008) - " .. + "https://en.wikipedia.org/wiki/The_Ramen_Girl", + "Ms. Koizumi Loves Ramen Noodles, manga series (2013-) - " .. + "https://en.wikipedia.org/wiki/Ms._Koizumi_Loves_Ramen_Noodles", + "Muteki Kanban Musume, manga series (2002-2006) - " .. + "https://en.wikipedia.org/wiki/Muteki_Kanban_Musume", + "Detective Conan - Ramen So Good, It's to Die For, anime episode (2012) - " .. + "https://www.detectiveconanworld.com/wiki/Ramen_So_Good,_It%27s_to_Die_For", + "Ramen Teh, film (2018) - " .. + "https://en.wikipedia.org/wiki/Ramen_Teh", + "Ramen yori taisetsuna mono, documentary (2013) - " .. + "https://www.imdb.com/title/tt2945642/", + "Ramen Cake dessert looks just like the real thing, is probably just as bad for you - " .. + "https://soranews24.com/2013/10/24/ramen-cake-dessert-looks-just-like-the-real-thing-is-just-as-terrible-for-you/", + -- Audio + "Japan Eats! Episode 155 - Oisa Ramen (2019) - " .. + "https://heritageradionetwork.org/podcast/oisa-ramen-i-am-just-a-mom-who-cares-to-give-her-best", + -- Videos + "What Owning a Ramen Restaurant in Japan is Like - " .. + "https://www.youtube.com/watch?v=gmIwxqdwgrI", + "Begin Japanology - Ramen - " .. + "https://www.youtube.com/watch?v=RosUc9UVuos", + "Japanology Plus - Ramen - " .. + "https://www.youtube.com/watch?v=K2Jzt04QY7A", + "In Search of the Perfect Bowl of Ramen - " .. + "https://www.youtube.com/watch?v=bhiFtP9qVYc", + "Rahaku TV (The Shin-Yokohama Ramen Museum Youtube Channel) - " .. + "http://www.youtube.com/rahakutv", + "The History of Ramen Yamagoya - " .. + "https://www.youtube.com/watch?v=8NBwKbT5QSQ", + "Ramen Daruma - " .. + "https://www.youtube.com/watch?v=zkhDYTR789w", + "The God of Ramen, trailer (2013) - " .. + "https://www.youtube.com/watch?v=mtwunTGnnR4", + "Ramen Heads, trailer (2017) - " .. + "https://www.youtube.com/watch?v=u61b5R_S4TM", + "Ramenomania - Noodles, Asia's Second Obsession - " .. + "https://www.youtube.com/watch?v=tfW89yAoTNs", + "Noodle Me - " .. + "https://www.youtube.com/watch?v=r9jLJRZ6gnc", + "Visio Documentary - Fat Ramen - " .. + "https://www.youtube.com/watch?v=o78fy4rJFCw", + "Ramen Talk and Q+A With Chef Ryu Takahashi and Chef Matt Kimura - " .. + "https://www.youtube.com/watch?v=tMvArDklmGU", +} + + +-- --------------------------------------------------------------------------- +-- Ramen functions +-- --------------------------------------------------------------------------- + +-- Load the util module early to use some helper functions. +local util = require("itteutil") + + +function ramen.prep_noodles(mode) + local noodles = "ramen in a" + -- Dish type, noodle type linked to broth density + local d_roll = math.random(1, 10) + -- 20% chance of tsukemen, 40% chance of noodle thickness description + if d_roll <= 2 then + noodles = "tsukemen from a" + elseif (d_roll >= 3) and (d_roll <= 6) then + noodles = util.pick(itte_config.ramen.noodle_broth_types)[1] + end + -- Broth type + local broths = itte_config.ramen.broths + if mode == "meat" then + broths = itte_config.ramen.broths_meat + end + return noodles .. " " .. util.pick(broths)[1] .. " broth" +end + + +function ramen.prep_toppings(mode) + -- 2-5 toppings + local t_roll = math.random(2, 5) + local toppings = "" + + -- Meat: reduce other toppings to max 3 ingredients + -- 25% chance of egg included + if mode == "meat" then + t_roll = math.random(2, 4) + toppings = util.pick(itte_config.ramen.meats)[1] .. ", " + local e_roll = math.random(1, 4) + if e_roll == 1 then + local egg = util.pick(itte_config.ramen.toppings_egg)[1] + toppings = toppings .. "{{toppings}} and " .. util.a_or_an(egg) .. " " .. + egg + end + end + + local tops = util.pick(itte_config.ramen.toppings, t_roll) + -- Check for egg and insert toppings between meat and egg + if (util.is_substr(toppings, "and a")) then + toppings = string.gsub(toppings, "{{toppings}}", table.concat(tops, ", ")) + toppings = string.gsub(toppings, " ", " ") + -- 2 toppings, no egg + elseif t_roll == 2 then + toppings = toppings .. table.concat(tops, " and ") + -- Multiple toppings, no egg + else + local last = tops[#tops] + table.remove(tops) + toppings = toppings .. table.concat(tops, ", ") .. " and " .. last + end + return toppings +end + + +function ramen.prep_condiments() + -- 20% chance of spices + local c_roll = math.random(1, 10) + local cond = util.pick(itte_config.ramen.sauce_verbs)[1] .. " with " .. + util.pick(ramen.sauces)[1] + if c_roll >= 9 then + cond = util.pick(itte_config.ramen.spice_verbs)[1] .. " with " .. + util.pick(ramen.spices)[1] + end + return cond +end + + +function ramen.prep_tapas(mode) + local tapas = util.pick(itte_config.ramen.tapas)[1] + if mode == "meat" then + tapas = util.pick(itte_config.ramen.tapas_meat)[1] + end + return "served with " .. tapas +end + + +function ramen.make_ramen(mode) + local r = "" + if mode == "veg" then + r = ramen.prep_noodles("veg") .. " with " .. + ramen.prep_toppings("veg") .. ", " .. ramen.prep_condiments() + elseif mode == "meat" then + r = ramen.prep_noodles("meat") .. " with " .. + ramen.prep_toppings("meat") .. ", " .. ramen.prep_condiments() + elseif mode == "veg_special" then + r = util.pick(itte_config.ramen.sets)[1] + elseif mode == "meat_special" then + r = util.pick(itte_config.ramen.sets_meat)[1] + end + + -- 50% chance of serving with tapas + local t_roll = math.random(1, 2) + if (t_roll == 1) and (util.is_substr(mode, "veg")) then + r = r .. ". " .. util.first_upper(ramen.prep_tapas("veg")) .. "." + + elseif (t_roll == 1) and (util.is_substr(mode, "meat")) then + r = r .. ". " .. util.first_upper(ramen.prep_tapas("meat")) .. "." + + else + r = r .. "." + end + return util.first_upper(r) +end + + +-- --------------------------------------------------------------------------- +-- Config +-- --------------------------------------------------------------------------- + +-- [[ +-- `itte_config` +-- Add custom settings and override the default config settings here. +-- Settings need to be appened to this variable to be collected by the reload +-- function. +-- ]] +itte_config = { + debug = true, + notify_errors = true, + messages = { + help = "一、二、三、らーめん缶! Hello, I am a ramen vending machine. " .. + "Please type a code for service: {{codes}} " .. + "Support: +81 012-700-1MIO どうぞめしあがれ。ヾ(^ω^ )ノ", + join = ". . . aaand we're in! (*´∇`*)", + part = "Okay, okay, I'll leave. (´・ω・`)", + ping = "ポーン!", + quit = "noodling off", + reload = "Ramen matrix reloaded! (´∀`)", + }, + errors = { + no_perm = "Can ... not, {{user}}. Sorry. " .. + "I'll get in hot water for that! Σ(°△°|||)︴", + notify_no_perm = "{{user}} attempted to use service code: {{code}}", + ns_auth_failed = "Error: Nickserv identification failed.", + sasl_auth_failed = "Error: SASL authentication failed.", + unknown_code = "What's that? Doesn't sound very noodly to me. (´・ω・`)?", + }, +} + +-- Custom variables +itte_config.ramen = ramen + +itte_config.messages.serve = "{{ramen}} ⊂(・▽・⊂)" +itte_config.messages.special = "**SPECIAL!!!** {{ramen}} " .. + "☆*:・゚ o(^ ∀ ^ o)" +itte_config.messages.water = { + "\x01ACTION happily pours the hot liquid into a bowl of noodles " .. + "and offers it to {{user}}\x01", + "Cheers! 自o(^_^ )", + "Water Level [/////////] 200% - Thanks! (^▽^ )", + "Thanks. Some people say I have a CRABby tempeRAMENt. I wonder why. " .. + "(´-` )", + "Q. What do you call noodles made from the core of a tree? " .. + "A. DuRAMEN. *ba dum tss* ┐('∀`;)┌", + "Q. What do you call noodles eaten on bulletin boards? " .. + "A. FoRAMEN. *ba dum tss* ┐('∀`;)┌", + "Q. What kind of noodles can be found at TV stations? " .. + "A. CameRAMEN. *ba dum tss* ┐('∀`;)┌", + "Ramen time anytime! 自o(´▽` )/", +} + + +-- --------------------------------------------------------------------------- +-- Handlers +-- --------------------------------------------------------------------------- + +-- Load the module. +local irc = require("itte") + + +--[[ +-- `itte_handlers` +-- Define any custom handlers in a new table, then assign the table to it. +-- Handlers can also be added to it directly e.g. `itte_handlers.hello()`. +-- Handler names should correspond to the service code name, e.g. a function +-- `itte_handlers.hello()` will be called within chat as +-- `[command_prefix]hello`, as in `!hello`. +--]] +local h = {} + + +-- Serve ramen. +function h.ramen(cxt, msg) + -- 1% chance of special set + local roll = math.random(1, 100) + if roll == 77 then + irc.message(cxt, { msg.reply_to }, + string.gsub(itte_config.messages.special, "{{ramen}}", + ramen.make_ramen("veg_special"))) + else + irc.message(cxt, { msg.reply_to }, + string.gsub(itte_config.messages.serve, "{{ramen}}", + ramen.make_ramen("veg"))) + end +end + + +-- Serve meat-based ramen. +function h.mramen(cxt, msg) + -- 1% chance of special set + local roll = math.random(1, 100) + if roll == 77 then + irc.message(cxt, { msg.reply_to }, + string.gsub(itte_config.messages.special, "{{ramen}}", + ramen.make_ramen("meat_special"))) + else + irc.message(cxt, { msg.reply_to }, + string.gsub(itte_config.messages.serve, "{{ramen}}", + ramen.make_ramen("meat"))) + end +end + + +-- Show help message. +function h.rollcall(cxt, msg) + irc._h.help(cxt, msg) +end + + +-- Reply with a random joke or link. +function h.water(cxt, msg) + local roll = math.random(1, 2) + local resp = util.pick(itte_config.messages.water)[1] + if roll == 2 then + resp = util.pick(itte_config.ramen.links)[1] + end + -- Replace user placeholder if present + if util.is_substr(resp, "{{user}}") then + resp = string.gsub(resp, "{{user}}", msg.sender) + end + irc.message(cxt, { msg.reply_to }, resp) +end + + +itte_handlers = h diff --git a/examples/ramenkan.lua b/examples/ramenkan.lua new file mode 100644 index 0000000..304bc70 --- /dev/null +++ b/examples/ramenkan.lua @@ -0,0 +1,26 @@ +-- Initialise module. +local ramenkan = require("itte") + +-- [[ +-- Set the config file paths. +-- +-- confs.prefix: the keyword prefix for the config files. When `run()` is +-- called, by default the client will look for config files as +-- [prefix].config.lua and [prefix].servers.lua. +-- +-- The `[prefix].config.lua` should contain the `itte_config` and +-- `itte_handlers` variables, and `[prefix].servers.lua` should contain the +-- `itte_servers` variable. The variables can be stored in the same file, but +-- if sharing the source, it is recommended to keep them in separate files to +-- help redact server connection details like passwords. +-- +-- `confs.config` and `confs.server` can be used to pass in the file names +-- individually if they are named differently from the default. +-- +-- Either set confs.prefix, *or* a combination of confs.config and +-- confs.server, but not both. +-- ]] +ramenkan.confs.prefix = "ramenkan" + +-- Call the run function. +ramenkan.run() diff --git a/examples/sample.servers.lua b/examples/sample.servers.lua new file mode 100644 index 0000000..6c2da99 --- /dev/null +++ b/examples/sample.servers.lua @@ -0,0 +1,24 @@ +-- --------------------------------------------------------------------------- +-- Server configuration +-- --------------------------------------------------------------------------- + +-- `itte_servers` +-- Below is a sample server configuration. +-- If the server does not support CAP negotiation (used for SASL +-- authentication), set: cap = nil +-- `auth_type` options: nil, "pass", "sasl", "nickserv" +-- ]] +itte_servers = { + server_name = { + host = "irc.example.tld", + port = 6667, + channels = { "#channel-name" }, + cap = { "sasl" }, + nick = "botnick", + auth_type = nil, + auth_user = "botuser", + auth_pass = "password", + code_prefix = "!", + admins = { adminuser = "password" }, + }, +} diff --git a/itte.lua b/itte.lua new file mode 100644 index 0000000..a2f4e7b --- /dev/null +++ b/itte.lua @@ -0,0 +1,638 @@ +-- --------------------------------------------------------------------------- +-- Itte +-- --------------------------------------------------------------------------- + +--[[ +-- The main module file. +--]] + + +-- --------------------------------------------------------------------------- +-- Variables +-- --------------------------------------------------------------------------- + +local itte = {} + +-- Store config names +itte.confs = { + prefix = nil, + config = ".config.lua", + servers = ".servers.lua", +} + +-- Default application settings +itte.config = { + debug = false, + notify_errors = false, + messages = { + help = "Service codes available: {{codes}}", + join = "We're in.", + part = "The bot is greater than the sum of its parts.", + ping = "Pong!", + quit = "Goodbye.", + reload = "Splines recticulated.", + }, + errors = { + no_perm = "I'm sorry, {{user}}. I'm afraid I can't do that.", + notify_no_perm = "{{user}} attempted to use service code: {{code}}", + ns_auth_failed = "Error: Nickserv identification failed.", + sasl_auth_failed = "Error: SASL authentication failed.", + unknown_code = "I don't understand what you just said.", + }, +} + +-- Store server info +itte.servers = {} + +-- Store custom handlers +itte.handlers = {} + +-- Internal only: store core handlers +itte._h = {} + +-- Internal only: store function docstrings +itte.docs = {} +itte.docs._h = {} + +-- Internal only: track application state +itte.state = { + connected = false, + connections = {}, +} + + +-- --------------------------------------------------------------------------- +-- Functions +-- --------------------------------------------------------------------------- + +local util = require("itteutil") + +itte.docs.help = [[ ([func_str]) + Given a function name, print a corresponding description. If no function name + is provided, print all function descriptions. + ]] +function itte.help(name) + util.get_docs(itte.docs, name) +end + + +itte.docs.get_config = [[ () + Load the config file. + ]] +function itte.get_config() + -- If prefix is not set, try custom file names or defaults + if itte.confs.prefix == nil then + util.source_file(itte.confs.config) + -- Avoid duplicate load if config and servers are set to the same file + if (itte.servers == nil) or (itte.confs.servers ~= itte.conf.config) then + util.source_file(itte.confs.servers) + end + + -- Prefix is set, use prefixed file names + else + util.source_file(itte.confs.prefix .. itte.confs.config) + util.source_file(itte.confs.prefix .. itte.confs.servers) + end + + -- Check for server info + if itte.servers ~= nil then + itte.servers = itte_servers + else + util.debug("config", "Error: servers not found.", itte.config.debug) + do return end + end + if itte_config ~= nil then itte.config = itte_config end + if itte_handlers ~= nil then itte.handlers = itte_handlers end +end + + +itte.docs.get_commands = [[ (server_table) + Return a table of IRC commands to parse and their responses. + ]] +function itte.get_commands(svr) + local default = { + -- CAP and SASL negotiation commands + cap_ls = { + -- No check, command is sent immediately after socket connect + -- before the USER/NICK greet + check = nil, + resp = "CAP LS 302", + }, + cap_req = { + -- Escape the * in "CAP * LS ..." + check = "CAP(.*)LS(.*)sasl", + resp = "CAP REQ ", + }, + auth_plain = { + -- The format of the CAP ACK varies among servers + -- e.g. "CAP [user] ACK ...", "CAP * ACK ..." + check = "CAP(.*)ACK(.*)sasl", + resp = "AUTHENTICATE PLAIN", + }, + auth_pass = { + check = "AUTHENTICATE +", + resp = "AUTHENTICATE ", + }, + cap_end = { + -- Text of the 903 success message varies among servers + -- e.g. "903 * :Authentication successful", + -- "903 [user] :SASL authentication successful" + check = "903(.*)successful", + resp = "CAP END", + }, + -- Nickserv commands + ns_identify = { + check = "NickServ IDENTIFY", + resp = "NickServ IDENTIFY " .. svr.auth_user .. " " .. + tostring(svr.auth_pass), + }, + ns_identify_pass = { + -- No response needed + check = "You are now identified", + resp = nil, + }, + -- Main commands + join = { + -- 001 is a welcome message + -- Cannot join earlier on some servers if not registered + check = "001 " .. svr.nick, + resp = "JOIN ", + }, + nick = { + check = "NOTICE", + resp = "NICK " .. svr.nick, + }, + pass = { + -- No check, command is sent immediately after socket connect + -- before the USER/NICK greet + check = nil, + resp = "PASS " .. svr.auth_user .. ":" .. tostring(svr.auth_pass), + }, + part = { + check = nil, + resp = "PART ", + }, + ping = { + check = "PING ", + resp = "PONG ", + }, + privmsg = { + check = "PRIVMSG(.*):!", + resp = "PRIVMSG ", + }, + quit = { + check = nil, + resp = "QUIT :", + }, + user = { + check = "NOTICE", + resp = "USER " .. svr.auth_user .. " 0 * " .. svr.auth_user, + }, + } + + return default +end + + +itte.docs.send_command = [[ (context_table, cmd_str) + Format a IRC command string to send through a socket connection. + ]] +function itte.send_command(con, str) + con:send(str .. "\r\n") + util.debug("send", str, itte.config.debug) +end + + +itte.docs.message = [[ (context_table, users_table, message_str) + Format and send a private message to a table of users. + ]] +function itte.message(cxt, users, str) + for n = 1, #users do + itte.send_command(cxt.con, cxt.cmds.privmsg.resp .. users[n] .. " :" .. + str) + end +end + + +itte.docs.connect_server = [[ (server_table) + Initialise a socket connection to a server and return a context table. + ]] +function itte.connect_server(svr) + -- Load server context + local context = svr + context.cmds = itte.get_commands(svr) + context.state = { + cap_greeted = false, + cap_ls = false, + cap_checked = false, + ns_checked = false, + authed = false, + } + + -- Connect + local sock = require("socket") + local ssl = require("ssl") + context.con = sock.tcp() + local con_params = { + mode = "client", + protocol = "tlsv1_2", + verify = "none", + options = "all", + } + util.debug("conn", "Connecting to " .. svr.host .. "/" .. svr.port .. + " ...", itte.config.debug) + context.con:connect(svr.host, svr.port) + context.con = ssl.wrap(context.con, con_params) + context.con:dohandshake() + context.state.connected = true + return context +end + + +itte.docs.negotiate_cap = [[ (context_table, data_str) + Negotiate client capabilities and authenticate with SASL. + ]] +function itte.negotiate_cap(cxt, str) + -- Some servers have a delay between ls and req + if (not cxt.state.cap_greeted) and (not cxt.state.cap_checked) and + (not cxt.state.cap_ls) then + itte.send_command(cxt.con, cxt.cmds.cap_ls.resp) + cxt.state.cap_ls = true + end + + if (str ~= nil) then + util.debug("auth", str, itte.config.debug) + + -- Wait for server to respond with capabilities list before greeting + if (not cxt.state.cap_greeted) then + itte.send_command(cxt.con, cxt.cmds.user.resp) + itte.send_command(cxt.con, cxt.cmds.nick.resp) + cxt.state.cap_greeted = true + end + + -- Request capabilities + if util.is_substr(str, cxt.cmds.cap_req.check) then + itte.send_command(cxt.con, cxt.cmds.cap_req.resp .. ":" .. + table.concat(cxt.cap, " ")) + -- Authenticate with plain SASL + elseif (util.is_substr(str, cxt.cmds.auth_plain.check)) then + itte.send_command(cxt.con, cxt.cmds.auth_plain.resp) + + elseif (util.is_substr(str, cxt.cmds.auth_pass.check)) then + local mime = require("mime") + -- Format of the string to encode: "user\0user\0password" + local sasl_pass, _ = mime.b64(cxt.auth_user .. "\0" .. cxt.auth_user .. + "\0" .. cxt.auth_pass) + itte.send_command(cxt.con, cxt.cmds.auth_pass.resp .. sasl_pass) + + -- Look for auth success signal and end cap negotiation + elseif (util.is_substr(str, cxt.cmds.cap_end.check)) then + itte.send_command(cxt.con, cxt.cmds.cap_end.resp) + cxt.state.authed = true + cxt.state.cap_checked = true + if (cxt.state.cap_checked) and (not cxt.state.authed) and + (itte.config.notify_errors) then + itte.send_command(cxt.con, cxt.cmds.cap_end.resp) + itte.message(cxt, util.table_keys(cxt.admins), + itte.config.errors.sasl_auth_failed) + cxt.state.cap_checked = true + end + end + end +end + + +itte.docs.auth_nickserv = [[ (context_table, data_str) + Authenticate with NickServ commands. + ]] +function itte.auth_nickserv(cxt, str) + if str ~= nil then + util.debug("auth", str, itte.config.debug) + + if util.is_substr(str, cxt.cmds.ns_identify.check) then + itte.send_command(cxt.con, cxt.cmds.ns_identify.resp) + cxt.state.ns_checked = true + elseif util.is_substr(str, cxt.cmds.ns_identify_pass.check) then + cxt.state.authed = true + if (cxt.state.ns_checked) and (not cxt.state.authed) and + (itte.config.notify_errors) then + itte.message(cxt, util.table_keys(cxt.admins). + itte.config.errors.ns_auth_failed) + end + end + end +end + + +itte.docs.is_admin = [[ (context_table, message_table) + Check whether a user is an admin. Return true if the user and password are + in the admin table, or false otherwise. + ]] +function itte.is_admin(cxt, msg) + -- No password provided + if table.concat(msg.code_params) == "" then + do return false end + -- Incorrect password provided + elseif msg.code_params ~= nil then + local in_admins = util.is_entry(cxt.admins, msg.sender, + msg.code_params[#msg.code_params]) + if not in_admins then + do return false end + else + return true + end + end +end + + +itte.docs.notify_no_perms = [[ (context_table, message_table) + Respond to an attempt to use service code by a non-admin user and send a + message to the admins if `notify_errors` is enabled in the config. + ]] +function itte.notify_no_perms(cxt, msg) + itte.message(cxt, { msg.reply_to }, string.gsub(itte.config.errors.no_perm, + "{{user}}", msg.sender)) + if itte.config.notify_errors then + local notify_msg = string.gsub(itte.config.errors.notify_no_perm, + "{{user}}", msg.sender) + notify_msg = string.gsub(notify_msg, "{{code}}", msg.code) + itte.message(cxt, util.table_keys(cxt.admins), notify_msg) + end +end + + +itte.docs.traverse_channels = [[ (context_table, mode_str, channels_table) + Given a context table, task mode and optional table of IRC channels, join or + part from the channels. Task modes: join, part. If no channel table is given, + it will join or leave the channels specified in the server configuration. + ]] +function itte.traverse_channels(cxt, mode, chans) + local channels = cxt.channels + if chans ~= nil then channels = chans end + + if mode == "join" then + itte.send_command(cxt.con, cxt.cmds.join.resp .. table.concat(channels, + ",")) + + elseif mode == "part" then + itte.send_command(cxt.con, cxt.cmds.part.resp .. table.concat(channels, + ",")) + end +end + + +itte.docs.parse_privmsg = [[ (context_table, data_str) + Given the context table and a PRIVMSG data string, parse the string and + return an associative table of values. + ]] +function itte.parse_privmsg(cxt, str) + local code_full = string.sub(str, string.find(str, ":" .. cxt.code_prefix) + + 1 + string.len(cxt.code_prefix)) + local msg = { + sender = string.sub(str, string.find(str, "!") + 2, + string.find(str, "@") - 1), + recipient = string.sub(str, string.find(str, "PRIVMSG") + 8, + string.find(str, ":!") - 2), + reply_to = string.sub(str, string.find(str, "!") + 2, + string.find(str, "@") - 1), + } + if util.is_substr(code_full, " ") then + msg.code = string.sub(code_full, 0, string.find(code_full, " ") - 1) + msg.code_params = util.split_str(string.sub(code_full, + string.find(code_full, " ") + 1)) + + else + msg.code = code_full + msg.code_params = {} + end + -- Reply to sender by default (private message), or reply in a channel if + -- original message recipient is a channel. + if util.is_substr(msg.recipient, "#") then + msg.reply_to = msg.recipient + end + + util.debug("privmsg", "{ " .. + "sender: " .. msg.sender .. + ", recipient: " .. msg.recipient .. + ", reply_to: " .. msg.reply_to .. + ", code: " .. msg.code .. + ", code_params: " .. table.concat(msg.code_params) .. + " }", itte.config.debug) + return msg +end + + +-- --------------------------------------------------------------------------- +-- Service code handlers +-- --------------------------------------------------------------------------- + +itte.docs._h.help = [[ (context_table, message_table) + Send a help message listing available service codes. +]] +-- Send a help message listing available service codes. +function itte._h.help(cxt, msg) + local custom_h = util.table_keys(itte.handlers) + local codes = cxt.code_prefix .. table.concat(custom_h, ", " .. + cxt.code_prefix) + -- Core service codes are shown only to admins + if itte.is_admin(cxt, msg) then + local core_h = util.table_keys(itte._h) + codes = cxt.code_prefix .. table.concat(core_h, ", " .. cxt.code_prefix) .. + ", " .. cxt.code_prefix .. table.concat(custom_h, ", " .. + cxt.code_prefix) + end + local help_msg = string.gsub(itte.config.messages.help, "{{codes}}", codes) + itte.message(cxt, { msg.reply_to }, help_msg) +end + + +itte.docs._h.join = [[ (context_table, message_table) + Join specified channels. + ]] +function itte._h.join(cxt, msg) + if not itte.is_admin(cxt, msg) then + itte.notify_no_perms(cxt, msg) + do return end + end + if msg.code_params ~= {} then + itte.traverse_channels(cxt, "join", msg.code_params) + itte.message(cxt, { msg.reply_to }, itte.config.messages.join) + end +end + + +itte.docs._h.part = [[ (context_table, message_table) + Leave specified channels. + ]] +function itte._h.part(cxt, msg) + if not itte.is_admin(cxt, msg) then + itte.notify_no_perms(cxt, msg) + do return end + end + if msg.code_params ~= {} then + itte.traverse_channels(cxt, "part", msg.code_params) + itte.message(cxt, { msg.reply_to }, itte.config.messages.part) + end +end + + +itte.docs._h.ping = [[ (context_table, message_table) + Send a "pong" message. + ]] +function itte._h.ping(cxt, msg) + itte.message(cxt, { msg.reply_to }, itte.config.messages.ping) +end + + +itte.docs._h.quit = [[ (context_table, message_table) + Disconnect from the server. + ]] +function itte._h.quit(cxt, msg) + if not itte.is_admin(cxt, msg) then + itte.notify_no_perms(cxt, msg) + do return end + end + itte.send_command(cxt.con, cxt.cmds.quit.resp .. itte.config.messages.quit) +end + + +itte.docs._h.reload = [[ (context_table, message_table) + Reload the server config. + ]] +function itte._h.reload(cxt, msg) + if not itte.is_admin(cxt, msg) then + itte.notify_no_perms(cxt, msg) + do return end + end + itte.get_config() + itte.message(cxt, { msg.reply_to }, itte.config.messages.reload) +end + + +-- --------------------------------------------------------------------------- +-- Service code mapping and runtime +-- --------------------------------------------------------------------------- + +itte.docs.listen = [[ (context_table, data_str) + Parse the socket data string and trigger a response to the server if the + string matches preset patterns. + ]] +function itte.listen(cxt, str) + util.debug("listen", str, itte.config.debug) + + -- Respond to server ping + if util.is_substr(str, cxt.cmds.ping.check) then + itte.send_command(cxt.con, string.gsub(str, cxt.cmds.ping.check, + cxt.cmds.ping.resp)) + + -- Respond to service code + elseif util.is_substr(str, cxt.cmds.privmsg.check) then + local msg = itte.parse_privmsg(cxt, str) + -- Check for the service code in the functions table before attempting to + -- call the function. + if util.has_key(itte._h, msg.code) then + itte._h[msg.code](cxt, msg) + -- Check the handlers table + elseif util.has_key(itte.handlers, msg.code) then + itte.handlers[msg.code](cxt, msg) + else + itte.message(cxt, { msg.reply_to }, itte.config.errors.unknown_code) + end + end +end + + +itte.docs.add_listener = [[ (context_table) + Monitor a socket connection and pass the data received for parsing. If the + connection is closed by the server and was the only server connection, + set `state.connected` to false to activate a client exit. + ]] +function itte.add_listener(cxt) + -- Set a short timeout to be non-blocking + cxt.con:settimeout(0.5) + local data, status = cxt.con:receive() + if data ~= nil then + itte.listen(cxt, data) + + elseif status == "closed" then + util.debug("conn", "Connection " .. status, itte.config.debug) + cxt.con:close() + itte.state.connections[cxt.name] = nil + -- Check if it is the last connection and trigger client exit + -- if no connections remain. + if #itte.state.connections == 0 then + util.debug("conn", "Exiting ...", itte.config.debug) + itte.state.connected = false + end + end +end + + +itte.docs.run = [[ () + Run the client. + ]] +function itte.run() + itte.get_config() + + -- Connect to servers and get context with socket ref for each server + local contexts = {} + for instance, prop in pairs(itte.servers) do + contexts[instance] = itte.connect_server(prop) + contexts[instance].name = instance + itte.state.connected = true + itte.state.connections[instance] = true + contexts[instance].con:settimeout(1) + + -- For PASS-based authentication, send PASS before greeting the server + if contexts[instance].auth_type == "pass" then + itte.send_command(contexts[instance].con, + contexts[instance].cmds.pass.resp) + contexts[instance].state.authed = true + end + + -- Greet the server in all auth scenarios except SASL, which greets the + -- server during CAP negotiation. + if (contexts[instance].auth_type ~= "sasl") then + itte.send_command(contexts[instance].con, + contexts[instance].cmds.user.resp) + itte.send_command(contexts[instance].con, + contexts[instance].cmds.nick.resp) + end + + local joined_chans = false + while (itte.state.connections[instance]) and (not joined_chans) do + local data, status = contexts[instance].con:receive() + + if contexts[instance].auth_type == "sasl" then + itte.negotiate_cap(contexts[instance], data) + + elseif contexts[instance].auth_type == "nickserv" then + itte.auth_nickserv(contexts[instance], data) + end + + -- Minimum requirements for joining channels: client has authenticated if + -- an auth type is set, or has received a NOTICE [nick] message during an + -- ident lookup. + if contexts[instance].state.authed then + itte.traverse_channels(contexts[instance], "join") + joined_chans = true + elseif (data ~= nil) then + if util.is_substr(data, contexts[instance].cmds.join.check) then + itte.traverse_channels(contexts[instance], "join") + joined_chans = true + end + end + end + end + + -- Add listeners + while itte.state.connected do + for instance, cxt in pairs(contexts) do + itte.add_listener(cxt) + end + end + +end + + +return itte diff --git a/itte.py b/itte.py deleted file mode 100644 index 6ec095e..0000000 --- a/itte.py +++ /dev/null @@ -1,185 +0,0 @@ -import argparse -import socket -import yaml -from time import sleep -from random import randint -from sys import exit - - -class Util: - """Utility functions.""" - - def yml(self, yml_file): - """Open a YAML file and return a dictionary of values.""" - try: - fh = open(yml_file, "r") - data = yaml.safe_load(fh) - fh.close() - except TypeError: - exit("[debug][err] Cannot load YML file. Please check it exists.") - return data - - def rand(self, lst): - """Return a random item from a given list.""" - return lst[randint(0, len(lst)-1)] - - def cli_flags(self): - """Parse command line flags.""" - self.argp = argparse.ArgumentParser() - self.argp.add_argument("-c", "--config", help="Config file") - return self.argp.parse_args() - - -class IRC: - """Methods for basic IRC communication.""" - - def config(self, def_conf): - """Load runtime settings from a YAML config file, and returns a - dictionary of config values. Looks for the file in a runtime path or in - the default location.""" - self.util = Util() - # Check for runtime config locatiion - flags = self.util.cli_flags() - if flags.config != "": - cfg = self.util.yml(flags.config) - else: - cfg = self.util.yml(def_conf) - self.server = (cfg["server"]["host"], cfg["server"]["port"]) - self.channels = cfg["channels"] - self.bot_nick = cfg["bot_nick"] - self.req_prefix = cfg["req_prefix"] - self.admin_user = cfg["admin"]["user"] - self.admin_code = cfg["admin"]["code"] - self.debug = cfg["debug"] - return cfg - - def run(self, listen_hook): - """A routine that connects to a server, joins channels, and attaches - the request listener hook to a loop.""" - self.connect(self.server, self.bot_nick) - # Wait for server to reply before joining channels - svr_greet = self.receive() - while ("001 " + self.bot_nick) not in svr_greet: - sleep(1) - svr_greet = self.receive() - self.join_channels(self.channels) - while 1: - data = self.receive() - self.keepalive(data, self.bot_nick) - self.msg = self.parse(data, self.req_prefix) - for c in self.channels: - # Pass in a context dict for handlers - listen_hook({"msg": self.msg, "listen_chan": c}) - - def connect(self, server, bot_nick): - """Connect to the server and sends user/nick information.""" - try: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect(server) - except ConnectionError as err: - exit("[debug][err] " + str(err)) - self.send("USER", bot_nick + " " + bot_nick + " " + \ - bot_nick + " " + bot_nick) - self.send("NICK", bot_nick) - - def disconnect(self, resp_msg, quit_msg): - """Notify the admin user and disconnect from the server.""" - self.send("PRIVMSG", resp_msg, recvr=self.admin_user) - self.send("QUIT", ":" + quit_msg) - # Currently only one server per app instance is supported, so - # disconnect also exits the app - exit("Shutting down ...") - - def keepalive(self, line, bot_nick): - """Stay connected to a server by responding to server pings.""" - if line.split(" ")[0] == "PING": - resp = line.replace("PING", "PONG", 1) - if self.debug: - print("[debug][send] " + resp) - self.sock.sendall(bytes(resp + "\r\n", "utf-8")) - # Fallback to ensure process exits if timeout occurs - elif (bot_nick in line.split(" ")[0]) and \ - ("QUIT :Ping timeout:" in line): - exit("[debug][err] Ping timeout, exited.") - - def join_channels(self, channels): - """Join channels given a list of channel names.""" - for c in channels: - if c.strip() != "" or c.strip() != "#": - self.send("JOIN", c) - - def send(self, command, text, *args, **kwargs): - """Send messages given the IRC command and text. Optionally specify a - message recipient with `recvr=user`.""" - recvr = kwargs.get("recvr", "") - if recvr != "": - recvr += " :" - if self.debug: - print("[debug][send] " + command + " " + recvr + text) - bs = bytes(command + " " + recvr + text + "\r\n", "utf-8") - try: - self.sock.sendall(bs) - except BrokenPipeError as err: - if self.debug: - print("[debug] " + str(err) + " at `" + \ - bs.decode("utf-8").strip() + "`") - pass - - def receive(self): - """Get messages from the connected socket.""" - data = self.sock.recv(4096).decode("utf-8").strip("\r\n") - if self.debug: - print("[debug][recv] " + data) - return data - - def parse(self, line, req_prefix): - """Using received data from a socket, extract the request, the nick and - username of the requester, the channel where the request originated and - return a dictionary of values.""" - data = {"req": "", "req_chan": "", "nick": "", "user": ""} - if (":" + req_prefix) in line: - data["req"] = line.split("PRIVMSG", 1)[1].split(":" + \ - req_prefix, 1)[1].strip() - data["req_chan"] = line.split("PRIVMSG ", \ - 1)[1].split(" :", 1)[0] - data["nick"] = line.split("!~", 1)[0][1:] - data["user"] = line.split("!~", 2)[0][0:].split("@", 1)[0] - return data - - def listen(self, context, trigger, handler, *args, **kwargs): - """Listen for a trigger and call the handler. It takes a context - dictionary (to determine the channel and recipient), trigger string and - corresponding handler function. Optional flags (chan, query, admin) can - be used to specify whether a trigger is active in channels or by - /msg. e.g. `chan=False, query=True` to make a trigger query-only. By - default, it will listen in both channels and private messages.""" - in_chan = kwargs.get("chan", True) - in_query = kwargs.get("query", True) - in_admin = kwargs.get("admin", False) - # Admin requests are query/pm only - if in_admin: - in_chan = False - msg = context["msg"] - channel = context["listen_chan"] - # Responses are sent via pm to the user by default, while requests made - # in a channel are sent to the channel. While it's possible to override - # the recvr, it usually easier to enable a trigger in the same location - # where the response will be sent. - context["recvr"] = msg["user"] - - if msg["req"] == trigger: - # Respond only in the channel where the request was made - if in_chan and channel == msg["req_chan"]: - context["recvr"] = msg["req_chan"] - handler(context) - # Respond to query/pm - elif in_query and msg["req_chan"] == self.bot_nick: - handler(context) - # Respond only to the admin user - elif in_admin and msg["user"].lower() == \ - self.admin_user.lower() and self.admin_code in msg["req"]: - handler(context) - - def reply(self, cxt, text): - """Alias of send() with PRIVMSG command preset.""" - self.send("PRIVMSG", text, recvr=cxt["recvr"]) diff --git a/itteutil.lua b/itteutil.lua new file mode 100644 index 0000000..d449dd4 --- /dev/null +++ b/itteutil.lua @@ -0,0 +1,252 @@ +-- --------------------------------------------------------------------------- +-- Itte Util +-- --------------------------------------------------------------------------- + +--[[ +-- A collection of helper functions used by Itte. +--]] + +local itteutil = {} +itteutil.docs = {} + + +itteutil.docs.get_docs = [===[ (docstring_table [, func_str [, printd_bool]]) + Given a table of docstrings and a function name, return the description for + the function name. If `name` is unspecified, return descriptions for all + functions. If `printd` is unspecified, print to stdout, or if set to false, + return results as a string. + ]===] +function itteutil.get_docs(docstr, name, printd) + local docs = "" + if name ~= nil then + -- 2nd-level nested objects, e.g. x.y.func() + if itteutil.is_substr(name, "%.") then + local sep, _ = string.find(name, "%.") + local dk = name:sub(1, sep - 1) + local nk = name:sub(sep + 1) + docs = "\n" .. name .. " " .. string.gsub(docstr[dk][nk]:sub(3), + " ", "") .. "\n" + else + docs = "\n" .. name .. " " .. string.gsub(docstr[name]:sub(3), " ", "") + .. "\n" + end + else + -- Sort on-site since associative arrays have no fixed order + local dk = itteutil.table_keys(docstr) + docs = "\n" + for c = 1, #dk do + -- 2nd-level nested objects, e.g. x.y.func() + if type(docstr[dk[c]]) == "table" then + local n_dk = itteutil.table_keys(docstr[dk[c]]) + for nc = 1, #n_dk do + docs = docs .. dk[c] .. "." .. n_dk[nc] .. " " .. + string.gsub(docstr[dk[c]][n_dk[nc]]:sub(3), " ", "") .. "\n" + end + else + docs = docs .. dk[c] .. " " .. string.gsub(docstr[dk[c]]:sub(3), + " ", "") .. "\n" + end + end + end + + if printd == false then + return docs + else + print(docs) + end +end + + + +itteutil.docs.help = [[ ([func_str]) + Given a function name, print a corresponding description. +]] +function itteutil.help(name) + itteutil.get_docs(itteutil.docs, name) +end + + +itteutil.docs.is_substr = [[ (str, find_str) + Check if a string is a substring of another string. Return true if it is a + substring, or false otherwise. + ]] +function itteutil.is_substr(str, search) + if (string.find(str, search) ~= nil) then + return true + else + return false + end +end + + +itteutil.docs.first_upper = [[ (str) + Return a string with the first character in uppercase. + ]] +function itteutil.first_upper(str) + return str:sub(1, 1):upper() .. str:sub(2) +end + + +itteutil.docs.first_lower = [[ (str) + Return a string with the first character in lowercase. + ]] +function itteutil.first_lower(str) + return str:sub(1, 1):lower() .. str:sub(2) +end + + +itteutil.docs.split_str = [[ (str, [, pattern_str]) + Split a string on a pattern separator and return a table of string values, + e.g. split_str("Hello world", "%S+"). If no pattern is specified, split + string on the space character. + ]] +function itteutil.split_str(str, sep) + local tbl = {} + local n = 0 + -- Other patterns: https://www.lua.org/pil/20.2.html + if sep == nil then sep = "%S+" end + for sub in string.gmatch(str, sep) do + n = n + 1 + tbl[n] = sub + end + return tbl +end + + +itteutil.docs.a_or_an = [[ (str) + Return the corresponding indefinite article for a word. + Does *not* apply to acronyms, e.g. FTP. + ]] +function itteutil.a_or_an(str) + local article = "a" + if itteutil.is_substr("a e i o u", str:sub(1, 1)) or + itteutil.is_substr("ho", str:sub(1, 2)) then + article = "an" + end + if (str == "one") or (itteutil.is_substr("eu", str:sub(1, 2))) or + (itteutil.is_substr("uni usa use usi usu", str:sub(1, 3))) then + article = "a" + end + return article +end + + +itteutil.docs.table_keys = [[ (table) + Return a sorted table of keys for an associative table. + ]] +function itteutil.table_keys(tbl) + local keys = {} + local n = 0 + for k, v in pairs(tbl) do + n = n + 1 + keys[n] = k + end + table.sort(keys) + return keys +end + + +itteutil.docs.has_key = [[ (table, find_str) + Given a table and string, check whether the string is a key in the table. + Return true if found, or false otherwise. + ]] +function itteutil.has_key(tbl, str) + local keys = itteutil.table_keys(tbl) + -- Primitive check for array-like table with numerical keys + if (keys[1] == 1) and (keys[#keys] == #keys) then + for k = 1, #keys do + if str == tbl[k] then + do return true end + end + end + -- Associative table + else + for k = 1, #keys do + if str == keys[k] then + do return true end + end + end + end + return false +end + + +itteutil.docs.is_entry = [[ (table, key_str, val_str) + Given an associative table, a key and value pair as strings, check whether + the pair is in the table. Return true if it exists in the table, or false + otherwise. + ]] +function itteutil.is_entry(tbl, key, val) + for k, v in pairs(tbl) do + if (key == k) and (val == v) then + do return true end + end + end + return false +end + + +itteutil.docs.pick = [===[ (table [, num_int [, unique_bool ]]) + Pseudo-randomly pick entries from a non-associative table and return the + entries in a table. If `num` is unspecified, it will return one entry. + If `unique` is unspecified, it will return non-recurring values. + ]===] +function itteutil.pick(tbl, num, unique) + local picks = {} + if num == nil then num = 1 end + if unique == nil then unique = true end + picks[1] = tbl[math.random(1, #tbl)] + + if (num > 1) and (num <= #tbl) then + for n = 2, num do + local p = tbl[math.random(1, #tbl)] + -- Check for duplicate + if unique then + local c = 1 + -- Avoid endless loop if all values in the table are identical + while (itteutil.has_key(picks, p)) and (c <= #tbl) do + p = tbl[math.random(1, #tbl)] + c = c + 1 + end + end + picks[n] = p + end + end + return picks +end + + +itteutil.docs.sleep = [[ (seconds_int) + Set a delay in seconds. + ]] +function itteutil.sleep(s) + local timer = os.clock() + while (os.clock() - timer < s) do end +end + + +itteutil.docs.debug = [[ (tag_str, debug_str [, enabled_bool]) + Format and print debug output if `enabled` is true. + ]] +function itteutil.debug(tag, str, enabled) + if enabled then + print("[" .. tag .. "] " .. str) + end +end + + +itteutil.docs.source_file = [[ (filename_str) + Load a source file. + ]] +function itteutil.source_file(str) + local func, err = loadfile(str) + if func then + return func() + else + print("Error: " .. err) + do return end + end +end + + +return itteutil diff --git a/ramenkan.py b/ramenkan.py deleted file mode 100755 index e9ade03..0000000 --- a/ramenkan.py +++ /dev/null @@ -1,191 +0,0 @@ -from mastodon import Mastodon -from random import randint - -from itte import IRC, Util - - -class Ramen: - """Requests with a ramen theme.""" - - def main(self): - """Instantiate an IRC object and attach the listeners.""" - # Load yaml sources used by request handlers - self.util = Util() - self.rand = self.util.rand - self.misc = self.util.yml("ramenkan/misc.yml") - self.links = self.util.yml("ramenkan/links.yml") - self.photos = self.util.yml("ramenkan/photos.yml") - self.dishes = self.util.yml("ramenkan/dishes.yml") - # Init irc object and load config - self.irc = IRC() - self.cfg = self.irc.config("ramenkan/default.config.yml") - # Init mastodon object - if "mastodon" in self.cfg: - self.masto = Mastodon( - api_base_url=self.cfg["mastodon"]["base_url"], - access_token=self.cfg["mastodon"]["access_token"], - client_id=self.cfg["mastodon"]["client_id"], - client_secret=self.cfg["mastodon"]["client_secret"] - ) - # Init request listeners - self.irc.run(self.add_listeners) - - def add_listeners(self, cxt): - """Map triggers to handlers.""" - self.irc.listen(cxt, "exit " + self.cfg["admin"]["code"], self.quit, \ - admin=True) - self.irc.listen(cxt, "rollcall", self.rollcall) - self.irc.listen(cxt, "help", self.rollcall) - self.irc.listen(cxt, "water " + self.cfg["bot_nick"], self.water) - self.irc.listen(cxt, "botsnack " + self.cfg["bot_nick"], self.botsnack) - self.irc.listen(cxt, "ramen", self.ramen) - self.irc.listen(cxt, "vramen", self.ramen_veggie) - self.irc.listen(cxt, "rk", self.ramen) - self.irc.listen(cxt, "rkveg", self.ramen_veggie) - self.irc.listen(cxt, "rklink", self.link) - self.irc.listen(cxt, "rkselfie", self.selfie) - if "mastodon" in self.cfg: - self.irc.listen(cxt, "rktoot", self.toot) - self.irc.listen(cxt, "rkvtoot", self.toot_veggie) - - def quit(self, cxt): - """Disconnect from the server and quit.""" - self.irc.disconnect("Okay, okay, I'll leave. (´・ω・`)", "noodling off") - - def rollcall(self, cxt): - """Handle request for app info.""" - self.irc.reply(cxt, self.misc["rollcall"]) - - def water(self, cxt): - """Handle water offer.""" - resp = self.misc["water"] - for index, r in enumerate(resp): - if "{{ nick }}" in r: - resp.append(r.replace("{{ nick }}", cxt["msg"]["nick"])) - resp.pop(index) - self.irc.reply(cxt, self.rand(resp)) - - def botsnack(self, cxt): - """Handle snack offer.""" - self.irc.reply(cxt, self.rand(self.misc["botsnack"])) - - def make_ramen_combo(self, *args, **kwargs): - """Generate a ramen dish. Optionally pass `veggie=True` for a - vegetarian dish.""" - dish = self.dishes - # Check vegetarian flag - is_veggie = kwargs.get("veggie", False) - # 20% chance of tsukemen - roll = randint(1, 10) - if roll <= 2: - combo = "tsukemen from " - else: - combo = "ramen in " - # Noodle type and broth richness - if randint(0, 1): - combo = self.rand(dish["noodle-shape"]) + ", " + \ - self.rand(dish["noodle-broth-type"]) + " " - combo = combo.capitalize() - # Broth type - if is_veggie: - combo += self.rand(dish["broth-veggie"]) - else: - combo += self.rand(dish["broth"] + dish["broth-veggie"]) - combo += " broth" - # Topping - n_top = randint(2, 5) - for n in range(2, n_top+1): - if n == 2: - if is_veggie: - prev_top = self.rand(dish["topping"]) - else: - prev_top = self.rand(dish["meat"]) - combo += " with " + prev_top - else: - if n > 2 and n < n_top: - combo += ", " - else: - combo += " and " - if is_veggie: - next_top = self.rand(dish["topping"]) - else: - # 20% chance of egg topping - roll = randint(1, 10) - if roll <= 2: - next_top = self.rand(dish["topping-egg"]) - else: - next_top = self.rand(dish["topping"]) - # Check for duplicate - while next_top in prev_top: - next_top = self.rand(dish["topping"]) - combo += next_top - prev_top = next_top - # Condiment and side dish - if randint(0, 1): - combo += ", sprinkled with " + self.rand(dish["condiment"]) + "." - else: - combo += "." - if randint(0, 1): - combo += " Served with " - if is_veggie: - combo += self.rand(dish["tapa-veggie"]) + "." - else: - combo += self.rand(dish["tapa"] + dish["tapa-veggie"]) + "." - return combo - - def pick_ramen(self, *args, **kwargs): - """Pick a ramen dish. Optionally set vegetarian selection with - `veggie=True`.""" - roll = randint(1, 100) - veggie = kwargs.get("veggie", False) - # 1% possibility of regional preset - if roll == 100 and veggie: - pick = (self.dishes["set-veggie"] + ".").capitalize() - elif roll == 100 and not veggie: - pick = (self.rand(self.dishes["set"] + \ - self.dishes["set-veggie"]) + ".").capitalize() - elif roll <= 99 and veggie: - pick = self.make_ramen_combo(veggie=True) - else: - pick = self.make_ramen_combo() - return pick - - def ramen(self, cxt): - """Handle ramen request.""" - self.irc.reply(cxt, self.pick_ramen()) - - def ramen_veggie(self, cxt): - """Handle vegetarian ramen request.""" - self.irc.reply(cxt, self.pick_ramen(veggie=True)) - - def link(self, cxt): - """Handle to display a titled link.""" - index = randint(0, len(self.links)-1) - self.irc.reply(cxt, self.links[index]["title"] + " " + \ - self.links[index]["link"]) - - def selfie(self, cxt): - """Handle to display a photo link.""" - self.irc.reply(cxt, self.rand(self.photos["ticket"])) - - def toot_wrapper(self, cxt, txt): - """Wrap around the Mastodon library's toot command for basic error - handling.""" - try: - self.masto.toot(self.misc["toot"]["prefix"] + " " + txt) - self.irc.reply(cxt, txt + " " + self.misc["toot"]["success"]) - except: - self.irc.reply(cxt, self.misc["toot"]["error"]) - pass - - def toot(self, cxt): - """Handle post ramen to Mastodon.""" - self.toot_wrapper(cxt, self.pick_ramen()) - - def toot_veggie(self, cxt): - """Handle post veggie ramen to Mastodon.""" - self.toot_wrapper(cxt, self.pick_ramen(veggie=True)) - - -app = Ramen() -app.main() diff --git a/ramenkan/config.sample.yml b/ramenkan/config.sample.yml deleted file mode 100644 index 32a429b..0000000 --- a/ramenkan/config.sample.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Server and channel settings -server: - host: "localhost" - port: 6667 - -channels: - - "#bots" - -bot_nick: "ramenkan" - -# User and code for admin actions -admin: - user: "user" - code: "ramen" - -# Request prefix, e.g. "!" for "!" -req_prefix: "!" - -# Print messages to stdout -debug: False - -# Mastodon account -# mastodon: -# base_url: "https://example.com" -# access_token: "" -# client_secret: "" -# client_id: "" diff --git a/ramenkan/dishes.yml b/ramenkan/dishes.yml deleted file mode 100644 index 51b4e08..0000000 --- a/ramenkan/dishes.yml +++ /dev/null @@ -1,143 +0,0 @@ -# Lists of ingredients to assemble a ramen dish -broth: - - Kobe beef bone - - tonkotsu - - pork bone and chicken - - pork bone and niboshi # dried little sardines - - pork bone, chicken and seafood - - pork bone, chicken, tuna and kelp - - pork bone and soy sauce - - chicken - - chicken, katsuo and konbu # skipjack tuna and kelp - - fish - - gyokai # dried anchovies/shrimp/squid/seaweed - - gyokai tonkotsu - - roasted tuna - - miso and truffle # (from chef Ryu Takahashi) - - milk - - curry and milk - -broth-veggie: - - miso - - spicy miso - - soy sauce - - grilled soy sauce and Rishiri kelp - - salt - - salt and soy sauce - -noodle-broth-type: - - thick noodles in rich - - medium-thick noodles in mild - - thin noodles in light - -noodle-shape: - - curly - - flat - - straight - -condiment: - - butter - - rayu # chili oil - - chili peppers - - chili sauce - - mayu # garlic oil - - spicy garlic and ginger oil - - sesame oil - - dashi # fish and seaweed stock - - white pepper - - black pepper - - ponzu # citrus sauce - - shichimi # spice blend - -topping: - - bean sprouts - - chili flakes - - sweet corn - - grated garlic - - roasted garlic chips - - beni shoga # pickled ginger - - bamboo shoots - - menma # fermented bamboo shoots - - dried mushrooms - - shiitake mushrooms - - kikurage # wood ear - - karashi takana # spicy pickled mustard greens - - negi # Welsh onion - - white onions - - spring onions - - sichuan peppers - - nori - - wakame - - sauerkraut - - sesame seeds - - scallions - - boiled spinach - -topping-egg: - - a raw egg - - a seasoned boiled egg - - an ajitama # egg marinated in soy sauce - -meat: - - Kobe beef fillet - - Kobe beef sirloin - - wagyu - - teppanyaki beef - - beef shank - - chicken breast - - chicken meatballs - - teppanyaki chicken - - crab - - crispy duck - - filet mignon - - kamaboko # fish paste/surimi - - narutomaki # fish paste with swirling pattern - - kakuni # cubed braised pork - - chashu # Japanese-style barbequed pork - - pork confit - - pork cutlet - - pork belly - - pork shoulder - - shrimp - - sea urchin - - scallops - - squid - -tapa: - - gyoza # dumplings (fried) - - karaage # fried chicken - - tako wasabi # marinated octopus - - takoyaki # octopus balls - - corn and shrimp salad - - crab salad - - enoki rolls - -tapa-veggie: - - edamame tossed in sea salt # boiled/steamed soyabeans - - tofu nuggets - - pickled daikon - - cucumber salad - - seaweed salad # wakame, sesame seeds, mirin - -set: - - Korean kimchi ramyeon with tteok - - Vancouver cold bonito beer broth ramen with egg whites - - Hakata tonkotsu ramen with pickled mustard greens - - Hakodate shio ramen with kelp, crab, shrimp, sea urchin and scallops - - Hakodate shrimp ramen with a hard-boiled egg and white onions - - Kagoshima tonkotsu ramen with chashu, dried sardines, kelp and scallions - - Kitakata niboshi-tonkotsu ramen with chashu, naruto and spring onions - - Kumamoto tonkotsu ramen with chashu, raw eggs, scallions and garlic chips - - Muroran curry ramen with chashu, wakame and bean sprouts - - Nagasaki champon with fried pork, scallions and seasonal greens - - Osaka Kobe beef sirloin ramen with a soft-boiled egg and arugula - - Sapporo miso ramen with scallops, sweet corn and bean sprouts - - Tokyo ramen in chicken broth with pork, menma, a boiled egg and nori - - Wakayama shoyu-tonkotsu ramen with pork slices, naruto and scallions - - Yokohama ramen with chashu, spinach, a soft-boiled egg and negi - -set-veggie: - - miso ramen with vegetable broth, cabbage and bean sprouts - - ramen with shiitake mushrooms, kombu and sesame oil - - ramen with soyabean pork slices, kikurage, bamboo shoots and scallions - - barley miso ramen with roasted vegetables diff --git a/ramenkan/links.yml b/ramenkan/links.yml deleted file mode 100644 index 1379f2b..0000000 --- a/ramenkan/links.yml +++ /dev/null @@ -1,70 +0,0 @@ -- title: Ramen, Soul Food Staple - link: https://www3.nhk.or.jp/nhkworld/en/food/articles/51.html -- title: Shin-Yokohama Ramen Museum, Yokohama - link: http://www.raumen.co.jp/english/ -- title: Ramen Street, Tokyo - link: https://en.wikipedia.org/wiki/Ramen_street -- title: Sapporo Ramen Republic - link: http://www.sapporo-esta.jp/ramen -- title: Ramen DB, a directory of ramen shops in Japan - link: https://ramendb.supleks.jp/ -- title: Ramen Adventures, a ramen shop review blog - link: http://www.ramenadventures.com/ -# Recipes -- title: 15 Fantastic Ramen Recipes - link: https://www3.nhk.or.jp/nhkworld/en/food/articles/131.html -# Instant noodles -- title: Momofuku Ando, inventor of instant ramen (1958) - link: https://www.nissin.com/en_jp/about/founder/ -- title: Instant cup noodles (1971) - link: https://en.wikipedia.org/wiki/Cup_Noodles -- title: Instant noodles as emergency food in disaster relief - link: https://instantnoodles.org/en/activities/support.html -- title: Samyang, the first Korean instant ramen (1963) - link: https://en.wikipedia.org/wiki/Samyang_ramen -- title: The Ramen Rater, an instant ramen blog - link: https://www.theramenrater.com/ -# Ramen in pop culture -- title: Ramen Manga, comic strips in Japanese about ramen - link: https://www.ramenbank.com/manga/ -- title: Flower Boy Ramen Shop, romantic comedy drama (2011) - link: https://en.wikipedia.org/wiki/Flower_Boy_Ramen_Shop -- title: The Ramen Girl, romantic comedy film (2008) - link: https://en.wikipedia.org/wiki/The_Ramen_Girl -- title: Ms. Koizumi Loves Ramen Noodles, manga series (2013-) - link: https://en.wikipedia.org/wiki/Ms._Koizumi_Loves_Ramen_Noodles -- title: Muteki Kanban Musume, manga series (2002-2006) - link: https://en.wikipedia.org/wiki/Muteki_Kanban_Musume -- title: Detective Conan - Ramen So Good, It's to Die For, anime episode (2012) - link: https://www.detectiveconanworld.com/wiki/Ramen_So_Good,_It%27s_to_Die_For -- title: Ramen Teh, film (2018) - link: https://en.wikipedia.org/wiki/Ramen_Teh -- title: Ramen yori taisetsuna mono, documentary (2013) - link: https://www.imdb.com/title/tt2945642/ -# Videos -- title: What Owning a Ramen Restaurant in Japan is Like - link: https://www.youtube.com/watch?v=gmIwxqdwgrI -- title: Begin Japanology - Ramen - title: https://www.youtube.com/watch?v=RosUc9UVuos -- title: Japanology Plus - Ramen - link: https://www.youtube.com/watch?v=K2Jzt04QY7A -- title: In Search of the Perfect Bowl of Ramen - link: https://www.youtube.com/watch?v=bhiFtP9qVYc -- title: Rahaku TV (The Shin-Yokohama Ramen Museum Youtube Channel) - link: http://www.youtube.com/rahakutv -- title: The History of Ramen Yamagoya - link: https://www.youtube.com/watch?v=8NBwKbT5QSQ -- title: Ramen Daruma - link: https://www.youtube.com/watch?v=zkhDYTR789w -- title: The God of Ramen, trailer (2013) - link: https://www.youtube.com/watch?v=mtwunTGnnR4 -- title: Ramen Heads, trailer (2017) - link: https://www.youtube.com/watch?v=u61b5R_S4TM -- title: Ramenomania - Noodles, Asia's Second Obsession - link: https://www.youtube.com/watch?v=tfW89yAoTNs -- title: Noodle Me - link: https://www.youtube.com/watch?v=r9jLJRZ6gnc -- title: Visio Documentary - Fat Ramen - link: https://www.youtube.com/watch?v=o78fy4rJFCw -- title: Ramen Talk and Q+A With Chef Ryu Takahashi and Chef Matt Kimura - link: https://www.youtube.com/watch?v=tMvArDklmGU diff --git a/ramenkan/misc.yml b/ramenkan/misc.yml deleted file mode 100644 index bd00cb9..0000000 --- a/ramenkan/misc.yml +++ /dev/null @@ -1,28 +0,0 @@ -rollcall: - "一、二、三、らーめん缶! - Hello, I am a ramen vending machine. Please type a code for service: - !help !ramen !vramen !rklink !rkselfie - - Support: +81 012-700-1MIO どうぞめしあがれ。" - -water: - - "\x01ACTION happily pours the hot liquid into a bowl of noodles - and offers it to {{ nick }}\x01" - - "Cheers! 自o(^_^ )" - - "Water Level [/////////] 200% - Thanks! (^▽^ )" - - "Thanks. Some people say I have a CRABby tempeRAMENt. - I wonder why. (´-` )" - - "Q. What do you call noodles made from the core of a tree? - A. DuRAMEN. *ba dum tss* ┐('∀`;)┌" - - "Q. What do you call noodles eaten on bulletin boards? - A. FoRAMEN. *ba dum tss* ┐('∀`;)┌" - - "Q. What kind of noodles can be found at TV stations? - A. CameRAMEN. *ba dum tss* ┐('∀`;)┌" - -botsnack: - - "CHIKIN RAAAAAMEN━━━(゜∀゜)━━━!!!!!" - - "Ramen time anytime! 自o(´▽` )/" - -toot: - prefix: "Now serving:" - success: "Now shared with Mastodon! (^v^)" - error: "Hyuuuuu! Sorry, I can't seem to toot right now. Ask me again later." diff --git a/ramenkan/photos.yml b/ramenkan/photos.yml deleted file mode 100644 index c59e8c9..0000000 --- a/ramenkan/photos.yml +++ /dev/null @@ -1,44 +0,0 @@ -# Photo links of ramen vending machines -# Ticket machines -ticket: - - https://c2.staticflickr.com/6/5077/5902028877_7d8c65b23f_b.jpg - - https://c1.staticflickr.com/3/2455/5777431350_36e147e719_z.jpg - - https://i.pinimg.com/originals/72/08/1e/72081e68ff0989d54fd24a86fcac6f2b.jpg - - https://i.pinimg.com/originals/9f/74/92/9f7492b7c456ac7ba350498a50c74b23.jpg - - https://c1.staticflickr.com/9/8157/7105010129_cd4b0d7e27_c.jpg - - https://c1.staticflickr.com/8/7600/27418644850_2c70342734_c.jpg - - https://c3.staticflickr.com/3/2049/2229742287_8df3e823a9_z.jpg - - https://c1.staticflickr.com/1/101/308523474_4295a27326_z.jpg - - https://media-cdn.tripadvisor.com/media/photo-s/0b/b6/27/f6/ramen-shop-menu-banner.jpg - - https://abroadabroad2011.files.wordpress.com/2013/03/sam_0040.jpg - - https://c1.staticflickr.com/6/5753/22803130417_ba86ebb4fa_c.jpg - - https://c1.staticflickr.com/6/5077/5902028877_7d8c65b23f_z.jpg - - https://c1.staticflickr.com/8/7504/15210528573_2612010fc3_c.jpg - - https://s3-media4.fl.yelpcdn.com/bphoto/iUHJczm_--xNqWEUzDS69Q/o.jpg - - https://thewholeworldornothing.com/wp-content/uploads/2017/01/vending-machine-waiter.jpg - - https://thebrunchingbooth.files.wordpress.com/2014/07/img_42681.jpg - - https://c1.staticflickr.com/8/7426/16587852342_0cbfa4a63c_c.jpg - - https://c1.staticflickr.com/9/8422/7535359482_b5537bbde0_c.jpg - - https://c1.staticflickr.com/5/4037/4530351457_8666286b56_z.jpg - - https://c1.staticflickr.com/1/97/244236130_0c22679f97_z.jpg - - https://c1.staticflickr.com/4/3479/3465112347_1be2868ff0_z.jpg - - https://1.bp.blogspot.com/_W-pG7tUmJk0/TDQ-J_vz9sI/AAAAAAAACac/xYjAvl8ZGOY/s1600/shinjuku_ramen_02.jpg - - https://thefoodieflight.files.wordpress.com/2015/10/img_3749.jpg - - https://static1.squarespace.com/static/57ad3b51c534a528e26a2e93/t/57f91e3be6f2e18cf1d11c82/1476472671797/ - - https://c1.staticflickr.com/1/166/417127513_e063ccf80a_z.jpg - -# Instant cup noodles -instant: - - http://www.toxel.com/wp-content/uploads/2009/06/vendingmachine04.jpg - - https://c1.staticflickr.com/3/2233/2347915323_9e9e2264a3_z.jpg - - https://c1.staticflickr.com/4/3818/10186967173_237d38b86f_c.jpg - - https://c1.staticflickr.com/8/7221/7175446233_076264a2b4_c.jpg - - https://c7.alamy.com/comp/BCDJRG/a-cup-noodle-vending-machine-instant-ramen-museum-osaka-japan-1-december-BCDJRG.jpg - -# Ramen can -can: - - https://c1.staticflickr.com/4/3625/3374051074_641b371d23_z.jpg - - https://c4.staticflickr.com/4/3214/2911821453_78c6178f54_z.jpg - - https://smallbiztrends.com/wp-content/uploads/2015/06/P1020817-728x485.jpg - - https://c1.staticflickr.com/9/8440/7745533776_7b9990be82_c.jpg - - https://c1.staticflickr.com/3/2268/4507516049_e7aa354489_z.jpg