otouto 3.13

good lord
This commit is contained in:
topkecleon 2016-08-13 22:26:44 -04:00
parent e19d2e1e84
commit 43a6b53c90
52 changed files with 1315 additions and 1617 deletions

232
README.md
View File

@ -1,7 +1,7 @@
# otouto # otouto
The plugin-wielding, multipurpose Telegram bot. 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. 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 | | For Users | For Coders |
|:----------------------------------------------|:------------------------------| |:----------------------------------------------|:------------------------------|
| [Setup](#setup) | [Plugins](#plugins) | | [Setup](#setup) | [Plugins](#plugins) |
| [Control plugins](#control-plugins) | [Bindings](#bindings) | | [Configuration](#configuration) | [Bindings](#bindings) |
| [Group administration](#group-administration) | [Database](#database) | | [Control plugins](#control-plugins) | [Database](#database) |
| [List of plugins](#list-of-plugins) | [Output style](#output-style) | | [Group administration](#group-administration) | [Output style](#output-style) |
| | [Contributors](#contributors) | | [List of plugins](#list-of-plugins) | [Contributors](#contributors) |
* * *
## Setup ## 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. 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. - `bot_api_key` as your bot authorization token from the BotFather.
- `admin` as your Telegram ID. - `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. 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`. 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`) ## Configuration
- `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`) 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.
- `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`)
* * * 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 ## 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. 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. | | `shell.lua` | /run | Executes shell commands on the host system. |
| `luarun.lua` | /lua | Executes Lua commands in the bot's environment. | | `luarun.lua` | /lua | Executes Lua commands in the bot's environment. |
* * *
## Group Administration ## 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. 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. 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. 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 ### Description of Privileges
| # | Title | Description | Scope | | | Title | Description | Scope |
|:-:|:--------------|:------------------------------------------------------------------|:-------| |:-:|:--------------|:------------------------------------------------------------------|:-------|
| 0 | Banned | Cannot enter the group(s). | Either | | 0 | Banned | Cannot enter the group(s). | Either |
| 1 | User | Default rank. | Local | | 1 | User | Default rank. | Local |
@ -121,7 +175,7 @@ Obviously, each greater rank inherits the privileges of the lower, positive rank
### Flags ### Flags
| # | Name | Description | | | Name | Description |
|:-:|:------------|:---------------------------------------------------------------------------------| |:-:|:------------|:---------------------------------------------------------------------------------|
| 1 | unlisted | Removes a group from the /groups listing. | | 1 | unlisted | Removes a group from the /groups listing. |
| 2 | antisquig | Automatically removes users for posting Arabic script or RTL characters. | | 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. 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 ## List of plugins
| Plugin | Command | Function | Aliases | | Plugin | Command | Function | Aliases |
|:----------------------|:------------------------------|:--------------------------------------------------------|:--------| |:----------------------|:------------------------------|:----------------------------------------------------------|:--------|
| `help.lua` | /help [command] | Returns a list of commands or command-specific help. | /h | | `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. | | `about.lua` | /about | Returns the about text as configured in config.lua. |
| `ping.lua` | /ping | The simplest plugin ever! | | `ping.lua` | /ping | The simplest plugin ever! |
| `echo.lua` | /echo text | Repeats a string of text. | | `echo.lua` | /echo text | Repeats a string of text. |
| `bing.lua` | /bing query | Returns Bing web results. | /g | | `bing.lua` | /bing query | Returns Bing web results. | /g |
| `gImages.lua` | /images query | Returns a Google image result. | /i | | `gImages.lua` | /images query | Returns a Google image result. | /i |
| `gMaps.lua` | /location query | Returns location data from Google Maps. | /loc | | `gMaps.lua` | /location query | Returns location data from Google Maps. | /loc |
| `youtube.lua` | /youtube query | Returns the top video result from YouTube. | /yt | | `youtube.lua` | /youtube query | Returns the top video result from YouTube. | /yt |
| `wikipedia.lua` | /wikipedia query | Returns the summary of a Wikipedia article. | /w | | `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` | /np [username] | Returns the song you are currently listening to. |
| `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. | | `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. |
| `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn | | `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn |
| `imdb.lua` | /imdb query | Returns film information from IMDb. | | `imdb.lua` | /imdb query | Returns film information from IMDb. |
| `hearthstone.lua` | /hearthstone query | Returns data for Hearthstone cards matching the query. | /hs | | `hearthstone.lua` | /hearthstone query | Returns data for Hearthstone cards matching the query. | /hs |
| `calc.lua` | /calc expression | Returns conversions and solutions to math expressions. | | `calc.lua` | /calc expression | Returns conversions and solutions to math expressions. |
| `bible.lua` | /bible reference | Returns a Bible verse. | /b | | `bible.lua` | /bible reference | Returns a Bible verse. | /b |
| `urbandictionary.lua` | /urban query | Returns the top definition from Urban Dictionary. | /ud | | `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. | | `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. | | `weather.lua` | /weather query | Returns current weather conditions for a given location. |
| `nick.lua` | /nick nickname | Set your nickname. /nick - will delete it. | | `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 | | `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. | | `eightball.lua` | /8ball | Returns an answer from a magic 8-ball. |
| `dice.lua` | /roll nDr | Returns RNG dice rolls. Uses D&D notation. | | `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 | | `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. | | `xkcd.lua` | /xkcd [query] | Returns an xkcd strip and its alt text. |
| `slap.lua` | /slap target | Gives someone a slap (or worse). | | `slap.lua` | /slap target | Gives someone a slap (or worse). |
| `commit.lua` | /commit | Returns a commit message from whatthecommit.com. | | `commit.lua` | /commit | Returns a commit message from whatthecommit.com. |
| `fortune.lua` | /fortune | Returns a UNIX fortune. | | `fortune.lua` | /fortune | Returns a UNIX fortune. |
| `pun.lua` | /pun | Returns a pun. | | `pun.lua` | /pun | Returns a pun. |
| `pokedex.lua` | /pokedex query | Returns a Pokedex entry. | /dex | | `pokedex.lua` | /pokedex query | Returns a Pokedex entry. | /dex |
| `currency.lua` | /cash [amount] cur to cur | Converts one currency to another. | | `currency.lua` | /cash [amount] cur to cur | Converts one currency to another. |
| `cats.lua` | /cat | Returns a cat picture. | | `cats.lua` | /cat | Returns a cat picture. |
| `reactions.lua` | /reactions | Returns a list of emoticons which can be posted by the bot. | | `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. | | `apod.lua` | /apod [date] | Returns the NASA Astronomy Picture of the Day. |
| `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. | | `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. |
| `patterns.lua` | /s/from/to/ | Search-and-replace using Lua patterns. | | `patterns.lua` | /s/from/to/ | Search-and-replace using Lua patterns. |
| `me.lua` | /me | Returns user-specific data stored by the bot. | | `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. | | `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. | | `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. |
## Plugins ## Plugins
otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot). 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. There are five standard plugin components.
| Component | Description | | Component | Description |
|:-----------|:-----------------------------------------------------| |:------------|:---------------------------------------------------------------|
| `action` | Main function. Expects `msg` table as an argument. | | `action` | Main function. Expects `msg` table as an argument. |
| `triggers` | Table of triggers for the plugin. Uses Lua patterns. | | `triggers` | Table of triggers for the plugin. Uses Lua patterns. |
| `init` | Optional function run when the plugin is loaded. | | `init` | Optional function run when the plugin is loaded. |
| `cron` | Optional function to be called every minute. | | `cron` | Optional function to be called every minute. |
| `command` | Basic command and syntax. Listed in the help text. | | `command` | Basic command and syntax. Listed in the help text. |
| `doc` | Usage for the plugin. Returned by "/help $command". | | `doc` | Usage for the plugin. Returned by "/help $command". |
| `error` | Plugin-specific error message; false for no message. | | `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`. 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. 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. 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. Several functions used in multiple plugins are defined in utilities.lua. Refer to that file for usage and documentation.
* * *
## Bindings ## 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/`.) 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". 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 ## 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. 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`. 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 ## Output style
otouto plugins should maintain a consistent visual style in their output. This provides a recognizable and comfortable user experience. 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)** > **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)**):** > **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 ### 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. 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 ## 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). 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).

View File

@ -1,9 +1,10 @@
-- For details on configuration values, see README.md#configuration.
return { return {
-- Your authorization token from the botfather. -- Your authorization token from the botfather.
bot_api_key = '', bot_api_key = nil,
-- Your Telegram ID. -- Your Telegram ID.
admin = 00000000, admin = nil,
-- Two-letter language code. -- Two-letter language code.
lang = 'en', lang = 'en',
-- The channel, group, or user to send error reports to. -- 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. -- The port used to communicate with tg for administration.lua.
-- If you change this, make sure you also modify launch-tg.sh. -- If you change this, make sure you also modify launch-tg.sh.
cli_port = 4567, 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 = [[ about_text = [[
I am otouto, the plugin-wielding, multipurpose Telegram bot. I am otouto, the plugin-wielding, multipurpose Telegram bot.
Send /help to get started. 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. errors = { -- Generic error messages.
generic = 'An unexpected error occurred.', generic = 'An unexpected error occurred.',
connection = 'Connection error.', connection = 'Connection error.',
results = 'No results found.', results = 'No results found.',
argument = 'Invalid argument.', argument = 'Invalid argument.',
syntax = 'Invalid syntax.', syntax = 'Invalid syntax.'
chatter_connection = 'I don\'t feel like talking right now.', },
chatter_response = 'I don\'t know what to say to that.'
-- 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. plugins = { -- To enable a plugin, add its name to the list.
'control',
'blacklist',
'about', 'about',
'ping', 'blacklist',
'whoami', 'calc',
'nick', 'cats',
'commit',
'control',
'currency',
'dice',
'echo', 'echo',
'eightball',
'gMaps', 'gMaps',
'wikipedia',
'hackernews', 'hackernews',
'imdb', 'imdb',
'calc', 'nick',
'urbandictionary', 'ping',
'time',
'eightball',
'dice',
'reddit',
'xkcd',
'slap',
'commit',
'pun', 'pun',
'currency', 'reddit',
'cats',
'shout', 'shout',
'slap',
'time',
'urbandictionary',
'whoami',
'wikipedia',
'xkcd',
-- Put new plugins above this line. -- Put new plugins above this line.
'help', 'help',
'greetings' 'greetings'

View File

@ -41,7 +41,7 @@ function bindings:request(method, parameters, file)
end end
local response = {} local response = {}
local body, boundary = MP_ENCODE(parameters) local body, boundary = MP_ENCODE(parameters)
local success = HTTPS.request{ local success, code = HTTPS.request{
url = self.BASE_URL .. method, url = self.BASE_URL .. method,
method = 'POST', method = 'POST',
headers = { headers = {
@ -52,8 +52,8 @@ function bindings:request(method, parameters, file)
sink = ltn12.sink.table(response) sink = ltn12.sink.table(response)
} }
local data = table.concat(response) local data = table.concat(response)
if not success then if not success or success == 1 then
print(method .. ': Connection error.') print(method .. ': Connection error. [' .. code .. ']')
return false, false return false, false
else else
local result = JSON.decode(data) local result = JSON.decode(data)

View File

@ -1,18 +1,17 @@
local bot = {} local bot = {}
local bindings -- Bot API bindings.
local utilities -- Miscellaneous and shared plugins.
-- Requires are moved to init to allow for reloads. bot.version = '3.13'
local bindings -- Load Telegram bindings.
local utilities -- Load miscellaneous and cross-plugin functions.
bot.version = '3.12' -- Function to be run on start and reload.
function bot:init(config)
function bot:init(config) -- The function run when the bot is started or reloaded.
bindings = require('otouto.bindings') bindings = require('otouto.bindings')
utilities = require('otouto.utilities') utilities = require('otouto.utilities')
assert( assert(
config.bot_api_key ~= '', config.bot_api_key,
'You did not set your bot token in the config!' 'You did not set your bot token in the config!'
) )
self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' 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 self.info = self.info.result
-- Load the "database"! ;) -- Load the "database"! ;)
self.database_name = config.database_name or self.info.username .. '.db'
if not self.database then if not self.database then
self.database = utilities.load_data(self.info.username..'.db') self.database = utilities.load_data(self.database_name)
end 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). -- Table to cache user info (usernames, IDs, etc).
self.database.users = self.database.users or {} self.database.users = self.database.users or {}
-- Table to store userdata (nicknames, lastfm usernames, etc). -- Table to store userdata (nicknames, lastfm usernames, etc).
self.database.userdata = self.database.userdata or {} 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. -- Save the bot's version in the database to make migration simpler.
self.database.version = bot.version self.database.version = bot.version
-- Add updated bot info to the user info cache. -- Add updated bot info to the user info cache.
self.database.users[tostring(self.info.id)] = self.info self.database.users[tostring(self.info.id)] = self.info
self.plugins = {} -- Load plugins. -- All plugins go into self.plugins. Plugins which accept forwarded messages
for _,v in ipairs(config.plugins) do -- and messages from blacklisted users also go into self.panoptic_plugins.
local p = require('otouto.plugins.'..v) self.plugins = {}
table.insert(self.plugins, p) self.panoptic_plugins = {}
if p.init then p.init(self, config) end local t = {} -- Petty pseudo-optimization.
if p.doc then p.doc = '```\n'..p.doc..'\n```' end 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 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..')')
self.last_update = self.last_update or 0 -- Set loop variables: Update offset, -- Set loop variables.
self.last_cron = self.last_cron or os.date('%M') -- the time of the last cron job, self.last_update = self.last_update or 0 -- Update offset.
self.last_database_save = self.last_database_save or os.date('%H') -- the time of the last database save, self.last_cron = self.last_cron or os.date('%M') -- Last cron job.
self.is_started = true -- and whether or not the bot should be running. self.last_database_save = self.last_database_save or os.date('%H') -- Last db save.
self.is_started = true
end 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. -- 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 if msg.reply_to_message then
self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from
elseif msg.forward_from then 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 self.database.users[tostring(msg.forward_from.id)] = msg.forward_from
elseif msg.new_chat_member then elseif msg.new_chat_member then
self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member 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 self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member
end 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 = msg.text or msg.caption or ''
msg.text_lower = msg.text:lower() msg.text_lower = msg.text:lower()
if msg.reply_to_message then if msg.reply_to_message then
msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or '' msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or ''
end 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() msg.text_lower = msg.text:lower()
end end
for _, plugin in ipairs(self.plugins) do -- If the message is forwarded or comes from a blacklisted yser,
for _, trigger in ipairs(plugin.triggers or {}) do
-- Do the thing.
for _, plugin in ipairs(plugint) do
for _, trigger in ipairs(plugin.triggers) do
if string.match(msg.text_lower, trigger) then if string.match(msg.text_lower, trigger) then
local success, result = pcall(function() local success, result = pcall(function()
return plugin.action(self, msg, config) 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) utilities.send_reply(self, msg, config.errors.generic)
end end
utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config)
msg = nil
return return
end -- Continue if the return value is true.
-- 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.
elseif result ~= true then elseif result ~= true then
msg = nil
return return
end end
end end
end end
end end
msg = nil
end end
-- main
function bot:run(config) function bot:run(config)
bot.init(self, config) -- Actually start the script. bot.init(self, config)
while self.is_started do
while self.is_started do -- Start a loop while the bot should be running. -- Update loop.
local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } )
local res = bindings.getUpdates(self, { timeout=20, offset = self.last_update+1 } )
if res then 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 self.last_update = v.update_id
if v.message then if v.message then
bot.on_msg_receive(self, v.message, config) bot.on_msg_receive(self, v.message, config)
@ -132,7 +170,8 @@ function bot:run(config)
print('Connection error while fetching updates.') print('Connection error while fetching updates.')
end 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') self.last_cron = os.date('%M')
for i,v in ipairs(self.plugins) do for i,v in ipairs(self.plugins) do
if v.cron then -- Call each plugin's cron function, if it has one. if v.cron then -- Call each plugin's cron function, if it has one.
@ -144,15 +183,14 @@ function bot:run(config)
end end
end end
-- Save the "database" every hour.
if self.last_database_save ~= os.date('%H') then 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') self.last_database_save = os.date('%H')
utilities.save_data(self.database_name, self.database)
end end
end end
-- Save the database before exiting. -- Save the database before exiting.
utilities.save_data(self.info.username..'.db', self.database) utilities.save_data(self.database_name, self.database)
print('Halted.') print('Halted.')
end end

View File

@ -1,36 +1,19 @@
local about = {}
local bot = require('otouto.bot') local bot = require('otouto.bot')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local about = {}
about.command = 'about' about.command = 'about'
about.doc = 'Returns information about the bot.' 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) function about:action(msg, config)
utilities.send_message(self, msg.chat.id, about.text, true, nil, true)
-- 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
end end
return about return about

View File

@ -9,8 +9,6 @@
It requires tg (http://github.com/vysheng/tg) with supergroup support. It requires tg (http://github.com/vysheng/tg) with supergroup support.
For more documentation, read the the manual (otou.to/rtfm). For more documentation, read the the manual (otou.to/rtfm).
Remember to load this before blacklist.lua.
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 1.11 - Removed /kickme and /broadcast. Users should leave manually, and
@ -20,6 +18,8 @@
necessary. 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.
]] ]]
local JSON = require('dkjson') local JSON = require('dkjson')
@ -36,11 +36,12 @@ function administration:init(config)
admins = {}, admins = {},
groups = {}, groups = {},
activity = {}, activity = {},
autokick_timer = os.date('%d') autokick_timer = os.date('%d'),
globalbans = {}
} }
end end
self.admin_temp = { administration.temp = {
help = {}, help = {},
flood = {} flood = {}
} }
@ -49,13 +50,25 @@ function administration:init(config)
administration.flags = administration.init_flags(config.cmd_pat) administration.flags = administration.init_flags(config.cmd_pat)
administration.init_command(self, config) 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.doc = 'Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.'
administration.command = 'groups [query]' administration.command = 'groups [query]'
-- In the worst case, don't send errors in reply to random messages. -- In the worst case, don't send errors in reply to random messages.
administration.error = false administration.error = false
-- Accept forwarded messages and messages from blacklisted users.
administration.panoptic = true
end end
function administration.init_flags(cmd_pat) return { function administration.init_flags(cmd_pat) return {
@ -106,18 +119,6 @@ function administration.init_flags(cmd_pat) return {
} }
} end } end
administration.antiflood = {
text = 5,
voice = 5,
audio = 5,
contact = 5,
photo = 10,
video = 10,
location = 10,
document = 10,
sticker = 20
}
administration.ranks = { administration.ranks = {
[0] = 'Banned', [0] = 'Banned',
[1] = 'Users', [1] = 'Users',
@ -159,8 +160,8 @@ function administration:get_rank(user_id_str, chat_id_str, config)
end end
end end
-- Return 0 if the user_id_str is blacklisted (and antihammer is not enabled). -- Return 0 if the user_id_str is globally banned (and antihammer is not enabled).
if self.database.blacklist[user_id_str] then if self.database.administration.globalbans[user_id_str] then
return 0 return 0
end end
@ -172,8 +173,9 @@ end
-- Returns an array of "user" tables. -- Returns an array of "user" tables.
function administration:get_targets(msg, config) function administration:get_targets(msg, config)
if msg.reply_to_message then 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 = {} local target = {}
for k,v in pairs(msg.reply_to_message.from) do for k,v in pairs(d) do
target[k] = v target[k] = v
end end
target.name = utilities.build_name(target.first_name, target.last_name) 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) local input = utilities.input(msg.text)
if input then if input then
local t = {} local t = {}
for _, user in ipairs(utilities.index(input)) do for user in input:gmatch('%g+') do
if self.database.users[user] then if self.database.users[user] then
local target = {} local target = {}
for k,v in pairs(self.database.users[user]) do 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) target.rank = administration.get_rank(self, target.id, msg.chat.id, config)
table.insert(t, target) table.insert(t, target)
elseif tonumber(user) then elseif tonumber(user) then
local id = math.abs(tonumber(user))
local target = { local target = {
id = tonumber(user), id = id,
id_str = user, id_str = tostring(id),
name = 'Unknown ('..user..')', name = 'Unknown ('..id..')',
rank = administration.get_rank(self, user, msg.chat.id, config) rank = administration.get_rank(self, user, msg.chat.id, config)
} }
table.insert(t, target) table.insert(t, target)
@ -227,7 +230,7 @@ function administration:mod_format(id)
id = tostring(id) id = tostring(id)
local user = self.database.users[id] or { first_name = 'Unknown' } local user = self.database.users[id] or { first_name = 'Unknown' }
local name = utilities.build_name(user.first_name, user.last_name) 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' local output = '' .. name .. ' `[' .. id .. ']`\n'
return output return output
end end
@ -356,36 +359,36 @@ function administration.init_command(self_, config_)
if not group.antiflood then if not group.antiflood then
group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) group.antiflood = JSON.decode(JSON.encode(administration.antiflood))
end end
if not self.admin_temp.flood[chat_id_str] then if not administration.temp.flood[chat_id_str] then
self.admin_temp.flood[chat_id_str] = {} administration.temp.flood[chat_id_str] = {}
end end
if not self.admin_temp.flood[chat_id_str][from_id_str] then if not administration.temp.flood[chat_id_str][from_id_str] then
self.admin_temp.flood[chat_id_str][from_id_str] = 0 administration.temp.flood[chat_id_str][from_id_str] = 0
end end
if msg.sticker then -- Thanks Brazil for discarding switches. 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 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 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 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 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 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 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 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 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 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.do_kick = true
user.reason = 'antiflood' user.reason = 'antiflood'
user.output = administration.flags[5].kicked:gsub('GROUPNAME', msg.chat.title) 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
end end
@ -586,7 +589,7 @@ function administration.init_command(self_, config_)
else else
local output = '*Commands for ' .. administration.ranks[rank] .. ':*\n' local output = '*Commands for ' .. administration.ranks[rank] .. ':*\n'
for i = 1, rank do 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' output = output .. '' .. config.cmd_pat .. val .. '\n'
end end
end end
@ -685,7 +688,7 @@ function administration.init_command(self_, config_)
}, },
{ -- /motd { -- /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', command = 'motd',
privilege = 1, 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, triggers = utilities.triggers(self_.info.username, config_.cmd_pat):t('setmotd', true):t('setqotd', true).table,
command = 'setmotd <motd>', command = 'setmotd <motd>',
privilege = 2, privilege = config_.administration.moderator_setmotd and 2 or 3,
interior = true, interior = true,
doc = 'Sets the group\'s message of the day. Markdown is supported. Pass "--" to delete the message.', 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 output = ''
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if input then if input then
local index = utilities.index(input) for i in input:gmatch('%g+') do
for _, i in ipairs(index) do
local n = tonumber(i) local n = tonumber(i)
if n and administration.flags[n] then if n and administration.flags[n] then
if group.flags[n] == true then if group.flags[n] == true then
@ -1069,7 +1071,10 @@ function administration.init_command(self_, config_)
group.bans[target.id_str] = nil group.bans[target.id_str] = nil
end end
if group.grouptype == 'supergroup' then 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 end
end end
@ -1138,7 +1143,10 @@ function administration.init_command(self_, config_)
utilities.send_reply(self, msg, target.name .. ' is the new governor.') utilities.send_reply(self, msg, target.name .. ' is the new governor.')
end end
if group.grouptype == 'supergroup' then 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) administration.update_desc(self, msg.chat.id, config)
end end
end end
@ -1195,7 +1203,7 @@ function administration.init_command(self_, config_)
for _, target in ipairs(targets) do for _, target in ipairs(targets) do
if target.err then if target.err then
output = output .. target.err .. '\n' 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' output = output .. target.name .. ' is already globally banned.\n'
elseif target.rank >= administration.get_rank(self, msg.from.id, msg.chat.id, config) then 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' 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 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 if group and group.flags[6] == true then
group.mods[target.id_str] = nil group.mods[target.id_str] = nil
group.bans[target.id_str] = true group.bans[target.id_str] = true
@ -1243,10 +1251,10 @@ function administration.init_command(self_, config_)
for _, target in ipairs(targets) do for _, target in ipairs(targets) do
if target.err then if target.err then
output = output .. target.err .. '\n' 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' output = output .. target.name .. ' is not globally banned.\n'
else 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' output = output .. target.name .. ' has been globally unbanned.\n'
end end
end end
@ -1333,7 +1341,7 @@ function administration.init_command(self_, config_)
action = function(self, msg, group, config) action = function(self, msg, group, config)
if msg.chat.id == msg.from.id then 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 elseif group then
utilities.send_reply(self, msg, 'I am already administrating this group.') utilities.send_reply(self, msg, 'I am already administrating this group.')
else else
@ -1344,8 +1352,7 @@ function administration.init_command(self_, config_)
end end
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if input then if input then
local index = utilities.index(input) for i in input:gmatch('%g+') do
for _, i in ipairs(index) do
local n = tonumber(i) local n = tonumber(i)
if n and administration.flags[n] and flags[n] ~= true then if n and administration.flags[n] and flags[n] ~= true then
flags[n] = true flags[n] = true
@ -1442,11 +1449,11 @@ function administration.init_command(self_, config_)
-- Generate help messages and ahelp keywords. -- Generate help messages and ahelp keywords.
self_.database.administration.help = {} self_.database.administration.help = {}
for i,_ in ipairs(administration.ranks) do for i,_ in ipairs(administration.ranks) do
self_.admin_temp.help[i] = {} administration.temp.help[i] = {}
end end
for _,v in ipairs(administration.commands) do for _,v in ipairs(administration.commands) do
if v.command then 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 if v.doc then
v.keyword = utilities.get_word(v.command, 1) v.keyword = utilities.get_word(v.command, 1)
end end
@ -1475,7 +1482,7 @@ function administration:action(msg, config)
end end
function administration:cron() function administration:cron()
self.admin_temp.flood = {} administration.temp.flood = {}
if os.date('%d') ~= self.database.administration.autokick_timer then if os.date('%d') ~= self.database.administration.autokick_timer then
self.database.administration.autokick_timer = os.date('%d') self.database.administration.autokick_timer = os.date('%d')
for _,v in pairs(self.database.administration.groups) do for _,v in pairs(self.database.administration.groups) do

View File

@ -10,79 +10,47 @@ local utilities = require('otouto.utilities')
apod.command = 'apod [date]' apod.command = 'apod [date]'
function apod:init(config) function apod:init(config)
apod.triggers = utilities.triggers(self.info.username, config.cmd_pat) apod.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('apod', true).table
:t('apod', true):t('apodhd', true):t('apodtext', true).table apod.doc = [[
apod.doc = config.cmd_pat .. [[apod [query] /apod [YYYY-MM-DD]
Returns the Astronomy Picture of the Day. 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. Source: nasa.gov
Examples: ]]
]] .. config.cmd_pat .. [[apodhd [query] apod.doc = apod.doc:gsub('/', config.cmd_pat)
Returns the image in HD, if available. apod.base_url = 'https://api.nasa.gov/planetary/apod?api_key=' .. (config.nasa_api_key or 'DEMO_KEY')
]] .. config.cmd_pat .. [[apodtext [query]
Returns the explanation of the APOD.
Source: nasa.gov]]
end end
function apod:action(msg, config) 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 input = utilities.input(msg.text)
local date = '*' local url = apod.base_url
local disable_page_preview = false local date = os.date('%F')
local url = 'https://api.nasa.gov/planetary/apod?api_key=' .. config.nasa_api_key
if input then if input then
if input:match('(%d+)%-(%d+)%-(%d+)$') then if input:match('^(%d+)%-(%d+)%-(%d+)$') then
url = url .. '&date=' .. URL.escape(input) url = url .. '&date=' .. URL.escape(input)
date = date .. input date = input
else
utilities.send_message(self, msg.chat.id, apod.doc, true, msg.message_id, true)
return
end end
else
date = date .. os.date("%F")
end end
date = date .. '*\n' local jstr, code = HTTPS.request(url)
if code ~= 200 then
local jstr, res = HTTPS.request(url)
if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local data = JSON.decode(jstr)
if data.error then
if jdat.error then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local img_url = jdat.url local output = string.format(
'<b>%s (</b><a href="%s">%s</a><b>)</b>\n%s',
if string.match(msg.text, '^'..config.cmd_pat..'apodhd*') then utilities.html_escape(data.title),
img_url = jdat.hdurl or jdat.url utilities.html_escape(data.hdurl or data.url),
end date,
utilities.html_escape(data.explanation)
local output = date .. '[' .. jdat.title .. '](' .. img_url .. ')' )
utilities.send_message(self, msg.chat.id, output, false, nil, 'html')
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)
end end
return apod return apod

View File

@ -5,11 +5,9 @@ local URL = require('socket.url')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function bible:init(config) function bible:init(config)
if not config.biblia_api_key then assert(config.biblia_api_key,
print('Missing config value: biblia_api_key.') 'bible.lua requires a Biblia API key from http://api.biblia.com.'
print('bible.lua will not be enabled.') )
return
end
bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table 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.doc = config.cmd_pat .. [[bible <reference>
@ -21,9 +19,9 @@ bible.command = 'bible <reference>'
function bible:action(msg, config) function bible:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then 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 return
end end

View File

@ -1,5 +1,4 @@
-- Credit to Juan (tg:JuanPotato; gh:JuanPotato) for this plugin. -- Credit to Juan (tg:JuanPotato; gh:JuanPotato) for this plugin.
-- Or rather, the seven lines that actually mean anything.
local bing = {} 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' bing.search_url = 'https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=\'%s\'&$format=json'
function bing:init(config) function bing:init(config)
if not config.bing_api_key then assert(config.bing_api_key,
print('Missing config value: bing_api_key.') 'bing.lua requires a Bing API key from http://datamarket.azure.com/dataset/bing/search.'
print('bing.lua will not be enabled.') )
return
end 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.triggers = utilities.triggers(self.info.username, config.cmd_pat)
bing.doc = config.cmd_pat .. [[bing <query> :t('bing', true):t('g', true):t('google', true).table
Returns the top web search results from Bing. bing.doc = [[
Aliases: ]] .. config.cmd_pat .. 'g, ' .. config.cmd_pat .. 'google' /bing <query>
Returns the top web results from Bing.
Aliases: /g, /google
]]
bing.doc = bing.doc:gsub('/', config.cmd_pat)
end end
function bing:action(msg, config) function bing:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text ~= '' then utilities.send_reply(self, msg, bing.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_reply(self, msg, bing.doc, true)
return
end
end end
local url = bing.search_url:format(URL.escape(input)) local url = bing.search_url:format(URL.escape(input))
local resbody = {} local resbody = {}
local _,b,_ = https.request{ local _, code = https.request{
url = url, url = url,
headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) }, headers = bing.headers,
sink = ltn12.sink.table(resbody), 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) utilities.send_reply(self, msg, config.errors.results)
return return
end 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 = {} local reslist = {}
for i = 1, limit do for i = 1, limit do
local result = dat.d.results[i] table.insert(reslist, string.format(
local s = '• [' .. result.Title:gsub('%]', '\\]') .. '](' .. result.Url:gsub('%)', '\\)') .. ')' '• <a href="%s">%s</a>',
table.insert(reslist, s) utilities.html_escape(data.d.results[i].Url),
utilities.html_escape(data.d.results[i].Title)
))
end end
local output = '*Search results for* _' .. utilities.md_escape(input) .. '_ *:*\n' .. table.concat(reslist, '\n') local output = string.format(
utilities.send_message(self, msg.chat.id, output, true, nil, true) '<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 end
return bing return bing

View File

@ -1,39 +1,15 @@
-- This plugin will allow the admin to blacklist users who will be unable to local utilities = require('otouto.utilities')
-- use the bot. This plugin should be at the top of your plugin list in config.
local blacklist = {} local blacklist = {}
local utilities = require('otouto.utilities') function blacklist:init(config)
local bindings = require('otouto.bindings') blacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('blacklist', true):t('unblacklist', true).table
function blacklist:init() blacklist.error = false
if not self.database.blacklist then
self.database.blacklist = {}
end
end end
blacklist.triggers = {
''
}
blacklist.error = false
function blacklist:action(msg, config) function blacklist:action(msg, config)
if self.database.blacklist[tostring(msg.from.id)] then if msg.from.id ~= config.admin then return true end
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
local targets = {} local targets = {}
if msg.reply_to_message then if msg.reply_to_message then
table.insert(targets, { table.insert(targets, {
@ -44,7 +20,7 @@ function blacklist:action(msg, config)
else else
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if input then if input then
for _, user in ipairs(utilities.index(input)) do for user in input:gmatch('%g+') do
if self.database.users[user] then if self.database.users[user] then
table.insert(targets, { table.insert(targets, {
id = self.database.users[user].id, id = self.database.users[user].id,

View File

@ -13,29 +13,16 @@ Returns solutions to mathematical expressions and conversions between common uni
end end
function calc:action(msg, config) function calc:action(msg, config)
local input = utilities.input_from_msg(msg)
local input = utilities.input(msg.text)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, calc.doc, true)
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)
return return
end end
output = '`' .. output .. '`' local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input)
local output = HTTPS.request(url)
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) output = output and '`'..output..'`' or config.errors.connection
utilities.send_reply(self, msg, output, true)
end end
return calc return calc

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
-- Commits from https://github.com/ngerakines/commitment.
local commit = {} local commit = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local http = require('socket.http')
commit.command = 'commit' commit.command = 'commit'
commit.doc = 'Returns a commit message from whatthecommit.com.' 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 commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table
end 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",
"Dont 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 Im supposed to do it, but I can't think of something better.",
"Dont 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) function commit:action(msg)
bindings.request(
local output = '`'..commits[math.random(#commits)]..'`' self,
utilities.send_message(self, msg.chat.id, output, true, nil, true) 'sendMessage',
{
chat_id = msg.chat.id,
text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```',
parse_mode = 'Markdown'
}
)
end end
return commit return commit

View File

@ -11,14 +11,14 @@ end
function echo:action(msg) function echo:action(msg)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true) utilities.send_message(self, msg.chat.id, echo.doc, true, msg.message_id, true)
else else
local output local output
if msg.chat.type == 'supergroup' then if msg.chat.type == 'supergroup' then
output = '*Echo:*\n"' .. utilities.md_escape(input) .. '"' output = utilities.style.enquote('Echo', input)
else else
output = utilities.md_escape(utilities.char.zwnj..input) output = utilities.md_escape(utilities.char.zwnj..input)
end end

View File

@ -6,11 +6,10 @@ local utilities = require('otouto.utilities')
function fortune:init(config) function fortune:init(config)
local s = io.popen('fortune'):read('*all') local s = io.popen('fortune'):read('*all')
if s:match('not found$') then assert(
print('fortune is not installed on this computer.') not s:match('not found$'),
print('fortune.lua will not be enabled.') 'fortune.lua requires the fortune program to be installed.'
return )
end
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 end

View File

@ -9,37 +9,28 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function gImages:init(config) function gImages:init(config)
if not config.google_api_key then assert(config.google_api_key and config.google_cse_key,
print('Missing config value: google_api_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.'
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
gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table 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.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. 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' 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 end
gImages.command = 'image <query>' gImages.command = 'image <query>'
function gImages:action(msg, config) function gImages:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, gImages.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, gImages.doc, true, msg.message_id, true)
return
end
end 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 if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then
url = url .. '&safe=high' url = url .. '&safe=high'

View File

@ -6,28 +6,26 @@ local utilities = require('otouto.utilities')
gMaps.command = 'location <query>' gMaps.command = 'location <query>'
function gMaps:init(config) function gMaps:init(config)
gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('location', true):t('loc', true).table gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat)
gMaps.doc = config.cmd_pat .. [[location <query> :t('location', true):t('loc', true).table
gMaps.doc = [[
/location <query>
Returns a location from Google Maps. Returns a location from Google Maps.
Alias: ]] .. config.cmd_pat .. 'loc' Alias: /loc
]]
gMaps.doc = gMaps.doc:gsub('/', config.cmd_pat)
end end
function gMaps:action(msg, config) function gMaps:action(msg, config)
local input = utilities.input_from_msg(msg)
local input = utilities.input(msg.text)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, gMaps.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, gMaps.doc, true, msg.message_id, true)
return
end
end end
local coords = utilities.get_coords(input, config) local coords = utilities.get_coords(input, config)
if type(coords) == 'string' then if type(coords) == 'string' then
utilities.send_reply(self, msg, coords) utilities.send_reply(self, msg, coords)
return
end end
bindings.sendLocation(self, { bindings.sendLocation(self, {
@ -36,7 +34,6 @@ function gMaps:action(msg, config)
longitude = coords.lon, longitude = coords.lon,
reply_to_message_id = msg.message_id reply_to_message_id = msg.message_id
} ) } )
end end
return gMaps return gMaps

View File

@ -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('&amp;', '&')
--[[
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

View File

@ -1,63 +1,32 @@
-- Put this on the bottom of your plugin list, after help.lua. local utilities = require('otouto.utilities')
-- If you want to configure your own greetings, copy the following table
-- (without the "config.") to your config.lua file.
local greetings = {} local greetings = {}
local utilities = require('otouto.utilities')
function greetings:init(config) function greetings:init(config)
config.greetings = config.greetings or { greetings.triggers = {}
['Hello, #NAME.'] = { for _, triggers in pairs(config.greetings) do
'hello', for i = 1, #triggers do
'hey', triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$'
'sup', table.insert(greetings.triggers, triggers[i])
'hi', end
'good morning', end
'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*$'
}
end end
function greetings:action(msg, config) function greetings:action(msg, config)
local nick
local nick = utilities.build_name(msg.from.first_name, msg.from.last_name)
if self.database.userdata[tostring(msg.from.id)] then 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 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, triggers in pairs(config.greetings) do
for _,response in pairs(responses) do for _, trigger in pairs(triggers) do
if msg.text_lower:match(response..',? '..self.info.first_name:lower()) then if string.match(msg.text_lower, trigger) then
local output = utilities.char.zwnj .. trigger:gsub('#NAME', nick) utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick))
utilities.send_message(self, msg.chat.id, output)
return return
end end
end end
end end
return true
end end
return greetings return greetings

View File

@ -1,63 +1,75 @@
local hackernews = {}
local HTTPS = require('ssl.https') local HTTPS = require('ssl.https')
local JSON = require('dkjson') local JSON = require('dkjson')
local bindings = require('otouto.bindings')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local hackernews = {}
hackernews.command = '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) function hackernews:init(config)
hackernews.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hackernews', true):t('hn', true).table 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.doc = [[Returns four (if group) or eight (if private message) top stories from Hacker News.
Alias: ]] .. config.cmd_pat .. 'hn' 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 end
function hackernews:action(msg, config) function hackernews:action(msg, config)
local now = os.time() / 60
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) if not hackernews.results or hackernews.last_update + config.hackernews_interval < now then
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' })
local jstr, res = HTTPS.request('https://hacker-news.firebaseio.com/v0/topstories.json') hackernews.results = get_hackernews_results()
if res ~= 200 then if not hackernews.results then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end
hackernews.last_update = now
end end
-- Four results in a group, eight in private.
local jdat = JSON.decode(jstr) local res_count = msg.chat.id == msg.from.id and 8 or 4
local output = '<b>Top Stories from Hacker News:</b>'
local res_count = 4
if msg.chat.id == msg.from.id then
res_count = 8
end
local output = '*Hacker News:*\n'
for i = 1, res_count do for i = 1, res_count do
local res_url = 'https://hacker-news.firebaseio.com/v0/item/' .. jdat[i] .. '.json' output = output .. hackernews.results[i]
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('&amp;', '&')
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
end end
utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
utilities.send_message(self, msg.chat.id, output, true, nil, true)
end end
return hackernews return hackernews

View File

@ -5,45 +5,36 @@ local hearthstone = {}
--local HTTPS = require('ssl.https') --local HTTPS = require('ssl.https')
local JSON = require('dkjson') local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local HTTPS = require('ssl.https')
function hearthstone:init(config) 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 if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then
print('Downloading Hearthstone database...') 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')
--local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json') if not jstr or res ~= 200 then
--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
print('Error connecting to hearthstonejson.com.') print('Error connecting to hearthstonejson.com.')
print('hearthstone.lua will not be enabled.') print('hearthstone.lua will not be enabled.')
hearthstone.command = nil
hearthstone.triggers = nil
return return
end end
self.database.hearthstone = JSON.decode(jstr)
self.database.hearthstone = d
self.database.hearthstone.expiration = os.time() + 600000 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.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table
hearthstone.doc = config.cmd_pat .. [[hearthstone <query> hearthstone.doc = config.cmd_pat .. [[hearthstone <query>
Returns Hearthstone card info. Returns Hearthstone card info.
Alias: ]] .. config.cmd_pat .. 'hs' Alias: ]] .. config.cmd_pat .. 'hs'
end end
hearthstone.command = 'hearthstone <query>'
local function format_card(card) local function format_card(card)
local ctype = card.type local ctype = card.type
@ -102,9 +93,9 @@ end
function hearthstone:action(msg, config) function hearthstone:action(msg, config)
local input = utilities.input(msg.text_lower) local input = utilities.input_from_msg(msg)
if not input then 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 return
end end

View File

@ -1,46 +1,47 @@
-- This plugin should go at the end of your plugin list in local utilities = require('otouto.utilities')
-- config.lua, but not after greetings.lua.
local help = {} local help = {}
local utilities = require('otouto.utilities')
local help_text
function help:init(config) 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.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.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.'
end end
function help:action(msg) function help:action(msg, config)
local input = utilities.input(msg.text_lower) local input = utilities.input(msg.text_lower)
if input then 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 for _,plugin in ipairs(self.plugins) do
if plugin.help_word == input:gsub('^/', '') then 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) utilities.send_message(self, msg.chat.id, output, true, nil, true)
return return
end end
end end
utilities.send_reply(self, msg, 'Sorry, there is no help for that command.') utilities.send_reply(self, msg, 'Sorry, there is no help for that command.')
else 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. -- Attempt to send the help message via PM.
-- If msg is from a group, tell the group whether the PM was successful. -- 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 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) 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 elseif msg.chat.type ~= 'private' then

62
otouto/plugins/id.lua Normal file
View File

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

View File

@ -14,14 +14,10 @@ end
function imdb:action(msg, config) function imdb:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, imdb.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, imdb.doc, true, msg.message_id, true)
return
end
end end
local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input) local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input)

43
otouto/plugins/isup.lua Normal file
View File

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

View File

@ -9,11 +9,9 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function lastfm:init(config) function lastfm:init(config)
if not config.lastfm_api_key then assert(config.lastfm_api_key,
print('Missing config value: lastfm_api_key.') 'lastfm.lua requires a last.fm API key from http://last.fm/api.'
print('lastfm.lua will not be enabled.') )
return
end
lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table 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.doc = config.cmd_pat .. [[np [username]

View File

@ -2,10 +2,21 @@ local luarun = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local URL = require('socket.url') local URL = require('socket.url')
local JSON = require('dkjson') local JSON, serpent
function luarun:init(config) function luarun:init(config)
luarun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lua', true):t('return', true).table 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 end
function luarun:action(msg, config) function luarun:action(msg, config)
@ -28,6 +39,7 @@ function luarun:action(msg, config)
local bot = require('otouto.bot') local bot = require('otouto.bot')
local bindings = require('otouto.bindings') local bindings = require('otouto.bindings')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local drua = require('otouto.drua-tg')
local JSON = require('dkjson') local JSON = require('dkjson')
local URL = require('socket.url') local URL = require('socket.url')
local HTTP = require('socket.http') local HTTP = require('socket.http')
@ -38,7 +50,7 @@ function luarun:action(msg, config)
output = 'Done!' output = 'Done!'
else else
if type(output) == 'table' then if type(output) == 'table' then
local s = JSON.encode(output, {indent=true}) local s = luarun.serialize(output)
if URL.escape(s):len() < 4000 then if URL.escape(s):len() < 4000 then
output = s output = s
end end

View File

@ -9,33 +9,59 @@ function me:init(config)
end end
function me:action(msg, config) function me:action(msg, config)
local user
local userdata = self.database.userdata[tostring(msg.from.id)] or {}
if msg.from.id == config.admin then if msg.from.id == config.admin then
if msg.reply_to_message 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 else
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if input then if input then
local user_id = utilities.id_from_username(self, input) if tonumber(input) then
if user_id then user = self.database.users[input]
userdata = self.database.userdata[tostring(user_id)] or {} 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
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 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 end
if output == '' then local output
if #data == 0 then
output = 'There is no data stored for this user.' 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 end
utilities.send_message(self, msg.chat.id, output, true, nil, true) utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end end

View File

@ -1,10 +1,18 @@
local patterns = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
patterns.triggers = { local patterns = {}
'^/?s/.-/.-$'
} 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) function patterns:action(msg)
if not msg.reply_to_message then return true end if not msg.reply_to_message then return true end
@ -24,8 +32,8 @@ function patterns:action(msg)
if res == false then if res == false then
utilities.send_reply(self, msg, 'Malformed pattern!') utilities.send_reply(self, msg, 'Malformed pattern!')
else else
output = output:sub(1, 4000) output = utilities.trim(output:sub(1, 4000))
output = '*Did you mean:*\n"' .. utilities.md_escape(utilities.trim(output)) .. '"' output = utilities.style.enquote('Did you mean', output)
utilities.send_reply(self, msg.reply_to_message, output, true) utilities.send_reply(self, msg.reply_to_message, output, true)
end end
end end

View File

@ -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.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pokedex', true):t('dex', true).table
pokedex.doc = config.cmd_pat .. [[pokedex <query> pokedex.doc = config.cmd_pat .. [[pokedex <query>
Returns a Pokedex entry from pokeapi.co. Returns a Pokedex entry from pokeapi.co.
Queries must be a number of the name of a Pokémon.
Alias: ]] .. config.cmd_pat .. 'dex' Alias: ]] .. config.cmd_pat .. 'dex'
end end
function pokedex:action(msg, config) function pokedex:action(msg, config)
bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'typing' } ) local input = utilities.input_from_msg(msg)
local input = utilities.input(msg.text_lower)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, pokedex.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, pokedex.doc, true, msg.message_id, true)
return
end
end end
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_url = url .. '/api/v1/pokemon/' .. input
@ -39,6 +36,11 @@ function pokedex:action(msg, config)
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
local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri local desc_url = url .. dex_jdat.descriptions[math.random(#dex_jdat.descriptions)].resource_uri
local desc_jstr, _ = HTTP.request(desc_url) local desc_jstr, _ = HTTP.request(desc_url)
if res ~= 200 then if res ~= 200 then

View File

@ -97,8 +97,12 @@ function pgc:action(msg)
if egg_count < 1 then if egg_count < 1 then
recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.' recommendation = 'Wait until you have atleast sixty Pokémon to evolve before using a lucky egg.'
else else
recommendation = 'Use %s lucky egg(s) for %s evolutions.' recommendation = string.format(
recommendation = recommendation:format(egg_count, egg_count*60) 'Use %s lucky egg%s for %s evolutions.',
egg_count,
egg_count == 1 and '' or 's',
egg_count * 60
)
end end
s = s:format(total_evolutions, recommendation) s = s:format(total_evolutions, recommendation)
output = output .. s output = output .. s

View File

@ -8,7 +8,8 @@ pokemon_go.command = 'pokego <team>'
function pokemon_go:init(config) function pokemon_go:init(config)
pokemon_go.triggers = utilities.triggers(self.info.username, config.cmd_pat) pokemon_go.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('pokego', true):t('pokégo', true) :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> 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.]] 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 local db = self.database.pokemon_go

View File

@ -12,10 +12,9 @@ end
function preview:action(msg) function preview:action(msg)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then 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 return
end end
@ -36,8 +35,8 @@ function preview:action(msg)
end end
-- Invisible zero-width, non-joiner. -- Invisible zero-width, non-joiner.
local output = '[](' .. input .. ')' local output = '<a href="' .. input .. '">' .. utilities.char.zwnj .. '</a>'
utilities.send_message(self, msg.chat.id, output, false, nil, true) utilities.send_message(self, msg.chat.id, output, false, nil, 'html')
end end

View File

@ -13,16 +13,6 @@ local utilities = require('otouto.utilities')
reactions.command = 'reactions' reactions.command = 'reactions'
reactions.doc = 'Returns a list of "reaction" emoticon commands.' reactions.doc = 'Returns a list of "reaction" emoticon commands.'
local mapping = {
['shrug'] = '¯\\_(ツ)_/¯',
['lenny'] = '( ͡° ͜ʖ ͡°)',
['flip'] = '(╯°□°)╯︵ ┻━┻',
['homo'] = ' o',
['look'] = 'ಠ_ಠ',
['shots?'] = 'SHOTS FIRED',
['facepalm'] = '(-‸ლ)'
}
local help local help
function reactions:init(config) function reactions:init(config)
@ -30,8 +20,8 @@ function reactions:init(config)
help = 'Reactions:\n' help = 'Reactions:\n'
reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table
local username = self.info.username:lower() local username = self.info.username:lower()
for trigger,reaction in pairs(mapping) do for trigger,reaction in pairs(config.reactions) do
help = help .. '' .. config.cmd_pat .. trigger:gsub('.%?', '') .. ': ' .. reaction .. '\n' help = help .. '' .. config.cmd_pat .. trigger .. ': ' .. reaction .. '\n'
table.insert(reactions.triggers, '^'..config.cmd_pat..trigger) 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..'@'..username)
table.insert(reactions.triggers, config.cmd_pat..trigger..'$') 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) utilities.send_message(self, msg.chat.id, help)
return return
end 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 if string.match(msg.text_lower, config.cmd_pat..trigger) then
utilities.send_message(self, msg.chat.id, reaction) utilities.send_message(self, msg.chat.id, reaction)
return return

View File

@ -8,87 +8,83 @@ 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
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 end
function remind:action(msg) function remind:action(msg, config)
-- Ensure there are arguments. If not, send doc.
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
if not input then 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 return
end end
-- Ensure first arg is a number. If not, send doc.
local duration = utilities.get_word(input, 1) local duration = tonumber(utilities.get_word(input, 1))
if not tonumber(duration) then if not duration then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true) utilities.send_reply(self, msg, remind.doc, true)
return return
end end
-- Duration must be between one minute and one year (approximately).
duration = tonumber(duration)
if duration < 1 then if duration < 1 then
duration = 1 duration = 1
elseif duration > 526000 then elseif duration > config.remind.max_duration then
duration = 526000 duration = config.remind.max_duration
end end
-- Ensure there is a second arg.
local message = utilities.input(input) local message = utilities.input(input)
if not message then 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 return
end 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 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 {} 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]) >= config.remind.max_reminders_private then
if msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) > 9 then output = 'Sorry, you already have the maximum number of reminders.'
utilities.send_reply(self, msg, 'Sorry, this group already has ten reminders.') elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then
return output = 'Sorry, this group already has the maximum number of reminders.'
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!'
else 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 end
utilities.send_reply(self, msg, output) utilities.send_reply(self, msg, output, true)
end end
function remind:cron() function remind:cron(config)
local time = os.time() local time = os.time()
-- Iterate over the group entries in the reminders database. -- Iterate over the group entries in the reminders database.
for chat_id, group in pairs(self.database.reminders) do for chat_id, group in pairs(self.database.reminders) do
local new_group = {}
-- Iterate over each reminder. -- 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. -- If the reminder is past-due, send it and nullify it.
-- Otherwise, add it to the replacement table. -- Otherwise, add it to the replacement table.
if time > reminder.time then 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) local res = utilities.send_message(self, chat_id, output, true, nil, true)
-- If the message fails to send, save it for later. -- If the message fails to send, save it for later (if enabled in config).
if not res then if res or not config.remind.persist then
table.insert(new_group, reminder) group[k] = nil
end end
else
table.insert(new_group, reminder)
end end
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
end end

View File

@ -20,7 +20,9 @@ function shell:action(msg, config)
return return
end 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 if output:len() == 0 then
output = 'Done!' output = 'Done!'
else else

View File

@ -3,6 +3,7 @@ local shout = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
shout.command = 'shout <text>' shout.command = 'shout <text>'
local utf8 = '('..utilities.char.utf_8..'*)'
function shout:init(config) function shout:init(config)
shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table
@ -11,22 +12,19 @@ end
function shout:action(msg) function shout:action(msg)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
if msg.reply_to_message and #msg.reply_to_message.text > 0 then utilities.send_reply(self, msg, shout.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, shout.doc, true, msg.message_id, true)
return
end
end end
input = utilities.trim(input) input = utilities.trim(input)
input = input:upper() input = input:upper()
local output = '' local output = ''
local inc = 0 local inc = 0
local ilen = 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 if ilen < 20 then
ilen = ilen + 1 ilen = ilen + 1
output = output .. match .. ' ' output = output .. match .. ' '
@ -34,7 +32,7 @@ function shout:action(msg)
end end
ilen = 0 ilen = 0
output = output .. '\n' 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 if ilen < 19 then
local spacing = '' local spacing = ''
for _ = 1, inc do for _ = 1, inc do

View File

@ -109,7 +109,21 @@ local slaps = {
function slap:action(msg) function slap:action(msg)
local input = utilities.input(msg.text) local input = utilities.input(msg.text)
local victor_id = msg.from.id 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 -- IDs
if victim_id then if victim_id then
if victim_id == victor_id then if victim_id == victor_id then

View File

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

View File

@ -5,6 +5,7 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
time.command = 'time <location>' time.command = 'time <location>'
time.base_url = 'https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s&timestamp=%s'
function time:init(config) function time:init(config)
time.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('time', true).table 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 end
function time:action(msg, config) function time:action(msg, config)
local input = utilities.input_from_msg(msg)
local input = utilities.input(msg.text)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, time.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, time.doc, true, msg.message_id, true)
return
end
end end
local coords = utilities.get_coords(input, config) local coords = utilities.get_coords(input, config)
@ -31,30 +27,33 @@ function time:action(msg, config)
end end
local now = os.time() local now = os.time()
local utc = os.time(os.date("!*t", now)) local utc = os.time(os.date('!*t', now))
local url = time.base_url:format(coords.lat, coords.lon, utc)
local url = 'https://maps.googleapis.com/maps/api/timezone/json?location=' .. coords.lat ..','.. coords.lon .. '&timestamp='..utc local jstr, code = HTTPS.request(url)
if code ~= 200 then
local jstr, res = HTTPS.request(url)
if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end 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 timestamp = now + data.rawOffset + data.dstOffset
local utcoff = (jdat.rawOffset + jdat.dstOffset) / 3600 local utcoff = (data.rawOffset + data.dstOffset) / 3600
if utcoff == math.abs(utcoff) then if utcoff == math.abs(utcoff) then
utcoff = '+'.. utilities.pretty_float(utcoff) utcoff = '+' .. utilities.pretty_float(utcoff)
else else
utcoff = utilities.pretty_float(utcoff) utcoff = utilities.pretty_float(utcoff)
end end
local output = os.date('!%I:%M %p\n', timestamp) .. os.date('!%A, %B %d, %Y\n', timestamp) .. jdat.timeZoneName .. ' (UTC' .. utcoff .. ')' local output = string.format('```\n%s\n%s (UTC%s)\n```',
output = '```\n' .. output .. '\n```' os.date('!%I:%M %p\n%A, %B %d, %Y', timestamp),
data.timeZoneName,
utcoff
)
utilities.send_reply(self, msg, output, true) utilities.send_reply(self, msg, output, true)
end end
return time return time

View File

@ -8,42 +8,38 @@ local utilities = require('otouto.utilities')
translate.command = 'translate [text]' translate.command = 'translate [text]'
function translate:init(config) 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] translate.doc = config.cmd_pat .. [[translate [text]
Translates input or the replied-to message into the bot's language.]] 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 end
function translate:action(msg, config) function translate:action(msg, config)
local input = utilities.input_from_msg(msg)
local input = utilities.input(msg.text)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, translate.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, translate.doc, true, msg.message_id, true)
return
end
end 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 url = translate.base_url:format(URL.escape(input))
local jstr, code = HTTPS.request(url)
local str, res = HTTPS.request(url) if code ~= 200 then
if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(str) local data = JSON.decode(jstr)
if jdat.code ~= 200 then if data.code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local output = jdat.text[1] utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true)
output = '*Translation:*\n"' .. utilities.md_escape(output) .. '"'
utilities.send_reply(self, msg.reply_to_message or msg, output, true)
end end
return translate return translate

View File

@ -6,50 +6,45 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
urbandictionary.command = 'urbandictionary <query>' urbandictionary.command = 'urbandictionary <query>'
urbandictionary.base_url = 'http://api.urbandictionary.com/v0/define?term='
function urbandictionary:init(config) function urbandictionary:init(config)
urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat) urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('urbandictionary', true):t('ud', true):t('urban', true).table :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. 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 end
function urbandictionary:action(msg, config) function urbandictionary:action(msg, config)
local input = utilities.input_from_msg(msg)
local input = utilities.input(msg.text)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, urbandictionary.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, urbandictionary.doc, true, msg.message_id, true)
return
end
end end
local url = 'http://api.urbandictionary.com/v0/define?term=' .. URL.escape(input) local url = urbandictionary.base_url .. URL.escape(input)
local jstr, code = HTTP.request(url)
local jstr, res = HTTP.request(url) if code ~= 200 then
if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local jdat = JSON.decode(jstr) local data = JSON.decode(jstr)
if jdat.result_type == "no_results" then local output
utilities.send_reply(self, msg, config.errors.results) if data.result_type == 'no_results' then
return 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 end
utilities.send_reply(self, msg, output, true)
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)
end end
return urbandictionary return urbandictionary

View File

@ -6,11 +6,9 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function weather:init(config) function weather:init(config)
if not config.owm_api_key then assert(config.owm_api_key,
print('Missing config value: owm_api_key.') 'weather.lua requires an OpenWeatherMap API key from http://openweathermap.org/API.'
print('weather.lua will not be enabled.') )
return
end
weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table weather.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('weather', true).table
weather.doc = config.cmd_pat .. [[weather <location> weather.doc = config.cmd_pat .. [[weather <location>
@ -21,14 +19,10 @@ weather.command = 'weather <location>'
function weather:action(msg, config) function weather:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, weather.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, weather.doc, true, msg.message_id, true)
return
end
end end
local coords = utilities.get_coords(input, config) local coords = utilities.get_coords(input, config)

View File

@ -1,51 +1,59 @@
local whoami = {} local whoami = {}
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
whoami.command = 'whoami' whoami.command = 'whoami'
function whoami:init(config) 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 = [[ whoami.doc = [[
Returns user and chat info for you or the replied-to message. Returns user and chat info for you or the replied-to message.
Alias: ]] .. config.cmd_pat .. 'who' Alias: ]] .. config.cmd_pat .. 'who'
end end
function whoami:action(msg) function whoami:action(msg)
-- Operate on the replied-to message, if it exists.
if msg.reply_to_message then msg = msg.reply_to_message or msg
msg = msg.reply_to_message -- If it's a private conversation, bot is chat, unless bot is from.
end 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.build_name(msg.from.first_name, msg.from.last_name) local from_name = utilities.html_escape(
utilities.build_name(
local chat_id = math.abs(msg.chat.id) msg.from.first_name,
if chat_id > 1000000000000 then msg.from.last_name
chat_id = chat_id - 1000000000000 )
end )
local chat_name = utilities.html_escape(
local user = 'You are @%s, also known as *%s* `[%s]`' chat.title
if msg.from.username then or utilities.build_name(chat.first_name, chat.last_name)
user = user:format(utilities.markdown_escape(msg.from.username), from_name, msg.from.id) )
else -- "Normalize" a group ID so it's not arbitrarily modified by the bot API.
user = 'You are *%s* `[%s]`,' local chat_id = math.abs(chat.id)
user = user:format(from_name, msg.from.id) if chat_id > 1000000000000 then chat_id = chat_id - 1000000000000 end
end -- Do the thing.
local output = string.format(
local group = '@%s, also known as *%s* `[%s]`.' 'You are %s <code>[%s]</code>, and you are messaging %s <code>[%s]</code>.',
if msg.chat.type == 'private' then msg.from.username and string.format(
group = group:format(utilities.markdown_escape(self.info.username), self.info.first_name, self.info.id) '@%s, also known as <b>%s</b>',
elseif msg.chat.username then msg.from.username,
group = group:format(utilities.markdown_escape(msg.chat.username), msg.chat.title, chat_id) from_name
else ) or '<b>' .. from_name .. '</b>',
group = '*%s* `[%s]`.' msg.from.id,
group = group:format(msg.chat.title, chat_id) msg.chat.username and string.format(
end '@%s, also known as <b>%s</b>',
chat.username,
local output = user .. ', and you are messaging ' .. group chat_name
) or '<b>' .. chat_name .. '</b>',
utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) 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 end
return whoami return whoami

View File

@ -12,101 +12,79 @@ function wikipedia:init(config)
wikipedia.doc = config.cmd_pat .. [[wikipedia <query> wikipedia.doc = config.cmd_pat .. [[wikipedia <query>
Returns an article from Wikipedia. Returns an article from Wikipedia.
Aliases: ]] .. config.cmd_pat .. 'w, ' .. config.cmd_pat .. 'wiki' Aliases: ]] .. config.cmd_pat .. 'w, ' .. config.cmd_pat .. 'wiki'
end 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='
local get_title = function(search) wikipedia.art_url = 'https://' .. config.lang .. '.wikipedia.org/wiki/'
for _,v in ipairs(search) do
if not v.snippet:match('may refer to:') then
return v.title
end
end
return false
end end
function wikipedia:action(msg, config) function wikipedia:action(msg, config)
local input = utilities.input_from_msg(msg)
-- 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)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, wikipedia.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, wikipedia.doc, true, msg.message_id, true)
return
end
end end
-- This kinda sucks, but whatever. local jstr, code = HTTPS.request(wikipedia.search_url .. URL.escape(input))
input = input:gsub('#', ' sharp') if code ~= 200 then
-- 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
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
jdat = JSON.decode(jstr) local data = JSON.decode(jstr)
if jdat.query.searchinfo.totalhits == 0 then if data.query.searchinfo.totalhits == 0 then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end 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 if not title then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
end end
local res_url = 'https://en.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&exchars=4000&exsectionformat=plain&titles=' local res_jstr, res_code = HTTPS.request(wikipedia.res_url .. URL.escape(title))
if res_code ~= 200 then
jstr, res = HTTPS.request(res_url .. URL.escape(title))
if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection) utilities.send_reply(self, msg, config.errors.connection)
return return
end end
local _ local _, text = next(JSON.decode(res_jstr).query.pages)
local text = JSON.decode(jstr).query.pages
_, text = next(text)
if not text then if not text then
utilities.send_reply(self, msg, config.errors.results) utilities.send_reply(self, msg, config.errors.results)
return return
else
text = text.extract
end end
-- Remove needless bits from the article, take only the first paragraph. text = text.extract
text = text:gsub('</?.->', '') -- Remove crap and take only the first paragraph.
text = text:gsub('</?.->', ''):gsub('%[.+%]', '')
local l = text:find('\n') local l = text:find('\n')
if l then if l then
text = text:sub(1, l-1) text = text:sub(1, l-1)
end end
local url = wikipedia.art_url .. URL.escape(title)
-- This block can be annoying to read. title = utilities.html_escape(title)
-- We use the initial title to make the url for later use. Then we remove -- If the beginning of the article is the title, embolden that.
-- the extra bits that won't be in the article. We determine whether the -- Otherwise, we'll add a title in bold.
-- first part of the text is the title, and if so, we embolden that. local short_title = title:gsub('%(.+%)', '')
-- Otherwise, we prepend the text with a bold title. Then we append a "Read local combined_text, count = text:gsub('^'..short_title, '<b>'..short_title..'</b>')
-- More" link. local body
local url = 'https://en.wikipedia.org/wiki/' .. URL.escape(title) if count == 1 then
title = title:gsub('%(.+%)', '') body = combined_text
local output
if string.match(text:sub(1, title:len()), title) then
output = '*' .. title .. '*' .. text:sub(title:len()+1)
else else
output = '*' .. title:gsub('%(.+%)', '') .. '*\n' .. text:gsub('%[.+%]','') body = '<b>' .. title .. '</b>\n' .. text
end end
output = output .. '\n[Read more.](' .. url:gsub('%)', '\\)') .. ')' local output = string.format(
'%s\n<a href="%s">Read more.</a>',
utilities.send_message(self, msg.chat.id, output, true, nil, true) body,
utilities.html_escape(url)
)
utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end end
return wikipedia return wikipedia

View File

@ -5,52 +5,48 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
xkcd.command = 'xkcd [i]' 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) function xkcd:init(config)
xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table
xkcd.doc = config.cmd_pat .. [[xkcd [i] 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.]] 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 end
function xkcd:action(msg, config) function xkcd:action(msg, config)
local input = utilities.get_word(msg.text, 2)
local jstr, res = HTTP.request('http://xkcd.com/info.0.json') if input == 'r' then
if res ~= 200 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) 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 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 end
return xkcd return xkcd

View File

@ -8,11 +8,9 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities') local utilities = require('otouto.utilities')
function youtube:init(config) function youtube:init(config)
if not config.google_api_key then assert(config.google_api_key,
print('Missing config value: google_api_key.') 'youtube.lua requires a Google API key from http://console.developers.google.com.'
print('youtube.lua will not be enabled.') )
return
end
youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table 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.doc = config.cmd_pat .. [[youtube <query>
@ -24,14 +22,10 @@ youtube.command = 'youtube <query>'
function youtube:action(msg, config) function youtube:action(msg, config)
local input = utilities.input(msg.text) local input = utilities.input_from_msg(msg)
if not input then if not input then
if msg.reply_to_message and msg.reply_to_message.text then utilities.send_reply(self, msg, youtube.doc, true)
input = msg.reply_to_message.text return
else
utilities.send_message(self, msg.chat.id, youtube.doc, true, msg.message_id, true)
return
end
end 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)

View File

@ -12,45 +12,38 @@ local bindings = require('otouto.bindings')
-- For the sake of ease to new contributors and familiarity to old contributors, -- For the sake of ease to new contributors and familiarity to old contributors,
-- we'll provide a couple of aliases to real bindings here. -- 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) 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', { return bindings.request(self, 'sendMessage', {
chat_id = chat_id, chat_id = chat_id,
text = text, text = text,
disable_web_page_preview = disable_web_page_preview, disable_web_page_preview = disable_web_page_preview,
reply_to_message_id = reply_to_message_id, reply_to_message_id = reply_to_message_id,
parse_mode = use_markdown and 'Markdown' or nil parse_mode = parse_mode
} ) } )
end end
function utilities:send_reply(old_msg, text, use_markdown) function utilities:send_reply(old_msg, text, use_markdown)
return bindings.request(self, 'sendMessage', { return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown)
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
} )
end end
-- get the indexed word in a string -- get the indexed word in a string
function utilities.get_word(s, i) function utilities.get_word(s, i)
s = s or '' s = s or ''
i = i or 1 i = i or 1
local t = {} local n = 0
for w in s:gmatch('%g+') do for w in s:gmatch('%g+') do
table.insert(t, w) n = n + 1
if n == i then return w end
end end
return t[i] or false return 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
end end
-- Returns the string after the first space. -- Returns the string after the first space.
@ -61,6 +54,10 @@ function utilities.input(s)
return s:sub(s:find(' ')+1) return s:sub(s:find(' ')+1)
end 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 -- Calculates the length of the given string as UTF-8 characters
function utilities.utf8_len(s) function utilities.utf8_len(s)
local chars = 0 local chars = 0
@ -82,13 +79,13 @@ end
-- Loads a JSON file as a table. -- Loads a JSON file as a table.
function utilities.load_data(filename) function utilities.load_data(filename)
local f = io.open(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 {} return {}
end end
local s = f:read('*all')
f:close()
local data = JSON.decode(s)
return data
end end
-- Saves a table to a JSON file. -- Saves a table to a JSON file.
@ -153,85 +150,14 @@ function utilities:resolve_username(input)
end end
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) function utilities:handle_exception(err, message, config)
local output = string.format(
if not err then err = '' end '\n[%s]\n%s: %s\n%s\n',
os.date('%F %T'),
local output = '\n[' .. os.date('%F %T', os.time()) .. ']\n' .. self.info.username .. ': ' .. err .. '\n' .. message .. '\n' self.info.username,
err or '',
message
)
if config.log_chat then if config.log_chat then
output = '```' .. output .. '```' output = '```' .. output .. '```'
utilities.send_message(self, config.log_chat, output, true, nil, true) utilities.send_message(self, config.log_chat, output, true, nil, true)
@ -265,16 +191,15 @@ function utilities.download_file(url, filename)
return filename return filename
end end
function utilities.markdown_escape(text) function utilities.md_escape(text)
text = text:gsub('_', '\\_') return text:gsub('_', '\\_')
text = text:gsub('%[', '\\[') :gsub('%[', '\\['):gsub('%]', '\\]')
text = text:gsub('%]', '\\]') :gsub('%*', '\\*'):gsub('`', '\\`')
text = text:gsub('%*', '\\*')
text = text:gsub('`', '\\`')
return text
end end
utilities.md_escape = utilities.markdown_escape function utilities.html_escape(text)
return text:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;')
end
utilities.triggers_meta = {} utilities.triggers_meta = {}
utilities.triggers_meta.__index = utilities.triggers_meta utilities.triggers_meta.__index = utilities.triggers_meta
@ -320,7 +245,7 @@ utilities.char = {
rtl_override = '', rtl_override = '',
rtl_mark = '', rtl_mark = '',
em_dash = '', em_dash = '',
utf_8 = '([%z\1-\127\194-\244][\128-\191]*)', utf_8 = '[%z\1-\127\194-\244][\128-\191]',
} }
utilities.set_meta = {} utilities.set_meta = {}
@ -354,4 +279,11 @@ function utilities.set_meta:__len()
return self.__count return self.__count
end 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 return utilities

View File

@ -1,11 +1,11 @@
#!/bin/sh #!/bin/sh
# Launch tg listening on the default port (change this if you've changed it in # 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 while true; do
tg/bin/telegram-cli -P 4567 -E 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.' echo 'tg has stopped. ^C to exit.'
sleep 5s sleep 5s
done done