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
The plugin-wielding, multipurpose Telegram bot.
[Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Development Group](http://telegram.me/BotDevelopment)
[Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Bot Development Group](http://telegram.me/BotDevelopment)
otouto is a plugin-based, IRC-style bot written for the [Telegram Bot API](http://core.telegram.org/bots/api). Originally written in February of 2015 as a set of Lua scripts to run on [telegram-cli](http://github.com/vysheng/tg), otouto was open-sourced and migrated to the bot API later in June that year.
@ -12,12 +12,10 @@ otouto is free software; you are free to redistribute it and/or modify it under
| For Users | For Coders |
|:----------------------------------------------|:------------------------------|
| [Setup](#setup) | [Plugins](#plugins) |
| [Control plugins](#control-plugins) | [Bindings](#bindings) |
| [Group administration](#group-administration) | [Database](#database) |
| [List of plugins](#list-of-plugins) | [Output style](#output-style) |
| | [Contributors](#contributors) |
* * *
| [Configuration](#configuration) | [Bindings](#bindings) |
| [Control plugins](#control-plugins) | [Database](#database) |
| [Group administration](#group-administration) | [Output style](#output-style) |
| [List of plugins](#list-of-plugins) | [Contributors](#contributors) |
## Setup
You _must_ have Lua (5.2+), luasocket, luasec, multipart-post, and dkjson installed. You should also have lpeg, though it is not required. It is recommended you install these with LuaRocks.
@ -27,27 +25,85 @@ To get started, clone the repository and set the following values in `config.lua
- `bot_api_key` as your bot authorization token from the BotFather.
- `admin` as your Telegram ID.
Optionally:
- `lang` as the two-letter code representing your language.
Some plugins are not enabled by default. If you wish to enable them, add them to the `plugins` array.
When you are ready to start the bot, run `./launch.sh`. To stop the bot, send "/halt" through Telegram. If you terminate the bot manually, you risk data loss. If you do you not want the bot to restart automatically, run it with `lua main.lua`.
Note that certain plugins, such as translate.lua and greetings.lua, will require privacy mode to be disabled. Additionally, some plugins may require or make use of various API keys:
Note that certain plugins, such as translate.lua and greetings.lua, will require privacy mode to be disabled. Additionally, some plugins may require or make use of various API keys. See [Configuration](#configuration) for details.
- `bing.lua`: [Bing Search API](http://datamarket.azure.com/dataset/bing/search) key (`bing_api_key`)
- `gImages.lua` & `youtube.lua`: Google [API](http://console.developers.google.com) and [CSE](https://cse.google.com/cse) keys (`google_api_key`, `google_cse_key`)
- `weather.lua`: [OpenWeatherMap](http://openweathermap.org) API key (`owm_api_key`)
- `lastfm.lua`: [last.fm](http://last.fm/api) API key (`lastfm_api_key`)
- `bible.lua`: [Biblia](http://api.biblia.com) API key (`biblia_api_key`)
- `cats.lua`: [The Cat API](http://thecatapi.com) API key (optional) (`thecatapi_key`)
- `apod.lua`: [NASA](http://api.nasa.gov) API key (`nasa_api_key`)
- `translate.lua`: [Yandex](http://tech.yandex.com/keys/get) API key (`yandex_key`)
- `chatter.lua`: [SimSimi](http://developer.simsimi.com/signUp) API key (`simsimi_key`)
## Configuration
otouto is configured in the `config.lua` file. It is the single point of configuration for the bot, and contains any necessary user-specific variables, such as API keys, custom error messages, and enabled plugins.
* * *
This section includes an exhaustive list of possible configuration values for otouto and official plugins.
### Bot configuration values
| Name | Default | Description |
|:--------------|:--------|:---------------------------------------------------|
| `bot_api_key` | `""` | Telegram bot API token. |
| `admin` | nil | Telegram ID of the bot owner. |
| `log_chat` | nil | Telegram ID of the recipient for error messages. |
| `cmd_pat` | `"/"` | Character (or string) to be used for bot commands. |
| `lang` | `"en"` | Two-letter ISO 639-1 language code. |
| `about_text` | `...` | Informational text to be returned by /about. |
#### Error messages
These are the generic error messages used by most plugins. These belong in a table named `errors`.
| Name | Default |
|:-------------|:----------------------------------|
| `generic` | `"An unexpected error occurred."` |
| `connection` | `"Connection error."` |
| `results` | `"No results found."` |
| `argument` | `"Invalid argument."` |
| `syntax` | `"Invalid syntax."` |
#### Plugins
This table is an array of the names of enabled plugins. To enable a plugin, add its name to the list.
### Plugin configuration values
| Name | Description
|:--------------------------|:--------------------------------------------------------------------------------------------|
| `google_api_key` | [Google API](http://console.developers.google.com) key for `gImages.lua` and `youtube.lua`. |
| `google_cse_key` | [Google CSE](http://cse.google.com/cse) key for `gImages.lua`. |
| `lastfm_api_key` | [last.fm API](http://last.fm/api) key for `lastfm.lua`. |
| `owm_api_key` | [OpenWeatherMap API](http://openweathermap.org/API) key for `weather.lua`. |
| `biblia_api_key` | [Biblia API](http://api.biblia.com) key for `bible.lua`. |
| `thecatapi_key` | [The Cat API](http://thecatapi.com) key for `cats.lua` (optional). |
| `nasa_api_key` | [NASA API](http://api.nasa.gov) key for the `apod.lua` (optional). |
| `yandex_key` | [Yandex API](http://tech.yandex.com/keys/get) key for `translate.lua`. |
| `bing_api_key` | [Bing Search API](http://datamarket.azure.com/dataset/bing/search) key for `bing.lua`. |
| `drua_block_on_blacklist` | Whether to block blacklisted users, if tg-cli is in use. |
| `cli_port` | The port to use for tg connections. |
| `hackernews_interval` | The lifespan, in minutes, for each set of results hackernews.lua before refreshing. |
| `hackernews_onstart` | Whether hackernews.lua should fetch articles at load (rather than waiting for demand). |
Some plugins have many configuration values which warrant their own section of the configuration file. That section will be the name of the plugin, without the file extension. They are listed below.
#### remind.lua
| Name | Default | Description |
|:------------------------|:---------|:---------------------------------------------------------|
| `persist` | `true` | Whether reminders should be saved if they fail for send. |
| `max_length` | `1000` | The maximum length for reminders, in bytes. |
| `max_duration` | `526000` | The maximum duration of a reminder, in minutes. |
| `max_reminders_group` | `10` | The maximum number of reminders for a group. |
| `max_reminders_private` | `50` | The maximum number of reminders in private. |
#### chatter.lua
| Name | Default | Description |
|:----------------|:-------------------------------------------|:-------------------------------------------------------------------------|
| `cleverbot_api` | `"https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text="` | Cleverbot API endpoint used by `cleverbot.lua`. |
| `connection` | `"I don't feel like talking right now."` | Generic response for connection errors. |
| `response` | `"I don't know what to say to that."` | Generic response for when the API has no response. |
#### greetings.lua
The `greetings` table is a list of custom responses for the greetings plugin. Each value is an array of triggers, and the key for that array is the response. The default values are inserted by the greetings plugin if there is no user configuration. In the responses, `#NAME` is replaced with the user's name or nickname. The bot's name is automatically appended to all triggers. Triggers are not case sensitive.
#### reactions.lua
The `reactions` table is also a list of custom responses, for the reactions plugin. Each value is a key/value pair, where the key is the trigger, and the value is the reaction. The reactions plugin differs from the greetings plugin by how it is triggered: A reaction command must be at the beginning or end of a line.
## Control plugins
Some plugins are designed to be used by the bot's owner. Here are some examples, how they're used, and what they do.
@ -61,14 +117,12 @@ Some plugins are designed to be used by the bot's owner. Here are some examples,
| `shell.lua` | /run | Executes shell commands on the host system. |
| `luarun.lua` | /lua | Executes Lua commands in the bot's environment. |
* * *
## Group Administration
The administration plugin enables self-hosted, single-realm group administration, supporting both normal groups and supergroups whch are owned by the bot owner. This works by sending TCP commands to an instance of tg running on the owner's account.
To get started, run `./tg-install.sh`. Note that this script is written for Ubuntu/Debian. If you're running Arch (the only acceptable alternative), you'll have to do it yourself. If that is the case, note that otouto uses the "test" branch of tg, and the AUR package `telegram-cli-git` will not be sufficient, as it does not have support for supergroups yet.
Once the installation is finished, enable the `administration` plugin in your config file. **The administration plugin must be loaded before the `about` and `blacklist` plugins.** You may have reason to change the default TCP port (4567); if that is the case, remember to change it in `tg-launch.sh` as well. Run `./tg-launch.sh` in a separate screen/tmux window. You'll have to enter your phone number and go through the login process the first time. The script is set to restart tg after two seconds, so you'll need to Ctrl+C after exiting.
Once the installation is finished, enable the `administration` plugin in your config file. You may have reason to change the default TCP port (4567); if that is the case, remember to change it in `tg-launch.sh` as well. Run `./tg-launch.sh` in a separate screen/tmux window. You'll have to enter your phone number and go through the login process the first time. The script is set to restart tg after two seconds, so you'll need to Ctrl+C after exiting.
While tg is running, you may start/reload otouto with `administration.lua` enabled, and have access to a wide variety of administrative commands and automata. The administration "database" is stored in `administration.json`. To start using otouto to administrate a group (note that you must be the owner (or an administrator)), send `/gadd` to that group. For a list of commands, use `/ahelp`. Below I'll describe various functions now available to you.
@ -108,7 +162,7 @@ Internal commands can only be run within an administrated group.
### Description of Privileges
| # | Title | Description | Scope |
| | Title | Description | Scope |
|:-:|:--------------|:------------------------------------------------------------------|:-------|
| 0 | Banned | Cannot enter the group(s). | Either |
| 1 | User | Default rank. | Local |
@ -121,7 +175,7 @@ Obviously, each greater rank inherits the privileges of the lower, positive rank
### Flags
| # | Name | Description |
| | Name | Description |
|:-:|:------------|:---------------------------------------------------------------------------------|
| 1 | unlisted | Removes a group from the /groups listing. |
| 2 | antisquig | Automatically removes users for posting Arabic script or RTL characters. |
@ -149,53 +203,53 @@ antiflood (flag 5) provides a system of automatic flood protection by removing u
Additionally, antiflood can be configured to automatically ban a user after he has been automatically kicked from a single group a certain number of times in one day. This is configurable as the antiflood value `autoban` and is set to three by default.
* * *
## List of plugins
| Plugin | Command | Function | Aliases |
|:----------------------|:------------------------------|:--------------------------------------------------------|:--------|
| `help.lua` | /help [command] | Returns a list of commands or command-specific help. | /h |
| `about.lua` | /about | Returns the about text as configured in config.lua. |
| `ping.lua` | /ping | The simplest plugin ever! |
| `echo.lua` | /echo text | Repeats a string of text. |
| `bing.lua` | /bing query | Returns Bing web results. | /g |
| `gImages.lua` | /images query | Returns a Google image result. | /i |
| `gMaps.lua` | /location query | Returns location data from Google Maps. | /loc |
| `youtube.lua` | /youtube query | Returns the top video result from YouTube. | /yt |
| `wikipedia.lua` | /wikipedia query | Returns the summary of a Wikipedia article. | /w |
| `lastfm.lua` | /np [username] | Returns the song you are currently listening to. |
| `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. |
| `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn |
| `imdb.lua` | /imdb query | Returns film information from IMDb. |
| `hearthstone.lua` | /hearthstone query | Returns data for Hearthstone cards matching the query. | /hs |
| `calc.lua` | /calc expression | Returns conversions and solutions to math expressions. |
| `bible.lua` | /bible reference | Returns a Bible verse. | /b |
| `urbandictionary.lua` | /urban query | Returns the top definition from Urban Dictionary. | /ud |
| `time.lua` | /time query | Returns the time, date, and a timezone for a location. |
| `weather.lua` | /weather query | Returns current weather conditions for a given location. |
| `nick.lua` | /nick nickname | Set your nickname. /nick - will delete it. |
| `whoami.lua` | /whoami | Returns user and chat info for you or the replied-to user. | /who |
| `eightball.lua` | /8ball | Returns an answer from a magic 8-ball. |
| `dice.lua` | /roll nDr | Returns RNG dice rolls. Uses D&D notation. |
| `reddit.lua` | /reddit [r/subreddit ¦ query] | Returns the top results from a subreddit, query, or r/all. | /r |
| `xkcd.lua` | /xkcd [query] | Returns an xkcd strip and its alt text. |
| `slap.lua` | /slap target | Gives someone a slap (or worse). |
| `commit.lua` | /commit | Returns a commit message from whatthecommit.com. |
| `fortune.lua` | /fortune | Returns a UNIX fortune. |
| `pun.lua` | /pun | Returns a pun. |
| `pokedex.lua` | /pokedex query | Returns a Pokedex entry. | /dex |
| `currency.lua` | /cash [amount] cur to cur | Converts one currency to another. |
| `cats.lua` | /cat | Returns a cat picture. |
| `reactions.lua` | /reactions | Returns a list of emoticons which can be posted by the bot. |
| `apod.lua` | /apod [date] | Returns the NASA Astronomy Picture of the Day. |
| `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. |
| `patterns.lua` | /s/from/to/ | Search-and-replace using Lua patterns. |
| `me.lua` | /me | Returns user-specific data stored by the bot. |
| `remind.lua` | /remind <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. |
* * *
| Plugin | Command | Function | Aliases |
|:----------------------|:------------------------------|:----------------------------------------------------------|:--------|
| `help.lua` | /help [command] | Returns a list of commands or command-specific help. | /h |
| `about.lua` | /about | Returns the about text as configured in config.lua. |
| `ping.lua` | /ping | The simplest plugin ever! |
| `echo.lua` | /echo text | Repeats a string of text. |
| `bing.lua` | /bing query | Returns Bing web results. | /g |
| `gImages.lua` | /images query | Returns a Google image result. | /i |
| `gMaps.lua` | /location query | Returns location data from Google Maps. | /loc |
| `youtube.lua` | /youtube query | Returns the top video result from YouTube. | /yt |
| `wikipedia.lua` | /wikipedia query | Returns the summary of a Wikipedia article. | /w |
| `lastfm.lua` | /np [username] | Returns the song you are currently listening to. |
| `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. |
| `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn |
| `imdb.lua` | /imdb query | Returns film information from IMDb. |
| `hearthstone.lua` | /hearthstone query | Returns data for Hearthstone cards matching the query. | /hs |
| `calc.lua` | /calc expression | Returns conversions and solutions to math expressions. |
| `bible.lua` | /bible reference | Returns a Bible verse. | /b |
| `urbandictionary.lua` | /urban query | Returns the top definition from Urban Dictionary. | /ud |
| `time.lua` | /time query | Returns the time, date, and a timezone for a location. |
| `weather.lua` | /weather query | Returns current weather conditions for a given location. |
| `nick.lua` | /nick nickname | Set your nickname. /nick - will delete it. |
| `whoami.lua` | /whoami | Returns user and chat info for you or the replied-to user. | /who |
| `eightball.lua` | /8ball | Returns an answer from a magic 8-ball. |
| `dice.lua` | /roll nDr | Returns RNG dice rolls. Uses D&D notation. |
| `reddit.lua` | /reddit [r/subreddit ¦ query] | Returns the top results from a subreddit, query, or r/all. | /r |
| `xkcd.lua` | /xkcd [query] | Returns an xkcd strip and its alt text. |
| `slap.lua` | /slap target | Gives someone a slap (or worse). |
| `commit.lua` | /commit | Returns a commit message from whatthecommit.com. |
| `fortune.lua` | /fortune | Returns a UNIX fortune. |
| `pun.lua` | /pun | Returns a pun. |
| `pokedex.lua` | /pokedex query | Returns a Pokedex entry. | /dex |
| `currency.lua` | /cash [amount] cur to cur | Converts one currency to another. |
| `cats.lua` | /cat | Returns a cat picture. |
| `reactions.lua` | /reactions | Returns a list of emoticons which can be posted by the bot. |
| `apod.lua` | /apod [date] | Returns the NASA Astronomy Picture of the Day. |
| `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. |
| `patterns.lua` | /s/from/to/ | Search-and-replace using Lua patterns. |
| `me.lua` | /me | Returns user-specific data stored by the bot. |
| `remind.lua` | /remind <duration> <message> | Reminds a user of something after a duration of minutes. |
| `channel.lua` | /ch <channel> \n <message> | Sends a markdown-enabled message to a channel. |
| `isup.lua` | /isup <url> | Returns the status of a website. |
| `starwars-crawl.lua` | /sw <title | number> | Returns the opening crawl from the specified Star Wars film. | /sw |
| `chuckfact.lua` | /chuck | Returns a fact about Chuck Norris. | /cn |
| `catfact.lua` | /catfact | Returns a fact about cats. |
## Plugins
otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot).
@ -204,29 +258,31 @@ Most plugins are intended for public use, but a few are for other purposes, like
There are five standard plugin components.
| Component | Description |
|:-----------|:-----------------------------------------------------|
| `action` | Main function. Expects `msg` table as an argument. |
| `triggers` | Table of triggers for the plugin. Uses Lua patterns. |
| `init` | Optional function run when the plugin is loaded. |
| `cron` | Optional function to be called every minute. |
| `command` | Basic command and syntax. Listed in the help text. |
| `doc` | Usage for the plugin. Returned by "/help $command". |
| `error` | Plugin-specific error message; false for no message. |
| Component | Description |
|:------------|:---------------------------------------------------------------|
| `action` | Main function. Expects `msg` table as an argument. |
| `triggers` | Table of triggers for the plugin. Uses Lua patterns. |
| `init` | Optional function run when the plugin is loaded. |
| `cron` | Optional function to be called every minute. |
| `command` | Basic command and syntax. Listed in the help text. |
| `doc` | Usage for the plugin. Returned by "/help $command". |
| `error` | Plugin-specific error message; false for no message. |
| `panoptic` | True if plugin should see all messages. (See below.) |
| `help_word` | Keyword for command-specific help. Generated if absent. |
No component is required, but some depend on others. For example, `action` will never be run if there's no `triggers`, and `doc` will never be seen if there's no `command`.
Return values from `action` are optional, but they do affect the flow. If it returns a table, that table will become `msg`, and `on_msg_receive` will continue with that. If it returns `true`, it will continue with the current `msg`.
If a plugin's `action` returns `true`, `on_msg_receive` will continue its loop.
When an action or cron function fails, the exception is caught and passed to the `handle_exception` utilty and is either printed to the console or send to the chat/channel defined in `log_chat` in config.lua.
The `panoptic` value is a boolean (or nil; its absence means false) to state whether the plugin should be included in the `panoptic_plugins` table. Plugins in this table are the only plugins whose triggers are checked against a message's text if that message is forwarded or from a blacklisted user.
Interactions with the bot API are straightforward. See the [Bindings section](#bindings) for details.
Several functions used in multiple plugins are defined in utilities.lua. Refer to that file for usage and documentation.
* * *
## Bindings
Calls to the Telegram bot API are performed with the `bindings.lua` file through the multipart-post library. otouto's bindings file supports all standard API methods and all arguments. Its main function, `bindings.request`, accepts four arguments: `self`, `method`, `parameters`, `file`. (At the very least, `self` should be a table containing `BASE_URL`, which is bot's API endpoint, ending with a slash, eg `https://api.telegram.org/bot123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ987654321/`.)
@ -279,8 +335,6 @@ bindings.sendPhoto(self, { chat_id = 987654321, photo = 'ABCDEFGHIJKLMNOPQRSTUVW
Upon success, bindings will return the deserialized result from the API. Upon failure, it will return false and the result. In the case of a connection error, it will return two false values. If an invalid method name is given, bindings will throw an exception. This is to mimic the behavior of more conventional bindings as well as to prevent "silent errors".
* * *
## Database
otouto doesn't use one. This isn't because of dedication to lightweightedness or some clever design choice. Interfacing with databases through Lua is never a simple, easy-to-learn process. As one of the goals of otouto is that it should be a bot which is easy to write plugins for, our approach to storing data is to treat our datastore like any ordinary Lua data structure. The "database" is a table accessible in the `database` value of the bot instance (usually `self.database`), and is saved as a JSON-encoded plaintext file each hour, or when the bot is told to halt. This way, keeping and interacting with persistent data is no different than interacting with a Lua table -- with one exception: Keys in tables used as associative arrays must not be numbers. If the index keys are too sparse, the JSON encoder/decoder will either change them to keys or throw an error.
@ -313,8 +367,6 @@ Alone, the database will have this structure:
Data from other plugins is usually saved in a table with the same name of that plugin. For example, administration.lua stores data in `database.administration`.
* * *
## Output style
otouto plugins should maintain a consistent visual style in their output. This provides a recognizable and comfortable user experience.
@ -323,7 +375,7 @@ Title lines should be **bold**, including any names and trailing punctuation (su
> **Star Wars: Episode IV - A New Hope (1977)**
>
> **Search results for** _star wars_ **:**
> **Search results for** _star wars_**:**
>
> **Changelog for otouto (**[Github](http://github.com/topkecleon/otouto)**):**
@ -346,8 +398,6 @@ Always name your links. Even then, use them with discretion. Excessive links mak
### Other Stuff
User IDs should appear within brackets, monospaced (`[123456789]`). Descriptions and information should be in plain text, but "flavor" text should be italic. The standard size for arbitrary lists (such as search results) is eight within a private conversation and four elsewhere. This is a trivial pair of numbers (leftover from the deprecated Google search API), but consistency is noticeable and desirable.
* * *
## Contributors
Everybody is free to contribute to otouto. If you are interested, you are invited to [fork the repo](http://github.com/topkecleon/otouto/fork) and start making pull requests. If you have an idea and you are not sure how to implement it, open an issue or bring it up in the [Bot Development group](http://telegram.me/BotDevelopment).

View File

@ -1,9 +1,10 @@
-- For details on configuration values, see README.md#configuration.
return {
-- Your authorization token from the botfather.
bot_api_key = '',
bot_api_key = nil,
-- Your Telegram ID.
admin = 00000000,
admin = nil,
-- Two-letter language code.
lang = 'en',
-- The channel, group, or user to send error reports to.
@ -12,74 +13,144 @@ return {
-- The port used to communicate with tg for administration.lua.
-- If you change this, make sure you also modify launch-tg.sh.
cli_port = 4567,
-- The block of text returned by /start.
-- The symbol that starts a command. Usually noted as '/' in documentation.
cmd_pat = '/',
-- If drua is used, should a user be blocked when he's blacklisted?
drua_block_on_blacklist = false,
-- The filename of the database. If left nil, defaults to $username.db.
database_name = nil,
-- The block of text returned by /start and /about..
about_text = [[
I am otouto, the plugin-wielding, multipurpose Telegram bot.
Send /help to get started.
]],
-- The symbol that starts a command. Usually noted as '/' in documentation.
cmd_pat = '/',
-- If drua is used, should a user be blocked when he's blacklisted? (and vice-versa)
drua_block_on_blacklist = false,
-- https://datamarket.azure.com/dataset/bing/search
bing_api_key = '',
-- http://console.developers.google.com
google_api_key = '',
-- https://cse.google.com/cse
google_cse_key = '',
-- http://openweathermap.org/appid
owm_api_key = '',
-- http://last.fm/api
lastfm_api_key = '',
-- http://api.biblia.com
biblia_api_key = '',
-- http://thecatapi.com/docs.html
thecatapi_key = '',
-- http://api.nasa.gov
nasa_api_key = '',
-- http://tech.yandex.com/keys/get
yandex_key = '',
-- http://developer.simsimi.com/signUp
simsimi_key = '',
simsimi_trial = true,
errors = { -- Generic error messages.
generic = 'An unexpected error occurred.',
connection = 'Connection error.',
results = 'No results found.',
argument = 'Invalid argument.',
syntax = 'Invalid syntax.',
chatter_connection = 'I don\'t feel like talking right now.',
chatter_response = 'I don\'t know what to say to that.'
syntax = 'Invalid syntax.'
},
-- https://datamarket.azure.com/dataset/bing/search
bing_api_key = nil,
-- http://console.developers.google.com
google_api_key = nil,
-- https://cse.google.com/cse
google_cse_key = nil,
-- http://openweathermap.org/appid
owm_api_key = nil,
-- http://last.fm/api
lastfm_api_key = nil,
-- http://api.biblia.com
biblia_api_key = nil,
-- http://thecatapi.com/docs.html
thecatapi_key = nil,
-- http://api.nasa.gov
nasa_api_key = nil,
-- http://tech.yandex.com/keys/get
yandex_key = nil,
-- Interval (in minutes) for hackernews.lua to update.
hackernews_interval = 60,
-- Whether hackernews.lua should update at load/reload.
hackernews_onstart = false,
-- Whether luarun should use serpent instead of dkjson for serialization.
luarun_serpent = false,
remind = {
persist = true,
max_length = 1000,
max_duration = 526000,
max_reminders_group = 10,
max_reminders_private = 50
},
chatter = {
cleverbot_api = 'https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text=',
connection = 'I don\'t feel like talking right now.',
response = 'I don\'t know what to say to that.'
},
greetings = {
["Hello, #NAME."] = {
"hello",
"hey",
"hi",
"good morning",
"good day",
"good afternoon",
"good evening"
},
["Goodbye, #NAME."] = {
"good%-?bye",
"bye",
"later",
"see ya",
"good night"
},
["Welcome back, #NAME."] = {
"i'm home",
"i'm back"
},
["You're welcome, #NAME."] = {
"thanks",
"thank you"
}
},
reactions = {
['shrug'] = '¯\\_(ツ)_/¯',
['lenny'] = '( ͡° ͜ʖ ͡°)',
['flip'] = '(╯°□°)╯︵ ┻━┻',
['look'] = 'ಠ_ಠ',
['shots'] = 'SHOTS FIRED',
['facepalm'] = '(-‸ლ)'
},
administration = {
-- Whether moderators can set a group's message of the day.
moderator_setmotd = false,
-- Default antiflood values.
antiflood = {
text = 5,
voice = 5,
audio = 5,
contact = 5,
photo = 10,
video = 10,
location = 10,
document = 10,
sticker = 20
}
},
plugins = { -- To enable a plugin, add its name to the list.
'control',
'blacklist',
'about',
'ping',
'whoami',
'nick',
'blacklist',
'calc',
'cats',
'commit',
'control',
'currency',
'dice',
'echo',
'eightball',
'gMaps',
'wikipedia',
'hackernews',
'imdb',
'calc',
'urbandictionary',
'time',
'eightball',
'dice',
'reddit',
'xkcd',
'slap',
'commit',
'nick',
'ping',
'pun',
'currency',
'cats',
'reddit',
'shout',
'slap',
'time',
'urbandictionary',
'whoami',
'wikipedia',
'xkcd',
-- Put new plugins above this line.
'help',
'greetings'

View File

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

View File

@ -1,18 +1,17 @@
local bot = {}
local bindings -- Bot API bindings.
local utilities -- Miscellaneous and shared plugins.
-- Requires are moved to init to allow for reloads.
local bindings -- Load Telegram bindings.
local utilities -- Load miscellaneous and cross-plugin functions.
bot.version = '3.13'
bot.version = '3.12'
function bot:init(config) -- The function run when the bot is started or reloaded.
-- Function to be run on start and reload.
function bot:init(config)
bindings = require('otouto.bindings')
utilities = require('otouto.utilities')
assert(
config.bot_api_key ~= '',
config.bot_api_key,
'You did not set your bot token in the config!'
)
self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/'
@ -25,45 +24,76 @@ function bot:init(config) -- The function run when the bot is started or reloade
self.info = self.info.result
-- Load the "database"! ;)
self.database_name = config.database_name or self.info.username .. '.db'
if not self.database then
self.database = utilities.load_data(self.info.username..'.db')
self.database = utilities.load_data(self.database_name)
end
-- Migration code 1.12 -> 1.13
-- Back to administration global ban list; copy over current blacklist.
if self.database.version ~= '3.13' then
if self.database.administration then
self.database.administration.globalbans = self.database.administration.globalbans or self.database.blacklist or {}
utilities.save_data(self.database_name, self.database)
self.database = utilities.load_data(self.database_name)
end
end
-- End migration code.
-- Table to cache user info (usernames, IDs, etc).
self.database.users = self.database.users or {}
-- Table to store userdata (nicknames, lastfm usernames, etc).
self.database.userdata = self.database.userdata or {}
-- Table to store the IDs of blacklisted users.
self.database.blacklist = self.database.blacklist or {}
-- Save the bot's version in the database to make migration simpler.
self.database.version = bot.version
-- Add updated bot info to the user info cache.
self.database.users[tostring(self.info.id)] = self.info
self.plugins = {} -- Load plugins.
for _,v in ipairs(config.plugins) do
local p = require('otouto.plugins.'..v)
table.insert(self.plugins, p)
if p.init then p.init(self, config) end
if p.doc then p.doc = '```\n'..p.doc..'\n```' end
-- All plugins go into self.plugins. Plugins which accept forwarded messages
-- and messages from blacklisted users also go into self.panoptic_plugins.
self.plugins = {}
self.panoptic_plugins = {}
local t = {} -- Petty pseudo-optimization.
for _, pname in ipairs(config.plugins) do
local plugin = require('otouto.plugins.'..pname)
table.insert(self.plugins, plugin)
if plugin.init then plugin.init(self, config) end
if plugin.panoptic then table.insert(self.panoptic_plugins, plugin) end
if plugin.doc then plugin.doc = '```\n'..plugin.doc..'\n```' end
if not plugin.triggers then plugin.triggers = t end
end
print('@' .. self.info.username .. ', AKA ' .. self.info.first_name ..' ('..self.info.id..')')
self.last_update = self.last_update or 0 -- Set loop variables: Update offset,
self.last_cron = self.last_cron or os.date('%M') -- the time of the last cron job,
self.last_database_save = self.last_database_save or os.date('%H') -- the time of the last database save,
self.is_started = true -- and whether or not the bot should be running.
-- Set loop variables.
self.last_update = self.last_update or 0 -- Update offset.
self.last_cron = self.last_cron or os.date('%M') -- Last cron job.
self.last_database_save = self.last_database_save or os.date('%H') -- Last db save.
self.is_started = true
end
function bot:on_msg_receive(msg, config) -- The fn run whenever a message is received.
-- Function to be run on each new message.
function bot:on_msg_receive(msg, config)
if msg.date < os.time() - 5 then return end -- Do not process old messages.
-- Do not process old messages.
if msg.date < os.time() - 5 then return end
-- plugint is the array of plugins we'll check the message against.
-- If the message is forwarded or from a blacklisted user, the bot will only
-- check against panoptic plugins.
local plugint = self.plugins
local from_id_str = tostring(msg.from.id)
-- Cache user info for those involved.
self.database.users[tostring(msg.from.id)] = msg.from
self.database.users[from_id_str] = msg.from
if msg.reply_to_message then
self.database.users[tostring(msg.reply_to_message.from.id)] = msg.reply_to_message.from
elseif msg.forward_from then
-- Forwards only go to panoptic plugins.
plugint = self.panoptic_plugins
self.database.users[tostring(msg.forward_from.id)] = msg.forward_from
elseif msg.new_chat_member then
self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member
@ -71,9 +101,14 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec
self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member
end
-- Messages from blacklisted users only go to panoptic plugins.
if self.database.blacklist[from_id_str] then
plugint = self.panoptic_plugins
end
-- If no text, use captions.
msg.text = msg.text or msg.caption or ''
msg.text_lower = msg.text:lower()
if msg.reply_to_message then
msg.reply_to_message.text = msg.reply_to_message.text or msg.reply_to_message.caption or ''
end
@ -84,8 +119,11 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec
msg.text_lower = msg.text:lower()
end
for _, plugin in ipairs(self.plugins) do
for _, trigger in ipairs(plugin.triggers or {}) do
-- If the message is forwarded or comes from a blacklisted yser,
-- Do the thing.
for _, plugin in ipairs(plugint) do
for _, trigger in ipairs(plugin.triggers) do
if string.match(msg.text_lower, trigger) then
local success, result = pcall(function()
return plugin.action(self, msg, config)
@ -100,29 +138,29 @@ function bot:on_msg_receive(msg, config) -- The fn run whenever a message is rec
utilities.send_reply(self, msg, config.errors.generic)
end
utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config)
msg = nil
return
end
-- If the action returns a table, make that table the new msg.
if type(result) == 'table' then
msg = result
-- If the action returns true, continue.
-- Continue if the return value is true.
elseif result ~= true then
msg = nil
return
end
end
end
end
msg = nil
end
-- main
function bot:run(config)
bot.init(self, config) -- Actually start the script.
while self.is_started do -- Start a loop while the bot should be running.
local res = bindings.getUpdates(self, { timeout=20, offset = self.last_update+1 } )
bot.init(self, config)
while self.is_started do
-- Update loop.
local res = bindings.getUpdates(self, { timeout = 20, offset = self.last_update + 1 } )
if res then
for _,v in ipairs(res.result) do -- Go through every new message.
-- Iterate over every new message.
for _,v in ipairs(res.result) do
self.last_update = v.update_id
if v.message then
bot.on_msg_receive(self, v.message, config)
@ -132,7 +170,8 @@ function bot:run(config)
print('Connection error while fetching updates.')
end
if self.last_cron ~= os.date('%M') then -- Run cron jobs every minute.
-- Run cron jobs every minute.
if self.last_cron ~= os.date('%M') then
self.last_cron = os.date('%M')
for i,v in ipairs(self.plugins) do
if v.cron then -- Call each plugin's cron function, if it has one.
@ -144,15 +183,14 @@ function bot:run(config)
end
end
-- Save the "database" every hour.
if self.last_database_save ~= os.date('%H') then
utilities.save_data(self.info.username..'.db', self.database) -- Save the database.
self.last_database_save = os.date('%H')
utilities.save_data(self.database_name, self.database)
end
end
-- Save the database before exiting.
utilities.save_data(self.info.username..'.db', self.database)
utilities.save_data(self.database_name, self.database)
print('Halted.')
end

View File

@ -1,36 +1,19 @@
local about = {}
local bot = require('otouto.bot')
local utilities = require('otouto.utilities')
local about = {}
about.command = 'about'
about.doc = 'Returns information about the bot.'
about.triggers = {
''
}
function about:init(config)
about.text = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.'
about.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('about'):t('start').table
end
function about:action(msg, config)
-- Filthy hack, but here is where we'll stop forwarded messages from hitting
-- other plugins.
if msg.forward_from then return end
local output = config.about_text .. '\nBased on [otouto](http://github.com/topkecleon/otouto) v'..bot.version..' by topkecleon.'
if
(msg.new_chat_member and msg.new_chat_member.id == self.info.id)
or msg.text_lower:match('^'..config.cmd_pat..'about$')
or msg.text_lower:match('^'..config.cmd_pat..'about@'..self.info.username:lower()..'$')
or msg.text_lower:match('^'..config.cmd_pat..'start$')
or msg.text_lower:match('^'..config.cmd_pat..'start@'..self.info.username:lower()..'$')
then
utilities.send_message(self, msg.chat.id, output, true, nil, true)
return
end
return true
utilities.send_message(self, msg.chat.id, about.text, true, nil, true)
end
return about

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 utilities = require('otouto.utilities')
local bindings = require('otouto.bindings')
local http = require('socket.http')
commit.command = 'commit'
commit.doc = 'Returns a commit message from whatthecommit.com.'
@ -11,420 +11,16 @@ function commit:init(config)
commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table
end
local commits = {
"One does not simply merge into master",
"Merging the merge",
"Another bug bites the dust",
"de-misunderestimating",
"Some shit.",
"add actual words",
"I CAN HAZ COMMENTZ.",
"giggle.",
"Whatever.",
"Finished fondling.",
"FONDLED THE CODE",
"this is how we generate our shit.",
"unh",
"It works!",
"unionfind is no longer being molested.",
"Well, it's doing something.",
"I'M PUSHING.",
"Whee.",
"Whee, good night.",
"It'd be nice if type errors caused the compiler to issue a type error",
"Fucking templates.",
"I hate this fucking language.",
"marks",
"that coulda been bad",
"hoo boy",
"It was the best of times, it was the worst of times",
"Fucking egotistical bastard. adds expandtab to vimrc",
"if you're not using et, fuck off",
"WHO THE FUCK CAME UP WITH MAKE?",
"This is a basic implementation that works.",
"By works, I meant 'doesnt work'. Works now..",
"Last time I said it works? I was kidding. Try this.",
"Just stop reading these for a while, ok..",
"Give me a break, it's 2am. But it works now.",
"Make that it works in 90% of the cases. 3:30.",
"Ok, 5am, it works. For real.",
"FOR REAL.",
"I don't know what these changes are supposed to accomplish but somebody told me to make them.",
"I don't get paid enough for this shit.",
"fix some fucking errors",
"first blush",
"So my boss wanted this button ...",
"uhhhhhh",
"forgot we're not using a smart language",
"include shit",
"To those I leave behind, good luck!",
"things occurred",
"i dunno, maybe this works",
"8==========D",
"No changes made",
"whooooooooooooooooooooooooooo",
"clarify further the brokenness of C++. why the fuck are we using C++?",
".",
"Friday 5pm",
"changes",
"A fix I believe, not like I tested or anything",
"Useful text",
"pgsql is being a pain",
"pgsql is more strict, increase the hackiness up to 11",
"c&p fail",
"syntax",
"fix",
"just shoot me",
"arrrggghhhhh fixed!",
"someone fails and it isn't me",
"totally more readable",
"better grepping",
"fix",
"fix bug, for realz",
"fix /sigh",
"Does this work",
"MOAR BIFURCATION",
"bifurcation",
"REALLY FUCKING FIXED",
"FIX",
"better ignores",
"More ignore",
"more ignores",
"more ignores",
"more ignores",
"more ignores",
"more ignores",
"more ignored words",
"more fixes",
"really ignore ignored worsd",
"fixes",
"/sigh",
"fix",
"fail",
"pointless limitation",
"omg what have I done?",
"added super-widget 2.0.",
"tagging release w.t.f.",
"I can't believe it took so long to fix this.",
"I must have been drunk.",
"This is why the cat shouldn't sit on my keyboard.",
"This is why git rebase is a horrible horrible thing.",
"ajax-loader hotness, oh yeah",
"small is a real HTML tag, who knew.",
"WTF is this.",
"Do things better, faster, stronger",
"Use a real JS construct, WTF knows why this works in chromium.",
"Added a banner to the default admin page. Please have mercy on me =(",
"needs more cow bell",
"Switched off unit test X because the build had to go out now and there was no time to fix it properly.",
"Updated",
"I must sleep... it's working... in just three hours...",
"I was wrong...",
"Completed with no bugs...",
"Fixed a little bug...",
"Fixed a bug in NoteLineCount... not seriously...",
"woa!! this one was really HARD!",
"Made it to compile...",
"changed things...",
"touched...",
"i think i fixed a bug...",
"perfect...",
"Moved something to somewhere... goodnight...",
"oops, forgot to add the file",
"Corrected mistakes",
"oops",
"oops!",
"put code that worked where the code that didn't used to be",
"Nothing to see here, move along",
"I am even stupider than I thought",
"I don't know what the hell I was thinking.",
"fixed errors in the previous commit",
"Committed some changes",
"Some bugs fixed",
"Minor updates",
"Added missing file in previous commit",
"bug fix",
"typo",
"bara bra grejjor",
"Continued development...",
"Does anyone read this? I'll be at the coffee shop accross the street.",
"That's just how I roll",
"work in progress",
"minor changes",
"some brief changes",
"assorted changes",
"lots and lots of changes",
"another big bag of changes",
"lots of changes after a lot of time",
"LOTS of changes. period",
"Test commit. Please ignore",
"I'm just a grunt. Don't blame me for this awful PoS.",
"I did it for the lulz!",
"I'll explain this when I'm sober .. or revert it",
"Obligatory placeholder commit message",
"A long time ago, in a galaxy far far away...",
"Fixed the build.",
"various changes",
"One more time, but with feeling.",
"Handled a particular error.",
"Fixed unnecessary bug.",
"Removed code.",
"Added translation.",
"Updated build targets.",
"Refactored configuration.",
"Locating the required gigapixels to render...",
"Spinning up the hamster...",
"Shovelling coal into the server...",
"Programming the flux capacitor",
"The last time I tried this the monkey didn't survive. Let's hope it works better this time.",
"I should have had a V8 this morning.",
"640K ought to be enough for anybody",
"pay no attention to the man behind the curtain",
"a few bits tried to escape, but we caught them",
"Who has two thumbs and remembers the rudiments of his linear algebra courses? Apparently, this guy.",
"workaround for ant being a pile of fail",
"Don't push this commit",
"rats",
"squash me",
"fixed mistaken bug",
"Final commit, ready for tagging",
"-m \'So I hear you like commits ...\'",
"epic",
"need another beer",
"Well the book was obviously wrong.",
"lolwhat?",
"Another commit to keep my CAN streak going.",
"I cannot believe that it took this long to write a test for this.",
"TDD: 1, Me: 0",
"Yes, I was being sarcastic.",
"Apparently works-for-me is a crappy excuse.",
"tl;dr",
"I would rather be playing SC2.",
"Crap. Tonight is raid night and I am already late.",
"I know what I am doing. Trust me.",
"You should have trusted me.",
"Is there an award for this?",
"Is there an achievement for this?",
"I'm totally adding this to epic win. +300",
"This really should not take 19 minutes to build.",
"fixed the israeli-palestinian conflict",
"SHIT ===> GOLD",
"Committing in accordance with the prophecy.",
"It compiles! Ship it!",
"LOL!",
"Reticulating splines...",
"SEXY RUSSIAN CODES WAITING FOR YOU TO CALL",
"s/import/include/",
"extra debug for stuff module",
"debug line test",
"debugo",
"remove debug<br/>all good",
"debug suff",
"more debug... who overwrote!",
"these confounded tests drive me nuts",
"For great justice.",
"QuickFix.",
"oops - thought I got that one.",
"removed echo and die statements, lolz.",
"somebody keeps erasing my changes.",
"doh.",
"pam anderson is going to love me.",
"added security.",
"arrgghh... damn this thing for not working.",
"jobs... steve jobs",
"and a comma",
"this is my quickfix branch and i will use to do my quickfixes",
"Fix my stupidness",
"and so the crazy refactoring process sees the sunlight after some months in the dark!",
"gave up and used tables.",
"[Insert your commit message here. Be sure to make it descriptive.]",
"Removed test case since code didn't pass QA",
"removed tests since i can't make them green",
"stuff",
"more stuff",
"Become a programmer, they said. It'll be fun, they said.",
"Same as last commit with changes",
"foo",
"just checking if git is working properly...",
"fixed some minor stuff, might need some additional work.",
"just trolling the repo",
"All your codebase are belong to us.",
"Somebody set up us the bomb.",
"should work I guess...",
"To be honest, I do not quite remember everything I changed here today. But it is all good, I tell ya.",
"well crap.",
"herpderp (redux)",
"herpderp",
"Derp",
"derpherp",
"Herping the derp",
"sometimes you just herp the derp so hard it herpderps",
"Derp. Fix missing constant post rename",
"Herping the fucking derp right here and now.",
"Derp, asset redirection in dev mode",
"mergederp",
"Derp search/replace fuckup",
"Herpy dooves.",
"Derpy hooves",
"derp, helper method rename",
"Herping the derp derp (silly scoping error)",
"Herp derp I left the debug in there and forgot to reset errors.",
"Reset error count between rows. herpderp",
"hey, what's that over there?!",
"hey, look over there!",
"It worked for me...",
"Does not work.",
"Either Hot Shit or Total Bollocks",
"Arrrrgggg",
"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)
local output = '`'..commits[math.random(#commits)]..'`'
utilities.send_message(self, msg.chat.id, output, true, nil, true)
bindings.request(
self,
'sendMessage',
{
chat_id = msg.chat.id,
text = '```\n' .. (http.request('http://whatthecommit.com/index.txt')) .. '\n```',
parse_mode = 'Markdown'
}
)
end
return commit

View File

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

View File

@ -6,11 +6,10 @@ local utilities = require('otouto.utilities')
function fortune:init(config)
local s = io.popen('fortune'):read('*all')
if s:match('not found$') then
print('fortune is not installed on this computer.')
print('fortune.lua will not be enabled.')
return
end
assert(
not s:match('not found$'),
'fortune.lua requires the fortune program to be installed.'
)
fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table
end

View File

@ -9,37 +9,28 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities')
function gImages:init(config)
if not config.google_api_key then
print('Missing config value: google_api_key.')
print('gImages.lua will not be enabled.')
return
elseif not config.google_cse_key then
print('Missing config value: google_cse_key.')
print('gImages.lua will not be enabled.')
return
end
assert(config.google_api_key and config.google_cse_key,
'gImages.lua requires a Google API key from http://console.developers.google.com and a Google Custom Search Engine key from http://cse.google.com/cse.'
)
gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('image', true):t('i', true):t('insfw', true).table
gImages.doc = config.cmd_pat .. [[image <query>
Returns a randomized top result from Google Images. Safe search is enabled by default; use "]] .. config.cmd_pat .. [[insfw" to disable it. NSFW results will not display an image preview.
Alias: ]] .. config.cmd_pat .. 'i'
gImages.search_url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key
end
gImages.command = 'image <query>'
function gImages:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, gImages.doc, true, msg.message_id, true)
return
end
utilities.send_reply(self, msg, gImages.doc, true)
return
end
local url = 'https://www.googleapis.com/customsearch/v1?&searchType=image&imgSize=xlarge&alt=json&num=8&start=1&key=' .. config.google_api_key .. '&cx=' .. config.google_cse_key
local url = gImages.search_url
if not string.match(msg.text, '^'..config.cmd_pat..'i[mage]*nsfw') then
url = url .. '&safe=high'

View File

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

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.
-- If you want to configure your own greetings, copy the following table
-- (without the "config.") to your config.lua file.
local utilities = require('otouto.utilities')
local greetings = {}
local utilities = require('otouto.utilities')
function greetings:init(config)
config.greetings = config.greetings or {
['Hello, #NAME.'] = {
'hello',
'hey',
'sup',
'hi',
'good morning',
'good day',
'good afternoon',
'good evening'
},
['Goodbye, #NAME.'] = {
'bye',
'later',
'see ya',
'good night'
},
['Welcome back, #NAME.'] = {
'i\'m home',
'i\'m back'
},
['You\'re welcome, #NAME.'] = {
'thanks',
'thank you'
}
}
greetings.triggers = {
self.info.first_name:lower() .. '%p*$'
}
greetings.triggers = {}
for _, triggers in pairs(config.greetings) do
for i = 1, #triggers do
triggers[i] = '^' .. triggers[i] .. ',? ' .. self.info.first_name:lower() .. '%p*$'
table.insert(greetings.triggers, triggers[i])
end
end
end
function greetings:action(msg, config)
local nick = utilities.build_name(msg.from.first_name, msg.from.last_name)
local nick
if self.database.userdata[tostring(msg.from.id)] then
nick = self.database.userdata[tostring(msg.from.id)].nickname or nick
nick = self.database.userdata[tostring(msg.from.id)].nickname
end
nick = nick or utilities.build_name(msg.from.first_name, msg.from.last_name)
for trigger,responses in pairs(config.greetings) do
for _,response in pairs(responses) do
if msg.text_lower:match(response..',? '..self.info.first_name:lower()) then
local output = utilities.char.zwnj .. trigger:gsub('#NAME', nick)
utilities.send_message(self, msg.chat.id, output)
for response, triggers in pairs(config.greetings) do
for _, trigger in pairs(triggers) do
if string.match(msg.text_lower, trigger) then
utilities.send_message(self, msg.chat.id, response:gsub('#NAME', nick))
return
end
end
end
return true
end
return greetings

View File

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

View File

@ -5,45 +5,36 @@ local hearthstone = {}
--local HTTPS = require('ssl.https')
local JSON = require('dkjson')
local utilities = require('otouto.utilities')
local HTTPS = require('ssl.https')
function hearthstone:init(config)
hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table
hearthstone.command = 'hearthstone <query>'
if not self.database.hearthstone or os.time() > self.database.hearthstone.expiration then
print('Downloading Hearthstone database...')
-- This stuff doesn't play well with lua-sec. Disable it for now; hack in curl.
--local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json')
--if res ~= 200 then
-- print('Error connecting to hearthstonejson.com.')
-- print('hearthstone.lua will not be enabled.')
-- return
--end
--local jdat = JSON.decode(jstr)
local s = io.popen('curl -s https://api.hearthstonejson.com/v1/latest/enUS/cards.json'):read('*all')
local d = JSON.decode(s)
if not d then
local jstr, res = HTTPS.request('https://api.hearthstonejson.com/v1/latest/enUS/cards.json')
if not jstr or res ~= 200 then
print('Error connecting to hearthstonejson.com.')
print('hearthstone.lua will not be enabled.')
hearthstone.command = nil
hearthstone.triggers = nil
return
end
self.database.hearthstone = d
self.database.hearthstone = JSON.decode(jstr)
self.database.hearthstone.expiration = os.time() + 600000
print('Download complete! It will be stored for a week.')
end
hearthstone.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hearthstone', true):t('hs').table
hearthstone.doc = config.cmd_pat .. [[hearthstone <query>
Returns Hearthstone card info.
Alias: ]] .. config.cmd_pat .. 'hs'
end
hearthstone.command = 'hearthstone <query>'
local function format_card(card)
local ctype = card.type
@ -102,9 +93,9 @@ end
function hearthstone:action(msg, config)
local input = utilities.input(msg.text_lower)
local input = utilities.input_from_msg(msg)
if not input then
utilities.send_message(self, msg.chat.id, hearthstone.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, hearthstone.doc, true)
return
end

View File

@ -1,46 +1,47 @@
-- This plugin should go at the end of your plugin list in
-- config.lua, but not after greetings.lua.
local utilities = require('otouto.utilities')
local help = {}
local utilities = require('otouto.utilities')
local help_text
function help:init(config)
local commandlist = {}
help_text = '*Available commands:*\n'..config.cmd_pat
for _,plugin in ipairs(self.plugins) do
if plugin.command then
table.insert(commandlist, plugin.command)
if plugin.doc then
plugin.help_word = utilities.get_word(plugin.command, 1)
end
end
end
table.insert(commandlist, 'help [command]')
table.sort(commandlist)
help_text = help_text .. table.concat(commandlist, '\n'..config.cmd_pat) .. '\nArguments: <required> [optional]'
help_text = help_text:gsub('%[', '\\[')
help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('help', true):t('h', true).table
help.command = 'help [command]'
help.doc = config.cmd_pat .. 'help [command] \nReturns usage information for a given command.'
end
function help:action(msg)
function help:action(msg, config)
local input = utilities.input(msg.text_lower)
if input then
if not help.help_word then
for _, plugin in ipairs(self.plugins) do
if plugin.command and plugin.doc and not plugin.help_word then
plugin.help_word = utilities.get_word(plugin.command, 1)
end
end
end
for _,plugin in ipairs(self.plugins) do
if plugin.help_word == input:gsub('^/', '') then
local output = '*Help for* _' .. plugin.help_word .. '_ *:*\n' .. plugin.doc
local output = '*Help for* _' .. plugin.help_word .. '_*:*\n' .. plugin.doc
utilities.send_message(self, msg.chat.id, output, true, nil, true)
return
end
end
utilities.send_reply(self, msg, 'Sorry, there is no help for that command.')
else
-- Generate the help message on first run.
if not help.text then
local commandlist = {}
for _, plugin in ipairs(self.plugins) do
if plugin.command then
table.insert(commandlist, plugin.command)
end
end
table.sort(commandlist)
help.text = '*Available commands:*\n' .. config.cmd_pat .. table.concat(commandlist, '\n'..config.cmd_pat) .. '\nArguments: <required> [optional]'
help.text = help.text:gsub('%[', '\\[')
end
-- Attempt to send the help message via PM.
-- If msg is from a group, tell the group whether the PM was successful.
local res = utilities.send_message(self, msg.from.id, help_text, true, nil, true)
local res = utilities.send_message(self, msg.from.id, help.text, true, nil, true)
if not res then
utilities.send_reply(self, msg, 'Please [message me privately](http://telegram.me/' .. self.info.username .. '?start=help) for a list of commands.', true)
elseif msg.chat.type ~= 'private' then

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)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, imdb.doc, true, msg.message_id, true)
return
end
utilities.send_reply(self, msg, imdb.doc, true)
return
end
local url = 'http://www.omdbapi.com/?t=' .. URL.escape(input)

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

View File

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

View File

@ -9,33 +9,59 @@ function me:init(config)
end
function me:action(msg, config)
local userdata = self.database.userdata[tostring(msg.from.id)] or {}
local user
if msg.from.id == config.admin then
if msg.reply_to_message then
userdata = self.database.userdata[tostring(msg.reply_to_message.from.id)]
user = msg.reply_to_message.from
else
local input = utilities.input(msg.text)
if input then
local user_id = utilities.id_from_username(self, input)
if user_id then
userdata = self.database.userdata[tostring(user_id)] or {}
if tonumber(input) then
user = self.database.users[input]
if not user then
utilities.send_reply(self, msg, 'Unrecognized ID.')
return
end
elseif input:match('^@') then
user = utilities.resolve_username(self, input)
if not user then
utilities.send_reply(self, msg, 'Unrecognized username.')
return
end
else
utilities.send_reply(self, msg, 'Invalid username or ID.')
return
end
end
end
end
user = user or msg.from
local userdata = self.database.userdata[tostring(user.id)] or {}
local output = ''
local data = {}
for k,v in pairs(userdata) do
output = output .. '*' .. k .. ':* `' .. tostring(v) .. '`\n'
table.insert(data, string.format(
'<b>%s</b> <code>%s</code>\n',
utilities.html_escape(k),
utilities.html_escape(v)
))
end
if output == '' then
local output
if #data == 0 then
output = 'There is no data stored for this user.'
else
output = string.format(
'<b>%s</b> <code>[%s]</code><b>:</b>\n',
utilities.html_escape(utilities.build_name(
user.first_name,
user.last_name
)),
user.id
) .. table.concat(data)
end
utilities.send_message(self, msg.chat.id, output, true, nil, true)
utilities.send_message(self, msg.chat.id, output, true, nil, 'html')
end

View File

@ -1,10 +1,18 @@
local patterns = {}
local utilities = require('otouto.utilities')
patterns.triggers = {
'^/?s/.-/.-$'
}
local patterns = {}
patterns.command = 's/<pattern>/<substitution>'
patterns.help_word = 'sed'
patterns.doc = [[
s/<pattern>/<substitution>
Replace all matches for the given pattern.
Uses Lua patterns.
]]
function patterns:init(config)
patterns.triggers = { config.cmd_pat .. '?s/.-/.-$' }
end
function patterns:action(msg)
if not msg.reply_to_message then return true end
@ -24,8 +32,8 @@ function patterns:action(msg)
if res == false then
utilities.send_reply(self, msg, 'Malformed pattern!')
else
output = output:sub(1, 4000)
output = '*Did you mean:*\n"' .. utilities.md_escape(utilities.trim(output)) .. '"'
output = utilities.trim(output:sub(1, 4000))
output = utilities.style.enquote('Did you mean', output)
utilities.send_reply(self, msg.reply_to_message, output, true)
end
end

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

View File

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

View File

@ -8,7 +8,8 @@ pokemon_go.command = 'pokego <team>'
function pokemon_go:init(config)
pokemon_go.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('pokego', true):t('pokégo', true)
:t('pokemongo', true):t('pokémongo', true).table
:t('pokemongo', true):t('pokémongo', true)
:t('pogo', true):t('mongo', true).table
pokemon_go.doc = config.cmd_pat .. [[pokego <team>
Set your Pokémon Go team for statistical purposes. The team must be valid, and can be referred to by name or color (or the first letter of either). Giving no team name will show statistics.]]
local db = self.database.pokemon_go

View File

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

View File

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

View File

@ -8,87 +8,83 @@ function remind:init(config)
self.database.reminders = self.database.reminders or {}
remind.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('remind', true).table
remind.doc = config.cmd_pat .. 'remind <duration> <message> \nRepeats a message after a duration of time, in minutes.'
config.remind = config.remind or {}
setmetatable(config.remind, { __index = function() return 1000 end })
remind.doc = config.cmd_pat .. [[remind <duration> <message>
Repeats a message after a duration of time, in minutes.
The maximum length of a reminder is %s characters. The maximum duration of a timer is %s minutes. The maximum number of reminders for a group is %s. The maximum number of reminders in private is %s.]]
remind.doc = remind.doc:format(config.remind.max_length, config.remind.max_duration, config.remind.max_reminders_group, config.remind.max_reminders_private)
end
function remind:action(msg)
-- Ensure there are arguments. If not, send doc.
function remind:action(msg, config)
local input = utilities.input(msg.text)
if not input then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, remind.doc, true)
return
end
-- Ensure first arg is a number. If not, send doc.
local duration = utilities.get_word(input, 1)
if not tonumber(duration) then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true)
local duration = tonumber(utilities.get_word(input, 1))
if not duration then
utilities.send_reply(self, msg, remind.doc, true)
return
end
-- Duration must be between one minute and one year (approximately).
duration = tonumber(duration)
if duration < 1 then
duration = 1
elseif duration > 526000 then
duration = 526000
elseif duration > config.remind.max_duration then
duration = config.remind.max_duration
end
-- Ensure there is a second arg.
local message = utilities.input(input)
if not message then
utilities.send_message(self, msg.chat.id, remind.doc, true, msg.message_id, true)
utilities.send_reply(self, msg, remind.doc, true)
return
end
if #message > config.remind.max_length then
utilities.send_reply(self, msg, 'The maximum length of reminders is ' .. config.remind.max_length .. '.')
return
end
local chat_id_str = tostring(msg.chat.id)
-- Make a database entry for the group/user if one does not exist.
local output
self.database.reminders[chat_id_str] = self.database.reminders[chat_id_str] or {}
-- Limit group reminders to 10 and private reminders to 50.
if msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) > 9 then
utilities.send_reply(self, msg, 'Sorry, this group already has ten reminders.')
return
elseif msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) > 49 then
utilities.send_reply(msg, 'Sorry, you already have fifty reminders.')
return
end
-- Put together the reminder with the expiration, message, and message to reply to.
local reminder = {
time = os.time() + duration * 60,
message = message
}
table.insert(self.database.reminders[chat_id_str], reminder)
local output = 'I will remind you in ' .. duration
if duration == 1 then
output = output .. ' minute!'
if msg.chat.type == 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_private then
output = 'Sorry, you already have the maximum number of reminders.'
elseif msg.chat.type ~= 'private' and utilities.table_size(self.database.reminders[chat_id_str]) >= config.remind.max_reminders_group then
output = 'Sorry, this group already has the maximum number of reminders.'
else
output = output .. ' minutes!'
table.insert(self.database.reminders[chat_id_str], {
time = os.time() + (duration * 60),
message = message
})
output = string.format(
'I will remind you in %s minute%s!',
duration,
duration == 1 and '' or 's'
)
end
utilities.send_reply(self, msg, output)
utilities.send_reply(self, msg, output, true)
end
function remind:cron()
function remind:cron(config)
local time = os.time()
-- Iterate over the group entries in the reminders database.
for chat_id, group in pairs(self.database.reminders) do
local new_group = {}
-- Iterate over each reminder.
for _, reminder in ipairs(group) do
for k, reminder in pairs(group) do
-- If the reminder is past-due, send it and nullify it.
-- Otherwise, add it to the replacement table.
if time > reminder.time then
local output = '*Reminder:*\n"' .. utilities.md_escape(reminder.message) .. '"'
local output = utilities.style.enquote('Reminder', reminder.message)
local res = utilities.send_message(self, chat_id, output, true, nil, true)
-- If the message fails to send, save it for later.
if not res then
table.insert(new_group, reminder)
-- If the message fails to send, save it for later (if enabled in config).
if res or not config.remind.persist then
group[k] = nil
end
else
table.insert(new_group, reminder)
end
end
-- Nullify the original table and replace it with the new one.
self.database.reminders[chat_id] = new_group
-- Nullify the table if it is empty.
if #new_group == 0 then
self.database.reminders[chat_id] = nil
end
end
end

View File

@ -20,7 +20,9 @@ function shell:action(msg, config)
return
end
local output = io.popen(input):read('*all')
local f = io.popen(input)
local output = f:read('*all')
f:close()
if output:len() == 0 then
output = 'Done!'
else

View File

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

View File

@ -109,7 +109,21 @@ local slaps = {
function slap:action(msg)
local input = utilities.input(msg.text)
local victor_id = msg.from.id
local victim_id = utilities.id_from_message(self, msg)
local victim_id
if msg.reply_to_message then
victim_id = msg.reply_to_message.from.id
else
if input then
if tonumber(input) then
victim_id = tonumber(input)
elseif input:match('^@') then
local t = utilities.resolve_username(self, input)
if t then
victim_id = t.id
end
end
end
end
-- IDs
if victim_id then
if victim_id == victor_id then

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

View File

@ -8,42 +8,38 @@ local utilities = require('otouto.utilities')
translate.command = 'translate [text]'
function translate:init(config)
translate.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('translate', true):t('tl', true).table
assert(config.yandex_key,
'translate.lua requires a Yandex translate API key from http://tech.yandex.com/keys/get.'
)
translate.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('translate', true):t('tl', true).table
translate.doc = config.cmd_pat .. [[translate [text]
Translates input or the replied-to message into the bot's language.]]
translate.base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=%s'
end
function translate:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, translate.doc, true, msg.message_id, true)
return
end
utilities.send_reply(self, msg, translate.doc, true)
return
end
local url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=' .. URL.escape(input)
local str, res = HTTPS.request(url)
if res ~= 200 then
local url = translate.base_url:format(URL.escape(input))
local jstr, code = HTTPS.request(url)
if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection)
return
end
local jdat = JSON.decode(str)
if jdat.code ~= 200 then
local data = JSON.decode(jstr)
if data.code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection)
return
end
local output = jdat.text[1]
output = '*Translation:*\n"' .. utilities.md_escape(output) .. '"'
utilities.send_reply(self, msg.reply_to_message or msg, output, true)
utilities.send_reply(self, msg.reply_to_message or msg, utilities.style.enquote('Translation', data.text[1]), true)
end
return translate

View File

@ -6,50 +6,45 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities')
urbandictionary.command = 'urbandictionary <query>'
urbandictionary.base_url = 'http://api.urbandictionary.com/v0/define?term='
function urbandictionary:init(config)
urbandictionary.triggers = utilities.triggers(self.info.username, config.cmd_pat)
:t('urbandictionary', true):t('ud', true):t('urban', true).table
urbandictionary.doc = config.cmd_pat .. [[urbandictionary <query>
urbandictionary.doc = [[
/urbandictionary <query>
Returns a definition from Urban Dictionary.
Aliases: ]] .. config.cmd_pat .. 'ud, ' .. config.cmd_pat .. 'urban'
Aliases: /ud, /urban
]]
urbandictionary.doc = urbandictionary.doc:gsub('/', config.cmd_pat)
end
function urbandictionary:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, urbandictionary.doc, true, msg.message_id, true)
return
end
utilities.send_reply(self, msg, urbandictionary.doc, true)
return
end
local url = 'http://api.urbandictionary.com/v0/define?term=' .. URL.escape(input)
local jstr, res = HTTP.request(url)
if res ~= 200 then
local url = urbandictionary.base_url .. URL.escape(input)
local jstr, code = HTTP.request(url)
if code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection)
return
end
local jdat = JSON.decode(jstr)
if jdat.result_type == "no_results" then
utilities.send_reply(self, msg, config.errors.results)
return
local data = JSON.decode(jstr)
local output
if data.result_type == 'no_results' then
output = config.errors.results
else
output = string.format('*%s*\n\n%s\n\n_%s_',
data.list[1].word:gsub('*', '*\\**'),
utilities.trim(utilities.md_escape(data.list[1].definition)),
utilities.trim((data.list[1].example or '')):gsub('_', '_\\__')
)
end
local output = '*' .. jdat.list[1].word .. '*\n\n' .. utilities.trim(jdat.list[1].definition)
if string.len(jdat.list[1].example) > 0 then
output = output .. '_\n\n' .. utilities.trim(jdat.list[1].example) .. '_'
end
output = output:gsub('%[', ''):gsub('%]', '')
utilities.send_message(self, msg.chat.id, output, true, nil, true)
utilities.send_reply(self, msg, output, true)
end
return urbandictionary

View File

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

View File

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

View File

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

View File

@ -5,52 +5,48 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities')
xkcd.command = 'xkcd [i]'
xkcd.base_url = 'https://xkcd.com/info.0.json'
xkcd.strip_url = 'http://xkcd.com/%s/info.0.json'
function xkcd:init(config)
xkcd.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('xkcd', true).table
xkcd.doc = config.cmd_pat .. [[xkcd [i]
Returns the latest xkcd strip and its alt text. If a number is given, returns that number strip. If "r" is passed in place of a number, returns a random strip.]]
local jstr = HTTP.request(xkcd.base_url)
if jstr then
local data = JSON.decode(jstr)
if data then
xkcd.latest = data.num
end
end
xkcd.latest = xkcd.latest or 1700
end
function xkcd:action(msg, config)
local jstr, res = HTTP.request('http://xkcd.com/info.0.json')
if res ~= 200 then
local input = utilities.get_word(msg.text, 2)
if input == 'r' then
input = math.random(xkcd.latest)
elseif tonumber(input) then
input = tonumber(input)
else
input = xkcd.latest
end
local url = xkcd.strip_url:format(input)
local jstr, code = HTTP.request(url)
if code == 404 then
utilities.send_reply(self, msg, config.errors.results)
elseif code ~= 200 then
utilities.send_reply(self, msg, config.errors.connection)
return
else
local data = JSON.decode(jstr)
local output = string.format('*%s (*[%s](%s)*)*\n_%s_',
data.safe_title:gsub('*', '*\\**'),
data.num,
data.img,
data.alt:gsub('_', '_\\__')
)
utilities.send_message(self, msg.chat.id, output, false, nil, true)
end
local latest = JSON.decode(jstr).num
local strip_num = latest
local input = utilities.input(msg.text)
if input then
if input == '404' then
utilities.send_message(self, msg.chat.id, '*404*\nNot found.', false, nil, true)
return
elseif tonumber(input) then
if tonumber(input) > latest then
strip_num = latest
else
strip_num = input
end
elseif input == 'r' then
strip_num = math.random(latest)
end
end
local res_url = 'http://xkcd.com/' .. strip_num .. '/info.0.json'
jstr, res = HTTP.request(res_url)
if res ~= 200 then
utilities.send_reply(self, msg, config.errors.connection)
return
end
local jdat = JSON.decode(jstr)
local output = '*' .. jdat.safe_title .. ' (*[' .. jdat.num .. '](' .. jdat.img .. ')*)*\n_' .. jdat.alt:gsub('_', '\\_') .. '_'
utilities.send_message(self, msg.chat.id, output, false, nil, true)
end
return xkcd

View File

@ -8,11 +8,9 @@ local JSON = require('dkjson')
local utilities = require('otouto.utilities')
function youtube:init(config)
if not config.google_api_key then
print('Missing config value: google_api_key.')
print('youtube.lua will not be enabled.')
return
end
assert(config.google_api_key,
'youtube.lua requires a Google API key from http://console.developers.google.com.'
)
youtube.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('youtube', true):t('yt', true).table
youtube.doc = config.cmd_pat .. [[youtube <query>
@ -24,14 +22,10 @@ youtube.command = 'youtube <query>'
function youtube:action(msg, config)
local input = utilities.input(msg.text)
local input = utilities.input_from_msg(msg)
if not input then
if msg.reply_to_message and msg.reply_to_message.text then
input = msg.reply_to_message.text
else
utilities.send_message(self, msg.chat.id, youtube.doc, true, msg.message_id, true)
return
end
utilities.send_reply(self, msg, youtube.doc, true)
return
end
local url = 'https://www.googleapis.com/youtube/v3/search?key=' .. config.google_api_key .. '&type=video&part=snippet&maxResults=4&q=' .. URL.escape(input)

View File

@ -12,45 +12,38 @@ local bindings = require('otouto.bindings')
-- For the sake of ease to new contributors and familiarity to old contributors,
-- we'll provide a couple of aliases to real bindings here.
-- Edit: To keep things working and allow for HTML messages, you can now pass a
-- string for use_markdown and that will be sent as the parse mode.
function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown)
local parse_mode
if type(use_markdown) == 'string' then
parse_mode = use_markdown
elseif use_markdown == true then
parse_mode = 'markdown'
end
return bindings.request(self, 'sendMessage', {
chat_id = chat_id,
text = text,
disable_web_page_preview = disable_web_page_preview,
reply_to_message_id = reply_to_message_id,
parse_mode = use_markdown and 'Markdown' or nil
parse_mode = parse_mode
} )
end
function utilities:send_reply(old_msg, text, use_markdown)
return bindings.request(self, 'sendMessage', {
chat_id = old_msg.chat.id,
text = text,
disable_web_page_preview = true,
reply_to_message_id = old_msg.message_id,
parse_mode = use_markdown and 'Markdown' or nil
} )
return utilities.send_message(self, old_msg.chat.id, text, true, old_msg.message_id, use_markdown)
end
-- get the indexed word in a string
function utilities.get_word(s, i)
s = s or ''
i = i or 1
local t = {}
local n = 0
for w in s:gmatch('%g+') do
table.insert(t, w)
n = n + 1
if n == i then return w end
end
return t[i] or false
end
-- Like get_word(), but better.
-- Returns the actual index.
function utilities.index(s)
local t = {}
for w in s:gmatch('%g+') do
table.insert(t, w)
end
return t
return false
end
-- Returns the string after the first space.
@ -61,6 +54,10 @@ function utilities.input(s)
return s:sub(s:find(' ')+1)
end
function utilities.input_from_msg(msg)
return utilities.input(msg.text) or (msg.reply_to_message and #msg.reply_to_message.text > 0 and msg.reply_to_message.text) or false
end
-- Calculates the length of the given string as UTF-8 characters
function utilities.utf8_len(s)
local chars = 0
@ -82,13 +79,13 @@ end
-- Loads a JSON file as a table.
function utilities.load_data(filename)
local f = io.open(filename)
if not f then
if f then
local s = f:read('*all')
f:close()
return JSON.decode(s)
else
return {}
end
local s = f:read('*all')
f:close()
local data = JSON.decode(s)
return data
end
-- Saves a table to a JSON file.
@ -153,85 +150,14 @@ function utilities:resolve_username(input)
end
end
-- Simpler than above function; only returns an ID.
-- Returns nil if no ID is available.
function utilities:id_from_username(input)
input = input:gsub('^@', '')
for _, user in pairs(self.database.users) do
if user.username and user.username:lower() == input:lower() then
return user.id
end
end
end
-- Simpler than below function; only returns an ID.
-- Returns nil if no ID is available.
function utilities:id_from_message(msg)
if msg.reply_to_message then
return msg.reply_to_message.from.id
else
local input = utilities.input(msg.text)
if input then
if tonumber(input) then
return tonumber(input)
elseif input:match('^@') then
return utilities.id_from_username(self, input)
end
end
end
end
function utilities:user_from_message(msg, no_extra)
local input = utilities.input(msg.text_lower)
local target = {}
if msg.reply_to_message then
for k,v in pairs(self.database.users[msg.reply_to_message.from.id_str]) do
target[k] = v
end
elseif input and tonumber(input) then
target.id = tonumber(input)
if self.database.users[input] then
for k,v in pairs(self.database.users[input]) do
target[k] = v
end
end
elseif input and input:match('^@') then
local uname = input:gsub('^@', '')
for _,v in pairs(self.database.users) do
if v.username and uname == v.username:lower() then
for key, val in pairs(v) do
target[key] = val
end
end
end
if not target.id then
target.err = 'Sorry, I don\'t recognize that username.'
end
else
target.err = 'Please specify a user via reply, ID, or username.'
end
if not no_extra then
if target.id then
target.id_str = tostring(target.id)
end
if not target.first_name then
target.first_name = 'User'
end
target.name = utilities.build_name(target.first_name, target.last_name)
end
return target
end
function utilities:handle_exception(err, message, config)
if not err then err = '' end
local output = '\n[' .. os.date('%F %T', os.time()) .. ']\n' .. self.info.username .. ': ' .. err .. '\n' .. message .. '\n'
local output = string.format(
'\n[%s]\n%s: %s\n%s\n',
os.date('%F %T'),
self.info.username,
err or '',
message
)
if config.log_chat then
output = '```' .. output .. '```'
utilities.send_message(self, config.log_chat, output, true, nil, true)
@ -265,16 +191,15 @@ function utilities.download_file(url, filename)
return filename
end
function utilities.markdown_escape(text)
text = text:gsub('_', '\\_')
text = text:gsub('%[', '\\[')
text = text:gsub('%]', '\\]')
text = text:gsub('%*', '\\*')
text = text:gsub('`', '\\`')
return text
function utilities.md_escape(text)
return text:gsub('_', '\\_')
:gsub('%[', '\\['):gsub('%]', '\\]')
:gsub('%*', '\\*'):gsub('`', '\\`')
end
utilities.md_escape = utilities.markdown_escape
function utilities.html_escape(text)
return text:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;')
end
utilities.triggers_meta = {}
utilities.triggers_meta.__index = utilities.triggers_meta
@ -320,7 +245,7 @@ utilities.char = {
rtl_override = '',
rtl_mark = '',
em_dash = '',
utf_8 = '([%z\1-\127\194-\244][\128-\191]*)',
utf_8 = '[%z\1-\127\194-\244][\128-\191]',
}
utilities.set_meta = {}
@ -354,4 +279,11 @@ function utilities.set_meta:__len()
return self.__count
end
-- Styling functions to keep things consistent and easily changeable across plugins.
-- More to be added.
utilities.style = {}
utilities.style.enquote = function(title, body)
return '*' .. title:gsub('*', '\\*') .. ':*\n"' .. utilities.md_escape(body) .. '"'
end
return utilities

View File

@ -1,11 +1,11 @@
#!/bin/sh
# Launch tg listening on the default port (change this if you've changed it in
# config.lua), delete state file after stop, wait two seconds, and restart.
# config.lua), delete state file after stop, wait five seconds, and restart.
while true; do
tg/bin/telegram-cli -P 4567 -E
rm ~/.telegram-cli/state
[ -f ~/.telegram-cli/state ] && rm ~/.telegram-cli/state
echo 'tg has stopped. ^C to exit.'
sleep 5s
done