From 43a6b53c9053320d2295d0f775cee8d0e42670cc Mon Sep 17 00:00:00 2001 From: topkecleon Date: Sat, 13 Aug 2016 22:26:44 -0400 Subject: [PATCH 1/5] otouto 3.13 good lord --- README.md | 232 +++++++++------ config.lua | 169 ++++++++--- otouto/bindings.lua | 6 +- otouto/bot.lua | 118 +++++--- otouto/plugins/about.lua | 33 +-- otouto/plugins/administration.lua | 121 ++++---- otouto/plugins/apod.lua | 78 ++--- otouto/plugins/bible.lua | 12 +- otouto/plugins/bing.lua | 76 ++--- otouto/plugins/blacklist.lua | 38 +-- otouto/plugins/calc.lua | 25 +- otouto/plugins/catfact.lua | 28 ++ otouto/plugins/chatter.lua | 80 ----- otouto/plugins/chuckfact.lua | 28 ++ otouto/plugins/cleverbot.lua | 36 +++ otouto/plugins/commit.lua | 426 +-------------------------- otouto/plugins/echo.lua | 4 +- otouto/plugins/fortune.lua | 9 +- otouto/plugins/gImages.lua | 25 +- otouto/plugins/gMaps.lua | 23 +- otouto/plugins/gSearch.lua | 79 ----- otouto/plugins/greetings.lua | 61 +--- otouto/plugins/hackernews.lua | 100 ++++--- otouto/plugins/hearthstone.lua | 31 +- otouto/plugins/help.lua | 47 +-- otouto/plugins/id.lua | 62 ++++ otouto/plugins/imdb.lua | 10 +- otouto/plugins/isup.lua | 43 +++ otouto/plugins/lastfm.lua | 8 +- otouto/plugins/luarun.lua | 16 +- otouto/plugins/me.lua | 48 ++- otouto/plugins/patterns.lua | 22 +- otouto/plugins/pokedex.lua | 20 +- otouto/plugins/pokego-calculator.lua | 8 +- otouto/plugins/pokemon-go.lua | 3 +- otouto/plugins/preview.lua | 9 +- otouto/plugins/reactions.lua | 16 +- otouto/plugins/remind.lua | 94 +++--- otouto/plugins/shell.lua | 4 +- otouto/plugins/shout.lua | 16 +- otouto/plugins/slap.lua | 16 +- otouto/plugins/starwars-crawl.lua | 78 +++++ otouto/plugins/time.lua | 43 ++- otouto/plugins/translate.lua | 36 +-- otouto/plugins/urbandictionary.lua | 51 ++-- otouto/plugins/weather.lua | 18 +- otouto/plugins/whoami.lua | 78 ++--- otouto/plugins/wikipedia.lua | 100 +++---- otouto/plugins/xkcd.lua | 68 ++--- otouto/plugins/youtube.lua | 18 +- otouto/utilities.lua | 158 +++------- tg-launch.sh | 4 +- 52 files changed, 1315 insertions(+), 1617 deletions(-) create mode 100644 otouto/plugins/catfact.lua delete mode 100644 otouto/plugins/chatter.lua create mode 100644 otouto/plugins/chuckfact.lua create mode 100644 otouto/plugins/cleverbot.lua delete mode 100644 otouto/plugins/gSearch.lua create mode 100644 otouto/plugins/id.lua create mode 100644 otouto/plugins/isup.lua create mode 100644 otouto/plugins/starwars-crawl.lua diff --git a/README.md b/README.md index 37fa9ed..f21c149 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # otouto The plugin-wielding, multipurpose Telegram bot. -[Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Development Group](http://telegram.me/BotDevelopment) +[Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Bot Development Group](http://telegram.me/BotDevelopment) otouto is a plugin-based, IRC-style bot written for the [Telegram Bot API](http://core.telegram.org/bots/api). Originally written in February of 2015 as a set of Lua scripts to run on [telegram-cli](http://github.com/vysheng/tg), otouto was open-sourced and migrated to the bot API later in June that year. @@ -12,12 +12,10 @@ otouto is free software; you are free to redistribute it and/or modify it under | For Users | For Coders | |:----------------------------------------------|:------------------------------| | [Setup](#setup) | [Plugins](#plugins) | -| [Control plugins](#control-plugins) | [Bindings](#bindings) | -| [Group administration](#group-administration) | [Database](#database) | -| [List of plugins](#list-of-plugins) | [Output style](#output-style) | -| | [Contributors](#contributors) | - -* * * +| [Configuration](#configuration) | [Bindings](#bindings) | +| [Control plugins](#control-plugins) | [Database](#database) | +| [Group administration](#group-administration) | [Output style](#output-style) | +| [List of plugins](#list-of-plugins) | [Contributors](#contributors) | ## Setup You _must_ have Lua (5.2+), luasocket, luasec, multipart-post, and dkjson installed. You should also have lpeg, though it is not required. It is recommended you install these with LuaRocks. @@ -27,27 +25,85 @@ To get started, clone the repository and set the following values in `config.lua - `bot_api_key` as your bot authorization token from the BotFather. - `admin` as your Telegram ID. -Optionally: - - - `lang` as the two-letter code representing your language. - Some plugins are not enabled by default. If you wish to enable them, add them to the `plugins` array. When you are ready to start the bot, run `./launch.sh`. To stop the bot, send "/halt" through Telegram. If you terminate the bot manually, you risk data loss. If you do you not want the bot to restart automatically, run it with `lua main.lua`. -Note that certain plugins, such as translate.lua and greetings.lua, will require privacy mode to be disabled. Additionally, some plugins may require or make use of various API keys: +Note that certain plugins, such as translate.lua and greetings.lua, will require privacy mode to be disabled. Additionally, some plugins may require or make use of various API keys. See [Configuration](#configuration) for details. - - `bing.lua`: [Bing Search API](http://datamarket.azure.com/dataset/bing/search) key (`bing_api_key`) - - `gImages.lua` & `youtube.lua`: Google [API](http://console.developers.google.com) and [CSE](https://cse.google.com/cse) keys (`google_api_key`, `google_cse_key`) - - `weather.lua`: [OpenWeatherMap](http://openweathermap.org) API key (`owm_api_key`) - - `lastfm.lua`: [last.fm](http://last.fm/api) API key (`lastfm_api_key`) - - `bible.lua`: [Biblia](http://api.biblia.com) API key (`biblia_api_key`) - - `cats.lua`: [The Cat API](http://thecatapi.com) API key (optional) (`thecatapi_key`) - - `apod.lua`: [NASA](http://api.nasa.gov) API key (`nasa_api_key`) - - `translate.lua`: [Yandex](http://tech.yandex.com/keys/get) API key (`yandex_key`) - - `chatter.lua`: [SimSimi](http://developer.simsimi.com/signUp) API key (`simsimi_key`) +## Configuration +otouto is configured in the `config.lua` file. It is the single point of configuration for the bot, and contains any necessary user-specific variables, such as API keys, custom error messages, and enabled plugins. -* * * +This section includes an exhaustive list of possible configuration values for otouto and official plugins. + +### Bot configuration values + +| Name | Default | Description | +|:--------------|:--------|:---------------------------------------------------| +| `bot_api_key` | `""` | Telegram bot API token. | +| `admin` | nil | Telegram ID of the bot owner. | +| `log_chat` | nil | Telegram ID of the recipient for error messages. | +| `cmd_pat` | `"/"` | Character (or string) to be used for bot commands. | +| `lang` | `"en"` | Two-letter ISO 639-1 language code. | +| `about_text` | `...` | Informational text to be returned by /about. | + +#### Error messages +These are the generic error messages used by most plugins. These belong in a table named `errors`. + +| Name | Default | +|:-------------|:----------------------------------| +| `generic` | `"An unexpected error occurred."` | +| `connection` | `"Connection error."` | +| `results` | `"No results found."` | +| `argument` | `"Invalid argument."` | +| `syntax` | `"Invalid syntax."` | + +#### Plugins +This table is an array of the names of enabled plugins. To enable a plugin, add its name to the list. + +### Plugin configuration values + +| Name | Description +|:--------------------------|:--------------------------------------------------------------------------------------------| +| `google_api_key` | [Google API](http://console.developers.google.com) key for `gImages.lua` and `youtube.lua`. | +| `google_cse_key` | [Google CSE](http://cse.google.com/cse) key for `gImages.lua`. | +| `lastfm_api_key` | [last.fm API](http://last.fm/api) key for `lastfm.lua`. | +| `owm_api_key` | [OpenWeatherMap API](http://openweathermap.org/API) key for `weather.lua`. | +| `biblia_api_key` | [Biblia API](http://api.biblia.com) key for `bible.lua`. | +| `thecatapi_key` | [The Cat API](http://thecatapi.com) key for `cats.lua` (optional). | +| `nasa_api_key` | [NASA API](http://api.nasa.gov) key for the `apod.lua` (optional). | +| `yandex_key` | [Yandex API](http://tech.yandex.com/keys/get) key for `translate.lua`. | +| `bing_api_key` | [Bing Search API](http://datamarket.azure.com/dataset/bing/search) key for `bing.lua`. | +| `drua_block_on_blacklist` | Whether to block blacklisted users, if tg-cli is in use. | +| `cli_port` | The port to use for tg connections. | +| `hackernews_interval` | The lifespan, in minutes, for each set of results hackernews.lua before refreshing. | +| `hackernews_onstart` | Whether hackernews.lua should fetch articles at load (rather than waiting for demand). | + +Some plugins have many configuration values which warrant their own section of the configuration file. That section will be the name of the plugin, without the file extension. They are listed below. + +#### remind.lua + +| Name | Default | Description | +|:------------------------|:---------|:---------------------------------------------------------| +| `persist` | `true` | Whether reminders should be saved if they fail for send. | +| `max_length` | `1000` | The maximum length for reminders, in bytes. | +| `max_duration` | `526000` | The maximum duration of a reminder, in minutes. | +| `max_reminders_group` | `10` | The maximum number of reminders for a group. | +| `max_reminders_private` | `50` | The maximum number of reminders in private. | + +#### chatter.lua + +| Name | Default | Description | +|:----------------|:-------------------------------------------|:-------------------------------------------------------------------------| +| `cleverbot_api` | `"https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text="` | Cleverbot API endpoint used by `cleverbot.lua`. | +| `connection` | `"I don't feel like talking right now."` | Generic response for connection errors. | +| `response` | `"I don't know what to say to that."` | Generic response for when the API has no response. | + +#### greetings.lua +The `greetings` table is a list of custom responses for the greetings plugin. Each value is an array of triggers, and the key for that array is the response. The default values are inserted by the greetings plugin if there is no user configuration. In the responses, `#NAME` is replaced with the user's name or nickname. The bot's name is automatically appended to all triggers. Triggers are not case sensitive. + +#### reactions.lua +The `reactions` table is also a list of custom responses, for the reactions plugin. Each value is a key/value pair, where the key is the trigger, and the value is the reaction. The reactions plugin differs from the greetings plugin by how it is triggered: A reaction command must be at the beginning or end of a line. ## Control plugins Some plugins are designed to be used by the bot's owner. Here are some examples, how they're used, and what they do. @@ -61,14 +117,12 @@ Some plugins are designed to be used by the bot's owner. Here are some examples, | `shell.lua` | /run | Executes shell commands on the host system. | | `luarun.lua` | /lua | Executes Lua commands in the bot's environment. | -* * * - ## Group Administration The administration plugin enables self-hosted, single-realm group administration, supporting both normal groups and supergroups whch are owned by the bot owner. This works by sending TCP commands to an instance of tg running on the owner's account. To get started, run `./tg-install.sh`. Note that this script is written for Ubuntu/Debian. If you're running Arch (the only acceptable alternative), you'll have to do it yourself. If that is the case, note that otouto uses the "test" branch of tg, and the AUR package `telegram-cli-git` will not be sufficient, as it does not have support for supergroups yet. -Once the installation is finished, enable the `administration` plugin in your config file. **The administration plugin must be loaded before the `about` and `blacklist` plugins.** You may have reason to change the default TCP port (4567); if that is the case, remember to change it in `tg-launch.sh` as well. Run `./tg-launch.sh` in a separate screen/tmux window. You'll have to enter your phone number and go through the login process the first time. The script is set to restart tg after two seconds, so you'll need to Ctrl+C after exiting. +Once the installation is finished, enable the `administration` plugin in your config file. You may have reason to change the default TCP port (4567); if that is the case, remember to change it in `tg-launch.sh` as well. Run `./tg-launch.sh` in a separate screen/tmux window. You'll have to enter your phone number and go through the login process the first time. The script is set to restart tg after two seconds, so you'll need to Ctrl+C after exiting. While tg is running, you may start/reload otouto with `administration.lua` enabled, and have access to a wide variety of administrative commands and automata. The administration "database" is stored in `administration.json`. To start using otouto to administrate a group (note that you must be the owner (or an administrator)), send `/gadd` to that group. For a list of commands, use `/ahelp`. Below I'll describe various functions now available to you. @@ -108,7 +162,7 @@ Internal commands can only be run within an administrated group. ### Description of Privileges -| # | Title | Description | Scope | +| | Title | Description | Scope | |:-:|:--------------|:------------------------------------------------------------------|:-------| | 0 | Banned | Cannot enter the group(s). | Either | | 1 | User | Default rank. | Local | @@ -121,7 +175,7 @@ Obviously, each greater rank inherits the privileges of the lower, positive rank ### Flags -| # | Name | Description | +| | Name | Description | |:-:|:------------|:---------------------------------------------------------------------------------| | 1 | unlisted | Removes a group from the /groups listing. | | 2 | antisquig | Automatically removes users for posting Arabic script or RTL characters. | @@ -149,53 +203,53 @@ antiflood (flag 5) provides a system of automatic flood protection by removing u Additionally, antiflood can be configured to automatically ban a user after he has been automatically kicked from a single group a certain number of times in one day. This is configurable as the antiflood value `autoban` and is set to three by default. -* * * - ## List of plugins -| Plugin | Command | Function | Aliases | -|:----------------------|:------------------------------|:--------------------------------------------------------|:--------| -| `help.lua` | /help [command] | Returns a list of commands or command-specific help. | /h | -| `about.lua` | /about | Returns the about text as configured in config.lua. | -| `ping.lua` | /ping | The simplest plugin ever! | -| `echo.lua` | /echo ‹text› | Repeats a string of text. | -| `bing.lua` | /bing ‹query› | Returns Bing web results. | /g | -| `gImages.lua` | /images ‹query› | Returns a Google image result. | /i | -| `gMaps.lua` | /location ‹query› | Returns location data from Google Maps. | /loc | -| `youtube.lua` | /youtube ‹query› | Returns the top video result from YouTube. | /yt | -| `wikipedia.lua` | /wikipedia ‹query› | Returns the summary of a Wikipedia article. | /w | -| `lastfm.lua` | /np [username] | Returns the song you are currently listening to. | -| `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. | -| `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn | -| `imdb.lua` | /imdb ‹query› | Returns film information from IMDb. | -| `hearthstone.lua` | /hearthstone ‹query› | Returns data for Hearthstone cards matching the query. | /hs | -| `calc.lua` | /calc ‹expression› | Returns conversions and solutions to math expressions. | -| `bible.lua` | /bible ‹reference› | Returns a Bible verse. | /b | -| `urbandictionary.lua` | /urban ‹query› | Returns the top definition from Urban Dictionary. | /ud | -| `time.lua` | /time ‹query› | Returns the time, date, and a timezone for a location. | -| `weather.lua` | /weather ‹query› | Returns current weather conditions for a given location. | -| `nick.lua` | /nick ‹nickname› | Set your nickname. /nick - will delete it. | -| `whoami.lua` | /whoami | Returns user and chat info for you or the replied-to user. | /who | -| `eightball.lua` | /8ball | Returns an answer from a magic 8-ball. | -| `dice.lua` | /roll ‹nDr› | Returns RNG dice rolls. Uses D&D notation. | -| `reddit.lua` | /reddit [r/subreddit ¦ query] | Returns the top results from a subreddit, query, or r/all. | /r | -| `xkcd.lua` | /xkcd [query] | Returns an xkcd strip and its alt text. | -| `slap.lua` | /slap ‹target› | Gives someone a slap (or worse). | -| `commit.lua` | /commit | Returns a commit message from whatthecommit.com. | -| `fortune.lua` | /fortune | Returns a UNIX fortune. | -| `pun.lua` | /pun | Returns a pun. | -| `pokedex.lua` | /pokedex ‹query› | Returns a Pokedex entry. | /dex | -| `currency.lua` | /cash [amount] ‹cur› to ‹cur› | Converts one currency to another. | -| `cats.lua` | /cat | Returns a cat picture. | -| `reactions.lua` | /reactions | Returns a list of emoticons which can be posted by the bot. | -| `apod.lua` | /apod [date] | Returns the NASA Astronomy Picture of the Day. | -| `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. | -| `patterns.lua` | /s/‹from›/‹to›/ | Search-and-replace using Lua patterns. | -| `me.lua` | /me | Returns user-specific data stored by the bot. | -| `remind.lua` | /remind | Reminds a user of something after a duration of minutes. | -| `channel.lua` | /ch \n | Sends a markdown-enabled message to a channel. | - -* * * +| Plugin | Command | Function | Aliases | +|:----------------------|:------------------------------|:----------------------------------------------------------|:--------| +| `help.lua` | /help [command] | Returns a list of commands or command-specific help. | /h | +| `about.lua` | /about | Returns the about text as configured in config.lua. | +| `ping.lua` | /ping | The simplest plugin ever! | +| `echo.lua` | /echo ‹text› | Repeats a string of text. | +| `bing.lua` | /bing ‹query› | Returns Bing web results. | /g | +| `gImages.lua` | /images ‹query› | Returns a Google image result. | /i | +| `gMaps.lua` | /location ‹query› | Returns location data from Google Maps. | /loc | +| `youtube.lua` | /youtube ‹query› | Returns the top video result from YouTube. | /yt | +| `wikipedia.lua` | /wikipedia ‹query› | Returns the summary of a Wikipedia article. | /w | +| `lastfm.lua` | /np [username] | Returns the song you are currently listening to. | +| `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. | +| `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn | +| `imdb.lua` | /imdb ‹query› | Returns film information from IMDb. | +| `hearthstone.lua` | /hearthstone ‹query› | Returns data for Hearthstone cards matching the query. | /hs | +| `calc.lua` | /calc ‹expression› | Returns conversions and solutions to math expressions. | +| `bible.lua` | /bible ‹reference› | Returns a Bible verse. | /b | +| `urbandictionary.lua` | /urban ‹query› | Returns the top definition from Urban Dictionary. | /ud | +| `time.lua` | /time ‹query› | Returns the time, date, and a timezone for a location. | +| `weather.lua` | /weather ‹query› | Returns current weather conditions for a given location. | +| `nick.lua` | /nick ‹nickname› | Set your nickname. /nick - will delete it. | +| `whoami.lua` | /whoami | Returns user and chat info for you or the replied-to user. | /who | +| `eightball.lua` | /8ball | Returns an answer from a magic 8-ball. | +| `dice.lua` | /roll ‹nDr› | Returns RNG dice rolls. Uses D&D notation. | +| `reddit.lua` | /reddit [r/subreddit ¦ query] | Returns the top results from a subreddit, query, or r/all. | /r | +| `xkcd.lua` | /xkcd [query] | Returns an xkcd strip and its alt text. | +| `slap.lua` | /slap ‹target› | Gives someone a slap (or worse). | +| `commit.lua` | /commit | Returns a commit message from whatthecommit.com. | +| `fortune.lua` | /fortune | Returns a UNIX fortune. | +| `pun.lua` | /pun | Returns a pun. | +| `pokedex.lua` | /pokedex ‹query› | Returns a Pokedex entry. | /dex | +| `currency.lua` | /cash [amount] ‹cur› to ‹cur› | Converts one currency to another. | +| `cats.lua` | /cat | Returns a cat picture. | +| `reactions.lua` | /reactions | Returns a list of emoticons which can be posted by the bot. | +| `apod.lua` | /apod [date] | Returns the NASA Astronomy Picture of the Day. | +| `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. | +| `patterns.lua` | /s/‹from›/‹to›/ | Search-and-replace using Lua patterns. | +| `me.lua` | /me | Returns user-specific data stored by the bot. | +| `remind.lua` | /remind | Reminds a user of something after a duration of minutes. | +| `channel.lua` | /ch \n | Sends a markdown-enabled message to a channel. | +| `isup.lua` | /isup | Returns the status of a website. | +| `starwars-crawl.lua` | /sw | Returns the opening crawl from the specified Star Wars film. | /sw | +| `chuckfact.lua` | /chuck | Returns a fact about Chuck Norris. | /cn | +| `catfact.lua` | /catfact | Returns a fact about cats. | ## Plugins otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot). @@ -204,29 +258,31 @@ Most plugins are intended for public use, but a few are for other purposes, like There are five standard plugin components. -| Component | Description | -|:-----------|:-----------------------------------------------------| -| `action` | Main function. Expects `msg` table as an argument. | -| `triggers` | Table of triggers for the plugin. Uses Lua patterns. | -| `init` | Optional function run when the plugin is loaded. | -| `cron` | Optional function to be called every minute. | -| `command` | Basic command and syntax. Listed in the help text. | -| `doc` | Usage for the plugin. Returned by "/help $command". | -| `error` | Plugin-specific error message; false for no message. | +| Component | Description | +|:------------|:---------------------------------------------------------------| +| `action` | Main function. Expects `msg` table as an argument. | +| `triggers` | Table of triggers for the plugin. Uses Lua patterns. | +| `init` | Optional function run when the plugin is loaded. | +| `cron` | Optional function to be called every minute. | +| `command` | Basic command and syntax. Listed in the help text. | +| `doc` | Usage for the plugin. Returned by "/help $command". | +| `error` | Plugin-specific error message; false for no message. | +| `panoptic` | True if plugin should see all messages. (See below.) | +| `help_word` | Keyword for command-specific help. Generated if absent. | No component is required, but some depend on others. For example, `action` will never be run if there's no `triggers`, and `doc` will never be seen if there's no `command`. -Return values from `action` are optional, but they do affect the flow. If it returns a table, that table will become `msg`, and `on_msg_receive` will continue with that. If it returns `true`, it will continue with the current `msg`. +If a plugin's `action` returns `true`, `on_msg_receive` will continue its loop. When an action or cron function fails, the exception is caught and passed to the `handle_exception` utilty and is either printed to the console or send to the chat/channel defined in `log_chat` in config.lua. +The `panoptic` value is a boolean (or nil; its absence means false) to state whether the plugin should be included in the `panoptic_plugins` table. Plugins in this table are the only plugins whose triggers are checked against a message's text if that message is forwarded or from a blacklisted user. + Interactions with the bot API are straightforward. See the [Bindings section](#bindings) for details. Several functions used in multiple plugins are defined in utilities.lua. Refer to that file for usage and documentation. -* * * - ## Bindings Calls to the Telegram bot API are performed with the `bindings.lua` file through the multipart-post library. otouto's bindings file supports all standard API methods and all arguments. Its main function, `bindings.request`, accepts four arguments: `self`, `method`, `parameters`, `file`. (At the very least, `self` should be a table containing `BASE_URL`, which is bot's API endpoint, ending with a slash, eg `https://api.telegram.org/bot123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ987654321/`.) @@ -279,8 +335,6 @@ bindings.sendPhoto(self, { chat_id = 987654321, photo = 'ABCDEFGHIJKLMNOPQRSTUVW Upon success, bindings will return the deserialized result from the API. Upon failure, it will return false and the result. In the case of a connection error, it will return two false values. If an invalid method name is given, bindings will throw an exception. This is to mimic the behavior of more conventional bindings as well as to prevent "silent errors". -* * * - ## Database otouto doesn't use one. This isn't because of dedication to lightweightedness or some clever design choice. Interfacing with databases through Lua is never a simple, easy-to-learn process. As one of the goals of otouto is that it should be a bot which is easy to write plugins for, our approach to storing data is to treat our datastore like any ordinary Lua data structure. The "database" is a table accessible in the `database` value of the bot instance (usually `self.database`), and is saved as a JSON-encoded plaintext file each hour, or when the bot is told to halt. This way, keeping and interacting with persistent data is no different than interacting with a Lua table -- with one exception: Keys in tables used as associative arrays must not be numbers. If the index keys are too sparse, the JSON encoder/decoder will either change them to keys or throw an error. @@ -313,8 +367,6 @@ Alone, the database will have this structure: Data from other plugins is usually saved in a table with the same name of that plugin. For example, administration.lua stores data in `database.administration`. -* * * - ## Output style otouto plugins should maintain a consistent visual style in their output. This provides a recognizable and comfortable user experience. @@ -323,7 +375,7 @@ Title lines should be **bold**, including any names and trailing punctuation (su > **Star Wars: Episode IV - A New Hope (1977)** > -> **Search results for** _star wars_ **:** +> **Search results for** _star wars_**:** > > **Changelog for otouto (**[Github](http://github.com/topkecleon/otouto)**):** @@ -346,8 +398,6 @@ Always name your links. Even then, use them with discretion. Excessive links mak ### Other Stuff User IDs should appear within brackets, monospaced (`[123456789]`). Descriptions and information should be in plain text, but "flavor" text should be italic. The standard size for arbitrary lists (such as search results) is eight within a private conversation and four elsewhere. This is a trivial pair of numbers (leftover from the deprecated Google search API), but consistency is noticeable and desirable. -* * * - ## Contributors Everybody is free to contribute to otouto. If you are interested, you are invited to [fork the repo](http://github.com/topkecleon/otouto/fork) and start making pull requests. If you have an idea and you are not sure how to implement it, open an issue or bring it up in the [Bot Development group](http://telegram.me/BotDevelopment). diff --git a/config.lua b/config.lua index 500e419..f18b5c6 100644 --- a/config.lua +++ b/config.lua @@ -1,9 +1,10 @@ + -- For details on configuration values, see README.md#configuration. return { -- Your authorization token from the botfather. - bot_api_key = '', + bot_api_key = nil, -- Your Telegram ID. - admin = 00000000, + admin = nil, -- Two-letter language code. lang = 'en', -- The channel, group, or user to send error reports to. @@ -12,74 +13,144 @@ return { -- The port used to communicate with tg for administration.lua. -- If you change this, make sure you also modify launch-tg.sh. cli_port = 4567, - -- The block of text returned by /start. + -- The symbol that starts a command. Usually noted as '/' in documentation. + cmd_pat = '/', + -- If drua is used, should a user be blocked when he's blacklisted? + drua_block_on_blacklist = false, + -- The filename of the database. If left nil, defaults to $username.db. + database_name = nil, + -- The block of text returned by /start and /about.. about_text = [[ I am otouto, the plugin-wielding, multipurpose Telegram bot. Send /help to get started. ]], - -- The symbol that starts a command. Usually noted as '/' in documentation. - cmd_pat = '/', - -- If drua is used, should a user be blocked when he's blacklisted? (and vice-versa) - drua_block_on_blacklist = false, - - -- https://datamarket.azure.com/dataset/bing/search - bing_api_key = '', - -- http://console.developers.google.com - google_api_key = '', - -- https://cse.google.com/cse - google_cse_key = '', - -- http://openweathermap.org/appid - owm_api_key = '', - -- http://last.fm/api - lastfm_api_key = '', - -- http://api.biblia.com - biblia_api_key = '', - -- http://thecatapi.com/docs.html - thecatapi_key = '', - -- http://api.nasa.gov - nasa_api_key = '', - -- http://tech.yandex.com/keys/get - yandex_key = '', - -- http://developer.simsimi.com/signUp - simsimi_key = '', - simsimi_trial = true, errors = { -- Generic error messages. generic = 'An unexpected error occurred.', connection = 'Connection error.', results = 'No results found.', argument = 'Invalid argument.', - syntax = 'Invalid syntax.', - chatter_connection = 'I don\'t feel like talking right now.', - chatter_response = 'I don\'t know what to say to that.' + syntax = 'Invalid syntax.' + }, + + -- https://datamarket.azure.com/dataset/bing/search + bing_api_key = nil, + -- http://console.developers.google.com + google_api_key = nil, + -- https://cse.google.com/cse + google_cse_key = nil, + -- http://openweathermap.org/appid + owm_api_key = nil, + -- http://last.fm/api + lastfm_api_key = nil, + -- http://api.biblia.com + biblia_api_key = nil, + -- http://thecatapi.com/docs.html + thecatapi_key = nil, + -- http://api.nasa.gov + nasa_api_key = nil, + -- http://tech.yandex.com/keys/get + yandex_key = nil, + -- Interval (in minutes) for hackernews.lua to update. + hackernews_interval = 60, + -- Whether hackernews.lua should update at load/reload. + hackernews_onstart = false, + -- Whether luarun should use serpent instead of dkjson for serialization. + luarun_serpent = false, + + remind = { + persist = true, + max_length = 1000, + max_duration = 526000, + max_reminders_group = 10, + max_reminders_private = 50 + }, + + chatter = { + cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=', + connection = 'I don\'t feel like talking right now.', + response = 'I don\'t know what to say to that.' + }, + + greetings = { + ["Hello, #NAME."] = { + "hello", + "hey", + "hi", + "good morning", + "good day", + "good afternoon", + "good evening" + }, + ["Goodbye, #NAME."] = { + "good%-?bye", + "bye", + "later", + "see ya", + "good night" + }, + ["Welcome back, #NAME."] = { + "i'm home", + "i'm back" + }, + ["You're welcome, #NAME."] = { + "thanks", + "thank you" + } + }, + + reactions = { + ['shrug'] = '¯\\_(ツ)_/¯', + ['lenny'] = '( ͡° ͜ʖ ͡°)', + ['flip'] = '(╯°□°)╯︵ ┻━┻', + ['look'] = 'ಠ_ಠ', + ['shots'] = 'SHOTS FIRED', + ['facepalm'] = '(-‸ლ)' + }, + + administration = { + -- Whether moderators can set a group's message of the day. + moderator_setmotd = false, + -- Default antiflood values. + antiflood = { + text = 5, + voice = 5, + audio = 5, + contact = 5, + photo = 10, + video = 10, + location = 10, + document = 10, + sticker = 20 + } }, plugins = { -- To enable a plugin, add its name to the list. - 'control', - 'blacklist', 'about', - 'ping', - 'whoami', - 'nick', + 'blacklist', + 'calc', + 'cats', + 'commit', + 'control', + 'currency', + 'dice', 'echo', + 'eightball', 'gMaps', - 'wikipedia', 'hackernews', 'imdb', - 'calc', - 'urbandictionary', - 'time', - 'eightball', - 'dice', - 'reddit', - 'xkcd', - 'slap', - 'commit', + 'nick', + 'ping', 'pun', - 'currency', - 'cats', + 'reddit', 'shout', + 'slap', + 'time', + 'urbandictionary', + 'whoami', + 'wikipedia', + 'xkcd', -- Put new plugins above this line. 'help', 'greetings' diff --git a/otouto/bindings.lua b/otouto/bindings.lua index 5d57f7e..c85c43d 100644 --- a/otouto/bindings.lua +++ b/otouto/bindings.lua @@ -41,7 +41,7 @@ function bindings:request(method, parameters, file) end local response = {} local body, boundary = MP_ENCODE(parameters) - local success = HTTPS.request{ + local success, code = HTTPS.request{ url = self.BASE_URL .. method, method = 'POST', headers = { @@ -52,8 +52,8 @@ function bindings:request(method, parameters, file) sink = ltn12.sink.table(response) } local data = table.concat(response) - if not success then - print(method .. ': Connection error.') + if not success or success == 1 then + print(method .. ': Connection error. [' .. code .. ']') return false, false else local result = JSON.decode(data) diff --git a/otouto/bot.lua b/otouto/bot.lua index 63660b7..5ea5241 100644 --- a/otouto/bot.lua +++ b/otouto/bot.lua @@ -1,18 +1,17 @@ local bot = {} +local bindings -- Bot API bindings. +local utilities -- Miscellaneous and shared plugins. --- Requires are moved to init to allow for reloads. -local bindings -- Load Telegram bindings. -local utilities -- Load miscellaneous and cross-plugin functions. +bot.version = '3.13' -bot.version = '3.12' - -function bot:init(config) -- The function run when the bot is started or reloaded. + -- Function to be run on start and reload. +function bot:init(config) bindings = require('otouto.bindings') utilities = require('otouto.utilities') assert( - config.bot_api_key ~= '', + config.bot_api_key, 'You did not set your bot token in the config!' ) self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' @@ -25,45 +24,76 @@ function bot:init(config) -- The function run when the bot is started or reloade self.info = self.info.result -- Load the "database"! ;) + self.database_name = config.database_name or self.info.username .. '.db' if not self.database then - self.database = utilities.load_data(self.info.username..'.db') + self.database = utilities.load_data(self.database_name) end + -- Migration code 1.12 -> 1.13 + -- Back to administration global ban list; copy over current blacklist. + if self.database.version ~= '3.13' then + if self.database.administration then + self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {} + utilities.save_data(self.database_name, self.database) + self.database = utilities.load_data(self.database_name) + end + end + -- End migration code. + -- Table to cache user info (usernames, IDs, etc). self.database.users = self.database.users or {} -- Table to store userdata (nicknames, lastfm usernames, etc). self.database.userdata = self.database.userdata or {} + -- Table to store the IDs of blacklisted users. + self.database.blacklist = self.database.blacklist or {} -- Save the bot's version in the database to make migration simpler. self.database.version = bot.version -- Add updated bot info to the user info cache. self.database.users[tostring(self.info.id)] = self.info - self.plugins = {} -- Load plugins. - for _,v in ipairs(config.plugins) do - local p = require('otouto.plugins.'..v) - table.insert(self.plugins, p) - if p.init then p.init(self, config) end - if p.doc then p.doc = '```\n'..p.doc..'\n```' end + -- All plugins go into self.plugins. Plugins which accept forwarded messages + -- and messages from blacklisted users also go into self.panoptic_plugins. + self.plugins = {} + self.panoptic_plugins = {} + local t = {} -- Petty pseudo-optimization. + for _, pname in ipairs(config.plugins) do + local plugin = require('otouto.plugins.'..pname) + table.insert(self.plugins, plugin) + if plugin.init then plugin.init(self, config) end + if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end + if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end + if not plugin.triggers then plugin.triggers = t end end print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')') - self.last_update = self.last_update or 0 -- Set loop variables: Update offset, - self.last_cron = self.last_cron or os.date('%M') -- the time of the last cron job, - self.last_database_save = self.last_database_save or os.date('%H') -- the time of the last database save, - self.is_started = true -- and whether or not the bot should be running. + -- Set loop variables. + self.last_update = self.last_update or 0 -- Update offset. + self.last_cron = self.last_cron or os.date('%M') -- Last cron job. + self.last_database_save = self.last_database_save or os.date('%H') -- Last db save. + self.is_started = true end -function bot:on_msg_receive(msg, config) -- The fn run whenever a message is received. + -- Function to be run on each new message. +function bot:on_msg_receive(msg, config) - if msg.date < os.time() - 5 then return end -- Do not process old messages. + -- Do not process old messages. + if msg.date < os.time() - 5 then return end + + -- plugint is the array of plugins we'll check the message against. + -- If the message is forwarded or from a blacklisted user, the bot will only + -- check against panoptic plugins. + local plugint = self.plugins + local from_id_str = tostring(msg.from.id) -- Cache user info for those involved. - self.database.users[tostring(msg.from.id)] = msg.from + self.database.users[from_id_str] = msg.from if msg.reply_to_message then self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from elseif msg.forward_from then + -- Forwards only go to panoptic plugins. + plugint = self.panoptic_plugins self.database.users[tostring(msg.forward_from.id)] = msg.forward_from elseif msg.new_chat_member then self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member @@ -71,9 +101,14 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member end + -- Messages from blacklisted users only go to panoptic plugins. + if self.database.blacklist[from_id_str] then + plugint = self.panoptic_plugins + end + + -- If no text, use captions. msg.text = msg.text or msg.caption or '' msg.text_lower = msg.text:lower() - if msg.reply_to_message then msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or '' end @@ -84,8 +119,11 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec msg.text_lower = msg.text:lower() end - for _, plugin in ipairs(self.plugins) do - for _, trigger in ipairs(plugin.triggers or {}) do + -- If the message is forwarded or comes from a blacklisted yser, + + -- Do the thing. + for _, plugin in ipairs(plugint) do + for _, trigger in ipairs(plugin.triggers) do if string.match(msg.text_lower, trigger) then local success, result = pcall(function() return plugin.action(self, msg, config) @@ -100,29 +138,29 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec utilities.send_reply(self, msg, config.errors.generic) end utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) + msg = nil return - end - -- If the action returns a table, make that table the new msg. - if type(result) == 'table' then - msg = result - -- If the action returns true, continue. + -- Continue if the return value is true. elseif result ~= true then + msg = nil return end end end end + msg = nil end + -- main function bot:run(config) - bot.init(self, config) -- Actually start the script. - - while self.is_started do -- Start a loop while the bot should be running. - - local res = bindings.getUpdates(self, { timeout=20, offset = self.last_update+1 } ) + bot.init(self, config) + while self.is_started do + -- Update loop. + local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } ) if res then - for _,v in ipairs(res.result) do -- Go through every new message. + -- Iterate over every new message. + for _,v in ipairs(res.result) do self.last_update = v.update_id if v.message then bot.on_msg_receive(self, v.message, config) @@ -132,7 +170,8 @@ function bot:run(config) print('Connection error while fetching updates.') end - if self.last_cron ~= os.date('%M') then -- Run cron jobs every minute. + -- Run cron jobs every minute. + if self.last_cron ~= os.date('%M') then self.last_cron = os.date('%M') for i,v in ipairs(self.plugins) do if v.cron then -- Call each plugin's cron function, if it has one. @@ -144,15 +183,14 @@ function bot:run(config) end end + -- Save the "database" every hour. if self.last_database_save ~= os.date('%H') then - utilities.save_data(self.info.username..'.db', self.database) -- Save the database. self.last_database_save = os.date('%H') + utilities.save_data(self.database_name, self.database) end - end - -- Save the database before exiting. - utilities.save_data(self.info.username..'.db', self.database) + utilities.save_data(self.database_name, self.database) print('Halted.') end diff --git a/otouto/plugins/about.lua b/otouto/plugins/about.lua index 3be9a53..6088ace 100644 --- a/otouto/plugins/about.lua +++ b/otouto/plugins/about.lua @@ -1,36 +1,19 @@ -local about = {} - local bot = require('otouto.bot') local utilities = require('otouto.utilities') +local about = {} + about.command = 'about' about.doc = 'Returns information about the bot.' -about.triggers = { - '' -} +function about:init(config) + about.text = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.' + about.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('about'):t('start').table +end function about:action(msg, config) - - -- Filthy hack, but here is where we'll stop forwarded messages from hitting - -- other plugins. - if msg.forward_from then return end - - local output = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.' - - if - (msg.new_chat_member and msg.new_chat_member.id == self.info.id) - or msg.text_lower:match('^'..config.cmd_pat..'about$') - or msg.text_lower:match('^'..config.cmd_pat..'about@'..self.info.username:lower()..'$') - or msg.text_lower:match('^'..config.cmd_pat..'start$') - or msg.text_lower:match('^'..config.cmd_pat..'start@'..self.info.username:lower()..'$') - then - utilities.send_message(self, msg.chat.id, output, true, nil, true) - return - end - - return true - + utilities.send_message(self, msg.chat.id, about.text, true, nil, true) end return about diff --git a/otouto/plugins/administration.lua b/otouto/plugins/administration.lua index faa5c6c..092190c 100644 --- a/otouto/plugins/administration.lua +++ b/otouto/plugins/administration.lua @@ -9,8 +9,6 @@ It requires tg (http://github.com/vysheng/tg) with supergroup support. For more documentation, read the the manual (otou.to/rtfm). - Remember to load this before blacklist.lua. - Important notices about updates will be here! 1.11 - Removed /kickme and /broadcast. Users should leave manually, and @@ -20,6 +18,8 @@ necessary. 1.11.1 - Bugfixes. /hammer can now be used in PM. + + 1.13 - Global banlist reinstated. Added default antiflood values to config. Bugfixes: Modding a user will no longer add him. Fixed kicks/bans in reply to join/leave notifications. ]] local JSON = require('dkjson') @@ -36,11 +36,12 @@ function administration:init(config) admins = {}, groups = {}, activity = {}, - autokick_timer = os.date('%d') + autokick_timer = os.date('%d'), + globalbans = {} } end - self.admin_temp = { + administration.temp = { help = {}, flood = {} } @@ -49,13 +50,25 @@ function administration:init(config) administration.flags = administration.init_flags(config.cmd_pat) administration.init_command(self, config) + administration.antiflood = config.administration.antiflood or { + text = 5, + voice = 5, + audio = 5, + contact = 5, + photo = 10, + video = 10, + location = 10, + document = 10, + sticker = 20 + } administration.doc = 'Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.' administration.command = 'groups [query]' -- In the worst case, don't send errors in reply to random messages. administration.error = false - + -- Accept forwarded messages and messages from blacklisted users. + administration.panoptic = true end function administration.init_flags(cmd_pat) return { @@ -106,18 +119,6 @@ function administration.init_flags(cmd_pat) return { } } end -administration.antiflood = { - text = 5, - voice = 5, - audio = 5, - contact = 5, - photo = 10, - video = 10, - location = 10, - document = 10, - sticker = 20 -} - administration.ranks = { [0] = 'Banned', [1] = 'Users', @@ -159,8 +160,8 @@ function administration:get_rank(user_id_str, chat_id_str, config) end end - -- Return 0 if the user_id_str is blacklisted (and antihammer is not enabled). - if self.database.blacklist[user_id_str] then + -- Return 0 if the user_id_str is globally banned (and antihammer is not enabled). + if self.database.administration.globalbans[user_id_str] then return 0 end @@ -172,8 +173,9 @@ end -- Returns an array of "user" tables. function administration:get_targets(msg, config) if msg.reply_to_message then + local d = msg.reply_to_message.new_chat_member or msg.reply_to_message.left_chat_member or msg.reply_to_message.from local target = {} - for k,v in pairs(msg.reply_to_message.from) do + for k,v in pairs(d) do target[k] = v end target.name = utilities.build_name(target.first_name, target.last_name) @@ -184,7 +186,7 @@ function administration:get_targets(msg, config) local input = utilities.input(msg.text) if input then local t = {} - for _, user in ipairs(utilities.index(input)) do + for user in input:gmatch('%g+') do if self.database.users[user] then local target = {} for k,v in pairs(self.database.users[user]) do @@ -195,10 +197,11 @@ function administration:get_targets(msg, config) target.rank = administration.get_rank(self, target.id, msg.chat.id, config) table.insert(t, target) elseif tonumber(user) then + local id = math.abs(tonumber(user)) local target = { - id = tonumber(user), - id_str = user, - name = 'Unknown ('..user..')', + id = id, + id_str = tostring(id), + name = 'Unknown ('..id..')', rank = administration.get_rank(self, user, msg.chat.id, config) } table.insert(t, target) @@ -227,7 +230,7 @@ function administration:mod_format(id) id = tostring(id) local user = self.database.users[id] or { first_name = 'Unknown' } local name = utilities.build_name(user.first_name, user.last_name) - name = utilities.markdown_escape(name) + name = utilities.md_escape(name) local output = '• ' .. name .. ' `[' .. id .. ']`\n' return output end @@ -356,36 +359,36 @@ function administration.init_command(self_, config_) if not group.antiflood then group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) end - if not self.admin_temp.flood[chat_id_str] then - self.admin_temp.flood[chat_id_str] = {} + if not administration.temp.flood[chat_id_str] then + administration.temp.flood[chat_id_str] = {} end - if not self.admin_temp.flood[chat_id_str][from_id_str] then - self.admin_temp.flood[chat_id_str][from_id_str] = 0 + if not administration.temp.flood[chat_id_str][from_id_str] then + administration.temp.flood[chat_id_str][from_id_str] = 0 end if msg.sticker then -- Thanks Brazil for discarding switches. - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.sticker + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.sticker elseif msg.photo then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.photo + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.photo elseif msg.document then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.document + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.document elseif msg.audio then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.audio + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.audio elseif msg.contact then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.contact + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.contact elseif msg.video then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.video + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.video elseif msg.location then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.location + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.location elseif msg.voice then - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.voice + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.voice else - self.admin_temp.flood[chat_id_str][from_id_str] = self.admin_temp.flood[chat_id_str][from_id_str] + group.antiflood.text + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.text end - if self.admin_temp.flood[chat_id_str][from_id_str] > 99 then + if administration.temp.flood[chat_id_str][from_id_str] > 99 then user.do_kick = true user.reason = 'antiflood' user.output = administration.flags[5].kicked:gsub('GROUPNAME', msg.chat.title) - self.admin_temp.flood[chat_id_str][from_id_str] = nil + administration.temp.flood[chat_id_str][from_id_str] = nil end end @@ -586,7 +589,7 @@ function administration.init_command(self_, config_) else local output = '*Commands for ' .. administration.ranks[rank] .. ':*\n' for i = 1, rank do - for _, val in ipairs(self.admin_temp.help[i]) do + for _, val in ipairs(administration.temp.help[i]) do output = output .. '• ' .. config.cmd_pat .. val .. '\n' end end @@ -685,7 +688,7 @@ function administration.init_command(self_, config_) }, { -- /motd - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('motd').table, + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('motd'):t('qotd').table, command = 'motd', privilege = 1, @@ -821,7 +824,7 @@ function administration.init_command(self_, config_) triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setmotd', true):t('setqotd', true).table, command = 'setmotd <motd>', - privilege = 2, + privilege = config_.administration.moderator_setmotd and 2 or 3, interior = true, doc = 'Sets the group\'s message of the day. Markdown is supported. Pass "--" to delete the message.', @@ -977,8 +980,7 @@ function administration.init_command(self_, config_) local output = '' local input = utilities.input(msg.text) if input then - local index = utilities.index(input) - for _, i in ipairs(index) do + for i in input:gmatch('%g+') do local n = tonumber(i) if n and administration.flags[n] then if group.flags[n] == true then @@ -1069,7 +1071,10 @@ function administration.init_command(self_, config_) group.bans[target.id_str] = nil end if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 2) + local chat_member = bindings.getChatMember(self, { chat_id = msg.chat.id, user_id = target.id }) + if chat_member and chat_member.result.status == 'member' then + drua.channel_set_admin(msg.chat.id, target.id, 2) + end end end end @@ -1138,7 +1143,10 @@ function administration.init_command(self_, config_) utilities.send_reply(self, msg, target.name .. ' is the new governor.') end if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 2) + local chat_member = bindings.getChatMember(self, { chat_id = msg.chat.id, user_id = target.id }) + if chat_member and chat_member.result.status == 'member' then + drua.channel_set_admin(msg.chat.id, target.id, 2) + end administration.update_desc(self, msg.chat.id, config) end end @@ -1195,7 +1203,7 @@ function administration.init_command(self_, config_) for _, target in ipairs(targets) do if target.err then output = output .. target.err .. '\n' - elseif self.database.blacklist[target.id_str] then + elseif self.database.administration.globalbans[target.id_str] then output = output .. target.name .. ' is already globally banned.\n' elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then output = output .. target.name .. ' is too privileged to be globally banned.\n' @@ -1211,7 +1219,7 @@ function administration.init_command(self_, config_) end end end - self.database.blacklist[target.id_str] = true + self.database.administration.globalbans[target.id_str] = true if group and group.flags[6] == true then group.mods[target.id_str] = nil group.bans[target.id_str] = true @@ -1243,10 +1251,10 @@ function administration.init_command(self_, config_) for _, target in ipairs(targets) do if target.err then output = output .. target.err .. '\n' - elseif not self.database.blacklist[target.id_str] then + elseif not self.database.administration.globalbans[target.id_str] then output = output .. target.name .. ' is not globally banned.\n' else - self.database.blacklist[target.id_str] = nil + self.database.administration.globalbans[target.id_str] = nil output = output .. target.name .. ' has been globally unbanned.\n' end end @@ -1333,7 +1341,7 @@ function administration.init_command(self_, config_) action = function(self, msg, group, config) if msg.chat.id == msg.from.id then - utilities.send_message(self, msg.chat.id, 'No.') + utilities.send_message(self, msg.chat.id, 'This is not a group.') elseif group then utilities.send_reply(self, msg, 'I am already administrating this group.') else @@ -1344,8 +1352,7 @@ function administration.init_command(self_, config_) end local input = utilities.input(msg.text) if input then - local index = utilities.index(input) - for _, i in ipairs(index) do + for i in input:gmatch('%g+') do local n = tonumber(i) if n and administration.flags[n] and flags[n] ~= true then flags[n] = true @@ -1442,11 +1449,11 @@ function administration.init_command(self_, config_) -- Generate help messages and ahelp keywords. self_.database.administration.help = {} for i,_ in ipairs(administration.ranks) do - self_.admin_temp.help[i] = {} + administration.temp.help[i] = {} end for _,v in ipairs(administration.commands) do if v.command then - table.insert(self_.admin_temp.help[v.privilege], v.command) + table.insert(administration.temp.help[v.privilege], v.command) if v.doc then v.keyword = utilities.get_word(v.command, 1) end @@ -1475,7 +1482,7 @@ function administration:action(msg, config) end function administration:cron() - self.admin_temp.flood = {} + administration.temp.flood = {} if os.date('%d') ~= self.database.administration.autokick_timer then self.database.administration.autokick_timer = os.date('%d') for _,v in pairs(self.database.administration.groups) do diff --git a/otouto/plugins/apod.lua b/otouto/plugins/apod.lua index 886feda..7350ffc 100644 --- a/otouto/plugins/apod.lua +++ b/otouto/plugins/apod.lua @@ -10,79 +10,47 @@ local utilities = require('otouto.utilities') apod.command = 'apod [date]' function apod:init(config) - apod.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('apod', true):t('apodhd', true):t('apodtext', true).table - apod.doc = config.cmd_pat .. [[apod [query] + apod.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('apod', true).table + apod.doc = [[ +/apod [YYYY-MM-DD] Returns the Astronomy Picture of the Day. -If the query is a date, in the format YYYY-MM-DD, the APOD of that day is returned. -Examples: -]] .. config.cmd_pat .. [[apodhd [query] -Returns the image in HD, if available. - -]] .. config.cmd_pat .. [[apodtext [query] -Returns the explanation of the APOD. - -Source: nasa.gov]] +Source: nasa.gov + ]] + apod.doc = apod.doc:gsub('/', config.cmd_pat) + apod.base_url = 'https://api.nasa.gov/planetary/apod?api_key=' .. (config.nasa_api_key or 'DEMO_KEY') end function apod:action(msg, config) - - if not config.nasa_api_key then - config.nasa_api_key = 'DEMO_KEY' - end - local input = utilities.input(msg.text) - local date = '*' - local disable_page_preview = false - - local url = 'https://api.nasa.gov/planetary/apod?api_key=' .. config.nasa_api_key - + local url = apod.base_url + local date = os.date('%F') if input then - if input:match('(%d+)%-(%d+)%-(%d+)$') then + if input:match('^(%d+)%-(%d+)%-(%d+)$') then url = url .. '&date=' .. URL.escape(input) - date = date .. input - else - utilities.send_message(self, msg.chat.id, apod.doc, true, msg.message_id, true) - return + date = input end - else - date = date .. os.date("%F") end - date = date .. '*\n' - - local jstr, res = HTTPS.request(url) - if res ~= 200 then + local jstr, code = HTTPS.request(url) + if code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - local jdat = JSON.decode(jstr) - - if jdat.error then + local data = JSON.decode(jstr) + if data.error then utilities.send_reply(self, msg, config.errors.results) return end - local img_url = jdat.url - - if string.match(msg.text, '^'..config.cmd_pat..'apodhd*') then - img_url = jdat.hdurl or jdat.url - end - - local output = date .. '[' .. jdat.title .. '](' .. img_url .. ')' - - if string.match(msg.text, '^'..config.cmd_pat..'apodtext*') then - output = output .. '\n' .. jdat.explanation - disable_page_preview = true - end - - if jdat.copyright then - output = output .. '\nCopyright: ' .. jdat.copyright - end - - utilities.send_message(self, msg.chat.id, output, disable_page_preview, nil, true) - + local output = string.format( + '<b>%s (</b><a href="%s">%s</a><b>)</b>\n%s', + utilities.html_escape(data.title), + utilities.html_escape(data.hdurl or data.url), + date, + utilities.html_escape(data.explanation) + ) + utilities.send_message(self, msg.chat.id, output, false, nil, 'html') end return apod diff --git a/otouto/plugins/bible.lua b/otouto/plugins/bible.lua index 69c88ef..2d85b43 100644 --- a/otouto/plugins/bible.lua +++ b/otouto/plugins/bible.lua @@ -5,11 +5,9 @@ local URL = require('socket.url') local utilities = require('otouto.utilities') function bible:init(config) - if not config.biblia_api_key then - print('Missing config value: biblia_api_key.') - print('bible.lua will not be enabled.') - return - end + assert(config.biblia_api_key, + 'bible.lua requires a Biblia API key from http://api.biblia.com.' + ) bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table bible.doc = config.cmd_pat .. [[bible <reference> @@ -21,9 +19,9 @@ bible.command = 'bible <reference>' function bible:action(msg, config) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - utilities.send_message(self, msg.chat.id, bible.doc, true, msg.message_id, true) + utilities.send_reply(self, msg, bible.doc, true) return end diff --git a/otouto/plugins/bing.lua b/otouto/plugins/bing.lua index 539fddd..c71e5ef 100644 --- a/otouto/plugins/bing.lua +++ b/otouto/plugins/bing.lua @@ -1,5 +1,4 @@ -- Credit to Juan (tg:JuanPotato; gh:JuanPotato) for this plugin. - -- Or rather, the seven lines that actually mean anything. local bing = {} @@ -15,54 +14,65 @@ bing.command = 'bing <query>' bing.search_url = 'https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=\'%s\'&$format=json' function bing:init(config) - if not config.bing_api_key then - print('Missing config value: bing_api_key.') - print('bing.lua will not be enabled.') - return - end - bing.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bing', true):t('g', true):t('google', true).table - bing.doc = config.cmd_pat .. [[bing <query> -Returns the top web search results from Bing. -Aliases: ]] .. config.cmd_pat .. 'g, ' .. config.cmd_pat .. 'google' + assert(config.bing_api_key, + 'bing.lua requires a Bing API key from http://datamarket.azure.com/dataset/bing/search.' + ) + + bing.headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) } + bing.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('bing', true):t('g', true):t('google', true).table + bing.doc = [[ +/bing <query> +Returns the top web results from Bing. +Aliases: /g, /google + ]] + bing.doc = bing.doc:gsub('/', config.cmd_pat) + end function bing:action(msg, config) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text ~= '' then - input = msg.reply_to_message.text - else - utilities.send_reply(self, msg, bing.doc, true) - return - end + utilities.send_reply(self, msg, bing.doc, true) + return end + local url = bing.search_url:format(URL.escape(input)) local resbody = {} - local _,b,_ = https.request{ + local _, code = https.request{ url = url, - headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) }, + headers = bing.headers, sink = ltn12.sink.table(resbody), } - if b ~= 200 then + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + + local data = JSON.decode(table.concat(resbody)) + -- Four results in a group, eight in private. + local limit = msg.chat.type == 'private' and 8 or 4 + -- No more results than provided. + limit = limit > #data.d.results and #data.d.results or limit + if limit == 0 then utilities.send_reply(self, msg, config.errors.results) return end - local dat = JSON.decode(table.concat(resbody)) - local limit = 4 - if msg.chat.type == 'private' then - limit = 8 - end - if limit > #dat.d.results then - limit = #dat.d.results - end + local reslist = {} for i = 1, limit do - local result = dat.d.results[i] - local s = '• [' .. result.Title:gsub('%]', '\\]') .. '](' .. result.Url:gsub('%)', '\\)') .. ')' - table.insert(reslist, s) + table.insert(reslist, string.format( + '• <a href="%s">%s</a>', + utilities.html_escape(data.d.results[i].Url), + utilities.html_escape(data.d.results[i].Title) + )) end - local output = '*Search results for* _' .. utilities.md_escape(input) .. '_ *:*\n' .. table.concat(reslist, '\n') - utilities.send_message(self, msg.chat.id, output, true, nil, true) + local output = string.format( + '<b>Search results for</b> <i>%s</i><b>:</b>\n%s', + utilities.html_escape(input), + table.concat(reslist, '\n') + ) + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end return bing diff --git a/otouto/plugins/blacklist.lua b/otouto/plugins/blacklist.lua index 3412c39..52e2926 100644 --- a/otouto/plugins/blacklist.lua +++ b/otouto/plugins/blacklist.lua @@ -1,39 +1,15 @@ - -- This plugin will allow the admin to blacklist users who will be unable to - -- use the bot. This plugin should be at the top of your plugin list in config. +local utilities = require('otouto.utilities') local blacklist = {} -local utilities = require('otouto.utilities') -local bindings = require('otouto.bindings') - -function blacklist:init() - if not self.database.blacklist then - self.database.blacklist = {} - end +function blacklist:init(config) + blacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('blacklist', true):t('unblacklist', true).table + blacklist.error = false end -blacklist.triggers = { - '' -} - -blacklist.error = false - function blacklist:action(msg, config) - if self.database.blacklist[tostring(msg.from.id)] then - return - elseif self.database.blacklist[tostring(msg.chat.id)] then - bindings.leaveChat(self, { chat_id = msg.chat.id }) - return - end - if not ( - msg.from.id == config.admin - and ( - msg.text:match('^'..config.cmd_pat..'blacklist') - or msg.text:match('^'..config.cmd_pat..'unblacklist') - ) - ) then - return true - end + if msg.from.id ~= config.admin then return true end local targets = {} if msg.reply_to_message then table.insert(targets, { @@ -44,7 +20,7 @@ function blacklist:action(msg, config) else local input = utilities.input(msg.text) if input then - for _, user in ipairs(utilities.index(input)) do + for user in input:gmatch('%g+') do if self.database.users[user] then table.insert(targets, { id = self.database.users[user].id, diff --git a/otouto/plugins/calc.lua b/otouto/plugins/calc.lua index a063aad..30e2ecd 100644 --- a/otouto/plugins/calc.lua +++ b/otouto/plugins/calc.lua @@ -13,29 +13,16 @@ Returns solutions to mathematical expressions and conversions between common uni end function calc:action(msg, config) - - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, calc.doc, true, msg.message_id, true) - return - end - end - - local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input) - - local output = HTTPS.request(url) - if not output then - utilities.send_reply(self, msg, config.errors.connection) + utilities.send_reply(self, msg, calc.doc, true) return end - output = '`' .. output .. '`' - - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - + local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input) + local output = HTTPS.request(url) + output = output and '`'..output..'`' or config.errors.connection + utilities.send_reply(self, msg, output, true) end return calc diff --git a/otouto/plugins/catfact.lua b/otouto/plugins/catfact.lua new file mode 100644 index 0000000..25a9bdb --- /dev/null +++ b/otouto/plugins/catfact.lua @@ -0,0 +1,28 @@ + -- Based on a plugin by matthewhesketh. + +local JSON = require('dkjson') +local HTTP = require('socket.http') +local utilities = require('otouto.utilities') + +local catfact = {} + +function catfact:init(config) + catfact.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('catfact', true).table + catfact.command = 'catfact' + catfact.doc = 'Returns a cat fact.' + catfact.url = 'http://catfacts-api.appspot.com/api/facts' +end + +function catfact:action(msg, config) + local jstr, code = HTTP.request(catfact.url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + local data = JSON.decode(jstr) + local output = '*Cat Fact*\n_' .. data.facts[1] .. '_' + utilities.send_message(self, msg.chat.id, output, true, nil, true) +end + +return catfact diff --git a/otouto/plugins/chatter.lua b/otouto/plugins/chatter.lua deleted file mode 100644 index 1e60875..0000000 --- a/otouto/plugins/chatter.lua +++ /dev/null @@ -1,80 +0,0 @@ - -- Put this absolutely at the end, even after greetings.lua. - -local chatter = {} - -local HTTP = require('socket.http') -local URL = require('socket.url') -local JSON = require('dkjson') -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - -function chatter:init(config) - if not config.simsimi_key then - print('Missing config value: simsimi_key.') - print('chatter.lua will not be enabled.') - return - end - - chatter.triggers = { - '' - } -end - -chatter.base_url = 'http://%sapi.simsimi.com/request.p?key=%s&lc=%s&ft=1.0&text=%s' - -function chatter:action(msg, config) - - if msg.text == '' then return true end - - if ( - not ( - msg.text_lower:match('^'..self.info.first_name:lower()..',') - or msg.text_lower:match('^@'..self.info.username:lower()..',') - or msg.from.id == msg.chat.id - --Uncomment the following line for Al Gore-like conversation. - --or (msg.reply_to_message and msg.reply_to_message.from.id == self.info.id) - ) - or msg.text:match('^'..config.cmd_pat) - or msg.text == '' - ) then - return true - end - - bindings.sendChatAction(self, { action = 'typing' } ) - - local input = msg.text_lower:gsub(self.info.first_name, 'simsimi') - input = input:gsub('@'..self.info.username, 'simsimi') - - local sandbox = config.simsimi_trial and 'sandbox.' or '' - - local url = chatter.base_url:format(sandbox, config.simsimi_key, config.lang, URL.escape(input)) - - local jstr, res = HTTP.request(url) - if res ~= 200 then - utilities.send_message(self, msg.chat.id, config.errors.chatter_connection) - return - end - - local jdat = JSON.decode(jstr) - if not jdat.response or jdat.response:match('^I HAVE NO RESPONSE.') then - utilities.send_message(self, msg.chat.id, config.errors.chatter_response) - return - end - local output = jdat.response - - -- Clean up the response here. - output = utilities.trim(output) - -- Simsimi will often refer to itself. Replace "simsimi" with the bot name. - output = output:gsub('%aimi?%aimi?', self.info.first_name) - -- Self-explanatory. - output = output:gsub('USER', msg.from.first_name) - -- Capitalize the first letter. - output = output:gsub('^%l', string.upper) - -- Add a period if there is no punctuation. - output = output:gsub('%P$', '%1.') - - utilities.send_message(self, msg.chat.id, output) - -end - -return chatter diff --git a/otouto/plugins/chuckfact.lua b/otouto/plugins/chuckfact.lua new file mode 100644 index 0000000..8cfc2d6 --- /dev/null +++ b/otouto/plugins/chuckfact.lua @@ -0,0 +1,28 @@ + -- Based on a plugin by matthewhesketh. + +local JSON = require('dkjson') +local HTTP = require('socket.http') +local utilities = require('otouto.utilities') + +local chuck = {} + +function chuck:init(config) + chuck.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('chuck', true):t('cn', true):t('chucknorris', true).table + chuck.command = 'chuck' + chuck.doc = 'Returns a fact about Chuck Norris.' + chuck.url = 'http://api.icndb.com/jokes/random' +end + +function chuck:action(msg, config) + local jstr, code = HTTP.request(chuck.url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + local data = JSON.decode(jstr) + local output = '*Chuck Norris Fact*\n_' .. data.value.joke .. '_' + utilities.send_message(self, msg.chat.id, output, true, nil, true) +end + +return chuck diff --git a/otouto/plugins/cleverbot.lua b/otouto/plugins/cleverbot.lua new file mode 100644 index 0000000..d859af9 --- /dev/null +++ b/otouto/plugins/cleverbot.lua @@ -0,0 +1,36 @@ +local HTTPS = require('ssl.https') +local URL = require('socket.url') +local JSON = require('dkjson') +local utilities = require('otouto.utilities') +local bindings = require('otouto.bindings') + +local cleverbot = {} + +function cleverbot:init(config) + cleverbot.name = '^' .. self.info.first_name:lower() .. ', ' + cleverbot.username = '^@' .. self.info.username:lower() .. ', ' + cleverbot.triggers = { + '^' .. self.info.first_name:lower() .. ', ', + '^@' .. self.info.username:lower() .. ', ' + } + cleverbot.url = config.chatter.cleverbot_api + cleverbot.error = false +end + +function cleverbot:action(msg, config) + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) + local input = msg.text_lower:gsub(cleverbot.name, ''):gsub(cleverbot.name, '') + local jstr, code = HTTPS.request(cleverbot.url .. URL.escape(input)) + if code ~= 200 then + utilities.send_message(self, msg.chat.id, config.chatter.connection) + return + end + local data = JSON.decode(jstr) + if not data.clever then + utilities.send_message(self, msg.chat.id, config.chatter.response) + return + end + utilities.send_message(self, msg.chat.id, data.clever) +end + +return cleverbot diff --git a/otouto/plugins/commit.lua b/otouto/plugins/commit.lua index 9de1d1e..5087669 100644 --- a/otouto/plugins/commit.lua +++ b/otouto/plugins/commit.lua @@ -1,8 +1,8 @@ - -- Commits from https://github.com/ngerakines/commitment. - local commit = {} local utilities = require('otouto.utilities') +local bindings = require('otouto.bindings') +local http = require('socket.http') commit.command = 'commit' commit.doc = 'Returns a commit message from whatthecommit.com.' @@ -11,420 +11,16 @@ function commit:init(config) commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table end -local commits = { - "One does not simply merge into master", - "Merging the merge", - "Another bug bites the dust", - "de-misunderestimating", - "Some shit.", - "add actual words", - "I CAN HAZ COMMENTZ.", - "giggle.", - "Whatever.", - "Finished fondling.", - "FONDLED THE CODE", - "this is how we generate our shit.", - "unh", - "It works!", - "unionfind is no longer being molested.", - "Well, it's doing something.", - "I'M PUSHING.", - "Whee.", - "Whee, good night.", - "It'd be nice if type errors caused the compiler to issue a type error", - "Fucking templates.", - "I hate this fucking language.", - "marks", - "that coulda been bad", - "hoo boy", - "It was the best of times, it was the worst of times", - "Fucking egotistical bastard. adds expandtab to vimrc", - "if you're not using et, fuck off", - "WHO THE FUCK CAME UP WITH MAKE?", - "This is a basic implementation that works.", - "By works, I meant 'doesnt work'. Works now..", - "Last time I said it works? I was kidding. Try this.", - "Just stop reading these for a while, ok..", - "Give me a break, it's 2am. But it works now.", - "Make that it works in 90% of the cases. 3:30.", - "Ok, 5am, it works. For real.", - "FOR REAL.", - "I don't know what these changes are supposed to accomplish but somebody told me to make them.", - "I don't get paid enough for this shit.", - "fix some fucking errors", - "first blush", - "So my boss wanted this button ...", - "uhhhhhh", - "forgot we're not using a smart language", - "include shit", - "To those I leave behind, good luck!", - "things occurred", - "i dunno, maybe this works", - "8==========D", - "No changes made", - "whooooooooooooooooooooooooooo", - "clarify further the brokenness of C++. why the fuck are we using C++?", - ".", - "Friday 5pm", - "changes", - "A fix I believe, not like I tested or anything", - "Useful text", - "pgsql is being a pain", - "pgsql is more strict, increase the hackiness up to 11", - "c&p fail", - "syntax", - "fix", - "just shoot me", - "arrrggghhhhh fixed!", - "someone fails and it isn't me", - "totally more readable", - "better grepping", - "fix", - "fix bug, for realz", - "fix /sigh", - "Does this work", - "MOAR BIFURCATION", - "bifurcation", - "REALLY FUCKING FIXED", - "FIX", - "better ignores", - "More ignore", - "more ignores", - "more ignores", - "more ignores", - "more ignores", - "more ignores", - "more ignored words", - "more fixes", - "really ignore ignored worsd", - "fixes", - "/sigh", - "fix", - "fail", - "pointless limitation", - "omg what have I done?", - "added super-widget 2.0.", - "tagging release w.t.f.", - "I can't believe it took so long to fix this.", - "I must have been drunk.", - "This is why the cat shouldn't sit on my keyboard.", - "This is why git rebase is a horrible horrible thing.", - "ajax-loader hotness, oh yeah", - "small is a real HTML tag, who knew.", - "WTF is this.", - "Do things better, faster, stronger", - "Use a real JS construct, WTF knows why this works in chromium.", - "Added a banner to the default admin page. Please have mercy on me =(", - "needs more cow bell", - "Switched off unit test X because the build had to go out now and there was no time to fix it properly.", - "Updated", - "I must sleep... it's working... in just three hours...", - "I was wrong...", - "Completed with no bugs...", - "Fixed a little bug...", - "Fixed a bug in NoteLineCount... not seriously...", - "woa!! this one was really HARD!", - "Made it to compile...", - "changed things...", - "touched...", - "i think i fixed a bug...", - "perfect...", - "Moved something to somewhere... goodnight...", - "oops, forgot to add the file", - "Corrected mistakes", - "oops", - "oops!", - "put code that worked where the code that didn't used to be", - "Nothing to see here, move along", - "I am even stupider than I thought", - "I don't know what the hell I was thinking.", - "fixed errors in the previous commit", - "Committed some changes", - "Some bugs fixed", - "Minor updates", - "Added missing file in previous commit", - "bug fix", - "typo", - "bara bra grejjor", - "Continued development...", - "Does anyone read this? I'll be at the coffee shop accross the street.", - "That's just how I roll", - "work in progress", - "minor changes", - "some brief changes", - "assorted changes", - "lots and lots of changes", - "another big bag of changes", - "lots of changes after a lot of time", - "LOTS of changes. period", - "Test commit. Please ignore", - "I'm just a grunt. Don't blame me for this awful PoS.", - "I did it for the lulz!", - "I'll explain this when I'm sober .. or revert it", - "Obligatory placeholder commit message", - "A long time ago, in a galaxy far far away...", - "Fixed the build.", - "various changes", - "One more time, but with feeling.", - "Handled a particular error.", - "Fixed unnecessary bug.", - "Removed code.", - "Added translation.", - "Updated build targets.", - "Refactored configuration.", - "Locating the required gigapixels to render...", - "Spinning up the hamster...", - "Shovelling coal into the server...", - "Programming the flux capacitor", - "The last time I tried this the monkey didn't survive. Let's hope it works better this time.", - "I should have had a V8 this morning.", - "640K ought to be enough for anybody", - "pay no attention to the man behind the curtain", - "a few bits tried to escape, but we caught them", - "Who has two thumbs and remembers the rudiments of his linear algebra courses? Apparently, this guy.", - "workaround for ant being a pile of fail", - "Don't push this commit", - "rats", - "squash me", - "fixed mistaken bug", - "Final commit, ready for tagging", - "-m \'So I hear you like commits ...\'", - "epic", - "need another beer", - "Well the book was obviously wrong.", - "lolwhat?", - "Another commit to keep my CAN streak going.", - "I cannot believe that it took this long to write a test for this.", - "TDD: 1, Me: 0", - "Yes, I was being sarcastic.", - "Apparently works-for-me is a crappy excuse.", - "tl;dr", - "I would rather be playing SC2.", - "Crap. Tonight is raid night and I am already late.", - "I know what I am doing. Trust me.", - "You should have trusted me.", - "Is there an award for this?", - "Is there an achievement for this?", - "I'm totally adding this to epic win. +300", - "This really should not take 19 minutes to build.", - "fixed the israeli-palestinian conflict", - "SHIT ===> GOLD", - "Committing in accordance with the prophecy.", - "It compiles! Ship it!", - "LOL!", - "Reticulating splines...", - "SEXY RUSSIAN CODES WAITING FOR YOU TO CALL", - "s/import/include/", - "extra debug for stuff module", - "debug line test", - "debugo", - "remove debug<br/>all good", - "debug suff", - "more debug... who overwrote!", - "these confounded tests drive me nuts", - "For great justice.", - "QuickFix.", - "oops - thought I got that one.", - "removed echo and die statements, lolz.", - "somebody keeps erasing my changes.", - "doh.", - "pam anderson is going to love me.", - "added security.", - "arrgghh... damn this thing for not working.", - "jobs... steve jobs", - "and a comma", - "this is my quickfix branch and i will use to do my quickfixes", - "Fix my stupidness", - "and so the crazy refactoring process sees the sunlight after some months in the dark!", - "gave up and used tables.", - "[Insert your commit message here. Be sure to make it descriptive.]", - "Removed test case since code didn't pass QA", - "removed tests since i can't make them green", - "stuff", - "more stuff", - "Become a programmer, they said. It'll be fun, they said.", - "Same as last commit with changes", - "foo", - "just checking if git is working properly...", - "fixed some minor stuff, might need some additional work.", - "just trolling the repo", - "All your codebase are belong to us.", - "Somebody set up us the bomb.", - "should work I guess...", - "To be honest, I do not quite remember everything I changed here today. But it is all good, I tell ya.", - "well crap.", - "herpderp (redux)", - "herpderp", - "Derp", - "derpherp", - "Herping the derp", - "sometimes you just herp the derp so hard it herpderps", - "Derp. Fix missing constant post rename", - "Herping the fucking derp right here and now.", - "Derp, asset redirection in dev mode", - "mergederp", - "Derp search/replace fuckup", - "Herpy dooves.", - "Derpy hooves", - "derp, helper method rename", - "Herping the derp derp (silly scoping error)", - "Herp derp I left the debug in there and forgot to reset errors.", - "Reset error count between rows. herpderp", - "hey, what's that over there?!", - "hey, look over there!", - "It worked for me...", - "Does not work.", - "Either Hot Shit or Total Bollocks", - "Arrrrgggg", - "Don’t mess with Voodoo", - "I expected something different.", - "Todo!!!", - "This is supposed to crash", - "No changes after this point.", - "I know, I know, this is not how I’m supposed to do it, but I can't think of something better.", - "Don’t even try to refactor it.", - "(c) Microsoft 1988", - "Please no changes this time.", - "Why The Fuck?", - "We should delete this crap before shipping.", - "Shit code!", - "ALL SORTS OF THINGS", - "Herpderp, shoulda check if it does really compile.", - "I CAN HAZ PYTHON, I CAN HAZ INDENTS", - "Major fixup.", - "less french words", - "breathe, =, breathe", - "IEize", - "this doesn't really make things faster, but I tried", - "this should fix it", - "forgot to save that file", - "Glue. Match sticks. Paper. Build script!", - "Argh! About to give up :(", - "Blaming regex.", - "oops", - "it's friday", - "yo recipes", - "Not sure why", - "lol digg", - "grrrr", - "For real, this time.", - "Feed. You. Stuff. No time.", - "I don't give a damn 'bout my reputation", - "DEAL WITH IT", - "commit", - "tunning", - "I really should've committed this when I finished it...", - "It's getting hard to keep up with the crap I've trashed", - "I honestly wish I could remember what was going on here...", - "I must enjoy torturing myself", - "For the sake of my sanity, just ignore this...", - "That last commit message about silly mistakes pales in comparision to this one", - "My bad", - "Still can't get this right...", - "Nitpicking about alphabetizing methods, minor OCD thing", - "Committing fixes in the dark, seriously, who killed my power!?", - "You can't see it, but I'm making a very angry face right now", - "Fix the fixes", - "It's secret!", - "Commit committed....", - "No time to commit.. My people need me!", - "Something fixed", - "I'm hungry", - "asdfasdfasdfasdfasdfasdfadsf", - "hmmm", - "formatted all", - "Replace all whitespaces with tabs.", - "s/ / /g", - "I'm too foo for this bar", - "Things went wrong...", - "??! what the ...", - "This solves it.", - "Working on tests (haha)", - "fixed conflicts (LOL merge -s ours; push -f)", - "last minute fixes.", - "fuckup.", - "Revert \"fuckup\".", - "should work now.", - "final commit.", - "done. going to bed now.", - "buenas those-things.", - "Your commit is writing checks your merge can't cash.", - "This branch is so dirty, even your mom can't clean it.", - "wip", - "Revert \"just testing, remember to revert\"", - "bla", - "harharhar", - "restored deleted entities just to be sure", - "added some filthy stuff", - "bugger", - "lol", - "oopsie B|", - "Copy pasta fail. still had a instead of a", - "Now added delete for real", - "grmbl", - "move your body every every body", - "Trying to fake a conflict", - "And a commit that I don't know the reason of...", - "ffs", - "that's all folks", - "Fucking submodule bull shit", - "apparently i did something…", - "bump to 0.0.3-dev:wq", - "pep8 - cause I fell like doing a barrel roll", - "pep8 fixer", - "it is hump day _^_", - "happy monday _ bleh _", - "after of this commit remember do a git reset hard", - "someday I gonna kill someone for this shit...", - "magic, have no clue but it works", - "I am sorry", - "dirty hack, have a better idea ?", - "Code was clean until manager requested to fuck it up", - " - Temporary commit.", - ":(:(", - "...", - "GIT :/", - "stopped caring 10 commits ago", - "Testing in progress ;)", - "Fixed Bug", - "Fixed errors", - "Push poorly written test can down the road another ten years", - "commented out failing tests", - "I'm human", - "TODO: write meaningful commit message", - "Pig", - "SOAP is a piece of shit", - "did everything", - "project lead is allergic to changes...", - "making this thing actually usable.", - "I was told to leave it alone, but I have this thing called OCD, you see", - "Whatever will be, will be 8{", - "It's 2015; why are we using ColdFusion?!", - "#GrammarNazi", - "Future self, please forgive me and don't hit me with the baseball bat again!", - "Hide those navs, boi!", - "Who knows...", - "Who knows WTF?!", - "I should get a raise for this.", - "Done, to whoever merges this, good luck.", - "Not one conflict, today was a good day.", - "First Blood", - "Fixed the fuck out of #526!", - "I'm too old for this shit!", - "One little whitespace gets its very own commit! Oh, life is so erratic!", - "please dont let this be the problem", - "good: no crash. bad: nothing happens", - "trying", - "trying harder", - "i tried", - "fml" -} - function commit:action(msg) - - local output = '`'..commits[math.random(#commits)]..'`' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - + bindings.request( + self, + 'sendMessage', + { + chat_id = msg.chat.id, + text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```', + parse_mode = 'Markdown' + } + ) end return commit diff --git a/otouto/plugins/echo.lua b/otouto/plugins/echo.lua index cb8d758..5cc1ad6 100644 --- a/otouto/plugins/echo.lua +++ b/otouto/plugins/echo.lua @@ -11,14 +11,14 @@ end function echo:action(msg) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true) else local output if msg.chat.type == 'supergroup' then - output = '*Echo:*\n"' .. utilities.md_escape(input) .. '"' + output = utilities.style.enquote('Echo', input) else output = utilities.md_escape(utilities.char.zwnj..input) end diff --git a/otouto/plugins/fortune.lua b/otouto/plugins/fortune.lua index 7d26eec..5d7f0d7 100644 --- a/otouto/plugins/fortune.lua +++ b/otouto/plugins/fortune.lua @@ -6,11 +6,10 @@ local utilities = require('otouto.utilities') function fortune:init(config) local s = io.popen('fortune'):read('*all') - if s:match('not found$') then - print('fortune is not installed on this computer.') - print('fortune.lua will not be enabled.') - return - end + assert( + not s:match('not found$'), + 'fortune.lua requires the fortune program to be installed.' + ) fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table end diff --git a/otouto/plugins/gImages.lua b/otouto/plugins/gImages.lua index e17a9d0..94eadff 100644 --- a/otouto/plugins/gImages.lua +++ b/otouto/plugins/gImages.lua @@ -9,37 +9,28 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function gImages:init(config) - if not config.google_api_key then - print('Missing config value: google_api_key.') - print('gImages.lua will not be enabled.') - return - elseif not config.google_cse_key then - print('Missing config value: google_cse_key.') - print('gImages.lua will not be enabled.') - return - end + assert(config.google_api_key and config.google_cse_key, + 'gImages.lua requires a Google API key from http://console.developers.google.com and a Google Custom Search Engine key from http://cse.google.com/cse.' + ) gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table gImages.doc = config.cmd_pat .. [[image <query> Returns a randomized top result from Google Images. Safe search is enabled by default; use "]] .. config.cmd_pat .. [[insfw" to disable it. NSFW results will not display an image preview. Alias: ]] .. config.cmd_pat .. 'i' + gImages.search_url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key end gImages.command = 'image <query>' function gImages:action(msg, config) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, gImages.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, gImages.doc, true) + return end - local url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key + local url = gImages.search_url if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then url = url .. '&safe=high' diff --git a/otouto/plugins/gMaps.lua b/otouto/plugins/gMaps.lua index 78f72a0..5838715 100644 --- a/otouto/plugins/gMaps.lua +++ b/otouto/plugins/gMaps.lua @@ -6,28 +6,26 @@ local utilities = require('otouto.utilities') gMaps.command = 'location <query>' function gMaps:init(config) - gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('location', true):t('loc', true).table - gMaps.doc = config.cmd_pat .. [[location <query> + gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('location', true):t('loc', true).table + gMaps.doc = [[ +/location <query> Returns a location from Google Maps. -Alias: ]] .. config.cmd_pat .. 'loc' +Alias: /loc + ]] + gMaps.doc = gMaps.doc:gsub('/', config.cmd_pat) end function gMaps:action(msg, config) - - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, gMaps.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, gMaps.doc, true) + return end local coords = utilities.get_coords(input, config) if type(coords) == 'string' then utilities.send_reply(self, msg, coords) - return end bindings.sendLocation(self, { @@ -36,7 +34,6 @@ function gMaps:action(msg, config) longitude = coords.lon, reply_to_message_id = msg.message_id } ) - end return gMaps diff --git a/otouto/plugins/gSearch.lua b/otouto/plugins/gSearch.lua deleted file mode 100644 index 851cc65..0000000 --- a/otouto/plugins/gSearch.lua +++ /dev/null @@ -1,79 +0,0 @@ -local gSearch = {} - -local HTTPS = require('ssl.https') -local URL = require('socket.url') -local JSON = require('dkjson') -local utilities = require('otouto.utilities') - -gSearch.command = 'google <query>' - -function gSearch:init(config) - gSearch.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('g', true):t('google', true):t('gnsfw', true).table - gSearch.doc = config.cmd_pat .. [[google <query> -Returns four (if group) or eight (if private message) results from Google. Safe search is enabled by default, use "]] .. config.cmd_pat .. [[gnsfw" to disable it. -Alias: ]] .. config.cmd_pat .. 'g' -end - -function gSearch:action(msg, config) - - local input = utilities.input(msg.text) - if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, gSearch.doc, true, msg.message_id, true) - return - end - end - - local url = 'https://ajax.googleapis.com/ajax/services/search/web?v=1.0' - - if msg.from.id == msg.chat.id then - url = url .. '&rsz=8' - else - url = url .. '&rsz=4' - end - - if not string.match(msg.text, '^'..config.cmd_pat..'g[oogle]*nsfw') then - url = url .. '&safe=active' - end - - url = url .. '&q=' .. URL.escape(input) - - local jstr, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - local jdat = JSON.decode(jstr) - if not jdat.responseData then - utilities.send_reply(self, msg, config.errors.connection) - return - end - if not jdat.responseData.results[1] then - utilities.send_reply(self, msg, config.errors.results) - return - end - - local output = '*Google results for* _' .. input .. '_ *:*\n' - for i,_ in ipairs(jdat.responseData.results) do - local title = jdat.responseData.results[i].titleNoFormatting:gsub('%[.+%]', ''):gsub('&', '&') ---[[ - if title:len() > 48 then - title = title:sub(1, 45) .. '...' - end -]]-- - local u = jdat.responseData.results[i].unescapedUrl - if u:find('%)') then - output = output .. '• ' .. title .. '\n' .. u:gsub('_', '\\_') .. '\n' - else - output = output .. '• [' .. title .. '](' .. u .. ')\n' - end - end - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - -end - -return gSearch diff --git a/otouto/plugins/greetings.lua b/otouto/plugins/greetings.lua index a2252c4..5375e66 100644 --- a/otouto/plugins/greetings.lua +++ b/otouto/plugins/greetings.lua @@ -1,63 +1,32 @@ - -- Put this on the bottom of your plugin list, after help.lua. - -- If you want to configure your own greetings, copy the following table - -- (without the "config.") to your config.lua file. +local utilities = require('otouto.utilities') local greetings = {} -local utilities = require('otouto.utilities') - function greetings:init(config) - config.greetings = config.greetings or { - ['Hello, #NAME.'] = { - 'hello', - 'hey', - 'sup', - 'hi', - 'good morning', - 'good day', - 'good afternoon', - 'good evening' - }, - ['Goodbye, #NAME.'] = { - 'bye', - 'later', - 'see ya', - 'good night' - }, - ['Welcome back, #NAME.'] = { - 'i\'m home', - 'i\'m back' - }, - ['You\'re welcome, #NAME.'] = { - 'thanks', - 'thank you' - } - } - - greetings.triggers = { - self.info.first_name:lower() .. '%p*$' - } + greetings.triggers = {} + for _, triggers in pairs(config.greetings) do + for i = 1, #triggers do + triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$' + table.insert(greetings.triggers, triggers[i]) + end + end end function greetings:action(msg, config) - - local nick = utilities.build_name(msg.from.first_name, msg.from.last_name) + local nick if self.database.userdata[tostring(msg.from.id)] then - nick = self.database.userdata[tostring(msg.from.id)].nickname or nick + nick = self.database.userdata[tostring(msg.from.id)].nickname end + nick = nick or utilities.build_name(msg.from.first_name, msg.from.last_name) - for trigger,responses in pairs(config.greetings) do - for _,response in pairs(responses) do - if msg.text_lower:match(response..',? '..self.info.first_name:lower()) then - local output = utilities.char.zwnj .. trigger:gsub('#NAME', nick) - utilities.send_message(self, msg.chat.id, output) + for response, triggers in pairs(config.greetings) do + for _, trigger in pairs(triggers) do + if string.match(msg.text_lower, trigger) then + utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick)) return end end end - - return true - end return greetings diff --git a/otouto/plugins/hackernews.lua b/otouto/plugins/hackernews.lua index 2bc8f53..47e8dc2 100644 --- a/otouto/plugins/hackernews.lua +++ b/otouto/plugins/hackernews.lua @@ -1,63 +1,75 @@ -local hackernews = {} - local HTTPS = require('ssl.https') local JSON = require('dkjson') -local bindings = require('otouto.bindings') local utilities = require('otouto.utilities') +local bindings = require('otouto.bindings') + +local hackernews = {} hackernews.command = 'hackernews' +local function get_hackernews_results() + local results = {} + local jstr, code = HTTPS.request(hackernews.topstories_url) + if code ~= 200 then return end + local data = JSON.decode(jstr) + for i = 1, 8 do + local ijstr, icode = HTTPS.request(hackernews.res_url:format(data[i])) + if icode ~= 200 then return end + local idata = JSON.decode(ijstr) + local result + if idata.url then + result = string.format( + '\n• <code>[</code><a href="%s">%s</a><code>]</code> <a href="%s">%s</a>', + utilities.html_escape(hackernews.art_url:format(idata.id)), + idata.id, + utilities.html_escape(idata.url), + utilities.html_escape(idata.title) + ) + else + result = string.format( + '\n• <code>[</code><a href="%s">%s</a><code>]</code> %s', + utilities.html_escape(hackernews.art_url:format(idata.id)), + idata.id, + utilities.html_escape(idata.title) + ) + end + table.insert(results, result) + end + return results +end + function hackernews:init(config) hackernews.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hackernews', true):t('hn', true).table hackernews.doc = [[Returns four (if group) or eight (if private message) top stories from Hacker News. Alias: ]] .. config.cmd_pat .. 'hn' + hackernews.topstories_url = 'https://hacker-news.firebaseio.com/v0/topstories.json' + hackernews.res_url = 'https://hacker-news.firebaseio.com/v0/item/%s.json' + hackernews.art_url = 'https://news.ycombinator.com/item?id=%s' + hackernews.last_update = 0 + if config.hackernews_onstart == true then + hackernews.results = get_hackernews_results() + if hackernews.results then hackernews.last_update = os.time() / 60 end + end end function hackernews:action(msg, config) - - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) - - local jstr, res = HTTPS.request('https://hacker-news.firebaseio.com/v0/topstories.json') - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return + local now = os.time() / 60 + if not hackernews.results or hackernews.last_update + config.hackernews_interval < now then + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) + hackernews.results = get_hackernews_results() + if not hackernews.results then + utilities.send_reply(self, msg, config.errors.connection) + return + end + hackernews.last_update = now end - - local jdat = JSON.decode(jstr) - - local res_count = 4 - if msg.chat.id == msg.from.id then - res_count = 8 - end - - local output = '*Hacker News:*\n' + -- Four results in a group, eight in private. + local res_count = msg.chat.id == msg.from.id and 8 or 4 + local output = '<b>Top Stories from Hacker News:</b>' for i = 1, res_count do - local res_url = 'https://hacker-news.firebaseio.com/v0/item/' .. jdat[i] .. '.json' - jstr, res = HTTPS.request(res_url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local res_jdat = JSON.decode(jstr) - local title = res_jdat.title:gsub('%[.+%]', ''):gsub('%(.+%)', ''):gsub('&', '&') - if title:len() > 48 then - title = title:sub(1, 45) .. '...' - end - local url = res_jdat.url - if not url then - utilities.send_reply(self, msg, config.errors.connection) - return - end - if url:find('%(') then - output = output .. '• ' .. title .. '\n' .. url:gsub('_', '\\_') .. '\n' - else - output = output .. '• [' .. title .. '](' .. url .. ')\n' - end - + output = output .. hackernews.results[i] end - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end return hackernews diff --git a/otouto/plugins/hearthstone.lua b/otouto/plugins/hearthstone.lua index 0a29ecf..05cbca8 100644 --- a/otouto/plugins/hearthstone.lua +++ b/otouto/plugins/hearthstone.lua @@ -5,45 +5,36 @@ local hearthstone = {} --local HTTPS = require('ssl.https') local JSON = require('dkjson') local utilities = require('otouto.utilities') +local HTTPS = require('ssl.https') function hearthstone:init(config) + hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table + hearthstone.command = 'hearthstone <query>' + if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then print('Downloading Hearthstone database...') - -- This stuff doesn't play well with lua-sec. Disable it for now; hack in curl. - --local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json') - --if res ~= 200 then - -- print('Error connecting to hearthstonejson.com.') - -- print('hearthstone.lua will not be enabled.') - -- return - --end - --local jdat = JSON.decode(jstr) - - local s = io.popen('curl -s https://api.hearthstonejson.com/v1/latest/enUS/cards.json'):read('*all') - local d = JSON.decode(s) - - if not d then + local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json') + if not jstr or res ~= 200 then print('Error connecting to hearthstonejson.com.') print('hearthstone.lua will not be enabled.') + hearthstone.command = nil + hearthstone.triggers = nil return end - - self.database.hearthstone = d + self.database.hearthstone = JSON.decode(jstr) self.database.hearthstone.expiration = os.time() + 600000 print('Download complete! It will be stored for a week.') end - hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table hearthstone.doc = config.cmd_pat .. [[hearthstone <query> Returns Hearthstone card info. Alias: ]] .. config.cmd_pat .. 'hs' end -hearthstone.command = 'hearthstone <query>' - local function format_card(card) local ctype = card.type @@ -102,9 +93,9 @@ end function hearthstone:action(msg, config) - local input = utilities.input(msg.text_lower) + local input = utilities.input_from_msg(msg) if not input then - utilities.send_message(self, msg.chat.id, hearthstone.doc, true, msg.message_id, true) + utilities.send_reply(self, msg, hearthstone.doc, true) return end diff --git a/otouto/plugins/help.lua b/otouto/plugins/help.lua index 7a804b7..8b2ed71 100644 --- a/otouto/plugins/help.lua +++ b/otouto/plugins/help.lua @@ -1,46 +1,47 @@ - -- This plugin should go at the end of your plugin list in - -- config.lua, but not after greetings.lua. +local utilities = require('otouto.utilities') local help = {} -local utilities = require('otouto.utilities') - -local help_text - function help:init(config) - local commandlist = {} - help_text = '*Available commands:*\n• '..config.cmd_pat - for _,plugin in ipairs(self.plugins) do - if plugin.command then - table.insert(commandlist, plugin.command) - if plugin.doc then - plugin.help_word = utilities.get_word(plugin.command, 1) - end - end - end - table.insert(commandlist, 'help [command]') - table.sort(commandlist) - help_text = help_text .. table.concat(commandlist, '\n• '..config.cmd_pat) .. '\nArguments: <required> [optional]' - help_text = help_text:gsub('%[', '\\[') help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('help', true):t('h', true).table + help.command = 'help [command]' help.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.' end -function help:action(msg) +function help:action(msg, config) local input = utilities.input(msg.text_lower) if input then + if not help.help_word then + for _, plugin in ipairs(self.plugins) do + if plugin.command and plugin.doc and not plugin.help_word then + plugin.help_word = utilities.get_word(plugin.command, 1) + end + end + end for _,plugin in ipairs(self.plugins) do if plugin.help_word == input:gsub('^/', '') then - local output = '*Help for* _' .. plugin.help_word .. '_ *:*\n' .. plugin.doc + local output = '*Help for* _' .. plugin.help_word .. '_*:*\n' .. plugin.doc utilities.send_message(self, msg.chat.id, output, true, nil, true) return end end utilities.send_reply(self, msg, 'Sorry, there is no help for that command.') else + -- Generate the help message on first run. + if not help.text then + local commandlist = {} + for _, plugin in ipairs(self.plugins) do + if plugin.command then + table.insert(commandlist, plugin.command) + end + end + table.sort(commandlist) + help.text = '*Available commands:*\n• ' .. config.cmd_pat .. table.concat(commandlist, '\n• '..config.cmd_pat) .. '\nArguments: <required> [optional]' + help.text = help.text:gsub('%[', '\\[') + end -- Attempt to send the help message via PM. -- If msg is from a group, tell the group whether the PM was successful. - local res = utilities.send_message(self, msg.from.id, help_text, true, nil, true) + local res = utilities.send_message(self, msg.from.id, help.text, true, nil, true) if not res then utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) elseif msg.chat.type ~= 'private' then diff --git a/otouto/plugins/id.lua b/otouto/plugins/id.lua new file mode 100644 index 0000000..d077573 --- /dev/null +++ b/otouto/plugins/id.lua @@ -0,0 +1,62 @@ +local utilities = require('otouto.utilities') + +local id = {} + +function id:init(config) + id.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('id', true).table + id.command = 'id <user>' + id.doc = config.cmd_pat .. [[id <user> ... +Returns the name, ID, and username (if applicable) for the given users. +Arguments must be usernames and/or IDs. Input is also accepted via reply. If no input is given, returns info for the user. + ]] +end + +function id.format(t) + if t.username then + return string.format( + '@%s, AKA <b>%s</b> <code>[%s]</code>.\n', + t.username, + utilities.build_name(t.first_name, t.last_name), + t.id + ) + else + return string.format( + '<b>%s</b> <code>[%s]</code>.\n', + utilities.build_name(t.first_name, t.last_name), + t.id + ) + end +end + +function id:action(msg) + local output + local input = utilities.input(msg.text) + if msg.reply_to_message then + output = id.format(msg.reply_to_message.from) + elseif input then + output = '' + for user in input:gmatch('%g+') do + if tonumber(user) then + if self.database.users[user] then + output = output .. id.format(self.database.users[user]) + else + output = output .. 'I don\'t recognize that ID (' .. user .. ').\n' + end + elseif user:match('^@') then + local t = utilities.resolve_username(self, user) + if t then + output = output .. id.format(t) + else + output = output .. 'I don\'t recognize that username (' .. user .. ').\n' + end + else + output = output .. 'Invalid username or ID (' .. user .. ').\n' + end + end + else + output = id.format(msg.from) + end + utilities.send_reply(self, msg, output, 'html') +end + +return id diff --git a/otouto/plugins/imdb.lua b/otouto/plugins/imdb.lua index 1a4e5c4..1f91930 100644 --- a/otouto/plugins/imdb.lua +++ b/otouto/plugins/imdb.lua @@ -14,14 +14,10 @@ end function imdb:action(msg, config) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, imdb.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, imdb.doc, true) + return end local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input) diff --git a/otouto/plugins/isup.lua b/otouto/plugins/isup.lua new file mode 100644 index 0000000..bdda1ae --- /dev/null +++ b/otouto/plugins/isup.lua @@ -0,0 +1,43 @@ + -- Based on a plugin by matthewhesketh. + +local HTTP = require('socket.http') +local HTTPS = require('ssl.https') +local utilities = require('otouto.utilities') + +local isup = {} + +function isup:init(config) + isup.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('websitedown', true):t('isitup', true):t('isup', true).table + + isup.doc = config.cmd_pat .. [[isup <url> +Returns the up or down status of a website.]] + isup.command = 'isup <url>' +end + +function isup:action(msg, config) + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, isup.doc) + return + end + + local protocol = HTTP + local url_lower = input:lower() + if url_lower:match('^https') then + protocol = HTTPS + elseif not url_lower:match('^http') then + input = 'http://' .. input + end + local _, code = protocol.request(input) + code = tonumber(code) + local output + if not code or code > 399 then + output = 'This website is down or nonexistent.' + else + output = 'This website is up.' + end + utilities.send_reply(self, msg, output, true) +end + +return isup diff --git a/otouto/plugins/lastfm.lua b/otouto/plugins/lastfm.lua index ea5eab9..76b47ed 100644 --- a/otouto/plugins/lastfm.lua +++ b/otouto/plugins/lastfm.lua @@ -9,11 +9,9 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function lastfm:init(config) - if not config.lastfm_api_key then - print('Missing config value: lastfm_api_key.') - print('lastfm.lua will not be enabled.') - return - end + assert(config.lastfm_api_key, + 'lastfm.lua requires a last.fm API key from http://last.fm/api.' + ) lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table lastfm.doc = config.cmd_pat .. [[np [username] diff --git a/otouto/plugins/luarun.lua b/otouto/plugins/luarun.lua index 1e9e095..dfa4b96 100644 --- a/otouto/plugins/luarun.lua +++ b/otouto/plugins/luarun.lua @@ -2,10 +2,21 @@ local luarun = {} local utilities = require('otouto.utilities') local URL = require('socket.url') -local JSON = require('dkjson') +local JSON, serpent function luarun:init(config) luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table + if config.luarun_serpent then + serpent = require('serpent') + luarun.serialize = function(t) + return serpent.block(t, {comment=false}) + end + else + JSON = require('dkjson') + luarun.serialize = function(t) + return JSON.encode(t, {indent=true}) + end + end end function luarun:action(msg, config) @@ -28,6 +39,7 @@ function luarun:action(msg, config) local bot = require('otouto.bot') local bindings = require('otouto.bindings') local utilities = require('otouto.utilities') + local drua = require('otouto.drua-tg') local JSON = require('dkjson') local URL = require('socket.url') local HTTP = require('socket.http') @@ -38,7 +50,7 @@ function luarun:action(msg, config) output = 'Done!' else if type(output) == 'table' then - local s = JSON.encode(output, {indent=true}) + local s = luarun.serialize(output) if URL.escape(s):len() < 4000 then output = s end diff --git a/otouto/plugins/me.lua b/otouto/plugins/me.lua index e692782..67afd38 100644 --- a/otouto/plugins/me.lua +++ b/otouto/plugins/me.lua @@ -9,33 +9,59 @@ function me:init(config) end function me:action(msg, config) - - local userdata = self.database.userdata[tostring(msg.from.id)] or {} - + local user if msg.from.id == config.admin then if msg.reply_to_message then - userdata = self.database.userdata[tostring(msg.reply_to_message.from.id)] + user = msg.reply_to_message.from else local input = utilities.input(msg.text) if input then - local user_id = utilities.id_from_username(self, input) - if user_id then - userdata = self.database.userdata[tostring(user_id)] or {} + if tonumber(input) then + user = self.database.users[input] + if not user then + utilities.send_reply(self, msg, 'Unrecognized ID.') + return + end + elseif input:match('^@') then + user = utilities.resolve_username(self, input) + if not user then + utilities.send_reply(self, msg, 'Unrecognized username.') + return + end + else + utilities.send_reply(self, msg, 'Invalid username or ID.') + return end end end end + user = user or msg.from + local userdata = self.database.userdata[tostring(user.id)] or {} - local output = '' + local data = {} for k,v in pairs(userdata) do - output = output .. '*' .. k .. ':* `' .. tostring(v) .. '`\n' + table.insert(data, string.format( + '<b>%s</b> <code>%s</code>\n', + utilities.html_escape(k), + utilities.html_escape(v) + )) end - if output == '' then + local output + if #data == 0 then output = 'There is no data stored for this user.' + else + output = string.format( + '<b>%s</b> <code>[%s]</code><b>:</b>\n', + utilities.html_escape(utilities.build_name( + user.first_name, + user.last_name + )), + user.id + ) .. table.concat(data) end - utilities.send_message(self, msg.chat.id, output, true, nil, true) + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end diff --git a/otouto/plugins/patterns.lua b/otouto/plugins/patterns.lua index bc44b60..7480cf2 100644 --- a/otouto/plugins/patterns.lua +++ b/otouto/plugins/patterns.lua @@ -1,10 +1,18 @@ -local patterns = {} - local utilities = require('otouto.utilities') -patterns.triggers = { - '^/?s/.-/.-$' -} +local patterns = {} + +patterns.command = 's/<pattern>/<substitution>' +patterns.help_word = 'sed' +patterns.doc = [[ +s/<pattern>/<substitution> +Replace all matches for the given pattern. +Uses Lua patterns. + ]] + +function patterns:init(config) + patterns.triggers = { config.cmd_pat .. '?s/.-/.-$' } +end function patterns:action(msg) if not msg.reply_to_message then return true end @@ -24,8 +32,8 @@ function patterns:action(msg) if res == false then utilities.send_reply(self, msg, 'Malformed pattern!') else - output = output:sub(1, 4000) - output = '*Did you mean:*\n"' .. utilities.md_escape(utilities.trim(output)) .. '"' + output = utilities.trim(output:sub(1, 4000)) + output = utilities.style.enquote('Did you mean', output) utilities.send_reply(self, msg.reply_to_message, output, true) end end diff --git a/otouto/plugins/pokedex.lua b/otouto/plugins/pokedex.lua index eb30c8e..8942e60 100644 --- a/otouto/plugins/pokedex.lua +++ b/otouto/plugins/pokedex.lua @@ -11,23 +11,20 @@ function pokedex:init(config) pokedex.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pokedex', true):t('dex', true).table pokedex.doc = config.cmd_pat .. [[pokedex <query> Returns a Pokedex entry from pokeapi.co. +Queries must be a number of the name of a Pokémon. Alias: ]] .. config.cmd_pat .. 'dex' end function pokedex:action(msg, config) - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) - - local input = utilities.input(msg.text_lower) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, pokedex.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, pokedex.doc, true) + return end + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) + local url = 'http://pokeapi.co' local dex_url = url .. '/api/v1/pokemon/' .. input @@ -39,6 +36,11 @@ function pokedex:action(msg, config) local dex_jdat = JSON.decode(dex_jstr) + if not dex_jdat.descriptions or not dex_jdat.descriptions[1] then + utilities.send_reply(self, msg, config.errors.results) + return + end + local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri local desc_jstr, _ = HTTP.request(desc_url) if res ~= 200 then diff --git a/otouto/plugins/pokego-calculator.lua b/otouto/plugins/pokego-calculator.lua index 5c3e289..1690edd 100644 --- a/otouto/plugins/pokego-calculator.lua +++ b/otouto/plugins/pokego-calculator.lua @@ -97,8 +97,12 @@ function pgc:action(msg) if egg_count < 1 then recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.' else - recommendation = 'Use %s lucky egg(s) for %s evolutions.' - recommendation = recommendation:format(egg_count, egg_count*60) + recommendation = string.format( + 'Use %s lucky egg%s for %s evolutions.', + egg_count, + egg_count == 1 and '' or 's', + egg_count * 60 + ) end s = s:format(total_evolutions, recommendation) output = output .. s diff --git a/otouto/plugins/pokemon-go.lua b/otouto/plugins/pokemon-go.lua index a122a6d..51cc909 100644 --- a/otouto/plugins/pokemon-go.lua +++ b/otouto/plugins/pokemon-go.lua @@ -8,7 +8,8 @@ pokemon_go.command = 'pokego <team>' function pokemon_go:init(config) pokemon_go.triggers = utilities.triggers(self.info.username, config.cmd_pat) :t('pokego', true):t('pokégo', true) - :t('pokemongo', true):t('pokémongo', true).table + :t('pokemongo', true):t('pokémongo', true) + :t('pogo', true):t('mongo', true).table pokemon_go.doc = config.cmd_pat .. [[pokego <team> Set your Pokémon Go team for statistical purposes. The team must be valid, and can be referred to by name or color (or the first letter of either). Giving no team name will show statistics.]] local db = self.database.pokemon_go diff --git a/otouto/plugins/preview.lua b/otouto/plugins/preview.lua index ecb7f66..802a0f0 100644 --- a/otouto/plugins/preview.lua +++ b/otouto/plugins/preview.lua @@ -12,10 +12,9 @@ end function preview:action(msg) - local input = utilities.input(msg.text) - + local input = utilities.input_from_msg(msg) if not input then - utilities.send_message(self, msg.chat.id, preview.doc, true, nil, true) + utilities.send_reply(self, msg, preview.doc, true) return end @@ -36,8 +35,8 @@ function preview:action(msg) end -- Invisible zero-width, non-joiner. - local output = '[​](' .. input .. ')' - utilities.send_message(self, msg.chat.id, output, false, nil, true) + local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>' + utilities.send_message(self, msg.chat.id, output, false, nil, 'html') end diff --git a/otouto/plugins/reactions.lua b/otouto/plugins/reactions.lua index ed4ddd6..d814f93 100644 --- a/otouto/plugins/reactions.lua +++ b/otouto/plugins/reactions.lua @@ -13,16 +13,6 @@ local utilities = require('otouto.utilities') reactions.command = 'reactions' reactions.doc = 'Returns a list of "reaction" emoticon commands.' -local mapping = { - ['shrug'] = '¯\\_(ツ)_/¯', - ['lenny'] = '( ͡° ͜ʖ ͡°)', - ['flip'] = '(╯°□°)╯︵ ┻━┻', - ['homo'] = '┌(┌ ^o^)┐', - ['look'] = 'ಠ_ಠ', - ['shots?'] = 'SHOTS FIRED', - ['facepalm'] = '(-‸ლ)' -} - local help function reactions:init(config) @@ -30,8 +20,8 @@ function reactions:init(config) help = 'Reactions:\n' reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table local username = self.info.username:lower() - for trigger,reaction in pairs(mapping) do - help = help .. '• ' .. config.cmd_pat .. trigger:gsub('.%?', '') .. ': ' .. reaction .. '\n' + for trigger,reaction in pairs(config.reactions) do + help = help .. '• ' .. config.cmd_pat .. trigger .. ': ' .. reaction .. '\n' table.insert(reactions.triggers, '^'..config.cmd_pat..trigger) table.insert(reactions.triggers, '^'..config.cmd_pat..trigger..'@'..username) table.insert(reactions.triggers, config.cmd_pat..trigger..'$') @@ -48,7 +38,7 @@ function reactions:action(msg, config) utilities.send_message(self, msg.chat.id, help) return end - for trigger,reaction in pairs(mapping) do + for trigger,reaction in pairs(config.reactions) do if string.match(msg.text_lower, config.cmd_pat..trigger) then utilities.send_message(self, msg.chat.id, reaction) return diff --git a/otouto/plugins/remind.lua b/otouto/plugins/remind.lua index 7722e9c..a9fd7d9 100644 --- a/otouto/plugins/remind.lua +++ b/otouto/plugins/remind.lua @@ -8,87 +8,83 @@ function remind:init(config) self.database.reminders = self.database.reminders or {} remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table - remind.doc = config.cmd_pat .. 'remind <duration> <message> \nRepeats a message after a duration of time, in minutes.' + + config.remind = config.remind or {} + setmetatable(config.remind, { __index = function() return 1000 end }) + + remind.doc = config.cmd_pat .. [[remind <duration> <message> +Repeats a message after a duration of time, in minutes. +The maximum length of a reminder is %s characters. The maximum duration of a timer is %s minutes. The maximum number of reminders for a group is %s. The maximum number of reminders in private is %s.]] + remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private) end -function remind:action(msg) - -- Ensure there are arguments. If not, send doc. +function remind:action(msg, config) local input = utilities.input(msg.text) if not input then - utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) + utilities.send_reply(self, msg, remind.doc, true) return end - -- Ensure first arg is a number. If not, send doc. - local duration = utilities.get_word(input, 1) - if not tonumber(duration) then - utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) + + local duration = tonumber(utilities.get_word(input, 1)) + if not duration then + utilities.send_reply(self, msg, remind.doc, true) return end - -- Duration must be between one minute and one year (approximately). - duration = tonumber(duration) + if duration < 1 then duration = 1 - elseif duration > 526000 then - duration = 526000 + elseif duration > config.remind.max_duration then + duration = config.remind.max_duration end - -- Ensure there is a second arg. local message = utilities.input(input) if not message then - utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) + utilities.send_reply(self, msg, remind.doc, true) return end + + if #message > config.remind.max_length then + utilities.send_reply(self, msg, 'The maximum length of reminders is ' .. config.remind.max_length .. '.') + return + end + local chat_id_str = tostring(msg.chat.id) - -- Make a database entry for the group/user if one does not exist. + local output self.database.reminders[chat_id_str] = self.database.reminders[chat_id_str] or {} - -- Limit group reminders to 10 and private reminders to 50. - if msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) > 9 then - utilities.send_reply(self, msg, 'Sorry, this group already has ten reminders.') - return - elseif msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) > 49 then - utilities.send_reply(msg, 'Sorry, you already have fifty reminders.') - return - end - -- Put together the reminder with the expiration, message, and message to reply to. - local reminder = { - time = os.time() + duration * 60, - message = message - } - table.insert(self.database.reminders[chat_id_str], reminder) - local output = 'I will remind you in ' .. duration - if duration == 1 then - output = output .. ' minute!' + if msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_private then + output = 'Sorry, you already have the maximum number of reminders.' + elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then + output = 'Sorry, this group already has the maximum number of reminders.' else - output = output .. ' minutes!' + table.insert(self.database.reminders[chat_id_str], { + time = os.time() + (duration * 60), + message = message + }) + output = string.format( + 'I will remind you in %s minute%s!', + duration, + duration == 1 and '' or 's' + ) end - utilities.send_reply(self, msg, output) + utilities.send_reply(self, msg, output, true) end -function remind:cron() +function remind:cron(config) local time = os.time() -- Iterate over the group entries in the reminders database. for chat_id, group in pairs(self.database.reminders) do - local new_group = {} -- Iterate over each reminder. - for _, reminder in ipairs(group) do + for k, reminder in pairs(group) do -- If the reminder is past-due, send it and nullify it. -- Otherwise, add it to the replacement table. if time > reminder.time then - local output = '*Reminder:*\n"' .. utilities.md_escape(reminder.message) .. '"' + local output = utilities.style.enquote('Reminder', reminder.message) local res = utilities.send_message(self, chat_id, output, true, nil, true) - -- If the message fails to send, save it for later. - if not res then - table.insert(new_group, reminder) + -- If the message fails to send, save it for later (if enabled in config). + if res or not config.remind.persist then + group[k] = nil end - else - table.insert(new_group, reminder) end end - -- Nullify the original table and replace it with the new one. - self.database.reminders[chat_id] = new_group - -- Nullify the table if it is empty. - if #new_group == 0 then - self.database.reminders[chat_id] = nil - end end end diff --git a/otouto/plugins/shell.lua b/otouto/plugins/shell.lua index 47b6adb..66a42b9 100644 --- a/otouto/plugins/shell.lua +++ b/otouto/plugins/shell.lua @@ -20,7 +20,9 @@ function shell:action(msg, config) return end - local output = io.popen(input):read('*all') + local f = io.popen(input) + local output = f:read('*all') + f:close() if output:len() == 0 then output = 'Done!' else diff --git a/otouto/plugins/shout.lua b/otouto/plugins/shout.lua index 105c166..1ea7bf0 100644 --- a/otouto/plugins/shout.lua +++ b/otouto/plugins/shout.lua @@ -3,6 +3,7 @@ local shout = {} local utilities = require('otouto.utilities') shout.command = 'shout <text>' +local utf8 = '('..utilities.char.utf_8..'*)' function shout:init(config) shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table @@ -11,22 +12,19 @@ end function shout:action(msg) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and #msg.reply_to_message.text > 0 then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, shout.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, shout.doc, true) + return end + input = utilities.trim(input) input = input:upper() local output = '' local inc = 0 local ilen = 0 - for match in input:gmatch(utilities.char.utf_8) do + for match in input:gmatch(utf8) do if ilen < 20 then ilen = ilen + 1 output = output .. match .. ' ' @@ -34,7 +32,7 @@ function shout:action(msg) end ilen = 0 output = output .. '\n' - for match in input:sub(2):gmatch(utilities.char.utf_8) do + for match in input:sub(2):gmatch(utf8) do if ilen < 19 then local spacing = '' for _ = 1, inc do diff --git a/otouto/plugins/slap.lua b/otouto/plugins/slap.lua index 33e0bbc..b3cbf7c 100644 --- a/otouto/plugins/slap.lua +++ b/otouto/plugins/slap.lua @@ -109,7 +109,21 @@ local slaps = { function slap:action(msg) local input = utilities.input(msg.text) local victor_id = msg.from.id - local victim_id = utilities.id_from_message(self, msg) + local victim_id + if msg.reply_to_message then + victim_id = msg.reply_to_message.from.id + else + if input then + if tonumber(input) then + victim_id = tonumber(input) + elseif input:match('^@') then + local t = utilities.resolve_username(self, input) + if t then + victim_id = t.id + end + end + end + end -- IDs if victim_id then if victim_id == victor_id then diff --git a/otouto/plugins/starwars-crawl.lua b/otouto/plugins/starwars-crawl.lua new file mode 100644 index 0000000..ed4594c --- /dev/null +++ b/otouto/plugins/starwars-crawl.lua @@ -0,0 +1,78 @@ + -- Based on a plugin by matthewhesketh. + +local HTTP = require('socket.http') +local JSON = require('dkjson') +local bindings = require('otouto.bindings') +local utilities = require('otouto.utilities') + +local starwars = {} + +function starwars:init(config) + starwars.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('starwars', true):t('sw', true).table + starwars.doc = config.cmd_pat .. [[starwars <query> +Returns the opening crawl from the specified Star Wars film. +Alias: ]] .. config.cmd_pat .. 'sw' + starwars.command = 'starwars <query>' + starwars.base_url = 'http://swapi.co/api/films/' +end + +local films_by_number = { + ['phantom menace'] = 4, + ['attack of the clones'] = 5, + ['revenge of the sith'] = 6, + ['new hope'] = 1, + ['empire strikes back'] = 2, + ['return of the jedi'] = 3, + ['force awakens'] = 7 +} + +local corrected_numbers = { + 4, + 5, + 6, + 1, + 2, + 3, + 7 +} + +function starwars:action(msg, config) + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, starwars.doc, true) + return + end + + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) + + local film + if tonumber(input) then + input = tonumber(input) + film = corrected_numbers[input] or input + else + for title, number in pairs(films_by_number) do + if string.match(input, title) then + film = number + break + end + end + end + + if not film then + utilities.send_reply(self, msg, config.errors.results) + return + end + + local url = starwars.base_url .. film + local jstr, code = HTTP.request(url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + + local output = '*' .. JSON.decode(jstr).opening_crawl .. '*' + utilities.send_message(self, msg.chat.id, output, true, nil, true) +end + +return starwars diff --git a/otouto/plugins/time.lua b/otouto/plugins/time.lua index edc43a7..e028428 100644 --- a/otouto/plugins/time.lua +++ b/otouto/plugins/time.lua @@ -5,6 +5,7 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') time.command = 'time <location>' +time.base_url = 'https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s×tamp=%s' function time:init(config) time.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('time', true).table @@ -13,15 +14,10 @@ Returns the time, date, and timezone for the given location.]] end function time:action(msg, config) - - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, time.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, time.doc, true) + return end local coords = utilities.get_coords(input, config) @@ -31,30 +27,33 @@ function time:action(msg, config) end local now = os.time() - local utc = os.time(os.date("!*t", now)) - - local url = 'https://maps.googleapis.com/maps/api/timezone/json?location=' .. coords.lat ..','.. coords.lon .. '×tamp='..utc - - local jstr, res = HTTPS.request(url) - if res ~= 200 then + local utc = os.time(os.date('!*t', now)) + local url = time.base_url:format(coords.lat, coords.lon, utc) + local jstr, code = HTTPS.request(url) + if code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - local jdat = JSON.decode(jstr) + local data = JSON.decode(jstr) + if data.status == 'ZERO_RESULTS' then + utilities.send_reply(self, msg, config.errors.results) + return + end - local timestamp = now + jdat.rawOffset + jdat.dstOffset - local utcoff = (jdat.rawOffset + jdat.dstOffset) / 3600 + local timestamp = now + data.rawOffset + data.dstOffset + local utcoff = (data.rawOffset + data.dstOffset) / 3600 if utcoff == math.abs(utcoff) then - utcoff = '+'.. utilities.pretty_float(utcoff) + utcoff = '+' .. utilities.pretty_float(utcoff) else utcoff = utilities.pretty_float(utcoff) end - local output = os.date('!%I:%M %p\n', timestamp) .. os.date('!%A, %B %d, %Y\n', timestamp) .. jdat.timeZoneName .. ' (UTC' .. utcoff .. ')' - output = '```\n' .. output .. '\n```' - + local output = string.format('```\n%s\n%s (UTC%s)\n```', + os.date('!%I:%M %p\n%A, %B %d, %Y', timestamp), + data.timeZoneName, + utcoff + ) utilities.send_reply(self, msg, output, true) - end return time diff --git a/otouto/plugins/translate.lua b/otouto/plugins/translate.lua index 2ab0ee0..5576d03 100644 --- a/otouto/plugins/translate.lua +++ b/otouto/plugins/translate.lua @@ -8,42 +8,38 @@ local utilities = require('otouto.utilities') translate.command = 'translate [text]' function translate:init(config) - translate.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('translate', true):t('tl', true).table + assert(config.yandex_key, + 'translate.lua requires a Yandex translate API key from http://tech.yandex.com/keys/get.' + ) + + translate.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('translate', true):t('tl', true).table translate.doc = config.cmd_pat .. [[translate [text] Translates input or the replied-to message into the bot's language.]] + translate.base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=%s' end function translate:action(msg, config) - - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, translate.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, translate.doc, true) + return end - local url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=' .. URL.escape(input) - - local str, res = HTTPS.request(url) - if res ~= 200 then + local url = translate.base_url:format(URL.escape(input)) + local jstr, code = HTTPS.request(url) + if code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - local jdat = JSON.decode(str) - if jdat.code ~= 200 then + local data = JSON.decode(jstr) + if data.code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - local output = jdat.text[1] - output = '*Translation:*\n"' .. utilities.md_escape(output) .. '"' - - utilities.send_reply(self, msg.reply_to_message or msg, output, true) - + utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true) end return translate diff --git a/otouto/plugins/urbandictionary.lua b/otouto/plugins/urbandictionary.lua index 06129e3..9a2cfcf 100644 --- a/otouto/plugins/urbandictionary.lua +++ b/otouto/plugins/urbandictionary.lua @@ -6,50 +6,45 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') urbandictionary.command = 'urbandictionary <query>' +urbandictionary.base_url = 'http://api.urbandictionary.com/v0/define?term=' function urbandictionary:init(config) urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat) :t('urbandictionary', true):t('ud', true):t('urban', true).table - urbandictionary.doc = config.cmd_pat .. [[urbandictionary <query> + urbandictionary.doc = [[ +/urbandictionary <query> Returns a definition from Urban Dictionary. -Aliases: ]] .. config.cmd_pat .. 'ud, ' .. config.cmd_pat .. 'urban' +Aliases: /ud, /urban + ]] + urbandictionary.doc = urbandictionary.doc:gsub('/', config.cmd_pat) end function urbandictionary:action(msg, config) - - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, urbandictionary.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, urbandictionary.doc, true) + return end - local url = 'http://api.urbandictionary.com/v0/define?term=' .. URL.escape(input) - - local jstr, res = HTTP.request(url) - if res ~= 200 then + local url = urbandictionary.base_url .. URL.escape(input) + local jstr, code = HTTP.request(url) + if code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - local jdat = JSON.decode(jstr) - if jdat.result_type == "no_results" then - utilities.send_reply(self, msg, config.errors.results) - return + local data = JSON.decode(jstr) + local output + if data.result_type == 'no_results' then + output = config.errors.results + else + output = string.format('*%s*\n\n%s\n\n_%s_', + data.list[1].word:gsub('*', '*\\**'), + utilities.trim(utilities.md_escape(data.list[1].definition)), + utilities.trim((data.list[1].example or '')):gsub('_', '_\\__') + ) end - - local output = '*' .. jdat.list[1].word .. '*\n\n' .. utilities.trim(jdat.list[1].definition) - if string.len(jdat.list[1].example) > 0 then - output = output .. '_\n\n' .. utilities.trim(jdat.list[1].example) .. '_' - end - - output = output:gsub('%[', ''):gsub('%]', '') - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - + utilities.send_reply(self, msg, output, true) end return urbandictionary diff --git a/otouto/plugins/weather.lua b/otouto/plugins/weather.lua index 06a1d31..60b0862 100644 --- a/otouto/plugins/weather.lua +++ b/otouto/plugins/weather.lua @@ -6,11 +6,9 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function weather:init(config) - if not config.owm_api_key then - print('Missing config value: owm_api_key.') - print('weather.lua will not be enabled.') - return - end + assert(config.owm_api_key, + 'weather.lua requires an OpenWeatherMap API key from http://openweathermap.org/API.' + ) weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table weather.doc = config.cmd_pat .. [[weather <location> @@ -21,14 +19,10 @@ weather.command = 'weather <location>' function weather:action(msg, config) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, weather.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, weather.doc, true) + return end local coords = utilities.get_coords(input, config) diff --git a/otouto/plugins/whoami.lua b/otouto/plugins/whoami.lua index fa51b13..946cdf4 100644 --- a/otouto/plugins/whoami.lua +++ b/otouto/plugins/whoami.lua @@ -1,51 +1,59 @@ local whoami = {} local utilities = require('otouto.utilities') +local bindings = require('otouto.bindings') whoami.command = 'whoami' function whoami:init(config) - whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who', true):t('whoami').table + whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who'):t('whoami').table whoami.doc = [[ Returns user and chat info for you or the replied-to message. Alias: ]] .. config.cmd_pat .. 'who' end function whoami:action(msg) - - if msg.reply_to_message then - msg = msg.reply_to_message - end - - local from_name = utilities.build_name(msg.from.first_name, msg.from.last_name) - - local chat_id = math.abs(msg.chat.id) - if chat_id > 1000000000000 then - chat_id = chat_id - 1000000000000 - end - - local user = 'You are @%s, also known as *%s* `[%s]`' - if msg.from.username then - user = user:format(utilities.markdown_escape(msg.from.username), from_name, msg.from.id) - else - user = 'You are *%s* `[%s]`,' - user = user:format(from_name, msg.from.id) - end - - local group = '@%s, also known as *%s* `[%s]`.' - if msg.chat.type == 'private' then - group = group:format(utilities.markdown_escape(self.info.username), self.info.first_name, self.info.id) - elseif msg.chat.username then - group = group:format(utilities.markdown_escape(msg.chat.username), msg.chat.title, chat_id) - else - group = '*%s* `[%s]`.' - group = group:format(msg.chat.title, chat_id) - end - - local output = user .. ', and you are messaging ' .. group - - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - + -- Operate on the replied-to message, if it exists. + msg = msg.reply_to_message or msg + -- If it's a private conversation, bot is chat, unless bot is from. + local chat = msg.from.id == msg.chat.id and self.info or msg.chat + -- Names for the user and group, respectively. HTML-escaped. + local from_name = utilities.html_escape( + utilities.build_name( + msg.from.first_name, + msg.from.last_name + ) + ) + local chat_name = utilities.html_escape( + chat.title + or utilities.build_name(chat.first_name, chat.last_name) + ) + -- "Normalize" a group ID so it's not arbitrarily modified by the bot API. + local chat_id = math.abs(chat.id) + if chat_id > 1000000000000 then chat_id = chat_id - 1000000000000 end + -- Do the thing. + local output = string.format( + 'You are %s <code>[%s]</code>, and you are messaging %s <code>[%s]</code>.', + msg.from.username and string.format( + '@%s, also known as <b>%s</b>', + msg.from.username, + from_name + ) or '<b>' .. from_name .. '</b>', + msg.from.id, + msg.chat.username and string.format( + '@%s, also known as <b>%s</b>', + chat.username, + chat_name + ) or '<b>' .. chat_name .. '</b>', + chat_id + ) + bindings.sendMessage(self, { + chat_id = msg.chat.id, + reply_to_message_id = msg.message_id, + disable_web_page_preview = true, + parse_mode = 'HTML', + text = output + }) end return whoami diff --git a/otouto/plugins/wikipedia.lua b/otouto/plugins/wikipedia.lua index 598798e..54336f5 100644 --- a/otouto/plugins/wikipedia.lua +++ b/otouto/plugins/wikipedia.lua @@ -12,101 +12,79 @@ function wikipedia:init(config) wikipedia.doc = config.cmd_pat .. [[wikipedia <query> Returns an article from Wikipedia. Aliases: ]] .. config.cmd_pat .. 'w, ' .. config.cmd_pat .. 'wiki' -end - -local get_title = function(search) - for _,v in ipairs(search) do - if not v.snippet:match('may refer to:') then - return v.title - end - end - return false + wikipedia.search_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=' + wikipedia.res_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles=' + wikipedia.art_url = 'https://' .. config.lang .. '.wikipedia.org/wiki/' end function wikipedia:action(msg, config) - - -- Get the query. If it's not in the message, check the replied-to message. - -- If those don't exist, send the help text. - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, wikipedia.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, wikipedia.doc, true) + return end - -- This kinda sucks, but whatever. - input = input:gsub('#', ' sharp') - - -- Disclaimer: These variables will be reused. - local jstr, res, jdat - - -- All pretty standard from here. - local search_url = 'https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=' - - jstr, res = HTTPS.request(search_url .. URL.escape(input)) - if res ~= 200 then + local jstr, code = HTTPS.request(wikipedia.search_url .. URL.escape(input)) + if code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - jdat = JSON.decode(jstr) - if jdat.query.searchinfo.totalhits == 0 then + local data = JSON.decode(jstr) + if data.query.searchinfo.totalhits == 0 then utilities.send_reply(self, msg, config.errors.results) return end - local title = get_title(jdat.query.search) + local title + for _, v in ipairs(data.query.search) do + if not v.snippet:match('may refer to:') then + title = v.title + break + end + end if not title then utilities.send_reply(self, msg, config.errors.results) return end - local res_url = 'https://en.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles=' - - jstr, res = HTTPS.request(res_url .. URL.escape(title)) - if res ~= 200 then + local res_jstr, res_code = HTTPS.request(wikipedia.res_url .. URL.escape(title)) + if res_code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) return end - local _ - local text = JSON.decode(jstr).query.pages - _, text = next(text) + local _, text = next(JSON.decode(res_jstr).query.pages) if not text then utilities.send_reply(self, msg, config.errors.results) return - else - text = text.extract end - -- Remove needless bits from the article, take only the first paragraph. - text = text:gsub('</?.->', '') + text = text.extract + -- Remove crap and take only the first paragraph. + text = text:gsub('</?.->', ''):gsub('%[.+%]', '') local l = text:find('\n') if l then text = text:sub(1, l-1) end - - -- This block can be annoying to read. - -- We use the initial title to make the url for later use. Then we remove - -- the extra bits that won't be in the article. We determine whether the - -- first part of the text is the title, and if so, we embolden that. - -- Otherwise, we prepend the text with a bold title. Then we append a "Read - -- More" link. - local url = 'https://en.wikipedia.org/wiki/' .. URL.escape(title) - title = title:gsub('%(.+%)', '') - local output - if string.match(text:sub(1, title:len()), title) then - output = '*' .. title .. '*' .. text:sub(title:len()+1) + local url = wikipedia.art_url .. URL.escape(title) + title = utilities.html_escape(title) + -- If the beginning of the article is the title, embolden that. + -- Otherwise, we'll add a title in bold. + local short_title = title:gsub('%(.+%)', '') + local combined_text, count = text:gsub('^'..short_title, '<b>'..short_title..'</b>') + local body + if count == 1 then + body = combined_text else - output = '*' .. title:gsub('%(.+%)', '') .. '*\n' .. text:gsub('%[.+%]','') + body = '<b>' .. title .. '</b>\n' .. text end - output = output .. '\n[Read more.](' .. url:gsub('%)', '\\)') .. ')' - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - + local output = string.format( + '%s\n<a href="%s">Read more.</a>', + body, + utilities.html_escape(url) + ) + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end return wikipedia diff --git a/otouto/plugins/xkcd.lua b/otouto/plugins/xkcd.lua index cc7c166..a2484a2 100644 --- a/otouto/plugins/xkcd.lua +++ b/otouto/plugins/xkcd.lua @@ -5,52 +5,48 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') xkcd.command = 'xkcd [i]' +xkcd.base_url = 'https://xkcd.com/info.0.json' +xkcd.strip_url = 'http://xkcd.com/%s/info.0.json' function xkcd:init(config) xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table xkcd.doc = config.cmd_pat .. [[xkcd [i] Returns the latest xkcd strip and its alt text. If a number is given, returns that number strip. If "r" is passed in place of a number, returns a random strip.]] + local jstr = HTTP.request(xkcd.base_url) + if jstr then + local data = JSON.decode(jstr) + if data then + xkcd.latest = data.num + end + end + xkcd.latest = xkcd.latest or 1700 end function xkcd:action(msg, config) - - local jstr, res = HTTP.request('http://xkcd.com/info.0.json') - if res ~= 200 then + local input = utilities.get_word(msg.text, 2) + if input == 'r' then + input = math.random(xkcd.latest) + elseif tonumber(input) then + input = tonumber(input) + else + input = xkcd.latest + end + local url = xkcd.strip_url:format(input) + local jstr, code = HTTP.request(url) + if code == 404 then + utilities.send_reply(self, msg, config.errors.results) + elseif code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) - return + else + local data = JSON.decode(jstr) + local output = string.format('*%s (*[%s](%s)*)*\n_%s_', + data.safe_title:gsub('*', '*\\**'), + data.num, + data.img, + data.alt:gsub('_', '_\\__') + ) + utilities.send_message(self, msg.chat.id, output, false, nil, true) end - local latest = JSON.decode(jstr).num - local strip_num = latest - - local input = utilities.input(msg.text) - if input then - if input == '404' then - utilities.send_message(self, msg.chat.id, '*404*\nNot found.', false, nil, true) - return - elseif tonumber(input) then - if tonumber(input) > latest then - strip_num = latest - else - strip_num = input - end - elseif input == 'r' then - strip_num = math.random(latest) - end - end - - local res_url = 'http://xkcd.com/' .. strip_num .. '/info.0.json' - - jstr, res = HTTP.request(res_url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local jdat = JSON.decode(jstr) - - local output = '*' .. jdat.safe_title .. ' (*[' .. jdat.num .. '](' .. jdat.img .. ')*)*\n_' .. jdat.alt:gsub('_', '\\_') .. '_' - - utilities.send_message(self, msg.chat.id, output, false, nil, true) - end return xkcd diff --git a/otouto/plugins/youtube.lua b/otouto/plugins/youtube.lua index 5ff9108..2de16ae 100644 --- a/otouto/plugins/youtube.lua +++ b/otouto/plugins/youtube.lua @@ -8,11 +8,9 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function youtube:init(config) - if not config.google_api_key then - print('Missing config value: google_api_key.') - print('youtube.lua will not be enabled.') - return - end + assert(config.google_api_key, + 'youtube.lua requires a Google API key from http://console.developers.google.com.' + ) youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table youtube.doc = config.cmd_pat .. [[youtube <query> @@ -24,14 +22,10 @@ youtube.command = 'youtube <query>' function youtube:action(msg, config) - local input = utilities.input(msg.text) + local input = utilities.input_from_msg(msg) if not input then - if msg.reply_to_message and msg.reply_to_message.text then - input = msg.reply_to_message.text - else - utilities.send_message(self, msg.chat.id, youtube.doc, true, msg.message_id, true) - return - end + utilities.send_reply(self, msg, youtube.doc, true) + return end local url = 'https://www.googleapis.com/youtube/v3/search?key=' .. config.google_api_key .. '&type=video&part=snippet&maxResults=4&q=' .. URL.escape(input) diff --git a/otouto/utilities.lua b/otouto/utilities.lua index 79016e1..22f5204 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -12,45 +12,38 @@ local bindings = require('otouto.bindings') -- For the sake of ease to new contributors and familiarity to old contributors, -- we'll provide a couple of aliases to real bindings here. + -- Edit: To keep things working and allow for HTML messages, you can now pass a + -- string for use_markdown and that will be sent as the parse mode. function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown) + local parse_mode + if type(use_markdown) == 'string' then + parse_mode = use_markdown + elseif use_markdown == true then + parse_mode = 'markdown' + end return bindings.request(self, 'sendMessage', { chat_id = chat_id, text = text, disable_web_page_preview = disable_web_page_preview, reply_to_message_id = reply_to_message_id, - parse_mode = use_markdown and 'Markdown' or nil + parse_mode = parse_mode } ) end function utilities:send_reply(old_msg, text, use_markdown) - return bindings.request(self, 'sendMessage', { - chat_id = old_msg.chat.id, - text = text, - disable_web_page_preview = true, - reply_to_message_id = old_msg.message_id, - parse_mode = use_markdown and 'Markdown' or nil - } ) + return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown) end -- get the indexed word in a string function utilities.get_word(s, i) s = s or '' i = i or 1 - local t = {} + local n = 0 for w in s:gmatch('%g+') do - table.insert(t, w) + n = n + 1 + if n == i then return w end end - return t[i] or false -end - - -- Like get_word(), but better. - -- Returns the actual index. -function utilities.index(s) - local t = {} - for w in s:gmatch('%g+') do - table.insert(t, w) - end - return t + return false end -- Returns the string after the first space. @@ -61,6 +54,10 @@ function utilities.input(s) return s:sub(s:find(' ')+1) end +function utilities.input_from_msg(msg) + return utilities.input(msg.text) or (msg.reply_to_message and #msg.reply_to_message.text > 0 and msg.reply_to_message.text) or false +end + -- Calculates the length of the given string as UTF-8 characters function utilities.utf8_len(s) local chars = 0 @@ -82,13 +79,13 @@ end -- Loads a JSON file as a table. function utilities.load_data(filename) local f = io.open(filename) - if not f then + if f then + local s = f:read('*all') + f:close() + return JSON.decode(s) + else return {} end - local s = f:read('*all') - f:close() - local data = JSON.decode(s) - return data end -- Saves a table to a JSON file. @@ -153,85 +150,14 @@ function utilities:resolve_username(input) end end - -- Simpler than above function; only returns an ID. - -- Returns nil if no ID is available. -function utilities:id_from_username(input) - input = input:gsub('^@', '') - for _, user in pairs(self.database.users) do - if user.username and user.username:lower() == input:lower() then - return user.id - end - end -end - - -- Simpler than below function; only returns an ID. - -- Returns nil if no ID is available. -function utilities:id_from_message(msg) - if msg.reply_to_message then - return msg.reply_to_message.from.id - else - local input = utilities.input(msg.text) - if input then - if tonumber(input) then - return tonumber(input) - elseif input:match('^@') then - return utilities.id_from_username(self, input) - end - end - end -end - -function utilities:user_from_message(msg, no_extra) - - local input = utilities.input(msg.text_lower) - local target = {} - if msg.reply_to_message then - for k,v in pairs(self.database.users[msg.reply_to_message.from.id_str]) do - target[k] = v - end - elseif input and tonumber(input) then - target.id = tonumber(input) - if self.database.users[input] then - for k,v in pairs(self.database.users[input]) do - target[k] = v - end - end - elseif input and input:match('^@') then - local uname = input:gsub('^@', '') - for _,v in pairs(self.database.users) do - if v.username and uname == v.username:lower() then - for key, val in pairs(v) do - target[key] = val - end - end - end - if not target.id then - target.err = 'Sorry, I don\'t recognize that username.' - end - else - target.err = 'Please specify a user via reply, ID, or username.' - end - - if not no_extra then - if target.id then - target.id_str = tostring(target.id) - end - if not target.first_name then - target.first_name = 'User' - end - target.name = utilities.build_name(target.first_name, target.last_name) - end - - return target - -end - function utilities:handle_exception(err, message, config) - - if not err then err = '' end - - local output = '\n[' .. os.date('%F %T', os.time()) .. ']\n' .. self.info.username .. ': ' .. err .. '\n' .. message .. '\n' - + local output = string.format( + '\n[%s]\n%s: %s\n%s\n', + os.date('%F %T'), + self.info.username, + err or '', + message + ) if config.log_chat then output = '```' .. output .. '```' utilities.send_message(self, config.log_chat, output, true, nil, true) @@ -265,16 +191,15 @@ function utilities.download_file(url, filename) return filename end -function utilities.markdown_escape(text) - text = text:gsub('_', '\\_') - text = text:gsub('%[', '\\[') - text = text:gsub('%]', '\\]') - text = text:gsub('%*', '\\*') - text = text:gsub('`', '\\`') - return text +function utilities.md_escape(text) + return text:gsub('_', '\\_') + :gsub('%[', '\\['):gsub('%]', '\\]') + :gsub('%*', '\\*'):gsub('`', '\\`') end -utilities.md_escape = utilities.markdown_escape +function utilities.html_escape(text) + return text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>') +end utilities.triggers_meta = {} utilities.triggers_meta.__index = utilities.triggers_meta @@ -320,7 +245,7 @@ utilities.char = { rtl_override = '‮', rtl_mark = '‏', em_dash = '—', - utf_8 = '([%z\1-\127\194-\244][\128-\191]*)', + utf_8 = '[%z\1-\127\194-\244][\128-\191]', } utilities.set_meta = {} @@ -354,4 +279,11 @@ function utilities.set_meta:__len() return self.__count end + -- Styling functions to keep things consistent and easily changeable across plugins. + -- More to be added. +utilities.style = {} +utilities.style.enquote = function(title, body) + return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"' +end + return utilities diff --git a/tg-launch.sh b/tg-launch.sh index 03c6205..570e0ba 100755 --- a/tg-launch.sh +++ b/tg-launch.sh @@ -1,11 +1,11 @@ #!/bin/sh # Launch tg listening on the default port (change this if you've changed it in -# config.lua), delete state file after stop, wait two seconds, and restart. +# config.lua), delete state file after stop, wait five seconds, and restart. while true; do tg/bin/telegram-cli -P 4567 -E - rm ~/.telegram-cli/state + [ -f ~/.telegram-cli/state ] && rm ~/.telegram-cli/state echo 'tg has stopped. ^C to exit.' sleep 5s done From 6fbd718af0e33dd5b985fc0f6f4582619574079f Mon Sep 17 00:00:00 2001 From: topkecleon <andwag@outlook.com> Date: Sat, 13 Aug 2016 22:43:23 -0400 Subject: [PATCH 2/5] whoop --- README.md | 2 +- otouto/bot.lua | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f21c149..c759bb6 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ Additionally, antiflood can be configured to automatically ban a user after he h | `remind.lua` | /remind <duration> <message> | Reminds a user of something after a duration of minutes. | | `channel.lua` | /ch <channel> \n <message> | Sends a markdown-enabled message to a channel. | | `isup.lua` | /isup <url> | Returns the status of a website. | -| `starwars-crawl.lua` | /sw <title | number> | Returns the opening crawl from the specified Star Wars film. | /sw | +| `starwars-crawl.lua` | /sw <title ¦ number> | Returns the opening crawl from the specified Star Wars film. | /sw | | `chuckfact.lua` | /chuck | Returns a fact about Chuck Norris. | /cn | | `catfact.lua` | /catfact | Returns a fact about cats. | diff --git a/otouto/bot.lua b/otouto/bot.lua index 5ea5241..1f851e6 100644 --- a/otouto/bot.lua +++ b/otouto/bot.lua @@ -55,14 +55,13 @@ function bot:init(config) -- and messages from blacklisted users also go into self.panoptic_plugins. self.plugins = {} self.panoptic_plugins = {} - local t = {} -- Petty pseudo-optimization. for _, pname in ipairs(config.plugins) do local plugin = require('otouto.plugins.'..pname) table.insert(self.plugins, plugin) if plugin.init then plugin.init(self, config) end if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end - if not plugin.triggers then plugin.triggers = t end + if not plugin.triggers then plugin.triggers = {} end end print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')') From 148d4b0dc5d76d94b05c3c1a4631fe7e69442075 Mon Sep 17 00:00:00 2001 From: topkecleon <andwag@outlook.com> Date: Sat, 13 Aug 2016 22:46:18 -0400 Subject: [PATCH 3/5] tab -> 4 spaces --- .editorconfig | 3 +- README.md | 62 +- config.lua | 286 +-- launch.sh | 6 +- otouto/bindings.lua | 104 +- otouto/bot.lua | 312 +-- otouto/drua-tg.lua | 202 +- otouto/plugins/about.lua | 8 +- otouto/plugins/administration.lua | 2854 +++++++++++++------------- otouto/plugins/apod.lua | 64 +- otouto/plugins/bandersnatch.lua | 18 +- otouto/plugins/bible.lua | 46 +- otouto/plugins/bing.lua | 96 +- otouto/plugins/blacklist.lua | 166 +- otouto/plugins/calc.lua | 22 +- otouto/plugins/catfact.lua | 26 +- otouto/plugins/cats.lua | 34 +- otouto/plugins/channel.lua | 76 +- otouto/plugins/chuckfact.lua | 26 +- otouto/plugins/cleverbot.lua | 42 +- otouto/plugins/commit.lua | 20 +- otouto/plugins/control.lua | 80 +- otouto/plugins/currency.lua | 62 +- otouto/plugins/dice.lua | 66 +- otouto/plugins/dilbert.lua | 46 +- otouto/plugins/echo.lua | 28 +- otouto/plugins/eightball.lua | 68 +- otouto/plugins/fortune.lua | 22 +- otouto/plugins/gImages.lua | 70 +- otouto/plugins/gMaps.lua | 40 +- otouto/plugins/greetings.lua | 40 +- otouto/plugins/hackernews.lua | 110 +- otouto/plugins/hearthstone.lua | 162 +- otouto/plugins/help.lua | 84 +- otouto/plugins/id.lua | 92 +- otouto/plugins/imdb.lua | 46 +- otouto/plugins/isup.lua | 50 +- otouto/plugins/lastfm.lua | 144 +- otouto/plugins/luarun.lua | 92 +- otouto/plugins/me.lua | 106 +- otouto/plugins/nick.lua | 60 +- otouto/plugins/patterns.lua | 46 +- otouto/plugins/ping.lua | 6 +- otouto/plugins/pokedex.lua | 78 +- otouto/plugins/pokego-calculator.lua | 174 +- otouto/plugins/pokemon-go.lua | 2 +- otouto/plugins/preview.lua | 46 +- otouto/plugins/pun.lua | 250 +-- otouto/plugins/reactions.lua | 50 +- otouto/plugins/reddit.lua | 116 +- otouto/plugins/remind.lua | 132 +- otouto/plugins/rmspic.lua | 42 +- otouto/plugins/setandget.lua | 88 +- otouto/plugins/shell.lua | 38 +- otouto/plugins/shout.lua | 66 +- otouto/plugins/slap.lua | 294 +-- otouto/plugins/starwars-crawl.lua | 98 +- otouto/plugins/time.lua | 76 +- otouto/plugins/translate.lua | 48 +- otouto/plugins/urbandictionary.lua | 56 +- otouto/plugins/weather.lua | 60 +- otouto/plugins/whoami.lua | 86 +- otouto/plugins/wikipedia.lua | 132 +- otouto/plugins/xkcd.lua | 68 +- otouto/plugins/youtube.lua | 52 +- otouto/utilities.lua | 282 +-- tg-launch.sh | 8 +- 67 files changed, 4168 insertions(+), 4167 deletions(-) diff --git a/.editorconfig b/.editorconfig index a7715af..90ffed2 100755 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,5 @@ insert_final_newline = true [*.lua] charset = utf-8 -indent_style = tab +indent_style = space +indent_size = 4 diff --git a/README.md b/README.md index c759bb6..337b3e0 100644 --- a/README.md +++ b/README.md @@ -292,26 +292,26 @@ Additionally, any method can be called as a key in the `bindings` table (for exa ``` bindings.request( - self, - 'sendMessage', - { - chat_id = 987654321, - text = 'Quick brown fox.', - reply_to_message_id = 54321, - disable_web_page_preview = false, - parse_method = 'Markdown' - } + self, + 'sendMessage', + { + chat_id = 987654321, + text = 'Quick brown fox.', + reply_to_message_id = 54321, + disable_web_page_preview = false, + parse_method = 'Markdown' + } ) bindings.sendMessage( - self, - { - chat_id = 987654321, - text = 'Quick brown fox.', - reply_to_message_id = 54321, - disable_web_page_preview = false, - parse_method = 'Markdown' - } + self, + { + chat_id = 987654321, + text = 'Quick brown fox.', + reply_to_message_id = 54321, + disable_web_page_preview = false, + parse_method = 'Markdown' + } ) ``` @@ -342,20 +342,20 @@ Alone, the database will have this structure: ``` { - users = { - ["55994550"] = { - id = 55994550, - first_name = "Drew", - username = "topkecleon" - } - }, - userdata = { - ["55994550"] = { - nickname = "Worst coder ever", - lastfm = "topkecleon" - } - }, - version = "3.11" + users = { + ["55994550"] = { + id = 55994550, + first_name = "Drew", + username = "topkecleon" + } + }, + userdata = { + ["55994550"] = { + nickname = "Worst coder ever", + lastfm = "topkecleon" + } + }, + version = "3.11" } ``` diff --git a/config.lua b/config.lua index f18b5c6..ee06ec1 100644 --- a/config.lua +++ b/config.lua @@ -1,159 +1,159 @@ -- For details on configuration values, see README.md#configuration. return { - -- Your authorization token from the botfather. - bot_api_key = nil, - -- Your Telegram ID. - admin = nil, - -- Two-letter language code. - lang = 'en', - -- The channel, group, or user to send error reports to. - -- If this is not set, errors will be printed to the console. - log_chat = nil, - -- The port used to communicate with tg for administration.lua. - -- If you change this, make sure you also modify launch-tg.sh. - cli_port = 4567, - -- The symbol that starts a command. Usually noted as '/' in documentation. - cmd_pat = '/', - -- If drua is used, should a user be blocked when he's blacklisted? - drua_block_on_blacklist = false, - -- The filename of the database. If left nil, defaults to $username.db. - database_name = nil, - -- The block of text returned by /start and /about.. - about_text = [[ + -- Your authorization token from the botfather. + bot_api_key = nil, + -- Your Telegram ID. + admin = nil, + -- Two-letter language code. + lang = 'en', + -- The channel, group, or user to send error reports to. + -- If this is not set, errors will be printed to the console. + log_chat = nil, + -- The port used to communicate with tg for administration.lua. + -- If you change this, make sure you also modify launch-tg.sh. + cli_port = 4567, + -- The symbol that starts a command. Usually noted as '/' in documentation. + cmd_pat = '/', + -- If drua is used, should a user be blocked when he's blacklisted? + drua_block_on_blacklist = false, + -- The filename of the database. If left nil, defaults to $username.db. + database_name = nil, + -- The block of text returned by /start and /about.. + about_text = [[ I am otouto, the plugin-wielding, multipurpose Telegram bot. Send /help to get started. - ]], + ]], - errors = { -- Generic error messages. - generic = 'An unexpected error occurred.', - connection = 'Connection error.', - results = 'No results found.', - argument = 'Invalid argument.', - syntax = 'Invalid syntax.' - }, + errors = { -- Generic error messages. + generic = 'An unexpected error occurred.', + connection = 'Connection error.', + results = 'No results found.', + argument = 'Invalid argument.', + syntax = 'Invalid syntax.' + }, - -- https://datamarket.azure.com/dataset/bing/search - bing_api_key = nil, - -- http://console.developers.google.com - google_api_key = nil, - -- https://cse.google.com/cse - google_cse_key = nil, - -- http://openweathermap.org/appid - owm_api_key = nil, - -- http://last.fm/api - lastfm_api_key = nil, - -- http://api.biblia.com - biblia_api_key = nil, - -- http://thecatapi.com/docs.html - thecatapi_key = nil, - -- http://api.nasa.gov - nasa_api_key = nil, - -- http://tech.yandex.com/keys/get - yandex_key = nil, - -- Interval (in minutes) for hackernews.lua to update. - hackernews_interval = 60, - -- Whether hackernews.lua should update at load/reload. - hackernews_onstart = false, - -- Whether luarun should use serpent instead of dkjson for serialization. - luarun_serpent = false, + -- https://datamarket.azure.com/dataset/bing/search + bing_api_key = nil, + -- http://console.developers.google.com + google_api_key = nil, + -- https://cse.google.com/cse + google_cse_key = nil, + -- http://openweathermap.org/appid + owm_api_key = nil, + -- http://last.fm/api + lastfm_api_key = nil, + -- http://api.biblia.com + biblia_api_key = nil, + -- http://thecatapi.com/docs.html + thecatapi_key = nil, + -- http://api.nasa.gov + nasa_api_key = nil, + -- http://tech.yandex.com/keys/get + yandex_key = nil, + -- Interval (in minutes) for hackernews.lua to update. + hackernews_interval = 60, + -- Whether hackernews.lua should update at load/reload. + hackernews_onstart = false, + -- Whether luarun should use serpent instead of dkjson for serialization. + luarun_serpent = false, - remind = { - persist = true, - max_length = 1000, - max_duration = 526000, - max_reminders_group = 10, - max_reminders_private = 50 - }, + remind = { + persist = true, + max_length = 1000, + max_duration = 526000, + max_reminders_group = 10, + max_reminders_private = 50 + }, - chatter = { - cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=', - connection = 'I don\'t feel like talking right now.', - response = 'I don\'t know what to say to that.' - }, + chatter = { + cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=', + connection = 'I don\'t feel like talking right now.', + response = 'I don\'t know what to say to that.' + }, - greetings = { - ["Hello, #NAME."] = { - "hello", - "hey", - "hi", - "good morning", - "good day", - "good afternoon", - "good evening" - }, - ["Goodbye, #NAME."] = { - "good%-?bye", - "bye", - "later", - "see ya", - "good night" - }, - ["Welcome back, #NAME."] = { - "i'm home", - "i'm back" - }, - ["You're welcome, #NAME."] = { - "thanks", - "thank you" - } - }, + greetings = { + ["Hello, #NAME."] = { + "hello", + "hey", + "hi", + "good morning", + "good day", + "good afternoon", + "good evening" + }, + ["Goodbye, #NAME."] = { + "good%-?bye", + "bye", + "later", + "see ya", + "good night" + }, + ["Welcome back, #NAME."] = { + "i'm home", + "i'm back" + }, + ["You're welcome, #NAME."] = { + "thanks", + "thank you" + } + }, - reactions = { - ['shrug'] = '¯\\_(ツ)_/¯', - ['lenny'] = '( ͡° ͜ʖ ͡°)', - ['flip'] = '(╯°□°)╯︵ ┻━┻', - ['look'] = 'ಠ_ಠ', - ['shots'] = 'SHOTS FIRED', - ['facepalm'] = '(-‸ლ)' - }, + reactions = { + ['shrug'] = '¯\\_(ツ)_/¯', + ['lenny'] = '( ͡° ͜ʖ ͡°)', + ['flip'] = '(╯°□°)╯︵ ┻━┻', + ['look'] = 'ಠ_ಠ', + ['shots'] = 'SHOTS FIRED', + ['facepalm'] = '(-‸ლ)' + }, - administration = { - -- Whether moderators can set a group's message of the day. - moderator_setmotd = false, - -- Default antiflood values. - antiflood = { - text = 5, - voice = 5, - audio = 5, - contact = 5, - photo = 10, - video = 10, - location = 10, - document = 10, - sticker = 20 - } - }, + administration = { + -- Whether moderators can set a group's message of the day. + moderator_setmotd = false, + -- Default antiflood values. + antiflood = { + text = 5, + voice = 5, + audio = 5, + contact = 5, + photo = 10, + video = 10, + location = 10, + document = 10, + sticker = 20 + } + }, - plugins = { -- To enable a plugin, add its name to the list. - 'about', - 'blacklist', - 'calc', - 'cats', - 'commit', - 'control', - 'currency', - 'dice', - 'echo', - 'eightball', - 'gMaps', - 'hackernews', - 'imdb', - 'nick', - 'ping', - 'pun', - 'reddit', - 'shout', - 'slap', - 'time', - 'urbandictionary', - 'whoami', - 'wikipedia', - 'xkcd', - -- Put new plugins above this line. - 'help', - 'greetings' - } + plugins = { -- To enable a plugin, add its name to the list. + 'about', + 'blacklist', + 'calc', + 'cats', + 'commit', + 'control', + 'currency', + 'dice', + 'echo', + 'eightball', + 'gMaps', + 'hackernews', + 'imdb', + 'nick', + 'ping', + 'pun', + 'reddit', + 'shout', + 'slap', + 'time', + 'urbandictionary', + 'whoami', + 'wikipedia', + 'xkcd', + -- Put new plugins above this line. + 'help', + 'greetings' + } } diff --git a/launch.sh b/launch.sh index be8b691..6f6f5f2 100755 --- a/launch.sh +++ b/launch.sh @@ -1,7 +1,7 @@ #!/bin/sh while true; do - lua main.lua - echo 'otouto has stopped. ^C to exit.' - sleep 5s + lua main.lua + echo 'otouto has stopped. ^C to exit.' + sleep 5s done diff --git a/otouto/bindings.lua b/otouto/bindings.lua index c85c43d..4ae3f00 100644 --- a/otouto/bindings.lua +++ b/otouto/bindings.lua @@ -1,10 +1,10 @@ --[[ - bindings.lua (rev. 2016/05/28) - otouto's bindings for the Telegram bot API. - https://core.telegram.org/bots/api - Copyright 2016 topkecleon. Published under the AGPLv3. + bindings.lua (rev. 2016/05/28) + otouto's bindings for the Telegram bot API. + https://core.telegram.org/bots/api + Copyright 2016 topkecleon. Published under the AGPLv3. - See the "Bindings" section of README.md for usage information. + See the "Bindings" section of README.md for usage information. ]]-- local bindings = {} @@ -22,56 +22,56 @@ local MP_ENCODE = require('multipart-post').encode -- response with failure. Returns false and false with a connection error. -- To mimic old/normal behavior, it errs if used with an invalid method. function bindings:request(method, parameters, file) - parameters = parameters or {} - for k,v in pairs(parameters) do - parameters[k] = tostring(v) - end - if file and next(file) ~= nil then - local file_type, file_name = next(file) - local file_file = io.open(file_name, 'r') - local file_data = { - filename = file_name, - data = file_file:read('*a') - } - file_file:close() - parameters[file_type] = file_data - end - if next(parameters) == nil then - parameters = {''} - end - local response = {} - local body, boundary = MP_ENCODE(parameters) - local success, code = HTTPS.request{ - url = self.BASE_URL .. method, - method = 'POST', - headers = { - ["Content-Type"] = "multipart/form-data; boundary=" .. boundary, - ["Content-Length"] = #body, - }, - source = ltn12.source.string(body), - sink = ltn12.sink.table(response) - } - local data = table.concat(response) - if not success or success == 1 then - print(method .. ': Connection error. [' .. code .. ']') - return false, false - else - local result = JSON.decode(data) - if not result then - return false, false - elseif result.ok then - return result - else - assert(result.description ~= 'Method not found', method .. ': Method not found.') - return false, result - end - end + parameters = parameters or {} + for k,v in pairs(parameters) do + parameters[k] = tostring(v) + end + if file and next(file) ~= nil then + local file_type, file_name = next(file) + local file_file = io.open(file_name, 'r') + local file_data = { + filename = file_name, + data = file_file:read('*a') + } + file_file:close() + parameters[file_type] = file_data + end + if next(parameters) == nil then + parameters = {''} + end + local response = {} + local body, boundary = MP_ENCODE(parameters) + local success, code = HTTPS.request{ + url = self.BASE_URL .. method, + method = 'POST', + headers = { + ["Content-Type"] = "multipart/form-data; boundary=" .. boundary, + ["Content-Length"] = #body, + }, + source = ltn12.source.string(body), + sink = ltn12.sink.table(response) + } + local data = table.concat(response) + if not success or success == 1 then + print(method .. ': Connection error. [' .. code .. ']') + return false, false + else + local result = JSON.decode(data) + if not result then + return false, false + elseif result.ok then + return result + else + assert(result.description ~= 'Method not found', method .. ': Method not found.') + return false, result + end + end end function bindings.gen(_, key) - return function(self, params, file) - return bindings.request(self, key, params, file) - end + return function(self, params, file) + return bindings.request(self, key, params, file) + end end setmetatable(bindings, { __index = bindings.gen }) diff --git a/otouto/bot.lua b/otouto/bot.lua index 1f851e6..b37649c 100644 --- a/otouto/bot.lua +++ b/otouto/bot.lua @@ -7,190 +7,190 @@ bot.version = '3.13' -- Function to be run on start and reload. function bot:init(config) - bindings = require('otouto.bindings') - utilities = require('otouto.utilities') + bindings = require('otouto.bindings') + utilities = require('otouto.utilities') - assert( - config.bot_api_key, - 'You did not set your bot token in the config!' - ) - self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' + assert( + config.bot_api_key, + 'You did not set your bot token in the config!' + ) + self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' - -- Fetch bot information. Try until it succeeds. - repeat - print('Fetching bot information...') - self.info = bindings.getMe(self) - until self.info - self.info = self.info.result + -- Fetch bot information. Try until it succeeds. + repeat + print('Fetching bot information...') + self.info = bindings.getMe(self) + until self.info + self.info = self.info.result - -- Load the "database"! ;) - self.database_name = config.database_name or self.info.username .. '.db' - if not self.database then - self.database = utilities.load_data(self.database_name) - end + -- Load the "database"! ;) + self.database_name = config.database_name or self.info.username .. '.db' + if not self.database then + self.database = utilities.load_data(self.database_name) + end - -- Migration code 1.12 -> 1.13 - -- Back to administration global ban list; copy over current blacklist. - if self.database.version ~= '3.13' then - if self.database.administration then - self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {} - utilities.save_data(self.database_name, self.database) - self.database = utilities.load_data(self.database_name) - end - end - -- End migration code. + -- Migration code 1.12 -> 1.13 + -- Back to administration global ban list; copy over current blacklist. + if self.database.version ~= '3.13' then + if self.database.administration then + self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {} + utilities.save_data(self.database_name, self.database) + self.database = utilities.load_data(self.database_name) + end + end + -- End migration code. - -- Table to cache user info (usernames, IDs, etc). - self.database.users = self.database.users or {} - -- Table to store userdata (nicknames, lastfm usernames, etc). - self.database.userdata = self.database.userdata or {} - -- Table to store the IDs of blacklisted users. - self.database.blacklist = self.database.blacklist or {} - -- Save the bot's version in the database to make migration simpler. - self.database.version = bot.version - -- Add updated bot info to the user info cache. - self.database.users[tostring(self.info.id)] = self.info + -- Table to cache user info (usernames, IDs, etc). + self.database.users = self.database.users or {} + -- Table to store userdata (nicknames, lastfm usernames, etc). + self.database.userdata = self.database.userdata or {} + -- Table to store the IDs of blacklisted users. + self.database.blacklist = self.database.blacklist or {} + -- Save the bot's version in the database to make migration simpler. + self.database.version = bot.version + -- Add updated bot info to the user info cache. + self.database.users[tostring(self.info.id)] = self.info - -- All plugins go into self.plugins. Plugins which accept forwarded messages - -- and messages from blacklisted users also go into self.panoptic_plugins. - self.plugins = {} - self.panoptic_plugins = {} - for _, pname in ipairs(config.plugins) do - local plugin = require('otouto.plugins.'..pname) - table.insert(self.plugins, plugin) - if plugin.init then plugin.init(self, config) end - if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end - if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end - if not plugin.triggers then plugin.triggers = {} end - end + -- All plugins go into self.plugins. Plugins which accept forwarded messages + -- and messages from blacklisted users also go into self.panoptic_plugins. + self.plugins = {} + self.panoptic_plugins = {} + for _, pname in ipairs(config.plugins) do + local plugin = require('otouto.plugins.'..pname) + table.insert(self.plugins, plugin) + if plugin.init then plugin.init(self, config) end + if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end + if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end + if not plugin.triggers then plugin.triggers = {} end + end - print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')') + print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')') - -- Set loop variables. - self.last_update = self.last_update or 0 -- Update offset. - self.last_cron = self.last_cron or os.date('%M') -- Last cron job. - self.last_database_save = self.last_database_save or os.date('%H') -- Last db save. - self.is_started = true + -- Set loop variables. + self.last_update = self.last_update or 0 -- Update offset. + self.last_cron = self.last_cron or os.date('%M') -- Last cron job. + self.last_database_save = self.last_database_save or os.date('%H') -- Last db save. + self.is_started = true end -- Function to be run on each new message. function bot:on_msg_receive(msg, config) - -- Do not process old messages. - if msg.date < os.time() - 5 then return end + -- Do not process old messages. + if msg.date < os.time() - 5 then return end - -- plugint is the array of plugins we'll check the message against. - -- If the message is forwarded or from a blacklisted user, the bot will only - -- check against panoptic plugins. - local plugint = self.plugins - local from_id_str = tostring(msg.from.id) + -- plugint is the array of plugins we'll check the message against. + -- If the message is forwarded or from a blacklisted user, the bot will only + -- check against panoptic plugins. + local plugint = self.plugins + local from_id_str = tostring(msg.from.id) - -- Cache user info for those involved. - self.database.users[from_id_str] = msg.from - if msg.reply_to_message then - self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from - elseif msg.forward_from then - -- Forwards only go to panoptic plugins. - plugint = self.panoptic_plugins - self.database.users[tostring(msg.forward_from.id)] = msg.forward_from - elseif msg.new_chat_member then - self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member - elseif msg.left_chat_member then - self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member - end + -- Cache user info for those involved. + self.database.users[from_id_str] = msg.from + if msg.reply_to_message then + self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from + elseif msg.forward_from then + -- Forwards only go to panoptic plugins. + plugint = self.panoptic_plugins + self.database.users[tostring(msg.forward_from.id)] = msg.forward_from + elseif msg.new_chat_member then + self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member + elseif msg.left_chat_member then + self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member + end - -- Messages from blacklisted users only go to panoptic plugins. - if self.database.blacklist[from_id_str] then - plugint = self.panoptic_plugins - end + -- Messages from blacklisted users only go to panoptic plugins. + if self.database.blacklist[from_id_str] then + plugint = self.panoptic_plugins + end - -- If no text, use captions. - msg.text = msg.text or msg.caption or '' - msg.text_lower = msg.text:lower() - if msg.reply_to_message then - msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or '' - end + -- If no text, use captions. + msg.text = msg.text or msg.caption or '' + msg.text_lower = msg.text:lower() + if msg.reply_to_message then + msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or '' + end - -- Support deep linking. - if msg.text:match('^'..config.cmd_pat..'start .+') then - msg.text = config.cmd_pat .. utilities.input(msg.text) - msg.text_lower = msg.text:lower() - end + -- Support deep linking. + if msg.text:match('^'..config.cmd_pat..'start .+') then + msg.text = config.cmd_pat .. utilities.input(msg.text) + msg.text_lower = msg.text:lower() + end - -- If the message is forwarded or comes from a blacklisted yser, + -- If the message is forwarded or comes from a blacklisted yser, - -- Do the thing. - for _, plugin in ipairs(plugint) do - for _, trigger in ipairs(plugin.triggers) do - if string.match(msg.text_lower, trigger) then - local success, result = pcall(function() - return plugin.action(self, msg, config) - end) - if not success then - -- If the plugin has an error message, send it. If it does - -- not, use the generic one specified in config. If it's set - -- to false, do nothing. - if plugin.error then - utilities.send_reply(self, msg, plugin.error) - elseif plugin.error == nil then - utilities.send_reply(self, msg, config.errors.generic) - end - utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) - msg = nil - return - -- Continue if the return value is true. - elseif result ~= true then - msg = nil - return - end - end - end - end - msg = nil + -- Do the thing. + for _, plugin in ipairs(plugint) do + for _, trigger in ipairs(plugin.triggers) do + if string.match(msg.text_lower, trigger) then + local success, result = pcall(function() + return plugin.action(self, msg, config) + end) + if not success then + -- If the plugin has an error message, send it. If it does + -- not, use the generic one specified in config. If it's set + -- to false, do nothing. + if plugin.error then + utilities.send_reply(self, msg, plugin.error) + elseif plugin.error == nil then + utilities.send_reply(self, msg, config.errors.generic) + end + utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) + msg = nil + return + -- Continue if the return value is true. + elseif result ~= true then + msg = nil + return + end + end + end + end + msg = nil end -- main function bot:run(config) - bot.init(self, config) - while self.is_started do - -- Update loop. - local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } ) - if res then - -- Iterate over every new message. - for _,v in ipairs(res.result) do - self.last_update = v.update_id - if v.message then - bot.on_msg_receive(self, v.message, config) - end - end - else - print('Connection error while fetching updates.') - end + bot.init(self, config) + while self.is_started do + -- Update loop. + local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } ) + if res then + -- Iterate over every new message. + for _,v in ipairs(res.result) do + self.last_update = v.update_id + if v.message then + bot.on_msg_receive(self, v.message, config) + end + end + else + print('Connection error while fetching updates.') + end - -- Run cron jobs every minute. - if self.last_cron ~= os.date('%M') then - self.last_cron = os.date('%M') - for i,v in ipairs(self.plugins) do - if v.cron then -- Call each plugin's cron function, if it has one. - local result, err = pcall(function() v.cron(self, config) end) - if not result then - utilities.handle_exception(self, err, 'CRON: ' .. i, config) - end - end - end - end + -- Run cron jobs every minute. + if self.last_cron ~= os.date('%M') then + self.last_cron = os.date('%M') + for i,v in ipairs(self.plugins) do + if v.cron then -- Call each plugin's cron function, if it has one. + local result, err = pcall(function() v.cron(self, config) end) + if not result then + utilities.handle_exception(self, err, 'CRON: ' .. i, config) + end + end + end + end - -- Save the "database" every hour. - if self.last_database_save ~= os.date('%H') then - self.last_database_save = os.date('%H') - utilities.save_data(self.database_name, self.database) - end - end - -- Save the database before exiting. - utilities.save_data(self.database_name, self.database) - print('Halted.') + -- Save the "database" every hour. + if self.last_database_save ~= os.date('%H') then + self.last_database_save = os.date('%H') + utilities.save_data(self.database_name, self.database) + end + end + -- Save the database before exiting. + utilities.save_data(self.database_name, self.database) + print('Halted.') end return bot diff --git a/otouto/drua-tg.lua b/otouto/drua-tg.lua index 3e83286..a9b5d0a 100644 --- a/otouto/drua-tg.lua +++ b/otouto/drua-tg.lua @@ -1,13 +1,13 @@ --[[ - drua-tg - Based on JuanPotato's lua-tg (https://github.com/juanpotato/lua-tg), - modified to work more naturally from an API bot. + drua-tg + Based on JuanPotato's lua-tg (https://github.com/juanpotato/lua-tg), + modified to work more naturally from an API bot. - Usage: - drua = require('drua-tg') - drua.IP = 'localhost' -- 'localhost' is default - drua.PORT = 4567 -- 4567 is default - drua.message(chat_id, text) + Usage: + drua = require('drua-tg') + drua.IP = 'localhost' -- 'localhost' is default + drua.PORT = 4567 -- 4567 is default + drua.message(chat_id, text) The MIT License (MIT) @@ -35,150 +35,150 @@ SOFTWARE. local SOCKET = require('socket') local comtab = { - add = { 'chat_add_user %s %s', 'channel_invite %s %s' }, - kick = { 'chat_del_user %s %s', 'channel_kick %s %s' }, - rename = { 'rename_chat %s "%s"', 'rename_channel %s "%s"' }, - link = { 'export_chat_link %s', 'export_channel_link %s' }, - photo_set = { 'chat_set_photo %s %s', 'channel_set_photo %s %s' }, - photo_get = { [0] = 'load_user_photo %s', 'load_chat_photo %s', 'load_channel_photo %s' }, - info = { [0] = 'user_info %s', 'chat_info %s', 'channel_info %s' } + add = { 'chat_add_user %s %s', 'channel_invite %s %s' }, + kick = { 'chat_del_user %s %s', 'channel_kick %s %s' }, + rename = { 'rename_chat %s "%s"', 'rename_channel %s "%s"' }, + link = { 'export_chat_link %s', 'export_channel_link %s' }, + photo_set = { 'chat_set_photo %s %s', 'channel_set_photo %s %s' }, + photo_get = { [0] = 'load_user_photo %s', 'load_chat_photo %s', 'load_channel_photo %s' }, + info = { [0] = 'user_info %s', 'chat_info %s', 'channel_info %s' } } local format_target = function(target) - target = tonumber(target) - if target < -1000000000000 then - target = 'channel#' .. math.abs(target) - 1000000000000 - return target, 2 - elseif target < 0 then - target = 'chat#' .. math.abs(target) - return target, 1 - else - target = 'user#' .. target - return target, 0 - end + target = tonumber(target) + if target < -1000000000000 then + target = 'channel#' .. math.abs(target) - 1000000000000 + return target, 2 + elseif target < 0 then + target = 'chat#' .. math.abs(target) + return target, 1 + else + target = 'user#' .. target + return target, 0 + end end local escape = function(text) - text = text:gsub('\\', '\\\\') - text = text:gsub('\n', '\\n') - text = text:gsub('\t', '\\t') - text = text:gsub('"', '\\"') - return text + text = text:gsub('\\', '\\\\') + text = text:gsub('\n', '\\n') + text = text:gsub('\t', '\\t') + text = text:gsub('"', '\\"') + return text end local drua = { - IP = 'localhost', - PORT = 4567 + IP = 'localhost', + PORT = 4567 } drua.send = function(command, do_receive) - local s = SOCKET.connect(drua.IP, drua.PORT) - assert(s, '\nUnable to connect to tg session.') - s:send(command..'\n') - local output - if do_receive then - output = string.match(s:receive('*l'), 'ANSWER (%d+)') - output = s:receive(tonumber(output)):gsub('\n$', '') - end - s:close() - return output + local s = SOCKET.connect(drua.IP, drua.PORT) + assert(s, '\nUnable to connect to tg session.') + s:send(command..'\n') + local output + if do_receive then + output = string.match(s:receive('*l'), 'ANSWER (%d+)') + output = s:receive(tonumber(output)):gsub('\n$', '') + end + s:close() + return output end drua.message = function(target, text) - target = format_target(target) - text = escape(text) - local command = 'msg %s "%s"' - command = command:format(target, text) - return drua.send(command) + target = format_target(target) + text = escape(text) + local command = 'msg %s "%s"' + command = command:format(target, text) + return drua.send(command) end drua.send_photo = function(target, photo) - target = format_target(target) - local command = 'send_photo %s %s' - command = command:format(target, photo) - return drua.send(command) + target = format_target(target) + local command = 'send_photo %s %s' + command = command:format(target, photo) + return drua.send(command) end drua.add_user = function(chat, target) - local a - chat, a = format_target(chat) - target = format_target(target) - local command = comtab.add[a]:format(chat, target) - return drua.send(command) + local a + chat, a = format_target(chat) + target = format_target(target) + local command = comtab.add[a]:format(chat, target) + return drua.send(command) end drua.kick_user = function(chat, target) - -- Get the group info so tg will recognize the target. - drua.get_info(chat) - local a - chat, a = format_target(chat) - target = format_target(target) - local command = comtab.kick[a]:format(chat, target) - return drua.send(command) + -- Get the group info so tg will recognize the target. + drua.get_info(chat) + local a + chat, a = format_target(chat) + target = format_target(target) + local command = comtab.kick[a]:format(chat, target) + return drua.send(command) end drua.rename_chat = function(chat, name) - local a - chat, a = format_target(chat) - local command = comtab.rename[a]:format(chat, name) - return drua.send(command) + local a + chat, a = format_target(chat) + local command = comtab.rename[a]:format(chat, name) + return drua.send(command) end drua.export_link = function(chat) - local a - chat, a = format_target(chat) - local command = comtab.link[a]:format(chat) - return drua.send(command, true) + local a + chat, a = format_target(chat) + local command = comtab.link[a]:format(chat) + return drua.send(command, true) end drua.get_photo = function(chat) - local a - chat, a = format_target(chat) - local command = comtab.photo_get[a]:format(chat) - local output = drua.send(command, true) - if output:match('FAIL') then - return false - else - return output:match('Saved to (.+)') - end + local a + chat, a = format_target(chat) + local command = comtab.photo_get[a]:format(chat) + local output = drua.send(command, true) + if output:match('FAIL') then + return false + else + return output:match('Saved to (.+)') + end end drua.set_photo = function(chat, photo) - local a - chat, a = format_target(chat) - local command = comtab.photo_set[a]:format(chat, photo) - return drua.send(command) + local a + chat, a = format_target(chat) + local command = comtab.photo_set[a]:format(chat, photo) + return drua.send(command) end drua.get_info = function(target) - local a - target, a = format_target(target) - local command = comtab.info[a]:format(target) - return drua.send(command, true) + local a + target, a = format_target(target) + local command = comtab.info[a]:format(target) + return drua.send(command, true) end drua.channel_set_admin = function(chat, user, rank) - chat = format_target(chat) - user = format_target(user) - local command = 'channel_set_admin %s %s %s' - command = command:format(chat, user, rank) - return drua.send(command) + chat = format_target(chat) + user = format_target(user) + local command = 'channel_set_admin %s %s %s' + command = command:format(chat, user, rank) + return drua.send(command) end drua.channel_set_about = function(chat, text) - chat = format_target(chat) - text = escape(text) - local command = 'channel_set_about %s "%s"' - command = command:format(chat, text) - return drua.send(command) + chat = format_target(chat) + text = escape(text) + local command = 'channel_set_about %s "%s"' + command = command:format(chat, text) + return drua.send(command) end drua.block = function(user) - return drua.send('block_user user#' .. user) + return drua.send('block_user user#' .. user) end drua.unblock = function(user) - return drua.send('unblock_user user#' .. user) + return drua.send('unblock_user user#' .. user) end return drua diff --git a/otouto/plugins/about.lua b/otouto/plugins/about.lua index 6088ace..3f453cb 100644 --- a/otouto/plugins/about.lua +++ b/otouto/plugins/about.lua @@ -7,13 +7,13 @@ about.command = 'about' about.doc = 'Returns information about the bot.' function about:init(config) - about.text = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.' - about.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('about'):t('start').table + about.text = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.' + about.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('about'):t('start').table end function about:action(msg, config) - utilities.send_message(self, msg.chat.id, about.text, true, nil, true) + utilities.send_message(self, msg.chat.id, about.text, true, nil, true) end return about diff --git a/otouto/plugins/administration.lua b/otouto/plugins/administration.lua index 092190c..82684ed 100644 --- a/otouto/plugins/administration.lua +++ b/otouto/plugins/administration.lua @@ -1,25 +1,25 @@ --[[ - administration.lua - Version 1.11 - Part of the otouto project. - © 2016 topkecleon <drew@otou.to> - GNU General Public License, version 2 + administration.lua + Version 1.11 + Part of the otouto project. + © 2016 topkecleon <drew@otou.to> + GNU General Public License, version 2 - This plugin provides self-hosted, single-realm group administration. - It requires tg (http://github.com/vysheng/tg) with supergroup support. - For more documentation, read the the manual (otou.to/rtfm). + This plugin provides self-hosted, single-realm group administration. + It requires tg (http://github.com/vysheng/tg) with supergroup support. + For more documentation, read the the manual (otou.to/rtfm). - Important notices about updates will be here! + Important notices about updates will be here! - 1.11 - Removed /kickme and /broadcast. Users should leave manually, and - announcements should be made via channel rather than spam. /setqotd now - handles forwarded messages correctly. /kick, /ban, /hammer, /mod, /admin - now support multiple arguments. Added get_targets function. No migration is - necessary. + 1.11 - Removed /kickme and /broadcast. Users should leave manually, and + announcements should be made via channel rather than spam. /setqotd now + handles forwarded messages correctly. /kick, /ban, /hammer, /mod, /admin + now support multiple arguments. Added get_targets function. No migration is + necessary. - 1.11.1 - Bugfixes. /hammer can now be used in PM. + 1.11.1 - Bugfixes. /hammer can now be used in PM. - 1.13 - Global banlist reinstated. Added default antiflood values to config. Bugfixes: Modding a user will no longer add him. Fixed kicks/bans in reply to join/leave notifications. + 1.13 - Global banlist reinstated. Added default antiflood values to config. Bugfixes: Modding a user will no longer add him. Fixed kicks/bans in reply to join/leave notifications. ]] local JSON = require('dkjson') @@ -30,1465 +30,1465 @@ local utilities = require('otouto.utilities') local administration = {} function administration:init(config) - -- Build the administration db if nonexistent. - if not self.database.administration then - self.database.administration = { - admins = {}, - groups = {}, - activity = {}, - autokick_timer = os.date('%d'), - globalbans = {} - } - end + -- Build the administration db if nonexistent. + if not self.database.administration then + self.database.administration = { + admins = {}, + groups = {}, + activity = {}, + autokick_timer = os.date('%d'), + globalbans = {} + } + end - administration.temp = { - help = {}, - flood = {} - } + administration.temp = { + help = {}, + flood = {} + } - drua.PORT = config.cli_port or 4567 + drua.PORT = config.cli_port or 4567 - administration.flags = administration.init_flags(config.cmd_pat) - administration.init_command(self, config) - administration.antiflood = config.administration.antiflood or { - text = 5, - voice = 5, - audio = 5, - contact = 5, - photo = 10, - video = 10, - location = 10, - document = 10, - sticker = 20 - } + administration.flags = administration.init_flags(config.cmd_pat) + administration.init_command(self, config) + administration.antiflood = config.administration.antiflood or { + text = 5, + voice = 5, + audio = 5, + contact = 5, + photo = 10, + video = 10, + location = 10, + document = 10, + sticker = 20 + } - administration.doc = 'Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.' - administration.command = 'groups [query]' + administration.doc = 'Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.' + administration.command = 'groups [query]' - -- In the worst case, don't send errors in reply to random messages. - administration.error = false - -- Accept forwarded messages and messages from blacklisted users. - administration.panoptic = true + -- In the worst case, don't send errors in reply to random messages. + administration.error = false + -- Accept forwarded messages and messages from blacklisted users. + administration.panoptic = true end function administration.init_flags(cmd_pat) return { - [1] = { - name = 'unlisted', - desc = 'Removes this group from the group listing.', - short = 'This group is unlisted.', - enabled = 'This group is no longer listed in '..cmd_pat..'groups.', - disabled = 'This group is now listed in '..cmd_pat..'groups.' - }, - [2] = { - name = 'antisquig', - desc = 'Automatically removes users who post Arabic script or RTL characters.', - short = 'This group does not allow Arabic script or RTL characters.', - enabled = 'Users will now be removed automatically for posting Arabic script and/or RTL characters.', - disabled = 'Users will no longer be removed automatically for posting Arabic script and/or RTL characters.', - kicked = 'You were automatically kicked from GROUPNAME for posting Arabic script and/or RTL characters.' - }, - [3] = { - name = 'antisquig++', - desc = 'Automatically removes users whose names contain Arabic script or RTL characters.', - short = 'This group does not allow users whose names contain Arabic script or RTL characters.', - enabled = 'Users whose names contain Arabic script and/or RTL characters will now be removed automatically.', - disabled = 'Users whose names contain Arabic script and/or RTL characters will no longer be removed automatically.', - kicked = 'You were automatically kicked from GROUPNAME for having a name which contains Arabic script and/or RTL characters.' - }, - [4] = { - name = 'antibot', - desc = 'Prevents the addition of bots by non-moderators.', - short = 'This group does not allow users to add bots.', - enabled = 'Non-moderators will no longer be able to add bots.', - disabled = 'Non-moderators will now be able to add bots.' - }, - [5] = { - name = 'antiflood', - desc = 'Prevents flooding by rate-limiting messages per user.', - short = 'This group automatically removes users who flood.', - enabled = 'Users will now be removed automatically for excessive messages. Use '..cmd_pat..'antiflood to configure limits.', - disabled = 'Users will no longer be removed automatically for excessive messages.', - kicked = 'You were automatically kicked from GROUPNAME for flooding.' - }, - [6] = { - name = 'antihammer', - desc = 'Allows globally banned users to enter this group. Note that users hammered in this group will also be banned locally.', - short = 'This group does not acknowledge global bans.', - enabled = 'This group will no longer remove users for being globally banned.', - disabled = 'This group will now remove users for being globally banned.' - } + [1] = { + name = 'unlisted', + desc = 'Removes this group from the group listing.', + short = 'This group is unlisted.', + enabled = 'This group is no longer listed in '..cmd_pat..'groups.', + disabled = 'This group is now listed in '..cmd_pat..'groups.' + }, + [2] = { + name = 'antisquig', + desc = 'Automatically removes users who post Arabic script or RTL characters.', + short = 'This group does not allow Arabic script or RTL characters.', + enabled = 'Users will now be removed automatically for posting Arabic script and/or RTL characters.', + disabled = 'Users will no longer be removed automatically for posting Arabic script and/or RTL characters.', + kicked = 'You were automatically kicked from GROUPNAME for posting Arabic script and/or RTL characters.' + }, + [3] = { + name = 'antisquig++', + desc = 'Automatically removes users whose names contain Arabic script or RTL characters.', + short = 'This group does not allow users whose names contain Arabic script or RTL characters.', + enabled = 'Users whose names contain Arabic script and/or RTL characters will now be removed automatically.', + disabled = 'Users whose names contain Arabic script and/or RTL characters will no longer be removed automatically.', + kicked = 'You were automatically kicked from GROUPNAME for having a name which contains Arabic script and/or RTL characters.' + }, + [4] = { + name = 'antibot', + desc = 'Prevents the addition of bots by non-moderators.', + short = 'This group does not allow users to add bots.', + enabled = 'Non-moderators will no longer be able to add bots.', + disabled = 'Non-moderators will now be able to add bots.' + }, + [5] = { + name = 'antiflood', + desc = 'Prevents flooding by rate-limiting messages per user.', + short = 'This group automatically removes users who flood.', + enabled = 'Users will now be removed automatically for excessive messages. Use '..cmd_pat..'antiflood to configure limits.', + disabled = 'Users will no longer be removed automatically for excessive messages.', + kicked = 'You were automatically kicked from GROUPNAME for flooding.' + }, + [6] = { + name = 'antihammer', + desc = 'Allows globally banned users to enter this group. Note that users hammered in this group will also be banned locally.', + short = 'This group does not acknowledge global bans.', + enabled = 'This group will no longer remove users for being globally banned.', + disabled = 'This group will now remove users for being globally banned.' + } } end administration.ranks = { - [0] = 'Banned', - [1] = 'Users', - [2] = 'Moderators', - [3] = 'Governors', - [4] = 'Administrators', - [5] = 'Owner' + [0] = 'Banned', + [1] = 'Users', + [2] = 'Moderators', + [3] = 'Governors', + [4] = 'Administrators', + [5] = 'Owner' } function administration:get_rank(user_id_str, chat_id_str, config) - user_id_str = tostring(user_id_str) - local user_id = tonumber(user_id_str) - chat_id_str = tostring(chat_id_str) + user_id_str = tostring(user_id_str) + local user_id = tonumber(user_id_str) + chat_id_str = tostring(chat_id_str) - -- Return 5 if the user_id_str is the bot or its owner. - if user_id == config.admin or user_id == self.info.id then - return 5 - end + -- Return 5 if the user_id_str is the bot or its owner. + if user_id == config.admin or user_id == self.info.id then + return 5 + end - -- Return 4 if the user_id_str is an administrator. - if self.database.administration.admins[user_id_str] then - return 4 - end + -- Return 4 if the user_id_str is an administrator. + if self.database.administration.admins[user_id_str] then + return 4 + end - if chat_id_str and self.database.administration.groups[chat_id_str] then - -- Return 3 if the user_id_str is the governor of the chat_id_str. - if self.database.administration.groups[chat_id_str].governor == user_id then - return 3 - -- Return 2 if the user_id_str is a moderator of the chat_id_str. - elseif self.database.administration.groups[chat_id_str].mods[user_id_str] then - return 2 - -- Return 0 if the user_id_str is banned from the chat_id_str. - elseif self.database.administration.groups[chat_id_str].bans[user_id_str] then - return 0 - -- Return 1 if antihammer is enabled. - elseif self.database.administration.groups[chat_id_str].flags[6] then - return 1 - end - end + if chat_id_str and self.database.administration.groups[chat_id_str] then + -- Return 3 if the user_id_str is the governor of the chat_id_str. + if self.database.administration.groups[chat_id_str].governor == user_id then + return 3 + -- Return 2 if the user_id_str is a moderator of the chat_id_str. + elseif self.database.administration.groups[chat_id_str].mods[user_id_str] then + return 2 + -- Return 0 if the user_id_str is banned from the chat_id_str. + elseif self.database.administration.groups[chat_id_str].bans[user_id_str] then + return 0 + -- Return 1 if antihammer is enabled. + elseif self.database.administration.groups[chat_id_str].flags[6] then + return 1 + end + end - -- Return 0 if the user_id_str is globally banned (and antihammer is not enabled). - if self.database.administration.globalbans[user_id_str] then - return 0 - end + -- Return 0 if the user_id_str is globally banned (and antihammer is not enabled). + if self.database.administration.globalbans[user_id_str] then + return 0 + end - -- Return 1 if the user_id_str is a regular user. - return 1 + -- Return 1 if the user_id_str is a regular user. + return 1 end -- Returns an array of "user" tables. function administration:get_targets(msg, config) - if msg.reply_to_message then - local d = msg.reply_to_message.new_chat_member or msg.reply_to_message.left_chat_member or msg.reply_to_message.from - local target = {} - for k,v in pairs(d) do - target[k] = v - end - target.name = utilities.build_name(target.first_name, target.last_name) - target.id_str = tostring(target.id) - target.rank = administration.get_rank(self, target.id, msg.chat.id, config) - return { target } - else - local input = utilities.input(msg.text) - if input then - local t = {} - for user in input:gmatch('%g+') do - if self.database.users[user] then - local target = {} - for k,v in pairs(self.database.users[user]) do - target[k] = v - end - target.name = utilities.build_name(target.first_name, target.last_name) - target.id_str = tostring(target.id) - target.rank = administration.get_rank(self, target.id, msg.chat.id, config) - table.insert(t, target) - elseif tonumber(user) then - local id = math.abs(tonumber(user)) - local target = { - id = id, - id_str = tostring(id), - name = 'Unknown ('..id..')', - rank = administration.get_rank(self, user, msg.chat.id, config) - } - table.insert(t, target) - elseif user:match('^@') then - local target = utilities.resolve_username(self, user) - if target then - target.rank = administration.get_rank(self, target.id, msg.chat.id, config) - target.id_str = tostring(target.id) - target.name = utilities.build_name(target.first_name, target.last_name) - table.insert(t, target) - else - table.insert(t, { err = 'Sorry, I do not recognize that username ('..user..').' }) - end - else - table.insert(t, { err = 'Invalid username or ID ('..user..').' }) - end - end - return t - else - return false - end - end + if msg.reply_to_message then + local d = msg.reply_to_message.new_chat_member or msg.reply_to_message.left_chat_member or msg.reply_to_message.from + local target = {} + for k,v in pairs(d) do + target[k] = v + end + target.name = utilities.build_name(target.first_name, target.last_name) + target.id_str = tostring(target.id) + target.rank = administration.get_rank(self, target.id, msg.chat.id, config) + return { target } + else + local input = utilities.input(msg.text) + if input then + local t = {} + for user in input:gmatch('%g+') do + if self.database.users[user] then + local target = {} + for k,v in pairs(self.database.users[user]) do + target[k] = v + end + target.name = utilities.build_name(target.first_name, target.last_name) + target.id_str = tostring(target.id) + target.rank = administration.get_rank(self, target.id, msg.chat.id, config) + table.insert(t, target) + elseif tonumber(user) then + local id = math.abs(tonumber(user)) + local target = { + id = id, + id_str = tostring(id), + name = 'Unknown ('..id..')', + rank = administration.get_rank(self, user, msg.chat.id, config) + } + table.insert(t, target) + elseif user:match('^@') then + local target = utilities.resolve_username(self, user) + if target then + target.rank = administration.get_rank(self, target.id, msg.chat.id, config) + target.id_str = tostring(target.id) + target.name = utilities.build_name(target.first_name, target.last_name) + table.insert(t, target) + else + table.insert(t, { err = 'Sorry, I do not recognize that username ('..user..').' }) + end + else + table.insert(t, { err = 'Invalid username or ID ('..user..').' }) + end + end + return t + else + return false + end + end end function administration:mod_format(id) - id = tostring(id) - local user = self.database.users[id] or { first_name = 'Unknown' } - local name = utilities.build_name(user.first_name, user.last_name) - name = utilities.md_escape(name) - local output = '• ' .. name .. ' `[' .. id .. ']`\n' - return output + id = tostring(id) + local user = self.database.users[id] or { first_name = 'Unknown' } + local name = utilities.build_name(user.first_name, user.last_name) + name = utilities.md_escape(name) + local output = '• ' .. name .. ' `[' .. id .. ']`\n' + return output end function administration:get_desc(chat_id, config) - local group = self.database.administration.groups[tostring(chat_id)] - local t = {} - if group.link then - table.insert(t, '*Welcome to* [' .. group.name .. '](' .. group.link .. ')*!*') - else - table.insert(t, '*Welcome to ' .. group.name .. '!*') - end - if group.motd then - table.insert(t, '*Message of the Day:*\n' .. group.motd) - end - if #group.rules > 0 then - local rulelist = '*Rules:*\n' - for i = 1, #group.rules do - rulelist = rulelist .. '*' .. i .. '.* ' .. group.rules[i] .. '\n' - end - table.insert(t, utilities.trim(rulelist)) - end - local flaglist = '' - for i = 1, #administration.flags do - if group.flags[i] then - flaglist = flaglist .. '• ' .. administration.flags[i].short .. '\n' - end - end - if flaglist ~= '' then - table.insert(t, '*Flags:*\n' .. utilities.trim(flaglist)) - end - if group.governor then - local gov = self.database.users[tostring(group.governor)] - local s - if gov then - s = utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`' - else - s = 'Unknown `[' .. group.governor .. ']`' - end - table.insert(t, '*Governor:* ' .. s) - end - local modstring = '' - for k,_ in pairs(group.mods) do - modstring = modstring .. administration.mod_format(self, k) - end - if modstring ~= '' then - table.insert(t, '*Moderators:*\n' .. utilities.trim(modstring)) - end - table.insert(t, 'Run '..config.cmd_pat..'ahelp@' .. self.info.username .. ' for a list of commands.') - return table.concat(t, '\n\n') + local group = self.database.administration.groups[tostring(chat_id)] + local t = {} + if group.link then + table.insert(t, '*Welcome to* [' .. group.name .. '](' .. group.link .. ')*!*') + else + table.insert(t, '*Welcome to ' .. group.name .. '!*') + end + if group.motd then + table.insert(t, '*Message of the Day:*\n' .. group.motd) + end + if #group.rules > 0 then + local rulelist = '*Rules:*\n' + for i = 1, #group.rules do + rulelist = rulelist .. '*' .. i .. '.* ' .. group.rules[i] .. '\n' + end + table.insert(t, utilities.trim(rulelist)) + end + local flaglist = '' + for i = 1, #administration.flags do + if group.flags[i] then + flaglist = flaglist .. '• ' .. administration.flags[i].short .. '\n' + end + end + if flaglist ~= '' then + table.insert(t, '*Flags:*\n' .. utilities.trim(flaglist)) + end + if group.governor then + local gov = self.database.users[tostring(group.governor)] + local s + if gov then + s = utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`' + else + s = 'Unknown `[' .. group.governor .. ']`' + end + table.insert(t, '*Governor:* ' .. s) + end + local modstring = '' + for k,_ in pairs(group.mods) do + modstring = modstring .. administration.mod_format(self, k) + end + if modstring ~= '' then + table.insert(t, '*Moderators:*\n' .. utilities.trim(modstring)) + end + table.insert(t, 'Run '..config.cmd_pat..'ahelp@' .. self.info.username .. ' for a list of commands.') + return table.concat(t, '\n\n') end function administration:update_desc(chat, config) - local group = self.database.administration.groups[tostring(chat)] - local desc = 'Welcome to ' .. group.name .. '!\n' - if group.motd then desc = desc .. group.motd .. '\n' end - if group.governor then - local gov = self.database.users[tostring(group.governor)] - desc = desc .. '\nGovernor: ' .. utilities.build_name(gov.first_name, gov.last_name) .. ' [' .. gov.id .. ']\n' - end - local s = '\n'..config.cmd_pat..'desc@' .. self.info.username .. ' for more information.' - desc = desc:sub(1, 250-s:len()) .. s - drua.channel_set_about(chat, desc) + local group = self.database.administration.groups[tostring(chat)] + local desc = 'Welcome to ' .. group.name .. '!\n' + if group.motd then desc = desc .. group.motd .. '\n' end + if group.governor then + local gov = self.database.users[tostring(group.governor)] + desc = desc .. '\nGovernor: ' .. utilities.build_name(gov.first_name, gov.last_name) .. ' [' .. gov.id .. ']\n' + end + local s = '\n'..config.cmd_pat..'desc@' .. self.info.username .. ' for more information.' + desc = desc:sub(1, 250-s:len()) .. s + drua.channel_set_about(chat, desc) end function administration:kick_user(chat, target, reason, config) - drua.kick_user(chat, target) - local victim = target - if self.database.users[tostring(target)] then - victim = utilities.build_name( - self.database.users[tostring(target)].first_name, - self.database.users[tostring(target)].last_name - ) .. ' [' .. victim .. ']' - end - local group = self.database.administration.groups[tostring(chat)].name - utilities.handle_exception(self, victim..' kicked from '..group, reason, config) + drua.kick_user(chat, target) + local victim = target + if self.database.users[tostring(target)] then + victim = utilities.build_name( + self.database.users[tostring(target)].first_name, + self.database.users[tostring(target)].last_name + ) .. ' [' .. victim .. ']' + end + local group = self.database.administration.groups[tostring(chat)].name + utilities.handle_exception(self, victim..' kicked from '..group, reason, config) end function administration.init_command(self_, config_) - administration.commands = { - - { -- generic, mostly autokicks - triggers = { '' }, - - privilege = 0, - interior = true, - - action = function(self, msg, group, config) - - local rank = administration.get_rank(self, msg.from.id, msg.chat.id, config) - local user = {} - local from_id_str = tostring(msg.from.id) - local chat_id_str = tostring(msg.chat.id) - - if rank < 2 then - local from_name = utilities.build_name(msg.from.first_name, msg.from.last_name) - - -- banned - if rank == 0 then - user.do_kick = true - user.dont_unban = true - user.reason = 'banned' - user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' - elseif group.flags[2] and ( -- antisquig - msg.text:match(utilities.char.arabic) - or msg.text:match(utilities.char.rtl_override) - or msg.text:match(utilities.char.rtl_mark) - ) then - user.do_kick = true - user.reason = 'antisquig' - user.output = administration.flags[2].kicked:gsub('GROUPNAME', msg.chat.title) - elseif group.flags[3] and ( -- antisquig++ - from_name:match(utilities.char.arabic) - or from_name:match(utilities.char.rtl_override) - or from_name:match(utilities.char.rtl_mark) - ) then - user.do_kick = true - user.reason = 'antisquig++' - user.output = administration.flags[3].kicked:gsub('GROUPNAME', msg.chat.title) - end - - -- antiflood - if group.flags[5] then - if not group.antiflood then - group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) - end - if not administration.temp.flood[chat_id_str] then - administration.temp.flood[chat_id_str] = {} - end - if not administration.temp.flood[chat_id_str][from_id_str] then - administration.temp.flood[chat_id_str][from_id_str] = 0 - end - if msg.sticker then -- Thanks Brazil for discarding switches. - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.sticker - elseif msg.photo then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.photo - elseif msg.document then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.document - elseif msg.audio then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.audio - elseif msg.contact then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.contact - elseif msg.video then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.video - elseif msg.location then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.location - elseif msg.voice then - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.voice - else - administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.text - end - if administration.temp.flood[chat_id_str][from_id_str] > 99 then - user.do_kick = true - user.reason = 'antiflood' - user.output = administration.flags[5].kicked:gsub('GROUPNAME', msg.chat.title) - administration.temp.flood[chat_id_str][from_id_str] = nil - end - end - - end - - local new_user = user - local new_rank = rank - - if msg.new_chat_member then - - -- I hate typing this out. - local noob = msg.new_chat_member - local noob_name = utilities.build_name(noob.first_name, noob.last_name) - - -- We'll make a new table for the new guy, unless he's also - -- the original guy. - if msg.new_chat_member.id ~= msg.from.id then - new_user = {} - new_rank = administration.get_rank(self,noob.id, msg.chat.id, config) - end - - if new_rank == 0 then - new_user.do_kick = true - new_user.dont_unban = true - new_user.reason = 'banned' - new_user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' - elseif new_rank == 1 then - if group.flags[3] and ( -- antisquig++ - noob_name:match(utilities.char.arabic) - or noob_name:match(utilities.char.rtl_override) - or noob_name:match(utilities.char.rtl_mark) - ) then - new_user.do_kick = true - new_user.reason = 'antisquig++' - new_user.output = administration.flags[3].kicked:gsub('GROUPNAME', msg.chat.title) - elseif ( -- antibot - group.flags[4] - and noob.username - and noob.username:match('bot$') - and rank < 2 - ) then - new_user.do_kick = true - new_user.reason = 'antibot' - end - else - -- Make the new user a group admin if he's a mod or higher. - if msg.chat.type == 'supergroup' then - drua.channel_set_admin(msg.chat.id, msg.new_chat_member.id, 2) - end - end - - elseif msg.new_chat_title then - if rank < 3 then - drua.rename_chat(msg.chat.id, group.name) - else - group.name = msg.new_chat_title - if group.grouptype == 'supergroup' then - administration.update_desc(self, msg.chat.id, config) - end - end - elseif msg.new_chat_photo then - if group.grouptype == 'group' then - if rank < 3 then - drua.set_photo(msg.chat.id, group.photo) - else - group.photo = drua.get_photo(msg.chat.id) - end - else - group.photo = drua.get_photo(msg.chat.id) - end - elseif msg.delete_chat_photo then - if group.grouptype == 'group' then - if rank < 3 then - drua.set_photo(msg.chat.id, group.photo) - else - group.photo = nil - end - else - group.photo = nil - end - end - - if new_user ~= user and new_user.do_kick then - administration.kick_user(self, msg.chat.id, msg.new_chat_member.id, new_user.reason, config) - if new_user.output then - utilities.send_message(self, msg.new_chat_member.id, new_user.output) - end - if not new_user.dont_unban and msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) - end - end - - if group.flags[5] and user.do_kick and not user.dont_unban then - if group.autokicks[from_id_str] then - group.autokicks[from_id_str] = group.autokicks[from_id_str] + 1 - else - group.autokicks[from_id_str] = 1 - end - if group.autokicks[from_id_str] >= group.autoban then - group.autokicks[from_id_str] = 0 - group.bans[from_id_str] = true - user.dont_unban = true - user.reason = 'antiflood autoban: ' .. user.reason - user.output = user.output .. '\nYou have been banned for being autokicked too many times.' - end - end - - if user.do_kick then - administration.kick_user(self, msg.chat.id, msg.from.id, user.reason, config) - if user.output then - utilities.send_message(self, msg.from.id, user.output) - end - if not user.dont_unban and msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) - end - end - - if msg.new_chat_member and not new_user.do_kick then - local output = administration.get_desc(self, msg.chat.id, config) - utilities.send_message(self, msg.new_chat_member.id, output, true, nil, true) - end - - -- Last active time for group listing. - if msg.text:len() > 0 then - for i,v in pairs(self.database.administration.activity) do - if v == chat_id_str then - table.remove(self.database.administration.activity, i) - table.insert(self.database.administration.activity, 1, chat_id_str) - end - end - end - - return true - - end - }, - - { -- /groups - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('groups', true).table, - - command = 'groups \\[query]', - privilege = 1, - interior = false, - doc = 'Returns a list of groups matching the query, or a list of all administrated groups.', - - action = function(self, msg, _, config) - local input = utilities.input(msg.text) - local search_res = '' - local grouplist = '' - for _, chat_id_str in ipairs(self.database.administration.activity) do - local group = self.database.administration.groups[chat_id_str] - if (not group.flags[1]) and group.link then -- no unlisted or unlinked groups - grouplist = grouplist .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' - if input and string.match(group.name:lower(), input:lower()) then - search_res = search_res .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' - end - end - end - local output - if search_res ~= '' then - output = '*Groups matching* _' .. input .. '_ *:*\n' .. search_res - elseif grouplist ~= '' then - output = '*Groups:*\n' .. grouplist - else - output = 'There are currently no listed groups.' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /ahelp - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('ahelp', true).table, - - command = 'ahelp \\[command]', - privilege = 1, - interior = false, - doc = 'Returns a list of realm-related commands for your rank (in a private message), or command-specific help.', - - action = function(self, msg, group, config) - local rank = administration.get_rank(self, msg.from.id, msg.chat.id, config) - local input = utilities.get_word(msg.text_lower, 2) - if input then - input = input:gsub('^'..config.cmd_pat..'', '') - local doc - for _,action in ipairs(administration.commands) do - if action.keyword == input then - doc = ''..config.cmd_pat..'' .. action.command:gsub('\\','') .. '\n' .. action.doc - break - end - end - if doc then - local output = '*Help for* _' .. input .. '_ :\n```\n' .. doc .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - else - local output = 'Sorry, there is no help for that command.\n'..config.cmd_pat..'ahelp@'..self.info.username - utilities.send_reply(self, msg, output) - end - else - local output = '*Commands for ' .. administration.ranks[rank] .. ':*\n' - for i = 1, rank do - for _, val in ipairs(administration.temp.help[i]) do - output = output .. '• ' .. config.cmd_pat .. val .. '\n' - end - end - output = output .. 'Arguments: <required> \\[optional]' - if utilities.send_message(self, msg.from.id, output, true, nil, true) then - if msg.from.id ~= msg.chat.id then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - else - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - end - end - }, - - { -- /ops - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('ops'):t('oplist').table, - - command = 'ops', - privilege = 1, - interior = true, - doc = 'Returns a list of moderators and the governor for the group.', - - action = function(self, msg, group, config) - local modstring = '' - for k,_ in pairs(group.mods) do - modstring = modstring .. administration.mod_format(self, k) - end - if modstring ~= '' then - modstring = '*Moderators for ' .. msg.chat.title .. ':*\n' .. modstring - end - local govstring = '' - if group.governor then - local gov = self.database.users[tostring(group.governor)] - if gov then - govstring = '*Governor:* ' .. utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`' - else - govstring = '*Governor:* Unknown `[' .. group.governor .. ']`' - end - end - local output = utilities.trim(modstring) ..'\n\n' .. utilities.trim(govstring) - if output == '\n\n' then - output = 'There are currently no moderators for this group.' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - - }, - - { -- /desc - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('desc'):t('description').table, - - command = 'description', - privilege = 1, - interior = true, - doc = 'Returns a description of the group (in a private message), including its motd, rules, flags, governor, and moderators.', - - action = function(self, msg, group, config) - local output = administration.get_desc(self, msg.chat.id, config) - if utilities.send_message(self, msg.from.id, output, true, nil, true) then - if msg.from.id ~= msg.chat.id then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - else - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - end - }, - - { -- /rules - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('rules?', true).table, - - command = 'rules \\[i]', - privilege = 1, - interior = true, - doc = 'Returns the group\'s list of rules, or a specific rule.', - - action = function(self, msg, group, config) - local output - local input = utilities.get_word(msg.text_lower, 2) - input = tonumber(input) - if #group.rules > 0 then - if input and group.rules[input] then - output = '*' .. input .. '.* ' .. group.rules[input] - else - output = '*Rules for ' .. msg.chat.title .. ':*\n' - for i,v in ipairs(group.rules) do - output = output .. '*' .. i .. '.* ' .. v .. '\n' - end - end - else - output = 'No rules have been set for ' .. msg.chat.title .. '.' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /motd - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('motd'):t('qotd').table, - - command = 'motd', - privilege = 1, - interior = true, - doc = 'Returns the group\'s message of the day.', - - action = function(self, msg, group, config) - local output = 'No MOTD has been set for ' .. msg.chat.title .. '.' - if group.motd then - output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. group.motd - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /link - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('link').table, - - command = 'link', - privilege = 1, - interior = true, - doc = 'Returns the group\'s link.', - - action = function(self, msg, group, config) - local output = 'No link has been set for ' .. msg.chat.title .. '.' - if group.link then - output = '[' .. msg.chat.title .. '](' .. group.link .. ')' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /kick - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('kick', true).table, - - command = 'kick <user>', - privilege = 2, - interior = true, - doc = 'Removes a user from the group. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then - output = output .. target.name .. ' is too privileged to be kicked.\n' - else - administration.kick_user(self, msg.chat.id, target.id, 'kicked by ' .. utilities.build_name(msg.from.first_name, msg.from.last_name), config) - output = output .. target.name .. ' has been kicked.\n' - if msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) - end - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /ban - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('ban', true).table, - - command = 'ban <user>', - privilege = 2, - interior = true, - doc = 'Bans a user from the group. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif group.bans[target.id_str] then - output = output .. target.name .. ' is already banned.\n' - elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then - output = output .. target.name .. ' is too privileged to be banned.\n' - else - administration.kick_user(self, msg.chat.id, target.id, 'banned by ' .. utilities.build_name(msg.from.first_name, msg.from.last_name), config) - output = output .. target.name .. ' has been banned.\n' - group.mods[target.id_str] = nil - group.bans[target.id_str] = true - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /unban - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('unban', true).table, - - command = 'unban <user>', - privilege = 2, - interior = true, - doc = 'Unbans a user from the group. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - else - if not group.bans[target.id_str] then - output = output .. target.name .. ' is not banned.\n' - else - output = output .. target.name .. ' has been unbanned.\n' - group.bans[target.id_str] = nil - end - if msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) - end - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /setmotd - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setmotd', true):t('setqotd', true).table, - - command = 'setmotd <motd>', - privilege = config_.administration.moderator_setmotd and 2 or 3, - interior = true, - doc = 'Sets the group\'s message of the day. Markdown is supported. Pass "--" to delete the message.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - local quoted = utilities.build_name(msg.from.first_name, msg.from.last_name) - if msg.reply_to_message and #msg.reply_to_message.text > 0 then - input = msg.reply_to_message.text - if msg.reply_to_message.forward_from then - quoted = utilities.build_name(msg.reply_to_message.forward_from.first_name, msg.reply_to_message.forward_from.last_name) - else - quoted = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) - end - end - if input then - if input == '--' or input == utilities.char.em_dash then - group.motd = nil - utilities.send_reply(self, msg, 'The MOTD has been cleared.') - else - if msg.text:match('^/setqotd') then - input = '_' .. utilities.md_escape(input) .. '_\n - ' .. utilities.md_escape(quoted) - end - group.motd = input - local output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. input - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - if group.grouptype == 'supergroup' then - administration.update_desc(self, msg.chat.id, config) - end - else - utilities.send_reply(self, msg, 'Please specify the new message of the day.') - end - end - }, - - { -- /setrules - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setrules', true).table, - - command = 'setrules <rules>', - privilege = 3, - interior = true, - doc = 'Sets the group\'s rules. Rules will be automatically numbered. Separate rules with a new line. Markdown is supported. Pass "--" to delete the rules.', - - action = function(self, msg, group, config) - local input = msg.text:match('^'..config.cmd_pat..'setrules[@'..self.info.username..']*(.+)') - if input == ' --' or input == ' ' .. utilities.char.em_dash then - group.rules = {} - utilities.send_reply(self, msg, 'The rules have been cleared.') - elseif input then - group.rules = {} - input = utilities.trim(input) .. '\n' - local output = '*Rules for ' .. msg.chat.title .. ':*\n' - local i = 1 - for l in input:gmatch('(.-)\n') do - output = output .. '*' .. i .. '.* ' .. l .. '\n' - i = i + 1 - table.insert(group.rules, utilities.trim(l)) - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - else - utilities.send_reply(self, msg, 'Please specify the new rules.') - end - end - }, - - { -- /changerule - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('changerule', true).table, - - command = 'changerule <i> <rule>', - privilege = 3, - interior = true, - doc = 'Changes a single rule. Pass "--" to delete the rule. If i is a number for which there is no rule, adds a rule by the next incremented number.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - local output = 'usage: `'..config.cmd_pat..'changerule <i> <newrule>`' - if input then - local rule_num = tonumber(input:match('^%d+')) - local new_rule = utilities.input(input) - if not rule_num then - output = 'Please specify which rule you want to change.' - elseif not new_rule then - output = 'Please specify the new rule.' - elseif new_rule == '--' or new_rule == utilities.char.em_dash then - if group.rules[rule_num] then - table.remove(group.rules, rule_num) - output = 'That rule has been deleted.' - else - output = 'There is no rule with that number.' - end - else - if not group.rules[rule_num] then - rule_num = #group.rules + 1 - end - group.rules[rule_num] = new_rule - output = '*' .. rule_num .. '*. ' .. new_rule - end - end - utilities.send_reply(self, msg, output, true) - end - }, - - { -- /setlink - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setlink', true).table, - - command = 'setlink <link>', - privilege = 3, - interior = true, - doc = 'Sets the group\'s join link. Pass "--" to regenerate the link.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - if input == '--' or input == utilities.char.em_dash then - group.link = drua.export_link(msg.chat.id) - utilities.send_reply(self, msg, 'The link has been regenerated.') - elseif input then - group.link = input - local output = '[' .. msg.chat.title .. '](' .. input .. ')' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - else - utilities.send_reply(self, msg, 'Please specify the new link.') - end - end - }, - - { -- /alist - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('alist').table, - - command = 'alist', - privilege = 3, - interior = true, - doc = 'Returns a list of administrators. Owner is denoted with a star character.', - - action = function(self, msg, group, config) - local output = '*Administrators:*\n' - output = output .. administration.mod_format(self, config.admin):gsub('\n', ' ★\n') - for id,_ in pairs(self.database.administration.admins) do - output = output .. administration.mod_format(self, id) - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /flags - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('flags?', true).table, - - command = 'flag \\[i] ...', - privilege = 3, - interior = true, - doc = 'Returns a list of flags or toggles the specified flags.', - - action = function(self, msg, group, config) - local output = '' - local input = utilities.input(msg.text) - if input then - for i in input:gmatch('%g+') do - local n = tonumber(i) - if n and administration.flags[n] then - if group.flags[n] == true then - group.flags[n] = false - output = output .. administration.flags[n].disabled .. '\n' - else - group.flags[n] = true - output = output .. administration.flags[n].enabled .. '\n' - end - end - end - if output == '' then - input = false - end - end - if not input then - output = '*Flags for ' .. msg.chat.title .. ':*\n' - for i, flag in ipairs(administration.flags) do - local status = group.flags[i] or false - output = output .. '*' .. i .. '. ' .. flag.name .. '* `[' .. tostring(status) .. ']`\n• ' .. flag.desc .. '\n' - end - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /antiflood - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('antiflood', true).table, - - command = 'antiflood \\[<type> <i>]', - privilege = 3, - interior = true, - doc = 'Returns a list of antiflood values or sets one.', - - action = function(self, msg, group, config) - if not group.flags[5] then - utilities.send_message(self, msg.chat.id, 'antiflood is not enabled. Use `'..config.cmd_pat..'flag 5` to enable it.', true, nil, true) - else - if not group.antiflood then - group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) - end - local input = utilities.input(msg.text_lower) - local output - if input then - local key, val = input:match('(%a+) (%d+)') - if not key or not val or not tonumber(val) then - output = 'Not a valid message type or number.' - elseif key == 'autoban' then - group.autoban = tonumber(val) - output = 'Users will now be autobanned after *' .. val .. '* autokicks.' - else - group.antiflood[key] = tonumber(val) - output = '*' .. key:gsub('^%l', string.upper) .. '* messages are now worth *' .. val .. '* points.' - end - else - output = 'usage: `'..config.cmd_pat..'antiflood <type> <i>`\nexample: `'..config.cmd_pat..'antiflood text 5`\nUse this command to configure the point values for each message type. When a user reaches 100 points, he is kicked. The points are reset each minute. The current values are:\n' - for k,v in pairs(group.antiflood) do - output = output .. '*'..k..':* `'..v..'`\n' - end - output = output .. 'Users will be banned automatically after *' .. group.autoban .. '* autokicks. Configure this with the *autoban* keyword.' - end - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - end - end - }, - - { -- /mod - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('mod', true).table, - - command = 'mod <user>', - privilege = 3, - interior = true, - doc = 'Promotes a user to a moderator. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - else - if target.rank > 1 then - output = output .. target.name .. ' is already a moderator or greater.\n' - else - output = output .. target.name .. ' is now a moderator.\n' - group.mods[target.id_str] = true - group.bans[target.id_str] = nil - end - if group.grouptype == 'supergroup' then - local chat_member = bindings.getChatMember(self, { chat_id = msg.chat.id, user_id = target.id }) - if chat_member and chat_member.result.status == 'member' then - drua.channel_set_admin(msg.chat.id, target.id, 2) - end - end - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /demod - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('demod', true).table, - - command = 'demod <user>', - privilege = 3, - interior = true, - doc = 'Demotes a moderator to a user. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - else - if not group.mods[target.id_str] then - output = output .. target.name .. ' is not a moderator.\n' - else - output = output .. target.name .. ' is no longer a moderator.\n' - group.mods[target.id_str] = nil - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 0) - end - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /gov - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('gov', true).table, - - command = 'gov <user>', - privilege = 4, - interior = true, - doc = 'Promotes a user to the governor. The current governor will be replaced. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local target = targets[1] - if target.err then - utilities.send_reply(self, msg, target.err) - else - if group.governor == target.id then - utilities.send_reply(self, msg, target.name .. ' is already the governor.') - else - group.bans[target.id_str] = nil - group.mods[target.id_str] = nil - group.governor = target.id - utilities.send_reply(self, msg, target.name .. ' is the new governor.') - end - if group.grouptype == 'supergroup' then - local chat_member = bindings.getChatMember(self, { chat_id = msg.chat.id, user_id = target.id }) - if chat_member and chat_member.result.status == 'member' then - drua.channel_set_admin(msg.chat.id, target.id, 2) - end - administration.update_desc(self, msg.chat.id, config) - end - end - else - utilities.send_reply(self, msg, 'Please specify a user via reply, username, or ID.') - end - end - }, - - { -- /degov - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('degov', true).table, - - command = 'degov <user>', - privilege = 4, - interior = true, - doc = 'Demotes the governor to a user. The administrator will become the new governor. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local target = targets[1] - if target.err then - utilities.send_reply(self, msg, target.err) - else - if group.governor ~= target.id then - utilities.send_reply(self, msg, target.name .. ' is not the governor.') - else - group.governor = msg.from.id - utilities.send_reply(self, msg, target.name .. ' is no longer the governor.') - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 0) - administration.update_desc(self, msg.chat.id, config) - end - end - else - utilities.send_reply(self, msg, 'Please specify a user via reply, username, or ID.') - end - end - }, - - { -- /hammer - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('hammer', true).table, - - command = 'hammer <user>', - privilege = 4, - interior = false, - doc = 'Bans a user from all groups. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif self.database.administration.globalbans[target.id_str] then - output = output .. target.name .. ' is already globally banned.\n' - elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then - output = output .. target.name .. ' is too privileged to be globally banned.\n' - else - if group then - administration.kick_user(self, msg.chat.id, target.id, 'hammered by ' .. utilities.build_name(msg.from.first_name, msg.from.last_name), config) - end - if #targets == 1 then - for k,v in pairs(self.database.administration.groups) do - if not v.flags[6] then - v.mods[target.id_str] = nil - drua.kick_user(k, target.id) - end - end - end - self.database.administration.globalbans[target.id_str] = true - if group and group.flags[6] == true then - group.mods[target.id_str] = nil - group.bans[target.id_str] = true - output = output .. target.name .. ' has been globally and locally banned.\n' - else - output = output .. target.name .. ' has been globally banned.\n' - end - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /unhammer - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('unhammer', true).table, - - command = 'unhammer <user>', - privilege = 4, - interior = false, - doc = 'Removes a global ban. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif not self.database.administration.globalbans[target.id_str] then - output = output .. target.name .. ' is not globally banned.\n' - else - self.database.administration.globalbans[target.id_str] = nil - output = output .. target.name .. ' has been globally unbanned.\n' - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /admin - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('admin', true).table, - - command = 'admin <user>', - privilege = 5, - interior = false, - doc = 'Promotes a user to an administrator. The target may be specified via reply, username, or ID.', - - action = function(self, msg, _, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif target.rank >= 4 then - output = output .. target.name .. ' is already an administrator or greater.\n' - else - for _, group in pairs(self.database.administration.groups) do - group.mods[target.id_str] = nil - end - self.database.administration.admins[target.id_str] = true - output = output .. target.name .. ' is now an administrator.\n' - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /deadmin - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('deadmin', true).table, - - command = 'deadmin <user>', - privilege = 5, - interior = false, - doc = 'Demotes an administrator to a user. The target may be specified via reply, username, or ID.', - - action = function(self, msg, _, config) - local targets = administration.get_targets(self, msg, config) - if targets then - local output = '' - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif target.rank ~= 4 then - output = output .. target.name .. ' is not an administrator.\n' - else - for chat_id, group in pairs(self.database.administration.groups) do - if group.grouptype == 'supergroup' then - drua.channel_set_admin(chat_id, target.id, 0) - end - end - self.database.administration.admins[target.id_str] = nil - output = output .. target.name .. ' is no longer an administrator.\n' - end - end - utilities.send_reply(self, msg, output) - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') - end - end - }, - - { -- /gadd - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('gadd', true).table, - - command = 'gadd \\[i] ...', - privilege = 5, - interior = false, - doc = 'Adds a group to the administration system. Pass numbers as arguments to enable those flags immediately.\nExample usage:\n\t/gadd 1 4 5\nThis would add a group and enable the unlisted flag, antibot, and antiflood.', - - action = function(self, msg, group, config) - if msg.chat.id == msg.from.id then - utilities.send_message(self, msg.chat.id, 'This is not a group.') - elseif group then - utilities.send_reply(self, msg, 'I am already administrating this group.') - else - local output = 'I am now administrating this group.' - local flags = {} - for i = 1, #administration.flags do - flags[i] = false - end - local input = utilities.input(msg.text) - if input then - for i in input:gmatch('%g+') do - local n = tonumber(i) - if n and administration.flags[n] and flags[n] ~= true then - flags[n] = true - output = output .. '\n' .. administration.flags[n].short - end - end - end - self.database.administration.groups[tostring(msg.chat.id)] = { - mods = {}, - governor = msg.from.id, - bans = {}, - flags = flags, - rules = {}, - grouptype = msg.chat.type, - name = msg.chat.title, - link = drua.export_link(msg.chat.id), - photo = drua.get_photo(msg.chat.id), - founded = os.time(), - autokicks = {}, - autoban = 3 - } - administration.update_desc(self, msg.chat.id, config) - table.insert(self.database.administration.activity, tostring(msg.chat.id)) - utilities.send_reply(self, msg, output) - drua.channel_set_admin(msg.chat.id, self.info.id, 2) - end - end - }, - - { -- /grem - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('grem', true):t('gremove', true).table, - - command = 'gremove \\[chat]', - privilege = 5, - interior = false, - doc = 'Removes a group from the administration system.', - - action = function(self, msg) - local input = utilities.input(msg.text) or tostring(msg.chat.id) - local output - if self.database.administration.groups[input] then - local chat_name = self.database.administration.groups[input].name - self.database.administration.groups[input] = nil - for i,v in ipairs(self.database.administration.activity) do - if v == input then - table.remove(self.database.administration.activity, i) - end - end - output = 'I am no longer administrating _' .. utilities.md_escape(chat_name) .. '_.' - else - if input == tostring(msg.chat.id) then - output = 'I do not administrate this group.' - else - output = 'I do not administrate that group.' - end - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /glist - triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('glist', false).table, - - command = 'glist', - privilege = 5, - interior = false, - doc = 'Returns a list (in a private message) of all administrated groups with their governors and links.', - - action = function(self, msg, group, config) - local output = '' - if utilities.table_size(self.database.administration.groups) > 0 then - for k,v in pairs(self.database.administration.groups) do - output = output .. '[' .. utilities.md_escape(v.name) .. '](' .. v.link .. ') `[' .. k .. ']`\n' - if v.governor then - local gov = self.database.users[tostring(v.governor)] - output = output .. '★ ' .. utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`\n' - end - end - else - output = 'There are no groups.' - end - if utilities.send_message(self, msg.from.id, output, true, nil, true) then - if msg.from.id ~= msg.chat.id then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - end - end - } - - } - - administration.triggers = {''} - - -- Generate help messages and ahelp keywords. - self_.database.administration.help = {} - for i,_ in ipairs(administration.ranks) do - administration.temp.help[i] = {} - end - for _,v in ipairs(administration.commands) do - if v.command then - table.insert(administration.temp.help[v.privilege], v.command) - if v.doc then - v.keyword = utilities.get_word(v.command, 1) - end - end - end + administration.commands = { + + { -- generic, mostly autokicks + triggers = { '' }, + + privilege = 0, + interior = true, + + action = function(self, msg, group, config) + + local rank = administration.get_rank(self, msg.from.id, msg.chat.id, config) + local user = {} + local from_id_str = tostring(msg.from.id) + local chat_id_str = tostring(msg.chat.id) + + if rank < 2 then + local from_name = utilities.build_name(msg.from.first_name, msg.from.last_name) + + -- banned + if rank == 0 then + user.do_kick = true + user.dont_unban = true + user.reason = 'banned' + user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' + elseif group.flags[2] and ( -- antisquig + msg.text:match(utilities.char.arabic) + or msg.text:match(utilities.char.rtl_override) + or msg.text:match(utilities.char.rtl_mark) + ) then + user.do_kick = true + user.reason = 'antisquig' + user.output = administration.flags[2].kicked:gsub('GROUPNAME', msg.chat.title) + elseif group.flags[3] and ( -- antisquig++ + from_name:match(utilities.char.arabic) + or from_name:match(utilities.char.rtl_override) + or from_name:match(utilities.char.rtl_mark) + ) then + user.do_kick = true + user.reason = 'antisquig++' + user.output = administration.flags[3].kicked:gsub('GROUPNAME', msg.chat.title) + end + + -- antiflood + if group.flags[5] then + if not group.antiflood then + group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) + end + if not administration.temp.flood[chat_id_str] then + administration.temp.flood[chat_id_str] = {} + end + if not administration.temp.flood[chat_id_str][from_id_str] then + administration.temp.flood[chat_id_str][from_id_str] = 0 + end + if msg.sticker then -- Thanks Brazil for discarding switches. + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.sticker + elseif msg.photo then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.photo + elseif msg.document then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.document + elseif msg.audio then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.audio + elseif msg.contact then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.contact + elseif msg.video then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.video + elseif msg.location then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.location + elseif msg.voice then + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.voice + else + administration.temp.flood[chat_id_str][from_id_str] = administration.temp.flood[chat_id_str][from_id_str] + group.antiflood.text + end + if administration.temp.flood[chat_id_str][from_id_str] > 99 then + user.do_kick = true + user.reason = 'antiflood' + user.output = administration.flags[5].kicked:gsub('GROUPNAME', msg.chat.title) + administration.temp.flood[chat_id_str][from_id_str] = nil + end + end + + end + + local new_user = user + local new_rank = rank + + if msg.new_chat_member then + + -- I hate typing this out. + local noob = msg.new_chat_member + local noob_name = utilities.build_name(noob.first_name, noob.last_name) + + -- We'll make a new table for the new guy, unless he's also + -- the original guy. + if msg.new_chat_member.id ~= msg.from.id then + new_user = {} + new_rank = administration.get_rank(self,noob.id, msg.chat.id, config) + end + + if new_rank == 0 then + new_user.do_kick = true + new_user.dont_unban = true + new_user.reason = 'banned' + new_user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' + elseif new_rank == 1 then + if group.flags[3] and ( -- antisquig++ + noob_name:match(utilities.char.arabic) + or noob_name:match(utilities.char.rtl_override) + or noob_name:match(utilities.char.rtl_mark) + ) then + new_user.do_kick = true + new_user.reason = 'antisquig++' + new_user.output = administration.flags[3].kicked:gsub('GROUPNAME', msg.chat.title) + elseif ( -- antibot + group.flags[4] + and noob.username + and noob.username:match('bot$') + and rank < 2 + ) then + new_user.do_kick = true + new_user.reason = 'antibot' + end + else + -- Make the new user a group admin if he's a mod or higher. + if msg.chat.type == 'supergroup' then + drua.channel_set_admin(msg.chat.id, msg.new_chat_member.id, 2) + end + end + + elseif msg.new_chat_title then + if rank < 3 then + drua.rename_chat(msg.chat.id, group.name) + else + group.name = msg.new_chat_title + if group.grouptype == 'supergroup' then + administration.update_desc(self, msg.chat.id, config) + end + end + elseif msg.new_chat_photo then + if group.grouptype == 'group' then + if rank < 3 then + drua.set_photo(msg.chat.id, group.photo) + else + group.photo = drua.get_photo(msg.chat.id) + end + else + group.photo = drua.get_photo(msg.chat.id) + end + elseif msg.delete_chat_photo then + if group.grouptype == 'group' then + if rank < 3 then + drua.set_photo(msg.chat.id, group.photo) + else + group.photo = nil + end + else + group.photo = nil + end + end + + if new_user ~= user and new_user.do_kick then + administration.kick_user(self, msg.chat.id, msg.new_chat_member.id, new_user.reason, config) + if new_user.output then + utilities.send_message(self, msg.new_chat_member.id, new_user.output) + end + if not new_user.dont_unban and msg.chat.type == 'supergroup' then + bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) + end + end + + if group.flags[5] and user.do_kick and not user.dont_unban then + if group.autokicks[from_id_str] then + group.autokicks[from_id_str] = group.autokicks[from_id_str] + 1 + else + group.autokicks[from_id_str] = 1 + end + if group.autokicks[from_id_str] >= group.autoban then + group.autokicks[from_id_str] = 0 + group.bans[from_id_str] = true + user.dont_unban = true + user.reason = 'antiflood autoban: ' .. user.reason + user.output = user.output .. '\nYou have been banned for being autokicked too many times.' + end + end + + if user.do_kick then + administration.kick_user(self, msg.chat.id, msg.from.id, user.reason, config) + if user.output then + utilities.send_message(self, msg.from.id, user.output) + end + if not user.dont_unban and msg.chat.type == 'supergroup' then + bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) + end + end + + if msg.new_chat_member and not new_user.do_kick then + local output = administration.get_desc(self, msg.chat.id, config) + utilities.send_message(self, msg.new_chat_member.id, output, true, nil, true) + end + + -- Last active time for group listing. + if msg.text:len() > 0 then + for i,v in pairs(self.database.administration.activity) do + if v == chat_id_str then + table.remove(self.database.administration.activity, i) + table.insert(self.database.administration.activity, 1, chat_id_str) + end + end + end + + return true + + end + }, + + { -- /groups + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('groups', true).table, + + command = 'groups \\[query]', + privilege = 1, + interior = false, + doc = 'Returns a list of groups matching the query, or a list of all administrated groups.', + + action = function(self, msg, _, config) + local input = utilities.input(msg.text) + local search_res = '' + local grouplist = '' + for _, chat_id_str in ipairs(self.database.administration.activity) do + local group = self.database.administration.groups[chat_id_str] + if (not group.flags[1]) and group.link then -- no unlisted or unlinked groups + grouplist = grouplist .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' + if input and string.match(group.name:lower(), input:lower()) then + search_res = search_res .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' + end + end + end + local output + if search_res ~= '' then + output = '*Groups matching* _' .. input .. '_ *:*\n' .. search_res + elseif grouplist ~= '' then + output = '*Groups:*\n' .. grouplist + else + output = 'There are currently no listed groups.' + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /ahelp + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('ahelp', true).table, + + command = 'ahelp \\[command]', + privilege = 1, + interior = false, + doc = 'Returns a list of realm-related commands for your rank (in a private message), or command-specific help.', + + action = function(self, msg, group, config) + local rank = administration.get_rank(self, msg.from.id, msg.chat.id, config) + local input = utilities.get_word(msg.text_lower, 2) + if input then + input = input:gsub('^'..config.cmd_pat..'', '') + local doc + for _,action in ipairs(administration.commands) do + if action.keyword == input then + doc = ''..config.cmd_pat..'' .. action.command:gsub('\\','') .. '\n' .. action.doc + break + end + end + if doc then + local output = '*Help for* _' .. input .. '_ :\n```\n' .. doc .. '\n```' + utilities.send_message(self, msg.chat.id, output, true, nil, true) + else + local output = 'Sorry, there is no help for that command.\n'..config.cmd_pat..'ahelp@'..self.info.username + utilities.send_reply(self, msg, output) + end + else + local output = '*Commands for ' .. administration.ranks[rank] .. ':*\n' + for i = 1, rank do + for _, val in ipairs(administration.temp.help[i]) do + output = output .. '• ' .. config.cmd_pat .. val .. '\n' + end + end + output = output .. 'Arguments: <required> \\[optional]' + if utilities.send_message(self, msg.from.id, output, true, nil, true) then + if msg.from.id ~= msg.chat.id then + utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') + end + else + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + end + end + }, + + { -- /ops + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('ops'):t('oplist').table, + + command = 'ops', + privilege = 1, + interior = true, + doc = 'Returns a list of moderators and the governor for the group.', + + action = function(self, msg, group, config) + local modstring = '' + for k,_ in pairs(group.mods) do + modstring = modstring .. administration.mod_format(self, k) + end + if modstring ~= '' then + modstring = '*Moderators for ' .. msg.chat.title .. ':*\n' .. modstring + end + local govstring = '' + if group.governor then + local gov = self.database.users[tostring(group.governor)] + if gov then + govstring = '*Governor:* ' .. utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`' + else + govstring = '*Governor:* Unknown `[' .. group.governor .. ']`' + end + end + local output = utilities.trim(modstring) ..'\n\n' .. utilities.trim(govstring) + if output == '\n\n' then + output = 'There are currently no moderators for this group.' + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + + }, + + { -- /desc + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('desc'):t('description').table, + + command = 'description', + privilege = 1, + interior = true, + doc = 'Returns a description of the group (in a private message), including its motd, rules, flags, governor, and moderators.', + + action = function(self, msg, group, config) + local output = administration.get_desc(self, msg.chat.id, config) + if utilities.send_message(self, msg.from.id, output, true, nil, true) then + if msg.from.id ~= msg.chat.id then + utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') + end + else + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + end + }, + + { -- /rules + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('rules?', true).table, + + command = 'rules \\[i]', + privilege = 1, + interior = true, + doc = 'Returns the group\'s list of rules, or a specific rule.', + + action = function(self, msg, group, config) + local output + local input = utilities.get_word(msg.text_lower, 2) + input = tonumber(input) + if #group.rules > 0 then + if input and group.rules[input] then + output = '*' .. input .. '.* ' .. group.rules[input] + else + output = '*Rules for ' .. msg.chat.title .. ':*\n' + for i,v in ipairs(group.rules) do + output = output .. '*' .. i .. '.* ' .. v .. '\n' + end + end + else + output = 'No rules have been set for ' .. msg.chat.title .. '.' + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /motd + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('motd'):t('qotd').table, + + command = 'motd', + privilege = 1, + interior = true, + doc = 'Returns the group\'s message of the day.', + + action = function(self, msg, group, config) + local output = 'No MOTD has been set for ' .. msg.chat.title .. '.' + if group.motd then + output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. group.motd + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /link + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('link').table, + + command = 'link', + privilege = 1, + interior = true, + doc = 'Returns the group\'s link.', + + action = function(self, msg, group, config) + local output = 'No link has been set for ' .. msg.chat.title .. '.' + if group.link then + output = '[' .. msg.chat.title .. '](' .. group.link .. ')' + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /kick + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('kick', true).table, + + command = 'kick <user>', + privilege = 2, + interior = true, + doc = 'Removes a user from the group. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then + output = output .. target.name .. ' is too privileged to be kicked.\n' + else + administration.kick_user(self, msg.chat.id, target.id, 'kicked by ' .. utilities.build_name(msg.from.first_name, msg.from.last_name), config) + output = output .. target.name .. ' has been kicked.\n' + if msg.chat.type == 'supergroup' then + bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) + end + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /ban + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('ban', true).table, + + command = 'ban <user>', + privilege = 2, + interior = true, + doc = 'Bans a user from the group. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif group.bans[target.id_str] then + output = output .. target.name .. ' is already banned.\n' + elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then + output = output .. target.name .. ' is too privileged to be banned.\n' + else + administration.kick_user(self, msg.chat.id, target.id, 'banned by ' .. utilities.build_name(msg.from.first_name, msg.from.last_name), config) + output = output .. target.name .. ' has been banned.\n' + group.mods[target.id_str] = nil + group.bans[target.id_str] = true + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /unban + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('unban', true).table, + + command = 'unban <user>', + privilege = 2, + interior = true, + doc = 'Unbans a user from the group. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + else + if not group.bans[target.id_str] then + output = output .. target.name .. ' is not banned.\n' + else + output = output .. target.name .. ' has been unbanned.\n' + group.bans[target.id_str] = nil + end + if msg.chat.type == 'supergroup' then + bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) + end + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /setmotd + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setmotd', true):t('setqotd', true).table, + + command = 'setmotd <motd>', + privilege = config_.administration.moderator_setmotd and 2 or 3, + interior = true, + doc = 'Sets the group\'s message of the day. Markdown is supported. Pass "--" to delete the message.', + + action = function(self, msg, group, config) + local input = utilities.input(msg.text) + local quoted = utilities.build_name(msg.from.first_name, msg.from.last_name) + if msg.reply_to_message and #msg.reply_to_message.text > 0 then + input = msg.reply_to_message.text + if msg.reply_to_message.forward_from then + quoted = utilities.build_name(msg.reply_to_message.forward_from.first_name, msg.reply_to_message.forward_from.last_name) + else + quoted = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) + end + end + if input then + if input == '--' or input == utilities.char.em_dash then + group.motd = nil + utilities.send_reply(self, msg, 'The MOTD has been cleared.') + else + if msg.text:match('^/setqotd') then + input = '_' .. utilities.md_escape(input) .. '_\n - ' .. utilities.md_escape(quoted) + end + group.motd = input + local output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. input + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + if group.grouptype == 'supergroup' then + administration.update_desc(self, msg.chat.id, config) + end + else + utilities.send_reply(self, msg, 'Please specify the new message of the day.') + end + end + }, + + { -- /setrules + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setrules', true).table, + + command = 'setrules <rules>', + privilege = 3, + interior = true, + doc = 'Sets the group\'s rules. Rules will be automatically numbered. Separate rules with a new line. Markdown is supported. Pass "--" to delete the rules.', + + action = function(self, msg, group, config) + local input = msg.text:match('^'..config.cmd_pat..'setrules[@'..self.info.username..']*(.+)') + if input == ' --' or input == ' ' .. utilities.char.em_dash then + group.rules = {} + utilities.send_reply(self, msg, 'The rules have been cleared.') + elseif input then + group.rules = {} + input = utilities.trim(input) .. '\n' + local output = '*Rules for ' .. msg.chat.title .. ':*\n' + local i = 1 + for l in input:gmatch('(.-)\n') do + output = output .. '*' .. i .. '.* ' .. l .. '\n' + i = i + 1 + table.insert(group.rules, utilities.trim(l)) + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + else + utilities.send_reply(self, msg, 'Please specify the new rules.') + end + end + }, + + { -- /changerule + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('changerule', true).table, + + command = 'changerule <i> <rule>', + privilege = 3, + interior = true, + doc = 'Changes a single rule. Pass "--" to delete the rule. If i is a number for which there is no rule, adds a rule by the next incremented number.', + + action = function(self, msg, group, config) + local input = utilities.input(msg.text) + local output = 'usage: `'..config.cmd_pat..'changerule <i> <newrule>`' + if input then + local rule_num = tonumber(input:match('^%d+')) + local new_rule = utilities.input(input) + if not rule_num then + output = 'Please specify which rule you want to change.' + elseif not new_rule then + output = 'Please specify the new rule.' + elseif new_rule == '--' or new_rule == utilities.char.em_dash then + if group.rules[rule_num] then + table.remove(group.rules, rule_num) + output = 'That rule has been deleted.' + else + output = 'There is no rule with that number.' + end + else + if not group.rules[rule_num] then + rule_num = #group.rules + 1 + end + group.rules[rule_num] = new_rule + output = '*' .. rule_num .. '*. ' .. new_rule + end + end + utilities.send_reply(self, msg, output, true) + end + }, + + { -- /setlink + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setlink', true).table, + + command = 'setlink <link>', + privilege = 3, + interior = true, + doc = 'Sets the group\'s join link. Pass "--" to regenerate the link.', + + action = function(self, msg, group, config) + local input = utilities.input(msg.text) + if input == '--' or input == utilities.char.em_dash then + group.link = drua.export_link(msg.chat.id) + utilities.send_reply(self, msg, 'The link has been regenerated.') + elseif input then + group.link = input + local output = '[' .. msg.chat.title .. '](' .. input .. ')' + utilities.send_message(self, msg.chat.id, output, true, nil, true) + else + utilities.send_reply(self, msg, 'Please specify the new link.') + end + end + }, + + { -- /alist + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('alist').table, + + command = 'alist', + privilege = 3, + interior = true, + doc = 'Returns a list of administrators. Owner is denoted with a star character.', + + action = function(self, msg, group, config) + local output = '*Administrators:*\n' + output = output .. administration.mod_format(self, config.admin):gsub('\n', ' ★\n') + for id,_ in pairs(self.database.administration.admins) do + output = output .. administration.mod_format(self, id) + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /flags + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('flags?', true).table, + + command = 'flag \\[i] ...', + privilege = 3, + interior = true, + doc = 'Returns a list of flags or toggles the specified flags.', + + action = function(self, msg, group, config) + local output = '' + local input = utilities.input(msg.text) + if input then + for i in input:gmatch('%g+') do + local n = tonumber(i) + if n and administration.flags[n] then + if group.flags[n] == true then + group.flags[n] = false + output = output .. administration.flags[n].disabled .. '\n' + else + group.flags[n] = true + output = output .. administration.flags[n].enabled .. '\n' + end + end + end + if output == '' then + input = false + end + end + if not input then + output = '*Flags for ' .. msg.chat.title .. ':*\n' + for i, flag in ipairs(administration.flags) do + local status = group.flags[i] or false + output = output .. '*' .. i .. '. ' .. flag.name .. '* `[' .. tostring(status) .. ']`\n• ' .. flag.desc .. '\n' + end + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /antiflood + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('antiflood', true).table, + + command = 'antiflood \\[<type> <i>]', + privilege = 3, + interior = true, + doc = 'Returns a list of antiflood values or sets one.', + + action = function(self, msg, group, config) + if not group.flags[5] then + utilities.send_message(self, msg.chat.id, 'antiflood is not enabled. Use `'..config.cmd_pat..'flag 5` to enable it.', true, nil, true) + else + if not group.antiflood then + group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) + end + local input = utilities.input(msg.text_lower) + local output + if input then + local key, val = input:match('(%a+) (%d+)') + if not key or not val or not tonumber(val) then + output = 'Not a valid message type or number.' + elseif key == 'autoban' then + group.autoban = tonumber(val) + output = 'Users will now be autobanned after *' .. val .. '* autokicks.' + else + group.antiflood[key] = tonumber(val) + output = '*' .. key:gsub('^%l', string.upper) .. '* messages are now worth *' .. val .. '* points.' + end + else + output = 'usage: `'..config.cmd_pat..'antiflood <type> <i>`\nexample: `'..config.cmd_pat..'antiflood text 5`\nUse this command to configure the point values for each message type. When a user reaches 100 points, he is kicked. The points are reset each minute. The current values are:\n' + for k,v in pairs(group.antiflood) do + output = output .. '*'..k..':* `'..v..'`\n' + end + output = output .. 'Users will be banned automatically after *' .. group.autoban .. '* autokicks. Configure this with the *autoban* keyword.' + end + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + end + end + }, + + { -- /mod + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('mod', true).table, + + command = 'mod <user>', + privilege = 3, + interior = true, + doc = 'Promotes a user to a moderator. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + else + if target.rank > 1 then + output = output .. target.name .. ' is already a moderator or greater.\n' + else + output = output .. target.name .. ' is now a moderator.\n' + group.mods[target.id_str] = true + group.bans[target.id_str] = nil + end + if group.grouptype == 'supergroup' then + local chat_member = bindings.getChatMember(self, { chat_id = msg.chat.id, user_id = target.id }) + if chat_member and chat_member.result.status == 'member' then + drua.channel_set_admin(msg.chat.id, target.id, 2) + end + end + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /demod + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('demod', true).table, + + command = 'demod <user>', + privilege = 3, + interior = true, + doc = 'Demotes a moderator to a user. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + else + if not group.mods[target.id_str] then + output = output .. target.name .. ' is not a moderator.\n' + else + output = output .. target.name .. ' is no longer a moderator.\n' + group.mods[target.id_str] = nil + end + if group.grouptype == 'supergroup' then + drua.channel_set_admin(msg.chat.id, target.id, 0) + end + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /gov + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('gov', true).table, + + command = 'gov <user>', + privilege = 4, + interior = true, + doc = 'Promotes a user to the governor. The current governor will be replaced. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local target = targets[1] + if target.err then + utilities.send_reply(self, msg, target.err) + else + if group.governor == target.id then + utilities.send_reply(self, msg, target.name .. ' is already the governor.') + else + group.bans[target.id_str] = nil + group.mods[target.id_str] = nil + group.governor = target.id + utilities.send_reply(self, msg, target.name .. ' is the new governor.') + end + if group.grouptype == 'supergroup' then + local chat_member = bindings.getChatMember(self, { chat_id = msg.chat.id, user_id = target.id }) + if chat_member and chat_member.result.status == 'member' then + drua.channel_set_admin(msg.chat.id, target.id, 2) + end + administration.update_desc(self, msg.chat.id, config) + end + end + else + utilities.send_reply(self, msg, 'Please specify a user via reply, username, or ID.') + end + end + }, + + { -- /degov + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('degov', true).table, + + command = 'degov <user>', + privilege = 4, + interior = true, + doc = 'Demotes the governor to a user. The administrator will become the new governor. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local target = targets[1] + if target.err then + utilities.send_reply(self, msg, target.err) + else + if group.governor ~= target.id then + utilities.send_reply(self, msg, target.name .. ' is not the governor.') + else + group.governor = msg.from.id + utilities.send_reply(self, msg, target.name .. ' is no longer the governor.') + end + if group.grouptype == 'supergroup' then + drua.channel_set_admin(msg.chat.id, target.id, 0) + administration.update_desc(self, msg.chat.id, config) + end + end + else + utilities.send_reply(self, msg, 'Please specify a user via reply, username, or ID.') + end + end + }, + + { -- /hammer + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('hammer', true).table, + + command = 'hammer <user>', + privilege = 4, + interior = false, + doc = 'Bans a user from all groups. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif self.database.administration.globalbans[target.id_str] then + output = output .. target.name .. ' is already globally banned.\n' + elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then + output = output .. target.name .. ' is too privileged to be globally banned.\n' + else + if group then + administration.kick_user(self, msg.chat.id, target.id, 'hammered by ' .. utilities.build_name(msg.from.first_name, msg.from.last_name), config) + end + if #targets == 1 then + for k,v in pairs(self.database.administration.groups) do + if not v.flags[6] then + v.mods[target.id_str] = nil + drua.kick_user(k, target.id) + end + end + end + self.database.administration.globalbans[target.id_str] = true + if group and group.flags[6] == true then + group.mods[target.id_str] = nil + group.bans[target.id_str] = true + output = output .. target.name .. ' has been globally and locally banned.\n' + else + output = output .. target.name .. ' has been globally banned.\n' + end + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /unhammer + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('unhammer', true).table, + + command = 'unhammer <user>', + privilege = 4, + interior = false, + doc = 'Removes a global ban. The target may be specified via reply, username, or ID.', + + action = function(self, msg, group, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif not self.database.administration.globalbans[target.id_str] then + output = output .. target.name .. ' is not globally banned.\n' + else + self.database.administration.globalbans[target.id_str] = nil + output = output .. target.name .. ' has been globally unbanned.\n' + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /admin + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('admin', true).table, + + command = 'admin <user>', + privilege = 5, + interior = false, + doc = 'Promotes a user to an administrator. The target may be specified via reply, username, or ID.', + + action = function(self, msg, _, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif target.rank >= 4 then + output = output .. target.name .. ' is already an administrator or greater.\n' + else + for _, group in pairs(self.database.administration.groups) do + group.mods[target.id_str] = nil + end + self.database.administration.admins[target.id_str] = true + output = output .. target.name .. ' is now an administrator.\n' + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /deadmin + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('deadmin', true).table, + + command = 'deadmin <user>', + privilege = 5, + interior = false, + doc = 'Demotes an administrator to a user. The target may be specified via reply, username, or ID.', + + action = function(self, msg, _, config) + local targets = administration.get_targets(self, msg, config) + if targets then + local output = '' + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif target.rank ~= 4 then + output = output .. target.name .. ' is not an administrator.\n' + else + for chat_id, group in pairs(self.database.administration.groups) do + if group.grouptype == 'supergroup' then + drua.channel_set_admin(chat_id, target.id, 0) + end + end + self.database.administration.admins[target.id_str] = nil + output = output .. target.name .. ' is no longer an administrator.\n' + end + end + utilities.send_reply(self, msg, output) + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID.') + end + end + }, + + { -- /gadd + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('gadd', true).table, + + command = 'gadd \\[i] ...', + privilege = 5, + interior = false, + doc = 'Adds a group to the administration system. Pass numbers as arguments to enable those flags immediately.\nExample usage:\n\t/gadd 1 4 5\nThis would add a group and enable the unlisted flag, antibot, and antiflood.', + + action = function(self, msg, group, config) + if msg.chat.id == msg.from.id then + utilities.send_message(self, msg.chat.id, 'This is not a group.') + elseif group then + utilities.send_reply(self, msg, 'I am already administrating this group.') + else + local output = 'I am now administrating this group.' + local flags = {} + for i = 1, #administration.flags do + flags[i] = false + end + local input = utilities.input(msg.text) + if input then + for i in input:gmatch('%g+') do + local n = tonumber(i) + if n and administration.flags[n] and flags[n] ~= true then + flags[n] = true + output = output .. '\n' .. administration.flags[n].short + end + end + end + self.database.administration.groups[tostring(msg.chat.id)] = { + mods = {}, + governor = msg.from.id, + bans = {}, + flags = flags, + rules = {}, + grouptype = msg.chat.type, + name = msg.chat.title, + link = drua.export_link(msg.chat.id), + photo = drua.get_photo(msg.chat.id), + founded = os.time(), + autokicks = {}, + autoban = 3 + } + administration.update_desc(self, msg.chat.id, config) + table.insert(self.database.administration.activity, tostring(msg.chat.id)) + utilities.send_reply(self, msg, output) + drua.channel_set_admin(msg.chat.id, self.info.id, 2) + end + end + }, + + { -- /grem + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('grem', true):t('gremove', true).table, + + command = 'gremove \\[chat]', + privilege = 5, + interior = false, + doc = 'Removes a group from the administration system.', + + action = function(self, msg) + local input = utilities.input(msg.text) or tostring(msg.chat.id) + local output + if self.database.administration.groups[input] then + local chat_name = self.database.administration.groups[input].name + self.database.administration.groups[input] = nil + for i,v in ipairs(self.database.administration.activity) do + if v == input then + table.remove(self.database.administration.activity, i) + end + end + output = 'I am no longer administrating _' .. utilities.md_escape(chat_name) .. '_.' + else + if input == tostring(msg.chat.id) then + output = 'I do not administrate this group.' + else + output = 'I do not administrate that group.' + end + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + }, + + { -- /glist + triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('glist', false).table, + + command = 'glist', + privilege = 5, + interior = false, + doc = 'Returns a list (in a private message) of all administrated groups with their governors and links.', + + action = function(self, msg, group, config) + local output = '' + if utilities.table_size(self.database.administration.groups) > 0 then + for k,v in pairs(self.database.administration.groups) do + output = output .. '[' .. utilities.md_escape(v.name) .. '](' .. v.link .. ') `[' .. k .. ']`\n' + if v.governor then + local gov = self.database.users[tostring(v.governor)] + output = output .. '★ ' .. utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`\n' + end + end + else + output = 'There are no groups.' + end + if utilities.send_message(self, msg.from.id, output, true, nil, true) then + if msg.from.id ~= msg.chat.id then + utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') + end + end + end + } + + } + + administration.triggers = {''} + + -- Generate help messages and ahelp keywords. + self_.database.administration.help = {} + for i,_ in ipairs(administration.ranks) do + administration.temp.help[i] = {} + end + for _,v in ipairs(administration.commands) do + if v.command then + table.insert(administration.temp.help[v.privilege], v.command) + if v.doc then + v.keyword = utilities.get_word(v.command, 1) + end + end + end end function administration:action(msg, config) - for _,command in ipairs(administration.commands) do - for _,trigger in pairs(command.triggers) do - if msg.text_lower:match(trigger) then - if - (command.interior and not self.database.administration.groups[tostring(msg.chat.id)]) - or administration.get_rank(self, msg.from.id, msg.chat.id, config) < command.privilege - then - break - end - local res = command.action(self, msg, self.database.administration.groups[tostring(msg.chat.id)], config) - if res ~= true then - return res - end - end - end - end - return true + for _,command in ipairs(administration.commands) do + for _,trigger in pairs(command.triggers) do + if msg.text_lower:match(trigger) then + if + (command.interior and not self.database.administration.groups[tostring(msg.chat.id)]) + or administration.get_rank(self, msg.from.id, msg.chat.id, config) < command.privilege + then + break + end + local res = command.action(self, msg, self.database.administration.groups[tostring(msg.chat.id)], config) + if res ~= true then + return res + end + end + end + end + return true end function administration:cron() - administration.temp.flood = {} - if os.date('%d') ~= self.database.administration.autokick_timer then - self.database.administration.autokick_timer = os.date('%d') - for _,v in pairs(self.database.administration.groups) do - v.autokicks = {} - end - end + administration.temp.flood = {} + if os.date('%d') ~= self.database.administration.autokick_timer then + self.database.administration.autokick_timer = os.date('%d') + for _,v in pairs(self.database.administration.groups) do + v.autokicks = {} + end + end end return administration diff --git a/otouto/plugins/apod.lua b/otouto/plugins/apod.lua index 7350ffc..e806812 100644 --- a/otouto/plugins/apod.lua +++ b/otouto/plugins/apod.lua @@ -10,47 +10,47 @@ local utilities = require('otouto.utilities') apod.command = 'apod [date]' function apod:init(config) - apod.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('apod', true).table - apod.doc = [[ + apod.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('apod', true).table + apod.doc = [[ /apod [YYYY-MM-DD] Returns the Astronomy Picture of the Day. Source: nasa.gov - ]] - apod.doc = apod.doc:gsub('/', config.cmd_pat) - apod.base_url = 'https://api.nasa.gov/planetary/apod?api_key=' .. (config.nasa_api_key or 'DEMO_KEY') + ]] + apod.doc = apod.doc:gsub('/', config.cmd_pat) + apod.base_url = 'https://api.nasa.gov/planetary/apod?api_key=' .. (config.nasa_api_key or 'DEMO_KEY') end function apod:action(msg, config) - local input = utilities.input(msg.text) - local url = apod.base_url - local date = os.date('%F') - if input then - if input:match('^(%d+)%-(%d+)%-(%d+)$') then - url = url .. '&date=' .. URL.escape(input) - date = input - end - end + local input = utilities.input(msg.text) + local url = apod.base_url + local date = os.date('%F') + if input then + if input:match('^(%d+)%-(%d+)%-(%d+)$') then + url = url .. '&date=' .. URL.escape(input) + date = input + end + end - local jstr, code = HTTPS.request(url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, code = HTTPS.request(url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local data = JSON.decode(jstr) - if data.error then - utilities.send_reply(self, msg, config.errors.results) - return - end + local data = JSON.decode(jstr) + if data.error then + utilities.send_reply(self, msg, config.errors.results) + return + end - local output = string.format( - '<b>%s (</b><a href="%s">%s</a><b>)</b>\n%s', - utilities.html_escape(data.title), - utilities.html_escape(data.hdurl or data.url), - date, - utilities.html_escape(data.explanation) - ) - utilities.send_message(self, msg.chat.id, output, false, nil, 'html') + local output = string.format( + '<b>%s (</b><a href="%s">%s</a><b>)</b>\n%s', + utilities.html_escape(data.title), + utilities.html_escape(data.hdurl or data.url), + date, + utilities.html_escape(data.explanation) + ) + utilities.send_message(self, msg.chat.id, output, false, nil, 'html') end return apod diff --git a/otouto/plugins/bandersnatch.lua b/otouto/plugins/bandersnatch.lua index bda4c93..46de247 100644 --- a/otouto/plugins/bandersnatch.lua +++ b/otouto/plugins/bandersnatch.lua @@ -5,8 +5,8 @@ local utilities = require('otouto.utilities') bandersnatch.command = 'bandersnatch' function bandersnatch:init(config) - bandersnatch.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bandersnatch'):t('bc').table - bandersnatch.doc = 'Shun the frumious Bandersnatch. \nAlias: ' .. config.cmd_pat .. 'bc' + bandersnatch.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bandersnatch'):t('bc').table + bandersnatch.doc = 'Shun the frumious Bandersnatch. \nAlias: ' .. config.cmd_pat .. 'bc' end local fullnames = { "Wimbledon Tennismatch", "Rinkydink Curdlesnoot", "Butawhiteboy Cantbekhan", "Benadryl Claritin", "Bombadil Rivendell", "Wanda's Crotchfruit", "Biblical Concubine", "Syphilis Cankersore", "Buckminster Fullerene", "Bourgeoisie Capitalist" } @@ -17,15 +17,15 @@ local lastnames = { "Coddleswort", "Crumplesack", "Curdlesnoot", "Calldispatch", function bandersnatch:action(msg) - local output + local output - if math.random(10) == 10 then - output = fullnames[math.random(#fullnames)] - else - output = firstnames[math.random(#firstnames)] .. ' ' .. lastnames[math.random(#lastnames)] - end + if math.random(10) == 10 then + output = fullnames[math.random(#fullnames)] + else + output = firstnames[math.random(#firstnames)] .. ' ' .. lastnames[math.random(#lastnames)] + end - utilities.send_message(self, msg.chat.id, '_'..output..'_', true, nil, true) + utilities.send_message(self, msg.chat.id, '_'..output..'_', true, nil, true) end diff --git a/otouto/plugins/bible.lua b/otouto/plugins/bible.lua index 2d85b43..69f8efa 100644 --- a/otouto/plugins/bible.lua +++ b/otouto/plugins/bible.lua @@ -5,12 +5,12 @@ local URL = require('socket.url') local utilities = require('otouto.utilities') function bible:init(config) - assert(config.biblia_api_key, - 'bible.lua requires a Biblia API key from http://api.biblia.com.' - ) + assert(config.biblia_api_key, + 'bible.lua requires a Biblia API key from http://api.biblia.com.' + ) - bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table - bible.doc = config.cmd_pat .. [[bible <reference> + bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table + bible.doc = config.cmd_pat .. [[bible <reference> Returns a verse from the American Standard Version of the Bible, or an apocryphal verse from the King James Version. Results from biblia.com. Alias: ]] .. config.cmd_pat .. 'b' end @@ -19,30 +19,30 @@ bible.command = 'bible <reference>' function bible:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, bible.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, bible.doc, true) + return + end - local url = 'http://api.biblia.com/v1/bible/content/ASV.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) + local url = 'http://api.biblia.com/v1/bible/content/ASV.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) - local output, res = HTTP.request(url) + local output, res = HTTP.request(url) - if not output or res ~= 200 or output:len() == 0 then - url = 'http://api.biblia.com/v1/bible/content/KJVAPOC.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) - output, res = HTTP.request(url) - end + if not output or res ~= 200 or output:len() == 0 then + url = 'http://api.biblia.com/v1/bible/content/KJVAPOC.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) + output, res = HTTP.request(url) + end - if not output or res ~= 200 or output:len() == 0 then - output = config.errors.results - end + if not output or res ~= 200 or output:len() == 0 then + output = config.errors.results + end - if output:len() > 4000 then - output = 'The text is too long to post here. Try being more specific.' - end + if output:len() > 4000 then + output = 'The text is too long to post here. Try being more specific.' + end - utilities.send_reply(self, msg, output) + utilities.send_reply(self, msg, output) end diff --git a/otouto/plugins/bing.lua b/otouto/plugins/bing.lua index c71e5ef..627ff3a 100644 --- a/otouto/plugins/bing.lua +++ b/otouto/plugins/bing.lua @@ -14,65 +14,65 @@ bing.command = 'bing <query>' bing.search_url = 'https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=\'%s\'&$format=json' function bing:init(config) - assert(config.bing_api_key, - 'bing.lua requires a Bing API key from http://datamarket.azure.com/dataset/bing/search.' - ) + assert(config.bing_api_key, + 'bing.lua requires a Bing API key from http://datamarket.azure.com/dataset/bing/search.' + ) - bing.headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) } - bing.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('bing', true):t('g', true):t('google', true).table - bing.doc = [[ + bing.headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) } + bing.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('bing', true):t('g', true):t('google', true).table + bing.doc = [[ /bing <query> Returns the top web results from Bing. Aliases: /g, /google - ]] - bing.doc = bing.doc:gsub('/', config.cmd_pat) + ]] + bing.doc = bing.doc:gsub('/', config.cmd_pat) end function bing:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, bing.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, bing.doc, true) + return + end - local url = bing.search_url:format(URL.escape(input)) - local resbody = {} - local _, code = https.request{ - url = url, - headers = bing.headers, - sink = ltn12.sink.table(resbody), - } - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local url = bing.search_url:format(URL.escape(input)) + local resbody = {} + local _, code = https.request{ + url = url, + headers = bing.headers, + sink = ltn12.sink.table(resbody), + } + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local data = JSON.decode(table.concat(resbody)) - -- Four results in a group, eight in private. - local limit = msg.chat.type == 'private' and 8 or 4 - -- No more results than provided. - limit = limit > #data.d.results and #data.d.results or limit - if limit == 0 then - utilities.send_reply(self, msg, config.errors.results) - return - end + local data = JSON.decode(table.concat(resbody)) + -- Four results in a group, eight in private. + local limit = msg.chat.type == 'private' and 8 or 4 + -- No more results than provided. + limit = limit > #data.d.results and #data.d.results or limit + if limit == 0 then + utilities.send_reply(self, msg, config.errors.results) + return + end - local reslist = {} - for i = 1, limit do - table.insert(reslist, string.format( - '• <a href="%s">%s</a>', - utilities.html_escape(data.d.results[i].Url), - utilities.html_escape(data.d.results[i].Title) - )) - end - local output = string.format( - '<b>Search results for</b> <i>%s</i><b>:</b>\n%s', - utilities.html_escape(input), - table.concat(reslist, '\n') - ) - utilities.send_message(self, msg.chat.id, output, true, nil, 'html') + local reslist = {} + for i = 1, limit do + table.insert(reslist, string.format( + '• <a href="%s">%s</a>', + utilities.html_escape(data.d.results[i].Url), + utilities.html_escape(data.d.results[i].Title) + )) + end + local output = string.format( + '<b>Search results for</b> <i>%s</i><b>:</b>\n%s', + utilities.html_escape(input), + table.concat(reslist, '\n') + ) + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end return bing diff --git a/otouto/plugins/blacklist.lua b/otouto/plugins/blacklist.lua index 52e2926..95a405e 100644 --- a/otouto/plugins/blacklist.lua +++ b/otouto/plugins/blacklist.lua @@ -3,92 +3,92 @@ local utilities = require('otouto.utilities') local blacklist = {} function blacklist:init(config) - blacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('blacklist', true):t('unblacklist', true).table - blacklist.error = false + blacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('blacklist', true):t('unblacklist', true).table + blacklist.error = false end function blacklist:action(msg, config) - if msg.from.id ~= config.admin then return true end - local targets = {} - if msg.reply_to_message then - table.insert(targets, { - id = msg.reply_to_message.from.id, - id_str = tostring(msg.reply_to_message.from.id), - name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) - }) - else - local input = utilities.input(msg.text) - if input then - for user in input:gmatch('%g+') do - if self.database.users[user] then - table.insert(targets, { - id = self.database.users[user].id, - id_str = tostring(self.database.users[user].id), - name = utilities.build_name(self.database.users[user].first_name, self.database.users[user].last_name) - }) - elseif tonumber(user) then - local t = { - id_str = user, - id = tonumber(user) - } - if tonumber(user) < 0 then - t.name = 'Group (' .. user .. ')' - else - t.name = 'Unknown (' .. user .. ')' - end - table.insert(targets, t) - elseif user:match('^@') then - local u = utilities.resolve_username(self, user) - if u then - table.insert(targets, { - id = u.id, - id_str = tostring(u.id), - name = utilities.build_name(u.first_name, u.last_name) - }) - else - table.insert(targets, { err = 'Sorry, I do not recognize that username ('..user..').' }) - end - else - table.insert(targets, { err = 'Invalid username or ID ('..user..').' }) - end - end - else - utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID, or a group or groups via ID.') - return - end - end - local output = '' - if msg.text:match('^'..config.cmd_pat..'blacklist') then - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif self.database.blacklist[target.id_str] then - output = output .. target.name .. ' is already blacklisted.\n' - else - self.database.blacklist[target.id_str] = true - output = output .. target.name .. ' is now blacklisted.\n' - if config.drua_block_on_blacklist and target.id > 0 then - require('otouto.drua-tg').block(target.id) - end - end - end - elseif msg.text:match('^'..config.cmd_pat..'unblacklist') then - for _, target in ipairs(targets) do - if target.err then - output = output .. target.err .. '\n' - elseif not self.database.blacklist[target.id_str] then - output = output .. target.name .. ' is not blacklisted.\n' - else - self.database.blacklist[target.id_str] = nil - output = output .. target.name .. ' is no longer blacklisted.\n' - if config.drua_block_on_blacklist and target.id > 0 then - require('otouto.drua-tg').unblock(target.id) - end - end - end - end - utilities.send_reply(self, msg, output) + if msg.from.id ~= config.admin then return true end + local targets = {} + if msg.reply_to_message then + table.insert(targets, { + id = msg.reply_to_message.from.id, + id_str = tostring(msg.reply_to_message.from.id), + name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) + }) + else + local input = utilities.input(msg.text) + if input then + for user in input:gmatch('%g+') do + if self.database.users[user] then + table.insert(targets, { + id = self.database.users[user].id, + id_str = tostring(self.database.users[user].id), + name = utilities.build_name(self.database.users[user].first_name, self.database.users[user].last_name) + }) + elseif tonumber(user) then + local t = { + id_str = user, + id = tonumber(user) + } + if tonumber(user) < 0 then + t.name = 'Group (' .. user .. ')' + else + t.name = 'Unknown (' .. user .. ')' + end + table.insert(targets, t) + elseif user:match('^@') then + local u = utilities.resolve_username(self, user) + if u then + table.insert(targets, { + id = u.id, + id_str = tostring(u.id), + name = utilities.build_name(u.first_name, u.last_name) + }) + else + table.insert(targets, { err = 'Sorry, I do not recognize that username ('..user..').' }) + end + else + table.insert(targets, { err = 'Invalid username or ID ('..user..').' }) + end + end + else + utilities.send_reply(self, msg, 'Please specify a user or users via reply, username, or ID, or a group or groups via ID.') + return + end + end + local output = '' + if msg.text:match('^'..config.cmd_pat..'blacklist') then + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif self.database.blacklist[target.id_str] then + output = output .. target.name .. ' is already blacklisted.\n' + else + self.database.blacklist[target.id_str] = true + output = output .. target.name .. ' is now blacklisted.\n' + if config.drua_block_on_blacklist and target.id > 0 then + require('otouto.drua-tg').block(target.id) + end + end + end + elseif msg.text:match('^'..config.cmd_pat..'unblacklist') then + for _, target in ipairs(targets) do + if target.err then + output = output .. target.err .. '\n' + elseif not self.database.blacklist[target.id_str] then + output = output .. target.name .. ' is not blacklisted.\n' + else + self.database.blacklist[target.id_str] = nil + output = output .. target.name .. ' is no longer blacklisted.\n' + if config.drua_block_on_blacklist and target.id > 0 then + require('otouto.drua-tg').unblock(target.id) + end + end + end + end + utilities.send_reply(self, msg, output) end return blacklist diff --git a/otouto/plugins/calc.lua b/otouto/plugins/calc.lua index 30e2ecd..b0bb305 100644 --- a/otouto/plugins/calc.lua +++ b/otouto/plugins/calc.lua @@ -7,22 +7,22 @@ local utilities = require('otouto.utilities') calc.command = 'calc <expression>' function calc:init(config) - calc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('calc', true).table - calc.doc = config.cmd_pat .. [[calc <expression> + calc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('calc', true).table + calc.doc = config.cmd_pat .. [[calc <expression> Returns solutions to mathematical expressions and conversions between common units. Results provided by mathjs.org.]] end function calc:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, calc.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, calc.doc, true) + return + end - local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input) - local output = HTTPS.request(url) - output = output and '`'..output..'`' or config.errors.connection - utilities.send_reply(self, msg, output, true) + local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input) + local output = HTTPS.request(url) + output = output and '`'..output..'`' or config.errors.connection + utilities.send_reply(self, msg, output, true) end return calc diff --git a/otouto/plugins/catfact.lua b/otouto/plugins/catfact.lua index 25a9bdb..e05e5af 100644 --- a/otouto/plugins/catfact.lua +++ b/otouto/plugins/catfact.lua @@ -7,22 +7,22 @@ local utilities = require('otouto.utilities') local catfact = {} function catfact:init(config) - catfact.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('catfact', true).table - catfact.command = 'catfact' - catfact.doc = 'Returns a cat fact.' - catfact.url = 'http://catfacts-api.appspot.com/api/facts' + catfact.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('catfact', true).table + catfact.command = 'catfact' + catfact.doc = 'Returns a cat fact.' + catfact.url = 'http://catfacts-api.appspot.com/api/facts' end function catfact:action(msg, config) - local jstr, code = HTTP.request(catfact.url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local data = JSON.decode(jstr) - local output = '*Cat Fact*\n_' .. data.facts[1] .. '_' - utilities.send_message(self, msg.chat.id, output, true, nil, true) + local jstr, code = HTTP.request(catfact.url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + local data = JSON.decode(jstr) + local output = '*Cat Fact*\n_' .. data.facts[1] .. '_' + utilities.send_message(self, msg.chat.id, output, true, nil, true) end return catfact diff --git a/otouto/plugins/cats.lua b/otouto/plugins/cats.lua index 7e99bb5..c6374b7 100644 --- a/otouto/plugins/cats.lua +++ b/otouto/plugins/cats.lua @@ -4,12 +4,12 @@ local HTTP = require('socket.http') local utilities = require('otouto.utilities') function cats:init(config) - if not config.thecatapi_key then - print('Missing config value: thecatapi_key.') - print('cats.lua will be enabled, but there are more features with a key.') - end + if not config.thecatapi_key then + print('Missing config value: thecatapi_key.') + print('cats.lua will be enabled, but there are more features with a key.') + end - cats.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cat').table + cats.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cat').table end cats.command = 'cat' @@ -17,21 +17,21 @@ cats.doc = 'Returns a cat!' function cats:action(msg, config) - local url = 'http://thecatapi.com/api/images/get?format=html&type=jpg' - if config.thecatapi_key then - url = url .. '&api_key=' .. config.thecatapi_key - end + local url = 'http://thecatapi.com/api/images/get?format=html&type=jpg' + if config.thecatapi_key then + url = url .. '&api_key=' .. config.thecatapi_key + end - local str, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local str, res = HTTP.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - str = str:match('<img src="(.-)">') - local output = '[Cat!]('..str..')' + str = str:match('<img src="(.-)">') + local output = '[Cat!]('..str..')' - utilities.send_message(self, msg.chat.id, output, false, nil, true) + utilities.send_message(self, msg.chat.id, output, false, nil, true) end diff --git a/otouto/plugins/channel.lua b/otouto/plugins/channel.lua index 4be994e..3ad84af 100644 --- a/otouto/plugins/channel.lua +++ b/otouto/plugins/channel.lua @@ -4,9 +4,9 @@ local bindings = require('otouto.bindings') local utilities = require('otouto.utilities') function channel:init(config) - channel.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ch', true).table - channel.command = 'ch <channel> \\n <message>' - channel.doc = config.cmd_pat .. [[ch <channel> + channel.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ch', true).table + channel.command = 'ch <channel> \\n <message>' + channel.doc = config.cmd_pat .. [[ch <channel> <message> Sends a message to a channel. Channel may be specified via ID or username. Messages are markdown-enabled. Users may only send messages to channels for which they are the owner or an administrator. @@ -20,41 +20,41 @@ The following markdown syntax is supported: end function channel:action(msg, config) - -- An exercise in using zero early returns. :) - local input = utilities.input(msg.text) - local output - if input then - local chat_id = utilities.get_word(input, 1) - local admin_list, t = bindings.getChatAdministrators(self, { chat_id = chat_id } ) - if admin_list then - local is_admin = false - for _, admin in ipairs(admin_list.result) do - if admin.user.id == msg.from.id then - is_admin = true - end - end - if is_admin then - local text = input:match('\n(.+)') - if text then - local success, result = utilities.send_message(self, chat_id, text, true, nil, true) - if success then - output = 'Your message has been sent!' - else - output = 'Sorry, I was unable to send your message.\n`' .. result.description .. '`' - end - else - output = 'Please enter a message to be sent. Markdown is supported.' - end - else - output = 'Sorry, you do not appear to be an administrator for that channel.' - end - else - output = 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`' - end - else - output = channel.doc - end - utilities.send_reply(self, msg, output, true) + -- An exercise in using zero early returns. :) + local input = utilities.input(msg.text) + local output + if input then + local chat_id = utilities.get_word(input, 1) + local admin_list, t = bindings.getChatAdministrators(self, { chat_id = chat_id } ) + if admin_list then + local is_admin = false + for _, admin in ipairs(admin_list.result) do + if admin.user.id == msg.from.id then + is_admin = true + end + end + if is_admin then + local text = input:match('\n(.+)') + if text then + local success, result = utilities.send_message(self, chat_id, text, true, nil, true) + if success then + output = 'Your message has been sent!' + else + output = 'Sorry, I was unable to send your message.\n`' .. result.description .. '`' + end + else + output = 'Please enter a message to be sent. Markdown is supported.' + end + else + output = 'Sorry, you do not appear to be an administrator for that channel.' + end + else + output = 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`' + end + else + output = channel.doc + end + utilities.send_reply(self, msg, output, true) end return channel diff --git a/otouto/plugins/chuckfact.lua b/otouto/plugins/chuckfact.lua index 8cfc2d6..2c287bb 100644 --- a/otouto/plugins/chuckfact.lua +++ b/otouto/plugins/chuckfact.lua @@ -7,22 +7,22 @@ local utilities = require('otouto.utilities') local chuck = {} function chuck:init(config) - chuck.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('chuck', true):t('cn', true):t('chucknorris', true).table - chuck.command = 'chuck' - chuck.doc = 'Returns a fact about Chuck Norris.' - chuck.url = 'http://api.icndb.com/jokes/random' + chuck.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('chuck', true):t('cn', true):t('chucknorris', true).table + chuck.command = 'chuck' + chuck.doc = 'Returns a fact about Chuck Norris.' + chuck.url = 'http://api.icndb.com/jokes/random' end function chuck:action(msg, config) - local jstr, code = HTTP.request(chuck.url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local data = JSON.decode(jstr) - local output = '*Chuck Norris Fact*\n_' .. data.value.joke .. '_' - utilities.send_message(self, msg.chat.id, output, true, nil, true) + local jstr, code = HTTP.request(chuck.url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + local data = JSON.decode(jstr) + local output = '*Chuck Norris Fact*\n_' .. data.value.joke .. '_' + utilities.send_message(self, msg.chat.id, output, true, nil, true) end return chuck diff --git a/otouto/plugins/cleverbot.lua b/otouto/plugins/cleverbot.lua index d859af9..548f9f0 100644 --- a/otouto/plugins/cleverbot.lua +++ b/otouto/plugins/cleverbot.lua @@ -7,30 +7,30 @@ local bindings = require('otouto.bindings') local cleverbot = {} function cleverbot:init(config) - cleverbot.name = '^' .. self.info.first_name:lower() .. ', ' - cleverbot.username = '^@' .. self.info.username:lower() .. ', ' - cleverbot.triggers = { - '^' .. self.info.first_name:lower() .. ', ', - '^@' .. self.info.username:lower() .. ', ' - } - cleverbot.url = config.chatter.cleverbot_api - cleverbot.error = false + cleverbot.name = '^' .. self.info.first_name:lower() .. ', ' + cleverbot.username = '^@' .. self.info.username:lower() .. ', ' + cleverbot.triggers = { + '^' .. self.info.first_name:lower() .. ', ', + '^@' .. self.info.username:lower() .. ', ' + } + cleverbot.url = config.chatter.cleverbot_api + cleverbot.error = false end function cleverbot:action(msg, config) - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) - local input = msg.text_lower:gsub(cleverbot.name, ''):gsub(cleverbot.name, '') - local jstr, code = HTTPS.request(cleverbot.url .. URL.escape(input)) - if code ~= 200 then - utilities.send_message(self, msg.chat.id, config.chatter.connection) - return - end - local data = JSON.decode(jstr) - if not data.clever then - utilities.send_message(self, msg.chat.id, config.chatter.response) - return - end - utilities.send_message(self, msg.chat.id, data.clever) + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) + local input = msg.text_lower:gsub(cleverbot.name, ''):gsub(cleverbot.name, '') + local jstr, code = HTTPS.request(cleverbot.url .. URL.escape(input)) + if code ~= 200 then + utilities.send_message(self, msg.chat.id, config.chatter.connection) + return + end + local data = JSON.decode(jstr) + if not data.clever then + utilities.send_message(self, msg.chat.id, config.chatter.response) + return + end + utilities.send_message(self, msg.chat.id, data.clever) end return cleverbot diff --git a/otouto/plugins/commit.lua b/otouto/plugins/commit.lua index 5087669..7ed5184 100644 --- a/otouto/plugins/commit.lua +++ b/otouto/plugins/commit.lua @@ -8,19 +8,19 @@ commit.command = 'commit' commit.doc = 'Returns a commit message from whatthecommit.com.' function commit:init(config) - commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table + commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table end function commit:action(msg) - bindings.request( - self, - 'sendMessage', - { - chat_id = msg.chat.id, - text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```', - parse_mode = 'Markdown' - } - ) + bindings.request( + self, + 'sendMessage', + { + chat_id = msg.chat.id, + text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```', + parse_mode = 'Markdown' + } + ) end return commit diff --git a/otouto/plugins/control.lua b/otouto/plugins/control.lua index cc4f1e1..b37f876 100644 --- a/otouto/plugins/control.lua +++ b/otouto/plugins/control.lua @@ -6,52 +6,52 @@ local utilities = require('otouto.utilities') local cmd_pat -- Prevents the command from being uncallable. function control:init(config) - cmd_pat = config.cmd_pat - control.triggers = utilities.triggers(self.info.username, cmd_pat, - {'^'..cmd_pat..'script'}):t('reload', true):t('halt').table + cmd_pat = config.cmd_pat + control.triggers = utilities.triggers(self.info.username, cmd_pat, + {'^'..cmd_pat..'script'}):t('reload', true):t('halt').table end function control:action(msg, config) - if msg.from.id ~= config.admin then - return - end + if msg.from.id ~= config.admin then + return + end - if msg.date < os.time() - 2 then return end + if msg.date < os.time() - 2 then return end - if msg.text_lower:match('^'..cmd_pat..'reload') then - for pac, _ in pairs(package.loaded) do - if pac:match('^otouto%.plugins%.') then - package.loaded[pac] = nil - end - end - package.loaded['otouto.bindings'] = nil - package.loaded['otouto.utilities'] = nil - package.loaded['otouto.drua-tg'] = nil - package.loaded['config'] = nil - if not msg.text_lower:match('%-config') then - for k, v in pairs(require('config')) do - config[k] = v - end - end - bot.init(self, config) - utilities.send_reply(self, msg, 'Bot reloaded!') - elseif msg.text_lower:match('^'..cmd_pat..'halt') then - self.is_started = false - utilities.send_reply(self, msg, 'Stopping bot!') - elseif msg.text_lower:match('^'..cmd_pat..'script') then - local input = msg.text_lower:match('^'..cmd_pat..'script\n(.+)') - if not input then - utilities.send_reply(self, msg, 'usage: ```\n'..cmd_pat..'script\n'..cmd_pat..'command <arg>\n...\n```', true) - return - end - input = input .. '\n' - for command in input:gmatch('(.-)\n') do - command = utilities.trim(command) - msg.text = command - bot.on_msg_receive(self, msg, config) - end - end + if msg.text_lower:match('^'..cmd_pat..'reload') then + for pac, _ in pairs(package.loaded) do + if pac:match('^otouto%.plugins%.') then + package.loaded[pac] = nil + end + end + package.loaded['otouto.bindings'] = nil + package.loaded['otouto.utilities'] = nil + package.loaded['otouto.drua-tg'] = nil + package.loaded['config'] = nil + if not msg.text_lower:match('%-config') then + for k, v in pairs(require('config')) do + config[k] = v + end + end + bot.init(self, config) + utilities.send_reply(self, msg, 'Bot reloaded!') + elseif msg.text_lower:match('^'..cmd_pat..'halt') then + self.is_started = false + utilities.send_reply(self, msg, 'Stopping bot!') + elseif msg.text_lower:match('^'..cmd_pat..'script') then + local input = msg.text_lower:match('^'..cmd_pat..'script\n(.+)') + if not input then + utilities.send_reply(self, msg, 'usage: ```\n'..cmd_pat..'script\n'..cmd_pat..'command <arg>\n...\n```', true) + return + end + input = input .. '\n' + for command in input:gmatch('(.-)\n') do + command = utilities.trim(command) + msg.text = command + bot.on_msg_receive(self, msg, config) + end + end end diff --git a/otouto/plugins/currency.lua b/otouto/plugins/currency.lua index 163d041..d156b24 100644 --- a/otouto/plugins/currency.lua +++ b/otouto/plugins/currency.lua @@ -6,8 +6,8 @@ local utilities = require('otouto.utilities') currency.command = 'cash [amount] <from> to <to>' function currency:init(config) - currency.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cash', true).table - currency.doc = config.cmd_pat .. [[cash [amount] <from> to <to> + currency.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cash', true).table + currency.doc = config.cmd_pat .. [[cash [amount] <from> to <to> Example: ]] .. config.cmd_pat .. [[cash 5 USD to EUR Returns exchange rates for various currencies. Source: Google Finance.]] @@ -15,44 +15,44 @@ end function currency:action(msg, config) - local input = msg.text:upper() - if not input:match('%a%a%a TO %a%a%a') then - utilities.send_message(self, msg.chat.id, currency.doc, true, msg.message_id, true) - return - end + local input = msg.text:upper() + if not input:match('%a%a%a TO %a%a%a') then + utilities.send_message(self, msg.chat.id, currency.doc, true, msg.message_id, true) + return + end - local from = input:match('(%a%a%a) TO') - local to = input:match('TO (%a%a%a)') - local amount = utilities.get_word(input, 2) - amount = tonumber(amount) or 1 - local result = 1 + local from = input:match('(%a%a%a) TO') + local to = input:match('TO (%a%a%a)') + local amount = utilities.get_word(input, 2) + amount = tonumber(amount) or 1 + local result = 1 - local url = 'https://www.google.com/finance/converter' + local url = 'https://www.google.com/finance/converter' - if from ~= to then + if from ~= to then - url = url .. '?from=' .. from .. '&to=' .. to .. '&a=' .. amount - local str, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + url = url .. '?from=' .. from .. '&to=' .. to .. '&a=' .. amount + local str, res = HTTPS.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - str = str:match('<span class=bld>(.*) %u+</span>') - if not str then - utilities.send_reply(self, msg, config.errors.results) - return - end + str = str:match('<span class=bld>(.*) %u+</span>') + if not str then + utilities.send_reply(self, msg, config.errors.results) + return + end - result = string.format('%.2f', str) + result = string.format('%.2f', str) - end + end - local output = amount .. ' ' .. from .. ' = ' .. result .. ' ' .. to .. '\n\n' - output = output .. os.date('!%F %T UTC') .. '\nSource: Google Finance`' - output = '```\n' .. output .. '\n```' + local output = amount .. ' ' .. from .. ' = ' .. result .. ' ' .. to .. '\n\n' + output = output .. os.date('!%F %T UTC') .. '\nSource: Google Finance`' + output = '```\n' .. output .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, nil, true) + utilities.send_message(self, msg.chat.id, output, true, nil, true) end diff --git a/otouto/plugins/dice.lua b/otouto/plugins/dice.lua index cdd4d29..07868fa 100644 --- a/otouto/plugins/dice.lua +++ b/otouto/plugins/dice.lua @@ -5,49 +5,49 @@ local utilities = require('otouto.utilities') dice.command = 'roll <nDr>' function dice:init(config) - dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table - dice.doc = config.cmd_pat .. [[roll <nDr> + dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table + dice.doc = config.cmd_pat .. [[roll <nDr> Returns a set of dice rolls, where n is the number of rolls and r is the range. If only a range is given, returns only one roll.]] end function dice:action(msg) - local input = utilities.input(msg.text_lower) - if not input then - utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) - return - end + local input = utilities.input(msg.text_lower) + if not input then + utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) + return + end - local count, range - if input:match('^[%d]+d[%d]+$') then - count, range = input:match('([%d]+)d([%d]+)') - elseif input:match('^d?[%d]+$') then - count = 1 - range = input:match('^d?([%d]+)$') - else - utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) - return - end + local count, range + if input:match('^[%d]+d[%d]+$') then + count, range = input:match('([%d]+)d([%d]+)') + elseif input:match('^d?[%d]+$') then + count = 1 + range = input:match('^d?([%d]+)$') + else + utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) + return + end - count = tonumber(count) - range = tonumber(range) + count = tonumber(count) + range = tonumber(range) - if range < 2 then - utilities.send_reply(self, msg, 'The minimum range is 2.') - return - end - if range > 1000 or count > 1000 then - utilities.send_reply(self, msg, 'The maximum range and count are 1000.') - return - end + if range < 2 then + utilities.send_reply(self, msg, 'The minimum range is 2.') + return + end + if range > 1000 or count > 1000 then + utilities.send_reply(self, msg, 'The maximum range and count are 1000.') + return + end - local output = '*' .. count .. 'd' .. range .. '*\n`' - for _ = 1, count do - output = output .. math.random(range) .. '\t' - end - output = output .. '`' + local output = '*' .. count .. 'd' .. range .. '*\n`' + for _ = 1, count do + output = output .. math.random(range) .. '\t' + end + output = output .. '`' - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) end diff --git a/otouto/plugins/dilbert.lua b/otouto/plugins/dilbert.lua index ff197d5..2075471 100644 --- a/otouto/plugins/dilbert.lua +++ b/otouto/plugins/dilbert.lua @@ -8,8 +8,8 @@ local utilities = require('otouto.utilities') dilbert.command = 'dilbert [date]' function dilbert:init(config) - dilbert.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('dilbert', true).table - dilbert.doc = config.cmd_pat .. [[dilbert [YYYY-MM-DD] + dilbert.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('dilbert', true).table + dilbert.doc = config.cmd_pat .. [[dilbert [YYYY-MM-DD] Returns the latest Dilbert strip or that of the provided date. Dates before the first strip will return the first strip. Dates after the last trip will return the last strip. Source: dilbert.com]] @@ -17,32 +17,32 @@ end function dilbert:action(msg, config) - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' } ) + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' } ) - local input = utilities.input(msg.text) - if not input then input = os.date('%F') end - if not input:match('^%d%d%d%d%-%d%d%-%d%d$') then input = os.date('%F') end + local input = utilities.input(msg.text) + if not input then input = os.date('%F') end + if not input:match('^%d%d%d%d%-%d%d%-%d%d$') then input = os.date('%F') end - local url = 'http://dilbert.com/strip/' .. URL.escape(input) - local str, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local url = 'http://dilbert.com/strip/' .. URL.escape(input) + local str, res = HTTP.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local strip_filename = '/tmp/' .. input .. '.gif' - local strip_file = io.open(strip_filename) - if strip_file then - strip_file:close() - strip_file = strip_filename - else - local strip_url = str:match('<meta property="og:image" content="(.-)"/>') - strip_file = utilities.download_file(strip_url, '/tmp/' .. input .. '.gif') - end + local strip_filename = '/tmp/' .. input .. '.gif' + local strip_file = io.open(strip_filename) + if strip_file then + strip_file:close() + strip_file = strip_filename + else + local strip_url = str:match('<meta property="og:image" content="(.-)"/>') + strip_file = utilities.download_file(strip_url, '/tmp/' .. input .. '.gif') + end - local strip_title = str:match('<meta property="article:publish_date" content="(.-)"/>') + local strip_title = str:match('<meta property="article:publish_date" content="(.-)"/>') - bindings.sendPhoto(self, { chat_id = msg.chat.id, caption = strip_title }, { photo = strip_file } ) + bindings.sendPhoto(self, { chat_id = msg.chat.id, caption = strip_title }, { photo = strip_file } ) end diff --git a/otouto/plugins/echo.lua b/otouto/plugins/echo.lua index 5cc1ad6..d0fabf9 100644 --- a/otouto/plugins/echo.lua +++ b/otouto/plugins/echo.lua @@ -5,25 +5,25 @@ local utilities = require('otouto.utilities') echo.command = 'echo <text>' function echo:init(config) - echo.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('echo', true).table - echo.doc = config.cmd_pat .. 'echo <text> \nRepeats a string of text.' + echo.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('echo', true).table + echo.doc = config.cmd_pat .. 'echo <text> \nRepeats a string of text.' end function echo:action(msg) - local input = utilities.input_from_msg(msg) + local input = utilities.input_from_msg(msg) - if not input then - utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true) - else - local output - if msg.chat.type == 'supergroup' then - output = utilities.style.enquote('Echo', input) - else - output = utilities.md_escape(utilities.char.zwnj..input) - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end + if not input then + utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true) + else + local output + if msg.chat.type == 'supergroup' then + output = utilities.style.enquote('Echo', input) + else + output = utilities.md_escape(utilities.char.zwnj..input) + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end end diff --git a/otouto/plugins/eightball.lua b/otouto/plugins/eightball.lua index 2e01cbd..2dd6dc2 100644 --- a/otouto/plugins/eightball.lua +++ b/otouto/plugins/eightball.lua @@ -6,52 +6,52 @@ eightball.command = '8ball' eightball.doc = 'Returns an answer from a magic 8-ball!' function eightball:init(config) - eightball.triggers = utilities.triggers(self.info.username, config.cmd_pat, - {'[Yy]/[Nn]%p*$'}):t('8ball', true).table + eightball.triggers = utilities.triggers(self.info.username, config.cmd_pat, + {'[Yy]/[Nn]%p*$'}):t('8ball', true).table end local ball_answers = { - "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.", - "There is a time and place for everything, but not now." + "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.", + "There is a time and place for everything, but not now." } local yesno_answers = { - 'Absolutely.', - 'In your dreams.', - 'Yes.', - 'No.' + 'Absolutely.', + 'In your dreams.', + 'Yes.', + 'No.' } function eightball:action(msg) - local output + local output - if msg.text_lower:match('y/n%p?$') then - output = yesno_answers[math.random(#yesno_answers)] - else - output = ball_answers[math.random(#ball_answers)] - end + if msg.text_lower:match('y/n%p?$') then + output = yesno_answers[math.random(#yesno_answers)] + else + output = ball_answers[math.random(#ball_answers)] + end - utilities.send_reply(self, msg, output) + utilities.send_reply(self, msg, output) end diff --git a/otouto/plugins/fortune.lua b/otouto/plugins/fortune.lua index 5d7f0d7..f415f34 100644 --- a/otouto/plugins/fortune.lua +++ b/otouto/plugins/fortune.lua @@ -5,13 +5,13 @@ local fortune = {} local utilities = require('otouto.utilities') function fortune:init(config) - local s = io.popen('fortune'):read('*all') - assert( - not s:match('not found$'), - 'fortune.lua requires the fortune program to be installed.' - ) + local s = io.popen('fortune'):read('*all') + assert( + not s:match('not found$'), + 'fortune.lua requires the fortune program to be installed.' + ) - fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table + fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table end fortune.command = 'fortune' @@ -19,11 +19,11 @@ fortune.doc = 'Returns a UNIX fortune.' function fortune:action(msg) - local fortunef = io.popen('fortune') - local output = fortunef:read('*all') - output = '```\n' .. output .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - fortunef:close() + local fortunef = io.popen('fortune') + local output = fortunef:read('*all') + output = '```\n' .. output .. '\n```' + utilities.send_message(self, msg.chat.id, output, true, nil, true) + fortunef:close() end diff --git a/otouto/plugins/gImages.lua b/otouto/plugins/gImages.lua index 94eadff..add99cb 100644 --- a/otouto/plugins/gImages.lua +++ b/otouto/plugins/gImages.lua @@ -9,58 +9,58 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function gImages:init(config) - assert(config.google_api_key and config.google_cse_key, - 'gImages.lua requires a Google API key from http://console.developers.google.com and a Google Custom Search Engine key from http://cse.google.com/cse.' - ) + assert(config.google_api_key and config.google_cse_key, + 'gImages.lua requires a Google API key from http://console.developers.google.com and a Google Custom Search Engine key from http://cse.google.com/cse.' + ) - gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table - gImages.doc = config.cmd_pat .. [[image <query> + gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table + gImages.doc = config.cmd_pat .. [[image <query> Returns a randomized top result from Google Images. Safe search is enabled by default; use "]] .. config.cmd_pat .. [[insfw" to disable it. NSFW results will not display an image preview. Alias: ]] .. config.cmd_pat .. 'i' - gImages.search_url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key + gImages.search_url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key end gImages.command = 'image <query>' function gImages:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, gImages.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, gImages.doc, true) + return + end - local url = gImages.search_url + local url = gImages.search_url - if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then - url = url .. '&safe=high' - end + if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then + url = url .. '&safe=high' + end - url = url .. '&q=' .. URL.escape(input) + url = url .. '&q=' .. URL.escape(input) - local jstr, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, res = HTTPS.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local jdat = JSON.decode(jstr) - if jdat.searchInformation.totalResults == '0' then - utilities.send_reply(self, msg, config.errors.results) - return - end + local jdat = JSON.decode(jstr) + if jdat.searchInformation.totalResults == '0' then + utilities.send_reply(self, msg, config.errors.results) + return + end - local i = math.random(jdat.queries.request[1].count) - local img_url = jdat.items[i].link - local img_title = jdat.items[i].title - local output = '[' .. img_title .. '](' .. img_url .. ')' + local i = math.random(jdat.queries.request[1].count) + local img_url = jdat.items[i].link + local img_title = jdat.items[i].title + local output = '[' .. img_title .. '](' .. img_url .. ')' - if msg.text:match('nsfw') then - utilities.send_reply(self, '*NSFW*\n'..msg, output) - else - utilities.send_message(self, msg.chat.id, output, false, nil, true) - end + if msg.text:match('nsfw') then + utilities.send_reply(self, '*NSFW*\n'..msg, output) + else + utilities.send_message(self, msg.chat.id, output, false, nil, true) + end end diff --git a/otouto/plugins/gMaps.lua b/otouto/plugins/gMaps.lua index 5838715..ea0baac 100644 --- a/otouto/plugins/gMaps.lua +++ b/otouto/plugins/gMaps.lua @@ -6,34 +6,34 @@ local utilities = require('otouto.utilities') gMaps.command = 'location <query>' function gMaps:init(config) - gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('location', true):t('loc', true).table - gMaps.doc = [[ + gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('location', true):t('loc', true).table + gMaps.doc = [[ /location <query> Returns a location from Google Maps. Alias: /loc - ]] - gMaps.doc = gMaps.doc:gsub('/', config.cmd_pat) + ]] + gMaps.doc = gMaps.doc:gsub('/', config.cmd_pat) end function gMaps:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, gMaps.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, gMaps.doc, true) + return + end - local coords = utilities.get_coords(input, config) - if type(coords) == 'string' then - utilities.send_reply(self, msg, coords) - end + local coords = utilities.get_coords(input, config) + if type(coords) == 'string' then + utilities.send_reply(self, msg, coords) + end - bindings.sendLocation(self, { - chat_id = msg.chat.id, - latitude = coords.lat, - longitude = coords.lon, - reply_to_message_id = msg.message_id - } ) + bindings.sendLocation(self, { + chat_id = msg.chat.id, + latitude = coords.lat, + longitude = coords.lon, + reply_to_message_id = msg.message_id + } ) end return gMaps diff --git a/otouto/plugins/greetings.lua b/otouto/plugins/greetings.lua index 5375e66..53a8910 100644 --- a/otouto/plugins/greetings.lua +++ b/otouto/plugins/greetings.lua @@ -3,30 +3,30 @@ local utilities = require('otouto.utilities') local greetings = {} function greetings:init(config) - greetings.triggers = {} - for _, triggers in pairs(config.greetings) do - for i = 1, #triggers do - triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$' - table.insert(greetings.triggers, triggers[i]) - end - end + greetings.triggers = {} + for _, triggers in pairs(config.greetings) do + for i = 1, #triggers do + triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$' + table.insert(greetings.triggers, triggers[i]) + end + end end function greetings:action(msg, config) - local nick - if self.database.userdata[tostring(msg.from.id)] then - nick = self.database.userdata[tostring(msg.from.id)].nickname - end - nick = nick or utilities.build_name(msg.from.first_name, msg.from.last_name) + local nick + if self.database.userdata[tostring(msg.from.id)] then + nick = self.database.userdata[tostring(msg.from.id)].nickname + end + nick = nick or utilities.build_name(msg.from.first_name, msg.from.last_name) - for response, triggers in pairs(config.greetings) do - for _, trigger in pairs(triggers) do - if string.match(msg.text_lower, trigger) then - utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick)) - return - end - end - end + for response, triggers in pairs(config.greetings) do + for _, trigger in pairs(triggers) do + if string.match(msg.text_lower, trigger) then + utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick)) + return + end + end + end end return greetings diff --git a/otouto/plugins/hackernews.lua b/otouto/plugins/hackernews.lua index 47e8dc2..512154a 100644 --- a/otouto/plugins/hackernews.lua +++ b/otouto/plugins/hackernews.lua @@ -8,68 +8,68 @@ local hackernews = {} hackernews.command = 'hackernews' local function get_hackernews_results() - local results = {} - local jstr, code = HTTPS.request(hackernews.topstories_url) - if code ~= 200 then return end - local data = JSON.decode(jstr) - for i = 1, 8 do - local ijstr, icode = HTTPS.request(hackernews.res_url:format(data[i])) - if icode ~= 200 then return end - local idata = JSON.decode(ijstr) - local result - if idata.url then - result = string.format( - '\n• <code>[</code><a href="%s">%s</a><code>]</code> <a href="%s">%s</a>', - utilities.html_escape(hackernews.art_url:format(idata.id)), - idata.id, - utilities.html_escape(idata.url), - utilities.html_escape(idata.title) - ) - else - result = string.format( - '\n• <code>[</code><a href="%s">%s</a><code>]</code> %s', - utilities.html_escape(hackernews.art_url:format(idata.id)), - idata.id, - utilities.html_escape(idata.title) - ) - end - table.insert(results, result) - end - return results + local results = {} + local jstr, code = HTTPS.request(hackernews.topstories_url) + if code ~= 200 then return end + local data = JSON.decode(jstr) + for i = 1, 8 do + local ijstr, icode = HTTPS.request(hackernews.res_url:format(data[i])) + if icode ~= 200 then return end + local idata = JSON.decode(ijstr) + local result + if idata.url then + result = string.format( + '\n• <code>[</code><a href="%s">%s</a><code>]</code> <a href="%s">%s</a>', + utilities.html_escape(hackernews.art_url:format(idata.id)), + idata.id, + utilities.html_escape(idata.url), + utilities.html_escape(idata.title) + ) + else + result = string.format( + '\n• <code>[</code><a href="%s">%s</a><code>]</code> %s', + utilities.html_escape(hackernews.art_url:format(idata.id)), + idata.id, + utilities.html_escape(idata.title) + ) + end + table.insert(results, result) + end + return results end function hackernews:init(config) - hackernews.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hackernews', true):t('hn', true).table - hackernews.doc = [[Returns four (if group) or eight (if private message) top stories from Hacker News. + hackernews.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hackernews', true):t('hn', true).table + hackernews.doc = [[Returns four (if group) or eight (if private message) top stories from Hacker News. Alias: ]] .. config.cmd_pat .. 'hn' - hackernews.topstories_url = 'https://hacker-news.firebaseio.com/v0/topstories.json' - hackernews.res_url = 'https://hacker-news.firebaseio.com/v0/item/%s.json' - hackernews.art_url = 'https://news.ycombinator.com/item?id=%s' - hackernews.last_update = 0 - if config.hackernews_onstart == true then - hackernews.results = get_hackernews_results() - if hackernews.results then hackernews.last_update = os.time() / 60 end - end + hackernews.topstories_url = 'https://hacker-news.firebaseio.com/v0/topstories.json' + hackernews.res_url = 'https://hacker-news.firebaseio.com/v0/item/%s.json' + hackernews.art_url = 'https://news.ycombinator.com/item?id=%s' + hackernews.last_update = 0 + if config.hackernews_onstart == true then + hackernews.results = get_hackernews_results() + if hackernews.results then hackernews.last_update = os.time() / 60 end + end end function hackernews:action(msg, config) - local now = os.time() / 60 - if not hackernews.results or hackernews.last_update + config.hackernews_interval < now then - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) - hackernews.results = get_hackernews_results() - if not hackernews.results then - utilities.send_reply(self, msg, config.errors.connection) - return - end - hackernews.last_update = now - end - -- Four results in a group, eight in private. - local res_count = msg.chat.id == msg.from.id and 8 or 4 - local output = '<b>Top Stories from Hacker News:</b>' - for i = 1, res_count do - output = output .. hackernews.results[i] - end - utilities.send_message(self, msg.chat.id, output, true, nil, 'html') + local now = os.time() / 60 + if not hackernews.results or hackernews.last_update + config.hackernews_interval < now then + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' }) + hackernews.results = get_hackernews_results() + if not hackernews.results then + utilities.send_reply(self, msg, config.errors.connection) + return + end + hackernews.last_update = now + end + -- Four results in a group, eight in private. + local res_count = msg.chat.id == msg.from.id and 8 or 4 + local output = '<b>Top Stories from Hacker News:</b>' + for i = 1, res_count do + output = output .. hackernews.results[i] + end + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end return hackernews diff --git a/otouto/plugins/hearthstone.lua b/otouto/plugins/hearthstone.lua index 05cbca8..2cee7a6 100644 --- a/otouto/plugins/hearthstone.lua +++ b/otouto/plugins/hearthstone.lua @@ -8,111 +8,111 @@ local utilities = require('otouto.utilities') local HTTPS = require('ssl.https') function hearthstone:init(config) - hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table - hearthstone.command = 'hearthstone <query>' + hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table + hearthstone.command = 'hearthstone <query>' - if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then + if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then - print('Downloading Hearthstone database...') + print('Downloading Hearthstone database...') - local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json') - if not jstr or res ~= 200 then - print('Error connecting to hearthstonejson.com.') - print('hearthstone.lua will not be enabled.') - hearthstone.command = nil - hearthstone.triggers = nil - return - end - self.database.hearthstone = JSON.decode(jstr) - self.database.hearthstone.expiration = os.time() + 600000 + local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json') + if not jstr or res ~= 200 then + print('Error connecting to hearthstonejson.com.') + print('hearthstone.lua will not be enabled.') + hearthstone.command = nil + hearthstone.triggers = nil + return + end + self.database.hearthstone = JSON.decode(jstr) + self.database.hearthstone.expiration = os.time() + 600000 - print('Download complete! It will be stored for a week.') + print('Download complete! It will be stored for a week.') - end + end - hearthstone.doc = config.cmd_pat .. [[hearthstone <query> + hearthstone.doc = config.cmd_pat .. [[hearthstone <query> Returns Hearthstone card info. Alias: ]] .. config.cmd_pat .. 'hs' end local function format_card(card) - local ctype = card.type - if card.race then - ctype = card.race - end - if card.rarity then - ctype = card.rarity .. ' ' .. ctype - end - if card.playerClass then - ctype = ctype .. ' (' .. card.playerClass .. ')' - elseif card.faction then - ctype = ctype .. ' (' .. card.faction .. ')' - end + local ctype = card.type + if card.race then + ctype = card.race + end + if card.rarity then + ctype = card.rarity .. ' ' .. ctype + end + if card.playerClass then + ctype = ctype .. ' (' .. card.playerClass .. ')' + elseif card.faction then + ctype = ctype .. ' (' .. card.faction .. ')' + end - local stats - if card.cost then - stats = card.cost .. 'c' - if card.attack then - stats = stats .. ' | ' .. card.attack .. 'a' - end - if card.health then - stats = stats .. ' | ' .. card.health .. 'h' - end - if card.durability then - stats = stats .. ' | ' .. card.durability .. 'd' - end - elseif card.health then - stats = card.health .. 'h' - end + local stats + if card.cost then + stats = card.cost .. 'c' + if card.attack then + stats = stats .. ' | ' .. card.attack .. 'a' + end + if card.health then + stats = stats .. ' | ' .. card.health .. 'h' + end + if card.durability then + stats = stats .. ' | ' .. card.durability .. 'd' + end + elseif card.health then + stats = card.health .. 'h' + end - -- unused? - local info - if card.text then - info = card.text:gsub('</?.->',''):gsub('%$','') - if card.flavor then - info = info .. '\n_' .. card.flavor .. '_' - end - elseif card.flavor then - info = card.flavor - else - info = nil - end + -- unused? + local info + if card.text then + info = card.text:gsub('</?.->',''):gsub('%$','') + if card.flavor then + info = info .. '\n_' .. card.flavor .. '_' + end + elseif card.flavor then + info = card.flavor + else + info = nil + end - local s = '*' .. card.name .. '*\n' .. ctype - if stats then - s = s .. '\n' .. stats - end - if info then - s = s .. '\n' .. info - end + local s = '*' .. card.name .. '*\n' .. ctype + if stats then + s = s .. '\n' .. stats + end + if info then + s = s .. '\n' .. info + end - return s + return s end function hearthstone:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, hearthstone.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, hearthstone.doc, true) + return + end - local output = '' - for _,v in pairs(self.database.hearthstone) do - if type(v) == 'table' and string.lower(v.name):match(input) then - output = output .. format_card(v) .. '\n\n' - end - end + local output = '' + for _,v in pairs(self.database.hearthstone) do + if type(v) == 'table' and string.lower(v.name):match(input) then + output = output .. format_card(v) .. '\n\n' + end + end - output = utilities.trim(output) - if output:len() == 0 then - utilities.send_reply(self, msg, config.errors.results) - return - end + output = utilities.trim(output) + if output:len() == 0 then + utilities.send_reply(self, msg, config.errors.results) + return + end - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) end diff --git a/otouto/plugins/help.lua b/otouto/plugins/help.lua index 8b2ed71..f8c8124 100644 --- a/otouto/plugins/help.lua +++ b/otouto/plugins/help.lua @@ -3,51 +3,51 @@ local utilities = require('otouto.utilities') local help = {} function help:init(config) - help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('help', true):t('h', true).table - help.command = 'help [command]' - help.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.' + help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('help', true):t('h', true).table + help.command = 'help [command]' + help.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.' end function help:action(msg, config) - local input = utilities.input(msg.text_lower) - if input then - if not help.help_word then - for _, plugin in ipairs(self.plugins) do - if plugin.command and plugin.doc and not plugin.help_word then - plugin.help_word = utilities.get_word(plugin.command, 1) - end - end - end - for _,plugin in ipairs(self.plugins) do - if plugin.help_word == input:gsub('^/', '') then - local output = '*Help for* _' .. plugin.help_word .. '_*:*\n' .. plugin.doc - utilities.send_message(self, msg.chat.id, output, true, nil, true) - return - end - end - utilities.send_reply(self, msg, 'Sorry, there is no help for that command.') - else - -- Generate the help message on first run. - if not help.text then - local commandlist = {} - for _, plugin in ipairs(self.plugins) do - if plugin.command then - table.insert(commandlist, plugin.command) - end - end - table.sort(commandlist) - help.text = '*Available commands:*\n• ' .. config.cmd_pat .. table.concat(commandlist, '\n• '..config.cmd_pat) .. '\nArguments: <required> [optional]' - help.text = help.text:gsub('%[', '\\[') - end - -- Attempt to send the help message via PM. - -- If msg is from a group, tell the group whether the PM was successful. - local res = utilities.send_message(self, msg.from.id, help.text, true, nil, true) - if not res then - utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) - elseif msg.chat.type ~= 'private' then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - end + local input = utilities.input(msg.text_lower) + if input then + if not help.help_word then + for _, plugin in ipairs(self.plugins) do + if plugin.command and plugin.doc and not plugin.help_word then + plugin.help_word = utilities.get_word(plugin.command, 1) + end + end + end + for _,plugin in ipairs(self.plugins) do + if plugin.help_word == input:gsub('^/', '') then + local output = '*Help for* _' .. plugin.help_word .. '_*:*\n' .. plugin.doc + utilities.send_message(self, msg.chat.id, output, true, nil, true) + return + end + end + utilities.send_reply(self, msg, 'Sorry, there is no help for that command.') + else + -- Generate the help message on first run. + if not help.text then + local commandlist = {} + for _, plugin in ipairs(self.plugins) do + if plugin.command then + table.insert(commandlist, plugin.command) + end + end + table.sort(commandlist) + help.text = '*Available commands:*\n• ' .. config.cmd_pat .. table.concat(commandlist, '\n• '..config.cmd_pat) .. '\nArguments: <required> [optional]' + help.text = help.text:gsub('%[', '\\[') + end + -- Attempt to send the help message via PM. + -- If msg is from a group, tell the group whether the PM was successful. + local res = utilities.send_message(self, msg.from.id, help.text, true, nil, true) + if not res then + utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true) + elseif msg.chat.type ~= 'private' then + utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') + end + end end return help diff --git a/otouto/plugins/id.lua b/otouto/plugins/id.lua index d077573..fd7568a 100644 --- a/otouto/plugins/id.lua +++ b/otouto/plugins/id.lua @@ -3,60 +3,60 @@ local utilities = require('otouto.utilities') local id = {} function id:init(config) - id.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('id', true).table - id.command = 'id <user>' - id.doc = config.cmd_pat .. [[id <user> ... + id.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('id', true).table + id.command = 'id <user>' + id.doc = config.cmd_pat .. [[id <user> ... Returns the name, ID, and username (if applicable) for the given users. Arguments must be usernames and/or IDs. Input is also accepted via reply. If no input is given, returns info for the user. - ]] + ]] end function id.format(t) - if t.username then - return string.format( - '@%s, AKA <b>%s</b> <code>[%s]</code>.\n', - t.username, - utilities.build_name(t.first_name, t.last_name), - t.id - ) - else - return string.format( - '<b>%s</b> <code>[%s]</code>.\n', - utilities.build_name(t.first_name, t.last_name), - t.id - ) - end + if t.username then + return string.format( + '@%s, AKA <b>%s</b> <code>[%s]</code>.\n', + t.username, + utilities.build_name(t.first_name, t.last_name), + t.id + ) + else + return string.format( + '<b>%s</b> <code>[%s]</code>.\n', + utilities.build_name(t.first_name, t.last_name), + t.id + ) + end end function id:action(msg) - local output - local input = utilities.input(msg.text) - if msg.reply_to_message then - output = id.format(msg.reply_to_message.from) - elseif input then - output = '' - for user in input:gmatch('%g+') do - if tonumber(user) then - if self.database.users[user] then - output = output .. id.format(self.database.users[user]) - else - output = output .. 'I don\'t recognize that ID (' .. user .. ').\n' - end - elseif user:match('^@') then - local t = utilities.resolve_username(self, user) - if t then - output = output .. id.format(t) - else - output = output .. 'I don\'t recognize that username (' .. user .. ').\n' - end - else - output = output .. 'Invalid username or ID (' .. user .. ').\n' - end - end - else - output = id.format(msg.from) - end - utilities.send_reply(self, msg, output, 'html') + local output + local input = utilities.input(msg.text) + if msg.reply_to_message then + output = id.format(msg.reply_to_message.from) + elseif input then + output = '' + for user in input:gmatch('%g+') do + if tonumber(user) then + if self.database.users[user] then + output = output .. id.format(self.database.users[user]) + else + output = output .. 'I don\'t recognize that ID (' .. user .. ').\n' + end + elseif user:match('^@') then + local t = utilities.resolve_username(self, user) + if t then + output = output .. id.format(t) + else + output = output .. 'I don\'t recognize that username (' .. user .. ').\n' + end + else + output = output .. 'Invalid username or ID (' .. user .. ').\n' + end + end + else + output = id.format(msg.from) + end + utilities.send_reply(self, msg, output, 'html') end return id diff --git a/otouto/plugins/imdb.lua b/otouto/plugins/imdb.lua index 1f91930..e748a35 100644 --- a/otouto/plugins/imdb.lua +++ b/otouto/plugins/imdb.lua @@ -8,39 +8,39 @@ local utilities = require('otouto.utilities') imdb.command = 'imdb <query>' function imdb:init(config) - imdb.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('imdb', true).table - imdb.doc = config.cmd_pat .. 'imdb <query> \nReturns an IMDb entry.' + imdb.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('imdb', true).table + imdb.doc = config.cmd_pat .. 'imdb <query> \nReturns an IMDb entry.' end function imdb:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, imdb.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, imdb.doc, true) + return + end - local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input) + local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input) - local jstr, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, res = HTTP.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local jdat = JSON.decode(jstr) + local jdat = JSON.decode(jstr) - if jdat.Response ~= 'True' then - utilities.send_reply(self, msg, config.errors.results) - return - end + if jdat.Response ~= 'True' then + utilities.send_reply(self, msg, config.errors.results) + return + end - local output = '*' .. jdat.Title .. ' ('.. jdat.Year ..')*\n' - output = output .. jdat.imdbRating ..'/10 | '.. jdat.Runtime ..' | '.. jdat.Genre ..'\n' - output = output .. '_' .. jdat.Plot .. '_\n' - output = output .. '[Read more.](http://imdb.com/title/' .. jdat.imdbID .. ')' + local output = '*' .. jdat.Title .. ' ('.. jdat.Year ..')*\n' + output = output .. jdat.imdbRating ..'/10 | '.. jdat.Runtime ..' | '.. jdat.Genre ..'\n' + output = output .. '_' .. jdat.Plot .. '_\n' + output = output .. '[Read more.](http://imdb.com/title/' .. jdat.imdbID .. ')' - utilities.send_message(self, msg.chat.id, output, true, nil, true) + utilities.send_message(self, msg.chat.id, output, true, nil, true) end diff --git a/otouto/plugins/isup.lua b/otouto/plugins/isup.lua index bdda1ae..b0f37d6 100644 --- a/otouto/plugins/isup.lua +++ b/otouto/plugins/isup.lua @@ -7,37 +7,37 @@ local utilities = require('otouto.utilities') local isup = {} function isup:init(config) - isup.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('websitedown', true):t('isitup', true):t('isup', true).table + isup.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('websitedown', true):t('isitup', true):t('isup', true).table - isup.doc = config.cmd_pat .. [[isup <url> + isup.doc = config.cmd_pat .. [[isup <url> Returns the up or down status of a website.]] - isup.command = 'isup <url>' + isup.command = 'isup <url>' end function isup:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, isup.doc) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, isup.doc) + return + end - local protocol = HTTP - local url_lower = input:lower() - if url_lower:match('^https') then - protocol = HTTPS - elseif not url_lower:match('^http') then - input = 'http://' .. input - end - local _, code = protocol.request(input) - code = tonumber(code) - local output - if not code or code > 399 then - output = 'This website is down or nonexistent.' - else - output = 'This website is up.' - end - utilities.send_reply(self, msg, output, true) + local protocol = HTTP + local url_lower = input:lower() + if url_lower:match('^https') then + protocol = HTTPS + elseif not url_lower:match('^http') then + input = 'http://' .. input + end + local _, code = protocol.request(input) + code = tonumber(code) + local output + if not code or code > 399 then + output = 'This website is down or nonexistent.' + else + output = 'This website is up.' + end + utilities.send_reply(self, msg, output, true) end return isup diff --git a/otouto/plugins/lastfm.lua b/otouto/plugins/lastfm.lua index 76b47ed..2c14b2d 100644 --- a/otouto/plugins/lastfm.lua +++ b/otouto/plugins/lastfm.lua @@ -9,12 +9,12 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function lastfm:init(config) - assert(config.lastfm_api_key, - 'lastfm.lua requires a last.fm API key from http://last.fm/api.' - ) + assert(config.lastfm_api_key, + 'lastfm.lua requires a last.fm API key from http://last.fm/api.' + ) - lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table - lastfm.doc = config.cmd_pat .. [[np [username] + lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table + lastfm.doc = config.cmd_pat .. [[np [username] Returns what you are or were last listening to. If you specify a username, info will be returned for that username. ]] .. config.cmd_pat .. [[fmset <username> @@ -25,84 +25,84 @@ lastfm.command = 'lastfm' function lastfm:action(msg, config) - local input = utilities.input(msg.text) - local from_id_str = tostring(msg.from.id) - self.database.userdata[from_id_str] = self.database.userdata[from_id_str] or {} + local input = utilities.input(msg.text) + local from_id_str = tostring(msg.from.id) + self.database.userdata[from_id_str] = self.database.userdata[from_id_str] or {} - if string.match(msg.text, '^'..config.cmd_pat..'lastfm') then - utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) - return - elseif string.match(msg.text, '^'..config.cmd_pat..'fmset') then - if not input then - utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) - elseif input == '--' or input == utilities.char.em_dash then - self.database.userdata[from_id_str].lastfm = nil - utilities.send_reply(self, msg, 'Your last.fm username has been forgotten.') - else - self.database.userdata[from_id_str].lastfm = input - utilities.send_reply(self, msg, 'Your last.fm username has been set to "' .. input .. '".') - end - return - end + if string.match(msg.text, '^'..config.cmd_pat..'lastfm') then + utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) + return + elseif string.match(msg.text, '^'..config.cmd_pat..'fmset') then + if not input then + utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) + elseif input == '--' or input == utilities.char.em_dash then + self.database.userdata[from_id_str].lastfm = nil + utilities.send_reply(self, msg, 'Your last.fm username has been forgotten.') + else + self.database.userdata[from_id_str].lastfm = input + utilities.send_reply(self, msg, 'Your last.fm username has been set to "' .. input .. '".') + end + return + end - local url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' .. config.lastfm_api_key .. '&user=' + local url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' .. config.lastfm_api_key .. '&user=' - local username - local alert = '' - if input then - username = input - elseif self.database.userdata[from_id_str].lastfm then - username = self.database.userdata[from_id_str].lastfm - elseif msg.from.username then - username = msg.from.username - alert = '\n\nYour username has been set to ' .. username .. '.\nTo change it, use '..config.cmd_pat..'fmset <username>.' - self.database.userdata[from_id_str].lastfm = username - else - utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') - return - end + local username + local alert = '' + if input then + username = input + elseif self.database.userdata[from_id_str].lastfm then + username = self.database.userdata[from_id_str].lastfm + elseif msg.from.username then + username = msg.from.username + alert = '\n\nYour username has been set to ' .. username .. '.\nTo change it, use '..config.cmd_pat..'fmset <username>.' + self.database.userdata[from_id_str].lastfm = username + else + utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') + return + end - url = url .. URL.escape(username) + url = url .. URL.escape(username) - local jstr, res - utilities.with_http_timeout( - 1, function () - jstr, res = HTTP.request(url) - end) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, res + utilities.with_http_timeout( + 1, function () + jstr, res = HTTP.request(url) + end) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local jdat = JSON.decode(jstr) - if jdat.error then - utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') - return - end + local jdat = JSON.decode(jstr) + if jdat.error then + utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') + return + end - jdat = jdat.recenttracks.track[1] or jdat.recenttracks.track - if not jdat then - utilities.send_reply(self, msg, 'No history for this user.' .. alert) - return - end + jdat = jdat.recenttracks.track[1] or jdat.recenttracks.track + if not jdat then + utilities.send_reply(self, msg, 'No history for this user.' .. alert) + return + end - local output = input or msg.from.first_name - output = '🎵 ' .. output + local output = input or msg.from.first_name + output = '🎵 ' .. output - if jdat['@attr'] and jdat['@attr'].nowplaying then - output = output .. ' is currently listening to:\n' - else - output = output .. ' last listened to:\n' - end + if jdat['@attr'] and jdat['@attr'].nowplaying then + output = output .. ' is currently listening to:\n' + else + output = output .. ' last listened to:\n' + end - local title = jdat.name or 'Unknown' - local artist = 'Unknown' - if jdat.artist then - artist = jdat.artist['#text'] - end + local title = jdat.name or 'Unknown' + local artist = 'Unknown' + if jdat.artist then + artist = jdat.artist['#text'] + end - output = output .. title .. ' - ' .. artist .. alert - utilities.send_message(self, msg.chat.id, output) + output = output .. title .. ' - ' .. artist .. alert + utilities.send_message(self, msg.chat.id, output) end diff --git a/otouto/plugins/luarun.lua b/otouto/plugins/luarun.lua index dfa4b96..add9222 100644 --- a/otouto/plugins/luarun.lua +++ b/otouto/plugins/luarun.lua @@ -5,59 +5,59 @@ local URL = require('socket.url') local JSON, serpent function luarun:init(config) - luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table - if config.luarun_serpent then - serpent = require('serpent') - luarun.serialize = function(t) - return serpent.block(t, {comment=false}) - end - else - JSON = require('dkjson') - luarun.serialize = function(t) - return JSON.encode(t, {indent=true}) - end - end + luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table + if config.luarun_serpent then + serpent = require('serpent') + luarun.serialize = function(t) + return serpent.block(t, {comment=false}) + end + else + JSON = require('dkjson') + luarun.serialize = function(t) + return JSON.encode(t, {indent=true}) + end + end end function luarun:action(msg, config) - if msg.from.id ~= config.admin then - return true - end + if msg.from.id ~= config.admin then + return true + end - local input = utilities.input(msg.text) - if not input then - utilities.send_reply(self, msg, 'Please enter a string to load.') - return - end + local input = utilities.input(msg.text) + if not input then + utilities.send_reply(self, msg, 'Please enter a string to load.') + return + end - if msg.text_lower:match('^'..config.cmd_pat..'return') then - input = 'return ' .. input - end + if msg.text_lower:match('^'..config.cmd_pat..'return') then + input = 'return ' .. input + end - local output = loadstring( [[ - local bot = require('otouto.bot') - local bindings = require('otouto.bindings') - local utilities = require('otouto.utilities') - local drua = require('otouto.drua-tg') - local JSON = require('dkjson') - local URL = require('socket.url') - local HTTP = require('socket.http') - local HTTPS = require('ssl.https') - return function (self, msg, config) ]] .. input .. [[ end - ]] )()(self, msg, config) - if output == nil then - output = 'Done!' - else - if type(output) == 'table' then - local s = luarun.serialize(output) - if URL.escape(s):len() < 4000 then - output = s - end - end - output = '```\n' .. tostring(output) .. '\n```' - end - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + local output = loadstring( [[ + local bot = require('otouto.bot') + local bindings = require('otouto.bindings') + local utilities = require('otouto.utilities') + local drua = require('otouto.drua-tg') + local JSON = require('dkjson') + local URL = require('socket.url') + local HTTP = require('socket.http') + local HTTPS = require('ssl.https') + return function (self, msg, config) ]] .. input .. [[ end + ]] )()(self, msg, config) + if output == nil then + output = 'Done!' + else + if type(output) == 'table' then + local s = luarun.serialize(output) + if URL.escape(s):len() < 4000 then + output = s + end + end + output = '```\n' .. tostring(output) .. '\n```' + end + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) end diff --git a/otouto/plugins/me.lua b/otouto/plugins/me.lua index 67afd38..668ecb4 100644 --- a/otouto/plugins/me.lua +++ b/otouto/plugins/me.lua @@ -3,65 +3,65 @@ local me = {} local utilities = require('otouto.utilities') function me:init(config) - me.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('me', true).table - me.command = 'me' - me.doc = 'Returns userdata stored by the bot.' + me.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('me', true).table + me.command = 'me' + me.doc = 'Returns userdata stored by the bot.' end function me:action(msg, config) - local user - if msg.from.id == config.admin then - if msg.reply_to_message then - user = msg.reply_to_message.from - else - local input = utilities.input(msg.text) - if input then - if tonumber(input) then - user = self.database.users[input] - if not user then - utilities.send_reply(self, msg, 'Unrecognized ID.') - return - end - elseif input:match('^@') then - user = utilities.resolve_username(self, input) - if not user then - utilities.send_reply(self, msg, 'Unrecognized username.') - return - end - else - utilities.send_reply(self, msg, 'Invalid username or ID.') - return - end - end - end - end - user = user or msg.from - local userdata = self.database.userdata[tostring(user.id)] or {} + local user + if msg.from.id == config.admin then + if msg.reply_to_message then + user = msg.reply_to_message.from + else + local input = utilities.input(msg.text) + if input then + if tonumber(input) then + user = self.database.users[input] + if not user then + utilities.send_reply(self, msg, 'Unrecognized ID.') + return + end + elseif input:match('^@') then + user = utilities.resolve_username(self, input) + if not user then + utilities.send_reply(self, msg, 'Unrecognized username.') + return + end + else + utilities.send_reply(self, msg, 'Invalid username or ID.') + return + end + end + end + end + user = user or msg.from + local userdata = self.database.userdata[tostring(user.id)] or {} - local data = {} - for k,v in pairs(userdata) do - table.insert(data, string.format( - '<b>%s</b> <code>%s</code>\n', - utilities.html_escape(k), - utilities.html_escape(v) - )) - end + local data = {} + for k,v in pairs(userdata) do + table.insert(data, string.format( + '<b>%s</b> <code>%s</code>\n', + utilities.html_escape(k), + utilities.html_escape(v) + )) + end - local output - if #data == 0 then - output = 'There is no data stored for this user.' - else - output = string.format( - '<b>%s</b> <code>[%s]</code><b>:</b>\n', - utilities.html_escape(utilities.build_name( - user.first_name, - user.last_name - )), - user.id - ) .. table.concat(data) - end + local output + if #data == 0 then + output = 'There is no data stored for this user.' + else + output = string.format( + '<b>%s</b> <code>[%s]</code><b>:</b>\n', + utilities.html_escape(utilities.build_name( + user.first_name, + user.last_name + )), + user.id + ) .. table.concat(data) + end - utilities.send_message(self, msg.chat.id, output, true, nil, 'html') + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end diff --git a/otouto/plugins/nick.lua b/otouto/plugins/nick.lua index 9812aa7..69e3e53 100644 --- a/otouto/plugins/nick.lua +++ b/otouto/plugins/nick.lua @@ -5,45 +5,45 @@ local utilities = require('otouto.utilities') nick.command = 'nick <nickname>' function nick:init(config) - nick.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('nick', true).table - nick.doc = config.cmd_pat .. [[nick <nickname> + nick.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('nick', true).table + nick.doc = config.cmd_pat .. [[nick <nickname> Set your nickname. Use "]] .. config.cmd_pat .. 'nick --" to delete it.' end function nick:action(msg, config) - local id_str, name + local id_str, name - if msg.from.id == config.admin and msg.reply_to_message then - id_str = tostring(msg.reply_to_message.from.id) - name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) - else - id_str = tostring(msg.from.id) - name = utilities.build_name(msg.from.first_name, msg.from.last_name) - end + if msg.from.id == config.admin and msg.reply_to_message then + id_str = tostring(msg.reply_to_message.from.id) + name = utilities.build_name(msg.reply_to_message.from.first_name, msg.reply_to_message.from.last_name) + else + id_str = tostring(msg.from.id) + name = utilities.build_name(msg.from.first_name, msg.from.last_name) + end - self.database.userdata[id_str] = self.database.userdata[id_str] or {} + self.database.userdata[id_str] = self.database.userdata[id_str] or {} - local output - local input = utilities.input(msg.text) - if not input then - if self.database.userdata[id_str].nickname then - output = name .. '\'s nickname is "' .. self.database.userdata[id_str].nickname .. '".' - else - output = name .. ' currently has no nickname.' - end - elseif utilities.utf8_len(input) > 32 then - output = 'The character limit for nicknames is 32.' - elseif input == '--' or input == utilities.char.em_dash then - self.database.userdata[id_str].nickname = nil - output = name .. '\'s nickname has been deleted.' - else - input = input:gsub('\n', ' ') - self.database.userdata[id_str].nickname = input - output = name .. '\'s nickname has been set to "' .. input .. '".' - end + local output + local input = utilities.input(msg.text) + if not input then + if self.database.userdata[id_str].nickname then + output = name .. '\'s nickname is "' .. self.database.userdata[id_str].nickname .. '".' + else + output = name .. ' currently has no nickname.' + end + elseif utilities.utf8_len(input) > 32 then + output = 'The character limit for nicknames is 32.' + elseif input == '--' or input == utilities.char.em_dash then + self.database.userdata[id_str].nickname = nil + output = name .. '\'s nickname has been deleted.' + else + input = input:gsub('\n', ' ') + self.database.userdata[id_str].nickname = input + output = name .. '\'s nickname has been set to "' .. input .. '".' + end - utilities.send_reply(self, msg, output) + utilities.send_reply(self, msg, output) end diff --git a/otouto/plugins/patterns.lua b/otouto/plugins/patterns.lua index 7480cf2..d23b8e9 100644 --- a/otouto/plugins/patterns.lua +++ b/otouto/plugins/patterns.lua @@ -8,34 +8,34 @@ patterns.doc = [[ s/<pattern>/<substitution> Replace all matches for the given pattern. Uses Lua patterns. - ]] + ]] function patterns:init(config) - patterns.triggers = { config.cmd_pat .. '?s/.-/.-$' } + patterns.triggers = { config.cmd_pat .. '?s/.-/.-$' } end function patterns:action(msg) - if not msg.reply_to_message then return true end - local output = msg.reply_to_message.text - if msg.reply_to_message.from.id == self.info.id then - output = output:gsub('Did you mean:\n"', '') - output = output:gsub('"$', '') - end - local m1, m2 = msg.text:match('^/?s/(.-)/(.-)/?$') - if not m2 then return true end - local res - res, output = pcall( - function() - return output:gsub(m1, m2) - end - ) - if res == false then - utilities.send_reply(self, msg, 'Malformed pattern!') - else - output = utilities.trim(output:sub(1, 4000)) - output = utilities.style.enquote('Did you mean', output) - utilities.send_reply(self, msg.reply_to_message, output, true) - end + if not msg.reply_to_message then return true end + local output = msg.reply_to_message.text + if msg.reply_to_message.from.id == self.info.id then + output = output:gsub('Did you mean:\n"', '') + output = output:gsub('"$', '') + end + local m1, m2 = msg.text:match('^/?s/(.-)/(.-)/?$') + if not m2 then return true end + local res + res, output = pcall( + function() + return output:gsub(m1, m2) + end + ) + if res == false then + utilities.send_reply(self, msg, 'Malformed pattern!') + else + output = utilities.trim(output:sub(1, 4000)) + output = utilities.style.enquote('Did you mean', output) + utilities.send_reply(self, msg.reply_to_message, output, true) + end end return patterns diff --git a/otouto/plugins/ping.lua b/otouto/plugins/ping.lua index c368c33..26913e6 100644 --- a/otouto/plugins/ping.lua +++ b/otouto/plugins/ping.lua @@ -5,12 +5,12 @@ local ping = {} local utilities = require('otouto.utilities') function ping:init(config) - ping.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ping'):t('annyong').table + ping.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ping'):t('annyong').table end function ping:action(msg, config) - local output = msg.text_lower:match('^'..config.cmd_pat..'ping') and 'Pong!' or 'Annyong.' - utilities.send_message(self, msg.chat.id, output) + local output = msg.text_lower:match('^'..config.cmd_pat..'ping') and 'Pong!' or 'Annyong.' + utilities.send_message(self, msg.chat.id, output) end return ping diff --git a/otouto/plugins/pokedex.lua b/otouto/plugins/pokedex.lua index 8942e60..8e08927 100644 --- a/otouto/plugins/pokedex.lua +++ b/otouto/plugins/pokedex.lua @@ -8,8 +8,8 @@ local utilities = require('otouto.utilities') pokedex.command = 'pokedex <query>' function pokedex:init(config) - pokedex.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pokedex', true):t('dex', true).table - pokedex.doc = config.cmd_pat .. [[pokedex <query> + pokedex.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pokedex', true):t('dex', true).table + pokedex.doc = config.cmd_pat .. [[pokedex <query> Returns a Pokedex entry from pokeapi.co. Queries must be a number of the name of a Pokémon. Alias: ]] .. config.cmd_pat .. 'dex' @@ -17,54 +17,54 @@ end function pokedex:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, pokedex.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, pokedex.doc, true) + return + end - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) - local url = 'http://pokeapi.co' + local url = 'http://pokeapi.co' - local dex_url = url .. '/api/v1/pokemon/' .. input - local dex_jstr, res = HTTP.request(dex_url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local dex_url = url .. '/api/v1/pokemon/' .. input + local dex_jstr, res = HTTP.request(dex_url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local dex_jdat = JSON.decode(dex_jstr) + local dex_jdat = JSON.decode(dex_jstr) - if not dex_jdat.descriptions or not dex_jdat.descriptions[1] then - utilities.send_reply(self, msg, config.errors.results) - return - end + if not dex_jdat.descriptions or not dex_jdat.descriptions[1] then + utilities.send_reply(self, msg, config.errors.results) + return + end - local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri - local desc_jstr, _ = HTTP.request(desc_url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri + local desc_jstr, _ = HTTP.request(desc_url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local desc_jdat = JSON.decode(desc_jstr) + local desc_jdat = JSON.decode(desc_jstr) - local poke_type - for _,v in ipairs(dex_jdat.types) do - local type_name = v.name:gsub("^%l", string.upper) - if not poke_type then - poke_type = type_name - else - poke_type = poke_type .. ' / ' .. type_name - end - end - poke_type = poke_type .. ' type' + local poke_type + for _,v in ipairs(dex_jdat.types) do + local type_name = v.name:gsub("^%l", string.upper) + if not poke_type then + poke_type = type_name + else + poke_type = poke_type .. ' / ' .. type_name + end + end + poke_type = poke_type .. ' type' - local output = '*' .. dex_jdat.name .. '*\n#' .. dex_jdat.national_id .. ' | ' .. poke_type .. '\n_' .. desc_jdat.description:gsub('POKMON', 'Pokémon'):gsub('Pokmon', 'Pokémon') .. '_' + local output = '*' .. dex_jdat.name .. '*\n#' .. dex_jdat.national_id .. ' | ' .. poke_type .. '\n_' .. desc_jdat.description:gsub('POKMON', 'Pokémon'):gsub('Pokmon', 'Pokémon') .. '_' - utilities.send_message(self, msg.chat.id, output, true, nil, true) + utilities.send_message(self, msg.chat.id, output, true, nil, true) end diff --git a/otouto/plugins/pokego-calculator.lua b/otouto/plugins/pokego-calculator.lua index 1690edd..4ba4866 100644 --- a/otouto/plugins/pokego-calculator.lua +++ b/otouto/plugins/pokego-calculator.lua @@ -3,110 +3,110 @@ local utilities = require('otouto.utilities') local pgc = {} function pgc:init(config) - pgc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('gocalc', true).table - pgc.doc = config.cmd_pat .. [[gocalc <required candy> <number of Pokémon> <number of candy> + pgc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('gocalc', true).table + pgc.doc = config.cmd_pat .. [[gocalc <required candy> <number of Pokémon> <number of candy> Calculates the number of Pokémon that must be transferred before evolving, how many evolutions the user is able to perform, and how many Pokémon and candy will be left over. All arguments must be positive numbers. Batch jobs may be performed by separating valid sets of arguments by lines. Example (forty pidgeys and three hundred pidgey candies): ]] .. config.cmd_pat .. 'gocalc 12 40 300' - pgc.command = 'gocalc <required candy> <#pokemon> <#candy>' + pgc.command = 'gocalc <required candy> <#pokemon> <#candy>' end -- This function written by Juan Potato. MIT-licensed. local pidgey_calc = function(candies_to_evolve, mons, candies) - local transferred = 0; - local evolved = 0; + local transferred = 0; + local evolved = 0; - while true do - if math.floor(candies / candies_to_evolve) == 0 or mons == 0 then - break - else - mons = mons - 1 - candies = candies - candies_to_evolve + 1 - evolved = evolved + 1 - if mons == 0 then - break - end - end - end + while true do + if math.floor(candies / candies_to_evolve) == 0 or mons == 0 then + break + else + mons = mons - 1 + candies = candies - candies_to_evolve + 1 + evolved = evolved + 1 + if mons == 0 then + break + end + end + end - while true do - if (candies + mons) < (candies_to_evolve + 1) or mons == 0 then - break - end - while candies < candies_to_evolve do - transferred = transferred + 1 - mons = mons - 1 - candies = candies + 1 - end - mons = mons - 1 - candies = candies - candies_to_evolve + 1 - evolved = evolved + 1 - end + while true do + if (candies + mons) < (candies_to_evolve + 1) or mons == 0 then + break + end + while candies < candies_to_evolve do + transferred = transferred + 1 + mons = mons - 1 + candies = candies + 1 + end + mons = mons - 1 + candies = candies - candies_to_evolve + 1 + evolved = evolved + 1 + end - return { - transfer = transferred, - evolve = evolved, - leftover_mons = mons, - leftover_candy = candies - } + return { + transfer = transferred, + evolve = evolved, + leftover_mons = mons, + leftover_candy = candies + } end local single_job = function(input) - local req_candy, mons, candies = input:match('^(%d+) (%d+) (%d+)$') - req_candy = tonumber(req_candy) - mons = tonumber(mons) - candies = tonumber(candies) - if not (req_candy and mons and candies) then - return { err = 'Invalid input: Three numbers expected.' } - elseif req_candy > 400 then - return { err = 'Invalid required candy: Maximum is 400.' } - elseif mons > 1000 then - return { err = 'Invalid number of Pokémon: Maximum is 1000.' } - elseif candies > 10000 then - return { err = 'Invalid number of candies: Maximum is 10000.' } - else - return pidgey_calc(req_candy, mons, candies) - end + local req_candy, mons, candies = input:match('^(%d+) (%d+) (%d+)$') + req_candy = tonumber(req_candy) + mons = tonumber(mons) + candies = tonumber(candies) + if not (req_candy and mons and candies) then + return { err = 'Invalid input: Three numbers expected.' } + elseif req_candy > 400 then + return { err = 'Invalid required candy: Maximum is 400.' } + elseif mons > 1000 then + return { err = 'Invalid number of Pokémon: Maximum is 1000.' } + elseif candies > 10000 then + return { err = 'Invalid number of candies: Maximum is 10000.' } + else + return pidgey_calc(req_candy, mons, candies) + end end function pgc:action(msg) - local input = utilities.input(msg.text) - if not input then - utilities.send_reply(self, msg, pgc.doc, true) - return - end - input = input .. '\n' - local output = '' - local total_evolutions = 0 - for line in input:gmatch('(.-)\n') do - local info = single_job(line) - output = output .. '`' .. line .. '`\n' - if info.err then - output = output .. info.err .. '\n\n' - else - total_evolutions = total_evolutions + info.evolve - local s = '*Transfer:* %s. \n*Evolve:* %s (%s XP, %s minutes). \n*Leftover:* %s mons, %s candy.\n\n' - s = s:format(info.transfer, info.evolve, info.evolve..'k', info.evolve*0.5, info.leftover_mons, info.leftover_candy) - output = output .. s - end - end - local s = '*Total evolutions:* %s. \n*Recommendation:* %s' - local recommendation - local egg_count = math.floor(total_evolutions/60) - if egg_count < 1 then - recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.' - else - recommendation = string.format( - 'Use %s lucky egg%s for %s evolutions.', - egg_count, - egg_count == 1 and '' or 's', - egg_count * 60 - ) - end - s = s:format(total_evolutions, recommendation) - output = output .. s - utilities.send_reply(self, msg, output, true) + local input = utilities.input(msg.text) + if not input then + utilities.send_reply(self, msg, pgc.doc, true) + return + end + input = input .. '\n' + local output = '' + local total_evolutions = 0 + for line in input:gmatch('(.-)\n') do + local info = single_job(line) + output = output .. '`' .. line .. '`\n' + if info.err then + output = output .. info.err .. '\n\n' + else + total_evolutions = total_evolutions + info.evolve + local s = '*Transfer:* %s. \n*Evolve:* %s (%s XP, %s minutes). \n*Leftover:* %s mons, %s candy.\n\n' + s = s:format(info.transfer, info.evolve, info.evolve..'k', info.evolve*0.5, info.leftover_mons, info.leftover_candy) + output = output .. s + end + end + local s = '*Total evolutions:* %s. \n*Recommendation:* %s' + local recommendation + local egg_count = math.floor(total_evolutions/60) + if egg_count < 1 then + recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.' + else + recommendation = string.format( + 'Use %s lucky egg%s for %s evolutions.', + egg_count, + egg_count == 1 and '' or 's', + egg_count * 60 + ) + end + s = s:format(total_evolutions, recommendation) + output = output .. s + utilities.send_reply(self, msg, output, true) end return pgc diff --git a/otouto/plugins/pokemon-go.lua b/otouto/plugins/pokemon-go.lua index 51cc909..79b4489 100644 --- a/otouto/plugins/pokemon-go.lua +++ b/otouto/plugins/pokemon-go.lua @@ -21,7 +21,7 @@ Set your Pokémon Go team for statistical purposes. The team must be valid, and db.membership = {} end for _, set in pairs(db.membership) do - setmetatable(set, utilities.set_meta) + setmetatable(set, utilities.set_meta) end end diff --git a/otouto/plugins/preview.lua b/otouto/plugins/preview.lua index 802a0f0..2b1a350 100644 --- a/otouto/plugins/preview.lua +++ b/otouto/plugins/preview.lua @@ -6,37 +6,37 @@ local utilities = require('otouto.utilities') preview.command = 'preview <link>' function preview:init(config) - preview.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('preview', true).table - preview.doc = config.cmd_pat .. 'preview <link> \nReturns a full-message, "unlinked" preview.' + preview.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('preview', true).table + preview.doc = config.cmd_pat .. 'preview <link> \nReturns a full-message, "unlinked" preview.' end function preview:action(msg) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, preview.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, preview.doc, true) + return + end - input = utilities.get_word(input, 1) - if not input:match('^https?://.+') then - input = 'http://' .. input - end + input = utilities.get_word(input, 1) + if not input:match('^https?://.+') then + input = 'http://' .. input + end - local res = HTTP.request(input) - if not res then - utilities.send_reply(self, msg, 'Please provide a valid link.') - return - end + local res = HTTP.request(input) + if not res then + utilities.send_reply(self, msg, 'Please provide a valid link.') + return + end - if res:len() == 0 then - utilities.send_reply(self, msg, 'Sorry, the link you provided is not letting us make a preview.') - return - end + if res:len() == 0 then + utilities.send_reply(self, msg, 'Sorry, the link you provided is not letting us make a preview.') + return + end - -- Invisible zero-width, non-joiner. - local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>' - utilities.send_message(self, msg.chat.id, output, false, nil, 'html') + -- Invisible zero-width, non-joiner. + local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>' + utilities.send_message(self, msg.chat.id, output, false, nil, 'html') end diff --git a/otouto/plugins/pun.lua b/otouto/plugins/pun.lua index 5063096..702ca9a 100644 --- a/otouto/plugins/pun.lua +++ b/otouto/plugins/pun.lua @@ -6,138 +6,138 @@ pun.command = 'pun' pun.doc = 'Returns a pun.' function pun:init(config) - pun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pun').table + pun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pun').table end local puns = { - "The person who invented the door-knock won the No-bell prize.", - "I couldn't work out how to fasten my seatbelt. Then it clicked.", - "Never trust atoms; they make up everything.", - "Singing in the shower is all fun and games until you get shampoo in your mouth - Then it becomes a soap opera.", - "I can't believe I got fired from the calendar factory. All I did was take a day off.", - "To the guy who invented zero: Thanks for nothing!", - "Enough with the cripple jokes! I just can't stand them.", - "I've accidentally swallowed some Scrabble tiles. My next crap could spell disaster.", - "How does Moses make his tea? Hebrews it.", - "Did you hear about the guy who got hit in the head with a can of soda? He was lucky it was a soft drink.", - "When William joined the army he disliked the phrase 'fire at will'.", - "There was a sign on the lawn at a rehab center that said 'Keep off the Grass'.", - "I wondered why the baseball was getting bigger. Then it hit me.", - "I can hear music coming out of my printer. I think the paper's jamming again.", - "I have a few jokes about unemployed people, but none of them work", - "Want to hear a construction joke? I'm working on it", - "I always take a second pair of pants when I go golfing, in case I get a hole in one.", - "I couldn't remember how to throw a boomerang, but then it came back to me.", - "I've decided that my wifi will be my valentine. IDK, we just have this connection.", - "A prisoner's favorite punctuation mark is the period. It marks the end of his sentence.", - "I used to go fishing with Skrillex, but he kept dropping the bass.", - "Two antennae met on a roof and got married. The wedding was okay, but the reception was incredible.", - "A book just fell on my head. I've only got my shelf to blame.", - "I dropped my steak on the floor. Now it's ground beef.", - "I used to have a fear of hurdles, but I got over it.", - "The outcome of war does not prove who is right, but only who is left.", - "Darth Vader tries not to burn his food, but it always comes out a little on the dark side.", - "The store keeps calling me to buy more furniture, but all I wanted was a one night stand.", - "This girl said she recognized me from the vegetarian club, but I'd never met herbivore.", - "Police arrested two kids yesterday, one was drinking battery acid, the other was eating fireworks. They charged one and let the other one off...", - "No more Harry Potter jokes guys. I'm Sirius.", - "It was hard getting over my addiction to hokey pokey, but I've turned myself around.", - "It takes a lot of balls to golf the way I do.", - "Why did everyone want to hang out with the mushroom? Because he was a fungi.", - "How much does a hipster weigh? An instagram.", - "I used to be addicted to soap, but I'm clean now.", - "When life gives you melons, you’re probably dyslexic.", - "What's with all the blind jokes? I just don't see the point.", - "If Apple made a car, would it have Windows?", - "Need an ark? I Noah guy.", - "The scarecrow won an award because he was outstanding in his field.", - "What's the difference between a man in a tux on a bicycle, and a man in a sweatsuit on a trycicle? A tire.", - "What do you do with a sick chemist? If you can't helium, and you can't curium, you'll just have to barium.", - "I'm reading a book about anti-gravity. It's impossible to put down.", - "Trying to write with a broken pencil is pointless.", - "When TVs go on vacation, they travel to remote islands.", - "I was going to tell a midget joke, but it's too short.", - "Jokes about German sausage are the wurst.", - "How do you organize a space party? You planet.", - "Sleeping comes so naturally to me, I could do it with my eyes closed.", - "I'm glad I know sign language; it's pretty handy.", - "Atheism is a non-prophet organization.", - "Velcro: What a rip-off!", - "If they made a Minecraft movie, it would be a blockbuster.", - "I don't trust people with graph paper. They're always plotting something", - "I had a friend who was addicted to brake fluid. He says he can stop anytime.", - "The form said I had Type A blood, but it was a Type O.", - "I went to to the shop to buy eight Sprites - I came home and realised I'd picked 7Up.", - "There was an explosion at a pie factory. 3.14 people died.", - "A man drove his car into a tree and found out how a Mercedes bends.", - "The experienced carpenter really nailed it, but the new guy screwed everything up.", - "I didn't like my beard at first, but then it grew on me.", - "Smaller babies may be delivered by stork, but the heavier ones need a crane.", - "What's the definition of a will? It's a dead giveaway.", - "I was going to look for my missing watch, but I could never find the time.", - "I hate elevators, and I often take steps to avoid them.", - "Did you hear about the guy whose whole left side was cut off? He's all right now.", - "It's not that the man did not know how to juggle, he just didn't have the balls to do it.", - "I used to be a loan shark, but I lost interest", - "I don't trust these stairs; they're always up to something.", - "My friend's bakery burned down last night. Now his business is toast.", - "Don't trust people that do acupuncture; they're back stabbers.", - "The man who survived mustard gas and pepper spray is now a seasoned veteran.", - "Police were called to a daycare where a three-year-old was resisting a rest.", - "When Peter Pan punches, they Neverland", - "The shoemaker did not deny his apprentice anything he needed. He gave him his awl.", - "I did a theatrical performance about puns. It was a play on words.", - "Show me a piano falling down a mineshaft and I'll show you A-flat minor.", - "Have you ever tried to eat a clock? It's very time consuming.", - "There was once a cross-eyed teacher who couldn't control his pupils.", - "A new type of broom came out and it is sweeping the nation.", - "I relish the fact that you've mustard the strength to ketchup to me.", - "I knew a woman who owned a taser. Man, was she stunning!", - "What did the grape say when it got stepped on? Nothing - but it let out a little whine.", - "It was an emotional wedding. Even the cake was in tiers.", - "When a clock is hungry it goes back four seconds.", - "The dead batteries were given out free of charge.", - "Why are there no knock-knock jokes about America? Because freedom rings.", - "When the cannibal showed up late to dinner, they gave him the cold shoulder.", - "I should have been sad when my flashlight died, but I was delighted.", - "Why don't tennis players ever get married? Love means nothing to them.", - "Pterodactyls can't be heard going to the bathroom because the P is silent.", - "Mermaids make calls on their shell phones.", - "What do you call an aardvark with three feet? A yaardvark.", - "Captain Kirk has three ears: A right ear, a left ear, and a final front ear.", - "How do celebrities stay cool? They have a lot of fans.", - "Without geometry, life is pointless.", - "Did you hear about the cow who tried to jump over a barbed-wire fence? It ended in udder destruction.", - "The truth may ring like a bell, but it is seldom ever tolled.", - "I used to work for the IRS, but my job was too taxing.", - "I used to be a programmer, but then I lost my drive.", - "Pediatricians are doctors with little patients.", - "I finally fired my masseuse today. She always rubbed me the wrong way.", - "I stayed up all night wondering where the sun went. Then it dawned on me.", - "What's the difference between a man and his dog? The man wears a suit; the dog just pants.", - "A psychic midget who escapes from prison is a small medium at large.", - "I've been to the dentist several times, so I know the drill.", - "The roundest knight at King Arthur's round table was Sir Cumference. He acquired his size from too much pi.", - "She was only a whiskey maker, but he loved her still.", - "Male deer have buck teeth.", - "Whiteboards are remarkable.", - "Visitors in Cuba are always Havana good time.", - "Why does electricity shock people? It doesn't know how to conduct itself.", - "Lancelot had a scary dream about his horse. It was a knight mare.", - "A tribe of cannibals captured a missionary and ate him. Afterward, they all had violent food poisoning. This just goes to show that you can't keep a good man down.", - "Heaven for gamblers is a paradise.", - "Old wheels aren't thrown away, they're just retired.", - "Horses are very stable animals.", - "Banks don't crash, they just lose their balance.", - "The career of a skier can go downhill very fast.", - "In democracy, it's your vote that counts. In feudalism, it's your count that votes.", - "A sea lion is nothing but an ionized seal.", - "The vegetables from my garden aren't that great. I guess you could say they're mediokra." + "The person who invented the door-knock won the No-bell prize.", + "I couldn't work out how to fasten my seatbelt. Then it clicked.", + "Never trust atoms; they make up everything.", + "Singing in the shower is all fun and games until you get shampoo in your mouth - Then it becomes a soap opera.", + "I can't believe I got fired from the calendar factory. All I did was take a day off.", + "To the guy who invented zero: Thanks for nothing!", + "Enough with the cripple jokes! I just can't stand them.", + "I've accidentally swallowed some Scrabble tiles. My next crap could spell disaster.", + "How does Moses make his tea? Hebrews it.", + "Did you hear about the guy who got hit in the head with a can of soda? He was lucky it was a soft drink.", + "When William joined the army he disliked the phrase 'fire at will'.", + "There was a sign on the lawn at a rehab center that said 'Keep off the Grass'.", + "I wondered why the baseball was getting bigger. Then it hit me.", + "I can hear music coming out of my printer. I think the paper's jamming again.", + "I have a few jokes about unemployed people, but none of them work", + "Want to hear a construction joke? I'm working on it", + "I always take a second pair of pants when I go golfing, in case I get a hole in one.", + "I couldn't remember how to throw a boomerang, but then it came back to me.", + "I've decided that my wifi will be my valentine. IDK, we just have this connection.", + "A prisoner's favorite punctuation mark is the period. It marks the end of his sentence.", + "I used to go fishing with Skrillex, but he kept dropping the bass.", + "Two antennae met on a roof and got married. The wedding was okay, but the reception was incredible.", + "A book just fell on my head. I've only got my shelf to blame.", + "I dropped my steak on the floor. Now it's ground beef.", + "I used to have a fear of hurdles, but I got over it.", + "The outcome of war does not prove who is right, but only who is left.", + "Darth Vader tries not to burn his food, but it always comes out a little on the dark side.", + "The store keeps calling me to buy more furniture, but all I wanted was a one night stand.", + "This girl said she recognized me from the vegetarian club, but I'd never met herbivore.", + "Police arrested two kids yesterday, one was drinking battery acid, the other was eating fireworks. They charged one and let the other one off...", + "No more Harry Potter jokes guys. I'm Sirius.", + "It was hard getting over my addiction to hokey pokey, but I've turned myself around.", + "It takes a lot of balls to golf the way I do.", + "Why did everyone want to hang out with the mushroom? Because he was a fungi.", + "How much does a hipster weigh? An instagram.", + "I used to be addicted to soap, but I'm clean now.", + "When life gives you melons, you’re probably dyslexic.", + "What's with all the blind jokes? I just don't see the point.", + "If Apple made a car, would it have Windows?", + "Need an ark? I Noah guy.", + "The scarecrow won an award because he was outstanding in his field.", + "What's the difference between a man in a tux on a bicycle, and a man in a sweatsuit on a trycicle? A tire.", + "What do you do with a sick chemist? If you can't helium, and you can't curium, you'll just have to barium.", + "I'm reading a book about anti-gravity. It's impossible to put down.", + "Trying to write with a broken pencil is pointless.", + "When TVs go on vacation, they travel to remote islands.", + "I was going to tell a midget joke, but it's too short.", + "Jokes about German sausage are the wurst.", + "How do you organize a space party? You planet.", + "Sleeping comes so naturally to me, I could do it with my eyes closed.", + "I'm glad I know sign language; it's pretty handy.", + "Atheism is a non-prophet organization.", + "Velcro: What a rip-off!", + "If they made a Minecraft movie, it would be a blockbuster.", + "I don't trust people with graph paper. They're always plotting something", + "I had a friend who was addicted to brake fluid. He says he can stop anytime.", + "The form said I had Type A blood, but it was a Type O.", + "I went to to the shop to buy eight Sprites - I came home and realised I'd picked 7Up.", + "There was an explosion at a pie factory. 3.14 people died.", + "A man drove his car into a tree and found out how a Mercedes bends.", + "The experienced carpenter really nailed it, but the new guy screwed everything up.", + "I didn't like my beard at first, but then it grew on me.", + "Smaller babies may be delivered by stork, but the heavier ones need a crane.", + "What's the definition of a will? It's a dead giveaway.", + "I was going to look for my missing watch, but I could never find the time.", + "I hate elevators, and I often take steps to avoid them.", + "Did you hear about the guy whose whole left side was cut off? He's all right now.", + "It's not that the man did not know how to juggle, he just didn't have the balls to do it.", + "I used to be a loan shark, but I lost interest", + "I don't trust these stairs; they're always up to something.", + "My friend's bakery burned down last night. Now his business is toast.", + "Don't trust people that do acupuncture; they're back stabbers.", + "The man who survived mustard gas and pepper spray is now a seasoned veteran.", + "Police were called to a daycare where a three-year-old was resisting a rest.", + "When Peter Pan punches, they Neverland", + "The shoemaker did not deny his apprentice anything he needed. He gave him his awl.", + "I did a theatrical performance about puns. It was a play on words.", + "Show me a piano falling down a mineshaft and I'll show you A-flat minor.", + "Have you ever tried to eat a clock? It's very time consuming.", + "There was once a cross-eyed teacher who couldn't control his pupils.", + "A new type of broom came out and it is sweeping the nation.", + "I relish the fact that you've mustard the strength to ketchup to me.", + "I knew a woman who owned a taser. Man, was she stunning!", + "What did the grape say when it got stepped on? Nothing - but it let out a little whine.", + "It was an emotional wedding. Even the cake was in tiers.", + "When a clock is hungry it goes back four seconds.", + "The dead batteries were given out free of charge.", + "Why are there no knock-knock jokes about America? Because freedom rings.", + "When the cannibal showed up late to dinner, they gave him the cold shoulder.", + "I should have been sad when my flashlight died, but I was delighted.", + "Why don't tennis players ever get married? Love means nothing to them.", + "Pterodactyls can't be heard going to the bathroom because the P is silent.", + "Mermaids make calls on their shell phones.", + "What do you call an aardvark with three feet? A yaardvark.", + "Captain Kirk has three ears: A right ear, a left ear, and a final front ear.", + "How do celebrities stay cool? They have a lot of fans.", + "Without geometry, life is pointless.", + "Did you hear about the cow who tried to jump over a barbed-wire fence? It ended in udder destruction.", + "The truth may ring like a bell, but it is seldom ever tolled.", + "I used to work for the IRS, but my job was too taxing.", + "I used to be a programmer, but then I lost my drive.", + "Pediatricians are doctors with little patients.", + "I finally fired my masseuse today. She always rubbed me the wrong way.", + "I stayed up all night wondering where the sun went. Then it dawned on me.", + "What's the difference between a man and his dog? The man wears a suit; the dog just pants.", + "A psychic midget who escapes from prison is a small medium at large.", + "I've been to the dentist several times, so I know the drill.", + "The roundest knight at King Arthur's round table was Sir Cumference. He acquired his size from too much pi.", + "She was only a whiskey maker, but he loved her still.", + "Male deer have buck teeth.", + "Whiteboards are remarkable.", + "Visitors in Cuba are always Havana good time.", + "Why does electricity shock people? It doesn't know how to conduct itself.", + "Lancelot had a scary dream about his horse. It was a knight mare.", + "A tribe of cannibals captured a missionary and ate him. Afterward, they all had violent food poisoning. This just goes to show that you can't keep a good man down.", + "Heaven for gamblers is a paradise.", + "Old wheels aren't thrown away, they're just retired.", + "Horses are very stable animals.", + "Banks don't crash, they just lose their balance.", + "The career of a skier can go downhill very fast.", + "In democracy, it's your vote that counts. In feudalism, it's your count that votes.", + "A sea lion is nothing but an ionized seal.", + "The vegetables from my garden aren't that great. I guess you could say they're mediokra." } function pun:action(msg) - utilities.send_reply(self, msg, puns[math.random(#puns)]) + utilities.send_reply(self, msg, puns[math.random(#puns)]) end diff --git a/otouto/plugins/reactions.lua b/otouto/plugins/reactions.lua index d814f93..6ea39f7 100644 --- a/otouto/plugins/reactions.lua +++ b/otouto/plugins/reactions.lua @@ -16,34 +16,34 @@ reactions.doc = 'Returns a list of "reaction" emoticon commands.' local help function reactions:init(config) - -- Generate a "help" message triggered by "/reactions". - help = 'Reactions:\n' - reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table - local username = self.info.username:lower() - for trigger,reaction in pairs(config.reactions) do - help = help .. '• ' .. config.cmd_pat .. trigger .. ': ' .. reaction .. '\n' - table.insert(reactions.triggers, '^'..config.cmd_pat..trigger) - table.insert(reactions.triggers, '^'..config.cmd_pat..trigger..'@'..username) - table.insert(reactions.triggers, config.cmd_pat..trigger..'$') - table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'$') - table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger) - table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger..'@'..username) - table.insert(reactions.triggers, config.cmd_pat..trigger..'\n') - table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'\n') - end + -- Generate a "help" message triggered by "/reactions". + help = 'Reactions:\n' + reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table + local username = self.info.username:lower() + for trigger,reaction in pairs(config.reactions) do + help = help .. '• ' .. config.cmd_pat .. trigger .. ': ' .. reaction .. '\n' + table.insert(reactions.triggers, '^'..config.cmd_pat..trigger) + table.insert(reactions.triggers, '^'..config.cmd_pat..trigger..'@'..username) + table.insert(reactions.triggers, config.cmd_pat..trigger..'$') + table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'$') + table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger) + table.insert(reactions.triggers, '\n'..config.cmd_pat..trigger..'@'..username) + table.insert(reactions.triggers, config.cmd_pat..trigger..'\n') + table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..username..'\n') + end end function reactions:action(msg, config) - if string.match(msg.text_lower, config.cmd_pat..'reactions') then - utilities.send_message(self, msg.chat.id, help) - return - end - for trigger,reaction in pairs(config.reactions) do - if string.match(msg.text_lower, config.cmd_pat..trigger) then - utilities.send_message(self, msg.chat.id, reaction) - return - end - end + if string.match(msg.text_lower, config.cmd_pat..'reactions') then + utilities.send_message(self, msg.chat.id, help) + return + end + for trigger,reaction in pairs(config.reactions) do + if string.match(msg.text_lower, config.cmd_pat..trigger) then + utilities.send_message(self, msg.chat.id, reaction) + return + end + end end return reactions diff --git a/otouto/plugins/reddit.lua b/otouto/plugins/reddit.lua index 8bdcb05..66db1db 100644 --- a/otouto/plugins/reddit.lua +++ b/otouto/plugins/reddit.lua @@ -8,29 +8,29 @@ local utilities = require('otouto.utilities') reddit.command = 'reddit [r/subreddit | query]' function reddit:init(config) - reddit.triggers = utilities.triggers(self.info.username, config.cmd_pat, {'^/r/'}):t('reddit', true):t('r', true):t('r/', true).table - reddit.doc = config.cmd_pat .. [[reddit [r/subreddit | query] + reddit.triggers = utilities.triggers(self.info.username, config.cmd_pat, {'^/r/'}):t('reddit', true):t('r', true):t('r/', true).table + reddit.doc = config.cmd_pat .. [[reddit [r/subreddit | query] Returns the top posts or results for a given subreddit or query. If no argument is given, returns the top posts from r/all. Querying specific subreddits is not supported. Aliases: ]] .. config.cmd_pat .. 'r, /r/subreddit' end local format_results = function(posts) - local output = '' - for _,v in ipairs(posts) do - local post = v.data - local title = post.title:gsub('%[', '('):gsub('%]', ')'):gsub('&', '&') - if title:len() > 256 then - title = title:sub(1, 253) - title = utilities.trim(title) .. '...' - end - local short_url = 'redd.it/' .. post.id - local s = '[' .. title .. '](' .. short_url .. ')' - if post.domain and not post.is_self and not post.over_18 then - s = '`[`[' .. post.domain .. '](' .. post.url:gsub('%)', '\\)') .. ')`]` ' .. s - end - output = output .. '• ' .. s .. '\n' - end - return output + local output = '' + for _,v in ipairs(posts) do + local post = v.data + local title = post.title:gsub('%[', '('):gsub('%]', ')'):gsub('&', '&') + if title:len() > 256 then + title = title:sub(1, 253) + title = utilities.trim(title) .. '...' + end + local short_url = 'redd.it/' .. post.id + local s = '[' .. title .. '](' .. short_url .. ')' + if post.domain and not post.is_self and not post.over_18 then + s = '`[`[' .. post.domain .. '](' .. post.url:gsub('%)', '\\)') .. ')`]` ' .. s + end + output = output .. '• ' .. s .. '\n' + end + return output end reddit.subreddit_url = 'http://www.reddit.com/%s/.json?limit=' @@ -38,46 +38,46 @@ reddit.search_url = 'http://www.reddit.com/search.json?q=%s&limit=' reddit.rall_url = 'http://www.reddit.com/.json?limit=' function reddit:action(msg, config) - -- Eight results in PM, four results elsewhere. - local limit = 4 - if msg.chat.type == 'private' then - limit = 8 - end - local text = msg.text_lower - if text:match('^/r/.') then - -- Normalize input so this hack works easily. - text = msg.text_lower:gsub('^/r/', config.cmd_pat..'r r/') - end - local input = utilities.input(text) - local source, url - if input then - if input:match('^r/.') then - input = utilities.get_word(input, 1) - url = reddit.subreddit_url:format(input) .. limit - source = '*/' .. utilities.md_escape(input) .. '*\n' - else - input = utilities.input(msg.text) - source = '*Results for* _' .. utilities.md_escape(input) .. '_ *:*\n' - input = URL.escape(input) - url = reddit.search_url:format(input) .. limit - end - else - url = reddit.rall_url .. limit - source = '*/r/all*\n' - end - local jstr, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - else - local jdat = JSON.decode(jstr) - if #jdat.data.children == 0 then - utilities.send_reply(self, msg, config.errors.results) - else - local output = format_results(jdat.data.children) - output = source .. output - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - end + -- Eight results in PM, four results elsewhere. + local limit = 4 + if msg.chat.type == 'private' then + limit = 8 + end + local text = msg.text_lower + if text:match('^/r/.') then + -- Normalize input so this hack works easily. + text = msg.text_lower:gsub('^/r/', config.cmd_pat..'r r/') + end + local input = utilities.input(text) + local source, url + if input then + if input:match('^r/.') then + input = utilities.get_word(input, 1) + url = reddit.subreddit_url:format(input) .. limit + source = '*/' .. utilities.md_escape(input) .. '*\n' + else + input = utilities.input(msg.text) + source = '*Results for* _' .. utilities.md_escape(input) .. '_ *:*\n' + input = URL.escape(input) + url = reddit.search_url:format(input) .. limit + end + else + url = reddit.rall_url .. limit + source = '*/r/all*\n' + end + local jstr, res = HTTP.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + else + local jdat = JSON.decode(jstr) + if #jdat.data.children == 0 then + utilities.send_reply(self, msg, config.errors.results) + else + local output = format_results(jdat.data.children) + output = source .. output + utilities.send_message(self, msg.chat.id, output, true, nil, true) + end + end end return reddit diff --git a/otouto/plugins/remind.lua b/otouto/plugins/remind.lua index a9fd7d9..888ef9c 100644 --- a/otouto/plugins/remind.lua +++ b/otouto/plugins/remind.lua @@ -5,87 +5,87 @@ local utilities = require('otouto.utilities') remind.command = 'remind <duration> <message>' function remind:init(config) - self.database.reminders = self.database.reminders or {} + self.database.reminders = self.database.reminders or {} - remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table + remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table - config.remind = config.remind or {} - setmetatable(config.remind, { __index = function() return 1000 end }) + config.remind = config.remind or {} + setmetatable(config.remind, { __index = function() return 1000 end }) - remind.doc = config.cmd_pat .. [[remind <duration> <message> + remind.doc = config.cmd_pat .. [[remind <duration> <message> Repeats a message after a duration of time, in minutes. The maximum length of a reminder is %s characters. The maximum duration of a timer is %s minutes. The maximum number of reminders for a group is %s. The maximum number of reminders in private is %s.]] - remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private) + remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private) end function remind:action(msg, config) - local input = utilities.input(msg.text) - if not input then - utilities.send_reply(self, msg, remind.doc, true) - return - end + local input = utilities.input(msg.text) + if not input then + utilities.send_reply(self, msg, remind.doc, true) + return + end - local duration = tonumber(utilities.get_word(input, 1)) - if not duration then - utilities.send_reply(self, msg, remind.doc, true) - return - end + local duration = tonumber(utilities.get_word(input, 1)) + if not duration then + utilities.send_reply(self, msg, remind.doc, true) + return + end - if duration < 1 then - duration = 1 - elseif duration > config.remind.max_duration then - duration = config.remind.max_duration - end - local message = utilities.input(input) - if not message then - utilities.send_reply(self, msg, remind.doc, true) - return - end + if duration < 1 then + duration = 1 + elseif duration > config.remind.max_duration then + duration = config.remind.max_duration + end + local message = utilities.input(input) + if not message then + utilities.send_reply(self, msg, remind.doc, true) + return + end - if #message > config.remind.max_length then - utilities.send_reply(self, msg, 'The maximum length of reminders is ' .. config.remind.max_length .. '.') - return - end + if #message > config.remind.max_length then + utilities.send_reply(self, msg, 'The maximum length of reminders is ' .. config.remind.max_length .. '.') + return + end - local chat_id_str = tostring(msg.chat.id) - local output - self.database.reminders[chat_id_str] = self.database.reminders[chat_id_str] or {} - if msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_private then - output = 'Sorry, you already have the maximum number of reminders.' - elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then - output = 'Sorry, this group already has the maximum number of reminders.' - else - table.insert(self.database.reminders[chat_id_str], { - time = os.time() + (duration * 60), - message = message - }) - output = string.format( - 'I will remind you in %s minute%s!', - duration, - duration == 1 and '' or 's' - ) - end - utilities.send_reply(self, msg, output, true) + local chat_id_str = tostring(msg.chat.id) + local output + self.database.reminders[chat_id_str] = self.database.reminders[chat_id_str] or {} + if msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_private then + output = 'Sorry, you already have the maximum number of reminders.' + elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then + output = 'Sorry, this group already has the maximum number of reminders.' + else + table.insert(self.database.reminders[chat_id_str], { + time = os.time() + (duration * 60), + message = message + }) + output = string.format( + 'I will remind you in %s minute%s!', + duration, + duration == 1 and '' or 's' + ) + end + utilities.send_reply(self, msg, output, true) end function remind:cron(config) - local time = os.time() - -- Iterate over the group entries in the reminders database. - for chat_id, group in pairs(self.database.reminders) do - -- Iterate over each reminder. - for k, reminder in pairs(group) do - -- If the reminder is past-due, send it and nullify it. - -- Otherwise, add it to the replacement table. - if time > reminder.time then - local output = utilities.style.enquote('Reminder', reminder.message) - local res = utilities.send_message(self, chat_id, output, true, nil, true) - -- If the message fails to send, save it for later (if enabled in config). - if res or not config.remind.persist then - group[k] = nil - end - end - end - end + local time = os.time() + -- Iterate over the group entries in the reminders database. + for chat_id, group in pairs(self.database.reminders) do + -- Iterate over each reminder. + for k, reminder in pairs(group) do + -- If the reminder is past-due, send it and nullify it. + -- Otherwise, add it to the replacement table. + if time > reminder.time then + local output = utilities.style.enquote('Reminder', reminder.message) + local res = utilities.send_message(self, chat_id, output, true, nil, true) + -- If the message fails to send, save it for later (if enabled in config). + if res or not config.remind.persist then + group[k] = nil + end + end + end + end end return remind diff --git a/otouto/plugins/rmspic.lua b/otouto/plugins/rmspic.lua index 40cf66f..b52d6e5 100644 --- a/otouto/plugins/rmspic.lua +++ b/otouto/plugins/rmspic.lua @@ -5,30 +5,30 @@ local bindings = require('otouto.bindings') local rms = {} function rms:init(config) - rms.BASE_URL = 'https://rms.sexy/img/' - rms.LIST = {} - local s, r = https.request(rms.BASE_URL) - if r ~= 200 then - print('Error connecting to rms.sexy.\nrmspic.lua will not be enabled.') - return - end - for link in s:gmatch('<a href=".-%.%a%a%a">(.-)</a>') do - table.insert(rms.LIST, link) - end - rms.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('rms').table + rms.BASE_URL = 'https://rms.sexy/img/' + rms.LIST = {} + local s, r = https.request(rms.BASE_URL) + if r ~= 200 then + print('Error connecting to rms.sexy.\nrmspic.lua will not be enabled.') + return + end + for link in s:gmatch('<a href=".-%.%a%a%a">(.-)</a>') do + table.insert(rms.LIST, link) + end + rms.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('rms').table end function rms:action(msg, config) - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' }) - local choice = rms.LIST[math.random(#rms.LIST)] - local filename = '/tmp/' .. choice - local image_file = io.open(filename) - if image_file then - image_file:close() - else - utilities.download_file(rms.BASE_URL .. choice, filename) - end - bindings.sendPhoto(self, { chat_id = msg.chat.id }, { photo = filename }) + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' }) + local choice = rms.LIST[math.random(#rms.LIST)] + local filename = '/tmp/' .. choice + local image_file = io.open(filename) + if image_file then + image_file:close() + else + utilities.download_file(rms.BASE_URL .. choice, filename) + end + bindings.sendPhoto(self, { chat_id = msg.chat.id }, { photo = filename }) end return rms diff --git a/otouto/plugins/setandget.lua b/otouto/plugins/setandget.lua index 122b942..6c5dd08 100644 --- a/otouto/plugins/setandget.lua +++ b/otouto/plugins/setandget.lua @@ -3,9 +3,9 @@ local setandget = {} local utilities = require('otouto.utilities') function setandget:init(config) - self.database.setandget = self.database.setandget or {} - setandget.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('set', true):t('get', true).table - setandget.doc = config.cmd_pat .. [[set <name> <value> + self.database.setandget = self.database.setandget or {} + setandget.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('set', true):t('get', true).table + setandget.doc = config.cmd_pat .. [[set <name> <value> Stores a value with the given name. Use "]] .. config.cmd_pat .. [[set <name> --" to delete the stored value. ]] .. config.cmd_pat .. [[get [name] Returns the stored value or a list of stored values.]] @@ -15,56 +15,56 @@ setandget.command = 'set <name> <value>' function setandget:action(msg, config) - local chat_id_str = tostring(msg.chat.id) - local input = utilities.input(msg.text) - self.database.setandget[chat_id_str] = self.database.setandget[chat_id_str] or {} + local chat_id_str = tostring(msg.chat.id) + local input = utilities.input(msg.text) + self.database.setandget[chat_id_str] = self.database.setandget[chat_id_str] or {} - if msg.text_lower:match('^'..config.cmd_pat..'set') then + if msg.text_lower:match('^'..config.cmd_pat..'set') then - if not input then - utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true) - return - end + if not input then + utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true) + return + end - local name = utilities.get_word(input:lower(), 1) - local value = utilities.input(input) + local name = utilities.get_word(input:lower(), 1) + local value = utilities.input(input) - if not name or not value then - utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true) - elseif value == '--' or value == '—' then - self.database.setandget[chat_id_str][name] = nil - utilities.send_message(self, msg.chat.id, 'That value has been deleted.') - else - self.database.setandget[chat_id_str][name] = value - utilities.send_message(self, msg.chat.id, '"' .. name .. '" has been set to "' .. value .. '".', true) - end + if not name or not value then + utilities.send_message(self, msg.chat.id, setandget.doc, true, nil, true) + elseif value == '--' or value == '—' then + self.database.setandget[chat_id_str][name] = nil + utilities.send_message(self, msg.chat.id, 'That value has been deleted.') + else + self.database.setandget[chat_id_str][name] = value + utilities.send_message(self, msg.chat.id, '"' .. name .. '" has been set to "' .. value .. '".', true) + end - elseif msg.text_lower:match('^'..config.cmd_pat..'get') then + elseif msg.text_lower:match('^'..config.cmd_pat..'get') then - if not input then - local output - if utilities.table_size(self.database.setandget[chat_id_str]) == 0 then - output = 'No values have been stored here.' - else - output = '*List of stored values:*\n' - for k,v in pairs(self.database.setandget[chat_id_str]) do - output = output .. '• ' .. k .. ': `' .. v .. '`\n' - end - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - return - end + if not input then + local output + if utilities.table_size(self.database.setandget[chat_id_str]) == 0 then + output = 'No values have been stored here.' + else + output = '*List of stored values:*\n' + for k,v in pairs(self.database.setandget[chat_id_str]) do + output = output .. '• ' .. k .. ': `' .. v .. '`\n' + end + end + utilities.send_message(self, msg.chat.id, output, true, nil, true) + return + end - local output - if self.database.setandget[chat_id_str][input:lower()] then - output = '`' .. self.database.setandget[chat_id_str][input:lower()] .. '`' - else - output = 'There is no value stored by that name.' - end + local output + if self.database.setandget[chat_id_str][input:lower()] then + output = '`' .. self.database.setandget[chat_id_str][input:lower()] .. '`' + else + output = 'There is no value stored by that name.' + end - utilities.send_message(self, msg.chat.id, output, true, nil, true) + utilities.send_message(self, msg.chat.id, output, true, nil, true) - end + end end diff --git a/otouto/plugins/shell.lua b/otouto/plugins/shell.lua index 66a42b9..fdb88ed 100644 --- a/otouto/plugins/shell.lua +++ b/otouto/plugins/shell.lua @@ -3,32 +3,32 @@ local shell = {} local utilities = require('otouto.utilities') function shell:init(config) - shell.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('run', true).table + shell.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('run', true).table end function shell:action(msg, config) - if msg.from.id ~= config.admin then - return - end + if msg.from.id ~= config.admin then + return + end - local input = utilities.input(msg.text) - input = input:gsub('—', '--') + local input = utilities.input(msg.text) + input = input:gsub('—', '--') - if not input then - utilities.send_reply(self, msg, 'Please specify a command to run.') - return - end + if not input then + utilities.send_reply(self, msg, 'Please specify a command to run.') + return + end - local f = io.popen(input) - local output = f:read('*all') - f:close() - if output:len() == 0 then - output = 'Done!' - else - output = '```\n' .. output .. '\n```' - end - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + local f = io.popen(input) + local output = f:read('*all') + f:close() + if output:len() == 0 then + output = 'Done!' + else + output = '```\n' .. output .. '\n```' + end + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) end diff --git a/otouto/plugins/shout.lua b/otouto/plugins/shout.lua index 1ea7bf0..c4b9b6d 100644 --- a/otouto/plugins/shout.lua +++ b/otouto/plugins/shout.lua @@ -6,45 +6,45 @@ shout.command = 'shout <text>' local utf8 = '('..utilities.char.utf_8..'*)' function shout:init(config) - shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table - shout.doc = config.cmd_pat .. 'shout <text> \nShouts something. Input may be the replied-to message.' + shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table + shout.doc = config.cmd_pat .. 'shout <text> \nShouts something. Input may be the replied-to message.' end function shout:action(msg) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, shout.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, shout.doc, true) + return + end - input = utilities.trim(input) - input = input:upper() + input = utilities.trim(input) + input = input:upper() - local output = '' - local inc = 0 - local ilen = 0 - for match in input:gmatch(utf8) do - if ilen < 20 then - ilen = ilen + 1 - output = output .. match .. ' ' - end - end - ilen = 0 - output = output .. '\n' - for match in input:sub(2):gmatch(utf8) do - if ilen < 19 then - local spacing = '' - for _ = 1, inc do - spacing = spacing .. ' ' - end - inc = inc + 1 - ilen = ilen + 1 - output = output .. match .. ' ' .. spacing .. match .. '\n' - end - end - output = '```\n' .. utilities.trim(output) .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, false, true) + local output = '' + local inc = 0 + local ilen = 0 + for match in input:gmatch(utf8) do + if ilen < 20 then + ilen = ilen + 1 + output = output .. match .. ' ' + end + end + ilen = 0 + output = output .. '\n' + for match in input:sub(2):gmatch(utf8) do + if ilen < 19 then + local spacing = '' + for _ = 1, inc do + spacing = spacing .. ' ' + end + inc = inc + 1 + ilen = ilen + 1 + output = output .. match .. ' ' .. spacing .. match .. '\n' + end + end + output = '```\n' .. utilities.trim(output) .. '\n```' + utilities.send_message(self, msg.chat.id, output, true, false, true) end diff --git a/otouto/plugins/slap.lua b/otouto/plugins/slap.lua index b3cbf7c..8919a17 100644 --- a/otouto/plugins/slap.lua +++ b/otouto/plugins/slap.lua @@ -5,160 +5,160 @@ local utilities = require('otouto.utilities') slap.command = 'slap [target]' function slap:init(config) - slap.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('slap', true).table - slap.doc = config.cmd_pat .. 'slap [target] \nSlap somebody.' + slap.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('slap', true).table + slap.doc = config.cmd_pat .. 'slap [target] \nSlap somebody.' end local slaps = { - 'VICTIM was shot by VICTOR.', - 'VICTIM was pricked to death.', - 'VICTIM walked into a cactus while trying to escape VICTOR.', - 'VICTIM drowned.', - 'VICTIM drowned whilst trying to escape VICTOR.', - 'VICTIM blew up.', - 'VICTIM was blown up by VICTOR.', - 'VICTIM hit the ground too hard.', - 'VICTIM fell from a high place.', - 'VICTIM fell off a ladder.', - 'VICTIM fell into a patch of cacti.', - 'VICTIM was doomed to fall by VICTOR.', - 'VICTIM was blown from a high place by VICTOR.', - 'VICTIM was squashed by a falling anvil.', - 'VICTIM went up in flames.', - 'VICTIM burned to death.', - 'VICTIM was burnt to a crisp whilst fighting VICTOR.', - 'VICTIM walked into a fire whilst fighting VICTOR.', - 'VICTIM tried to swim in lava.', - 'VICTIM tried to swim in lava while trying to escape VICTOR.', - 'VICTIM was struck by lightning.', - 'VICTIM was slain by VICTOR.', - 'VICTIM got finished off by VICTOR.', - 'VICTIM was killed by magic.', - 'VICTIM was killed by VICTOR using magic.', - 'VICTIM starved to death.', - 'VICTIM suffocated in a wall.', - 'VICTIM fell out of the world.', - 'VICTIM was knocked into the void by VICTOR.', - 'VICTIM withered away.', - 'VICTIM was pummeled by VICTOR.', - 'VICTIM was fragged by VICTOR.', - 'VICTIM was desynchronized.', - 'VICTIM was wasted.', - 'VICTIM was busted.', - 'VICTIM\'s bones are scraped clean by the desolate wind.', - 'VICTIM has died of dysentery.', - 'VICTIM fainted.', - 'VICTIM is out of usable Pokemon! VICTIM whited out!', - 'VICTIM is out of usable Pokemon! VICTIM blacked out!', - 'VICTIM whited out!', - 'VICTIM blacked out!', - 'VICTIM says goodbye to this cruel world.', - 'VICTIM got rekt.', - 'VICTIM was sawn in half by VICTOR.', - 'VICTIM died. I blame VICTOR.', - 'VICTIM was axe-murdered by VICTOR.', - 'VICTIM\'s melon was split by VICTOR.', - 'VICTIM was sliced and diced by VICTOR.', - 'VICTIM was split from crotch to sternum by VICTOR.', - 'VICTIM\'s death put another notch in VICTOR\'s axe.', - 'VICTIM died impossibly!', - 'VICTIM died from VICTOR\'s mysterious tropical disease.', - 'VICTIM escaped infection by dying.', - 'VICTIM played hot-potato with a grenade.', - 'VICTIM was knifed by VICTOR.', - 'VICTIM fell on his sword.', - 'VICTIM ate a grenade.', - 'VICTIM practiced being VICTOR\'s clay pigeon.', - 'VICTIM is what\'s for dinner!', - 'VICTIM was terminated by VICTOR.', - 'VICTIM was shot before being thrown out of a plane.', - 'VICTIM was not invincible.', - 'VICTIM has encountered an error.', - 'VICTIM died and reincarnated as a goat.', - 'VICTOR threw VICTIM off a building.', - 'VICTIM is sleeping with the fishes.', - 'VICTIM got a premature burial.', - 'VICTOR replaced all of VICTIM\'s music with Nickelback.', - 'VICTOR spammed VICTIM\'s email.', - 'VICTOR made VICTIM a knuckle sandwich.', - 'VICTOR slapped VICTIM with pure nothing.', - 'VICTOR hit VICTIM with a small, interstellar spaceship.', - 'VICTIM was quickscoped by VICTOR.', - 'VICTOR put VICTIM in check-mate.', - 'VICTOR RSA-encrypted VICTIM and deleted the private key.', - 'VICTOR put VICTIM in the friendzone.', - 'VICTOR slaps VICTIM with a DMCA takedown request!', - 'VICTIM became a corpse blanket for VICTOR.', - 'Death is when the monsters get you. Death comes for VICTIM.', - 'Cowards die many times before their death. VICTIM never tasted death but once.', - 'VICTIM died of hospital gangrene.', - 'VICTIM got a house call from Doctor VICTOR.', - 'VICTOR beheaded VICTIM.', - 'VICTIM got stoned...by an angry mob.', - 'VICTOR sued the pants off VICTIM.', - 'VICTIM was impeached.', - 'VICTIM was one-hit KO\'d by VICTOR.', - 'VICTOR sent VICTIM to /dev/null.', - 'VICTOR sent VICTIM down the memory hole.', - 'VICTIM was a mistake.', - '"VICTIM was a mistake." - VICTOR', - 'VICTOR checkmated VICTIM in two moves.' + 'VICTIM was shot by VICTOR.', + 'VICTIM was pricked to death.', + 'VICTIM walked into a cactus while trying to escape VICTOR.', + 'VICTIM drowned.', + 'VICTIM drowned whilst trying to escape VICTOR.', + 'VICTIM blew up.', + 'VICTIM was blown up by VICTOR.', + 'VICTIM hit the ground too hard.', + 'VICTIM fell from a high place.', + 'VICTIM fell off a ladder.', + 'VICTIM fell into a patch of cacti.', + 'VICTIM was doomed to fall by VICTOR.', + 'VICTIM was blown from a high place by VICTOR.', + 'VICTIM was squashed by a falling anvil.', + 'VICTIM went up in flames.', + 'VICTIM burned to death.', + 'VICTIM was burnt to a crisp whilst fighting VICTOR.', + 'VICTIM walked into a fire whilst fighting VICTOR.', + 'VICTIM tried to swim in lava.', + 'VICTIM tried to swim in lava while trying to escape VICTOR.', + 'VICTIM was struck by lightning.', + 'VICTIM was slain by VICTOR.', + 'VICTIM got finished off by VICTOR.', + 'VICTIM was killed by magic.', + 'VICTIM was killed by VICTOR using magic.', + 'VICTIM starved to death.', + 'VICTIM suffocated in a wall.', + 'VICTIM fell out of the world.', + 'VICTIM was knocked into the void by VICTOR.', + 'VICTIM withered away.', + 'VICTIM was pummeled by VICTOR.', + 'VICTIM was fragged by VICTOR.', + 'VICTIM was desynchronized.', + 'VICTIM was wasted.', + 'VICTIM was busted.', + 'VICTIM\'s bones are scraped clean by the desolate wind.', + 'VICTIM has died of dysentery.', + 'VICTIM fainted.', + 'VICTIM is out of usable Pokemon! VICTIM whited out!', + 'VICTIM is out of usable Pokemon! VICTIM blacked out!', + 'VICTIM whited out!', + 'VICTIM blacked out!', + 'VICTIM says goodbye to this cruel world.', + 'VICTIM got rekt.', + 'VICTIM was sawn in half by VICTOR.', + 'VICTIM died. I blame VICTOR.', + 'VICTIM was axe-murdered by VICTOR.', + 'VICTIM\'s melon was split by VICTOR.', + 'VICTIM was sliced and diced by VICTOR.', + 'VICTIM was split from crotch to sternum by VICTOR.', + 'VICTIM\'s death put another notch in VICTOR\'s axe.', + 'VICTIM died impossibly!', + 'VICTIM died from VICTOR\'s mysterious tropical disease.', + 'VICTIM escaped infection by dying.', + 'VICTIM played hot-potato with a grenade.', + 'VICTIM was knifed by VICTOR.', + 'VICTIM fell on his sword.', + 'VICTIM ate a grenade.', + 'VICTIM practiced being VICTOR\'s clay pigeon.', + 'VICTIM is what\'s for dinner!', + 'VICTIM was terminated by VICTOR.', + 'VICTIM was shot before being thrown out of a plane.', + 'VICTIM was not invincible.', + 'VICTIM has encountered an error.', + 'VICTIM died and reincarnated as a goat.', + 'VICTOR threw VICTIM off a building.', + 'VICTIM is sleeping with the fishes.', + 'VICTIM got a premature burial.', + 'VICTOR replaced all of VICTIM\'s music with Nickelback.', + 'VICTOR spammed VICTIM\'s email.', + 'VICTOR made VICTIM a knuckle sandwich.', + 'VICTOR slapped VICTIM with pure nothing.', + 'VICTOR hit VICTIM with a small, interstellar spaceship.', + 'VICTIM was quickscoped by VICTOR.', + 'VICTOR put VICTIM in check-mate.', + 'VICTOR RSA-encrypted VICTIM and deleted the private key.', + 'VICTOR put VICTIM in the friendzone.', + 'VICTOR slaps VICTIM with a DMCA takedown request!', + 'VICTIM became a corpse blanket for VICTOR.', + 'Death is when the monsters get you. Death comes for VICTIM.', + 'Cowards die many times before their death. VICTIM never tasted death but once.', + 'VICTIM died of hospital gangrene.', + 'VICTIM got a house call from Doctor VICTOR.', + 'VICTOR beheaded VICTIM.', + 'VICTIM got stoned...by an angry mob.', + 'VICTOR sued the pants off VICTIM.', + 'VICTIM was impeached.', + 'VICTIM was one-hit KO\'d by VICTOR.', + 'VICTOR sent VICTIM to /dev/null.', + 'VICTOR sent VICTIM down the memory hole.', + 'VICTIM was a mistake.', + '"VICTIM was a mistake." - VICTOR', + 'VICTOR checkmated VICTIM in two moves.' } -- optimize later function slap:action(msg) - local input = utilities.input(msg.text) - local victor_id = msg.from.id - local victim_id - if msg.reply_to_message then - victim_id = msg.reply_to_message.from.id - else - if input then - if tonumber(input) then - victim_id = tonumber(input) - elseif input:match('^@') then - local t = utilities.resolve_username(self, input) - if t then - victim_id = t.id - end - end - end - end - -- IDs - if victim_id then - if victim_id == victor_id then - victor_id = self.info.id - end - else - if not input then - victor_id = self.info.id - victim_id = msg.from.id - end - end - -- Names - local victor_name, victim_name - if input and not victim_id then - victim_name = input - else - local victim_id_str = tostring(victim_id) - if self.database.userdata[victim_id_str] and self.database.userdata[victim_id_str].nickname then - victim_name = self.database.userdata[victim_id_str].nickname - elseif self.database.users[victim_id_str] then - victim_name = utilities.build_name(self.database.users[victim_id_str].first_name, self.database.users[victim_id_str].last_name) - else - victim_name = victim_id_str - end - end - local victor_id_str = tostring(victor_id) - if self.database.userdata[victor_id_str] and self.database.userdata[victor_id_str].nickname then - victor_name = self.database.userdata[victor_id_str].nickname - elseif self.database.users[victor_id_str] then - victor_name = utilities.build_name(self.database.users[victor_id_str].first_name, self.database.users[victor_id_str].last_name) - else - victor_name = self.info.first_name - end - local output = utilities.char.zwnj .. slaps[math.random(#slaps)]:gsub('VICTIM', victim_name):gsub('VICTOR', victor_name) - utilities.send_message(self, msg.chat.id, output) + local input = utilities.input(msg.text) + local victor_id = msg.from.id + local victim_id + if msg.reply_to_message then + victim_id = msg.reply_to_message.from.id + else + if input then + if tonumber(input) then + victim_id = tonumber(input) + elseif input:match('^@') then + local t = utilities.resolve_username(self, input) + if t then + victim_id = t.id + end + end + end + end + -- IDs + if victim_id then + if victim_id == victor_id then + victor_id = self.info.id + end + else + if not input then + victor_id = self.info.id + victim_id = msg.from.id + end + end + -- Names + local victor_name, victim_name + if input and not victim_id then + victim_name = input + else + local victim_id_str = tostring(victim_id) + if self.database.userdata[victim_id_str] and self.database.userdata[victim_id_str].nickname then + victim_name = self.database.userdata[victim_id_str].nickname + elseif self.database.users[victim_id_str] then + victim_name = utilities.build_name(self.database.users[victim_id_str].first_name, self.database.users[victim_id_str].last_name) + else + victim_name = victim_id_str + end + end + local victor_id_str = tostring(victor_id) + if self.database.userdata[victor_id_str] and self.database.userdata[victor_id_str].nickname then + victor_name = self.database.userdata[victor_id_str].nickname + elseif self.database.users[victor_id_str] then + victor_name = utilities.build_name(self.database.users[victor_id_str].first_name, self.database.users[victor_id_str].last_name) + else + victor_name = self.info.first_name + end + local output = utilities.char.zwnj .. slaps[math.random(#slaps)]:gsub('VICTIM', victim_name):gsub('VICTOR', victor_name) + utilities.send_message(self, msg.chat.id, output) end return slap diff --git a/otouto/plugins/starwars-crawl.lua b/otouto/plugins/starwars-crawl.lua index ed4594c..500be3e 100644 --- a/otouto/plugins/starwars-crawl.lua +++ b/otouto/plugins/starwars-crawl.lua @@ -8,71 +8,71 @@ local utilities = require('otouto.utilities') local starwars = {} function starwars:init(config) - starwars.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('starwars', true):t('sw', true).table - starwars.doc = config.cmd_pat .. [[starwars <query> + starwars.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('starwars', true):t('sw', true).table + starwars.doc = config.cmd_pat .. [[starwars <query> Returns the opening crawl from the specified Star Wars film. Alias: ]] .. config.cmd_pat .. 'sw' - starwars.command = 'starwars <query>' - starwars.base_url = 'http://swapi.co/api/films/' + starwars.command = 'starwars <query>' + starwars.base_url = 'http://swapi.co/api/films/' end local films_by_number = { - ['phantom menace'] = 4, - ['attack of the clones'] = 5, - ['revenge of the sith'] = 6, - ['new hope'] = 1, - ['empire strikes back'] = 2, - ['return of the jedi'] = 3, - ['force awakens'] = 7 + ['phantom menace'] = 4, + ['attack of the clones'] = 5, + ['revenge of the sith'] = 6, + ['new hope'] = 1, + ['empire strikes back'] = 2, + ['return of the jedi'] = 3, + ['force awakens'] = 7 } local corrected_numbers = { - 4, - 5, - 6, - 1, - 2, - 3, - 7 + 4, + 5, + 6, + 1, + 2, + 3, + 7 } function starwars:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, starwars.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, starwars.doc, true) + return + end - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) + bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) - local film - if tonumber(input) then - input = tonumber(input) - film = corrected_numbers[input] or input - else - for title, number in pairs(films_by_number) do - if string.match(input, title) then - film = number - break - end - end - end + local film + if tonumber(input) then + input = tonumber(input) + film = corrected_numbers[input] or input + else + for title, number in pairs(films_by_number) do + if string.match(input, title) then + film = number + break + end + end + end - if not film then - utilities.send_reply(self, msg, config.errors.results) - return - end + if not film then + utilities.send_reply(self, msg, config.errors.results) + return + end - local url = starwars.base_url .. film - local jstr, code = HTTP.request(url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local url = starwars.base_url .. film + local jstr, code = HTTP.request(url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local output = '*' .. JSON.decode(jstr).opening_crawl .. '*' - utilities.send_message(self, msg.chat.id, output, true, nil, true) + local output = '*' .. JSON.decode(jstr).opening_crawl .. '*' + utilities.send_message(self, msg.chat.id, output, true, nil, true) end return starwars diff --git a/otouto/plugins/time.lua b/otouto/plugins/time.lua index e028428..81af8a1 100644 --- a/otouto/plugins/time.lua +++ b/otouto/plugins/time.lua @@ -8,52 +8,52 @@ time.command = 'time <location>' time.base_url = 'https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s×tamp=%s' function time:init(config) - time.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('time', true).table - time.doc = config.cmd_pat .. [[time <location> + time.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('time', true).table + time.doc = config.cmd_pat .. [[time <location> Returns the time, date, and timezone for the given location.]] end function time:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, time.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, time.doc, true) + return + end - local coords = utilities.get_coords(input, config) - if type(coords) == 'string' then - utilities.send_reply(self, msg, coords) - return - end + local coords = utilities.get_coords(input, config) + if type(coords) == 'string' then + utilities.send_reply(self, msg, coords) + return + end - local now = os.time() - local utc = os.time(os.date('!*t', now)) - local url = time.base_url:format(coords.lat, coords.lon, utc) - local jstr, code = HTTPS.request(url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local now = os.time() + local utc = os.time(os.date('!*t', now)) + local url = time.base_url:format(coords.lat, coords.lon, utc) + local jstr, code = HTTPS.request(url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local data = JSON.decode(jstr) - if data.status == 'ZERO_RESULTS' then - utilities.send_reply(self, msg, config.errors.results) - return - end + local data = JSON.decode(jstr) + if data.status == 'ZERO_RESULTS' then + utilities.send_reply(self, msg, config.errors.results) + return + end - local timestamp = now + data.rawOffset + data.dstOffset - local utcoff = (data.rawOffset + data.dstOffset) / 3600 - if utcoff == math.abs(utcoff) then - utcoff = '+' .. utilities.pretty_float(utcoff) - else - utcoff = utilities.pretty_float(utcoff) - end - local output = string.format('```\n%s\n%s (UTC%s)\n```', - os.date('!%I:%M %p\n%A, %B %d, %Y', timestamp), - data.timeZoneName, - utcoff - ) - utilities.send_reply(self, msg, output, true) + local timestamp = now + data.rawOffset + data.dstOffset + local utcoff = (data.rawOffset + data.dstOffset) / 3600 + if utcoff == math.abs(utcoff) then + utcoff = '+' .. utilities.pretty_float(utcoff) + else + utcoff = utilities.pretty_float(utcoff) + end + local output = string.format('```\n%s\n%s (UTC%s)\n```', + os.date('!%I:%M %p\n%A, %B %d, %Y', timestamp), + data.timeZoneName, + utcoff + ) + utilities.send_reply(self, msg, output, true) end return time diff --git a/otouto/plugins/translate.lua b/otouto/plugins/translate.lua index 5576d03..b427aef 100644 --- a/otouto/plugins/translate.lua +++ b/otouto/plugins/translate.lua @@ -8,38 +8,38 @@ local utilities = require('otouto.utilities') translate.command = 'translate [text]' function translate:init(config) - assert(config.yandex_key, - 'translate.lua requires a Yandex translate API key from http://tech.yandex.com/keys/get.' - ) + assert(config.yandex_key, + 'translate.lua requires a Yandex translate API key from http://tech.yandex.com/keys/get.' + ) - translate.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('translate', true):t('tl', true).table - translate.doc = config.cmd_pat .. [[translate [text] + translate.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('translate', true):t('tl', true).table + translate.doc = config.cmd_pat .. [[translate [text] Translates input or the replied-to message into the bot's language.]] - translate.base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=%s' + translate.base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=%s' end function translate:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, translate.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, translate.doc, true) + return + end - local url = translate.base_url:format(URL.escape(input)) - local jstr, code = HTTPS.request(url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local url = translate.base_url:format(URL.escape(input)) + local jstr, code = HTTPS.request(url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local data = JSON.decode(jstr) - if data.code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local data = JSON.decode(jstr) + if data.code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true) + utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true) end return translate diff --git a/otouto/plugins/urbandictionary.lua b/otouto/plugins/urbandictionary.lua index 9a2cfcf..2dacaad 100644 --- a/otouto/plugins/urbandictionary.lua +++ b/otouto/plugins/urbandictionary.lua @@ -9,42 +9,42 @@ urbandictionary.command = 'urbandictionary <query>' urbandictionary.base_url = 'http://api.urbandictionary.com/v0/define?term=' function urbandictionary:init(config) - urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat) - :t('urbandictionary', true):t('ud', true):t('urban', true).table - urbandictionary.doc = [[ + urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat) + :t('urbandictionary', true):t('ud', true):t('urban', true).table + urbandictionary.doc = [[ /urbandictionary <query> Returns a definition from Urban Dictionary. Aliases: /ud, /urban - ]] - urbandictionary.doc = urbandictionary.doc:gsub('/', config.cmd_pat) + ]] + urbandictionary.doc = urbandictionary.doc:gsub('/', config.cmd_pat) end function urbandictionary:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, urbandictionary.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, urbandictionary.doc, true) + return + end - local url = urbandictionary.base_url .. URL.escape(input) - local jstr, code = HTTP.request(url) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local url = urbandictionary.base_url .. URL.escape(input) + local jstr, code = HTTP.request(url) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local data = JSON.decode(jstr) - local output - if data.result_type == 'no_results' then - output = config.errors.results - else - output = string.format('*%s*\n\n%s\n\n_%s_', - data.list[1].word:gsub('*', '*\\**'), - utilities.trim(utilities.md_escape(data.list[1].definition)), - utilities.trim((data.list[1].example or '')):gsub('_', '_\\__') - ) - end - utilities.send_reply(self, msg, output, true) + local data = JSON.decode(jstr) + local output + if data.result_type == 'no_results' then + output = config.errors.results + else + output = string.format('*%s*\n\n%s\n\n_%s_', + data.list[1].word:gsub('*', '*\\**'), + utilities.trim(utilities.md_escape(data.list[1].definition)), + utilities.trim((data.list[1].example or '')):gsub('_', '_\\__') + ) + end + utilities.send_reply(self, msg, output, true) end return urbandictionary diff --git a/otouto/plugins/weather.lua b/otouto/plugins/weather.lua index 60b0862..7747ba3 100644 --- a/otouto/plugins/weather.lua +++ b/otouto/plugins/weather.lua @@ -6,12 +6,12 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function weather:init(config) - assert(config.owm_api_key, - 'weather.lua requires an OpenWeatherMap API key from http://openweathermap.org/API.' - ) + assert(config.owm_api_key, + 'weather.lua requires an OpenWeatherMap API key from http://openweathermap.org/API.' + ) - weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table - weather.doc = config.cmd_pat .. [[weather <location> + weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table + weather.doc = config.cmd_pat .. [[weather <location> Returns the current weather conditions for a given location.]] end @@ -19,37 +19,37 @@ weather.command = 'weather <location>' function weather:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, weather.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, weather.doc, true) + return + end - local coords = utilities.get_coords(input, config) - if type(coords) == 'string' then - utilities.send_reply(self, msg, coords) - return - end + local coords = utilities.get_coords(input, config) + if type(coords) == 'string' then + utilities.send_reply(self, msg, coords) + return + end - local url = 'http://api.openweathermap.org/data/2.5/weather?APPID=' .. config.owm_api_key .. '&lat=' .. coords.lat .. '&lon=' .. coords.lon + local url = 'http://api.openweathermap.org/data/2.5/weather?APPID=' .. config.owm_api_key .. '&lat=' .. coords.lat .. '&lon=' .. coords.lon - local jstr, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, res = HTTP.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local jdat = JSON.decode(jstr) - if jdat.cod ~= 200 then - utilities.send_reply(self, msg, 'Error: City not found.') - return - end + local jdat = JSON.decode(jstr) + if jdat.cod ~= 200 then + utilities.send_reply(self, msg, 'Error: City not found.') + return + end - local celsius = string.format('%.2f', jdat.main.temp - 273.15) - local fahrenheit = string.format('%.2f', celsius * (9/5) + 32) - local output = '`' .. celsius .. '°C | ' .. fahrenheit .. '°F, ' .. jdat.weather[1].description .. '.`' + local celsius = string.format('%.2f', jdat.main.temp - 273.15) + local fahrenheit = string.format('%.2f', celsius * (9/5) + 32) + local output = '`' .. celsius .. '°C | ' .. fahrenheit .. '°F, ' .. jdat.weather[1].description .. '.`' - utilities.send_reply(self, msg, output, true) + utilities.send_reply(self, msg, output, true) end diff --git a/otouto/plugins/whoami.lua b/otouto/plugins/whoami.lua index 946cdf4..9059159 100644 --- a/otouto/plugins/whoami.lua +++ b/otouto/plugins/whoami.lua @@ -6,54 +6,54 @@ local bindings = require('otouto.bindings') whoami.command = 'whoami' function whoami:init(config) - whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who'):t('whoami').table - whoami.doc = [[ + whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who'):t('whoami').table + whoami.doc = [[ Returns user and chat info for you or the replied-to message. Alias: ]] .. config.cmd_pat .. 'who' end function whoami:action(msg) - -- Operate on the replied-to message, if it exists. - msg = msg.reply_to_message or msg - -- If it's a private conversation, bot is chat, unless bot is from. - local chat = msg.from.id == msg.chat.id and self.info or msg.chat - -- Names for the user and group, respectively. HTML-escaped. - local from_name = utilities.html_escape( - utilities.build_name( - msg.from.first_name, - msg.from.last_name - ) - ) - local chat_name = utilities.html_escape( - chat.title - or utilities.build_name(chat.first_name, chat.last_name) - ) - -- "Normalize" a group ID so it's not arbitrarily modified by the bot API. - local chat_id = math.abs(chat.id) - if chat_id > 1000000000000 then chat_id = chat_id - 1000000000000 end - -- Do the thing. - local output = string.format( - 'You are %s <code>[%s]</code>, and you are messaging %s <code>[%s]</code>.', - msg.from.username and string.format( - '@%s, also known as <b>%s</b>', - msg.from.username, - from_name - ) or '<b>' .. from_name .. '</b>', - msg.from.id, - msg.chat.username and string.format( - '@%s, also known as <b>%s</b>', - chat.username, - chat_name - ) or '<b>' .. chat_name .. '</b>', - chat_id - ) - bindings.sendMessage(self, { - chat_id = msg.chat.id, - reply_to_message_id = msg.message_id, - disable_web_page_preview = true, - parse_mode = 'HTML', - text = output - }) + -- Operate on the replied-to message, if it exists. + msg = msg.reply_to_message or msg + -- If it's a private conversation, bot is chat, unless bot is from. + local chat = msg.from.id == msg.chat.id and self.info or msg.chat + -- Names for the user and group, respectively. HTML-escaped. + local from_name = utilities.html_escape( + utilities.build_name( + msg.from.first_name, + msg.from.last_name + ) + ) + local chat_name = utilities.html_escape( + chat.title + or utilities.build_name(chat.first_name, chat.last_name) + ) + -- "Normalize" a group ID so it's not arbitrarily modified by the bot API. + local chat_id = math.abs(chat.id) + if chat_id > 1000000000000 then chat_id = chat_id - 1000000000000 end + -- Do the thing. + local output = string.format( + 'You are %s <code>[%s]</code>, and you are messaging %s <code>[%s]</code>.', + msg.from.username and string.format( + '@%s, also known as <b>%s</b>', + msg.from.username, + from_name + ) or '<b>' .. from_name .. '</b>', + msg.from.id, + msg.chat.username and string.format( + '@%s, also known as <b>%s</b>', + chat.username, + chat_name + ) or '<b>' .. chat_name .. '</b>', + chat_id + ) + bindings.sendMessage(self, { + chat_id = msg.chat.id, + reply_to_message_id = msg.message_id, + disable_web_page_preview = true, + parse_mode = 'HTML', + text = output + }) end return whoami diff --git a/otouto/plugins/wikipedia.lua b/otouto/plugins/wikipedia.lua index 54336f5..0b81144 100644 --- a/otouto/plugins/wikipedia.lua +++ b/otouto/plugins/wikipedia.lua @@ -8,83 +8,83 @@ local utilities = require('otouto.utilities') wikipedia.command = 'wikipedia <query>' function wikipedia:init(config) - wikipedia.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('wikipedia', true):t('wiki', true):t('w', true).table - wikipedia.doc = config.cmd_pat .. [[wikipedia <query> + wikipedia.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('wikipedia', true):t('wiki', true):t('w', true).table + wikipedia.doc = config.cmd_pat .. [[wikipedia <query> Returns an article from Wikipedia. Aliases: ]] .. config.cmd_pat .. 'w, ' .. config.cmd_pat .. 'wiki' - wikipedia.search_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=' - wikipedia.res_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles=' - wikipedia.art_url = 'https://' .. config.lang .. '.wikipedia.org/wiki/' + wikipedia.search_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=' + wikipedia.res_url = 'https://' .. config.lang .. '.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles=' + wikipedia.art_url = 'https://' .. config.lang .. '.wikipedia.org/wiki/' end function wikipedia:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, wikipedia.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, wikipedia.doc, true) + return + end - local jstr, code = HTTPS.request(wikipedia.search_url .. URL.escape(input)) - if code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, code = HTTPS.request(wikipedia.search_url .. URL.escape(input)) + if code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local data = JSON.decode(jstr) - if data.query.searchinfo.totalhits == 0 then - utilities.send_reply(self, msg, config.errors.results) - return - end + local data = JSON.decode(jstr) + if data.query.searchinfo.totalhits == 0 then + utilities.send_reply(self, msg, config.errors.results) + return + end - local title - for _, v in ipairs(data.query.search) do - if not v.snippet:match('may refer to:') then - title = v.title - break - end - end - if not title then - utilities.send_reply(self, msg, config.errors.results) - return - end + local title + for _, v in ipairs(data.query.search) do + if not v.snippet:match('may refer to:') then + title = v.title + break + end + end + if not title then + utilities.send_reply(self, msg, config.errors.results) + return + end - local res_jstr, res_code = HTTPS.request(wikipedia.res_url .. URL.escape(title)) - if res_code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local res_jstr, res_code = HTTPS.request(wikipedia.res_url .. URL.escape(title)) + if res_code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local _, text = next(JSON.decode(res_jstr).query.pages) - if not text then - utilities.send_reply(self, msg, config.errors.results) - return - end + local _, text = next(JSON.decode(res_jstr).query.pages) + if not text then + utilities.send_reply(self, msg, config.errors.results) + return + end - text = text.extract - -- Remove crap and take only the first paragraph. - text = text:gsub('</?.->', ''):gsub('%[.+%]', '') - local l = text:find('\n') - if l then - text = text:sub(1, l-1) - end - local url = wikipedia.art_url .. URL.escape(title) - title = utilities.html_escape(title) - -- If the beginning of the article is the title, embolden that. - -- Otherwise, we'll add a title in bold. - local short_title = title:gsub('%(.+%)', '') - local combined_text, count = text:gsub('^'..short_title, '<b>'..short_title..'</b>') - local body - if count == 1 then - body = combined_text - else - body = '<b>' .. title .. '</b>\n' .. text - end - local output = string.format( - '%s\n<a href="%s">Read more.</a>', - body, - utilities.html_escape(url) - ) - utilities.send_message(self, msg.chat.id, output, true, nil, 'html') + text = text.extract + -- Remove crap and take only the first paragraph. + text = text:gsub('</?.->', ''):gsub('%[.+%]', '') + local l = text:find('\n') + if l then + text = text:sub(1, l-1) + end + local url = wikipedia.art_url .. URL.escape(title) + title = utilities.html_escape(title) + -- If the beginning of the article is the title, embolden that. + -- Otherwise, we'll add a title in bold. + local short_title = title:gsub('%(.+%)', '') + local combined_text, count = text:gsub('^'..short_title, '<b>'..short_title..'</b>') + local body + if count == 1 then + body = combined_text + else + body = '<b>' .. title .. '</b>\n' .. text + end + local output = string.format( + '%s\n<a href="%s">Read more.</a>', + body, + utilities.html_escape(url) + ) + utilities.send_message(self, msg.chat.id, output, true, nil, 'html') end return wikipedia diff --git a/otouto/plugins/xkcd.lua b/otouto/plugins/xkcd.lua index a2484a2..ce0a262 100644 --- a/otouto/plugins/xkcd.lua +++ b/otouto/plugins/xkcd.lua @@ -9,44 +9,44 @@ xkcd.base_url = 'https://xkcd.com/info.0.json' xkcd.strip_url = 'http://xkcd.com/%s/info.0.json' function xkcd:init(config) - xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table - xkcd.doc = config.cmd_pat .. [[xkcd [i] + xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table + xkcd.doc = config.cmd_pat .. [[xkcd [i] Returns the latest xkcd strip and its alt text. If a number is given, returns that number strip. If "r" is passed in place of a number, returns a random strip.]] - local jstr = HTTP.request(xkcd.base_url) - if jstr then - local data = JSON.decode(jstr) - if data then - xkcd.latest = data.num - end - end - xkcd.latest = xkcd.latest or 1700 + local jstr = HTTP.request(xkcd.base_url) + if jstr then + local data = JSON.decode(jstr) + if data then + xkcd.latest = data.num + end + end + xkcd.latest = xkcd.latest or 1700 end function xkcd:action(msg, config) - local input = utilities.get_word(msg.text, 2) - if input == 'r' then - input = math.random(xkcd.latest) - elseif tonumber(input) then - input = tonumber(input) - else - input = xkcd.latest - end - local url = xkcd.strip_url:format(input) - local jstr, code = HTTP.request(url) - if code == 404 then - utilities.send_reply(self, msg, config.errors.results) - elseif code ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - else - local data = JSON.decode(jstr) - local output = string.format('*%s (*[%s](%s)*)*\n_%s_', - data.safe_title:gsub('*', '*\\**'), - data.num, - data.img, - data.alt:gsub('_', '_\\__') - ) - utilities.send_message(self, msg.chat.id, output, false, nil, true) - end + local input = utilities.get_word(msg.text, 2) + if input == 'r' then + input = math.random(xkcd.latest) + elseif tonumber(input) then + input = tonumber(input) + else + input = xkcd.latest + end + local url = xkcd.strip_url:format(input) + local jstr, code = HTTP.request(url) + if code == 404 then + utilities.send_reply(self, msg, config.errors.results) + elseif code ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + else + local data = JSON.decode(jstr) + local output = string.format('*%s (*[%s](%s)*)*\n_%s_', + data.safe_title:gsub('*', '*\\**'), + data.num, + data.img, + data.alt:gsub('_', '_\\__') + ) + utilities.send_message(self, msg.chat.id, output, false, nil, true) + end end return xkcd diff --git a/otouto/plugins/youtube.lua b/otouto/plugins/youtube.lua index 2de16ae..c86ea3d 100644 --- a/otouto/plugins/youtube.lua +++ b/otouto/plugins/youtube.lua @@ -8,12 +8,12 @@ local JSON = require('dkjson') local utilities = require('otouto.utilities') function youtube:init(config) - assert(config.google_api_key, - 'youtube.lua requires a Google API key from http://console.developers.google.com.' - ) + assert(config.google_api_key, + 'youtube.lua requires a Google API key from http://console.developers.google.com.' + ) - youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table - youtube.doc = config.cmd_pat .. [[youtube <query> + youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table + youtube.doc = config.cmd_pat .. [[youtube <query> Returns the top result from YouTube. Alias: ]] .. config.cmd_pat .. 'yt' end @@ -22,32 +22,32 @@ youtube.command = 'youtube <query>' function youtube:action(msg, config) - local input = utilities.input_from_msg(msg) - if not input then - utilities.send_reply(self, msg, youtube.doc, true) - return - end + local input = utilities.input_from_msg(msg) + if not input then + utilities.send_reply(self, msg, youtube.doc, true) + return + end - local url = 'https://www.googleapis.com/youtube/v3/search?key=' .. config.google_api_key .. '&type=video&part=snippet&maxResults=4&q=' .. URL.escape(input) + local url = 'https://www.googleapis.com/youtube/v3/search?key=' .. config.google_api_key .. '&type=video&part=snippet&maxResults=4&q=' .. URL.escape(input) - local jstr, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end + local jstr, res = HTTPS.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end - local jdat = JSON.decode(jstr) - if jdat.pageInfo.totalResults == 0 then - utilities.send_reply(self, msg, config.errors.results) - return - end + local jdat = JSON.decode(jstr) + if jdat.pageInfo.totalResults == 0 then + utilities.send_reply(self, msg, config.errors.results) + return + end - local vid_url = 'https://www.youtube.com/watch?v=' .. jdat.items[1].id.videoId - local vid_title = jdat.items[1].snippet.title - vid_title = vid_title:gsub('%(.+%)',''):gsub('%[.+%]','') - local output = '[' .. vid_title .. '](' .. vid_url .. ')' + local vid_url = 'https://www.youtube.com/watch?v=' .. jdat.items[1].id.videoId + local vid_title = jdat.items[1].snippet.title + vid_title = vid_title:gsub('%(.+%)',''):gsub('%[.+%]','') + local output = '[' .. vid_title .. '](' .. vid_url .. ')' - utilities.send_message(self, msg.chat.id, output, false, nil, true) + utilities.send_message(self, msg.chat.id, output, false, nil, true) end diff --git a/otouto/utilities.lua b/otouto/utilities.lua index 22f5204..86ba0a1 100644 --- a/otouto/utilities.lua +++ b/otouto/utilities.lua @@ -15,47 +15,47 @@ local bindings = require('otouto.bindings') -- Edit: To keep things working and allow for HTML messages, you can now pass a -- string for use_markdown and that will be sent as the parse mode. function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown) - local parse_mode - if type(use_markdown) == 'string' then - parse_mode = use_markdown - elseif use_markdown == true then - parse_mode = 'markdown' - end - return bindings.request(self, 'sendMessage', { - chat_id = chat_id, - text = text, - disable_web_page_preview = disable_web_page_preview, - reply_to_message_id = reply_to_message_id, - parse_mode = parse_mode - } ) + local parse_mode + if type(use_markdown) == 'string' then + parse_mode = use_markdown + elseif use_markdown == true then + parse_mode = 'markdown' + end + return bindings.request(self, 'sendMessage', { + chat_id = chat_id, + text = text, + disable_web_page_preview = disable_web_page_preview, + reply_to_message_id = reply_to_message_id, + parse_mode = parse_mode + } ) end function utilities:send_reply(old_msg, text, use_markdown) - return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown) + return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown) end -- get the indexed word in a string function utilities.get_word(s, i) - s = s or '' - i = i or 1 - local n = 0 - for w in s:gmatch('%g+') do - n = n + 1 - if n == i then return w end - end - return false + s = s or '' + i = i or 1 + local n = 0 + for w in s:gmatch('%g+') do + n = n + 1 + if n == i then return w end + end + return false end -- Returns the string after the first space. function utilities.input(s) - if not s:find(' ') then - return false - end - return s:sub(s:find(' ')+1) + if not s:find(' ') then + return false + end + return s:sub(s:find(' ')+1) end function utilities.input_from_msg(msg) - return utilities.input(msg.text) or (msg.reply_to_message and #msg.reply_to_message.text > 0 and msg.reply_to_message.text) or false + return utilities.input(msg.text) or (msg.reply_to_message and #msg.reply_to_message.text > 0 and msg.reply_to_message.text) or false end -- Calculates the length of the given string as UTF-8 characters @@ -72,180 +72,180 @@ end -- Trims whitespace from a string. function utilities.trim(str) - local s = str:gsub('^%s*(.-)%s*$', '%1') - return s + local s = str:gsub('^%s*(.-)%s*$', '%1') + return s end -- Loads a JSON file as a table. function utilities.load_data(filename) - local f = io.open(filename) - if f then - local s = f:read('*all') - f:close() - return JSON.decode(s) - else - return {} - end + local f = io.open(filename) + if f then + local s = f:read('*all') + f:close() + return JSON.decode(s) + else + return {} + end end -- Saves a table to a JSON file. function utilities.save_data(filename, data) - local s = JSON.encode(data) - local f = io.open(filename, 'w') - f:write(s) - f:close() + local s = JSON.encode(data) + local f = io.open(filename, 'w') + f:write(s) + f:close() end -- Gets coordinates for a location. Used by gMaps.lua, time.lua, weather.lua. function utilities.get_coords(input, config) - local url = 'http://maps.googleapis.com/maps/api/geocode/json?address=' .. URL.escape(input) + local url = 'http://maps.googleapis.com/maps/api/geocode/json?address=' .. URL.escape(input) - local jstr, res = HTTP.request(url) - if res ~= 200 then - return config.errors.connection - end + local jstr, res = HTTP.request(url) + if res ~= 200 then + return config.errors.connection + end - local jdat = JSON.decode(jstr) - if jdat.status == 'ZERO_RESULTS' then - return config.errors.results - end + local jdat = JSON.decode(jstr) + if jdat.status == 'ZERO_RESULTS' then + return config.errors.results + end - return { - lat = jdat.results[1].geometry.location.lat, - lon = jdat.results[1].geometry.location.lng - } + return { + lat = jdat.results[1].geometry.location.lat, + lon = jdat.results[1].geometry.location.lng + } end -- Get the number of values in a key/value table. function utilities.table_size(tab) - local i = 0 - for _,_ in pairs(tab) do - i = i + 1 - end - return i + local i = 0 + for _,_ in pairs(tab) do + i = i + 1 + end + return i end -- Just an easy way to get a user's full name. -- Alternatively, abuse it to concat two strings like I do. function utilities.build_name(first, last) - if last then - return first .. ' ' .. last - else - return first - end + if last then + return first .. ' ' .. last + else + return first + end end function utilities:resolve_username(input) - input = input:gsub('^@', '') - for _, user in pairs(self.database.users) do - if user.username and user.username:lower() == input:lower() then - local t = {} - for key, val in pairs(user) do - t[key] = val - end - return t - end - end + input = input:gsub('^@', '') + for _, user in pairs(self.database.users) do + if user.username and user.username:lower() == input:lower() then + local t = {} + for key, val in pairs(user) do + t[key] = val + end + return t + end + end end function utilities:handle_exception(err, message, config) - local output = string.format( - '\n[%s]\n%s: %s\n%s\n', - os.date('%F %T'), - self.info.username, - err or '', - message - ) - if config.log_chat then - output = '```' .. output .. '```' - utilities.send_message(self, config.log_chat, output, true, nil, true) - else - print(output) - end + local output = string.format( + '\n[%s]\n%s: %s\n%s\n', + os.date('%F %T'), + self.info.username, + err or '', + message + ) + if config.log_chat then + output = '```' .. output .. '```' + utilities.send_message(self, config.log_chat, output, true, nil, true) + else + print(output) + end end function utilities.download_file(url, filename) - if not filename then - filename = url:match('.+/(.-)$') or os.time() - filename = '/tmp/' .. filename - end - local body = {} - local doer = HTTP - local do_redir = true - if url:match('^https') then - doer = HTTPS - do_redir = false - end - local _, res = doer.request{ - url = url, - sink = ltn12.sink.table(body), - redirect = do_redir - } - if res ~= 200 then return false end - local file = io.open(filename, 'w+') - file:write(table.concat(body)) - file:close() - return filename + if not filename then + filename = url:match('.+/(.-)$') or os.time() + filename = '/tmp/' .. filename + end + local body = {} + local doer = HTTP + local do_redir = true + if url:match('^https') then + doer = HTTPS + do_redir = false + end + local _, res = doer.request{ + url = url, + sink = ltn12.sink.table(body), + redirect = do_redir + } + if res ~= 200 then return false end + local file = io.open(filename, 'w+') + file:write(table.concat(body)) + file:close() + return filename end function utilities.md_escape(text) - return text:gsub('_', '\\_') - :gsub('%[', '\\['):gsub('%]', '\\]') - :gsub('%*', '\\*'):gsub('`', '\\`') + return text:gsub('_', '\\_') + :gsub('%[', '\\['):gsub('%]', '\\]') + :gsub('%*', '\\*'):gsub('`', '\\`') end function utilities.html_escape(text) - return text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>') + return text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>') end utilities.triggers_meta = {} utilities.triggers_meta.__index = utilities.triggers_meta function utilities.triggers_meta:t(pattern, has_args) - local username = self.username:lower() - table.insert(self.table, '^'..self.cmd_pat..pattern..'$') - table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'$') - if has_args then - table.insert(self.table, '^'..self.cmd_pat..pattern..'%s+[^%s]*') - table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'%s+[^%s]*') - end - return self + local username = self.username:lower() + table.insert(self.table, '^'..self.cmd_pat..pattern..'$') + table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'$') + if has_args then + table.insert(self.table, '^'..self.cmd_pat..pattern..'%s+[^%s]*') + table.insert(self.table, '^'..self.cmd_pat..pattern..'@'..username..'%s+[^%s]*') + end + return self end function utilities.triggers(username, cmd_pat, trigger_table) - local self = setmetatable({}, utilities.triggers_meta) - self.username = username - self.cmd_pat = cmd_pat - self.table = trigger_table or {} - return self + local self = setmetatable({}, utilities.triggers_meta) + self.username = username + self.cmd_pat = cmd_pat + self.table = trigger_table or {} + return self end function utilities.with_http_timeout(timeout, fun) - local original = HTTP.TIMEOUT - HTTP.TIMEOUT = timeout - fun() - HTTP.TIMEOUT = original + local original = HTTP.TIMEOUT + HTTP.TIMEOUT = timeout + fun() + HTTP.TIMEOUT = original end function utilities.pretty_float(x) - if x % 1 == 0 then - return tostring(math.floor(x)) - else - return tostring(x) - end + if x % 1 == 0 then + return tostring(math.floor(x)) + else + return tostring(x) + end end -- This table will store unsavory characters that are not properly displayed, -- or are just not fun to type. utilities.char = { - zwnj = '‌', - arabic = '[\216-\219][\128-\191]', - rtl_override = '‮', - rtl_mark = '‏', - em_dash = '—', - utf_8 = '[%z\1-\127\194-\244][\128-\191]', + zwnj = '‌', + arabic = '[\216-\219][\128-\191]', + rtl_override = '‮', + rtl_mark = '‏', + em_dash = '—', + utf_8 = '[%z\1-\127\194-\244][\128-\191]', } utilities.set_meta = {} @@ -283,7 +283,7 @@ end -- More to be added. utilities.style = {} utilities.style.enquote = function(title, body) - return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"' + return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"' end return utilities diff --git a/tg-launch.sh b/tg-launch.sh index 570e0ba..62d2513 100755 --- a/tg-launch.sh +++ b/tg-launch.sh @@ -4,8 +4,8 @@ # config.lua), delete state file after stop, wait five seconds, and restart. while true; do - tg/bin/telegram-cli -P 4567 -E - [ -f ~/.telegram-cli/state ] && rm ~/.telegram-cli/state - echo 'tg has stopped. ^C to exit.' - sleep 5s + tg/bin/telegram-cli -P 4567 -E + [ -f ~/.telegram-cli/state ] && rm ~/.telegram-cli/state + echo 'tg has stopped. ^C to exit.' + sleep 5s done From 460c06ae6c25e48c4efa0187f0a8d46f62cc67a8 Mon Sep 17 00:00:00 2001 From: topkecleon <andwag@outlook.com> Date: Sat, 13 Aug 2016 23:36:59 -0400 Subject: [PATCH 4/5] readme fix --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 337b3e0..bc8e33a 100644 --- a/README.md +++ b/README.md @@ -244,10 +244,10 @@ Additionally, antiflood can be configured to automatically ban a user after he h | `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. | | `patterns.lua` | /s/‹from›/‹to›/ | Search-and-replace using Lua patterns. | | `me.lua` | /me | Returns user-specific data stored by the bot. | -| `remind.lua` | /remind <duration> <message> | Reminds a user of something after a duration of minutes. | -| `channel.lua` | /ch <channel> \n <message> | Sends a markdown-enabled message to a channel. | -| `isup.lua` | /isup <url> | Returns the status of a website. | -| `starwars-crawl.lua` | /sw <title ¦ number> | Returns the opening crawl from the specified Star Wars film. | /sw | +| `remind.lua` | /remind ‹duration› ‹message› | Reminds a user of something after a duration of minutes. | +| `channel.lua` | /ch ‹channel› \n ‹message› | Sends a markdown-enabled message to a channel. | +| `isup.lua` | /isup ‹url› | Returns the status of a website. | +| `starwars-crawl.lua` | /sw ‹title ¦ number› | Returns the opening crawl from the specified Star Wars film. | /sw | | `chuckfact.lua` | /chuck | Returns a fact about Chuck Norris. | /cn | | `catfact.lua` | /catfact | Returns a fact about cats. | From 0c1ac4a119a838032b32d35b3f2f51d06c66b7ef Mon Sep 17 00:00:00 2001 From: topkecleon <andwag@outlook.com> Date: Sat, 13 Aug 2016 23:51:58 -0400 Subject: [PATCH 5/5] more readme stuff --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index bc8e33a..ecdc764 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,6 @@ While tg is running, you may start/reload otouto with `administration.lua` enabl | /gadd | Adds a group to the administrative system. | 5 | N | | /grem | Removes a group from the administrative system. | 5 | Y | | /glist | Returns a list of all administrated groups and their governors. | 5 | N | -| /broadcast | Broadcasts a message to all administrated groups. | 5 | N | Internal commands can only be run within an administrated group.