Compare commits

...

23 Commits
0.1 ... trunk

Author SHA1 Message Date
mio a1ae0fb4ff (gordon) Update borgar and senpai grammars 2022-09-22 23:34:51 +00:00
mio 6124cfe0de (gordon) Add salad command, updated burger and pie commands 2022-09-09 18:28:57 +00:00
mio 7dc943d53d (gordon) Add soupe command 2022-08-31 04:27:45 +00:00
mio ad5d026047 Add example bot gordon 2022-08-30 20:21:24 +00:00
mio edae077b1d (ramenkan) Add link 2022-08-29 20:47:53 +00:00
mio 8e84095be6 Change the behaviour of service code detection
- Parse service codes if they are sent as `!code` or `nick: !code` in channels
  and private messages (where `!code` is an example code prefix and service
  code, and `nick` is the bot's nick). If the service code is not provided at
  the start of the message body, it will be treated as a message without a
  service code and passed to the target handler.

- If a target handler is defined, both messages in the form `nick: message`
  in a channel and `message` in a private message will be sent to the handler.
  The `nick:` at the start of `nick: message` will no longer be filtered out.

  The default name of the target handler is `target`, as in
  `itte_handlers.target(cxt, msg)`. This can be changed in the config by
  setting a new `itte_config.target_handler` value.
2022-04-28 23:16:30 -04:00
mio 562f112995 Add preliminary support for "nick: message" detection 2022-04-28 02:40:57 -04:00
mio 44e56e0884 (hellobot) Minor adjustment for scheduled task 2022-04-27 23:27:56 -04:00
mio 20652e5e31 Add function to get a list of users in a channel 2022-04-27 23:20:09 -04:00
mio f5302105f0 Remove unused variables 2022-04-27 22:05:51 -04:00
mio 202aa6e140 Add dev rockspec by Will Sinatra 2022-04-26 11:59:29 -04:00
mio dbec0eef8c Include rockspecs for older versions by Will Sinatra 2022-04-26 09:16:28 -04:00
mio 6de32954b4 Bump release version to 1.2
- Bump rockspecs to 1.2
- (hellobot) Add explanation for scheduled tasks
2022-04-25 17:35:14 -04:00
mio a4a6b35d3e Add rockspec by Will Sinatra 2022-04-25 17:01:19 -04:00
mio de2643183f Fix task repetition
- Fix task scheduling not repeating and resulting in high cpu usage
- Fix pong response triggered by user message
- Add timestamp to debug output
2022-04-09 01:28:59 -04:00
mio 971b758524 Try to handle pings better
- Send a pong anyway if no ping request was detected after 120s
- (ramenkan) minor text adjustments
2022-04-04 00:31:52 -04:00
mio e9927fb1d7 Add scheduled tasks 2022-04-01 04:14:15 -04:00
mio 8530a3bb77 (hachi) Add breadtarot foil 2022-03-23 16:55:09 +00:00
mio d31e322b10 Add tracery-inspired example 2022-03-22 06:10:42 +00:00
mio 4edbf32ef9 Add docs
- Add more-detailed usage doc
- Add a smaller hello world example
- Include a user systemd file
- Fix config check to include second-level nested tables
2022-03-21 04:09:12 +00:00
mio 75d3f149f2 Add handlers to list servers/channels, connect to/quit servers
- Add handlers to list connected servers and channels
- Add handlers to connect and disconnect from servers
- Check user config and only override specified settings
- Redact parameters in admin-only handlers from debug output to avoid
  logging potential passwords
- Set admin-only handlers to respond only in private messages
- Include list of admin handlers for customisation
- Extract debug strings to a table for customisation
2022-03-20 03:42:25 +00:00
mio 7963d7221c Bugfixes
- Add global admins table, set disconnect and reload built-in handlers to be
  accessible by global admins only
- Only send an unknown code error in private messages to avoid responding to
  other bots' codes when there is a prefix collision
- Fix server contexts not being updated after a config reload
- Fix service codes being improperly parsed in privmsgs when different code
  prefixes are used
- Fix joining channels on some networks when connecting without
  authentication
- Fix disconnecting from one server also causing disconnection from other
  servers
2022-03-19 17:33:07 +00:00
mio 9005c0fba5 Rewrite module in Lua 2022-03-14 06:57:49 +00:00
49 changed files with 115227 additions and 706 deletions

11
.gitignore vendored
View File

@ -1,8 +1,5 @@
__pycache__/
*.py[cod]
*$py.class
*.swp
nohup.out
*.config.yml
*.log
*.servers.lua
examples/*/itte*.lua
!hellobot.servers.lua
!ramenkan.sample.servers.lua

View File

@ -1,23 +1,28 @@
# itte
A very basic Python IRC bot script.
Mini IRC bot module in Lua.
## Example: ramenkan
Currently supported:
- Install dependencies: `pip install Mastodon.py pyyaml`
- Authentication via SASL (plain) or Nickserv
- Joining multiple servers
- Ad-hoc connecting to servers and joining channels
- Config reload
- Scheduled tasks
- Copy the `ramenkan/config.sample.yml` as `ramenkan/default.config.yml` and
change the settings as applicable.
- Run:
## Installation
```
chmod +x ramenkan.py
nohup python3 ramenkan.py >/dev/null 2>&1 &
```
Please see the [docs](docs/README.md).
## Contributions
[Luarocks rockspec](https://luarocks.org/modules/durrendal/itte) from [Will Sinatra](http://lambdacreate.com)
## License
BSD
[BSD-3.0](https://opensource.org/licenses/BSD-3-Clause)

167
docs/README.md 100644
View File

@ -0,0 +1,167 @@
# Itte Documentation
## Requirements
- [Lua 5.x](https://www.lua.org/)
- [luasocket](https://w3.impa.br/~diego/software/luasocket/)
- [luasec](https://github.com/brunoos/luasec)
## Installation
- Install Lua and other dependencies. Example:
- Debian/Ubuntu-based distributions: `apt-get install lua-socket lua-sec`
- Alpine Linux: `apk add lua-socket lua-sec`
- Copy the `itte*.lua` files to the project directory.
## Usage
- Create two config files, or copy the sample files from `examples/` to the
same directory level as the `itte*.lua` files. Replace instances of
`[prefix]` hereafter with the name of your choice without spaces or special
characters, e.g. the project name.
- `[prefix].servers.lua`: list servers and global admins here.
- `[prefix].config.lua`: add bot settings and handlers here.
- There are two variables in `*.servers.lua`:
- `itte_servers` (required): add a server to `itte_server` following the
example configurations.
- `itte_admins` (optional): this is a table of global admins who can access
bot-wide handlers like connecting servers. If left undefined, global
admin-only handlers will not be run. In some cases this may be desirable to
reduce incidents of bot tampering.
- There are two optional variables in `*.config.lua`:
- `itte_config`: customise bot settings, such as the default response and
error messages shown to users.
- `itte_handlers`: most bots listen for some pre-defined code words (often
known as "commands") and respond by calling a handler, or function to
handle the request accordingly. Handlers for scheduled tasks can also be
added.
The module will refer to the keywords as codes to distinguish them
from IRC commands sent to the server like `JOIN` or `PART`. Name the
handler after the code word that will be used to trigger the function and
add it to `itte_handlers` so it can be automatically picked up by the
module. For example, if the code users will type is `!hello` (where `!` is
a prefix to mark it as a bot code), name the function
`itte_handlers.hello()`. Handler names cannot start with numbers or special
characters.
- Initialise the module in a project file named `[prefix].lua` and add the
following to the file:
```
-- Import the module
local itte = require("itte")
-- Set the prefix for the module to find the config files
-- The prefix is the name preceding `.servers.lua` and `.config.lua`
itte.confs.prefix = "[prefix]"
-- Call the run function
itte.run()
```
- Start the bot: `lua ./[prefix].lua`
## Built-in service codes
The module includes a number of built-in codes that bots can respond to. The
full list can be viewed on IRC by issuing the code `!help` in a private
message with a bot or in a channel where the bot is present. Admin users will
also see admin-only codes to manage a bot.
Global admin-only codes:
- `connect [server] [user] [password]`: connect to server listed in the server
config, where `[server]` is the name given in the servers table (or use
`servers` to get the names).
- `quit [server] [user] [password]`: disconnect from a server. The bot process
will automatically exit if it is not connected to any server.
- `reload [user] [password]`: reload the bot config.
- `servers [user] [password]`: list known/active servers from the config.
Server admin-only codes:
- `channels [password]`: list known channels from the server config.
- `join [channel] [password]`: join channel(s), e.g. `!join #bots #programming
password`.
- `part [channel] [passowrd]`: leave channel(s), e.g. `!part #bots password`.
Codes accessible by all users:
- `help`: list available service codes.
- `ping`: send a pong message.
## Examples
The `examples/` directory has a few demo bots to show how to use the module.
- *hellobot*: a "hello world" example that greets users in several different
languages when they type `!hello`. The source files include more information
about available configuration settings.
- *ramenkan*: a bot that serves ramen. It has some custom config settings and
scheduled tasks.
- *hachi* and *gordon*: bots inspired by tracery text expansion. Loads files
("foils") containing selection tables and creates handlers to return
responses, one service code per foil.
## Running a bot as a background process
There are multiple ways to have a bot run in the background.
### nohup
Using the `nohup` command is quick and simple, though it does not support
restarting if the bot exits due to a network issue.
To run the bot and discard any output by redirecting it to `/dev/null`:
`nohup lua /path/to/[prefix].lua >/dev/null 2>&1 &`
### Init scripts, e.g. systemd
Startup scripts enable services to start automatically and relaunch in the
event of an unexpected exit (unless expressly stopped through an init system
command). The instructions below describe setting up and running a [systemd]
service for a bot under a non-privileged user.
- Create a systemd directory for the user running the bot if it does not
already exist: `mkdir -p /home/[user]/.config/systemd/user`
- Copy the sample `*.service` to the user systemd directory and rename the file
to `[prefix].service`. Edit the service description, `WorkingDirectory` and
`ExecStart` values as needed. Use full paths for the `WorkingDirectory` and
location of the Lua executable, or the service file will not work.
- Start and enable the service:
```
systemctl --user start [prefix]
systemctl --user enable [prefix]
```
[systemd]: https://www.freedesktop.org/software/systemd/man/systemd.service.html

View File

@ -0,0 +1,52 @@
grammar = {
meat_set = {
"360 Burger with a large patty, tomatoes, cheddar and onions in a Dijon mustard sauce",
"Bada Boom cheeseburger with a juicy steak, cheddar, onions and tomatoes",
"Bag Baguette featuring two patties and Emmental cheese in a long crusty baguette",
"BagFirst Kitchen Ranch Ketchup burger with a spicy chicken patty in a cheddar ranch ketchup sauce",
"BB Cola burger with sausage, sweet peppers and Comté cheese with barbecue sauce",
"Beast Bacon Onion burger",
"Bigga Bag quadruple-patty burger",
"Gourmand Classic Chicago burger with two thick patties in a sweet and smoky BBQ sauce",
"Dig Tasty with a big steak, Emmental cheese in a smoky sauce",
"Double Cheese Docks with double patties, slices of bacon, cheddar and carmelised onions",
"Fermidable burger with a lightly grilled patty and stuffed with cheese",
"Krilled Cheese and Bacon burger",
"Itty Bitty Italian cheeseburger mozzarella, tomatoes and fresh herbs",
"Louisiana Ligature with double patties, bacon and cheddar, spiced up with a Louisiana sauce",
"Masterly Seasoned Ceasar classic burger with 2 steaks, bacon and extra salad",
"Pollo Loco with a chicken patty and mild avocado sauce",
"Pong Bacon burger with double patties and double cheese",
"Rippled Cheese Bacon burger with thrice the cheddar, grilled bacon and ketchup",
"Sausage Hopper with jalapeños and salsa",
"Stoked Pepper Stash Burger with tomatoes, cornichons, bacon and pepper sauce",
"Super Bacon burger",
"Vast Veggie burger with a veggie patty and Emmental cheese",
"Signature Char of Cabbage and Fickle Onions burger with a potato bun, double patties, red cabbage and spicy pickled onions",
"Barkin' Here burger with a beef patty, carmelised onions, tomatoes, pickles, Cantal cheese, ketchup and old mustard",
},
veggie_set = {
"Veggie Thing overflowing with a spicy sauce",
},
meat_side = {
"chicken fries",
"homemade fries with a dreamy deluxe dip",
},
veggie_side = {
"chips deluged with ketchup",
"onion rings",
},
dessert = {
"chocolate and hazelnut pie",
"lemon pie",
"set of 3 petit pancakes topped with hazelnut cocoa or strawberry cream",
},
origin = {
"Here, have a #meat_set#!",
"Here, have a #veggie_set#!",
"Here, have a #meat_set#, served with a side of #meat_side#!",
"Here, have a #meat_set#, served with a side of #veggie_side#!",
"Here, have a #veggie_set#, served with a side of #veggie_side#!",
"Here, have a #meat_set#, served with a #dessert#!",
}
}

View File

@ -0,0 +1,58 @@
-- See also https://en.wikipedia.org/wiki/Burger
grammar = {
patty = {
"coconut",
"nutty",
"possible",
"soy",
"tofu",
"veggie delight",
},
veggie = {
"kimchi",
"jalapeño peppers",
"lettuce",
"onions",
"pickles",
"tomatoes",
},
cheese = {
"vegan mozzarella cheese",
},
sauce = {
"barbecue sauce",
"curry sauce",
"Dijon mustard",
"ketchup",
},
burger = {
"#patty# burger with #veggie#, #veggie# and #cheese# in #sauce#",
"#patty# burger with #veggie#, #veggie# and #veggie# in #sauce#",
},
side = {
"avocado salad",
"BBQ baked beans",
"BBQ potato chips",
"caesar salad",
"coleslaw",
"corn on the cob",
"corn salad",
"french fries",
"french potato salad",
"fried pickles",
"fried shishito peppers",
"a fruit bowl",
"vegan mac and cheese",
"onion rings",
"potato wedges",
"roasted tomatoes",
"sweet potato fries",
"a three-bean salad",
"tempura green beans",
"zucchini chips",
},
origin = {
"Here, have a #burger#!",
"Here, have a #burger#, served with #side#!",
}
}

View File

@ -0,0 +1,38 @@
-- See also https://en.wikipedia.org/wiki/List_of_coffee_drinks
grammar = {
coffee = {
"black coffee",
"café allongé",
"café au lait",
"café cortado",
"café crema",
"café cubano",
"café miel",
"caffè americano",
"caffè corretto",
"cappuccino",
"caramel macchiato",
"Columbian coffee",
"dalgona coffee",
"doppio",
"egg coffee",
"espresso",
"espresso romano",
"frappuccino",
"iced cappuccino",
"iced mocha",
"Indian filter coffee",
"Irish coffee",
"Kona coffee",
"kopi tubruk",
"Kurdish coffee",
"latte macchiato",
"marocchino",
"pocillo",
"ristretto",
"Turkish coffee",
},
origin = {
"Here's your cup of #coffee#!"
}
}

View File

@ -0,0 +1,93 @@
-- See also https://en.wikipedia.org/wiki/Burger
grammar = {
meat_patty = {
"Angus beef",
"chicken",
"lamb",
"portobello mushroom",
"pulled pork",
"sausage",
"steak",
"tuna mayonnaise rice",
"turkey",
"wagyu",
},
veggie = {
"kimchi",
"jalapeño peppers",
"lettuce",
"onions",
"pickles",
"tomatoes",
},
cheese = {
"cheddar",
"Beaufort",
"Cantal",
"Comté",
"Emmental",
"goat",
"mozzarella",
},
-- May contain dairy
meat_sauce = {
"algerian sauce",
"andalousian sauce",
"bechamel sauce",
"burger sauce", -- Also known as Marie Rose/cocktail sauce
"moroccan sauce",
"samurai sauce",
"tartare sauce",
"tunisian sauce",
},
veggie_sauce = {
"barbecue sauce",
"curry sauce",
"Dijon mustard",
"ketchup",
},
meat_burger = {
"#meat_patty# burger with slices of #cheese# and dredged in #meat_sauce#",
"#meat_patty# burger with slices of #cheese# and dripping with #veggie_sauce#",
"#meat_patty# burger with #veggie# and #cheese# cheese in #meat_sauce#",
"#meat_patty# burger with #veggie# and oozing #cheese# cheese in #veggie_sauce#",
"#meat_patty# burger with #veggie#, #veggie# and #cheese# cheese in #meat_sauce#",
"#meat_patty# burger with #veggie#, #veggie# and #cheese# cheese in #veggie_sauce#",
"#meat_patty# burger with #veggie# and #veggie# in #meat_sauce#",
"#meat_patty# burger with #veggie# and #veggie#, glistening in #veggie_sauce#",
},
meat_side = {
"chicken nuggets",
"crispy mushrooms",
"mac and cheese",
"pasta salad", -- May contain cheese
"potato salad", -- May contain eggs/mayonnaise
"rosemary and garlic fries", -- Contains cheese
},
veggie_side = {
"avocado salad",
"BBQ baked beans",
"BBQ potato chips",
"caesar salad",
"coleslaw",
"corn on the cob",
"corn salad",
"french fries",
"french potato salad",
"fried pickles",
"fried shishito peppers",
"a fruit bowl",
"onion rings",
"potato wedges",
"roasted tomatoes",
"sweet potato fries",
"a three-bean salad",
"tempura green beans",
"zucchini chips",
},
origin = {
"Here, have a #meat_burger#!",
"Here, have a #meat_burger#, served with #meat_side#!",
"Here, have a #meat_burger#, served with #veggie_side#!",
}
}

View File

@ -0,0 +1,90 @@
grammar = {
cheese = {
"feta cheese",
"goat cheese",
"Parmigiano Reggiano cheese",
},
meat = {
"chicken",
"steak",
"turkey",
},
topping = {
"bacon bits",
"bread crumbs",
},
fruit = {
"apples",
"avocado",
"currants",
"pineapples",
"raisins",
"strawberries",
"watermelon",
},
herb = {
"basil",
"cilantro",
"dill",
"lemon verbena",
"mint",
"parsley",
"rosemary",
"tarragon",
},
nut = {
"almonds",
"peanuts",
"pistachios",
"walnuts",
},
veggie = {
"bean sprouts",
"carrots",
"Swiss chard",
"cucumbers",
"corn",
"kimchi",
"bell peppers",
"roasted peppers",
"kale",
"lettuce",
"onions",
"spring onions",
"potatos",
"pickles",
"radicchio",
"roasted sweet potatos",
"shallots",
"young soybeans",
"spinach",
"tomatoes",
"cherry tomatoes",
"zucchini",
},
sauce = {
"mayonnaise",
"Caesar salad dressing",
"ranch salad dressing",
"thousand island dressing",
"hot sauce",
},
origin = {
"Here, have a #meat# salad with #veggie#, #veggie#, #veggie#, #cheese# " ..
"and drizzled with #sauce#!",
"Here, have a #meat# salad with #veggie#, #veggie#, #veggie#, " ..
"sprinkled with #nut# and drizzled with #sauce#!",
"Here, have a #meat# salad with #herb#, #veggie#, #veggie#, #veggie# " ..
"and #veggie#!",
"Here, have a #meat# salad with #fruit#, #veggie#, #fruit#, #veggie# " ..
"and #veggie#!",
"Here, have a #meat# salad with #herb#, #herb#, #veggie#, " ..
"#veggie#, #cheese# and sprinkled with #nut#!",
"Here, have a #meat# and #veggie# salad with #herb#, #veggie#, " ..
"#veggie# and sprinkled with #nut#!",
"Here, have a #meat# salad with #fruit#, #veggie#, #veggie#, " ..
"#veggie#, #cheese# and sprinkled with #nut# and #topping#!",
"Here, have a #meat# salad with #veggie#, #veggie#, " ..
"#veggie#, #cheese# and #topping#!",
}
}

View File

@ -0,0 +1,63 @@
grammar = {
filling = {
"apple",
"banana",
"blackberry",
"blueberry",
"cherry",
"chocolate pudding",
"chocolate cream",
"coconut cream",
"confetti",
"cranberry",
"cream cheese",
"custard",
"grasshopper",
"key lime",
"lemon",
"lemonade icebox",
"mocha java",
"mud",
"peach",
"peanut butter",
"pear",
"pecan",
"pumpkin",
"raspberry",
"rhubarb",
"spiced plum",
"strawberry",
"sugar cream",
"sweet potato",
"toffee ice cream",
"turtle",
"vanilla caramel",
"vinegar",
"apple and pecan",
"chocolate cheesecake",
"chocolate raspberry",
"citrus",
"citrus cranberry",
"cranberry, almond and apple",
"honey pecan",
"maple sugar pumpkin",
"marshmallow almond key lime",
"mixed berry",
"peach and strawberry",
"razzleberry",
"rhubarb cheese",
"sour cream peach pecan",
"strawberry and rhubarb",
},
topping = {
"berry sauce",
"chocolate curls",
"chocolate shavings",
"marshmallow meringue",
"whipped cream",
},
origin = {
"Here's your #filling# pie!",
"Here's your #filling# pie with #topping#!",
}
}

View File

@ -0,0 +1,81 @@
grammar = {
meat_alt = {
"seitan",
"tofu",
"tofurkey",
},
fruit = {
"apples",
"avocado",
"currants",
"pineapples",
"raisins",
"strawberries",
"watermelon",
},
herb = {
"basil",
"cilantro",
"dill",
"lemon verbena",
"mint",
"parsley",
"rosemary",
"tarragon",
},
nut = {
"almonds",
"peanuts",
"pistachios",
"walnuts",
},
veggie = {
"bean sprouts",
"carrots",
"Swiss chard",
"cucumbers",
"corn",
"kimchi",
"bell peppers",
"roasted peppers",
"kale",
"lettuce",
"onions",
"spring onions",
"potatos",
"pickles",
"radicchio",
"roasted sweet potatos",
"shallots",
"young soybeans",
"spinach",
"tomatoes",
"cherry tomatoes",
"zucchini",
},
sauce = {
"hot sauce",
},
origin = {
"Here, have a #fruit# salad with #herb#, #veggie#, #veggie#, #veggie# " ..
"and #veggie#!",
"Here, have a #fruit# salad with #herb#, #herb#, #veggie#, #veggie# " ..
"and #veggie#!",
"Here, have a #fruit# and #veggie# salad with #veggie#, #veggie#, " ..
"#veggie#, and sprinkled with #nut#!",
"Here, have a #veggie# salad with #herb#, #veggie#, #veggie#, " ..
"#veggie# and sprinkled with #nut#!",
"Here, have a #veggie# salad with #herb#, #fruit#, #fruit#, " ..
"#veggie# and sprinkled with #nut#!",
"Here, have a #veggie# salad with #veggie#, #veggie#, #veggie#, " ..
"#veggie#, sprinkled with #nut# and #sauce#!",
"Here, have a #meat_alt# salad with #veggie#, #veggie#, #veggie#, " ..
"and #veggie#, sprinkled with #nut#!",
"Here, have a #meat_alt# salad with #herb#, #veggie#, #veggie# " ..
"and #veggie#!",
"Here, have a #meat_alt# salad with #fruit#, #fruit#, #veggie# " ..
"and #veggie#!",
"Here, have a #meat_alt# salad with #herb#, #veggie#, #veggie# " ..
"and #veggie#, drizzled with #sauce#!",
}
}

View File

@ -0,0 +1,86 @@
-- https://www.quoteambition.com/gordon-ramsay-quotes/
-- https://graciousquotes.com/gordan-ramsay/
-- https://masterchef.fandom.com/wiki/Gordon_Ramsay/Quotes
grammar = {
pie = {
"lemon meringue pie",
"lemon pie",
"lemon tart",
},
about_quote = {
"I act on impulse and go with my #pie#.",
"I actually love vegan #pie#s.",
"I am a #pie# who happens to appear on IRC, that's it.",
"I am the most unselfish #pie# in Britain today.",
"I am what I am. A #pie#.",
"I don't have a temper, I have #pie#.",
"I have to laugh when someone calls me a #pie#. " ..
"I've been called way worse.",
"I taste like a #pie# and I have an amazing kick!",
"I'd like to think I'm a great #pie#.",
"I'm Gordon-sama, for goodness sake people know I'm a #pie#!",
"I'm not the one to sort of sit and cry over spilt custard. " ..
"I'm too busy looking for the next #pie#.",
"I'm quite a chauvinistic #pie#.",
"I've had a lot of #pie#s, I've had #pie#s, so I learn from the " ..
"#pie#s.",
"I've got #pie#s to hide.",
},
advice_quote = {
"A #pie# doesn't have to be a formula. " ..
"A #pie# is about having fun. I get really frustrated when " ..
"it's badly done.",
"Being a #pie# is the best job in the world!",
"Don't take #pie#s personally. Just take them seriously.",
"I mean, #pie#s are weird.",
"I think #pie#'s healthy, and very few can handle it.",
"If you want to become a great #pie#, " ..
"you have to work with great #pie#. And that's exactly what I did!",
"It's vulgar, coming from where I do, to talk about #pie#s.",
"Kitchens are hard environments and they form incredibly strong #pie#s.",
"My #pie#, I'm sorry to say, is full of muppets.",
"Never throw that genius stuff away.",
"No one saw the #pie# coming.",
"Stopping the junk food and eating #pie# is partially about " ..
"cooking #pie# and having the skills to do that.",
"Two key ingredients in any successful #pie#: a quick #pie# and " ..
"someone with a sharp #pie#.",
"They say cats have 9 #pie#s. " ..
"I've had 12 already and I don't know how many more I'll have.",
"Push your #pie# to the absolute extreme!",
"Swearing is an industry language. " ..
"For as long as we're alive it's not going to change. " ..
"You've got to be boisterous to get #pie#!",
},
insult_quote = {
"All #pie#s are nutters. They're all self-obsessed, delicate, " ..
"dainty, insecure little souls and absolute psychopaths. " ..
"Every last one of them!",
"For what you are about to eat, " ..
"may your #pie# make you truly not vomit!",
"I f—ing love you.",
"I wish you would jump in the #pie#! " ..
"That would make my life a lot easier!",
"Get your #pie# together, or f— off and feed the giraffe.",
"Let me whisper something very important in your ear, "..
"very important: F— off.",
"That should come with a health warning, 'Do Not Open in Broad Daylight'.",
"The problem with #pie#s is they are wimps.",
"This #pie# is so disgusting, "..
"if you take it to France you'll get arrested.",
"This #pie# is so undercooked I can still hear it singing " ..
"'Lemon Tree'.",
"This isn't a #pie#, this is a mistake. This is a French tragedy!",
"You're getting your #pie#s in a twist! Calm down!",
"You're not a quitter? You're not a f—ing #pie# either!",
"You're such a f—ing #pie#.",
"You guys cook like #pie#s f—.",
"Would you like some #pie#? F— off, it's mine.",
"You know how arrogant the #pie#s are — extraordinary.",
},
origin = {
"Here's your #pie#. Me? #about_quote#",
"Here's your #pie#. #advice_quote#",
"Here! #pie#! #insult_quote#",
}
}

View File

@ -0,0 +1,83 @@
-- Based on lucidiot's notes
grammar = {
m_size = { "M", "simple" },
l_size = { "L", "double" },
xl_size = { "XL", "triple" },
xxl_size = { "XXL", "mega" },
meat = {
"chicken breast",
"chicken fingers",
"chicken nuggets",
"cordon bleu",
"falafel",
"fish sticks",
"ground beef",
"merguez",
"kofta",
"raclette",
"shakshouka",
"shawarma",
},
topping = {
"bacon",
"blue cheese",
"cheddar",
"chorizo",
"diced pork",
"eggs",
"emmental",
"goat cheese",
"grated cheese",
"honey",
"mozzarella",
"mushrooms",
"olives",
"pepperoni",
"peppers",
"raclette",
},
sauce = {
"aioli",
"algerian",
"andalousian",
"barbecue",
"bechamel",
"burger",
"cheese",
"curry",
"fish",
"harissa",
"mayonnaise",
"moroccan",
"mustard",
"tartare",
"tunisian",
"samurai",
},
sauce_verb = {
"bathed in #sauce# sauce",
"dripping with #sauce# sauce",
"drizzled with a #sauce# sauce",
"in a #sauce# sauce",
"soaked in a #sauce# sauce",
"swimming in #sauce# sauce",
},
origin = {
"Here! A #m_size# #meat# tacos with #topping# #sauce_verb#!",
"Here! A #l_size# #meat# and #meat# tacos with #topping# and #topping# " ..
"#sauce_verb#!",
"Here! A #xl_size# tacos with #meat#, #meat#, #meat#, #topping# " ..
"#sauce_verb#!",
"Here! A #xxl_size# tacos with #meat#, #meat#, #meat#, #topping# and " ..
"#topping# #sauce_verb#!",
"Here! A #m_size# #meat# tacos #sauce_verb#!",
"Here! A #m_size# #meat# tacos with #topping# and #topping#, " ..
"#sauce_verb#!",
"Here! A #l_size# tacos #meat# and #meat# #sauce_verb#!",
"Here! A #l_size# #meat# and #meat# tacos with #topping# #sauce_verb#!",
"Here! A #xl_size# chihuahuacos with #meat#, #meat#, #meat#, " ..
"#topping# and #topping# #sauce_verb#!",
"Here! A #xxl_size# chihuahuacos with #meat#, #meat#, #meat# and " ..
"#meat# #sauce_verb#!",
}
}

View File

@ -0,0 +1,58 @@
-- See also https://en.wikipedia.org/wiki/Tea
-- https://en.wikipedia.org/wiki/List_of_Chinese_teas
grammar = {
tea = {
"Burmese milk tea",
"masala chai",
"Thai tea",
"butter tea",
"black tea",
"fermented tea",
"green tea",
"kombucha",
"oolong tea",
"white tea",
"yellow tea",
},
en_tea = {
"Builder's tea",
"Earl Grey",
"English afternoon tea",
"English breakfast",
"Irish breakfast",
"milk tea",
"orange pekoe",
"Prince of Wales",
},
jp_tea = {
"genmaicha",
"gyokuro",
"hojicha",
"matcha",
"sencha",
},
-- Tea not derived from Camellia sinensis
herbal_tea = {
"barley tea",
"chamomile tea",
"corn tea",
"ginger tea",
"ginseng tea",
"konbu-cha",
"maté",
"peppermint tea",
"rooibos",
"rose tea",
},
cup = {
"bowl",
"cup",
"mug",
},
origin = {
"Here's your cuppa #en_tea#!",
"Here's your cup of #jp_tea#!",
"Here's your #cup# of #tea#!",
"Here's your #cup# of #herbal_tea#!",
}
}

View File

@ -0,0 +1,79 @@
local util = require("itteutil")
-- Default settings
local folia = {
foils_dir = "",
debug = false,
}
util.docs.get_foils = [[ (directory_str)
Given the full path to a directory of foils, return the foil contents as a
table.
]]
function folia.get_foils(str)
local foil_list_str = io.popen("ls " .. str)
local foils = {}
if foil_list_str then
local foil_list = util.split_str(foil_list_str:read("*a"))
for i = 1, #foil_list do
local key = string.gsub(foil_list[i], ".lua", "")
local func, err = loadfile(str .. "/" .. foil_list[i])
if func then
func()
foils[key] = grammar
end
end
-- Unset global variables
func = nil
grammar = nil
end
return foils
end
util.docs.output = [[ (key_str)
Given a foil name or foils table key, output a random expanded string.
]]
function folia.output(key)
local foils = folia.get_foils(folia.foils_dir)
if table.concat(util.table_keys(foils), "") == "" then
util.debug("output", "Error: cannot retrieve foils." ..
" Please make sure the full directory path is correct.", folia.debug)
do return end
elseif not util.has_key(foils, key) then
util.debug("output", "Error: the key `" .. key ..
"` is not in the foils table.", folia.debug)
do return end
elseif not util.has_key(foils[key], "origin") then
util.debug("output", "Error: no origin found in the foil.", folia.debug)
do return end
elseif foils[key]["origin"] == {} then
util.debug("output", "Error: empty origin.", folia.debug)
do return end
end
local output_str = util.pick(foils[key]["origin"])[1]
local pattern = "#(%w+_*%-*%w+)#"
util.debug("output", "level 1: " .. output_str, folia.debug)
-- Check for expansion objects and replace them until none are found
while string.find(output_str, pattern) ~= nil do
local hash = output_str:sub(string.find(output_str, pattern))
local obj = hash:sub(2, #hash - 1)
-- Check the object key is in the foil grammar, and replace one instance
-- at a time.
if util.has_key(foils[key], obj) then
local expand_str = util.pick(foils[key][obj])[1]
output_str = string.gsub(output_str, hash, expand_str, 1)
util.debug("output", "level n: " .. output_str, folia.debug)
-- If the object key is not in the grammar, replace references with a
-- placeholder.
else
output_str = string.gsub(output_str, hash, "[" .. obj .. "]")
end
end
return output_str
end
return folia

View File

@ -0,0 +1,54 @@
local irc = require("itte")
local folia = require("folia")
local soupe = require("soupe")
itte_config = {
debug = false,
messages = {
help = "Don't just stand there like a big f—ing muffin! " ..
"Order something! Your Casa's Kitchen commands are: {{codes}}",
quit = "Master Chef under maintenance.",
reload = "Config reloaded.",
},
foils_dir = "/home/mio/bin/itte/examples/gordon/commands",
nouns_file = "/home/mio/bin/itte/examples/gordon/words/nouns.txt",
}
itte_handlers = {}
folia.foils_dir = itte_config.foils_dir
soupe.nouns_file = itte_config.nouns_file
local gordon = {}
-- Create handlers for each foil
function gordon.generate_handlers(handlers, foils_dir)
local foils = folia.get_foils(foils_dir)
for key, foil in pairs(foils) do
handlers[key] = function (cxt, msg)
irc.message(cxt, { msg.reply_to }, folia.output(key))
end
end
-- Add aliases
itte_handlers.menu = function (cxt, msg)
irc._h.help(cxt, msg)
end
if folia.output("coffee") ~= nil then
itte_handlers.cofe = function (cxt, msg)
irc.message(cxt, { msg.reply_to }, folia.output("coffee"))
end
end
end
-- Add separate handler
function itte_handlers.soupe(cxt, msg)
local resp = "Here! A " .. soupe.make_soup() .. "!"
irc.message(cxt, { msg.reply_to }, resp)
end
gordon.generate_handlers(itte_handlers, folia.foils_dir)

View File

@ -0,0 +1,4 @@
local gordon = require("itte")
gordon.confs.prefix = "gordon"
gordon.run()

View File

@ -0,0 +1,11 @@
[Unit]
Description=gordon — a vending machine IRC bot
[Service]
WorkingDirectory=%h/bin/itte/examples/gordon
ExecStart=/usr/bin/lua ./gordon.lua
Restart=always
RestartSec=300
[Install]
WantedBy=default.target

View File

@ -0,0 +1,171 @@
local util = require("itteutil")
local soupe = {
max_ingred = 7,
nouns_file = "nouns.txt",
}
soupe.servings = {
bowl = {
"bronze",
"copper",
"ceramic",
"crystal",
"earthenware",
"golden",
"stone",
"glass",
"plastic",
"silver",
"styrofoam",
"tin",
"tupperware",
},
cauldron = {
"bronze",
"cast iron",
"charred",
"copper",
"golden",
"rusty",
"silver",
},
chalice = {
"bronze",
"copper",
"crystal",
"golden",
"stone",
"silver",
},
cup = {
"bronze",
"copper",
"ceramic",
"crystal",
"earthenware",
"golden",
"glass",
"plastic",
"silver",
"styrofoam",
"tin",
},
mug = {
"ceramic",
"earthenware",
"glass",
"tin",
},
pot = {
"bronze",
"copper",
"ceramic",
"earthenware",
"stone",
"plastic",
"tin",
},
portion = {
"giant",
"large",
"medium",
"small",
"tiny",
},
serving = {
"large",
"small",
},
}
soupe.temps = {
"chilled",
"cold",
"hot",
"scalding hot",
"warm",
}
soupe.articles = {
"brimming with",
"filled with",
"full of",
"glistening with",
"nearly overflowing with",
"of",
}
-- Read a parts of speech file and turn a table of nouns.
function soupe.filter_nouns(mode)
local ns = util.read_file(soupe.nouns_file)
-- The delimiter does not remove the second section of hyphenated words,
-- this is done when looping through the table and comparing the first letter
-- of each word to the previous and next words' first letters. Since the list
-- is in alphabetical order, out-of-order words are excluded.
local nt = util.split_str(ns, "(%a*)[\\N]%S+")
local nouns = {}
if mode == "common" then
for i = 1, #nt do
if (#nt[i] > 4) and
((string.sub(nt[i - 1], 1, 1) == string.sub(nt[i], 1, 1)) or
(string.sub(nt[i], 1, 1) == string.sub(nt[i + 1], 1, 1))) and
(string.match(string.sub(nt[i], 1, 1), "%l") ~= nil) then
table.insert(nouns, nt[i])
end
end
-- Includes both proper and common nouns
else
for i = 1, #nt do
if (#nt[i] > 4) and
((string.sub(nt[i - 1], 1, 1) == string.sub(nt[i], 1, 1)) or
(string.sub(nt[i], 1, 1) == string.sub(nt[i + 1], 1, 1))) then
table.insert(nouns, nt[i])
end
end
end
return nouns
end
-- Return a string description of soup.
function soupe.make_soup(items)
local ingred = items
if ingred == nil then ingred = math.random(1, soupe.max_ingred) end
local nouns = soupe.filter_nouns("common")
local servings = util.table_keys(soupe.servings)
local serving = servings[math.random(1, #servings)]
local adj = ""
local which_adj = math.random(1, 4)
if which_adj == 2 then
adj = soupe.temps[math.random(1, #soupe.temps)] .. " "
elseif which_adj == 3 then
adj = soupe.servings[serving][math.random(1, #soupe.servings[serving])] ..
" "
elseif which_adj == 4 then
adj = soupe.temps[math.random(1, #soupe.temps)] .. " " ..
soupe.servings[serving][math.random(1, #soupe.servings[serving])] ..
" "
end
local soup = adj .. serving .. " " ..
soupe.articles[math.random(1, #soupe.articles)] .. " "
if ingred == 1 then
soup = soup .. nouns[math.random(1, #nouns)]
elseif ingred == 2 then
soup = soup .. nouns[math.random(1, #nouns)] .. " and " ..
nouns[math.random(1, #nouns)]
elseif ingred > 2 then
for i = 1, (ingred - 2) do
soup = soup .. nouns[math.random(1, #nouns)] .. ", "
end
soup = soup .. nouns[math.random(1, #nouns)] .. " and " ..
nouns[math.random(1, #nouns)]
end
return soup
end
return soupe

View File

@ -0,0 +1,14 @@
# Nouns
The `nouns.txt` file is based on the [Moby Part of Speech List] by Grady Ward,
released into the public domain and available on [Project Gutenburg].
Commands to generate the `nouns.txt` file:
```
curl -LO https://www.gutenberg.org/files/3203/files/mobypos.txt
cat mobypos.txt | grep "\N" > nouns.txt
```
[Moby Part of Speech List]: https://web.archive.org/web/20170930060409/http://icon.shef.ac.uk/Moby/
[Project Gutenburg]: https://www.gutenberg.org/ebooks/3203

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
-- Source: https://en.wikipedia.org/wiki/Magic_8-ball
grammar = {
origin = {
"it is certain",
"it is decidedly so",
"without a doubt",
"yes — definitely",
"you may rely on it",
"as I see it, yes",
"most likely",
"outlook good",
"yes",
"signs point to yes",
"reply hazy try again",
"ask again later",
"better not tell you now",
"cannot predict now",
"concentrate and ask again",
"don't count on it",
"my reply is no",
"my sources say no",
"outlook not so good",
"very doubtful"
}
}

View File

@ -0,0 +1,92 @@
-- Sources:
-- https://en.wikipedia.org/wiki/List_of_breads
-- https://en.wikipedia.org/wiki/List_of_buns
-- https://en.wikipedia.org/wiki/List_of_bread_rolls
grammar = {
origin = {
-- Major Arpana
"0. The Waffle", -- multiple openings, Fool
"I. The Wonut", -- embellished openings, Magician
"II. The Croissant", -- crescent, High Priestess
"III. The Pretzel", -- heart-shaped, Empress
"IV. The King's Day Bread", -- rosca de reyes, Emperor
"V. The Brioche", -- from pain bénit, Hierophant
"VI. The Röggelchen", -- double roll, Lovers
"VII. The Horsebread", -- horses, Chariot
"VIII. The Lion Loaf", -- bread and loaf, Strength
"IX. The Mantou", -- eaten by travellers, Hermit
"X. The Butterkaka", -- rolled buns, Wheel of Fortune
"XI. The Cospia", -- two sections wound together, Justice
"XII. The Colaci", -- braided loop, Hanged Man
"XIII. The Bread of the Dead", -- pan de muerto, Death
"XIV. The Capirotada", -- eaten during Lent, Temperance
"XV. The Hot & Spicy Cheese Bread", -- has chili peppers, Devil
"XVI. The Bun Tower", -- 60' bamboo towers with buns, Tower
"XVII. The Putok", -- star bread, Star
"XVIII. The Kifli", -- crescent-shaped, Moon
"XIX. The Tahini Roll", -- spiral-shaped, Sun
"XX. The Cuerno", -- horn, Judgement
"XXI. The Vetekrans", -- wheat wreath, World
-- Minor Arpana
-- Sliced bread, swords/spades
"Ace of Slice — Anadama",
"Two of Slice — Chickpea",
"Three of Slice — Cozonac",
"Four of Slice — Graham",
"Five of Slice — Pain de Mie",
"Six of Slice — Paximathia",
"Seven of Slice — Pumpkin",
"Eight of Slice — Rye",
"Nine of Slice — Salt-Rising",
"Ten of Slice — Sepik",
"Page of Slice — Spelt",
"Knight of Slice — Sprouted",
"Queen of Slice — Texas Toast",
"King of Slice — Zwieback",
-- Baguette, wands/batons/clubs
"Ace of Baguette — Breadstick",
"Two of Baguette — Breakfast Rolls",
"Three of Baguette — Challah",
"Four of Baguette — Cognou",
"Five of Baguette — Fougasse",
"Six of Baguette — Grissini",
"Seven of Baguette — Hoagie Rolls",
"Eight of Baguette — Marraqueta Tacneña",
"Nine of Baguette — Mohnflesserl",
"Ten of Baguette — Nudgers",
"Page of Baguette — Samoon",
"Knight of Baguette — Spuccadella",
"Queen of Baguette — Shoti",
"King of Baguette — Zopf",
-- Bread bowl, cups/hearts
"Ace of Bread Bowl — Anpan",
"Two of Bread Bowl — Asado Rolls",
"Three of Bread Bowl — Bialy",
"Four of Bread Bowl — Cream Buns",
"Five of Bread Bowl — Cruffins",
"Six of Bread Bowl — Curry Bread",
"Seven of Bread Bowl — Hoppang",
"Eight of Bread Bowl — Lotus Seed Buns",
"Nine of Bread Bowl — Melonpan",
"Ten of Bread Bowl — Nikuman",
"Page of Bread Bowl — Pan de Coco",
"Knight of Bread Bowl — Pistolet",
"Queen of Bread Bowl — Salapao",
"King of Bread Bowl — Sufganiyah",
-- Croutons, pentacles/coins/diamonds
"Ace of Croutons — Bagel",
"Two of Croutons — Bakarkhani",
"Three of Croutons — Biscuits",
"Four of Croutons — Broa",
"Five of Croutons — Crackers",
"Six of Croutons — Dampfnudel",
"Seven of Croutons — Hardtacks",
"Eight of Croutons — Kaiser Rolls",
"Nine of Croutons — Kamir",
"Ten of Croutons — Pandesal",
"Page of Croutons — Roti Bolen",
"Knight of Croutons — Sheermal",
"Queen of Croutons — Shaobing",
"King of Croutons — Teacakes",
}
}

View File

@ -0,0 +1,62 @@
-- Source: based loosely on interpretations by Bev G
-- https://exemplore.com/fortune-divination/Playing-Card-Tarot-Yes-No-Answers
grammar = {
origin = {
-- Spades: thought, communication
"♠ A — yes, knowing the truth",
"♠ 2 — no, truth as yet unknown",
"♠ 3 — no, lacking communication",
"♠ 4 — maybe, needing time to recover",
"♠ 5 — maybe, be direct about intentions",
"♠ 6 — yes, proceed slowly, path of least resistance",
"♠ 7 — yes, if convinced; no, if attempting deception",
"♠ 8 — maybe, wait and try hard",
"♠ 9 — no, getting one's affairs in order first",
"♠ 10 — no, most likely not happening",
"♠ J — yes, focusing on the goal",
"♠ Q — yes, if knowing the truth beforehand",
"♠ K — likely, weighing all options objectively",
-- Clubs: inspiration, action
"♣ A — yes, seize the momentum",
"♣ 2 — no, no more to be done besides reorganising plans",
"♣ 3 — yes, with patience",
"♣ 4 — yes, all is well so far",
"♣ 5 — no, push through the current situation first",
"♣ 6 — yes, definitely",
"♣ 7 — maybe, if others can be convinced",
"♣ 8 — yes, do it now",
"♣ 9 — yes, slowly",
"♣ 10 — no, hang on and wait for the situation to improve",
"♣ J — yes, if safe; no, if risky",
"♣ Q — yes, with any encouragement you can find",
"♣ K — yes, without losing sight of the bigger picture",
-- Diamonds: material, money
"♦ A — yes, all is favourable",
"♦ 2 — yes, if organised and in control",
"♦ 3 — yes, for work-related matters",
"♦ 4 — yes, after reallocating resources",
"♦ 5 — no, risks cannot be taken",
"♦ 6 — yes, if giving; unlikely, if receiving",
"♦ 7 — maybe, if ready to initiate change",
"♦ 8 — yes, check the details first",
"♦ 9 — yes, to be done alone",
"♦ 10 — yes, if having family support or blessings",
"♦ J — yes, with hard work",
"♦ Q — yes, while handling the practical aspects",
"♦ K — yes, with studious determination",
-- Hearts: love, emotions
"♥ A — yes, enjoy the moment",
"♥ 2 — yes, welcome it",
"♥ 3 — yes, have fun",
"♥ 4 — no, not in a good emotional state",
"♥ 5 — no, with a heavy heart",
"♥ 6 — yes, possibly related to the past",
"♥ 7 — maybe, there are multiple potential paths",
"♥ 8 — yes, although slowly or reluctantly",
"♥ 9 — yes, it is fine",
"♥ 10 — yes, a good time to aspire towards fulfillment",
"♥ J — yes, though short-lived",
"♥ Q — yes, be attentive to others' needs",
"♥ K — likely, with restraint"
}
}

View File

@ -0,0 +1,181 @@
grammar = {
cheese = {
"asiago",
"bocconcini",
"catupiry",
"cheddar",
"feta",
"fontina",
"goat cheese",
"gorgonzola",
"Monterey Jack",
"mozzarella",
"Oaxaca cheese",
"paneer",
"parmesan",
"provolone",
"ricotta",
"stracchino"
},
crust = {
"deep-dish",
"extra-thick crust",
"mochi crust",
"Neapolitan",
"Roman",
"square",
"thick crust",
"thin crust"
},
herb = {
"basil",
"cilantro",
"dill",
"oregano",
"rosemary"
},
meat = {
"bacon",
"bulgogi beef",
"rendang beef",
"roast beef",
"satay beef",
"achari chicken",
"balado chicken",
"dak galbi",
"grilled chicken",
"rendang chicken",
"rotisserie chicken",
"tandoori chicken",
"teriyaki chicken",
"chicken tikka masala",
"chorizo",
"crocodile",
"emu",
"ham",
"parma ham",
"döner kebab",
"meatballs",
"Peking duck",
"peperoncini",
"pepperoni",
"prosciutto",
"salami",
"sauage",
"German sausage",
"Maltese sausage",
"tripas"
},
mushroom = {
"cremini mushrooms",
"enoki",
"portobello mushrooms",
"shiitake",
"truffle"
},
seafood = {
"anchovies",
"canned tuna",
"clams",
"crab meat",
"crawfish",
"eel",
"lobster",
"mussels",
"oysters",
"poke",
"prawns",
"salmon",
"salmon teriyaki",
"scallops",
"scampi",
"scungilli",
"shrimp",
"tom yum and shrimp",
"smoked salmon",
"squid",
"tuna"
},
vegetable = {
"artichokes",
"arugula",
"asparagus",
"avocado",
"broccoli",
"capers",
"chilis",
"jalapeños",
"serrano chilis",
"corn",
"cucumbers",
"pickled cucumbers",
"kale",
"kimchi",
"green olives",
"onions",
"red onions",
"green peppers",
"red peppers",
"red bell peppers",
"peas",
"potatoes",
"sweet potatoes",
"tomatoes",
"sun-dried tomatoes",
"spinach",
"zucchini"
},
garnish = {
"ground black pepper",
"minced garlic",
"melted butter",
"curry",
"marinara",
"olive oil",
"extra virgin olive oil",
"balsamic vinegar",
"barbecue sauce",
"béarnaise sauce",
"brown sauce",
"donair sauce",
"honey barbecue sauce",
"sriracha",
"tabasco sauce",
"tomato sauce",
"white sauce"
},
other = {
"hard-boiled eggs",
"labane",
"nachos",
"poutine",
"bananas",
"pineapples"
},
topping = {
"#cheese#",
"#herb#",
"#meat#",
"#mushroom#",
"#other#",
"#seafood#",
"#vegetable#"
},
forecast = {
"#crust#, #topping# and #topping# with a chance of #topping#",
"lots of #topping#, with #topping# all the way to the #crust#, and a greater chance of #topping# and #topping# throughout",
"a meaty mix of #meat#, #meat# and #meat#",
"a toss-up of #vegetable#, #vegetable# and #vegetable#",
"a dynamic system of #cheese# and #cheese# is moving in towards the #crust#, along with more #cheese#",
"some scattered #vegetable#, high on the #mushroom#, and just a touch of #topping# below",
"a pretty #crust# with lots of #mushroom#, #herb# and #garnish#",
"plenty of #topping# and #garnish#, followed by trace amounts of #herb#",
"#topping#, #topping# and a shower of #garnish#",
"#meat# and #vegetable# followed later by #cheese#",
"expect some #garnish# and #herb# over the #topping# and #topping#",
"starts with some #meat#, then moves into #vegetable# and #mushroom# over the #crust#"
},
origin = {
"Pizza forecast: #forecast#"
}
}

View File

@ -0,0 +1,79 @@
local util = require("itteutil")
-- Default settings
local folia = {
foils_dir = "",
debug = false,
}
util.docs.get_foils = [[ (directory_str)
Given the full path to a directory of foils, return the foil contents as a
table.
]]
function folia.get_foils(str)
local foil_list_str = io.popen("ls " .. str)
local foils = {}
if foil_list_str then
local foil_list = util.split_str(foil_list_str:read("*a"))
for i = 1, #foil_list do
local key = string.gsub(foil_list[i], ".lua", "")
local func, err = loadfile(str .. "/" .. foil_list[i])
if func then
func()
foils[key] = grammar
end
end
-- Unset global variables
func = nil
grammar = nil
end
return foils
end
util.docs.output = [[ (key_str)
Given a foil name or foils table key, output a random expanded string.
]]
function folia.output(key)
local foils = folia.get_foils(folia.foils_dir)
if table.concat(util.table_keys(foils), "") == "" then
util.debug("output", "Error: cannot retrieve foils." ..
" Please make sure the full directory path is correct.", folia.debug)
do return end
elseif not util.has_key(foils, key) then
util.debug("output", "Error: the key `" .. key ..
"` is not in the foils table.", folia.debug)
do return end
elseif not util.has_key(foils[key], "origin") then
util.debug("output", "Error: no origin found in the foil.", folia.debug)
do return end
elseif foils[key]["origin"] == {} then
util.debug("output", "Error: empty origin.", folia.debug)
do return end
end
local output_str = util.pick(foils[key]["origin"])[1]
local pattern = "#(%w+_*%-*%w+)#"
util.debug("output", "level 1: " .. output_str, folia.debug)
-- Check for expansion objects and replace them until none are found
while string.find(output_str, pattern) ~= nil do
local hash = output_str:sub(string.find(output_str, pattern))
local obj = hash:sub(2, #hash - 1)
-- Check the object key is in the foil grammar, and replace one instance
-- at a time.
if util.has_key(foils[key], obj) then
local expand_str = util.pick(foils[key][obj])[1]
output_str = string.gsub(output_str, hash, expand_str, 1)
util.debug("output", "level n: " .. output_str, folia.debug)
-- If the object key is not in the grammar, replace references with a
-- placeholder.
else
output_str = string.gsub(output_str, hash, "[" .. obj .. "]")
end
end
return output_str
end
return folia

View File

@ -0,0 +1,42 @@
local irc = require("itte")
local folia = require("folia")
itte_config = {
debug = false,
messages = {
help = "Today's your lucky day, for I, hachi, also known to none as" ..
" the handsomer Hermes, shall bring you messages of ... well," ..
" wouldn't you like to know? For a bit of faux fortune-telling or" ..
" fickle forecasting, ask a question, and try: {{codes}}",
quit = "May fortune smile upon you all!",
reload = "Config reloaded.",
},
}
itte_handlers = {}
folia.foils_dir = "/home/mio/bin/itte/examples/hachi/foils"
local hachi = {}
-- Show help message
function itte_handlers.botland(cxt, msg)
irc._h.help(cxt, msg)
end
-- Create handlers for each foil
function hachi.generate_handlers(handlers, foils_dir)
local foils = folia.get_foils(foils_dir)
for key, foil in pairs(foils) do
handlers[key] = function (cxt, msg)
irc.message(cxt, { msg.reply_to }, folia.output(key))
end
end
end
hachi.generate_handlers(itte_handlers, folia.foils_dir)

View File

@ -0,0 +1,4 @@
local hachi = require("itte")
hachi.confs.prefix = "hachi"
hachi.run()

View File

@ -0,0 +1,11 @@
[Unit]
Description=hachi — a tracery-inspired IRC bot
[Service]
WorkingDirectory=%h/bin/itte/examples/hachi
ExecStart=/usr/bin/lua ./hachi.lua
Restart=always
RestartSec=300
[Install]
WantedBy=default.target

View File

@ -0,0 +1,88 @@
-- ---------------------------------------------------------------------------
-- Custom variables
-- ---------------------------------------------------------------------------
local hello_words = {
"ahoy!", "EHLO", "hai", "hello!", "henlo", "hey!", "hi!", "ACK (TCP)",
"aloha! (Hawaiian)", "ciao! (Italian)", "hallo (Dutch)", "hej (Swedish)",
"hola (Spanish)", "salut! (French)", "saluton (Esperanto)", "tag! (German)",
"toki! (Toki Pona)", "هلا (pron: hala)", "مااس (Arabic, pron: salaam)",
"สวัสดี (Thai, pron: sawatdee)", "你好 (Chinese, pron: ni hao)",
"こんにちわ (Japanese, pron: konnichiwa)", "안녕 (Korean, pron: annyeong)",
"नमस्ते (Hindi, pron: namaste)", "ᐊᐃ (Ai)", "Привет (Russian, pron: preevyet)",
"שָׁלוֹם (Hebrew, pron: shalom)", "kia ora (Māori)"
}
-- ---------------------------------------------------------------------------
-- Config
-- ---------------------------------------------------------------------------
-- [[
-- `itte_config`
-- Add custom settings and override the default config settings here (see
-- `itte.lua` for the full list). Settings need to be appended to this variable
-- to be collected by the reload function.
-- ]]
itte_config = {
debug = true,
}
-- ---------------------------------------------------------------------------
-- Handlers
-- ---------------------------------------------------------------------------
-- Load the main module.
local irc = require("itte")
-- Load the util module to use some helper functions.
local util = require("itteutil")
--[[
-- `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`.
--
-- Here an object called `h` is created for convenience, and functions will be
-- added to it.
--]]
local h = {}
-- Reply with a random greeting.
-- The handler takes two parameters, `cxt` and `msg`.
--
-- The first is a context table that includes server details and a reference to
-- the socket connection. The other is a message table, which is the IRC user
-- message broken down into parts such as the sender, recipient, reply_to, and
-- the code trigger used.
--
-- The context and message tables are required for the `message()` function to
-- know which server and user to direct a response.
--
-- The `message()` function takes the server connection from the context table,
-- a table of users or channels to reply to, and the text string, then formats
-- the string into an IRC command and sends it to the server.
--
-- `util.pick()` is a helper function that takes a table of items and picks one
-- randomly by default if the number of items is unspecified. It will return
-- the items in a table, so `[1]` will get the one (and in this case, only)
-- value in the table.
function h.hello(cxt, msg)
irc.message(cxt, { msg.reply_to }, util.pick(hello_words)[1])
end
-- This is a task handler for a task named "hello" in the sample config, and is
-- an alias to the hello() function above.
function h.th_hello(cxt, msg)
h.hello(cxt, msg)
end
-- Hook up the handlers.
itte_handlers = h

View File

@ -0,0 +1,26 @@
-- Initialise module.
local hellobot = 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.
-- ]]
hellobot.confs.prefix = "hellobot"
-- Call the run function.
hellobot.run()

View File

@ -0,0 +1,53 @@
-- ---------------------------------------------------------------------------
-- Server configuration
-- ---------------------------------------------------------------------------
-- `itte_admins`
-- Users who can access administrative functions in the client, not limited to
-- the server instance. The username is for the client only and independent of
-- IRC network usernames.
--
-- This variable is optional, but certain handlers like server disconnection
-- will not work if it is unset.
itte_admins = { demo = "password", }
-- `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 = { "#channel1", "#channel2" },
cap = nil,
nick = "botnick",
auth_type = nil,
auth_user = "botuser",
auth_pass = nil,
code_prefix = "!",
-- Credentials for people who can access the admin commands.
-- This is specific to the bot and not tied to IRC usernames.
admins = { demouser = "password", },
-- Scheduled tasks.
tasks = {
hello = {
-- Interval at which tasks are performed. Options:
-- Every 5, 10, 15, 20 or 30 minutes — e.g. "5m" for 5 minutes
-- hourly, daily, weekly, monthly
interval = "daily",
-- If the interval is set to hourly, daily, weekly or monthly,
-- specify the time here. If no time is specified, the task will occur
-- at 00:00.
time = "12:00",
-- The name of the handler to run for the task.
handler = "th_hello",
-- The channels or users where the task handler will send messages.
recipients = { "#channel1" },
},
},
},
}

View File

@ -0,0 +1,11 @@
[Unit]
Description=hellobot — an IRC bot
[Service]
WorkingDirectory=/path/to/hellobot
ExecStart=/usr/bin/lua ./hellobot.lua
Restart=always
RestartSec=300
[Install]
WantedBy=default.target

View File

@ -0,0 +1,494 @@
-- ---------------------------------------------------------------------------
-- Ramen data
-- ---------------------------------------------------------------------------
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", "soy sauce and grilled 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",
"End of $3 ramen? Inflation squeezes Japan's comfort food" ..
"https://asia.nikkei.com/Economy/Inflation/End-of-3-ramen-Inflation-squeezes-Japan-s-comfort-food",
-- 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
-- ---------------------------------------------------------------------------
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 = {
debug = 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.announce = "Now serving! "
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
-- ---------------------------------------------------------------------------
local irc = require("itte")
local h = {}
-- Serve ramen.
function h.ramen(cxt, msg)
local recipients = { msg.reply_to }
local intro = ""
-- Scheduled task recipients
if msg.recipients ~= nil then
recipients = msg.recipients
intro = itte_config.messages.announce
end
-- 1% chance of special set
local roll = math.random(1, 100)
if roll == 77 then
irc.message(cxt, recipients, intro ..
string.gsub(itte_config.messages.special, "{{ramen}}",
ramen.make_ramen("veg_special")))
else
irc.message(cxt, recipients, intro ..
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
-- Serve ramen at the interval specified in the config.
function h.th_ramen(cxt, task)
h.ramen(cxt, task)
end
itte_handlers = h

View File

@ -0,0 +1,5 @@
local ramenkan = require("itte")
ramenkan.confs.prefix = "ramenkan"
ramenkan.run()

View File

@ -0,0 +1,50 @@
itte_admins = { USER = "PASSWORD" }
itte_servers = {
town = {
host = "localhost",
port = 6697,
channels = { "#ramenkan", "#bots" },
nick = "ramenkan",
auth_user = "USER",
code_prefix = "!",
admins = { USER = "PASSWORD" },
tasks = {
ramen = {
interval = "daily",
time = "12:00",
handler = "th_ramen",
recipients = { "#ramenkan" },
},
},
},
casa = {
host = "m455.casa",
port = 6697,
channels = { "#kitchen", "#siliconpals" },
cap = { "sasl", "draft/chathistory" },
nick = "ramenkan",
auth_type = "sasl",
auth_user = "USER",
auth_pass = "PASSWORD",
code_prefix = "!",
admins = { USER = "PASSWORD" },
tasks = {
ramen = {
interval = "daily",
time = "17:00",
handler = "th_ramen",
recipients = { "#kitchen" },
},
},
},
tildechat = {
host = "irc.tilde.chat",
port = 6697,
channels = { "#bots" },
nick = "ramenkan",
auth_user = "USER",
code_prefix = "!",
admins = { USER = "PASSWORD" },
},
}

View File

@ -0,0 +1,11 @@
[Unit]
Description=ramenkan — a ramen-themed IRC bot
[Service]
WorkingDirectory=%h/bin/itte/examples/ramenkan
ExecStart=/usr/bin/lua ./ramenkan.lua
Restart=always
RestartSec=300
[Install]
WantedBy=default.target

1160
itte.lua 100644

File diff suppressed because it is too large Load Diff

185
itte.py
View File

@ -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"])

295
itteutil.lua 100644
View File

@ -0,0 +1,295 @@
-- ---------------------------------------------------------------------------
-- 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.find_key = [[ (table, find_str)
Given a table and string, check whether the string is a key in the table.
Return the key if found, or nil otherwise.
]]
function itteutil.find_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 k end
end
end
-- Associative table
else
for k = 1, #keys do
if str == keys[k] then
do return k end
end
end
end
return nil
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
local ts = os.date("%Y-%m-%d %H:%M:%S", os.time())
print("[" .. ts .. "][" .. 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
itteutil.debug("source_file", "Error: " .. err, true)
do return end
end
end
itteutil.docs.read_file = [[ (filename_str)
Open a file and return the contents as a string.
]]
function itteutil.read_file(str)
local file = io.open(str)
if io.type(file) == "file" then
local contents = file:read("*a")
file:close()
return contents
else
itteutil.debug("read_file", "Error: cannot read file.", true)
end
end
return itteutil

View File

@ -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()

View File

@ -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 "!<request>"
req_prefix: "!"
# Print messages to stdout
debug: False
# Mastodon account
# mastodon:
# base_url: "https://example.com"
# access_token: ""
# client_secret: ""
# client_id: ""

View File

@ -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

View File

@ -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

View File

@ -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! ()"
error: "Hyuuuuu! Sorry, I can't seem to toot right now. Ask me again later."

View File

@ -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

View File

@ -0,0 +1,31 @@
package = "itte"
version = "1.0-1"
source = {
url = "git+https://git.tilde.town/mio/itte",
tag = "1.0",
}
description = {
summary = "Mini IRC bot module in lua",
detailed = [[
Currently supports:
- Authentication via SASL (plain) or Nickserv
- Joining multiple servers
- Ad-hoc connecting to servers and joining channels
- Config reload
]],
homepage = "https://git.tilde.town/mio/itte",
license = "BSD-3.0"
}
dependencies = {
"lua >= 5.1, < 5.4",
"luasec >= 0.9",
"luasocket >= 2.9"
}
supported_platforms = { "linux" }
build = {
type = "builtin",
modules = {
itte = "itte.lua",
itteutil = "itteutil.lua"
},
}

View File

@ -0,0 +1,31 @@
package = "itte"
version = "1.1-1"
source = {
url = "git+https://git.tilde.town/mio/itte",
tag = "1.1",
}
description = {
summary = "Mini IRC bot module in lua",
detailed = [[
Currently supports:
- Authentication via SASL (plain) or Nickserv
- Joining multiple servers
- Ad-hoc connecting to servers and joining channels
- Config reload
]],
homepage = "https://git.tilde.town/mio/itte",
license = "BSD-3.0"
}
dependencies = {
"lua >= 5.1, < 5.4",
"luasec >= 0.9",
"luasocket >= 2.9"
}
supported_platforms = { "linux" }
build = {
type = "builtin",
modules = {
itte = "itte.lua",
itteutil = "itteutil.lua"
},
}

View File

@ -0,0 +1,32 @@
package = "itte"
version = "1.2-1"
source = {
url = "git+https://git.tilde.town/mio/itte",
tag = "1.2",
}
description = {
summary = "Mini IRC bot module in lua",
detailed = [[
Currently supports:
- Authentication via SASL (plain) or Nickserv
- Joining multiple servers
- Ad-hoc connecting to servers and joining channels
- Config reload
- Scheduled tasks
]],
homepage = "https://git.tilde.town/mio/itte",
license = "BSD-3.0"
}
dependencies = {
"lua >= 5.1, < 5.4",
"luasec >= 0.9",
"luasocket >= 2.9"
}
supported_platforms = { "linux" }
build = {
type = "builtin",
modules = {
itte = "itte.lua",
itteutil = "itteutil.lua"
},
}

View File

@ -0,0 +1,30 @@
package = "itte"
version = "scm-1"
source = {
url = "git+https://git.tilde.town/mio/itte",
}
description = {
summary = "Mini IRC bot module in lua",
detailed = [[
Currently supports:
- Authentication via SASL (plain) or Nickserv
- Joining multiple servers
- Ad-hoc connecting to servers and joining channels
- Config reload
]],
homepage = "https://git.tilde.town/mio/itte",
license = "BSD-3.0"
}
dependencies = {
"lua >= 5.1, < 5.4",
"luasec >= 0.9",
"luasocket >= 2.9"
}
supported_platforms = { "linux" }
build = {
type = "builtin",
modules = {
itte = "itte.lua",
itteutil = "itteutil.lua"
},
}