From b7ed1dbc8016c3bf3731352d82d590a993be02eb Mon Sep 17 00:00:00 2001 From: Akamaru Date: Sun, 17 Jul 2016 13:22:27 +0200 Subject: [PATCH] Alles miku MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erste Anpassungen für Mikudayobot --- .gitignore | 3 +- README.md | 314 +--- config.lua.example | 4 + drua-tg.lua | 154 -- launch.sh | 2 +- main.lua | 2 +- {otouto => miku}/bindings.lua | 20 +- miku/bot.lua | 378 +++++ {otouto => miku}/mimetype.lua | 0 {otouto => miku}/plugins/9gag.lua | 9 +- miku/plugins/about.lua | 38 + {otouto => miku}/plugins/adfly.lua | 4 +- miku/plugins/afk.lua | 123 ++ {otouto => miku}/plugins/app_store.lua | 6 +- miku/plugins/bImages.lua | 75 + miku/plugins/banhammer.lua | 253 +++ {otouto => miku}/plugins/bitly.lua | 4 +- {otouto => miku}/plugins/bitly_create.lua | 18 +- miku/plugins/br.lua | 51 + miku/plugins/btc.lua | 40 + miku/plugins/calc.lua | 39 + miku/plugins/cats.lua | 39 + miku/plugins/channel.lua | 63 + miku/plugins/channels.lua | 90 ++ miku/plugins/cleverbot.lua | 35 + miku/plugins/clypit.lua | 33 + {otouto => miku}/plugins/control.lua | 22 +- {otouto => miku}/plugins/creds.lua | 4 +- miku/plugins/currency.lua | 64 + miku/plugins/dailymotion.lua | 31 + miku/plugins/deviantart.lua | 52 + miku/plugins/dhl.lua | 38 + miku/plugins/dropbox.lua | 39 + miku/plugins/echo.lua | 42 + miku/plugins/entergroup.lua | 57 + {otouto => miku}/plugins/expand.lua | 2 +- {otouto => miku}/plugins/facebook.lua | 32 +- miku/plugins/fefe.lua | 36 + miku/plugins/flickr.lua | 76 + miku/plugins/flickr_search.lua | 53 + {otouto => miku}/plugins/forecast.lua | 8 +- miku/plugins/gImages.lua | 242 +++ miku/plugins/gMaps.lua | 43 + {otouto => miku}/plugins/gSearch.lua | 29 +- miku/plugins/games.lua | 147 ++ miku/plugins/gdrive.lua | 99 ++ {otouto => miku}/plugins/get.lua | 4 +- miku/plugins/getfile.lua | 113 ++ miku/plugins/gfycat.lua | 36 + {otouto => miku}/plugins/github.lua | 14 +- miku/plugins/golem.lua | 68 + miku/plugins/googl.lua | 47 + miku/plugins/gps.lua | 38 + {otouto => miku}/plugins/greetings.lua | 10 +- miku/plugins/hackernews.lua | 45 + miku/plugins/heise.lua | 48 + miku/plugins/hello.lua | 13 + {otouto => miku}/plugins/help.lua | 15 +- miku/plugins/id.lua | 130 ++ miku/plugins/ifttt.lua | 80 + miku/plugins/images.lua | 21 + {otouto => miku}/plugins/imdb.lua | 4 +- {otouto => miku}/plugins/imgblacklist.lua | 42 +- miku/plugins/imgur.lua | 60 + miku/plugins/instagram.lua | 80 + miku/plugins/ip_info.lua | 92 ++ miku/plugins/isup.lua | 85 + miku/plugins/leave_group.lua | 52 + {otouto => miku}/plugins/location_manager.lua | 4 +- {otouto => miku}/plugins/luarun.lua | 8 +- miku/plugins/lyrics.lua | 51 + miku/plugins/magische_miesmuschel.lua | 23 + {otouto => miku}/plugins/media.lua | 41 +- miku/plugins/minecraft_server.lua | 86 + miku/plugins/minecraft_skin.lua | 31 + miku/plugins/myanimelist.lua | 228 +++ miku/plugins/notify.lua | 103 ++ miku/plugins/pagespeed_insights.lua | 39 + {otouto => miku}/plugins/pasteee.lua | 4 +- miku/plugins/pixabay.lua | 120 ++ miku/plugins/play_store.lua | 62 + miku/plugins/plugins.lua | 230 +++ miku/plugins/pocket.lua | 149 ++ {otouto => miku}/plugins/pokedex.lua | 4 +- {otouto => miku}/plugins/preview.lua | 2 +- miku/plugins/qr.lua | 86 + {otouto => miku}/plugins/quotes.lua | 14 +- miku/plugins/random.lua | 67 + {otouto => miku}/plugins/reddit.lua | 27 +- miku/plugins/reddit_post.lua | 55 + {otouto => miku}/plugins/remind.lua | 24 +- {otouto => miku}/plugins/respond.lua | 4 +- {otouto => miku}/plugins/roll.lua | 2 +- {otouto => miku}/plugins/rss.lua | 117 +- {otouto => miku}/plugins/set.lua | 6 +- {otouto => miku}/plugins/shell.lua | 3 +- miku/plugins/site_header.lua | 28 + miku/plugins/soundcloud.lua | 41 + miku/plugins/speedtest.lua | 17 + miku/plugins/spotify.lua | 52 + miku/plugins/stats.lua | 144 ++ miku/plugins/steam.lua | 63 + miku/plugins/streamable.lua | 53 + miku/plugins/surrogate.lua | 16 + miku/plugins/tagesschau.lua | 56 + {otouto => miku}/plugins/tagesschau_eil.lua | 14 +- miku/plugins/tex.lua | 34 + miku/plugins/thetvdb.lua | 96 ++ {otouto => miku}/plugins/time.lua | 2 +- miku/plugins/translate.lua | 141 ++ miku/plugins/tweet.lua | 204 +++ miku/plugins/twitch.lua | 40 + {otouto => miku}/plugins/twitter.lua | 6 +- {otouto => miku}/plugins/twitter_send.lua | 26 +- miku/plugins/twitter_user.lua | 119 ++ {otouto => miku}/plugins/urbandictionary.lua | 8 +- miku/plugins/venue.lua | 31 + miku/plugins/vimeo.lua | 38 + miku/plugins/vine.lua | 45 + {otouto => miku}/plugins/weather.lua | 8 +- miku/plugins/webshot.lua | 83 + miku/plugins/wiimmfi.lua | 58 + miku/plugins/wikia.lua | 45 + miku/plugins/wikipedia.lua | 225 +++ miku/plugins/xkcd.lua | 37 + miku/plugins/yourls.lua | 57 + {otouto => miku}/plugins/youtube.lua | 117 +- {otouto => miku}/plugins/youtube_channel.lua | 2 +- miku/plugins/youtube_dl.lua | 68 + {otouto => miku}/plugins/youtube_playlist.lua | 2 +- {otouto => miku}/plugins/youtube_search.lua | 4 +- miku/redis-old.lua | 39 + {otouto => miku}/redis.lua | 0 {otouto => miku}/utilities.lua | 620 +++++--- otouto/bot.lua | 198 --- otouto/plugins/about.lua | 33 - otouto/plugins/administration.lua | 1414 ----------------- otouto/plugins/apod.lua | 87 - otouto/plugins/bandersnatch.lua | 35 - otouto/plugins/bible.lua | 53 - otouto/plugins/bing.lua | 70 - otouto/plugins/blacklist.lua | 45 - otouto/plugins/calc.lua | 43 - otouto/plugins/cats.lua | 38 - otouto/plugins/channel.lua | 65 - otouto/plugins/chatter.lua | 80 - otouto/plugins/commit.lua | 430 ----- otouto/plugins/currency.lua | 61 - otouto/plugins/dice.lua | 56 - otouto/plugins/dilbert.lua | 51 - otouto/plugins/echo.lua | 34 - otouto/plugins/eightball.lua | 58 - otouto/plugins/fortune.lua | 31 - otouto/plugins/gImages.lua | 74 - otouto/plugins/gMaps.lua | 44 - otouto/plugins/hackernews.lua | 65 - otouto/plugins/hearthstone.lua | 130 -- otouto/plugins/images.lua | 16 - otouto/plugins/lastfm.lua | 111 -- otouto/plugins/me.lua | 29 - otouto/plugins/nick.lua | 51 - otouto/plugins/patterns.lua | 33 - otouto/plugins/ping.lua | 16 - otouto/plugins/pun.lua | 144 -- otouto/plugins/reactions.lua | 52 - otouto/plugins/shout.lua | 52 - otouto/plugins/slap.lua | 130 -- otouto/plugins/translate.lua | 51 - otouto/plugins/whoami.lua | 51 - otouto/plugins/wikipedia.lua | 112 -- otouto/plugins/xkcd.lua | 58 - tg-install.sh | 15 - tg-launch.sh | 11 - 173 files changed, 7350 insertions(+), 5016 deletions(-) delete mode 100644 drua-tg.lua rename {otouto => miku}/bindings.lua (83%) create mode 100644 miku/bot.lua rename {otouto => miku}/mimetype.lua (100%) rename {otouto => miku}/plugins/9gag.lua (76%) create mode 100644 miku/plugins/about.lua rename {otouto => miku}/plugins/adfly.lua (93%) create mode 100644 miku/plugins/afk.lua rename {otouto => miku}/plugins/app_store.lua (96%) create mode 100644 miku/plugins/bImages.lua create mode 100644 miku/plugins/banhammer.lua rename {otouto => miku}/plugins/bitly.lua (92%) rename {otouto => miku}/plugins/bitly_create.lua (86%) create mode 100644 miku/plugins/br.lua create mode 100644 miku/plugins/btc.lua create mode 100644 miku/plugins/calc.lua create mode 100644 miku/plugins/cats.lua create mode 100644 miku/plugins/channel.lua create mode 100644 miku/plugins/channels.lua create mode 100644 miku/plugins/cleverbot.lua create mode 100644 miku/plugins/clypit.lua rename {otouto => miku}/plugins/control.lua (67%) rename {otouto => miku}/plugins/creds.lua (97%) create mode 100644 miku/plugins/currency.lua create mode 100644 miku/plugins/dailymotion.lua create mode 100644 miku/plugins/deviantart.lua create mode 100644 miku/plugins/dhl.lua create mode 100644 miku/plugins/dropbox.lua create mode 100644 miku/plugins/echo.lua create mode 100644 miku/plugins/entergroup.lua rename {otouto => miku}/plugins/expand.lua (95%) rename {otouto => miku}/plugins/facebook.lua (85%) create mode 100644 miku/plugins/fefe.lua create mode 100644 miku/plugins/flickr.lua create mode 100644 miku/plugins/flickr_search.lua rename {otouto => miku}/plugins/forecast.lua (97%) create mode 100644 miku/plugins/gImages.lua create mode 100644 miku/plugins/gMaps.lua rename {otouto => miku}/plugins/gSearch.lua (78%) create mode 100644 miku/plugins/games.lua create mode 100644 miku/plugins/gdrive.lua rename {otouto => miku}/plugins/get.lua (93%) create mode 100644 miku/plugins/getfile.lua create mode 100644 miku/plugins/gfycat.lua rename {otouto => miku}/plugins/github.lua (84%) create mode 100644 miku/plugins/golem.lua create mode 100644 miku/plugins/googl.lua create mode 100644 miku/plugins/gps.lua rename {otouto => miku}/plugins/greetings.lua (72%) create mode 100644 miku/plugins/hackernews.lua create mode 100644 miku/plugins/heise.lua create mode 100644 miku/plugins/hello.lua rename {otouto => miku}/plugins/help.lua (89%) create mode 100644 miku/plugins/id.lua create mode 100644 miku/plugins/ifttt.lua create mode 100644 miku/plugins/images.lua rename {otouto => miku}/plugins/imdb.lua (94%) rename {otouto => miku}/plugins/imgblacklist.lua (65%) create mode 100644 miku/plugins/imgur.lua create mode 100644 miku/plugins/instagram.lua create mode 100644 miku/plugins/ip_info.lua create mode 100644 miku/plugins/isup.lua create mode 100644 miku/plugins/leave_group.lua rename {otouto => miku}/plugins/location_manager.lua (95%) rename {otouto => miku}/plugins/luarun.lua (87%) create mode 100644 miku/plugins/lyrics.lua create mode 100644 miku/plugins/magische_miesmuschel.lua rename {otouto => miku}/plugins/media.lua (62%) create mode 100644 miku/plugins/minecraft_server.lua create mode 100644 miku/plugins/minecraft_skin.lua create mode 100644 miku/plugins/myanimelist.lua create mode 100644 miku/plugins/notify.lua create mode 100644 miku/plugins/pagespeed_insights.lua rename {otouto => miku}/plugins/pasteee.lua (91%) create mode 100644 miku/plugins/pixabay.lua create mode 100644 miku/plugins/play_store.lua create mode 100644 miku/plugins/plugins.lua create mode 100644 miku/plugins/pocket.lua rename {otouto => miku}/plugins/pokedex.lua (95%) rename {otouto => miku}/plugins/preview.lua (95%) create mode 100644 miku/plugins/qr.lua rename {otouto => miku}/plugins/quotes.lua (84%) create mode 100644 miku/plugins/random.lua rename {otouto => miku}/plugins/reddit.lua (70%) create mode 100644 miku/plugins/reddit_post.lua rename {otouto => miku}/plugins/remind.lua (82%) rename {otouto => miku}/plugins/respond.lua (96%) rename {otouto => miku}/plugins/roll.lua (92%) rename {otouto => miku}/plugins/rss.lua (70%) rename {otouto => miku}/plugins/set.lua (90%) rename {otouto => miku}/plugins/shell.lua (92%) create mode 100644 miku/plugins/site_header.lua create mode 100644 miku/plugins/soundcloud.lua create mode 100644 miku/plugins/speedtest.lua create mode 100644 miku/plugins/spotify.lua create mode 100644 miku/plugins/stats.lua create mode 100644 miku/plugins/steam.lua create mode 100644 miku/plugins/streamable.lua create mode 100644 miku/plugins/surrogate.lua create mode 100644 miku/plugins/tagesschau.lua rename {otouto => miku}/plugins/tagesschau_eil.lua (89%) create mode 100644 miku/plugins/tex.lua create mode 100644 miku/plugins/thetvdb.lua rename {otouto => miku}/plugins/time.lua (98%) create mode 100644 miku/plugins/translate.lua create mode 100644 miku/plugins/tweet.lua create mode 100644 miku/plugins/twitch.lua rename {otouto => miku}/plugins/twitter.lua (97%) rename {otouto => miku}/plugins/twitter_send.lua (90%) create mode 100644 miku/plugins/twitter_user.lua rename {otouto => miku}/plugins/urbandictionary.lua (82%) create mode 100644 miku/plugins/venue.lua create mode 100644 miku/plugins/vimeo.lua create mode 100644 miku/plugins/vine.lua rename {otouto => miku}/plugins/weather.lua (96%) create mode 100644 miku/plugins/webshot.lua create mode 100644 miku/plugins/wiimmfi.lua create mode 100644 miku/plugins/wikia.lua create mode 100644 miku/plugins/wikipedia.lua create mode 100644 miku/plugins/xkcd.lua create mode 100644 miku/plugins/yourls.lua rename {otouto => miku}/plugins/youtube.lua (50%) rename {otouto => miku}/plugins/youtube_channel.lua (97%) create mode 100644 miku/plugins/youtube_dl.lua rename {otouto => miku}/plugins/youtube_playlist.lua (96%) rename {otouto => miku}/plugins/youtube_search.lua (94%) create mode 100644 miku/redis-old.lua rename {otouto => miku}/redis.lua (100%) rename {otouto => miku}/utilities.lua (58%) delete mode 100644 otouto/bot.lua delete mode 100644 otouto/plugins/about.lua delete mode 100644 otouto/plugins/administration.lua delete mode 100644 otouto/plugins/apod.lua delete mode 100644 otouto/plugins/bandersnatch.lua delete mode 100644 otouto/plugins/bible.lua delete mode 100644 otouto/plugins/bing.lua delete mode 100644 otouto/plugins/blacklist.lua delete mode 100644 otouto/plugins/calc.lua delete mode 100644 otouto/plugins/cats.lua delete mode 100644 otouto/plugins/channel.lua delete mode 100644 otouto/plugins/chatter.lua delete mode 100644 otouto/plugins/commit.lua delete mode 100644 otouto/plugins/currency.lua delete mode 100644 otouto/plugins/dice.lua delete mode 100644 otouto/plugins/dilbert.lua delete mode 100644 otouto/plugins/echo.lua delete mode 100644 otouto/plugins/eightball.lua delete mode 100644 otouto/plugins/fortune.lua delete mode 100644 otouto/plugins/gImages.lua delete mode 100644 otouto/plugins/gMaps.lua delete mode 100644 otouto/plugins/hackernews.lua delete mode 100644 otouto/plugins/hearthstone.lua delete mode 100644 otouto/plugins/images.lua delete mode 100644 otouto/plugins/lastfm.lua delete mode 100644 otouto/plugins/me.lua delete mode 100644 otouto/plugins/nick.lua delete mode 100644 otouto/plugins/patterns.lua delete mode 100644 otouto/plugins/ping.lua delete mode 100644 otouto/plugins/pun.lua delete mode 100644 otouto/plugins/reactions.lua delete mode 100644 otouto/plugins/shout.lua delete mode 100644 otouto/plugins/slap.lua delete mode 100644 otouto/plugins/translate.lua delete mode 100644 otouto/plugins/whoami.lua delete mode 100644 otouto/plugins/wikipedia.lua delete mode 100644 otouto/plugins/xkcd.lua delete mode 100644 tg-install.sh delete mode 100644 tg-launch.sh diff --git a/.gitignore b/.gitignore index 327396a..6b8ae6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config.lua *.db tg -otouto-dev-1.rockspec +tmp/* +last_commit.txt \ No newline at end of file diff --git a/README.md b/README.md index 177d009..8e9b72d 100644 --- a/README.md +++ b/README.md @@ -1,230 +1,109 @@ # Brawlbot v2 [![Build Status](https://travis-ci.org/Brawl345/Brawlbot-v2.svg?branch=master)](https://travis-ci.org/Brawl345/Brawlbot-v2) -The plugin-wielding, multipurpose Telegram bot. +Der multifunktionale Telegram-Bot. -[Public Bot](http://telegram.me/mokubot) | [Official Channel](http://telegram.me/otouto) | [Development Group](http://telegram.me/BotDevelopment) +[Entwickler auf Telegram](http://telegram.me/Brawl) | [Offizieller Kanal](https://telegram.me/brawlbot_updates) -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. +Brawlbot ist ein auf Plugins basierender Bot, der die [offizielle Telegram Bot API](http://core.telegram.org/bots/api) benutzt. Ursprünglich wurde er im Dezember 2014 auf Basis von Yagops [Telegram Bot](https://github.com/yagop/telegram-bot/) entwickelt, da aber die Entwicklung von tg-cli [zum Stillstand](https://brawlbot.tk/posts/ein-neuanfang) gekommen ist, wurden alle Plugins des bisher proprietären Brawlbots im Juni 2016 auf die Bot-API portiert und open-sourced. +**Brawlbot v2 basiert auf [otouto](https://github.com/topkecleon/otouto) von Topkecleon.** -otouto is free software; you are free to redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3. See **LICENSE** for details. +Brawlbot v2 ist freie Software; du darfst in modifizieren und weiterverbreiten, allerdings musst du dich an die GNU Affero General Public License v3 halten, siehe **LICENSE** für Details. -**The Manual** +##Anleitung -| For Users | For Coders | +| Für User | Für Entwickler| |:----------------------------------------------|:------------------------------| | [Setup](#setup) | [Plugins](#plugins) | -| [Control plugins](#control-plugins) | [Bindings](#bindings) | -| [Group Administration](#group-administration) | [Output style](#output-style) | -| [List of plugins](#list-of-plugins) | [Contributors](#contributors) | +| [Bot steuern](#bot-steuern) | [Bindings](#bindings) | +| | [Datenbank](#datenbank) +* * * +# Für User ## 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. +### Ubuntu und Debian +Ubuntu und Debian liefern Luarocks nur für Lua 5.1 aus. Um Luarocks für Lua 5.2 zu verwenden, folge bitte der [Anleitung auf StackOverflow](http://stackoverflow.com/a/20359102) -To get started, clone the repository and set the following values in `config.lua`: +### Setup +Du benötigst **Lua 5.2+**, eine aktive **Redis-Instanz** und die folgenden **LuaRocks-Module**: +* luasocket +* luasec +* multipart-post +* dkjson +* lpeg +* redis-lua +* fakeredis +* oauth +* xml +* feedparser +* serpent - - `bot_api_key` as your bot authorization token from the BotFather. - - `admin` as your Telegram ID. +Klone danach diese Repo. kopiere die `config.lua.example` nach `config.lua` und trage folgendes ein: -Optionally: + - `bot_api_key`: API-Token vom BotFather + - `admin`: Deine Telegram-ID - - `lang` as the two-letter code representing your language. +Starte danach den Bot mit `./launch.sh`. Um den Bot anzuhalten, führe erst `/halt` über Telegram aus. -Some plugins are not enabled by default. If you wish to enable them, add them to the `plugins` array. +Beim Start werden einige Werte in die Redis-Datenbank unter `telegram:credentials` und `telegram:enabled_plugins` eingetragen. Mit `/plugins enable` kannst du Plugins aktivieren, es sind nicht alle von Haus aus aktiviert. -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: - - - `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`) +Einige Plugins benötigen API-Keys, bitte gehe die einzelnen Plugins durch, bevor du sie aktivierst! * * * -## 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. +## Bot steuern +Ein Administrator kann den Bot über folgende Plugins steuern: -| Plugin | Command | Function | +| Plugin | Kommando | Funktion | |:----------------|:-----------|:---------------------------------------------------| -| `control.lua` | /reload | Reloads all plugins and configuration. | -| | /halt | Shuts down the bot after saving the database. | -| | /script | Runs a list a bot commands, separated by newlines. | -| `blacklist.lua` | /blacklist | Blocks people from using the bot. | -| `shell.lua` | /run | Executes shell commands on the host system. | -| `luarun.lua` | /lua | Executes Lua commands in the bot's environment. | +| `banhammer.lua` | Siehe /hilfe banhammer| Blockt User vom Bot und kann Whitelist aktivieren +| `control.lua` | /restart | Startet den Bot neu | +| | /halt | Speichert die Datenbank und stoppt den Bot | +| | /script | Führt mehrere Kommandos aus, getrennt mit Zeilenumbrüchen | +| `luarun.lua` | /lua | Führt LUA-Kommandos aus | +| `plugins.lua` | /plugins enable/disable | Aktiviert/deaktiviert Plugins | +| `shell.lua` | /sh | Führt Shell-Kommandos aus | * * * -## 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. - -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. - -| Command | Function | Privilege | Internal? | -|:------------|:------------------------------------------------|:----------|:----------| -| /groups | Returns a list of administrated groups (except the unlisted). | 1 | N | -| /ahelp | Returns a list of accessible administrative commands. | 1 | Y | -| /ops | Returns a list of the moderators and governor of a group. | 1 | Y | -| /desc | Returns detailed information for a group. | 1 | Y | -| /rules | Returns the rules of a group. | 1 | Y | -| /motd | Returns the message of the day of a group. | 1 | Y | -| /link | Returns the link for a group. | 1 | Y | -| /kick | Removes the target from the group. | 2 | Y | -| /ban | Bans the target from the group. | 2 | Y | -| /unban | Unbans the target from the group. | 2 | Y | -| /setmotd | Sets the message of the day for a group. | 2 | Y | -| /changerule | Changes an individual group rule. | 3 | Y | -| /setrules | Sets the rules for a group. | 3 | Y | -| /setlink | Sets the link for a group. | 3 | Y | -| /alist | Returns a list of administrators. | 3 | Y | -| /flags | Returns a list of flags and their states, or toggles one. | 3 | Y | -| /antiflood | Configures antiflood (flag 5) settings. | 3 | Y | -| /mod | Promotes a user to a moderator. | 3 | Y | -| /demod | Demotes a moderator to a user. | 3 | Y | -| /gov | Promotes a user to the governor. | 4 | Y | -| /degov | Demotes the governor to a user. | 4 | Y | -| /hammer | Blacklists and globally bans a user. | 4 | N | -| /unhammer | Unblacklists and globally bans a user. | 4 | N | -| /admin | Promotes a user to an administrator. | 5 | N | -| /deadmin | Demotes an administrator to a user. | 5 | N | -| /gadd | Adds a group to the administrative system. | 5 | N | -| /grem | Removes a group from the administrative system. | 5 | Y | -| /glist | Returns a list of all administrated groups and their governors. | 5 | N | -| /broadcast | Broadcasts a message to all administrated groups. | 5 | N | - -Internal commands can only be run within an administrated group. - -### Description of Privileges - -| # | Title | Description | Scope | -|:-:|:--------------|:------------------------------------------------------------------|:-------| -| 0 | Banned | Cannot enter the group(s). | Either | -| 1 | User | Default rank. | Local | -| 2 | Moderator | Can kick/ban/unban users. Can set MOTD. | Local | -| 3 | Governor | Can set rules/link, promote/demote moderators, modify flags. | Local | -| 4 | Administrator | Can globally ban/unban users, promote/demote governors. | Global | -| 5 | Owner | Can add/remove groups, broadcast, promote/demote administrators. | Global | - -Obviously, each greater rank inherits the privileges of the lower, positive ranks. - -### Flags - -| # | Name | Description | -|:-:|:------------|:---------------------------------------------------------------------------------| -| 1 | unlisted | Removes a group from the /groups listing. | -| 2 | antisquig | Automatically removes users for posting Arabic script or RTL characters. | -| 3 | antisquig++ | Automatically removes users whose names contain Arabic script or RTL characters. | -| 4 | antibot | Prevents bots from being added by non-moderators. | -| 5 | antiflood | Prevents flooding by rate-limiting messages per user. | -| 6 | antihammer | Allows globally-banned users to enter a group. | - -#### antiflood -antiflood (flag 5) provides a system of automatic flood protection by removing users who post too much. It is entirely configurable by a group's governor, an administrator, or the bot owner. For each message to a particular group, a user is awarded a certain number of "points". The number of points is different for each message type. When the user reaches 100 points, he is removed. Points are reset each minute. In this way, if a user posts twenty messages within one minute, he is removed. - -**Default antiflood values:** - -| Type | Points | -|:-----|:------:| -| text | 5 | -| contact | 5 | -| audio | 5 | -| voice | 5 | -| photo | 10 | -| document | 10 | -| location | 10 | -| video | 10 | -| sticker | 20 | - -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. +## Gruppenadministration über tg-cli +Dieses Feature wird in Brawlbot nicht unterstützt. * * * -## List of plugins +## Liste aller Plugins -| Plugin | Command | Function | Aliases | -|:----------------------|:------------------------------|:--------------------------------------------------------|:--------| -| `help.lua` | /help [command] | Returns a list of commands or command-specific help. | /h | -| `about.lua` | /about | Returns the about text as configured in config.lua. | -| `ping.lua` | /ping | The simplest plugin ever! | -| `echo.lua` | /echo ‹text› | Repeats a string of text. | -| `bing.lua` | /bing ‹query› | Returns Bing web results. | /g | -| `gImages.lua` | /images ‹query› | Returns a Google image result. | /i | -| `gMaps.lua` | /location ‹query› | Returns location data from Google Maps. | /loc | -| `youtube.lua` | /youtube ‹query› | Returns the top video result from YouTube. | /yt | -| `wikipedia.lua` | /wikipedia ‹query› | Returns the summary of a Wikipedia article. | /w | -| `lastfm.lua` | /np [username] | Returns the song you are currently listening to. | -| `lastfm.lua` | /fmset [username] | Sets your username for /np. /fmset -- will delete it. | -| `hackernews.lua` | /hackernews | Returns the latest posts from Hacker News. | /hn | -| `imdb.lua` | /imdb ‹query› | Returns film information from IMDb. | -| `hearthstone.lua` | /hearthstone ‹query› | Returns data for Hearthstone cards matching the query. | /hs | -| `calc.lua` | /calc ‹expression› | Returns conversions and solutions to math expressions. | -| `bible.lua` | /bible ‹reference› | Returns a Bible verse. | /b | -| `urbandictionary.lua` | /urban ‹query› | Returns the top definition from Urban Dictionary. | /ud | -| `time.lua` | /time ‹query› | Returns the time, date, and a timezone for a location. | -| `weather.lua` | /weather ‹query› | Returns current weather conditions for a given location. | -| `nick.lua` | /nick ‹nickname› | Set your nickname. /nick - will delete it. | -| `whoami.lua` | /whoami | Returns user and chat info for you or the replied-to user. | /who | -| `eightball.lua` | /8ball | Returns an answer from a magic 8-ball. | -| `dice.lua` | /roll ‹nDr› | Returns RNG dice rolls. Uses D&D notation. | -| `reddit.lua` | /reddit [r/subreddit ¦ query] | Returns the top results from a subreddit, query, or r/all. | /r | -| `xkcd.lua` | /xkcd [query] | Returns an xkcd strip and its alt text. | -| `slap.lua` | /slap ‹target› | Gives someone a slap (or worse). | -| `commit.lua` | /commit | Returns a commit message from whatthecommit.com. | -| `fortune.lua` | /fortune | Returns a UNIX fortune. | -| `pun.lua` | /pun | Returns a pun. | -| `pokedex.lua` | /pokedex ‹query› | Returns a Pokedex entry. | /dex | -| `currency.lua` | /cash [amount] ‹cur› to ‹cur› | Converts one currency to another. | -| `cats.lua` | /cat | Returns a cat picture. | -| `reactions.lua` | /reactions | Returns a list of emoticons which can be posted by the bot. | -| `apod.lua` | /apod [date] | Returns the NASA Astronomy Picture of the Day. | -| `dilbert.lua` | /dilbert [date] | Returns a Dilbert strip. | -| `patterns.lua` | /s/‹from›/‹to›/ | Search-and-replace using Lua patterns. | -| `me.lua` | /me | Returns user-specific data stored by the bot. | -| `remind.lua` | /remind | Reminds a user of something after a duration of minutes. | +Brawlbot erhält laufend neue Plugins und wird kontinuierlich weiterentwickelt! Siehe [hier](https://github.com/Brawl345/Brawlbot-v2/tree/master/otouto/plugins) für eine Liste aller Plugins. * * * - +#Für Entwickler ## Plugins -otouto uses a robust plugin system, similar to yagop's [Telegram-Bot](http://github.com/yagop/telegram-bot). +Brawlbot benutzt ein Plugin-System, ähnlich Yagops [Telegram-Bot](http://github.com/yagop/telegram-bot). -Most plugins are intended for public use, but a few are for other purposes, like those for [use by the bot's owner](#control-plugins). See [here](#list-of-plugins) for a list of plugins. +Ein Plugin kann fünf Komponenten haben, aber nur zwei werden benötigt: -A plugin can have five components, and two of them are required: - -| Component | Description | Required? | +| Komponente | Beschreibung | Benötigt? | |:------------------|:---------------------------------------------|:----------| -| `plugin:action` | Main function. Expects `msg` table as an argument. | Y | -| `plugin.triggers` | Table of triggers for the plugin. Uses Lua patterns. | Y | -| `plugin:init` | Optional function run when the plugin is loaded. | N | -| `plugin:cron` | Optional function to be called every minute. | N | -| `plugin.command` | Basic command and syntax. Listed in the help text. | N | -| `plugin.doc` | Usage for the plugin. Returned by "/help $command". | N | -| `plugin.error` | Plugin-specific error message; false for no message. | N | +| `plugin:action` | Hauptfunktion. Benötigt `msg` als Argument, empfohlen wird auch `matches` als drittes Argument nach `config` | J | +| `plugin.triggers` | Tabelle von Triggern, (Lua-Patterns), auf die der Bot reagiert | J | +| `plugin:init` | Optionale Funkion, die beim Start geladen wird | N | +| `plugin:cron` | Wird jede Minute ausgeführt | N | +| `plugin.command` | Einfaches Kommando mit Syntax. Wird bei `/hilfe` gelistet | N | +| `plugin.doc` | Plugin-Hilfe. Wird mit `/help $kommando` gelistet | N | +| `plugin.error` | Plugin-spezifische Fehlermeldung | N | -The `bot:on_msg_receive` function adds a few variables to the `msg` table for your convenience. These are self-explanatory: `msg.from.id_str`, `msg.to.id_str`, `msg.chat.id_str`, `msg.text_lower`, `msg.from.name`. +Die`bot:on_msg_receive` Funktion fügt einige nützte Variablen zur ` msg` Tabelle hinzu. Diese sind:`msg.from.id_str`, `msg.to.id_str`, `msg.chat.id_str`, `msg.text_lower`, `msg.from.name`. -Return values from `plugin:action` are optional, but they do effect 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`. +Rückgabewerte für `plugin:action` sind optional, aber wenn eine Tabelle zurückgegeben wird, wird diese die neue `msg`,-Tabelle und `on_msg_receive` wird damit fortfahren. -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. +Interaktionen mit der Bot-API sind sehr einfach. Siehe [Bindings](#bindings) für Details. -Interactions with the bot API are straightforward. See the [Bindings section](#bindings) for details. - -Several functions used in multiple plugins are defined in utilities.lua. Refer to that file for usage and documentation. +Einige Funktionen, die oft benötigt werden, sind in `utilites.lua` verfügbar. * * * ## Bindings +**Diese Sektion wurde noch nicht lokalisiert.** 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/`.) `method` is the name of the API method. `parameters` (optional) is a table of key/value pairs of the method's parameters to be sent with the method. `file` (super-optional) is a table of a single key/value pair, where the key is the name of the parameter and the value is the filename (if these are included in `parameters` instead, otouto will attempt to send the filename as a file ID). @@ -278,53 +157,34 @@ Upon success, bindings will return the deserialized result from the API. Upon fa * * * -## Output style -otouto plugins should maintain a consistent visual style in their output. This provides a recognizable and comfortable user experience. +## Datenbank +Brawlbot benutzt eine interne Datenbank, wie Otouto sie benutzt und Redis. Die "Datenbank" ist eine Tabelle, auf die über die Variable `database` zugegriffen werden kann (normalerweise `self.database`) und die als JSON-encodierte Plaintext-Datei jede Stunde gespeichert wird oder wenn der Bot gestoppt wird (über `/halt`). -### Titles -Title lines should be **bold**, including any names and trailing punctuation (such as colons). The exception to this rule is if the title line includes a query, which should be _italic_. It is also acceptable to have a link somewhere inside a title, usually within parentheses. eg: +Das ist die Datenbank-Struktur: -> **Star Wars: Episode IV - A New Hope (1977)** -> -> **Search results for** _star wars_**:** -> -> **Changelog for otouto (**[Github](http://github.com/topkecleon/otouto)**):** +``` +{ + users = { + ["55994550"] = { + id = 55994550, + first_name = "Drew", + username = "topkecleon" + } + }, + userdata = { + ["55994550"] = { + nickname = "Best coder ever", + lastfm = "topkecleon" + } + }, + version = "2.1" +} +``` -### Lists -Numerated lists should be done with the number and its following punctuation bolded. Unnumbered lists should use the bullet character ( • ). eg: +`database.users` speichert User-Informationen, wie Usernamen, IDs, etc., wenn der Bot den User sieht. Jeder Tabellen-Key ist die User-ID als String. -> **1.** Life as a quick brown fox. -> -> **2.** The art of jumping over lazy dogs. +`database.userdata` speichert Daten von verschiedenen Plugins, hierzu wird aber für Brawlbot-Plugins Redis verwendet. -and +`database.version` speichert die Bot-Version. -> • Life as a quick brown fox. -> -> • The art of jumping over lazy dogs. - -### Links -Always name your links. Even then, use them with discretion. Excessive links make a post look messy. Links are reasonable when a user may want to learn more about something, but should be avoided when all desirable information is provided. One appropriate use of linking is to provide a preview of an image, as xkcd.lua and apod.lua do. - -### 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). - -The creator and maintainer of otouto is [topkecleon](http://github.com/topkecleon). He can be contacted via [Telegram](http://telegram.me/topkecleon), [Twitter](http://twitter.com/topkecleon), or [email](mailto:drew@otou.to). - -[List of contributors.](https://github.com/topkecleon/otouto/graphs/contributors) - -There are a a few ways to contribute if you are not a programmer. For one, your feedback is always appreciated. Drop me a line on Telegram or on Twitter. Secondly, we are always looking for new ideas for plugins. Most new plugins start with community input. Feel free to suggest them on Github or in the Bot Dev group. You can also donate Bitcoin to the following address: -`1BxegZJ73hPu218UrtiY8druC7LwLr82gS` - -Contributions are appreciated in all forms. Monetary contributions will go toward server costs. Donators will be eternally honored (at their discretion) on this page. - -| Donators (in chronological order) | -|:----------------------------------------------| -| [n8 c00](http://telegram.me/n8_c00) | -| [Alex](http://telegram.me/sandu) | -| [Brayden](http://telegram.me/bb010g) | +* * * \ No newline at end of file diff --git a/config.lua.example b/config.lua.example index a4e4eb7..5f7c9c9 100644 --- a/config.lua.example +++ b/config.lua.example @@ -21,6 +21,10 @@ Sende /hilfe, um zu starten -- The symbol that starts a command. Usually noted as '/' in documentation. cmd_pat = '/', + -- false = only whitelisted users can use inline querys + -- NOTE that it doesn't matter, if the chat is whitelisted! The USER must be whitelisted! + enable_inline_for_everyone = true, + errors = { -- Generic error messages used in various plugins. generic = 'An unexpected error occurred.', connection = 'Verbindungsfehler.', diff --git a/drua-tg.lua b/drua-tg.lua deleted file mode 100644 index da4055b..0000000 --- a/drua-tg.lua +++ /dev/null @@ -1,154 +0,0 @@ ---[[ - drua-tg - A fork of JuanPotato's lua-tg (https://github.com/juanpotato/lua-tg), - modified to work more naturally from an API bot. - - Usage: - drua = require('drua-tg') - drua.IP = 'localhost' - drua.PORT = 4567 - drua.message(chat_id, text) -]]-- - -local SOCKET = require('socket') - -local comtab = { - add = { 'chat_add_user %s %s', 'channel_invite %s %s' }, - kick = { 'chat_del_user %s %s', 'channel_kick %s %s' }, - rename = { 'rename_chat %s "%s"', 'rename_channel %s "%s"' }, - link = { 'export_chat_link %s', 'export_channel_link %s' }, - photo_set = { 'chat_set_photo %s %s', 'channel_set_photo %s %s' }, - photo_get = { [0] = 'load_user_photo %s', 'load_chat_photo %s', 'load_channel_photo %s' }, - info = { [0] = 'user_info %s', 'chat_info %s', 'channel_info %s' } -} - -local format_target = function(target) - target = tonumber(target) - if target < -1000000000000 then - target = 'channel#' .. math.abs(target) - 1000000000000 - return target, 2 - elseif target < 0 then - target = 'chat#' .. math.abs(target) - return target, 1 - else - target = 'user#' .. target - return target, 0 - end -end - -local escape = function(text) - text = text:gsub('\\', '\\\\') - text = text:gsub('\n', '\\n') - text = text:gsub('\t', '\\t') - text = text:gsub('"', '\\"') - return text -end - -local drua = { - IP = 'localhost', - PORT = 4567 -} - -drua.send = function(command, do_receive) - local s = SOCKET.connect(drua.IP, drua.PORT) - assert(s, '\nUnable to connect to tg session.') - s:send(command..'\n') - local output - if do_receive then - output = string.match(s:receive('*l'), 'ANSWER (%d+)') - output = s:receive(tonumber(output)):gsub('\n$', '') - end - s:close() - return output -end - -drua.message = function(target, text) - target = format_target(target) - text = escape(text) - local command = 'msg %s "%s"' - command = command:format(target, text) - return drua.send(command) -end - -drua.send_photo = function(target, photo) - target = format_target(target) - local command = 'send_photo %s %s' - command = command:format(target, photo) - return drua.send(command) -end - -drua.add_user = function(chat, target) - local a - chat, a = format_target(chat) - target = format_target(target) - local command = comtab.add[a]:format(chat, target) - return drua.send(command) -end - -drua.kick_user = function(chat, target) - -- Get the group info so tg will recognize the target. - drua.get_info(chat) - local a - chat, a = format_target(chat) - target = format_target(target) - local command = comtab.kick[a]:format(chat, target) - return drua.send(command) -end - -drua.rename_chat = function(chat, name) - local a - chat, a = format_target(chat) - local command = comtab.rename[a]:format(chat, name) - return drua.send(command) -end - -drua.export_link = function(chat) - local a - chat, a = format_target(chat) - local command = comtab.link[a]:format(chat) - return drua.send(command, true) -end - -drua.get_photo = function(chat) - local a - chat, a = format_target(chat) - local command = comtab.photo_get[a]:format(chat) - local output = drua.send(command, true) - if output:match('FAIL') then - return false - else - return output:match('Saved to (.+)') - end -end - -drua.set_photo = function(chat, photo) - local a - chat, a = format_target(chat) - local command = comtab.photo_set[a]:format(chat, photo) - return drua.send(command) -end - -drua.get_info = function(target) - local a - target, a = format_target(target) - local command = comtab.info[a]:format(target) - return drua.send(command, true) -end - -drua.channel_set_admin = function(chat, user, rank) - chat = format_target(chat) - user = format_target(user) - local command = 'channel_set_admin %s %s %s' - command = command:format(chat, user, rank) - return drua.send(command) -end - -drua.channel_set_about = function(chat, text) - chat = format_target(chat) - text = escape(text) - local command = 'channel_set_about %s "%s"' - command = command:format(chat, text) - return drua.send(command) -end - -return drua diff --git a/launch.sh b/launch.sh index be8b691..088fcb7 100755 --- a/launch.sh +++ b/launch.sh @@ -2,6 +2,6 @@ while true; do lua main.lua - echo 'otouto has stopped. ^C to exit.' + echo 'Miku wurde gestoppt. ^C zum beenden.' sleep 5s done diff --git a/main.lua b/main.lua index 846b012..5bfdb4b 100644 --- a/main.lua +++ b/main.lua @@ -1,4 +1,4 @@ -local bot = require('otouto.bot') +local bot = require('miku.bot') local instance = {} local config = require('config') diff --git a/otouto/bindings.lua b/miku/bindings.lua similarity index 83% rename from otouto/bindings.lua rename to miku/bindings.lua index 5d57f7e..2005064 100644 --- a/otouto/bindings.lua +++ b/miku/bindings.lua @@ -27,14 +27,20 @@ function bindings:request(method, parameters, file) parameters[k] = tostring(v) end if file and next(file) ~= nil then - local file_type, file_name = next(file) - local file_file = io.open(file_name, 'r') - local file_data = { - filename = file_name, + local file_type, file_name = next(file) + if not file_name then return false end + if string.match(file_name, '/home/pi/Mikubot-V2/tmp/') then + local file_file = io.open(file_name, 'r') + local file_data = { + filename = file_name, data = file_file:read('*a') - } - file_file:close() - parameters[file_type] = file_data + } + file_file:close() + parameters[file_type] = file_data + else + local file_type, file_name = next(file) + parameters[file_type] = file_name + end end if next(parameters) == nil then parameters = {''} diff --git a/miku/bot.lua b/miku/bot.lua new file mode 100644 index 0000000..d4a040a --- /dev/null +++ b/miku/bot.lua @@ -0,0 +1,378 @@ +local bot = {} + +-- Requires are moved to init to allow for reloads. +local bindings -- Load Telegram bindings. +local utilities -- Load miscellaneous and cross-plugin functions. +local redis = (loadfile "./miku/redis.lua")() + +bot.version = '2.1' + +function bot:init(config) -- The function run when the bot is started or reloaded. + + bindings = require('miku.bindings') + utilities = require('miku.utilities') + redis = (loadfile "./miku/redis.lua")() + cred_data = load_cred() + + assert( + config.bot_api_key and config.bot_api_key ~= '', + 'You did not set your bot token in the config!' + ) + self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' + + -- Fetch bot information. Try until it succeeds. + repeat + print('Fetching bot information...') + self.info = bindings.getMe(self) + until self.info + self.info = self.info.result + + -- Load the "database"! ;) + if not self.database then + self.database = utilities.load_data(self.info.username..'.db') + end + + -- MIGRATION CODE 2.0 -> 2.1 + if self.database.users and self.database.version ~= '2.1' then + self.database.userdata = {} + for id, user in pairs(self.database.users) do + self.database.userdata[id] = {} + self.database.userdata[id].nickname = user.nickname + self.database.userdata[id].lastfm = user.lastfm + user.nickname = nil + user.lastfm = nil + user.id_str = nil + user.name = nil + 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 {} + -- 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 = self.database.users or {} -- Table to cache userdata. + self.database.users[tostring(self.info.id)] = self.info + + self.plugins = {} -- Load plugins. + enabled_plugins = load_plugins() + for k,v in pairs(enabled_plugins) do + local p = require('miku.plugins.'..v) + -- print('loading plugin',v) + table.insert(self.plugins, p) + self.plugins[k].name = v + if p.init then p.init(self, config) end + end + + print('Bot wurde erfolgreich gestartet!\n@' .. 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. + +end + +function bot:on_msg_receive(msg, config) -- The fn run whenever a message is received. + -- remove comment to enable debugging + -- vardump(msg) + -- Cache user info for those involved. + + if msg.date < os.time() - 5 then return end -- Do not process old messages. + + -- Cache user info for those involved. + self.database.users[tostring(msg.from.id)] = 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 + self.database.users[tostring(msg.forward_from.id)] = msg.forward_from + elseif msg.new_chat_member then + self.database.users[tostring(msg.new_chat_member.id)] = msg.new_chat_member + elseif msg.left_chat_member then + self.database.users[tostring(msg.left_chat_member.id)] = msg.left_chat_member + end + + msg = utilities.enrich_message(msg) + + -- Support deep linking. + if msg.text:match('^'..config.cmd_pat..'start .+') then + msg.text = config.cmd_pat .. utilities.input(msg.text) + msg.text_lower = msg.text:lower() + end + + -- gsub out user name if multiple bots are in the same group + if msg.text:match(config.cmd_pat..'([A-Za-z0-9-_-]+)@'..self.info.username) then + msg.text = string.gsub(msg.text, config.cmd_pat..'([A-Za-z0-9-_-]+)@'..self.info.username, "/%1") + msg.text_lower = msg.text:lower() + end + + msg = pre_process_msg(self, msg, config) + + for _, plugin in ipairs(self.plugins) do + match_plugins(self, msg, config, plugin) + end +end + +function bot:on_callback_receive(callback, msg, config) -- whenever a new callback is received + -- remove comments to enable debugging + -- vardump(msg) + -- vardump(callback) + + if msg.date < os.time() - 1800 then -- Do not process old messages. + utilities.answer_callback_query(self, callback, 'Nachricht älter als eine halbe Stunde, bitte sende den Befehl selbst noch einmal.', true) + return + end + + if not callback.data:find(':') or not callback.data:find('@'..self.info.username..' ') then + return + end + callback.data = string.gsub(callback.data, '@'..self.info.username..' ', "") + local called_plugin = callback.data:match('(.*):.*') + local param = callback.data:sub(callback.data:find(':')+1) + + print('Callback Query "'..param..'" für Plugin "'..called_plugin..'" ausgelöst von '..callback.from.first_name..' ('..callback.from.id..')') + + msg = utilities.enrich_message(msg) + + for _, plugin in ipairs(self.plugins) do + if plugin.name == called_plugin then + if is_plugin_disabled_on_chat(plugin.name, msg) then return end + plugin:callback(callback, msg, self, config, param) + end + end +end + +-- NOTE: To enable InlineQuerys, send /setinline to @BotFather +function bot:process_inline_query(inline_query, config) -- When an inline query is received + -- remove comment to enable debugging + -- vardump(inline_query) + + if not config.enable_inline_for_everyone then + local is_whitelisted = redis:get('whitelist:user#id'..inline_query.from.id) + if not is_whitelisted then return end + end + + if inline_query.query == '' then return end + if inline_query.query:match('"') then + inline_query.query = inline_query.query:gsub('"', '\\"') + end + + for _, plugin in ipairs(self.plugins) do + match_inline_plugins(self, inline_query, config, plugin) + end +end + +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 } ) + if res then + for _,v in ipairs(res.result) do -- Go through every new message. + self.last_update = v.update_id + if v.inline_query then + bot.process_inline_query(self, v.inline_query, config) + elseif v.callback_query then + bot.on_callback_receive(self, v.callback_query, v.callback_query.message, config) + elseif v.message then + bot.on_msg_receive(self, v.message, config) + end + end + else + print('Connection error while fetching updates.') + end + + if self.last_cron ~= os.date('%M') then -- Run cron jobs every minute. + self.last_cron = os.date('%M') + utilities.save_data(self.info.username..'.db', self.database) -- Save the database. + for i,v in ipairs(self.plugins) do + if v.cron then -- Call each plugin's cron function, if it has one. + local result, err = pcall(function() v.cron(self, config) end) + if not result then + utilities.handle_exception(self, err, 'CRON: ' .. i, config) + end + end + end + end + 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') + end + end + + -- Save the database before exiting. + utilities.save_data(self.info.username..'.db', self.database) + print('Halted.') +end + +-- Apply plugin.pre_process function +function pre_process_msg(self, msg, config) + for _,plugin in ipairs(self.plugins) do + if plugin.pre_process and msg then + -- print('Preprocess '..plugin.name) -- remove comment to restore old behaviour + new_msg = plugin:pre_process(msg, self, config) + end + end + return new_msg +end + +function match_inline_plugins(self, inline_query, config, plugin) + for _, trigger in pairs(plugin.inline_triggers or {}) do + if string.match(string.lower(inline_query.query), trigger) then + local success, result = pcall(function() + for k, pattern in pairs(plugin.inline_triggers) do + matches = match_pattern(pattern, inline_query.query) + if matches then + break; + end + end + print('Inline: '..plugin.name..' triggered') + return plugin.inline_callback(self, inline_query, config, matches) + end) + if not success then + print(result) + end + end + end +end + +function match_plugins(self, msg, config, plugin) + for _, trigger in pairs(plugin.triggers or {}) do + if string.match(msg.text_lower, trigger) then + -- Check if Plugin is disabled + if is_plugin_disabled_on_chat(plugin.name, msg) then return end + local success, result = pcall(function() + -- trying to port matches to miku + for k, pattern in pairs(plugin.triggers) do + matches = match_pattern(pattern, msg.text) + if matches then + break; + end + end + print(plugin.name..' triggered') + return plugin.action(self, msg, config, matches) + end) + if not success then + -- If the plugin has an error message, send it. If it does + -- not, use the generic one specified in config. If it's set + -- to false, do nothing. + if plugin.error then + utilities.send_reply(self, msg, plugin.error) + elseif plugin.error == nil then + utilities.send_reply(self, msg, config.errors.generic, true) + end + utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) + 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. + elseif result ~= true then + return + end + end + end +end + +function is_plugin_disabled_on_chat(plugin_name, msg) + local hash = get_redis_hash(msg, 'disabled_plugins') + local disabled = redis:hget(hash, plugin_name) + + -- Plugin is disabled + if disabled == 'true' then + print('Plugin '..plugin_name..' ist in diesem Chat deaktiviert') + return true + else + return false + end +end + +function load_plugins() + enabled_plugins = redis:smembers('telegram:enabled_plugins') + if not enabled_plugins[1] then + create_plugin_set() + end + return enabled_plugins +end + +-- create plugin set if it doesn't exist +function create_plugin_set() + enabled_plugins = { + 'control', + 'about', + 'id', + 'echo', + 'banhammer', + 'channels', + 'plugins', + 'help', + 'greetings' + } + print ('Aktiviere Plugins und speicher in telegram:enabled_plugins') + for _,plugin in pairs(enabled_plugins) do + redis:sadd("telegram:enabled_plugins", plugin) + end +end + +function load_cred() + if redis:exists("telegram:credentials") == false then + -- If credentials hash doesnt exists + print ("Neuer Credentials-Hash in telegram:credentials erstellt.") + create_cred() + end + return redis:hgetall("telegram:credentials") +end + +-- create credentials hash with redis +function create_cred() + cred = { + bitly_access_token = "", + cloudinary_apikey = "", + cloudinary_api_secret = "", + cloudinary_public_id = "", + derpibooru_apikey = "", + fb_access_token = "", + flickr_apikey = "", + forecastio_apikey = "", + ftp_site = "", + ftp_username = "", + ftp_password = "", + gender_apikey = "", + golem_apikey = "", + google_apikey = "", + google_cse_id = "", + gitlab_private_token = "", + gitlab_project_id = "", + instagram_access_token = "", + lyricsnmusic_apikey = "", + mal_username = "", + mal_pw = "", + neutrino_userid = "", + neutrino_apikey = "", + owm_apikey = "", + page2images_restkey = "", + plex_token = "", + sankaku_username = "", + sankaku_pw = "", + soundcloud_client_id = "", + tumblr_api_key = "", + tw_consumer_key = "", + tw_consumer_secret = "", + tw_access_token = "", + tw_access_token_secret = "", + x_mashape_key = "", + yandex_translate_apikey = "", + yandex_rich_content_apikey = "", + yourls_site_url = "", + yourls_signature_token = "" + } + redis:hmset("telegram:credentials", cred) + print ('Credentials gespeichert in telegram:credentials') +end + +return bot \ No newline at end of file diff --git a/otouto/mimetype.lua b/miku/mimetype.lua similarity index 100% rename from otouto/mimetype.lua rename to miku/mimetype.lua diff --git a/otouto/plugins/9gag.lua b/miku/plugins/9gag.lua similarity index 76% rename from otouto/plugins/9gag.lua rename to miku/plugins/9gag.lua index e32fda6..015451c 100644 --- a/otouto/plugins/9gag.lua +++ b/miku/plugins/9gag.lua @@ -3,8 +3,8 @@ local ninegag = {} local HTTP = require('socket.http') local URL = require('socket.url') local JSON = require('dkjson') -local utilities = require('otouto.utilities') -local bindings = require('otouto.bindings') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') ninegag.command = '9gag' @@ -22,19 +22,20 @@ function ninegag:get_9GAG() -- random max json table size local i = math.random(#gag) local link_image = gag[i].src local title = gag[i].title + local post_url = gag[i].url return link_image, title, post_url end function ninegag:action(msg, config) utilities.send_typing(self, msg.chat.id, 'upload_photo') - local url, title = ninegag:get_9GAG() + local url, title, post_url = ninegag:get_9GAG() if not url then utilities.send_reply(self, msg, config.errors.connection) return end local file = download_to_file(url) - utilities.send_photo(self, msg.chat.id, file, title) + utilities.send_photo(self, msg.chat.id, file, title, msg.message_id, '{"inline_keyboard":[[{"text":"Post aufrufen","url":"'..post_url..'"}]]}') end return ninegag diff --git a/miku/plugins/about.lua b/miku/plugins/about.lua new file mode 100644 index 0000000..e20bafb --- /dev/null +++ b/miku/plugins/about.lua @@ -0,0 +1,38 @@ +local about = {} + +local bot = require('miku.bot') +local utilities = require('miku.utilities') + +about.command = 'about' +about.doc = '`Sendet Informationen über den Bot.`' + +about.triggers = { + '/about', + '/start' +} + +function about:action(msg, config) + + -- Filthy hack, but here is where we'll stop forwarded messages from hitting + -- other plugins. + -- disabled to restore old behaviour + -- if msg.forward_from then return end + + local output = config.about_text .. '\nBrawlbot v'..bot.version..', basierend auf miku von topkecleon.' + + if + (msg.new_chat_member and msg.new_chat_member.id == self.info.id) + or msg.text_lower:match('^'..config.cmd_pat..'about$') + or msg.text_lower:match('^'..config.cmd_pat..'about@'..self.info.username:lower()..'$') + or msg.text_lower:match('^'..config.cmd_pat..'start$') + or msg.text_lower:match('^'..config.cmd_pat..'start@'..self.info.username:lower()..'$') + then + utilities.send_message(self, msg.chat.id, output, true, nil, true) + return + end + + return true + +end + +return about diff --git a/otouto/plugins/adfly.lua b/miku/plugins/adfly.lua similarity index 93% rename from otouto/plugins/adfly.lua rename to miku/plugins/adfly.lua index 190fef6..51d8d55 100644 --- a/otouto/plugins/adfly.lua +++ b/miku/plugins/adfly.lua @@ -1,8 +1,8 @@ local adfly = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local HTTPS = require('ssl.https') -local redis = (loadfile "./otouto/redis.lua")() +local redis = (loadfile "./miku/redis.lua")() function adfly:init(config) adfly.triggers = { diff --git a/miku/plugins/afk.lua b/miku/plugins/afk.lua new file mode 100644 index 0000000..96fb6ff --- /dev/null +++ b/miku/plugins/afk.lua @@ -0,0 +1,123 @@ +-- original plugin by Akamaru [https://ponywave.de] +-- I added Redis and automatic online switching back in 2015 + +local afk = {} + +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() + +function afk:init(config) + afk.triggers = { + "^/([A|a][F|f][K|k])$", + "^/([A|a][F|f][K|k]) (.*)$" + } + afk.doc = [[* +]]..config.cmd_pat..[[afk* _[Text]_: Setzt Status auf AFK mit optionalem Text]] +end + +afk.command = 'afk [Text]' + +function afk:is_offline(hash) + local afk = redis:hget(hash, 'afk') + if afk == "true" then + return true + else + return false + end +end + +function afk:get_afk_text(hash) + local afk_text = redis:hget(hash, 'afk_text') + if afk_text ~= nil and afk_text ~= "" and afk_text ~= "false" then + return afk_text + else + return false + end +end + +function afk:switch_afk(user_name, user_id, chat_id, timestamp, text) + local hash = 'afk:'..chat_id..':'..user_id + + if afk:is_offline(hash) then + local afk_text = afk:get_afk_text(hash) + if afk_text then + return 'Du bist bereits AFK ('..afk_text..')!' + else + return 'Du bist bereits AFK!' + end + end + + print('Setting redis hash afk in '..hash..' to true') + redis:hset(hash, 'afk', true) + print('Setting redis hash timestamp in '..hash..' to '..timestamp) + redis:hset(hash, 'time', timestamp) + + if text then + print('Setting redis hash afk_text in '..hash..' to '..text) + redis:hset(hash, 'afk_text', text) + return user_name..' ist AFK ('..text..')' + else + return user_name..' ist AFK' + end +end + +function afk:pre_process(msg, self) + if msg.chat.type == "private" then + -- Ignore + return + end + + local user_name = get_name(msg) + local user_id = msg.from.id + local chat_id = msg.chat.id + local hash = 'afk:'..chat_id..':'..user_id + + + if afk:is_offline(hash) then + local afk_text = afk:get_afk_text(hash) + + -- calculate afk time + local timestamp = redis:hget(hash, 'time') + local current_timestamp = msg.date + local afk_time = current_timestamp - timestamp + local seconds = afk_time % 60 + local minutes = math.floor(afk_time / 60) + local minutes = minutes % 60 + local hours = math.floor(afk_time / 3600) + if minutes == 00 and hours == 00 then + duration = seconds..' Sekunden' + elseif hours == 00 and minutes ~= 00 then + duration = string.format("%02d:%02d", minutes, seconds)..' Minuten' + elseif hours ~= 00 then + duration = string.format("%02d:%02d:%02d", hours, minutes, seconds)..' Stunden' + end + + redis:hset(hash, 'afk', false) + if afk_text then + redis:hset(hash, 'afk_text', false) + local afk_text = afk_text:gsub("%*","") + local afk_text = afk_text:gsub("_","") + utilities.send_message(self, msg.chat.id, user_name..' ist wieder da (war: *'..afk_text..'* für '..duration..')!', true, nil, true) + else + utilities.send_message(self, msg.chat.id, user_name..' ist wieder da (war '..duration..' weg)!') + end + end + + return msg +end + +function afk:action(msg) + if msg.chat.type == "private" then + utilities.send_reply(self, msg, "Mir ist's egal, ob du AFK bist ._.") + return + end + + local user_id = msg.from.id + local chat_id = msg.chat.id + local user_name = get_name(msg) + local timestamp = msg.date + + utilities.send_reply(self, msg, afk:switch_afk(user_name, user_id, chat_id, timestamp, matches[2])) +end + +return afk \ No newline at end of file diff --git a/otouto/plugins/app_store.lua b/miku/plugins/app_store.lua similarity index 96% rename from otouto/plugins/app_store.lua rename to miku/plugins/app_store.lua index 9be7eb1..0568d59 100644 --- a/otouto/plugins/app_store.lua +++ b/miku/plugins/app_store.lua @@ -2,12 +2,12 @@ local app_store = {} local https = require('ssl.https') local json = require('dkjson') -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() app_store.triggers = { "itunes.apple.com/(.*)/app/(.*)/id(%d+)", - "^!itunes (%d+)$", + "^/itunes (%d+)$", "itunes.apple.com/app/id(%d+)" } diff --git a/miku/plugins/bImages.lua b/miku/plugins/bImages.lua new file mode 100644 index 0000000..0389e9b --- /dev/null +++ b/miku/plugins/bImages.lua @@ -0,0 +1,75 @@ +local bImages = {} + +local HTTPS = require('ssl.https') +HTTPS.timeout = 10 +local URL = require('socket.url') +local JSON = require('dkjson') +local redis = (loadfile "./miku/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function bImages:init(config) + if not cred_data.bing_search_key then + print('Missing config value: bing_search_key.') + print('bImages.lua will not be enabled.') + return + end + + bImages.triggers = {"^/nil$"} + bImages.inline_triggers = { + "^b (.*)" + } +end + +local apikey = cred_data.bing_search_key +local BASE_URL = 'https://api.cognitive.microsoft.com/bing/v5.0' + +function bImages:getImages(query) + local url = BASE_URL..'/images/search?q='..URL.escape(query)..'&count=50&mkt=de-de' + local response_body = {} + local request_constructor = { + url = url, + method = "GET", + sink = ltn12.sink.table(response_body), + redirect = false, + headers = { + ["Ocp-Apim-Subscription-Key"] = apikey + } + } + local ok, response_code, response_headers = HTTPS.request(request_constructor) + if not ok then return end + local images = JSON.decode(table.concat(response_body)).value + if not images[1] then return end + + + + local results = '[' + for n in pairs(images) do + if images[n].encodingFormat == 'jpeg' then -- Inline-Querys MUST use JPEG photos! + local photo_url = images[n].contentUrl + local thumb_url = images[n].thumbnailUrl + results = results..'{"type":"photo","id":"'..math.random(100000000000000000)..'","photo_url":"'..photo_url..'","thumb_url":"'..thumb_url..'","photo_width":'..images[n].width..',"photo_height":'..images[n].height..',"reply_markup":{"inline_keyboard":[[{"text":"Bing aufrufen","url":"'..images[n].webSearchUrl..'"},{"text":"Bild aufrufen","url":"'..photo_url..'"}]]}},' + end + end + + local results = results:sub(0, -2) + local results = results..']' + cache_data('bImages', string.lower(query), results, 1209600, 'key') + return results +end + +function bImages:inline_callback(inline_query, config, matches) + local query = matches[1] + local results = redis:get('telegram:cache:bImages:'..string.lower(query)) + if not results then + results = bImages:getImages(query) + end + + if not results then return end + utilities.answer_inline_query(self, inline_query, results, 3600) +end + +function bImages:action() +end + +return bImages \ No newline at end of file diff --git a/miku/plugins/banhammer.lua b/miku/plugins/banhammer.lua new file mode 100644 index 0000000..9c33bcb --- /dev/null +++ b/miku/plugins/banhammer.lua @@ -0,0 +1,253 @@ +local banhammer = {} + +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() + +banhammer.command = 'banhammer ' + +function banhammer:init(config) + banhammer.triggers = { + "^/(whitelist) (enable)$", + "^/(whitelist) (disable)$", + "^/(whitelist) (user) (%d+)$", + "^/(whitelist) (chat)$", + "^/(whitelist) (delete) (user) (%d+)$", + "^/(whitelist) (delete) (chat)$", + "^/(ban) (user) (%d+)$", + "^/(ban) (delete) (%d+)$", + "^/(kick) (%d+)$" + } + banhammer.doc = [[* +]]..config.cmd_pat..[[whitelist* __/__: Aktiviert/deaktiviert Whitelist +*]]..config.cmd_pat..[[whitelist* user __: Whiteliste User +*]]..config.cmd_pat..[[whitelist* chat: Whiteliste ganze Gruppe +*]]..config.cmd_pat..[[whitelist* delete user __: Lösche User von der Whitelist +*]]..config.cmd_pat..[[whitelist* delete chat: Lösche ganze Gruppe von der Whitelist +*]]..config.cmd_pat..[[ban* user __: Kicke User vom Chat und kicke ihn, wenn er erneut beitritt +*]]..config.cmd_pat..[[ban* delete __: Entbanne User +*]]..config.cmd_pat..[[kick* __: Kicke User aus dem Chat]] +end + +function banhammer:kick_user(user_id, chat_id, self, onlykick) + if user_id == tostring(our_id) then + return "Ich werde mich nicht selbst kicken!" + else + local request = bindings.request(self, 'kickChatMember', { + chat_id = chat_id, + user_id = user_id + } ) + if onlykick then return end + if not request then return 'User gebannt, aber kicken war nicht erfolgreich. Bin ich Administrator oder ist der User hier überhaupt?' end + return 'User '..user_id..' gebannt!' + end +end + +function banhammer:ban_user(user_id, chat_id, self) + if user_id == tostring(our_id) then + return "Ich werde mich nicht selbst kicken!" + else + -- Save to redis + local hash = 'banned:'..chat_id..':'..user_id + redis:set(hash, true) + -- Kick from chat + return banhammer:kick_user(user_id, chat_id, self) + end +end + +function banhammer:unban_user(user_id, chat_id, self, chat_type) + local hash = 'banned:'..chat_id..':'..user_id + redis:del(hash) + if chat_type == 'supergroup' then -- how can bots be admins anyway? + local request = bindings.request(self, 'unbanChatMember', { + chat_id = chat_id, + user_id = user_id + } ) + end + return 'User '..user_id..' wurde entbannt.' +end + +function banhammer:is_banned(user_id, chat_id) + local hash = 'banned:'..chat_id..':'..user_id + local banned = redis:get(hash) + return banned or false +end + +function banhammer:is_user_whitelisted(id) + local hash = 'whitelist:user#id'..id + local white = redis:get(hash) or false + return white +end + +function banhammer:is_chat_whitelisted(id) + local hash = 'whitelist:chat#id'..id + local white = redis:get(hash) or false + return white +end + +function banhammer:pre_process(msg, self, config) + -- SERVICE MESSAGE + if msg.new_chat_member then + local user_id = msg.new_chat_member.id + print('Checking invited user '..user_id) + local banned = banhammer:is_banned(user_id, msg.chat.id) + if banned then + print('User is banned!') + banhammer:kick_user(user_id, msg.chat.id, self, true) + end + -- No further checks + return msg + end + + -- BANNED USER TALKING + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + local user_id = msg.from.id + local chat_id = msg.chat.id + local banned = banhammer:is_banned(user_id, chat_id) + if banned then + print('Banned user talking!') + banhammer:ban_user(user_id, chat_id, self) + msg.text = '' + end + end + + + -- WHITELIST + local hash = 'whitelist:enabled' + local whitelist = redis:get(hash) + local issudo = is_sudo(msg, config) + + -- Allow all sudo users even if whitelist is allowed + if whitelist and not issudo then + print('Whitelist enabled and not sudo') + -- Check if user or chat is whitelisted + local allowed = banhammer:is_user_whitelisted(msg.from.id) + local has_been_warned = redis:hget('user:'..msg.from.id, 'has_been_warned') + + if not allowed then + print('User '..msg.from.id..' not whitelisted') + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + allowed = banhammer:is_chat_whitelisted(msg.chat.id) + if not allowed then + print ('Chat '..msg.chat.id..' not whitelisted') + else + print ('Chat '..msg.chat.id..' whitelisted :)') + end + else + if not has_been_warned then + utilities.send_reply(self, msg, "Dies ist ein privater Bot, der erst nach einer Freischaltung benutzt werden kann.\nThis is a private bot, which can only be after an approval.") + redis:hset('user:'..msg.from.id, 'has_been_warned', true) + else + print('User has already been warned!') + end + end + else + print('User '..msg.from.id..' allowed :)') + end + + if not allowed then + msg.text = '' + msg.text_lower = '' + msg.entities = '' + end + + -- else + -- print('Whitelist not enabled or is sudo') + end + + return msg +end + +function banhammer:action(msg, config, matches) + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + return + end + + if matches[1] == 'ban' then + local user_id = matches[3] + local chat_id = msg.chat.id + + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + if matches[2] == 'user' then + local text = banhammer:ban_user(user_id, chat_id, self) + utilities.send_reply(self, msg, text) + return + end + if matches[2] == 'delete' then + local text = banhammer:unban_user(user_id, chat_id, self, msg.chat.type) + utilities.send_reply(self, msg, text) + return + end + else + utilities.send_reply(self, msg, 'Das ist keine Chat-Gruppe') + return + end + end + + if matches[1] == 'kick' then + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + banhammer:kick_user(matches[2], msg.chat.id, self, true) + return + else + utilities.send_reply(self, msg, 'Das ist keine Chat-Gruppe') + return + end + end + + if matches[1] == 'whitelist' then + if matches[2] == 'enable' then + local hash = 'whitelist:enabled' + redis:set(hash, true) + utilities.send_reply(self, msg, 'Whitelist aktiviert') + return + end + + if matches[2] == 'disable' then + local hash = 'whitelist:enabled' + redis:del(hash) + utilities.send_reply(self, msg, 'Whitelist deaktiviert') + return + end + + if matches[2] == 'user' then + local hash = 'whitelist:user#id'..matches[3] + redis:set(hash, true) + utilities.send_reply(self, msg, 'User '..matches[3]..' whitelisted') + return + end + + if matches[2] == 'chat' then + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + local hash = 'whitelist:chat#id'..msg.chat.id + redis:set(hash, true) + utilities.send_reply(self, msg, 'Chat '..msg.chat.id..' whitelisted') + return + else + utilities.send_reply(self, msg, 'Das ist keine Chat-Gruppe!') + return + end + end + + if matches[2] == 'delete' and matches[3] == 'user' then + local hash = 'whitelist:user#id'..matches[4] + redis:del(hash) + utilities.send_reply(self, msg, 'User '..matches[4]..' von der Whitelist entfernt!') + return + end + + if matches[2] == 'delete' and matches[3] == 'chat' then + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + local hash = 'whitelist:chat#id'..msg.chat.id + redis:del(hash) + utilities.send_reply(self, msg, 'Chat '..msg.chat.id..' von der Whitelist entfernt') + return + else + utilities.send_reply(self, msg, 'Das ist keine Chat-Gruppe!') + return + end + end + end +end + +return banhammer \ No newline at end of file diff --git a/otouto/plugins/bitly.lua b/miku/plugins/bitly.lua similarity index 92% rename from otouto/plugins/bitly.lua rename to miku/plugins/bitly.lua index d1f9653..0b01102 100644 --- a/otouto/plugins/bitly.lua +++ b/miku/plugins/bitly.lua @@ -2,8 +2,8 @@ local bitly = {} local https = require('ssl.https') local json = require('dkjson') -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() function bitly:init(config) if not cred_data.bitly_access_token then diff --git a/otouto/plugins/bitly_create.lua b/miku/plugins/bitly_create.lua similarity index 86% rename from otouto/plugins/bitly_create.lua rename to miku/plugins/bitly_create.lua index 1b34ff1..db23436 100644 --- a/otouto/plugins/bitly_create.lua +++ b/miku/plugins/bitly_create.lua @@ -4,10 +4,10 @@ local http = require('socket.http') 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 utilities = require('miku.utilities') +local bindings = require('miku.bindings') local OAuth = require "OAuth" -local redis = (loadfile "./otouto/redis.lua")() +local redis = (loadfile "./miku/redis.lua")() function bitly_create:init(config) if not cred_data.bitly_client_id then @@ -25,7 +25,7 @@ function bitly_create:init(config) end bitly_create.triggers = { - "^/short (auth) (.+)$", + "^/short(auth)(.+)$", "^/short (auth)$", "^/short (unauth)$", "^/short (me)$", @@ -93,12 +93,16 @@ function bitly_create:action(msg, config, matches) if matches[1] == 'auth' and matches[2] then utilities.send_reply(self, msg, bitly_create:get_bitly_access_token(hash, matches[2]), true) + local message_id = redis:hget(hash, 'bitly_login_msg') + utilities.edit_message(self, msg.chat.id, message_id, '*Anmeldung abgeschlossen!*', true, true) + redis:hdel(hash, 'bitly_login_msg') return end if matches[1] == 'auth' then - utilities.send_reply(self, msg, 'Bitte logge dich ein und folge den Anweisungen:\n[Bei Bitly anmelden](https://bitly.com/oauth/authorize?client_id='..client_id..'&redirect_uri='..redirect_uri..')', true) - return + local result = utilities.send_reply(self, msg, '*Bitte logge dich ein und folge den Anweisungen.*', true, '{"inline_keyboard":[[{"text":"Bei Bitly anmelden","url":"https://bitly.com/oauth/authorize?client_id='..client_id..'&redirect_uri='..redirect_uri..'&state='..self.info.username..'"}]]}') + redis:hset(hash, 'bitly_login_msg', result.result.message_id) + return end if matches[1] == 'unauth' and bitly_access_token then @@ -127,7 +131,7 @@ function bitly_create:action(msg, config, matches) print('Not signed in, will use global bitly access_token') bitly_access_token = cred_data.bitly_access_token end - + if matches[2] == nil then long_url = url_encode(matches[1]) domain = 'bit.ly' diff --git a/miku/plugins/br.lua b/miku/plugins/br.lua new file mode 100644 index 0000000..6acfb48 --- /dev/null +++ b/miku/plugins/br.lua @@ -0,0 +1,51 @@ +local br = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +br.triggers = { + "br.de/nachrichten/(.*).html$" + } + +function br:get_br_article(article) + local url = 'https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url=%22http://www.br.de/nachrichten/'..article..'.html%22%20and%20xpath=%22//div[@id=%27content%27]/div[1]/div[2]/div[1]|//div[@class=%27lead_picture%27]/img%22&format=json' + local res,code = https.request(url) + local data = json.decode(res).query.results + if code ~= 200 then return "HTTP-Fehler" end + if not data then return "HTTP-Fehler" end + + local subtitle = data.div.h1.em + local title = string.sub(data.div.h1.content, 3) + local teaser = string.sub(data.div.p[1].content, 2) + if data.div.p[3] then + updated = data.div.p[3].content + else + updated = data.div.p[2].content + end + if data.img then + image_url = 'https://www.br.de'..data.img.src + end + local text = '*'..subtitle..' - '..title..'*'..teaser..'\n_'..updated..'_' + + if data.img then + return text, image_url + else + return text + end +end + +function br:action(msg, config, matches) + local article = URL.escape(matches[1]) + local text, image_url = br:get_br_article(article) + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url, 'br_teaser.jpg') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) +end + +return br \ No newline at end of file diff --git a/miku/plugins/btc.lua b/miku/plugins/btc.lua new file mode 100644 index 0000000..e4a88fa --- /dev/null +++ b/miku/plugins/btc.lua @@ -0,0 +1,40 @@ +local btc = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function btc:init(config) + btc.triggers = { + "^/btc$" + } + btc.doc = [[* +]]..config.cmd_pat..[[btc*: Zeigt aktuellen Bitcoin-Kurs an]] +end + +btc.command = 'btc' + +-- See https://bitcoinaverage.com/api +function btc:getBTCX() + local base_url = 'https://api.bitcoinaverage.com/ticker/global/' + -- Do request on bitcoinaverage, the final / is critical! + local res,code = https.request(base_url.."EUR/") + + if code ~= 200 then return nil end + local data = json.decode(res) + local ask = string.gsub(data.ask, "%.", ",") + local bid = string.gsub(data.bid, "%.", ",") + + -- Easy, it's right there + text = 'BTC/EUR\n'..'*Kaufen:* '..ask..'\n'..'*Verkaufen:* '..bid + return text +end + + +function btc:action(msg, config, matches) + utilities.send_reply(self, msg, btc:getBTCX(cur), true) +end + +return btc \ No newline at end of file diff --git a/miku/plugins/calc.lua b/miku/plugins/calc.lua new file mode 100644 index 0000000..e1e5dbc --- /dev/null +++ b/miku/plugins/calc.lua @@ -0,0 +1,39 @@ +local calc = {} + +local URL = require('socket.url') +local http = require('socket.http') +local utilities = require('miku.utilities') + +calc.command = 'calc ' + +function calc:init(config) + calc.triggers = { + "^/calc (.*)$" + } + calc.doc = [[* +]]..config.cmd_pat..[[calc* _[Ausdruck]_: Rechnet]] +end + +function calc:mathjs(exp) + local exp = string.gsub(exp, ",", "%.") + local url = 'http://api.mathjs.org/v1/' + url = url..'?expr='..URL.escape(exp) + local b,c = http.request(url) + local text = nil + if c == 200 then + text = '= '..string.gsub(b, "%.", ",") + + elseif c == 400 then + text = b + else + text = 'Unerwarteter Fehler\n' + ..'Ist api.mathjs.org erreichbar?' + end + return text +end + +function calc:action(msg, config, matches) + utilities.send_reply(self, msg, calc:mathjs(matches[1])) +end + +return calc diff --git a/miku/plugins/cats.lua b/miku/plugins/cats.lua new file mode 100644 index 0000000..bea66b2 --- /dev/null +++ b/miku/plugins/cats.lua @@ -0,0 +1,39 @@ +local cats = {} + +local HTTP = require('socket.http') +local utilities = require('miku.utilities') + +cats.command = 'cat [gif]' + +function cats:init(config) + if not cred_data.cat_apikey then + print('Missing config value: cat_apikey.') + print('cats.lua will be enabled, but there are more features with a key.') + end + + cats.triggers = { + "^/cat$", + "^/cat (gif)$" + } + + cats.doc = [[* +]]..config.cmd_pat..[[cat*: Postet eine zufällige Katze +*]]..config.cmd_pat..[[cat* _gif_: Postet eine zufällige, animierte Katze]] +end + + +local apikey = cred_data.cat_apikey or "" -- apply for one here: http://thecatapi.com/api-key-registration.html + +function cats:action(msg, config) + if matches[1] == 'gif' then + local url = 'http://thecatapi.com/api/images/get?type=gif&apikey='..apikey + local file = download_to_file(url, 'miau.gif') + utilities.send_document(self, msg.chat.id, file, nil, msg.message_id) + else + local url = 'http://thecatapi.com/api/images/get?type=jpg,png&apikey='..apikey + local file = download_to_file(url, 'miau.png') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end +end + +return cats diff --git a/miku/plugins/channel.lua b/miku/plugins/channel.lua new file mode 100644 index 0000000..0659d91 --- /dev/null +++ b/miku/plugins/channel.lua @@ -0,0 +1,63 @@ +local channel = {} + +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') + +channel.command = 'ch \\n ' +channel.doc = [[* +/ch*_ _ +__ + +Sendet eine Nachricht in den Kanal. Der Kanal kann per Username oder ID bestimmt werden, Markdown wird unterstützt. Du musst Administrator oder Besitzer des Kanals sein. + +Markdown-Syntax: + *Fetter Text* + _Kursiver Text_ + [Text](URL) + `Inline-Codeblock` + `‌`‌`Größere Code-Block über mehrere Zeilen`‌`‌` + +*Der Kanalname muss mit einem @ beginnen!*]] + +function channel:init(config) + channel.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ch', true).table +end + +function channel:action(msg, config) + local input = utilities.input(msg.text) + local output + if input then + local chat_id = utilities.get_word(input, 1) + local admin_list, t = bindings.getChatAdministrators(self, { chat_id = chat_id } ) + if admin_list then + local is_admin = false + for _, admin in ipairs(admin_list.result) do + if admin.user.id == msg.from.id then + is_admin = true + end + end + if is_admin then + local text = input:match('\n(.+)') + if text then + local success, result = utilities.send_message(self, chat_id, text, true, nil, true) + if success then + output = 'Deine Nachricht wurde versendet!' + else + output = 'Sorry, ich konnte deine Nachricht nicht senden.\n`' .. result.description .. '`' + end + else + output = 'Bitte gebe deine Nachricht ein. Markdown wird unterstützt.' + end + else + output = 'Es sieht nicht so aus, als wärst du der Administrator dieses Kanals.' + end + else + output = 'Sorry, ich konnte die Administratorenliste nicht abrufen. Falls du den Kanalnamen benutzt: Beginnt er mit einem @?\n`' .. t.description .. '`' + end + else + output = channel.doc + end + utilities.send_reply(self, msg, output, true) +end + +return channel diff --git a/miku/plugins/channels.lua b/miku/plugins/channels.lua new file mode 100644 index 0000000..beb77e8 --- /dev/null +++ b/miku/plugins/channels.lua @@ -0,0 +1,90 @@ +local channels = {} + +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() + +channels.command = 'channel ' + +function channels:init(config) + channels.triggers = { + "^/channel? (enable)", + "^/channel? (disable)" + } + channels.doc = [[* +]]..config.cmd_pat..[[channel* __/__: Aktiviert/deaktiviert den Bot im Chat]] +end + +-- Checks if bot was disabled on specific chat +function channels:is_channel_disabled(msg) + local hash = 'chat:'..msg.chat.id..':disabled' + local disabled = redis:get(hash) + + if not disabled or disabled == "false" then + return false + end + + return disabled +end + +function channels:enable_channel(msg) + local hash = 'chat:'..msg.chat.id..':disabled' + local disabled = redis:get(hash) + if disabled then + print('Setting redis variable '..hash..' to false') + redis:set(hash, false) + return 'Channel aktiviert' + else + return 'Channel ist nicht deaktiviert!' + end +end + +function channels:disable_channel(msg) + local hash = 'chat:'..msg.chat.id..':disabled' + local disabled = redis:get(hash) + if disabled ~= "true" then + print('Setting redis variable '..hash..' to true') + redis:set(hash, true) + return 'Channel deaktiviert' + else + return 'Channel ist bereits deaktiviert!' + end +end + +function channels:pre_process(msg, self, config) + -- If is sudo can reeanble the channel + if is_sudo(msg, config) then + if msg.text == "/channel enable" then + channels:enable_channel(msg) + end + end + + if channels:is_channel_disabled(msg) then + print('Channel wurde deaktiviert') + msg.text = '' + msg.text_lower = '' + msg.entities = '' + end + + return msg +end + +function channels:action(msg, config, matches) + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + return + end + + -- Enable a channel + if matches[1] == 'enable' then + utilities.send_reply(self, msg, channels:enable_channel(msg)) + return + end + -- Disable a channel + if matches[1] == 'disable' then + utilities.send_reply(self, msg, channels:disable_channel(msg)) + return + end +end + +return channels \ No newline at end of file diff --git a/miku/plugins/cleverbot.lua b/miku/plugins/cleverbot.lua new file mode 100644 index 0000000..743085c --- /dev/null +++ b/miku/plugins/cleverbot.lua @@ -0,0 +1,35 @@ +local cleverbot = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local utilities = require('miku.utilities') +local json = require('dkjson') + +function cleverbot:init(config) + cleverbot.triggers = { + "^/cbot (.*)$" + } + + cleverbot.doc = [[* +]]..config.cmd_pat..[[cbot* __*: Befragt den Cleverbot]] +end + +cleverbot.command = 'cbot ' + +function cleverbot:action(msg, config) + local text = msg.text + local url = "https://brawlbot.tk/apis/chatter-bot-api/cleverbot.php?text="..URL.escape(text) + local query = https.request(url) + if query == nil then utilities.send_reply(self, msg, 'Ein Fehler ist aufgetreten :(') return end + local decode = json.decode(query) + local answer = string.gsub(decode.clever, "Ä", "Ä") + local answer = string.gsub(answer, "ä", "ä") + local answer = string.gsub(answer, "Ö", "Ö") + local answer = string.gsub(answer, "ö", "ö") + local answer = string.gsub(answer, "Ü", "Ü") + local answer = string.gsub(answer, "ü", "ü") + local answer = string.gsub(answer, "ß", "ß") + utilities.send_reply(self, msg, answer) +end + +return cleverbot diff --git a/miku/plugins/clypit.lua b/miku/plugins/clypit.lua new file mode 100644 index 0000000..913837c --- /dev/null +++ b/miku/plugins/clypit.lua @@ -0,0 +1,33 @@ +local clypit = {} + +local http = require('socket.http') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +clypit.triggers = { + "clyp.it/([A-Za-z0-9-_-]+)" +} + +function clypit:get_clypit_details(shortcode) + local BASE_URL = "http://api.clyp.it" + local url = BASE_URL..'/'..shortcode + local res,code = http.request(url) + if code ~= 200 then return nil end + local data = json.decode(res) + + local title = data.Title + local duration = data.Duration + + local audio = download_to_file(data.Mp3Url) + return audio, title, duration +end + +function clypit:action(msg, config, matches) + utilities.send_typing(self, msg.chat.id, 'upload_audio') + local audio, title, duration = clypit:get_clypit_details(matches[1]) + if not audio then return utilities.send_reply(self, msg, config.errors.connection) end + utilities.send_audio(self, msg.chat.id, audio, nil, msg.message_id, duration, nil, title) +end + +return clypit diff --git a/otouto/plugins/control.lua b/miku/plugins/control.lua similarity index 67% rename from otouto/plugins/control.lua rename to miku/plugins/control.lua index 564d652..db8e800 100644 --- a/otouto/plugins/control.lua +++ b/miku/plugins/control.lua @@ -1,14 +1,14 @@ local control = {} -local bot = require('otouto.bot') -local utilities = require('otouto.utilities') +local bot = require('miku.bot') +local utilities = require('miku.utilities') local cmd_pat -- Prevents the command from being uncallable. function control:init(config) cmd_pat = config.cmd_pat control.triggers = utilities.triggers(self.info.username, cmd_pat, - {'^'..cmd_pat..'script'}):t('reload', true):t('halt').table + {'^'..cmd_pat..'script'}):t('restart', true):t('halt').table end function control:action(msg, config) @@ -17,25 +17,25 @@ function control:action(msg, config) return end - if msg.date < os.time() - 1 then return end + if msg.date < os.time() - 2 then return end - if msg.text_lower:match('^'..cmd_pat..'reload') then + if msg.text_lower:match('^'..cmd_pat..'restart') then for pac, _ in pairs(package.loaded) do - if pac:match('^otouto%.plugins%.') then + if pac:match('^miku%.plugins%.') then package.loaded[pac] = nil end end - package.loaded['otouto.bindings'] = nil - package.loaded['otouto.utilities'] = nil + package.loaded['miku.bindings'] = nil + package.loaded['miku.utilities'] = nil package.loaded['config'] = nil if msg.text_lower:match('%+config') then for k, v in pairs(require('config')) do config[k] = v end end bot.init(self, config) - utilities.send_reply(self, msg, 'Bot reloaded!') + utilities.send_reply(self, msg, 'Bot neu gestartet!') elseif msg.text_lower:match('^'..cmd_pat..'halt') then self.is_started = false - utilities.send_reply(self, msg, 'Stopping bot!') + utilities.send_reply(self, msg, 'Stoppe Bot!') elseif msg.text_lower:match('^'..cmd_pat..'script') then local input = msg.text_lower:match('^'..cmd_pat..'script\n(.+)') if not input then @@ -46,7 +46,7 @@ function control:action(msg, config) for command in input:gmatch('(.-)\n') do command = utilities.trim(command) msg.text = command - bot.on_msg_receive(self, msg) + bot.on_msg_receive(self, msg, config) end end diff --git a/otouto/plugins/creds.lua b/miku/plugins/creds.lua similarity index 97% rename from otouto/plugins/creds.lua rename to miku/plugins/creds.lua index ff32f46..913529e 100644 --- a/otouto/plugins/creds.lua +++ b/miku/plugins/creds.lua @@ -1,7 +1,7 @@ local creds_manager = {} -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() function creds_manager:init(config) creds_manager.triggers = { diff --git a/miku/plugins/currency.lua b/miku/plugins/currency.lua new file mode 100644 index 0000000..71ddcf8 --- /dev/null +++ b/miku/plugins/currency.lua @@ -0,0 +1,64 @@ +local currency = {} + +local HTTPS = require('ssl.https') +local utilities = require('miku.utilities') + +currency.command = 'cash [Menge] ' + +function currency:init(config) + currency.triggers = { + "^/cash ([A-Za-z]+)$", + "^/cash ([A-Za-z]+) ([A-Za-z]+)$", + "^/cash (%d+[%d%.,]*) ([A-Za-z]+) ([A-Za-z]+)$", + "^(/eur)$" + } + currency.doc = [[* +]]..config.cmd_pat..[[cash* _[Menge]_ __ __ +Beispiel: _]]..config.cmd_pat..[[cash 5 USD EUR_]] +end + +function currency:action(msg, config) + if not matches[2] then + from = string.upper(matches[1]) + to = 'EUR' + amount = 1 + elseif matches[3] then + from = string.upper(matches[2]) + to = string.upper(matches[3]) + amount = matches[1] + else + from = string.upper(matches[1]) + to = string.upper(matches[2]) + amount = 1 + end + + local amount = string.gsub(amount, ",", ".") + amount = tonumber(amount) + local result = 1 + local BASE_URL = 'https://www.google.com/finance/converter' + if from == to then + utilities.send_reply(self, msg, 'Jaja, sehr witzig...') + return + end + + local url = BASE_URL..'?from='..from..'&to='..to..'&a='..amount + local str, res = HTTPS.request(url) + if res ~= 200 then + utilities.send_reply(self, msg, config.errors.connection) + return + end + + local str = str:match('(.*) %u+') + if not str then + utilities.send_reply(self, msg, 'Keine gültige Währung - sieh dir die Währungsliste bei [Google Finanzen](https://www.google.com/finance/converter) an.', true) + return + end + local result = string.format('%.2f', str) + local result = string.gsub(result, "%.", ",") + + local amount = tostring(string.gsub(amount, "%.", ",")) + local output = amount..' '..from..' = *'..result..' '..to..'*' + utilities.send_reply(self, msg, output, true) +end + +return currency \ No newline at end of file diff --git a/miku/plugins/dailymotion.lua b/miku/plugins/dailymotion.lua new file mode 100644 index 0000000..c865e2c --- /dev/null +++ b/miku/plugins/dailymotion.lua @@ -0,0 +1,31 @@ +local dailymotion = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +dailymotion.triggers = { + "dailymotion.com/video/([A-Za-z0-9-_-]+)" +} + +local BASE_URL = 'https://api.dailymotion.com' + +function dailymotion:send_dailymotion_info (dm_code) + local url = BASE_URL..'/video/'..dm_code + local res,code = https.request(url) + if code ~= 200 then return nil end + local data = json.decode(res) + + local title = data.title + local channel = data.channel + local text = '*'..title..'*\nHochgeladen in die Kategorie *'..channel..'*' + return text +end + +function dailymotion:action(msg, config, matches) + local text = dailymotion:send_dailymotion_info(matches[1]) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + utilities.send_reply(self, msg, text, true) +end + +return dailymotion diff --git a/miku/plugins/deviantart.lua b/miku/plugins/deviantart.lua new file mode 100644 index 0000000..34861e8 --- /dev/null +++ b/miku/plugins/deviantart.lua @@ -0,0 +1,52 @@ +local deviantart = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +deviantart.triggers = { + "http://(.*).deviantart.com/art/(.*)" +} + +local BASE_URL = 'https://backend.deviantart.com' + +function deviantart:get_da_data (da_code) + local url = BASE_URL..'/oembed?url='..da_code + local res,code = https.request(url) + if code ~= 200 then return nil end + local data = json.decode(res) + return data +end + +function deviantart:send_da_data (data) + local title = data.title + local category = data.category + local author_name = data.author_name + local text = title..' von '..author_name..'\n'..category + + if data.rating == "adult" then + return title..' von '..author_name..'\n'..category..'\n(NSFW)' + else + local image_url = data.fullsize_url + if image_url == nil then + image_url = data.url + end + local file = download_to_file(image_url) + return text, file + end +end + +function deviantart:action(msg, config, matches) + local data = deviantart:get_da_data('http://'..matches[1]..'.deviantart.com/art/'..matches[2]) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + + local text, file = deviantart:send_da_data(data) + if file then + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + else + utilities.send_reply(self, msg, text) + return + end +end + +return deviantart diff --git a/miku/plugins/dhl.lua b/miku/plugins/dhl.lua new file mode 100644 index 0000000..806bae3 --- /dev/null +++ b/miku/plugins/dhl.lua @@ -0,0 +1,38 @@ +local dhl = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function dhl:init(config) + dhl.triggers = { + "/dhl (%d+)$" + } + dhl.doc = [[* +]]..config.cmd_pat..[[dhl* __: Aktueller Status der Sendung]] +end + + +local BASE_URL = 'https://mobil.dhl.de' + +function dhl:sendungsstatus(id) + local url = BASE_URL..'/shipmentdetails.html?shipmentId='..id + local res,code = https.request(url) + if code ~= 200 then return "Fehler beim Abrufen von mobil.dhl.de" end + local status = string.match(res, "
(.-)
") + local status = all_trim(status) + local zeit = string.match(res, "
(.-)
") + local zeit = all_trim(zeit) + if not zeit or zeit == '
' then + return status + end + return '*'..status..'*\n_Stand: '..zeit..'_' +end + +function dhl:action(msg, config, matches) + local sendungs_id = matches[1] + if string.len(sendungs_id) < 8 then return end + utilities.send_reply(self, msg, dhl:sendungsstatus(sendungs_id), true) +end + +return dhl diff --git a/miku/plugins/dropbox.lua b/miku/plugins/dropbox.lua new file mode 100644 index 0000000..47770f3 --- /dev/null +++ b/miku/plugins/dropbox.lua @@ -0,0 +1,39 @@ +-- Doesn't use the API for now, maybe we can integrate some cool features? + +local dropbox = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +dropbox.triggers = { + "dropbox.com/s/([a-z0-9]+)/(.*)" +} + +function dropbox:action(msg, config, matches) + local folder = matches[1] + local file = string.gsub(matches[2], "?dl=0", "") + local link = 'https://dl.dropboxusercontent.com/s/'..folder..'/'..file + + local v,code = https.request(link) + if code == 200 then + if string.ends(link, ".png") or string.ends(link, ".jpe?g")then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(link) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + return + elseif string.ends(link, ".webp") or string.ends(link, ".gif") then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(link) + utilities.send_document(self, msg.chat.id, file, nil, msg.message_id) + return + else + utilities.send_reply(self, msg, link) + end + return + else + return + end +end + +return dropbox diff --git a/miku/plugins/echo.lua b/miku/plugins/echo.lua new file mode 100644 index 0000000..e5f1883 --- /dev/null +++ b/miku/plugins/echo.lua @@ -0,0 +1,42 @@ +local echo = {} + +local utilities = require('miku.utilities') + +echo.command = 'echo ' + +function echo:init(config) + echo.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('echo', true).table + echo.inline_triggers = { + "^e (.*)" + } + echo.doc = [[* +]]..config.cmd_pat..[[echo* __: Gibt den Text aus]] +end + +function echo:inline_callback(inline_query, config, matches) + local text = matches[1] + local results = '[' + + -- enable custom markdown button + if text:match('%[.*%]%(.*%)') or text:match('%*.*%*') or text:match('_.*_') or text:match('`.*`') then + results = results..'{"type":"article","id":"'..math.random(100000000000000000)..'","thumb_url":"https://anditest.perseus.uberspace.de/inlineQuerys/echo/custom.jpg","title":"Eigenes Markdown","description":"'..text..'","input_message_content":{"message_text":"'..text..'","parse_mode":"Markdown"}},' + end + + local results = results..'{"type":"article","id":"'..math.random(100000000000000000)..'","thumb_url":"https://anditest.perseus.uberspace.de/inlineQuerys/echo/fett.jpg","title":"Fett","description":"*'..text..'*","input_message_content":{"message_text":"*'..text..'*","parse_mode":"Markdown"}},{"type":"article","id":"'..math.random(100000000000000000)..'","thumb_url":"https://anditest.perseus.uberspace.de/inlineQuerys/echo/kursiv.jpg","title":"Kursiv","description":"_'..text..'_","input_message_content":{"message_text":"_'..text..'_","parse_mode":"Markdown"}},{"type":"article","id":"'..math.random(100000000000000000)..'","thumb_url":"https://anditest.perseus.uberspace.de/inlineQuerys/echo/fixedsys.jpg","title":"Feste Breite","description":"`'..text..'`","input_message_content":{"message_text":"`'..text..'`","parse_mode":"Markdown"}}]' + utilities.answer_inline_query(self, inline_query, results, 0) +end + +function echo:action(msg) + local input = utilities.input(msg.text) + 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) .. '"' + end + utilities.send_message(self, msg.chat.id, input, true, nil, true) + end +end + +return echo diff --git a/miku/plugins/entergroup.lua b/miku/plugins/entergroup.lua new file mode 100644 index 0000000..e2958d6 --- /dev/null +++ b/miku/plugins/entergroup.lua @@ -0,0 +1,57 @@ +local entergroup = {} + +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +entergroup.triggers = { + '/nil' +} + +function entergroup:chat_new_user(msg, self) + local user_name = msg.new_chat_member.first_name + local chat_title = msg.chat.title + if msg.from.username then + at_name = ' (@'..msg.from.username..')' + else + at_name = '' + end + if msg.from.id == msg.new_chat_member.id then -- entered through link + added_by = '' + else + added_by = '\n'..msg.from.name..at_name..' hat dich hinzugefügt!' + end + if msg.new_chat_member.id == self.info.id then -- don't say hello to ourselves + return + end + local text = 'Hallo '..user_name..', willkommen bei *'..chat_title..'*!'..added_by + utilities.send_reply(self, msg, text, true) +end + +function entergroup:chat_del_user(msg, self) + if msg.left_chat_member.id == msg.from.id then -- silent ignore, if user wasn't kicked + return + end + local user_name = msg.left_chat_member.first_name + if msg.from.username then + at_name = ' (@'..msg.from.username..')' + else + at_name = '' + end + local text = user_name..' wurde von '..msg.from.first_name..at_name..' aus der Gruppe gekickt.' + utilities.send_reply(self, msg, text, true) +end + +function entergroup:pre_process(msg, self) + if msg.new_chat_member then + entergroup:chat_new_user(msg, self) + elseif msg.left_chat_member then + entergroup:chat_del_user(msg, self) + end + + return msg +end + +function entergroup:action(msg) +end + +return entergroup diff --git a/otouto/plugins/expand.lua b/miku/plugins/expand.lua similarity index 95% rename from otouto/plugins/expand.lua rename to miku/plugins/expand.lua index 60b056e..4435394 100644 --- a/otouto/plugins/expand.lua +++ b/miku/plugins/expand.lua @@ -1,7 +1,7 @@ local expand = {} local http = require('socket.http') -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') function expand:init(config) expand.triggers = { diff --git a/otouto/plugins/facebook.lua b/miku/plugins/facebook.lua similarity index 85% rename from otouto/plugins/facebook.lua rename to miku/plugins/facebook.lua index 2bbd215..2d29a05 100644 --- a/otouto/plugins/facebook.lua +++ b/miku/plugins/facebook.lua @@ -4,9 +4,9 @@ local http = require('socket.http') 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 redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') +local redis = (loadfile "./miku/redis.lua")() function facebook:init(config) if not cred_data.fb_access_token then @@ -53,7 +53,7 @@ function facebook:fb_post (id, story_id) local message = data.message local name = data.name if data.link then - link = '\n'..data.name..':\n'..data.link + link = '\n'..data.name..':\n'..utilities.md_escape(data.link) else link = "" end @@ -76,7 +76,7 @@ function facebook:send_facebook_photo(photo_id, receiver) local from = '*'..data.from.name..'*' if data.name then - text = from..' hat ein Bild gepostet:\n'..data.name + text = from..' hat ein Bild gepostet:\n'..utilities.md_escape(data.name) else text = from..' hat ein Bild gepostet:' end @@ -93,12 +93,7 @@ function facebook:send_facebook_video(video_id) local from = '*'..data.from.name..'*' local description = data.description local source = data.source - if data.title then - text = from..' hat ein Video gepostet:\n'..description..'\n['..data.title..']('..source..')' - else - text = from..' hat ein Video gepostet:\n'..description..'\n'..utilities.md_escape(source) - end - return text + return from..' hat ein Video gepostet:\n'..description, source, data.title end function facebook:facebook_info(name) @@ -156,9 +151,10 @@ function facebook:action(msg, config, matches) else photo_id = matches[4] end - utilities.send_typing(self, msg.chat.id, 'upload_photo') local text, image_url = facebook:send_facebook_photo(photo_id, receiver) - local file = download_to_file(image_url) + if not image_url then return end + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url, 'photo.jpg') utilities.send_reply(self, msg, text, true) utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) return @@ -168,8 +164,14 @@ function facebook:action(msg, config, matches) else video_id = matches[3] end - local output = facebook:send_facebook_video(video_id) - utilities.send_reply(self, msg, output, true) + local output, video_url, title = facebook:send_facebook_video(video_id) + if not title then + title = 'Video aufrufen' + else + title = 'VIDEO: '..title + end + if not video_url then return end + utilities.send_reply(self, msg, output, true, '{"inline_keyboard":[[{"text":"'..utilities.md_escape(title)..'","url":"'..video_url..'"}]]}') return else utilities.send_reply(self, msg, facebook:facebook_info(matches[1]), true) diff --git a/miku/plugins/fefe.lua b/miku/plugins/fefe.lua new file mode 100644 index 0000000..a4b3911 --- /dev/null +++ b/miku/plugins/fefe.lua @@ -0,0 +1,36 @@ +local fefe = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +fefe.triggers = { + "blog.fefe.de/%?ts=%w%w%w%w%w%w%w%w" +} + +function fefe:post(id) + local url = 'http://'..id + local results, code = https.request(url) + if code ~= 200 then return "HTTP-Fehler" end + if string.match(results, "No entries found.") then return "Eintrag nicht gefunden." end + + local line = string.sub( results, string.find(results, "
  • %[l]", "") + -- replace "

    " with newline; "" and "" with "*" + local text = text:gsub("

    ", "\n\n"):gsub("

    ", "\n\n") + local text = text:gsub("", "*"):gsub("", "*") + local text = text:gsub("", "_"):gsub("", "_") + -- format quotes and links markdown-like + local text = text:gsub("", ")["):gsub("", "]") + local text = text:gsub("

    ", "\n\n> "):gsub("
    ", "\n\n") + + return text +end + +function fefe:action(msg, config, matches) + utilities.send_reply(self, msg, fefe:post(matches[1])) +end + +return fefe diff --git a/miku/plugins/flickr.lua b/miku/plugins/flickr.lua new file mode 100644 index 0000000..9670cf0 --- /dev/null +++ b/miku/plugins/flickr.lua @@ -0,0 +1,76 @@ +local flickr = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function flickr:init(config) + if not cred_data.flickr_apikey then + print('Missing config value: flickr_apikey.') + print('flickr.lua will not be enabled.') + return + end + + flickr.triggers = { + "flickr.com/photos/([A-Za-z0-9-_-]+)/([0-9]+)" + } +end + +local BASE_URL = 'https://api.flickr.com/services/rest' + +local makeOurDate = function(dateString) + local pattern = "(%d+)%-(%d+)%-(%d+) (%d+)%:(%d+)%:(%d+)" + local year, month, day, hours, minutes, seconds = dateString:match(pattern) + return day..'.'..month..'.'..year..' um '..hours..':'..minutes..':'..seconds..' Uhr' +end + +function flickr:get_flickr_photo_data (photo_id) + local apikey = cred_data.flickr_apikey + local url = BASE_URL..'/?method=flickr.photos.getInfo&api_key='..apikey..'&photo_id='..photo_id..'&format=json&nojsoncallback=1' + local res,code = https.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res).photo + return data +end + +function flickr:send_flickr_photo_data(data) + local title = data.title._content + local username = data.owner.username + local taken = data.dates.taken + local views = data.views + if data.usage.candownload == 1 then + local text = '"'..title..'", aufgenommen am '..makeOurDate(taken)..' von '..username..' ('..comma_value(data.views)..' Aufrufe)' + local image_url = 'https://farm'..data.farm..'.staticflickr.com/'..data.server..'/'..data.id..'_'..data.originalsecret..'_o_d.'..data.originalformat + if data.originalformat == 'gif' then + return text, image_url, true + else + return text, image_url + end + else + return '"'..title..'", aufgenommen '..taken..' von '..username..' ('..data.views..' Aufrufe)\nBild konnte nicht gedownloadet werden (Keine Berechtigung)' + end +end + +function flickr:action(msg, config, matches) + local data = flickr:get_flickr_photo_data(matches[2]) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + local text, image_url, isgif = flickr:send_flickr_photo_data(data) + + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + if isgif then + utilities.send_document(self, msg.chat.id, file, text, msg.message_id) + return + else + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + return + end + else + utilities.send_reply(self, msg, text) + return + end +end + +return flickr \ No newline at end of file diff --git a/miku/plugins/flickr_search.lua b/miku/plugins/flickr_search.lua new file mode 100644 index 0000000..a8ab715 --- /dev/null +++ b/miku/plugins/flickr_search.lua @@ -0,0 +1,53 @@ +local flickr_search = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function flickr_search:init(config) + if not cred_data.flickr_apikey then + print('Missing config value: flickr_apikey.') + print('flickr_search.lua will not be enabled.') + return + end + + flickr_search.triggers = { + "^/flickr (.*)$" + } +end + +flickr_search.command = 'flickr ' + +local apikey = cred_data.flickr_apikey +local BASE_URL = 'https://api.flickr.com/services/rest' + +function flickr_search:get_flickr (term) + local url = BASE_URL..'/?method=flickr.photos.search&api_key='..apikey..'&format=json&nojsoncallback=1&privacy_filter=1&safe_search=3&extras=url_o&text='..term + local b,c = https.request(url) + if c ~= 200 then return nil end + local photo = json.decode(b).photos.photo + -- truly randomize + math.randomseed(os.time()) + -- random max json table size + local i = math.random(#photo) + local link_image = photo[i].url_o + return link_image +end + +function flickr_search:action(msg, config, matches) + local url = flickr_search:get_flickr(matches[1]) + if not url then utilities.send_reply(self, msg, config.errors.results) return end + + local file = download_to_file(url) + + if string.ends(url, ".gif") then + utilities.send_document(self, msg.chat.id, file, url) + return + else + utilities.send_photo(self, msg.chat.id, file, url) + return + end +end + +return flickr_search \ No newline at end of file diff --git a/otouto/plugins/forecast.lua b/miku/plugins/forecast.lua similarity index 97% rename from otouto/plugins/forecast.lua rename to miku/plugins/forecast.lua index 47fcc3c..47cef27 100644 --- a/otouto/plugins/forecast.lua +++ b/miku/plugins/forecast.lua @@ -3,9 +3,9 @@ local forecast = {} 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 redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') +local redis = (loadfile "./miku/redis.lua")() function forecast:init(config) if not cred_data.forecastio_apikey then @@ -36,7 +36,7 @@ function forecast:init(config) ]] end -forecast.command = 'forecast' +forecast.command = 'f [Ort]' local BASE_URL = "https://api.forecast.io/forecast" local apikey = cred_data.forecastio_apikey diff --git a/miku/plugins/gImages.lua b/miku/plugins/gImages.lua new file mode 100644 index 0000000..ebe1c46 --- /dev/null +++ b/miku/plugins/gImages.lua @@ -0,0 +1,242 @@ + -- You need a Google API key and a Google Custom Search Engine set up to use this, in config.google_api_key and config.google_cse_key, respectively. + -- You must also sign up for the CSE in the Google Developer Console, and enable image results. + +local gImages = {} + +local HTTPS = require('ssl.https') +HTTPS.timeout = 10 +local URL = require('socket.url') +local JSON = require('dkjson') +local redis = (loadfile "./miku/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function gImages:init(config) + if not cred_data.google_apikey then + print('Missing config value: google_apikey.') + print('gImages.lua will not be enabled.') + return + elseif not cred_data.google_cse_id then + print('Missing config value: google_cse_id.') + print('gImages.lua will not be enabled.') + return + end + + gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('img', true):t('i', true).table + gImages.doc = [[* +]]..config.cmd_pat..[[img* __ +Sucht Bild mit Google und versendet es (SafeSearch aktiv) +Alias: *]]..config.cmd_pat..[[i*]] +end + +gImages.command = 'img ' + +-- Yes, the callback is copied from below, but I can't think of another method :\ +function gImages:callback(callback, msg, self, config, input) + if not msg then return end + utilities.answer_callback_query(self, callback, 'Suche nochmal nach "'..URL.unescape(input)..'"') + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local hash = 'telegram:cache:gImages' + local results = redis:smembers(hash..':'..string.lower(URL.unescape(input))) + + if not results[1] then + print('doing web request') + results = gImages:get_image(input) + if results == 403 then + utilities.send_reply(self, msg, config.errors.quotaexceeded, true) + return + elseif not results then + utilities.send_reply(self, msg, config.errors.results, true) + return + end + gImages:cache_result(results, input) + end + + -- Random image from table + local i = math.random(#results) + + -- Thanks to Amedeo for this! + local failed = true + local nofTries = 0 + + while failed and nofTries < #results do + if results[i].image then + img_url = results[i].link + mimetype = results[i].mime + context = results[i].image.contextLink + else -- from cache + img_url = results[i] + mimetype = redis:hget(hash..':'..img_url, 'mime') + context = redis:hget(hash..':'..img_url, 'contextLink') + end + + -- It's important to save the image with the right ending! + if mimetype == 'image/gif' then + file = download_to_file(img_url, 'img.gif') + elseif mimetype == 'image/png' then + file = download_to_file(img_url, 'img.png') + elseif mimetype == 'image/jpeg' then + file = download_to_file(img_url, 'img.jpg') + else + file = nil + end + + if not file then + nofTries = nofTries + 1 + i = i+1 + if i > #results then + i = 1 + end + else + failed = false + end + + end + + if failed then + utilities.send_reply(self, msg, 'Fehler beim Herunterladen eines Bildes.', true, '{"inline_keyboard":[[{"text":"Nochmal versuchen","callback_data":"@'..self.info.username..' gImages:'..input..'"}]]}') + return + end + + if mimetype == 'image/gif' then + result = utilities.send_document(self, msg.chat.id, file, nil, msg.message_id, '{"inline_keyboard":[[{"text":"Seite aufrufen","url":"'..context..'"},{"text":"Bild aufrufen","url":"'..img_url..'"},{"text":"Nochmal suchen","callback_data":"@'..self.info.username..' gImages:'..input..'"}]]}') + else + result = utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id, '{"inline_keyboard":[[{"text":"Seite aufrufen","url":"'..context..'"},{"text":"Bild aufrufen","url":"'..img_url..'"},{"text":"Nochmal suchen","callback_data":"@'..self.info.username..' gImages:'..input..'"}]]}') + end + + if not result then + utilities.send_reply(self, msg, config.errors.connection, true, '{"inline_keyboard":[[{"text":"Nochmal versuchen","callback_data":"@'..self.info.username..' gImages:'..input..'"}]]}') + return + end +end + +function gImages:get_image(input) + local apikey = cred_data.google_apikey -- 100 requests is RIDICULOUS, Google! + local cseid = cred_data.google_cse_id + local BASE_URL = 'https://www.googleapis.com/customsearch/v1' + local url = BASE_URL..'/?searchType=image&alt=json&num=10&key='..apikey..'&cx='..cseid..'&safe=high'..'&q=' .. input .. '&fields=items(link,mime,image(contextLink))' + local jstr, res = HTTPS.request(url) + local jdat = JSON.decode(jstr).items + + if not jdat then + return 'NORESULTS' + end + + if jdat.error then + if jdat.error.code == 403 then + return 403 + else + return false + end + end + + return jdat +end + +function gImages:cache_result(results, text) + local cache = {} + for v in pairs(results) do + table.insert(cache, results[v].link) + end + for n, link in pairs(cache) do + redis:hset('telegram:cache:gImages:'..link, 'mime', results[n].mime) + redis:hset('telegram:cache:gImages:'..link, 'contextLink', results[n].image.contextLink) + redis:expire('telegram:cache:gImages:'..link, 1209600) + end + cache_data('gImages', string.lower(text), cache, 1209600, 'set') +end + +function gImages:action(msg, config, matches) + 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, gImages.doc, true, msg.message_id, true) + return + end + end + + print ('Checking if search contains blacklisted word: '..input) + if is_blacklisted(input) then + utilities.send_reply(self, msg, 'Vergiss es! ._.') + return + end + + utilities.send_typing(self, msg.chat.id, 'upload_photo') + + local hash = 'telegram:cache:gImages' + local results = redis:smembers(hash..':'..string.lower(input)) + + if not results[1] then + print('doing web request') + results = gImages:get_image(URL.escape(input)) + if results == 403 then + utilities.send_reply(self, msg, config.errors.quotaexceeded, true) + return + elseif not results or results == 'NORESULTS' then + utilities.send_reply(self, msg, config.errors.results, true) + return + end + gImages:cache_result(results, input) + end + + -- Random image from table + local i = math.random(#results) + + -- Thanks to Amedeo for this! + local failed = true + local nofTries = 0 + + while failed and nofTries < #results do + if results[i].image then + img_url = results[i].link + mimetype = results[i].mime + context = results[i].image.contextLink + else -- from cache + img_url = results[i] + mimetype = redis:hget(hash..':'..img_url, 'mime') + context = redis:hget(hash..':'..img_url, 'contextLink') + end + + -- It's important to save the image with the right ending! + if mimetype == 'image/gif' then + file = download_to_file(img_url, 'img.gif') + elseif mimetype == 'image/png' then + file = download_to_file(img_url, 'img.png') + elseif mimetype == 'image/jpeg' then + file = download_to_file(img_url, 'img.jpg') + else + file = nil + end + + if not file then + nofTries = nofTries + 1 + i = i+1 + if i > #results then + i = 1 + end + else + failed = false + end + + end + + if failed then + utilities.send_reply(self, msg, 'Fehler beim Herunterladen eines Bildes.', true, '{"inline_keyboard":[[{"text":"Nochmal versuchen","callback_data":"@'..self.info.username..' gImages:'..URL.escape(input)..'"}]]}') + return + end + + if mimetype == 'image/gif' then + result = utilities.send_document(self, msg.chat.id, file, nil, msg.message_id, '{"inline_keyboard":[[{"text":"Seite aufrufen","url":"'..context..'"},{"text":"Bild aufrufen","url":"'..img_url..'"},{"text":"Nochmal suchen","callback_data":"@'..self.info.username..' gImages:'..URL.escape(input)..'"}]]}') + else + result = utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id, '{"inline_keyboard":[[{"text":"Seite aufrufen","url":"'..context..'"},{"text":"Bild aufrufen","url":"'..img_url..'"},{"text":"Nochmal suchen","callback_data":"@'..self.info.username..' gImages:'..URL.escape(input)..'"}]]}') + end + + if not result then + utilities.send_reply(self, msg, config.errors.connection, true, '{"inline_keyboard":[[{"text":"Nochmal versuchen","callback_data":"@'..self.info.username..' gImages:'..URL.escape(input)..'"}]]}') + return + end +end + +return gImages diff --git a/miku/plugins/gMaps.lua b/miku/plugins/gMaps.lua new file mode 100644 index 0000000..801a0af --- /dev/null +++ b/miku/plugins/gMaps.lua @@ -0,0 +1,43 @@ +local gMaps = {} + +local URL = require('socket.url') +local utilities = require('miku.utilities') + +gMaps.command = 'loc ' + +function gMaps:init(config) + gMaps.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('loc', true).table + gMaps.doc = [[* +]]..config.cmd_pat..[[loc* __: Sendet Ort via Google Maps]] +end + +function gMaps:get_staticmap(area, lat, lon) + local base_api = "https://maps.googleapis.com/maps/api" + local url = base_api .. "/staticmap?size=600x300&zoom=12¢er="..URL.escape(area).."&markers=color:red"..URL.escape("|"..area) + + local file = download_to_file(url) + return file +end + +function gMaps: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, gMaps.doc, true, msg.message_id, true) + return + end + end + utilities.send_typing(self, msg.chat.id, 'find_location') + local coords = utilities.get_coords(input, config) + if type(coords) == 'string' then + utilities.send_reply(self, msg, coords) + return + end + + utilities.send_location(self, msg.chat.id, coords.lat, coords.lon, msg.message_id) + utilities.send_photo(self, msg.chat.id, gMaps:get_staticmap(input, coords.lat, coords.lon), nil, msg.message_id) +end + +return gMaps diff --git a/otouto/plugins/gSearch.lua b/miku/plugins/gSearch.lua similarity index 78% rename from otouto/plugins/gSearch.lua rename to miku/plugins/gSearch.lua index 9331606..05ede88 100644 --- a/otouto/plugins/gSearch.lua +++ b/miku/plugins/gSearch.lua @@ -3,17 +3,15 @@ local gSearch = {} local HTTPS = require('ssl.https') local URL = require('socket.url') local JSON = require('dkjson') -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') -gSearch.command = 'google ' +gSearch.command = 'google ' 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 -Sendet Suchergebnisse von Google -Alias: ]]..config.cmd_pat..[[g -```]] + gSearch.doc = [[* +]]..config.cmd_pat..[[google* __: Sendet Suchergebnisse von Google +Alias: _]]..config.cmd_pat..[[g_]] end function gSearch:googlethat(query, config) @@ -27,8 +25,7 @@ function gSearch:googlethat(query, config) -- Do the request local res, code = HTTPS.request(api..parameters) if code == 403 then - utilities.send_reply(self, msg, config.errors.quotaexceeded) - return + return '403' end if code ~= 200 then utilities.send_reply(self, msg, config.errors.connection) @@ -69,8 +66,18 @@ function gSearch:action(msg, config) end end - local results, stats = gSearch:googlethat(input, config) - utilities.send_message(self, msg.chat.id, gSearch:stringlinks(results, stats), true, nil, true) + local results, stats = gSearch:googlethat(input, onfig) + if results == '403' then + utilities.send_reply(self, msg, config.errors.quotaexceeded) + return + end + + if not results then + utilities.send_reply(self, msg, config.errors.results) + return + end + + utilities.send_message(self, msg.chat.id, gSearch:stringlinks(results, stats), true, nil, true, '{"inline_keyboard":[[{"text":"Alle Ergebnisse anzeigen","url":"https://www.google.com/search?q='..URL.escape(input)..'"}]]}') end diff --git a/miku/plugins/games.lua b/miku/plugins/games.lua new file mode 100644 index 0000000..7ddc2e2 --- /dev/null +++ b/miku/plugins/games.lua @@ -0,0 +1,147 @@ +local games = {} + +local http = require('socket.http') +local URL = require('socket.url') +local xml = require("xml") +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +games.command = 'game ' + +function games:init(config) + games.triggers = { + "^/game (.+)$" + } + games.doc = [[* +]]..config.cmd_pat..[[game*_ _: Sendet Infos zum Spiel]] +end + +local BASE_URL = 'http://thegamesdb.net/api' + +local makeOurDate = function(dateString) + local pattern = "(%d+)%/(%d+)%/(%d+)" + local month, day, year = dateString:match(pattern) + return day..'.'..month..'.'..year +end + + +function games:get_game_id(game) + local url = BASE_URL..'/GetGamesList.php?name='..game + local res,code = http.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local result = xml.load(res) + if xml.find(result, 'id') then + local game = xml.find(result, 'id')[1] + return game + else + return nil + end +end + +function games:send_game_photo(result, self, msg) + local BASE_URL = xml.find(result, 'baseImgUrl')[1] + local images = {} + + if xml.find(result, 'fanart') then + local fanart = xml.find(result, 'fanart')[1] + local fanrt_url = BASE_URL..fanart[1] + table.insert(images, fanrt_url) + end + + if xml.find(result, 'boxart', 'side', 'front') then + local boxart = xml.find(result, 'boxart', 'side', 'front')[1] + local boxart_url = BASE_URL..boxart + table.insert(images, boxart_url) + end + + local i = 0 + for k, v in pairs(images) do + i = i+1 + local file = download_to_file(v, 'game'..i..'.jpg') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end +end + +function games:send_game_data(game_id, self, msg) + local url = BASE_URL..'/GetGame.php?id='..game_id + local res,code = http.request(url) + if code ~= 200 then return nil end + local result = xml.load(res) + + local title = xml.find(result, 'GameTitle')[1] + local platform = xml.find(result, 'Platform')[1] + + if xml.find(result, 'ReleaseDate') then + date = ', erschienen am '..makeOurDate(xml.find(result, 'ReleaseDate')[1]) + else + date = '' + end + + if xml.find(result, 'Overview') then + desc = '\n_'..string.sub(xml.find(result, 'Overview')[1], 1, 200) .. '..._' + else + desc = '' + end + + if xml.find(result, 'Genres') then + local genres = xml.find(result, 'Genres') + local genre_count = tablelength(genres)-1 + if genre_count == 1 then + genre = '\nGenre: '..genres[1][1] + else + local genre_loop = '\nGenres: ' + for v in pairs(genres) do + if v == 'xml' then break; end + if v < genre_count then + genre_loop = genre_loop..genres[v][1]..', ' + else + genre_loop = genre_loop..genres[v][1] + end + end + genre = genre_loop + end + else + genre = '' + end + + if xml.find(result, 'Players') then + players = '\nSpieler: '..xml.find(result, 'Players')[1] + else + players = '' + end + + if xml.find(result, 'Youtube') then + video = '\n[Video auf YouTube ansehen]('..xml.find(result, 'Youtube')[1]..')' + else + video = '' + end + + if xml.find(result, 'Publisher') then + publisher = '\nPublisher: '..xml.find(result, 'Publisher')[1] + else + publisher = '' + end + + local text = '*'..title..'* für *'..platform..'*'..date..desc..genre..players..video..publisher + utilities.send_reply(self, msg, text, true) + + if xml.find(result, 'fanrt') or xml.find(result, 'boxart') then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + games:send_game_photo(result, self, msg) + end + return +end + + +function games:action(msg, config, matches) + local game = URL.escape(matches[1]) + local game_id = games:get_game_id(game) + if not game_id then + utilities.send_reply(self, msg, 'Spiel nicht gefunden!') + return + else + games:send_game_data(game_id, self, msg) + end +end + +return games diff --git a/miku/plugins/gdrive.lua b/miku/plugins/gdrive.lua new file mode 100644 index 0000000..a9158d9 --- /dev/null +++ b/miku/plugins/gdrive.lua @@ -0,0 +1,99 @@ +local gdrive = {} + +local utilities = require('miku.utilities') +local https = require('ssl.https') +local ltn12 = require('ltn12') +local json = require('dkjson') +local bindings = require('miku.bindings') + +function gdrive:init(config) + if not cred_data.google_apikey then + print('Missing config value: google_apikey.') + print('gdrive.lua will not be enabled.') + return + end + + gdrive.triggers = { + "docs.google.com/(.*)/d/([A-Za-z0-9-_-]+)", + "drive.google.com/(.*)/d/([A-Za-z0-9-_-]+)", + "drive.google.com/(open)%?id=([A-Za-z0-9-_-]+)" + } +end + +local apikey = cred_data.google_apikey + +local BASE_URL = 'https://www.googleapis.com/drive/v2' + +function gdrive:get_drive_document_data (docid) + local apikey = cred_data.google_apikey + local url = BASE_URL..'/files/'..docid..'?key='..apikey..'&fields=id,title,mimeType,ownerNames,exportLinks,fileExtension' + local res,code = https.request(url) + local res = string.gsub(res, 'image/', '') + local res = string.gsub(res, 'application/', '') + if code ~= 200 then return nil end + local data = json.decode(res) + return data +end + +function gdrive:send_drive_document_data(data, self, msg) + local title = data.title + local mimetype = data.mimeType + local id = data.id + local owner = data.ownerNames[1] + local text = '"'..title..'", freigegeben von '..owner + if data.exportLinks then + if data.exportLinks.png then + local image_url = data.exportLinks.png + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + return + else + local pdf_url = data.exportLinks.pdf + utilities.send_typing(self, msg.chat.id, 'upload_document') + local file = download_to_file(pdf_url) + utilities.send_document(self, msg.chat.id, file, text, msg.message_id) + return + end + else + local get_file_url = 'https://drive.google.com/uc?id='..id + local ext = data.fileExtension + if mimetype == "png" or mimetype == "jpg" or mimetype == "jpeg" or mimetype == "gif" or mimetype == "webp" then + local respbody = {} + local options = { + url = get_file_url, + sink = ltn12.sink.table(respbody), + redirect = false + } + local response = {https.request(options)} -- luasec doesn't support 302 redirects, so we must contact gdrive again + local code = response[2] + local headers = response[3] + local file_url = headers.location + if ext == "jpg" or ext == "jpeg" or ext == "png" then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(file_url) + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + return + else + utilities.send_typing(self, msg.chat.id, 'upload_document') + local file = download_to_file(file_url) + utilities.send_document(self, msg.chat.id, file, text, msg.message_id) + return + end + else + local text = '*'..title..'*, freigegeben von _'..owner..'_\n[Direktlink]('..get_file_url..')' + utilities.send_reply(self, msg, text, true) + return + end + end +end + +function gdrive:action(msg, config, matches) + local docid = matches[2] + local data = gdrive:get_drive_document_data(docid) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + gdrive:send_drive_document_data(data, self, msg) + return +end + +return gdrive \ No newline at end of file diff --git a/otouto/plugins/get.lua b/miku/plugins/get.lua similarity index 93% rename from otouto/plugins/get.lua rename to miku/plugins/get.lua index 9d008a7..e7ea079 100644 --- a/otouto/plugins/get.lua +++ b/miku/plugins/get.lua @@ -1,7 +1,7 @@ local get = {} -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() get.command = 'get ' diff --git a/miku/plugins/getfile.lua b/miku/plugins/getfile.lua new file mode 100644 index 0000000..5ce92a6 --- /dev/null +++ b/miku/plugins/getfile.lua @@ -0,0 +1,113 @@ +-- YOU NEED THE FOLLOWING FOLDERS: photo, document, video, voice +-- PLEASE ADJUST YOUR PATH BELOW +-- Save your bot api key in redis set telegram:credentials! + +local media_download = {} + +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') +local ltn12 = require('ltn12') +local HTTPS = require('ssl.https') +local redis = (loadfile "./miku/redis.lua")() + +media_download.triggers = { + '/nil' +} + +function media_download:download_to_file_permanently(url, file_name) + local respbody = {} + local options = { + url = url, + sink = ltn12.sink.table(respbody), + redirect = false + } + local response = nil + response = {HTTPS.request(options)} + + local code = response[2] + local headers = response[3] + local status = response[4] + + if code ~= 200 then return false end + + -- TODO: Save, when folder doesn't exist + -- Create necessary folders in this folder! + local file_path = "/home/YOURPATH/tmp/"..file_name + file = io.open(file_path, "w+") + file:write(table.concat(respbody)) + file:close() + print("Downloaded to: "..file_path) + return true +end + +function media_download:pre_process(msg, self) + if msg.photo then + local lv = #msg.photo -- find biggest photo, always the last value + file_id = msg.photo[lv].file_id + file_size = msg.photo[lv].file_size + elseif msg.video then + file_id = msg.video.file_id + file_size = msg.video.file_size + elseif msg.sticker then + file_id = msg.sticker.file_id + file_size = msg.sticker.file_size + elseif msg.voice then + file_id = msg.voice.file_id + file_size = msg.voice.file_size + elseif msg.audio then + file_id = msg.audio.file_id + file_size = msg.audio.file_size + elseif msg.document then + file_id = msg.document.file_id + file_size = msg.document.file_size + else + return + end + + if file_size > 19922944 then + print('File is over 20 MB - can\'t download :(') + return + end + + -- Check if file has already been downloaded + local already_downloaded = redis:sismember('telegram:file_id', file_id) + if already_downloaded == true then + print('File has already been downloaded in the past, skipping...') + return + end + + -- Saving file to the Telegram Cloud + local request = bindings.request(self, 'getFile', { + file_id = file_id + } ) + + -- Getting file from the Telegram Cloud + if not request then + print('Download failed!') + return + end + + -- Use original filename for documents + if msg.document then + file_path = 'document/'..file_id..'-'..msg.document.file_name -- to not overwrite a file + else + file_path = request.result.file_path + end + + -- Construct what we want + local download_url = 'https://api.telegram.org/file/bot'..cred_data.bot_api_key..'/'..request.result.file_path + local ok = media_download:download_to_file_permanently(download_url, file_path) + if not ok then + print('Download failed!') + return + end + + -- Save file_id to redis to prevent downloading the same file over and over when forwarding + redis:sadd('telegram:file_id', file_id) + return msg +end + +function media_download:action(msg) +end + +return media_download diff --git a/miku/plugins/gfycat.lua b/miku/plugins/gfycat.lua new file mode 100644 index 0000000..a3857c8 --- /dev/null +++ b/miku/plugins/gfycat.lua @@ -0,0 +1,36 @@ +-- Thanks to Akamaru for the API entrypoints and the initial idea + +local gfycat = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +gfycat.triggers = { + "gfycat.com/([A-Za-z0-9-_-]+)" +} + +function gfycat:send_gfycat_video(name, self, msg) + local BASE_URL = "https://gfycat.com" + local url = BASE_URL..'/cajax/get/'..name + local res,code = https.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res).gfyItem + utilities.send_typing(self, msg.chat.id, 'upload_video') + local file = download_to_file(data.webmUrl) + if file == nil then + send_reply(self, msg, 'Fehler beim Herunterladen von '..name) + return + else + utilities.send_video(self, msg.chat.id, file, nil, msg.message_id) + return + end +end + +function gfycat:action(msg, config, matches) + local name = matches[1] + gfycat:send_gfycat_video(name, self, msg) + return +end + +return gfycat diff --git a/otouto/plugins/github.lua b/miku/plugins/github.lua similarity index 84% rename from otouto/plugins/github.lua rename to miku/plugins/github.lua index a29328e..370c1fd 100644 --- a/otouto/plugins/github.lua +++ b/miku/plugins/github.lua @@ -4,9 +4,9 @@ local http = require('socket.http') 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 redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') +local redis = (loadfile "./miku/redis.lua")() function github:init(config) github.triggers = { @@ -31,9 +31,9 @@ end function github:send_github_data(data) if not data.owner then return nil end - local name = '*'..data.name..'*' - local description = '_'..data.description..'_' - local owner = data.owner.login + local name = '*'..utilities.md_escape(data.name)..'*' + local description = '_'..utilities.md_escape(data.description)..'_' + local owner = utilities.md_escape(data.owner.login) local clone_url = data.clone_url if data.language == nil or data.language == "" then language = '' @@ -57,7 +57,7 @@ end function github:send_gh_commit_data(gh_code, gh_commit_sha, data) if not data.committer then return nil end local committer = data.committer.name - local message = data.message + local message = utilities.md_escape(data.message) local text = '`'..gh_code..'@'..gh_commit_sha..'` von *'..committer..'*:\n'..message return text end diff --git a/miku/plugins/golem.lua b/miku/plugins/golem.lua new file mode 100644 index 0000000..e0aeec8 --- /dev/null +++ b/miku/plugins/golem.lua @@ -0,0 +1,68 @@ +local golem = {} + +local http = require('socket.http') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function golem:init(config) + if not cred_data.golem_apikey then + print('Missing config value: golem_apikey.') + print('golem.lua will not be enabled.') + return + end + + golem.triggers = { + "golem.de/news/([A-Za-z0-9-_-]+)-(%d+).html" + } +end + +local BASE_URL = 'http://api.golem.de/api' + +function golem:get_golem_data (article_identifier) + local apikey = cred_data.golem_apikey + local url = BASE_URL..'/article/meta/'..article_identifier..'/?key='..apikey..'&format=json' + local res,code = http.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res).data + + local url = BASE_URL..'/article/images/'..article_identifier..'/?key='..apikey..'&format=json' + local res,code = http.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local image_data = json.decode(res).data + return data, image_data +end + +function golem:send_golem_data(data, image_data) + local headline = '*'..data.headline..'*' + if data.subheadline ~= "" then + subheadline = '\n_'..data.subheadline..'_' + else + subheadline = "" + end + local subheadline = data.subheadline + local abstracttext = data.abstracttext + local text = headline..subheadline..'\n'..abstracttext + if image_data[1] then + image_url = image_data[1].native.url + else + image_url = data.leadimg.url + end + return text, image_url +end + +function golem:action(msg, config, matches) + local article_identifier = matches[2] + local data, image_data = golem:get_golem_data(article_identifier) + if not data and not image_data then utilities.send_reply(self, msg, config.errors.connection) return end + local text, image_url = golem:send_golem_data(data, image_data) + + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) +end + +return golem \ No newline at end of file diff --git a/miku/plugins/googl.lua b/miku/plugins/googl.lua new file mode 100644 index 0000000..5ff63bf --- /dev/null +++ b/miku/plugins/googl.lua @@ -0,0 +1,47 @@ +local googl = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function googl:init(config) + if not cred_data.google_apikey then + print('Missing config value: google_apikey.') + print('googl.lua will not be enabled.') + return + end + + googl.triggers = { + "goo.gl/([A-Za-z0-9-_-/-/]+)" + } +end + +local BASE_URL = 'https://www.googleapis.com/urlshortener/v1' + +local makeOurDate = function(dateString) + local pattern = "(%d+)%-(%d+)%-(%d+)" + local year, month, day = dateString:match(pattern) + return day..'.'..month..'.'..year +end + +function googl:send_googl_info (shorturl) + local apikey = cred_data.google_apikey + local url = BASE_URL..'/url?key='..apikey..'&shortUrl=http://goo.gl/'..shorturl..'&projection=FULL&fields=longUrl,created,analytics(allTime(shortUrlClicks))' + local res,code = https.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res) + + local longUrl = data.longUrl + local shortUrlClicks = data.analytics.allTime.shortUrlClicks + local created = makeOurDate(data.created) + local text = longUrl..'\n'..shortUrlClicks..' mal geklickt (erstellt am '..created..')' + + return text +end + +function googl:action(msg, config, matches) + local shorturl = matches[1] + utilities.send_reply(self, msg, googl:send_googl_info(shorturl)) +end + +return googl diff --git a/miku/plugins/gps.lua b/miku/plugins/gps.lua new file mode 100644 index 0000000..4c6c2ae --- /dev/null +++ b/miku/plugins/gps.lua @@ -0,0 +1,38 @@ +local gps = {} + +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +gps.command = 'gps ,' + +function gps:init(config) + gps.triggers = { + "^/gps ([^,]*)[,%s]([^,]*)$", + "google.de/maps/@([^,]*)[,%s]([^,]*)", + "google.com/maps/@([^,]*)[,%s]([^,]*)", + "google.de/maps/place/@([^,]*)[,%s]([^,]*)", + "google.com/maps/place/@([^,]*)[,%s]([^,]*)" + } + gps.doc = [[* +]]..config.cmd_pat..[[gps* __,__: Sendet Karte mit diesen Koordinaten]] +end + +function gps:action(msg, config, matches) + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local lat = matches[1] + local lon = matches[2] + + local zooms = {16, 18} + + local urls = {} + for i in ipairs(zooms) do + local zoom = zooms[i] + local url = "https://maps.googleapis.com/maps/api/staticmap?zoom=" .. zoom .. "&size=600x300&maptype=hybrid¢er=" .. lat .. "," .. lon .. "&markers=color:red%7Clabel:•%7C" .. lat .. "," .. lon + local file = download_to_file(url, 'zoom_'..i..'.png') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + + utilities.send_location(self, msg.chat.id, lat, lon, msg.message_id) +end + +return gps diff --git a/otouto/plugins/greetings.lua b/miku/plugins/greetings.lua similarity index 72% rename from otouto/plugins/greetings.lua rename to miku/plugins/greetings.lua index f254f5d..c33693b 100644 --- a/otouto/plugins/greetings.lua +++ b/miku/plugins/greetings.lua @@ -4,7 +4,7 @@ local greetings = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') function greetings:init(config) config.greetings = config.greetings or { @@ -41,12 +41,16 @@ end function greetings:action(msg, config) - local nick = self.database.users[msg.from.id_str].nickname or msg.from.first_name + local nick = utilities.build_name(msg.from.first_name, msg.from.last_name) + if self.database.userdata[tostring(msg.from.id)] then + nick = self.database.userdata[tostring(msg.from.id)].nickname or nick + end 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 - utilities.send_message(self, msg.chat.id, utilities.latcyr(trigger:gsub('#NAME', nick))) + local output = utilities.char.zwnj .. trigger:gsub('#NAME', nick) + utilities.send_message(self, msg.chat.id, output) return end end diff --git a/miku/plugins/hackernews.lua b/miku/plugins/hackernews.lua new file mode 100644 index 0000000..bb1558e --- /dev/null +++ b/miku/plugins/hackernews.lua @@ -0,0 +1,45 @@ +local hackernews = {} + +local https = require('ssl.https') +local json = require('dkjson') +local URL = require('socket.url') +local utilities = require('miku.utilities') + +hackernews.triggers = { + "news.ycombinator.com/item%?id=(%d+)" +} + +local BASE_URL = 'https://hacker-news.firebaseio.com/v0' + +function hackernews:send_hackernews_post (hn_code) + local url = BASE_URL..'/item/'..hn_code..'.json' + local res,code = https.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res) + + local by = data.by + local title = data.title + + if data.url then + url = '\n[Link besuchen]('..data.url..')' + else + url = '' + end + + if data.text then + post = '\n'..unescape_html(data.text) + post = string.gsub(post, '

    ', ' ') + else + post = '' + end + local text = '*'..title..'* von _'..by..'_'..post..url + + return text +end + +function hackernews:action(msg, config, matches) + local hn_code = matches[1] + utilities.send_reply(self, msg, hackernews:send_hackernews_post(hn_code), true) +end + +return hackernews diff --git a/miku/plugins/heise.lua b/miku/plugins/heise.lua new file mode 100644 index 0000000..1fc085e --- /dev/null +++ b/miku/plugins/heise.lua @@ -0,0 +1,48 @@ +local heise = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +heise.triggers = { + "heise.de/newsticker/meldung/(.*).html$" + } + +function heise:get_heise_article(article) + local url = 'https://query.yahooapis.com/v1/public/yql?q=select%20content,src,strong%20from%20html%20where%20url=%22http://www.heise.de/newsticker/meldung/'..article..'.html%22%20and%20xpath=%22//div[@id=%27mitte_news%27]/article/header/h2|//div[@id=%27mitte_news%27]/article/div/p[1]/strong|//div[@id=%27mitte_news%27]/article/div/figure/img%22&format=json' + local res,code = https.request(url) + local data = json.decode(res).query.results + if code ~= 200 then return "HTTP-Fehler" end + + local title = data.h2 + if data.strong then + teaser = '\n'..data.strong + else + teaser = '' + end + if data.img then + image_url = 'https:'..data.img.src + end + local text = '*'..title..'*'..teaser + + if data.img then + return text, image_url + else + return text + end +end + +function heise:action(msg, config, matches) + local article = URL.escape(matches[1]) + local text, image_url = heise:get_heise_article(article) + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url, 'heise_teaser.jpg') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) +end + +return heise \ No newline at end of file diff --git a/miku/plugins/hello.lua b/miku/plugins/hello.lua new file mode 100644 index 0000000..6ee0cd9 --- /dev/null +++ b/miku/plugins/hello.lua @@ -0,0 +1,13 @@ +local hello = {} + +local utilities = require('miku.utilities') + +hello.triggers = { + "^[Ss][Aa][Gg] [Hh][Aa][Ll][Ll][Oo] [Zz][Uu] (.*)$" +} + +function hello:action(msg, config, matches) + utilities.send_reply(self, msg, 'Hallo, '..matches[1]..'!') +end + +return hello diff --git a/otouto/plugins/help.lua b/miku/plugins/help.lua similarity index 89% rename from otouto/plugins/help.lua rename to miku/plugins/help.lua index 45ee4f5..61748bc 100644 --- a/otouto/plugins/help.lua +++ b/miku/plugins/help.lua @@ -3,17 +3,22 @@ local help = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local help_text function help:init(config) + help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hilfe', true):t('help', true).table +end + +function help:action(msg, config) local commandlist = {} help_text = '*Verfügbare Befehle:*\n• '..config.cmd_pat for _,plugin in ipairs(self.plugins) do if plugin.command then + table.insert(commandlist, plugin.command) --help_text = help_text .. '\n• '..config.cmd_pat .. plugin.command:gsub('%[', '\\[') end @@ -21,17 +26,9 @@ function help:init(config) table.insert(commandlist, 'hilfe [Plugin]') table.sort(commandlist) - help_text = help_text .. table.concat(commandlist, '\n• '..config.cmd_pat) .. '\nParameter: [optional]' help_text = help_text:gsub('%[', '\\[') - - help.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('hilfe', true):t('help', true).table - -end - -function help:action(msg) - local input = utilities.input(msg.text_lower) -- Attempts to send the help message via PM. diff --git a/miku/plugins/id.lua b/miku/plugins/id.lua new file mode 100644 index 0000000..9f435a7 --- /dev/null +++ b/miku/plugins/id.lua @@ -0,0 +1,130 @@ +local id = {} + +local redis = (loadfile "./miku/redis.lua")() +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') + +id.command = 'id' + +function id:init(config) + id.triggers = { + "^/id$", + "^/ids? (chat)$" + } + id.doc = [[``` +Returns user and chat info for you or the replied-to message. +Alias: ]]..config.cmd_pat..[[who +```]] +end + +function id:get_member_count(self, msg, chat_id) + return bindings.request(self, 'getChatMembersCount', { + chat_id = chat_id + } ) +end + +function id:user_print_name(user) -- Yes, copied from stats plugin + if user.name then + return user.name + end + + local text = '' + if user.first_name then + text = user.last_name..' ' + end + if user.lastname then + text = text..user.last_name + end + + return text +end + +function id:get_user(user_id, chat_id) + local user_info = {} + local uhash = 'user:'..user_id + local user = redis:hgetall(uhash) + user_info.name = id:user_print_name(user) + user_info.id = user_id + return user_info +end + +function id:action(msg) + + if matches[1] == "/id" then + if msg.reply_to_message then + msg = msg.reply_to_message + msg.from.name = utilities.build_name(msg.from.first_name, msg.from.last_name) + end + + local chat_id = msg.chat.id + local user = 'Du bist @%s, auch bekannt als *%s* `[%s]`' + if msg.from.username then + user = user:format(utilities.markdown_escape(msg.from.username), msg.from.name, msg.from.id) + else + user = 'Du bist *%s* `[%s]`,' + user = user:format(msg.from.name, msg.from.id) + end + + local group = '@%s, auch bekannt als *%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 .. ', und du bist in der Gruppe ' .. group + + utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) + elseif matches[1] == "chat" then + if msg.chat.type ~= 'group' and msg.chat.type ~= 'supergroup' then + utilities.send_reply(self, msg, 'Das hier ist keine Gruppe!') + return + end + local chat_name = msg.chat.title + local chat_id = msg.chat.id + -- Users on chat + local hash = 'chat:'..chat_id..':users' + local users = redis:smembers(hash) + local users_info = {} + -- Get user info + for i = 1, #users do + local user_id = users[i] + local user_info = id:get_user(user_id, chat_id) + table.insert(users_info, user_info) + end + + -- get all administrators and the creator + local administrators = utilities.get_chat_administrators(self, chat_id) + local admins = {} + for num in pairs(administrators.result) do + if administrators.result[num].status ~= 'creator' then + table.insert(admins, tostring(administrators.result[num].user.id)) + else + creator_id = administrators.result[num].user.id + end + end + local result = id:get_member_count(self, msg, chat_id) + local member_count = result.result - 1 -- minus the bot + if member_count == 1 then + member_count = 'ist *1 Mitglied' + else + member_count = 'sind *'..member_count..' Mitglieder' + end + local text = 'IDs für *'..chat_name..'* `['..chat_id..']`\nHier '..member_count..':*\n---------\n' + for k,user in pairs(users_info) do + if table.contains(admins, tostring(user.id)) then + text = text..'*'..user.name..'* `['..user.id..']` _Administrator_\n' + elseif tostring(creator_id) == user.id then + text = text..'*'..user.name..'* `['..user.id..']` _Gruppenersteller_\n' + else + text = text..'*'..user.name..'* `['..user.id..']`\n' + end + end + utilities.send_reply(self, msg, text, true) + end +end + +return id diff --git a/miku/plugins/ifttt.lua b/miku/plugins/ifttt.lua new file mode 100644 index 0000000..b4dba17 --- /dev/null +++ b/miku/plugins/ifttt.lua @@ -0,0 +1,80 @@ +local ifttt = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local redis = (loadfile "./miku/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function ifttt:init(config) + ifttt.triggers = { + "^/ifttt (!set) (.*)$", + "^/ifttt (!unauth)$", + "^/ifttt (.*)%&(.*)%&(.*)%&(.*)", + "^/ifttt (.*)%&(.*)%&(.*)", + "^/ifttt (.*)%&(.*)", + "^/ifttt (.*)$" + } + + ifttt.doc = [[* +]]..config.cmd_pat..[[ifttt* _!set_ __: Speichere deinen Schlüssel ein (erforderlich) +*]]..config.cmd_pat..[[ifttt* _!unauth_: Löscht deinen Account aus dem Bot +*]]..config.cmd_pat..[[ifttt* __&__&__&__: Führt [Event] mit den optionalen Parametern Value1, Value2 und Value3 aus +Beispiel: `/ifttt DeinFestgelegterName&Hallo&NochEinHallo`: Führt 'DeinFestgelegterName' mit den Parametern 'Hallo' und 'NochEinHallo' aus.]] +end + +ifttt.command = 'ifttt &&&' + +local BASE_URL = 'https://maker.ifttt.com/trigger' + +function ifttt:set_ifttt_key(hash, key) + print('Setting ifttt in redis hash '..hash..' to '..key) + redis:hset(hash, 'ifttt', key) + return '*Schlüssel eingespeichert!* Das Plugin kann jetzt verwendet werden.' +end + +function ifttt:do_ifttt_request(key, event, value1, value2, value3) + if not value1 then + url = BASE_URL..'/'..event..'/with/key/'..key + elseif not value2 then + url = BASE_URL..'/'..event..'/with/key/'..key..'/?value1='..URL.escape(value1) + elseif not value3 then + url = BASE_URL..'/'..event..'/with/key/'..key..'/?value1='..URL.escape(value1)..'&value2='..URL.escape(value2) + else + url = BASE_URL..'/'..event..'/with/key/'..key..'/?value1='..URL.escape(value1)..'&value2='..URL.escape(value2)..'&value3='..URL.escape(value3) + end + + local res,code = https.request(url) + if code ~= 200 then return "*Ein Fehler ist aufgetreten!* Aktion wurde nicht ausgeführt." end + + return "*Event \""..event.."\" getriggert!*" +end + +function ifttt:action(msg, config, matches) + local hash = 'user:'..msg.from.id + local key = redis:hget(hash, 'ifttt') + local event = matches[1] + local value1 = matches[2] + local value2 = matches[3] + local value3 = matches[4] + + if event == '!set' then + utilities.send_reply(self, msg, ifttt:set_ifttt_key(hash, value1), true) + return + end + + if not key then + utilities.send_reply(self, msg, '*Bitte speichere zuerst deinen Schlüssel ein!* Aktiviere dazu den [Maker Channel](https://ifttt.com/maker) und speichere deinen Schlüssel mit `/ifttt !set KEY` ein', true) + return + end + + if event == '!unauth' then + redis:hdel(hash, 'ifttt') + utilities.send_reply(self, msg, '*Erfolgreich ausgeloggt!*', true) + return + end + + utilities.send_reply(self, msg, ifttt:do_ifttt_request(key, event, value1, value2, value3), true) +end + +return ifttt \ No newline at end of file diff --git a/miku/plugins/images.lua b/miku/plugins/images.lua new file mode 100644 index 0000000..629ba33 --- /dev/null +++ b/miku/plugins/images.lua @@ -0,0 +1,21 @@ +local images = {} + +local utilities = require('miku.utilities') +images.triggers = { + "(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Pp][Nn][Gg])$", + "(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Jj][Pp][Ee]?[Gg])$" +} + +function images:action(msg) + local url = matches[1] + local file, last_modified, nocache = get_cached_file(url, nil, msg.chat.id, 'upload_photo', self) + local result = utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + + if nocache then return end + if not result then return end + + -- Cache File-ID und Last-Modified-Header in Redis + cache_file(result, url, last_modified) +end + +return images diff --git a/otouto/plugins/imdb.lua b/miku/plugins/imdb.lua similarity index 94% rename from otouto/plugins/imdb.lua rename to miku/plugins/imdb.lua index b82d803..5dc1496 100644 --- a/otouto/plugins/imdb.lua +++ b/miku/plugins/imdb.lua @@ -3,8 +3,8 @@ local imdb = {} local HTTP = require('socket.http') local URL = require('socket.url') local JSON = require('dkjson') -local utilities = require('otouto.utilities') -local bindings = require('otouto.bindings') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') imdb.command = 'imdb ' diff --git a/otouto/plugins/imgblacklist.lua b/miku/plugins/imgblacklist.lua similarity index 65% rename from otouto/plugins/imgblacklist.lua rename to miku/plugins/imgblacklist.lua index d75fe45..d8bc3b7 100644 --- a/otouto/plugins/imgblacklist.lua +++ b/miku/plugins/imgblacklist.lua @@ -1,12 +1,16 @@ local imgblacklist = {} -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() imgblacklist.command = 'imgblacklist' function imgblacklist:init(config) - imgblacklist.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('imgblacklist', true).table + imgblacklist.triggers = { + "^/imgblacklist show$", + "^/imgblacklist (add) (.*)$", + "^/imgblacklist (remove) (.*)$" + } imgblacklist.doc = [[* ]]..config.cmd_pat..[[imgblacklist* _show_: Zeige Blacklist *]]..config.cmd_pat..[[imgblacklist* _add_ __: Fügt Wort der Blacklist hinzu @@ -47,30 +51,34 @@ function imgblacklist:remove_blacklist(word) end end -function imgblacklist:action(msg) +function imgblacklist:action(msg, config, matches) if msg.from.id ~= config.admin then utilities.send_reply(self, msg, config.errors.sudo) return end - local input = utilities.input(msg.text) - local input = string.lower(input) + + local action = matches[1] + if matches[2] then word = string.lower(matches[2]) else word = nil end _blacklist = redis:smembers("telegram:img_blacklist") - if input:match('(add) (.*)') then - local word = input:match('add (.*)') - output = imgblacklist:add_blacklist(word) - elseif input:match('(remove) (.*)') then - local word = input:match('remove (.*)') - output = imgblacklist:remove_blacklist(word) - elseif input:match('(show)') then - output = imgblacklist:show_blacklist() - else - utilities.send_message(self, msg.chat.id, imgblacklist.doc, true, msg.message_id, true) + if action == 'add' and not word then + utilities.send_reply(self, msg, imgblacklist.doc, true) return + elseif action == "add" and word then + utilities.send_reply(self, msg, imgblacklist:add_blacklist(word), true) + return end + + if action == 'remove' and not word then + utilities.send_reply(self, msg, imgblacklist.doc, true) + return + elseif action == "remove" and word then + utilities.send_reply(self, msg, imgblacklist:remove_blacklist(word), true) + return + end - utilities.send_message(self, msg.chat.id, output, true, nil, true) + utilities.send_reply(self, msg, imgblacklist:show_blacklist()) end return imgblacklist diff --git a/miku/plugins/imgur.lua b/miku/plugins/imgur.lua new file mode 100644 index 0000000..bd24e01 --- /dev/null +++ b/miku/plugins/imgur.lua @@ -0,0 +1,60 @@ +local imgur = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function imgur:init(config) + if not cred_data.imgur_client_id then + print('Missing config value: imgur_client_id.') + print('imgur.lua will not be enabled.') + return + end + + imgur.triggers = { + "imgur.com/([A-Za-z0-9]+).gifv", + "https?://imgur.com/([A-Za-z0-9]+)" + } +end + +local client_id = cred_data.imgur_client_id +local BASE_URL = 'https://api.imgur.com/3' + +function imgur:get_imgur_data(imgur_code) + local response_body = {} + local request_constructor = { + url = BASE_URL..'/image/'..imgur_code, + method = "GET", + sink = ltn12.sink.table(response_body), + headers = { + Authorization = 'Client-ID '..client_id + } + } + local ok, response_code, response_headers, response_status_line = https.request(request_constructor) + if not ok then + return nil + end + + local response_body = json.decode(table.concat(response_body)) + + if response_body.status ~= 200 then return nil end + + return response_body.data.link +end + +function imgur:action(msg) + local imgur_code = matches[1] + if imgur_code == "login" then return nil end + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local link = imgur:get_imgur_data(imgur_code) + if link then + local file = download_to_file(link) + if string.ends(link, ".gif") then + utilities.send_document(self, msg.chat.id, file, nil, msg.message_id) + else + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + end +end + +return imgur diff --git a/miku/plugins/instagram.lua b/miku/plugins/instagram.lua new file mode 100644 index 0000000..d411cbf --- /dev/null +++ b/miku/plugins/instagram.lua @@ -0,0 +1,80 @@ +local instagram = {} + +local https = require('ssl.https') +local json = require('dkjson') +local URL = require('socket.url') +local utilities = require('miku.utilities') + +function instagram:init(config) + if not cred_data.instagram_access_token then + print('Missing config value: instagram_access_token.') + print('instagram.lua will not be enabled.') + return + end + + instagram.triggers = { + "instagram.com/p/([A-Za-z0-9-_-]+)" + } +end + +local BASE_URL = 'https://api.instagram.com/v1' +local access_token = cred_data.instagram_access_token + +function instagram:get_insta_data(insta_code) + local url = BASE_URL..'/media/shortcode/'..insta_code..'?access_token='..access_token + local res,code = https.request(url) + if code ~= 200 then return nil end + local data = json.decode(res).data + return data +end + +function instagram:send_instagram_data(data) + -- Header + local username = data.user.username + local full_name = data.user.full_name + if username == full_name then + header = full_name..' hat ein' + else + header = full_name..' ('..username..') hat ein' + end + if data.type == 'video' then + header = header..' Video gepostet' + else + header = header..' Foto gepostet' + end + + -- Caption + if data.caption == nil then + caption = '' + else + caption = ':\n'..data.caption.text + end + + -- Footer + local comments = comma_value(data.comments.count) + local likes = comma_value(data.likes.count) + local footer = '\n'..likes..' Likes, '..comments..' Kommentare' + if data.type == 'video' then + footer = '\n'..data.videos.standard_resolution.url..footer + end + + -- Image + local image_url = data.images.standard_resolution.url + + return header..caption..footer, image_url +end + +function instagram:action(msg, config, matches) + local insta_code = matches[1] + local data = instagram:get_insta_data(insta_code) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + + local text, image_url = instagram:send_instagram_data(data) + if not image_url then utilities.send_reply(self, msg, config.errors.connection) return end + + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) +end + +return instagram diff --git a/miku/plugins/ip_info.lua b/miku/plugins/ip_info.lua new file mode 100644 index 0000000..6ebbff2 --- /dev/null +++ b/miku/plugins/ip_info.lua @@ -0,0 +1,92 @@ +local ip_info = {} + +local http = require('socket.http') +local json = require('dkjson') +local URL = require('socket.url') +local utilities = require('miku.utilities') + +function ip_info:init(config) + ip_info.triggers = { + "^/ip (.*)$", + "^/dns (.*)$" + } + + ip_info.doc = [[* +]]..config.cmd_pat..[[ip* __: Sendet Infos zu dieser IP]] +end + +ip_info.command = 'ip ' + +local BASE_URL = 'http://ip-api.com/json' + +function ip_info:get_host_data(host) + local url = BASE_URL..'/'..host..'?lang=de&fields=country,regionName,city,zip,lat,lon,isp,org,as,status,message,reverse,query' + local res,code = http.request(url) + if code ~= 200 then return "HTTP-FEHLER: "..code end + local data = json.decode(res) + if data.status == 'fail' then + return nil + end + + local isp = data.isp + + local url + if data.lat and data.lon then + lat = tostring(data.lat) + lon = tostring(data.lon) + url = "https://maps.googleapis.com/maps/api/staticmap?zoom=16&size=600x300&maptype=hybrid¢er="..lat..","..lon.."&markers=color:red%7Clabel:•%7C"..lat..","..lon + end + + if data.query == host then + query = '' + else + query = ' / '..data.query + end + + if data.reverse ~= "" and data.reverse ~= host then + host_addr = ' ('..data.reverse..')' + else + host_addr = '' + end + + -- Location + if data.zip ~= "" then + zipcode = data.zip..' ' + else + zipcode = '' + end + + local city = data.city + + if data.regionName ~= "" then + region = ', '..data.regionName + else + region = '' + end + + if data.country ~= "" then + country = ', '..data.country + else + country = '' + end + + local text = host..query..host_addr..' ist bei '..isp..':\n' + local location = zipcode..city..region..country + return text..location, url +end + +function ip_info:action(msg, config, matches) + local host = matches[1] + local text, image_url = ip_info:get_host_data(host) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url, 'map.png') + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + else + utilities.send_reply(self, msg, text) + end +end + +return ip_info diff --git a/miku/plugins/isup.lua b/miku/plugins/isup.lua new file mode 100644 index 0000000..1c5a4ac --- /dev/null +++ b/miku/plugins/isup.lua @@ -0,0 +1,85 @@ +local isup = {} + +local http = require('socket.http') +local https = require('ssl.https') +local socket = require('socket') +local URL = require('socket.url') +local utilities = require('miku.utilities') + +function isup:init(config) + isup.triggers = { + "^/isup (.*)$", + "^/ping (.*)$" + } + + isup.doc = [[* +]]..config.cmd_pat..[[isup* __: Prüft, ob die URL up ist]] +end + +function isup:is_up_socket(ip, port) + print('Connect to', ip, port) + local c = socket.try(socket.tcp()) + c:settimeout(3) + local conn = c:connect(ip, port) + if not conn then + return false + else + c:close() + return true + end +end + +function isup:is_up_http(url) + -- Parse URL from input, default to http + local parsed_url = URL.parse(url, { scheme = 'http', authority = '' }) + -- Fix URLs without subdomain not parsed properly + if not parsed_url.host and parsed_url.path then + parsed_url.host = parsed_url.path + parsed_url.path = "" + end + -- Re-build URL + local url = URL.build(parsed_url) + + local protocols = { + ["https"] = https, + ["http"] = http + } + local options = { + url = url, + redirect = false, + method = "GET" + } + local response = { protocols[parsed_url.scheme].request(options) } + local code = tonumber(response[2]) + if code == nil or code >= 400 then + return false + end + return true +end + +function isup:isup(url) + local pattern = '^(%d%d?%d?%.%d%d?%d?%.%d%d?%d?%.%d%d?%d?):?(%d?%d?%d?%d?%d?)$' + local ip,port = string.match(url, pattern) + local result = nil + + -- /isup 8.8.8.8:53 + if ip then + port = port or '80' + result = isup:is_up_socket(ip, port) + else + result = isup:is_up_http(url) + end + return result +end + +function isup:action(msg, config) + if isup:isup(matches[1]) then + utilities.send_reply(self, msg, matches[1]..' ist UP! ✅') + return + else + utilities.send_reply(self, msg, matches[1]..' ist DOWN! ❌') + return + end +end + +return isup diff --git a/miku/plugins/leave_group.lua b/miku/plugins/leave_group.lua new file mode 100644 index 0000000..21f425f --- /dev/null +++ b/miku/plugins/leave_group.lua @@ -0,0 +1,52 @@ +local leave_group = {} + +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +leave_group.triggers = { + '/nil' +} + +local report_to_admin = true -- set to false to not be notified, when Bot leaves groups without you + +function leave_group:check_for_admin(msg, self, config) + local result = bindings.request(self, 'getChatMember', { + chat_id = msg.chat.id, + user_id = config.admin + } ) + if not result.ok then + print('Konnte nicht prüfen, ob Admin in Gruppe ist! Verlasse sie sicherheitshalber...') + return false + end + if result.result.status ~= "member" and result.result.status ~= "administrator" and result.result.status ~= "creator" then + return false + else + return true + end +end + +function leave_group:pre_process(msg, self, config) + if msg.group_chat_created or msg.new_chat_member then + local admin_in_group = leave_group:check_for_admin(msg, self, config) + if not admin_in_group then + print('Admin ist nicht in der Gruppe, verlasse sie deshalb...') + utilities.send_reply(self, msg, 'Dieser Bot wurde in eine fremde Gruppe hinzugefügt. Dies wird gemeldet!\nThis bot was added to foreign group. This incident will be reported!') + local result = bindings.request(self, 'leaveChat', { + chat_id = msg.chat.id + } ) + local chat_name = msg.chat.title + local chat_id = msg.chat.id + local from = msg.from.name + local from_id = msg.from.id + if report_to_admin then + utilities.send_message(self, config.admin, '#WARNUNG: Bot wurde in fremde Gruppe hinzugefügt:\nGruppenname: '..chat_name..' ('..chat_id..')\nHinzugefügt von: '..from..' ('..from_id..')') + end + end + end + return msg +end + +function leave_group:action(msg) +end + +return leave_group diff --git a/otouto/plugins/location_manager.lua b/miku/plugins/location_manager.lua similarity index 95% rename from otouto/plugins/location_manager.lua rename to miku/plugins/location_manager.lua index aece496..9d8a194 100644 --- a/otouto/plugins/location_manager.lua +++ b/miku/plugins/location_manager.lua @@ -1,7 +1,7 @@ local loc_manager = {} -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() function loc_manager:init(config) loc_manager.triggers = { diff --git a/otouto/plugins/luarun.lua b/miku/plugins/luarun.lua similarity index 87% rename from otouto/plugins/luarun.lua rename to miku/plugins/luarun.lua index 1e9e095..b8d9176 100644 --- a/otouto/plugins/luarun.lua +++ b/miku/plugins/luarun.lua @@ -1,6 +1,6 @@ local luarun = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local URL = require('socket.url') local JSON = require('dkjson') @@ -25,9 +25,9 @@ function luarun:action(msg, config) end local output = loadstring( [[ - local bot = require('otouto.bot') - local bindings = require('otouto.bindings') - local utilities = require('otouto.utilities') + local bot = require('miku.bot') + local bindings = require('miku.bindings') + local utilities = require('miku.utilities') local JSON = require('dkjson') local URL = require('socket.url') local HTTP = require('socket.http') diff --git a/miku/plugins/lyrics.lua b/miku/plugins/lyrics.lua new file mode 100644 index 0000000..a7f66dd --- /dev/null +++ b/miku/plugins/lyrics.lua @@ -0,0 +1,51 @@ +local lyrics = {} + +local http = require('socket.http') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function lyrics:init(config) + if not cred_data.lyricsnmusic_apikey then + print('Missing config value: lyricsnmusic_apikey.') + print('lyrics.lua will not be enabled.') + return + end + + lyrics.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lyrics', true).table + lyrics.doc = [[* +]]..config.cmd_pat..[[lyrics* __: Postet Liedertext]] +end + +lyrics.command = 'lyrics ' + +function lyrics:getLyrics(text) + local apikey = cred_data.lyricsnmusic_apikey + local q = url_encode(text) + local b = http.request("http://api.lyricsnmusic.com/songs?api_key="..apikey.."&q=" .. q) + response = json.decode(b) + local reply = "" + if #response > 0 then + -- grab first match + local result = response[1] + reply = result.title .. " - " .. result.artist.name .. "\n" .. result.snippet .. "\n[Ganzen Liedertext ansehen](" .. result.url .. ")" + else + reply = nil + end + return reply +end + +function lyrics:action(msg, config, matches) + 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, lyrics.doc, true, msg.message_id, true) + return + end + end + + utilities.send_reply(self, msg, lyrics:getLyrics(input), true) +end + +return lyrics diff --git a/miku/plugins/magische_miesmuschel.lua b/miku/plugins/magische_miesmuschel.lua new file mode 100644 index 0000000..6aeabc8 --- /dev/null +++ b/miku/plugins/magische_miesmuschel.lua @@ -0,0 +1,23 @@ +local muschel = {} + +local utilities = require('miku.utilities') + +muschel.triggers = { + "^[Mm][Aa][Gg][Ii][Ss][Cc][Hh][Ee] [Mm][Ii][Ee][Ss][Mm][Uu][Ss][Cc][Hh][Ee][Ll], (.*)$" +} + +function muschel:frag_die_muschel() + local possibilities = { + "Ja", + "Nein", + "Eines Tages vielleicht" + } + local random = math.random(3) + return possibilities[random] +end + +function muschel:action(msg, config, matches) + utilities.send_reply(self, msg, muschel:frag_die_muschel()) +end + +return muschel diff --git a/otouto/plugins/media.lua b/miku/plugins/media.lua similarity index 62% rename from otouto/plugins/media.lua rename to miku/plugins/media.lua index bd1ba4a..7bd97b1 100644 --- a/otouto/plugins/media.lua +++ b/miku/plugins/media.lua @@ -1,7 +1,10 @@ local media = {} -local utilities = require('otouto.utilities') -local mimetype = (loadfile "./otouto/mimetype.lua")() +local HTTP = require('socket.http') +local HTTPS = require('ssl.https') +local redis = (loadfile "./miku/redis.lua")() +local utilities = require('miku.utilities') +local mimetype = (loadfile "./miku/mimetype.lua")() media.triggers = { "(https?://[%w-_%.%?%.:,/%+=&%[%]]+%.(gif))$", @@ -28,40 +31,28 @@ function media:action(msg) local ext = matches[2] local receiver = msg.chat.id - utilities.send_typing(self, receiver, 'upload_document') - local file = download_to_file(url) + local file, last_modified, nocache = get_cached_file(url, nil, msg.chat.id, 'upload_document', self) local mime_type = mimetype.get_content_type_no_sub(ext) if ext == 'gif' then print('send gif') - utilities.send_document(self, receiver, file, nil, msg.message_id) - return - - elseif mime_type == 'text' then - print('send_document') - utilities.send_document(self, receiver, file, nil, msg.message_id) - return - - elseif mime_type == 'image' then - print('send_photo') - utilities.send_photo(self, receiver, file, nil, msg.message_id) - return - + result = utilities.send_document(self, receiver, file, nil, msg.message_id) elseif mime_type == 'audio' then print('send_audio') - utilities.send_audio(self, receiver, file, nil, msg.message_id) - return - + result = utilities.send_audio(self, receiver, file, nil, msg.message_id) elseif mime_type == 'video' then print('send_video') - utilities.send_video(self, receiver, file, nil, msg.message_id) - return - + result = utilities.send_video(self, receiver, file, nil, msg.message_id) else print('send_file') - utilities.send_document(self, receiver, file, nil, msg.message_id) - return + result = utilities.send_document(self, receiver, file, nil, msg.message_id) end + + if nocache then return end + if not result then return end + + -- Cache File-ID und Last-Modified-Header in Redis + cache_file(result, url, last_modified) end return media diff --git a/miku/plugins/minecraft_server.lua b/miku/plugins/minecraft_server.lua new file mode 100644 index 0000000..0cf85ef --- /dev/null +++ b/miku/plugins/minecraft_server.lua @@ -0,0 +1,86 @@ +local mc_server = {} + +local http = require('socket.http') +local ltn12 = require('ltn12') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function mc_server:init(config) + mc_server.triggers = { + "^/mine (.*)$" + } + mc_server.doc = [[* +]]..config.cmd_pat..[[mine* __: Sucht Minecraft-Server und sendet Infos. Standard-Port: 25565 +*]]..config.cmd_pat..[[mine* __ __: Sucht Minecraft-Server auf Port und sendet Infos. +]] +end + +mc_server.command = "mine [Port]" + +function mc_server:mineSearch(ip, port) + local responseText = "" + local api = "https://mcapi.us/server/status" + local parameters = "?ip="..(URL.escape(ip) or "").."&port="..(URL.escape(port) or "").."&players=true" + local respbody = {} + local body, code, headers, status = http.request{ + url = api..parameters, + method = "GET", + redirect = true, + sink = ltn12.sink.table(respbody) + } + local body = table.concat(respbody) + if (status == nil) then return "FEHLER: status = nil" end + if code ~=200 then return "FEHLER: "..code..". Status: "..status end + local jsonData = json.decode(body) + responseText = responseText..ip..":"..port..":\n" + if (jsonData.motd ~= nil and jsonData.motd ~= '') then + local tempMotd = "" + tempMotd = jsonData.motd:gsub('%ยง.', '') + if (jsonData.motd ~= nil) then responseText = responseText.."*MOTD*: "..tempMotd.."\n" end + end + if (jsonData.online ~= nil) then + if jsonData.online == true then + server_online = "Ja" + else + server_online = "Nein" + end + responseText = responseText.."*Online*: "..server_online.."\n" + end + if (jsonData.players ~= nil) then + if (jsonData.players.max ~= nil and jsonData.players.max ~= 0) then + responseText = responseText.."*Slots*: "..jsonData.players.max.."\n" + end + if (jsonData.players.now ~= nil and jsonData.players.max ~= 0) then + responseText = responseText.."*Spieler online*: "..jsonData.players.now.."\n" + end + if (jsonData.players.sample ~= nil and jsonData.players.sample ~= false) then + responseText = responseText.."*Spieler*: "..table.concat(jsonData.players.sample, ", ").."\n" + end + if (jsonData.server.name ~= nil and jsonData.server.name ~= "") then + responseText = responseText.."*Server*: "..jsonData.server.name.."\n" + end + end + return responseText +end + +function mc_server:parseText(text, mc_server) + if (text == nil or text == "/mine") then + return mc_server.doc + end + ip, port = string.match(text, "^/mine (.-) (.*)$") + if (ip ~= nil and port ~= nil) then + return mc_server:mineSearch(ip, port) + end + local ip = string.match(text, "^/mine (.*)$") + if (ip ~= nil) then + return mc_server:mineSearch(ip, "25565") + end + return "FEHLER: Keine Input IP!" +end + +function mc_server:action(msg, config, matches) + utilities.send_reply(self, msg, mc_server:parseText(msg.text, mc_server), true) +end + +return mc_server diff --git a/miku/plugins/minecraft_skin.lua b/miku/plugins/minecraft_skin.lua new file mode 100644 index 0000000..a2c738f --- /dev/null +++ b/miku/plugins/minecraft_skin.lua @@ -0,0 +1,31 @@ +local mc_skin = {} + +local utilities = require('miku.utilities') + +function mc_skin:init(config) + mc_skin.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('skin', true).table + mc_skin.doc = [[* +]]..config.cmd_pat..[[skin* __: Sendet Minecraft-Skin dieses Nutzers]] +end + +mc_skin.command = 'skin ' + +local BASE_URL = 'http://ip-api.com/json' + +function mc_skin:action(msg, config, matches) + 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, mc_skin.doc, true, msg.message_id, true) + return + end + end + + local url = 'http://www.minecraft-skin-viewer.net/3d.php?layers=true&aa=true&a=0&w=330&wt=10&abg=330&abd=40&ajg=340&ajd=20&ratio=13&format=png&login='..input..'&headOnly=false&displayHairs=true&randomness=341.png' + local file = download_to_file(url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) +end + +return mc_skin diff --git a/miku/plugins/myanimelist.lua b/miku/plugins/myanimelist.lua new file mode 100644 index 0000000..1571f0c --- /dev/null +++ b/miku/plugins/myanimelist.lua @@ -0,0 +1,228 @@ +local mal = {} + +local http = require('socket.http') +local URL = require('socket.url') +local xml = require("xml") +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +mal.command = 'anime , /manga ' + +function mal:init(config) + if not cred_data.mal_user then + print('Missing config value: mal_user.') + print('myanimelist.lua will not be enabled.') + return + elseif not cred_data.mal_pw then + print('Missing config value: mal_pw.') + print('myanimelist.lua will not be enabled.') + return + end + + mal.triggers = { + "^/(anime) (.+)$", + "^/(manga) (.+)$" + } + mal.doc = [[* +]]..config.cmd_pat..[[anime*_ _: Sendet Infos zum Anime +*]]..config.cmd_pat..[[manga*_ _: Sendet Infos zum Manga +]] +end + +local user = cred_data.mal_user +local password = cred_data.mal_pw + +local BASE_URL = 'http://'..user..':'..password..'@myanimelist.net/api' + +function mal:delete_tags(str) + str = string.gsub( str, '
    ', '') + str = string.gsub( str, '%[i%]', '') + str = string.gsub( str, '%[/i%]', '') + str = string.gsub( str, '—', ' — ') + return str +end + +local makeOurDate = function(dateString) + local pattern = "(%d+)%-(%d+)%-(%d+)" + local year, month, day = dateString:match(pattern) + return day..'.'..month..'.'..year +end + +function mal:get_mal_info(query, typ) + if typ == 'anime' then + url = BASE_URL..'/anime/search.xml?q='..query + elseif typ == 'manga' then + url = BASE_URL..'/manga/search.xml?q='..query + end + local res,code = http.request(url) + if code ~= 200 then return "HTTP-Fehler" end + local result = xml.load(res) + return result +end + +function mal:send_anime_data(result, receiver) + local title = xml.find(result, 'title')[1] + local id = xml.find(result, 'id')[1] + local mal_url = 'http://myanimelist.net/anime/'..id + + if xml.find(result, 'synonyms')[1] then + alt_name = '\noder: '..unescape(mal:delete_tags(xml.find(result, 'synonyms')[1])) + else + alt_name = '' + end + + if xml.find(result, 'synopsis')[1] then + desc = '\n'..unescape(mal:delete_tags(string.sub(xml.find(result, 'synopsis')[1], 1, 200))) .. '...' + else + desc = '' + end + + if xml.find(result, 'episodes')[1] then + episodes = '\nEpisoden: '..xml.find(result, 'episodes')[1] + else + episodes = '' + end + + if xml.find(result, 'status')[1] then + status = ' ('..xml.find(result, 'status')[1]..')' + else + status = '' + end + + if xml.find(result, 'score')[1] ~= "0.00" then + score = '\nScore: '..string.gsub(xml.find(result, 'score')[1], "%.", ",") + else + score = '' + end + + if xml.find(result, 'type')[1] then + typ = '\nTyp: '..xml.find(result, 'type')[1] + else + typ = '' + end + + if xml.find(result, 'start_date')[1] ~= "0000-00-00" then + startdate = '\nVeröffentlichungszeitraum: '..makeOurDate(xml.find(result, 'start_date')[1]) + else + startdate = '' + end + + if xml.find(result, 'end_date')[1] ~= "0000-00-00" then + enddate = ' - '..makeOurDate(xml.find(result, 'end_date')[1]) + else + enddate = '' + end + + local text = '*'..title..'*'..alt_name..typ..episodes..status..score..startdate..enddate..'_'..desc..'_\n[Auf MyAnimeList ansehen]('..mal_url..')' + if xml.find(result, 'image') then + local image_url = xml.find(result, 'image')[1] + return text, image_url + else + return text + end +end + +function mal:send_manga_data(result) + local title = xml.find(result, 'title')[1] + local id = xml.find(result, 'id')[1] + local mal_url = 'http://myanimelist.net/manga/'..id + + if xml.find(result, 'type')[1] then + typ = ' ('..xml.find(result, 'type')[1]..')' + else + typ = '' + end + + if xml.find(result, 'synonyms')[1] then + alt_name = '\noder: '..unescape(mal:delete_tags(xml.find(result, 'synonyms')[1])) + else + alt_name = '' + end + + if xml.find(result, 'chapters')[1] then + chapters = '\nKapitel: '..xml.find(result, 'chapters')[1] + else + chapters = '' + end + + if xml.find(result, 'status')[1] then + status = ' ('..xml.find(result, 'status')[1]..')' + else + status = '' + end + + if xml.find(result, 'volumes')[1] then + volumes = '\nBände '..xml.find(result, 'volumes')[1] + else + volumes = '' + end + + if xml.find(result, 'score')[1] ~= "0.00" then + score = '\nScore: '..xml.find(result, 'score')[1] + else + score = '' + end + + if xml.find(result, 'start_date')[1] ~= "0000-00-00" then + startdate = '\nVeröffentlichungszeitraum: '..makeOurDate(xml.find(result, 'start_date')[1]) + else + startdate = '' + end + + if xml.find(result, 'end_date')[1] ~= "0000-00-00" then + enddate = ' - '..makeOurDate(xml.find(result, 'end_date')[1]) + else + enddate = '' + end + + if xml.find(result, 'synopsis')[1] then + desc = '\n'..unescape(mal:delete_tags(string.sub(xml.find(result, 'synopsis')[1], 1, 200))) .. '...' + else + desc = '' + end + + local text = '*'..title..'*'..alt_name..typ..chapters..status..volumes..score..startdate..enddate..'_'..desc..'_\n[Auf MyAnimeList ansehen]('..mal_url..')' + if xml.find(result, 'image') then + local image_url = xml.find(result, 'image')[1] + return text, image_url + else + return text + end +end + +function mal:action(msg, config, matches) + local query = URL.escape(matches[2]) + if matches[1] == 'anime' then + local anime_info = mal:get_mal_info(query, 'anime') + if anime_info == "HTTP-Fehler" then + utilities.send_reply(self, msg, 'Anime nicht gefunden!') + return + else + local text, image_url = mal:send_anime_data(anime_info) + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) + return + end + elseif matches[1] == 'manga' then + local manga_info = mal:get_mal_info(query, 'manga') + if manga_info == "HTTP-Fehler" then + utilities.send_reply(self, msg, 'Manga nicht gefunden!') + return + else + local text, image_url = mal:send_manga_data(manga_info) + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) + return + end + end +end + +return mal diff --git a/miku/plugins/notify.lua b/miku/plugins/notify.lua new file mode 100644 index 0000000..2a76aae --- /dev/null +++ b/miku/plugins/notify.lua @@ -0,0 +1,103 @@ +-- INFO: Stats must be activated, so that it can collect all members of a group and save his/her id to redis. +-- You can deactivate it afterwards. + +local notify = {} + +local redis = (loadfile "./miku/redis.lua")() +local utilities = require('miku.utilities') + +function notify:init(config) + notify.triggers = { + "^/notify (del)$", + "^/notify$" + } + + notify.doc = [[* +]]..config.cmd_pat..[[notify* (del): Benachrichtigt dich privat, wenn du erwähnt wirst (bzw. schaltet das Feature wieder aus)]] +end + +notify.command = 'notify [del]' + +-- See https://stackoverflow.com/a/32854917 +function isWordFoundInString(word,input) + return select(2,input:gsub('^' .. word .. '%W+','')) + + select(2,input:gsub('%W+' .. word .. '$','')) + + select(2,input:gsub('^' .. word .. '$','')) + + select(2,input:gsub('%W+' .. word .. '%W+','')) > 0 +end + +function notify:pre_process(msg, self) + local notify_users = redis:smembers('notify:ls') + + -- I call this beautiful lady the "if soup" + if msg.chat.type == 'chat' or msg.chat.type == 'supergroup' then + if msg.text then + for _,user in pairs(notify_users) do + if isWordFoundInString('@'..user, string.lower(msg.text)) then + local chat_id = msg.chat.id + local id = redis:hget('notify:'..user, 'id') + -- check, if user has sent at least one message to the group, + -- so that we don't send the user some private text, when he/she is not + -- in the group. + if redis:sismember('chat:'..chat_id..':users', id) then + + -- ignore message, if user is mentioning him/herself + if id == tostring(msg.from.id) then break; end + + local send_date = run_command('date -d @'..msg.date..' +"%d.%m.%Y um %H:%M:%S Uhr"') + local send_date = string.gsub(send_date, "\n", "") + local from = string.gsub(msg.from.name, "%_", " ") + local chat_name = string.gsub(msg.chat.title, "%_", " ") + local text = from..' am '..send_date..' in "'..chat_name..'":\n\n'..msg.text + utilities.send_message(self, id, text) + end + end + end + end + end + + return msg +end + +function notify:action(msg, config, matches) + if not msg.from.username then + utilities.send_reply(self, msg, 'Du hast keinen Usernamen und kannst daher dieses Feature nicht nutzen. Tut mir leid!' ) + return + end + + local username = string.lower(msg.from.username) + + local hash = 'notify:'..username + + if matches[1] == "del" then + if not redis:sismember('notify:ls', username) then + utilities.send_reply(self, msg, 'Du wirst noch gar nicht benachrichtigt!') + return + end + print('Setting notify in redis hash '..hash..' to false') + redis:hset(hash, 'notify', false) + print('Removing '..username..' from redis set notify:ls') + redis:srem('notify:ls', username) + utilities.send_reply(self, msg, 'Du erhälst jetzt keine Benachrichtigungen mehr, wenn du angesprochen wirst.') + return + else + if redis:sismember('notify:ls', username) then + utilities.send_reply(self, msg, 'Du wirst schon benachrichtigt!') + return + end + print('Setting notify in redis hash '..hash..' to true') + redis:hset(hash, 'notify', true) + print('Setting id in redis hash '..hash..' to '..msg.from.id) + redis:hset(hash, 'id', msg.from.id) + print('Adding '..username..' to redis set notify:ls') + redis:sadd('notify:ls', username) + local res = utilities.send_message(self, msg.from.id, 'Du erhälst jetzt Benachrichtigungen, wenn du angesprochen wirst, nutze `/notify del` zum Deaktivieren.', true, nil, true) + if not res then + utilities.send_reply(self, msg, 'Bitte schreibe mir [privat](http://telegram.me/' .. self.info.username .. '?start=notify), um den Vorgang abzuschließen.', true) + elseif msg.chat.type ~= 'private' then + utilities.send_reply(self, msg, 'Du erhälst jetzt Benachrichtigungen, wenn du angesprochen wirst, nutze `/notify del` zum Deaktivieren.', true) + end + end +end + +return notify \ No newline at end of file diff --git a/miku/plugins/pagespeed_insights.lua b/miku/plugins/pagespeed_insights.lua new file mode 100644 index 0000000..6f8f159 --- /dev/null +++ b/miku/plugins/pagespeed_insights.lua @@ -0,0 +1,39 @@ +local pagespeed_insights = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function pagespeed_insights:init(config) + if not cred_data.google_apikey then + print('Missing config value: google_apikey.') + print('pagespeed_insights.lua will not be enabled.') + return + end + + pagespeed_insights.triggers = { + "^/speed (https?://[%w-_%.%?%.:/%+=&]+)" + } + pagespeed_insights.doc = [[* +]]..config.cmd_pat..[[speed* __: Testet Geschwindigkeit der Seite mit PageSpeed Insights]] +end + +local BASE_URL = 'https://www.googleapis.com/pagespeedonline/v2' + +function pagespeed_insights:get_pagespeed(test_url) + local apikey = cred_data.google_apikey + local url = BASE_URL..'/runPagespeed?url='..test_url..'&key='..apikey..'&fields=id,ruleGroups(SPEED(score))' + local res,code = https.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res) + return data.id..' hat einen PageSpeed-Score von *'..data.ruleGroups.SPEED.score..' Punkten.*' +end + +function pagespeed_insights:action(msg, config, matches) + utilities.send_typing(self, msg.chat.id, 'typing') + local text = pagespeed_insights:get_pagespeed(matches[1]) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + utilities.send_reply(self, msg, text, true) +end + +return pagespeed_insights diff --git a/otouto/plugins/pasteee.lua b/miku/plugins/pasteee.lua similarity index 91% rename from otouto/plugins/pasteee.lua rename to miku/plugins/pasteee.lua index 3d7d45e..62706a7 100644 --- a/otouto/plugins/pasteee.lua +++ b/miku/plugins/pasteee.lua @@ -1,7 +1,7 @@ local pasteee = {} -local bot = require('otouto.bot') -local utilities = require('otouto.utilities') +local bot = require('miku.bot') +local utilities = require('miku.utilities') function pasteee:init(config) if not cred_data.pasteee_key then diff --git a/miku/plugins/pixabay.lua b/miku/plugins/pixabay.lua new file mode 100644 index 0000000..e9a5982 --- /dev/null +++ b/miku/plugins/pixabay.lua @@ -0,0 +1,120 @@ +local pixabay = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() + +function pixabay:init(config) + if not cred_data.pixabay_apikey then + print('Missing config value: pixabay_apikey.') + print('pixabay.lua will not be enabled.') + return + end + + pixabay.triggers = { + "^/pix(id) (%d+)", + "^/pix (.*)$", + "(pixabay.com).*%-(%d+)" + } + pixabay.doc = [[* +]]..config.cmd_pat..[[pix* __: Sendet lizenzfreies Bild]] +end + +pixabay.command = 'pix ' + +local BASE_URL = 'https://pixabay.com/api' +local apikey = cred_data.pixabay_apikey + +function pixabay:get_pixabay_directlink(id) + local url = BASE_URL..'/?key='..apikey..'&lang=de&id='..id + local b,c = https.request(url) + if c ~= 200 then return nil end + local data = json.decode(b) + if data.totalHits == 0 then return 'NOPIX' end + + local webformatURL = data.hits[1].webformatURL + local image_url = string.gsub(webformatURL, '_640.jpg', '_960.jpg') + + -- Link to full, high resolution image + local preview_url = data.hits[1].previewURL + local image_code = string.sub(preview_url, 59) + local image_code = string.sub(image_code, 0, -9) + local full_url = 'https://pixabay.com/de/photos/download/'..image_code..'.jpg' + + local user = data.hits[1].user + local tags = data.hits[1].tags + local page_url = data.hits[1].pageURL + + -- cache this shit + local hash = 'telegram:cache:pixabay:'..id + print('Caching data in '..hash..' with timeout 1209600') + redis:hset(hash, 'image_url', image_url) + redis:hset(hash, 'full_url', full_url) + redis:hset(hash, 'page_url', page_url) + redis:hset(hash, 'user', user) + redis:hset(hash, 'tags', tags) + redis:expire(hash, 1209600) -- 1209600 = two weeks + + return image_url, full_url, page_url, user, tags +end + +function pixabay:get_pixabay(term) + local count = 70 -- how many pictures should be returned (3 to 200) NOTE: more pictures = higher load time + local url = BASE_URL..'/?key='..apikey..'&lang=de&safesearch=true&per_page='..count..'&image_type=photo&q='..term + local b,c = https.request(url) + if c ~= 200 then return nil end + local photo = json.decode(b) + if photo.totalHits == 0 then return 'NOPIX' end + local photos = photo.hits + -- truly randomize + math.randomseed(os.time()) + -- random max json table size + local i = math.random(#photos) + + local webformatURL = photos[i].webformatURL + local image_url = string.gsub(webformatURL, '_640.jpg', '_960.jpg') + + -- Link to full, high resolution image + local preview_url = photos[i].previewURL + local image_code = string.sub(preview_url, 59) + local image_code = string.sub(image_code, 0, -9) + local full_url = 'https://pixabay.com/de/photos/download/'..image_code..'.jpg' + + local user = photos[i].user + local tags = photos[i].tags + local page_url = photos[i].pageURL + + return image_url, full_url, page_url, user, tags +end + +function pixabay:action(msg, config, matches) + local term = matches[1] + if matches[2] then + if redis:exists("telegram:cache:pixabay:"..matches[2]) == true then -- if cached + local hash = 'telegram:cache:pixabay:'..matches[2] + url = redis:hget(hash, 'image_url') + full_url = redis:hget(hash, 'full_url') + page_url = redis:hget(hash, 'page_url') + user = redis:hget(hash, 'user') + tags = redis:hget(hash, 'tags') + else + url, full_url, page_url, user, tags = pixabay:get_pixabay_directlink(matches[2]) + end + else + url, full_url, page_url, user, tags = pixabay:get_pixabay(term) + end + + if url == 'NOPIX' then + utilities.send_reply(self, msg, config.errors.results) + return + else + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(url) + local text = '"'..tags..'" von '..user + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id, '{"inline_keyboard":[[{"text":"Seite aufrufen","url":"'..page_url..'"},{"text":"Volles Bild (Login notwendig)","url":"'..full_url..'"}]]}') + return + end +end + +return pixabay diff --git a/miku/plugins/play_store.lua b/miku/plugins/play_store.lua new file mode 100644 index 0000000..ca3ca45 --- /dev/null +++ b/miku/plugins/play_store.lua @@ -0,0 +1,62 @@ +local play_store = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +function play_store:init(config) + if not cred_data.x_mashape_key then + print('Missing config value: x_mashape_key.') + print('play_store.lua will not be enabled.') + return + end + + play_store.triggers = { + "play.google.com/store/apps/details%?id=(.*)" + } +end + +local BASE_URL = 'https://apps.p.mashape.com/google/application' + +function play_store:get_playstore_data (appid) + local apikey = cred_data.x_mashape_key + local url = BASE_URL..'/'..appid..'?mashape-key='..apikey + local res,code = https.request(url) + if code ~= 200 then return nil end + local data = json.decode(res).data + return data +end + +function play_store:send_playstore_data(data) + local title = data.title + local developer = data.developer.id + local category = data.category.name + local rating = data.rating.average + local installs = data.performance.installs + local description = data.description + if data.version == "Varies with device" then + appversion = "variiert je nach Gerät" + else + appversion = data.version + end + if data.price == 0 then + price = "Gratis" + else + price = data.price + end + local text = '*'..title..'* von *'..developer..'* aus der Kategorie _'..category..'_, durschnittlich bewertet mit '..rating..' Sternen.\n_'..description..'_\n'..installs..' Installationen, Version '..appversion + return text +end + +function play_store:action(msg, config, matches) + local appid = matches[1] + local data = play_store:get_playstore_data(appid) + if data == nil then + return + else + utilities.send_reply(self, msg, play_store:send_playstore_data(data), true) + return + end +end + +return play_store diff --git a/miku/plugins/plugins.lua b/miku/plugins/plugins.lua new file mode 100644 index 0000000..02302d5 --- /dev/null +++ b/miku/plugins/plugins.lua @@ -0,0 +1,230 @@ +local plugin_manager = {} + +local bot = require('miku.bot') +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() + +function plugin_manager:init(config) + plugin_manager.triggers = { + "^/plugins$", + "^/plugins? (enable) ([%w_%.%-]+)$", + "^/plugins? (disable) ([%w_%.%-]+)$", + "^/plugins? (enable) ([%w_%.%-]+) (chat) (%d+)", + "^/plugins? (enable) ([%w_%.%-]+) (chat)", + "^/plugins? (disable) ([%w_%.%-]+) (chat) (%d+)", + "^/plugins? (disable) ([%w_%.%-]+) (chat)", + "^/plugins? (reload)$", + "^/(reload)$" + } + plugin_manager.doc = [[* +]]..config.cmd_pat..[[plugins*: Listet alle Plugins auf +*]]..config.cmd_pat..[[plugins* _enable/disable_ __: Aktiviert/deaktiviert Plugin +*]]..config.cmd_pat..[[plugins* _enable/disable_ __ chat: Aktiviert/deaktiviert Plugin im aktuellen Chat +*]]..config.cmd_pat..[[plugins* _enable/disable_ __ __: Aktiviert/deaktiviert Plugin in diesem Chat +*]]..config.cmd_pat..[[reload*: Lädt Plugins neu]] +end + +plugin_manager.command = 'plugins ' + +-- Returns the key (index) in the config.enabled_plugins table +function plugin_manager:plugin_enabled(name, chat) + for k,v in pairs(enabled_plugins) do + if name == v then + return k + end + end + -- If not found + return false +end + +-- Returns true if file exists in plugins folder +function plugin_manager:plugin_exists(name) + for k,v in pairs(plugins_names()) do + if name..'.lua' == v then + return true + end + end + return false +end + +function plugin_manager:list_plugins() + local text = '' + for k, v in pairs(plugins_names()) do + -- ✔ enabled, ❌ disabled + local status = '❌' + -- Check if is enabled + for k2, v2 in pairs(enabled_plugins) do + if v == v2..'.lua' then + status = '✔' + end + end + if not only_enabled or status == '✔' then + -- get the name + v = string.match (v, "(.*)%.lua") + text = text..v..' '..status..'\n' + end + end + return text +end + +function plugin_manager:reload_plugins(self, config, plugin_name, status) + for pac, _ in pairs(package.loaded) do + if pac:match('^miku%.plugins%.') then + package.loaded[pac] = nil + end + end + package.loaded['miku.bindings'] = nil + package.loaded['miku.utilities'] = nil + package.loaded['config'] = nil + bot.init(self, config) + if plugin_name then + return 'Plugin '..plugin_name..' wurde '..status + else + return 'Plugins neu geladen' + end +end + +function plugin_manager:enable_plugin(self, config, plugin_name) + print('checking if '..plugin_name..' exists') + -- Check if plugin is enabled + if plugin_manager:plugin_enabled(plugin_name) then + return 'Plugin '..plugin_name..' ist schon aktiviert' + end + -- Checks if plugin exists + if plugin_manager:plugin_exists(plugin_name) then + -- Add to redis set + redis:sadd('telegram:enabled_plugins', plugin_name) + print(plugin_name..' saved to redis set telegram:enabled_plugins') + -- Reload the plugins + return plugin_manager:reload_plugins(self, config, plugin_name, 'aktiviert') + else + return 'Plugin '..plugin_name..' existiert nicht' + end +end + +function plugin_manager:disable_plugin(self, config, name, chat) + -- Check if plugins exists + if not plugin_manager:plugin_exists(name) then + return 'Plugin '..name..' existiert nicht' + end + local k = plugin_manager:plugin_enabled(name) + -- Check if plugin is enabled + if not k then + return 'Plugin '..name..' ist nicht aktiviert' + end + -- Disable and reload + redis:srem('telegram:enabled_plugins', name) + print(name..' saved to redis set telegram:enabled_plugins') + return plugin_manager:reload_plugins(self, config, name, 'deaktiviert') +end + +function plugin_manager:disable_plugin_on_chat(msg, plugin) + if not plugin_manager:plugin_exists(plugin) then + return "Plugin existiert nicht!" + end + + if not msg.chat then + hash = 'chat:'..msg..':disabled_plugins' + else + hash = get_redis_hash(msg, 'disabled_plugins') + end + local disabled = redis:hget(hash, plugin) + + if disabled ~= 'true' then + print('Setting '..plugin..' in redis hash '..hash..' to true') + redis:hset(hash, plugin, true) + return 'Plugin '..plugin..' für diesen Chat deaktiviert.' + else + return 'Plugin '..plugin..' wurde für diesen Chat bereits deaktiviert.' + end +end + +function plugin_manager:reenable_plugin_on_chat(msg, plugin) + if not plugin_manager:plugin_exists(plugin) then + return "Plugin existiert nicht!" + end + + if not msg.chat then + hash = 'chat:'..msg..':disabled_plugins' + else + hash = get_redis_hash(msg, 'disabled_plugins') + end + local disabled = redis:hget(hash, plugin) + + if disabled == nil then return 'Es gibt keine deaktivierten Plugins für disen Chat.' end + + if disabled == 'true' then + print('Setting '..plugin..' in redis hash '..hash..' to false') + redis:hset(hash, plugin, false) + return 'Plugin '..plugin..' wurde für diesen Chat reaktiviert.' + else + return 'Plugin '..plugin..' ist nicht deaktiviert.' + end +end + +function plugin_manager:action(msg, config, matches) + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + return + end + + -- Show the available plugins + if matches[1] == '/plugins' then + utilities.send_reply(self, msg, plugin_manager:list_plugins()) + return + end + + -- Reenable a plugin for this chat + if matches[1] == 'enable' and matches[3] == 'chat' then + local plugin = matches[2] + if matches[4] then + local id = matches[4] + print("enable "..plugin..' on chat#id'..id) + utilities.send_reply(self, msg, plugin_manager:reenable_plugin_on_chat(id, plugin)) + return + else + print("enable "..plugin..' on this chat') + utilities.send_reply(self, msg, plugin_manager:reenable_plugin_on_chat(msg, plugin)) + return + end + end + + -- Enable a plugin + if matches[1] == 'enable' then + local plugin_name = matches[2] + print("enable: "..matches[2]) + utilities.send_reply(self, msg, plugin_manager:enable_plugin(self, config, plugin_name)) + return + end + + -- Disable a plugin on a chat + if matches[1] == 'disable' and matches[3] == 'chat' then + local plugin = matches[2] + if matches[4] then + local id = matches[4] + print("disable "..plugin..' on chat#id'..id) + utilities.send_reply(self, msg, plugin_manager:disable_plugin_on_chat(id, plugin)) + return + else + print("disable "..plugin..' on this chat') + utilities.send_reply(self, msg, plugin_manager:disable_plugin_on_chat(msg, plugin)) + return + end + end + + -- Disable a plugin + if matches[1] == 'disable' then + print("disable: "..matches[2]) + utilities.send_reply(self, msg, plugin_manager:disable_plugin(self, config, matches[2])) + return + end + + -- Reload all the plugins! + if matches[1] == 'reload' then + utilities.send_reply(self, msg, plugin_manager:reload_plugins(self, config)) + return + end +end + +return plugin_manager \ No newline at end of file diff --git a/miku/plugins/pocket.lua b/miku/plugins/pocket.lua new file mode 100644 index 0000000..d9d23b6 --- /dev/null +++ b/miku/plugins/pocket.lua @@ -0,0 +1,149 @@ +local pocket = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local redis = (loadfile "./miku/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function pocket:init(config) + if not cred_data.pocket_consumer_key then + print('Missing config value: pocket_consumer_key.') + print('pocket.lua will not be enabled.') + return + end + + pocket.triggers = { + "^/pocket(set)(.+)$", + "^/pocket (add) (https?://.*)$", + "^/pocket (archive) (%d+)$", + "^/pocket (readd) (%d+)$", + "^/pocket (unfavorite) (%d+)$", + "^/pocket (favorite) (%d+)$", + "^/pocket (delete) (%d+)$", + "^/pocket (unauth)$", + "^/pocket$" + } + + pocket.doc = [[* +]]..config.cmd_pat..[[pocket*: Postet Liste deiner Links +*]]..config.cmd_pat..[[pocket* add _(url)_: Fügt diese URL deiner Liste hinzu +*]]..config.cmd_pat..[[pocket* archive _[id]_: Archiviere diesen Eintrag +*]]..config.cmd_pat..[[pocket* readd _[id]_: De-archiviere diesen Eintrag +*]]..config.cmd_pat..[[pocket* favorite _[id]_: Favorisiere diesen Eintrag +*]]..config.cmd_pat..[[pocket* unfavorite _[id]_: Entfavorisiere diesen Eintrag +*]]..config.cmd_pat..[[pocket* delete _[id]_: Lösche diesen Eintrag +*]]..config.cmd_pat..[[pocket* unauth: Löscht deinen Account aus dem Bot]] +end + +pocket.command = 'pocket ' + +local BASE_URL = 'https://getpocket.com/v3' +local consumer_key = cred_data.pocket_consumer_key +local headers = { + ["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF8", + ["X-Accept"] = "application/json" +} + +function pocket:set_pocket_access_token(hash, access_token) + if string.len(access_token) ~= 30 then return '*Inkorrekter Access-Token*' end + print('Setting pocket in redis hash '..hash..' to users access_token') + redis:hset(hash, 'pocket', access_token) + return '*Authentifizierung abgeschlossen!*\nDas Plugin kann jetzt verwendet werden.' +end + +function pocket:list_pocket_items(access_token) + local items = post_petition(BASE_URL..'/get', 'consumer_key='..consumer_key..'&access_token='..access_token..'&state=unread&sort=newest&detailType=simple', headers) + + if items.status == 2 then return 'Keine Elemente eingespeichert.' end + if items.status ~= 1 then return 'Ein Fehler beim Holen der Elemente ist aufgetreten.' end + + local text = '' + for element in pairs(items.list) do + title = items.list[element].given_title + if not title or title == "" then title = items.list[element].resolved_title end + text = text..'#'..items.list[element].item_id..': '..title..'\n— '..items.list[element].resolved_url..'\n\n' + end + + return text +end + +function pocket:add_pocket_item(access_token, url) + local result = post_petition(BASE_URL..'/add', 'consumer_key='..consumer_key..'&access_token='..access_token..'&url='..url, headers) + if result.status ~= 1 then return 'Ein Fehler beim Hinzufügen der URL ist aufgetreten :(' end + local given_url = result.item.given_url + if result.item.title == "" or not result.item.title then + title = 'Seite' + else + title = '"'..result.item.title..'"' + end + local code = result.item.response_code + + local text = title..' ('..given_url..') hinzugefügt!' + if code ~= "200" and code ~= "0" then text = text..'\nAber die Seite liefert Fehler '..code..' zurück.' end + return text +end + +function pocket:modify_pocket_item(access_token, action, id) + local result = post_petition(BASE_URL..'/send', 'consumer_key='..consumer_key..'&access_token='..access_token..'&actions=[{"action":"'..action..'","item_id":'..id..'}]', headers) + if result.status ~= 1 then return 'Ein Fehler ist aufgetreten :(' end + + if action == 'readd' then + if result.action_results[1] == false then + return 'Dieser Eintrag existiert nicht!' + end + local url = result.action_results[1].normal_url + return url..' wieder de-archiviert' + end + if result.action_results[1] == true then + return 'Aktion ausgeführt.' + else + return 'Ein Fehler ist aufgetreten.' + end +end + +function pocket:action(msg, config, matches) + local hash = 'user:'..msg.from.id + local access_token = redis:hget(hash, 'pocket') + + if matches[1] == 'set' then + local access_token = matches[2] + utilities.send_reply(self, msg, pocket:set_pocket_access_token(hash, access_token), true) + local message_id = redis:hget(hash, 'pocket_login_msg') + utilities.edit_message(self, msg.chat.id, message_id, '*Anmeldung abgeschlossen!*', true, true) + redis:hdel(hash, 'pocket_login_msg') + return + end + + if not access_token then + local result = utilities.send_reply(self, msg, '*Bitte authentifiziere dich zuerst, indem du dich anmeldest.*', true, '{"inline_keyboard":[[{"text":"Bei Pocket anmelden","url":"https://brawlbot.tk/apis/callback/pocket/connect.php"}]]}') + redis:hset(hash, 'pocket_login_msg', result.result.message_id) + return + end + + if matches[1] == 'unauth' then + redis:hdel(hash, 'pocket') + utilities.send_reply(self, msg, 'Erfolgreich ausgeloggt! Du kannst den Zugriff [in deinen Einstellungen](https://getpocket.com/connected_applications) endgültig entziehen.', true) + return + end + + if matches[1] == 'add' then + utilities.send_reply(self, msg, pocket:add_pocket_item(access_token, matches[2])) + return + end + + if matches[1] == 'archive' or matches[1] == 'delete' or matches[1] == 'readd' or matches[1] == 'favorite' or matches[1] == 'unfavorite' then + utilities.send_reply(self, msg, pocket:modify_pocket_item(access_token, matches[1], matches[2])) + return + end + + if msg.chat.type == 'chat' or msg.chat.type == 'supergroup' then + utilities.send_reply(self, msg, 'Ausgeben deiner privaten Pocket-Liste in einem öffentlichen Chat wird feige verweigert. Bitte schreibe mich privat an!', true) + return + else + utilities.send_reply(self, msg, pocket:list_pocket_items(access_token)) + return + end +end + +return pocket \ No newline at end of file diff --git a/otouto/plugins/pokedex.lua b/miku/plugins/pokedex.lua similarity index 95% rename from otouto/plugins/pokedex.lua rename to miku/plugins/pokedex.lua index a38de1a..4a0782c 100644 --- a/otouto/plugins/pokedex.lua +++ b/miku/plugins/pokedex.lua @@ -2,8 +2,8 @@ local pokedex = {} local HTTP = require('socket.http') local JSON = require('dkjson') -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') pokedex.command = 'pokedex ' diff --git a/otouto/plugins/preview.lua b/miku/plugins/preview.lua similarity index 95% rename from otouto/plugins/preview.lua rename to miku/plugins/preview.lua index 7b535d6..8ceb6d1 100644 --- a/otouto/plugins/preview.lua +++ b/miku/plugins/preview.lua @@ -1,7 +1,7 @@ local preview = {} local HTTP = require('socket.http') -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') preview.command = 'preview ' diff --git a/miku/plugins/qr.lua b/miku/plugins/qr.lua new file mode 100644 index 0000000..f50fe75 --- /dev/null +++ b/miku/plugins/qr.lua @@ -0,0 +1,86 @@ +local qr = {} + +local http = require('socket.http') +local URL = require('socket.url') +local utilities = require('miku.utilities') + +function qr:init(config) + qr.triggers = { + '^/qr "(%w+)" "(%w+)" (.+)$', + "^/qr (.+)$" + } + qr.doc = [[* +]]..config.cmd_pat..[[qr* __: Sendet QR-Code mit diesem Text +*]]..config.cmd_pat..[[qr* _"[Hintergrundfarbe]"_ _"[Datenfarbe]"_ _[Text]_ +Farbe mit Text: red|green|blue|purple|black|white|gray +Farbe als HEX: ("a56729" ist braun) +oder Farbe als Dezimalwert: ("255-192-203" ist pink)]] +end + +qr.command = 'qr ' + +function qr:get_hex(str) + local colors = { + red = "f00", + blue = "00f", + green = "0f0", + yellow = "ff0", + purple = "f0f", + white = "fff", + black = "000", + gray = "ccc" + } + + for color, value in pairs(colors) do + if color == str then + return value + end + end + + return str +end + +function qr:qr(text, color, bgcolor) + + local url = "http://api.qrserver.com/v1/create-qr-code/?" + .."size=600x600" --fixed size otherways it's low detailed + .."&data="..URL.escape(utilities.trim(text)) + + if color then + url = url.."&color="..qr:get_hex(color) + end + if bgcolor then + url = url.."&bgcolor="..qr:get_hex(bgcolor) + end + + local response, code, headers = http.request(url) + + if code ~= 200 then + return nil + end + + if #response > 0 then + return url + end + + return nil +end + +function qr:action(msg, config, matches) + local text = matches[1] + local color + local back + + if #matches > 1 then + text = matches[3] + color = matches[2] + back = matches[1] + end + + local image_url = qr:qr(text, color, back) + if not image_url then utilities.send_reply(self, msg, config.errors.connection) return end + local file = download_to_file(image_url, 'qr.png') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) +end + +return qr diff --git a/otouto/plugins/quotes.lua b/miku/plugins/quotes.lua similarity index 84% rename from otouto/plugins/quotes.lua rename to miku/plugins/quotes.lua index f8aa1cc..9564c3f 100644 --- a/otouto/plugins/quotes.lua +++ b/miku/plugins/quotes.lua @@ -1,9 +1,9 @@ local quotes = {} -local bot = require('otouto.bot') -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() -require("./otouto/plugins/pasteee") +local bot = require('miku.bot') +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() +require("./miku/plugins/pasteee") function quotes:init(config) quotes.triggers = { @@ -75,7 +75,7 @@ function quotes:list_quotes(msg) text = text..num..") "..quote..'\n' end if not text or text == "" then - return 'Es wurden noch keine Zitate gespeichert.\nSpeichere doch welche mit !addquote [Zitat]' + return '*Es wurden noch keine Zitate gespeichert.*\nSpeichere doch welche mit `/addquote [Zitat]`', true else return upload(text) end @@ -99,10 +99,10 @@ function quotes:action(msg, config, matches) elseif matches[1] == "listquotes" then local link, iserror = quotes:list_quotes(msg) if iserror then - utilities.send_reply(self, msg, link) + utilities.send_reply(self, msg, link, true) return end - utilities.send_reply(self, msg, '[Lise aller Zitate auf Paste.ee ansehen]('..link..')', true) + utilities.send_reply(self, msg, 'Ich habe eine Liste aller Zitate hochgeladen.', false, '{"inline_keyboard":[[{"text":"Alle Zitate abrufen","url":"'..link..'"}]]}') return end utilities.send_reply(self, msg, quotes.doc, true) diff --git a/miku/plugins/random.lua b/miku/plugins/random.lua new file mode 100644 index 0000000..9bad539 --- /dev/null +++ b/miku/plugins/random.lua @@ -0,0 +1,67 @@ +local fun = {} + +local utilities = require('miku.utilities') + +function fun:init(config) + fun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('random', true).table + fun.doc = [[* +]]..config.cmd_pat..[[random* __: Schau, was passiert!]] +end + +fun.command = 'random ' + +function fun:choose_random(user_name, other_user) + randoms = { + user_name..' schlägt '..other_user..' mit einem stinkenden Fisch.', + user_name..' versucht, '..other_user..' mit einem Messer zu töten, bringt sich dabei aber selbst um.', + user_name..' versucht, '..other_user..' mit einem Messer zu töten, stolpert aber und schlitzt sich dabei das Knie auf.', + user_name..' ersticht '..other_user..'.', + user_name..' tritt '..other_user..'.', + user_name..' hat '..other_user..' umgebracht! Möge er in der Hölle schmoren!', + user_name..' hat die Schnauze voll von '..other_user..' und sperrt ihn in einen Schrank.', + user_name..' erwürgt '..other_user..'. BILD sprach als erstes mit der Hand.', + user_name..' schickt '..other_user..' nach /dev/null.', + user_name..' umarmt '..other_user..'.', + user_name..' verschenkt eine Kartoffel an '..other_user..'.', + user_name..' melkt '..other_user..'. *muuh* :D', + user_name..' wirft einen Gameboy auf '..other_user..'.', + user_name..' hetzt die NSA auf '..other_user..'.', + user_name..' ersetzt alle CDs von '..other_user..' durch Nickelback-CDs.', + other_user..' melkt '..user_name..'. *muuh* :D', + user_name..' ist in '..other_user..' verliebt.', + user_name..' schmeißt '..other_user..' in einen Fluss.', + user_name..' klaut '..other_user..' einen Lolli.', + user_name..' hätte gern Sex mit '..other_user..'.', + user_name..' schenkt '..other_user..' ein Foto von seinem Penis.', + user_name..' dreht durch und wirft '..other_user..' in einen Häcksler.', + user_name..' gibt '..other_user..' einen Keks.', + user_name..' lacht '..other_user..' aus.', + user_name..' gibt '..other_user..[[ ganz viel Liebe. ( ͡° ͜ʖ ͡°)]], + user_name..' lädt '..other_user..' zum Essen ein.', + user_name..' schwatzt '..other_user..' Ubuntu auf.', + user_name..' fliegt mit '..other_user..' nach Hawaii.', + user_name..' küsst '..other_user..' leidenschaftlich.' + } + math.randomseed(os.time()) + math.randomseed(os.time()) + local random = math.random(#randoms) + return randoms[random] +end + +function fun:action(msg, config, matches) + 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, fun.doc, true, msg.message_id, true) + return + end + end + + local user_name = get_name(msg) + local result = fun:choose_random(user_name, input) + utilities.send_message(self, msg.chat.id, result) +end + +return fun diff --git a/otouto/plugins/reddit.lua b/miku/plugins/reddit.lua similarity index 70% rename from otouto/plugins/reddit.lua rename to miku/plugins/reddit.lua index 21bfb00..fcb1ef9 100644 --- a/otouto/plugins/reddit.lua +++ b/miku/plugins/reddit.lua @@ -1,19 +1,16 @@ local reddit = {} -local HTTP = require('socket.http') +local https = require('ssl.https') local URL = require('socket.url') local JSON = require('dkjson') -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') -reddit.command = 'reddit [r/subreddit | query]' +reddit.command = 'reddit [r/subreddit | Suchbegriff]' function reddit:init(config) reddit.triggers = utilities.triggers(self.info.username, config.cmd_pat, {'^/r/'}):t('reddit', true):t('r', true):t('r/', true).table - reddit.doc = [[``` -]]..config.cmd_pat..[[reddit [r/subreddit | query] -Returns the top posts or results for a given subreddit or query. If no argument is given, returns the top posts from r/all. Querying specific subreddits is not supported. -Aliases: ]]..config.cmd_pat..[[r, /r/subreddit -```]] + reddit.doc = [[* +]]..config.cmd_pat..[[r* _[r/subreddit | Suchbegriff]_: Gibt Top-Posts oder Ergebnisse eines Subreddits aus. Wenn kein Argument gegeben ist, wird /r/all genommen.]] end local format_results = function(posts) @@ -25,8 +22,8 @@ local format_results = function(posts) title = title:sub(1, 253) title = utilities.trim(title) .. '...' end - local short_url = 'redd.it/' .. post.id - local s = '[' .. title .. '](' .. short_url .. ')' + local short_url = 'https://redd.it/' .. post.id + local s = '[' .. unescape(title) .. '](' .. short_url .. ')' if post.domain and not post.is_self and not post.over_18 then s = '`[`[' .. post.domain .. '](' .. post.url:gsub('%)', '\\)') .. ')`]` ' .. s end @@ -35,9 +32,9 @@ local format_results = function(posts) return output end -reddit.subreddit_url = 'http://www.reddit.com/%s/.json?limit=' -reddit.search_url = 'http://www.reddit.com/search.json?q=%s&limit=' -reddit.rall_url = 'http://www.reddit.com/.json?limit=' +reddit.subreddit_url = 'https://www.reddit.com/%s/.json?limit=' +reddit.search_url = 'https://www.reddit.com/search.json?q=%s&limit=' +reddit.rall_url = 'https://www.reddit.com/.json?limit=' function reddit:action(msg, config) -- Eight results in PM, four results elsewhere. @@ -59,7 +56,7 @@ function reddit:action(msg, config) source = '*/' .. utilities.md_escape(input) .. '*\n' else input = utilities.input(msg.text) - source = '*Results for* _' .. utilities.md_escape(input) .. '_ *:*\n' + source = '*Ergebnisse für* _' .. utilities.md_escape(input) .. '_ *:*\n' input = URL.escape(input) url = reddit.search_url:format(input) .. limit end @@ -67,7 +64,7 @@ function reddit:action(msg, config) url = reddit.rall_url .. limit source = '*/r/all*\n' end - local jstr, res = HTTP.request(url) + local jstr, res = https.request(url) if res ~= 200 then utilities.send_reply(self, msg, config.errors.connection) else diff --git a/miku/plugins/reddit_post.lua b/miku/plugins/reddit_post.lua new file mode 100644 index 0000000..62ecaab --- /dev/null +++ b/miku/plugins/reddit_post.lua @@ -0,0 +1,55 @@ +local reddit_post = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') + +reddit_post.triggers = { + "reddit.com/r/([A-Za-z0-9-/-_-.]+)/comments/([A-Za-z0-9-/-_-.]+)" +} + +local BASE_URL = 'https://www.reddit.com' + +function reddit_post:get_reddit_data(subreddit, reddit_code) + local url = BASE_URL..'/r/'..subreddit..'/comments/'..reddit_code..'.json' + local res,code = https.request(url) + if code ~= 200 then return nil end + local data = json.decode(res) + return data +end + +function reddit_post:send_reddit_data(data) + local title = utilities.md_escape(data[1].data.children[1].data.title) + local author = utilities.md_escape(data[1].data.children[1].data.author) + local subreddit = utilities.md_escape(data[1].data.children[1].data.subreddit) + if string.len(data[1].data.children[1].data.selftext) > 300 then + selftext = string.sub(unescape(data[1].data.children[1].data.selftext:gsub("%b<>", "")), 1, 300) .. '...' + else + selftext = unescape(data[1].data.children[1].data.selftext:gsub("%b<>", "")) + end + if not data[1].data.children[1].data.is_self then + url = data[1].data.children[1].data.url + else + url = '' + end + local score = comma_value(data[1].data.children[1].data.score) + local comments = comma_value(data[1].data.children[1].data.num_comments) + local text = '*'..author..'* in */r/'..subreddit..'* _('..score..' Upvotes - '..comments..' Kommentare)_:\n'..title..'\n'..selftext..url + return text +end + + +function reddit_post:action(msg, config, matches) + local subreddit = matches[1] + local reddit_code = matches[2] + local data = reddit_post:get_reddit_data(subreddit, reddit_code) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + + local text = reddit_post:send_reddit_data(data) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + + utilities.send_reply(self, msg, text, true) +end + +return reddit_post diff --git a/otouto/plugins/remind.lua b/miku/plugins/remind.lua similarity index 82% rename from otouto/plugins/remind.lua rename to miku/plugins/remind.lua index ed982c0..1f7ecaa 100644 --- a/otouto/plugins/remind.lua +++ b/miku/plugins/remind.lua @@ -1,17 +1,15 @@ local remind = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') -remind.command = 'remind ' +remind.command = 'remind ' 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 - Repeats a message after a duration of time, in minutes. - ```]] + remind.doc = [[* + ]]..config.cmd_pat..[[remind* __ __: Erinnert dich in X Minuten an die Nachricht]] end function remind:action(msg) @@ -44,10 +42,10 @@ function remind:action(msg) self.database.reminders[msg.chat.id_str] = self.database.reminders[msg.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[msg.chat.id_str]) > 9 then - utilities.send_reply(self, msg, 'Sorry, this group already has ten reminders.') + utilities.send_reply(self, msg, 'Diese Gruppe hat schon zehn Erinnerungen!') return elseif msg.chat.type == 'private' and utilities.table_size(self.database.reminders[msg.chat.id_str]) > 49 then - utilities.send_reply(msg, 'Sorry, you already have fifty reminders.') + utilities.send_reply(msg, 'Du hast schon 50 Erinnerungen!') return end -- Put together the reminder with the expiration, message, and message to reply to. @@ -56,11 +54,11 @@ function remind:action(msg) message = message } table.insert(self.database.reminders[msg.chat.id_str], reminder) - local output = 'I will remind you in ' .. duration + local output = 'Ich werde dich in ' .. duration if duration == 1 then - output = output .. ' minute!' + output = output .. ' Minute erinnern!' else - output = output .. ' minutes!' + output = output .. ' Minuten erinnern!' end utilities.send_reply(self, msg, output) end @@ -75,7 +73,7 @@ function remind:cron() -- 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 = '*ERINNERUNG:*\n"' .. utilities.md_escape(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 @@ -94,4 +92,4 @@ function remind:cron() end end -return remind +return remind \ No newline at end of file diff --git a/otouto/plugins/respond.lua b/miku/plugins/respond.lua similarity index 96% rename from otouto/plugins/respond.lua rename to miku/plugins/respond.lua index 04052c7..7b5f636 100644 --- a/otouto/plugins/respond.lua +++ b/miku/plugins/respond.lua @@ -1,8 +1,8 @@ local respond = {} local https = require('ssl.https') -local utilities = require('otouto.utilities') -local bindings = require('otouto.bindings') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') function respond:init(config) respond.triggers = { diff --git a/otouto/plugins/roll.lua b/miku/plugins/roll.lua similarity index 92% rename from otouto/plugins/roll.lua rename to miku/plugins/roll.lua index f445ed3..bac0dcd 100644 --- a/otouto/plugins/roll.lua +++ b/miku/plugins/roll.lua @@ -1,6 +1,6 @@ local roll = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') roll.command = 'roll' diff --git a/otouto/plugins/rss.lua b/miku/plugins/rss.lua similarity index 70% rename from otouto/plugins/rss.lua rename to miku/plugins/rss.lua index 94112b7..74a23ba 100644 --- a/otouto/plugins/rss.lua +++ b/miku/plugins/rss.lua @@ -3,19 +3,30 @@ local rss = {} local http = require('socket.http') local https = require('ssl.https') local url = require('socket.url') -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local bindings = require('miku.bindings') +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() local feedparser = require("feedparser") rss.command = 'rss ' function rss:init(config) - rss.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('rss', true).table + rss.triggers = { + "^/(rss) @(.*)$", + "^/rss$", + "^/rss (sub) (https?://[%w-_%.%?%.:/%+=&%~]+) @(.*)$", + "^/rss (sub) (https?://[%w-_%.%?%.:/%+=&%~]+)$", + "^/rss (del) (%d+) @(.*)$", + "^/rss (del) (%d+)$", + "^/rss (del)", + "^/rss (sync)$" + } rss.doc = [[* -]]..config.cmd_pat..[[rss*: Feed-Abonnements anzeigen -*]]..config.cmd_pat..[[rss* _sub_ __: Diesen Feed abonnieren -*]]..config.cmd_pat..[[rss* _del_ _<#>_: Diesen Feed deabonnieren -*]]..config.cmd_pat..[[rss* _sync_: Feeds syncen (nur Superuser)]] +]]..config.cmd_pat..[[rss* _@[Kanalname]_: Feed-Abonnements anzeigen +*]]..config.cmd_pat..[[rss* _sub_ __ _@[Kanalname]_: Diesen Feed abonnieren +*]]..config.cmd_pat..[[rss* _del_ _<#>_ _@[Kanalname]_: Diesen Feed deabonnieren +*]]..config.cmd_pat..[[rss* _sync_: Feeds syncen (nur Superuser) +Der Kanalname ist optional]] end function tail(n, k) @@ -193,58 +204,106 @@ function rss:print_subs(id, chat_name) if not subs[1] then return 'Keine Feeds abonniert!' end + local keyboard = '{"keyboard":[[' + local keyboard_buttons = '' local text = '*'..chat_name..'* hat abonniert:\n---------\n' for k,v in pairs(subs) do text = text .. k .. ") " .. v .. '\n' + if k == #subs then + keyboard_buttons = keyboard_buttons..'{"text":"/rss del '..k..'"}' + break; + end + keyboard_buttons = keyboard_buttons..'{"text":"/rss del '..k..'"},' end - return text + local keyboard = keyboard..keyboard_buttons..']], "one_time_keyboard":true, "selective":true, "resize_keyboard":true}' + return text, keyboard end -function rss:action(msg, config) - local input = utilities.input(msg.text) +function rss:action(msg, config, matches) local id = "user#id" .. msg.from.id - if msg.chat.type == 'channel' then - print('Kanäle werden momentan nicht unterstützt') - end if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then id = 'chat#id'..msg.chat.id end - if not input then - if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then - chat_name = msg.chat.title - else - chat_name = msg.chat.first_name + -- For channels + if matches[1] == 'sub' and matches[2] and matches[3] then + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + return + end + local id = '@'..matches[3] + local result = utilities.get_chat_info(self, id) + if not result then + utilities.send_reply(self, msg, 'Diesen Kanal gibt es nicht!') + return end + local output = rss:subscribe(id, matches[2]) + utilities.send_reply(self, msg, output, true) + return + elseif matches[1] == 'del' and matches[2] and matches[3] then + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + return + end + local id = '@'..matches[3] + local result = utilities.get_chat_info(self, id) + if not result then + utilities.send_reply(self, msg, 'Diesen Kanal gibt es nicht!') + return + end + local output = rss:unsubscribe(id, matches[2]) + utilities.send_reply(self, msg, output, true) + return + elseif matches[1] == 'rss' and matches[2] then + local id = '@'..matches[2] + local result = utilities.get_chat_info(self, id) + if not result then + utilities.send_reply(self, msg, 'Diesen Kanal gibt es nicht!') + return + end + local chat_name = result.result.title local output = rss:print_subs(id, chat_name) utilities.send_reply(self, msg, output, true) return end - - if input:match('(sub) (https?://[%w-_%.%?%.:/%+=&%~]+)$') then + + if msg.chat.type == 'group' or msg.chat.type == 'supergroup' then + chat_name = msg.chat.title + else + chat_name = msg.chat.first_name + end + + if matches[1] == 'sub' and matches[2] then if msg.from.id ~= config.admin then utilities.send_reply(self, msg, config.errors.sudo) return end - local rss_url = input:match('(https?://[%w-_%.%?%.:/%+=&%~]+)$') - local output = rss:subscribe(id, rss_url) + local output = rss:subscribe(id, matches[2]) utilities.send_reply(self, msg, output, true) - elseif input:match('(del) (%d+)$') then + return + elseif matches[1] == 'del' and matches[2] then if msg.from.id ~= config.admin then utilities.send_reply(self, msg, config.errors.sudo) return end - local rss_url = input:match('(%d+)$') - local output = rss:unsubscribe(id, rss_url) - utilities.send_reply(self, msg, output, true) - elseif input:match('(sync)$') then + local output = rss:unsubscribe(id, matches[2]) + utilities.send_reply(self, msg, output, true, '{"hide_keyboard":true}') + return + elseif matches[1] == 'del' and not matches[2] then + local list_subs, keyboard = rss:print_subs(id, chat_name) + utilities.send_reply(self, msg, list_subs, true, keyboard) + return + elseif matches[1] == 'sync' then if msg.from.id ~= config.admin then utilities.send_reply(self, msg, config.errors.sudo) return end rss:cron(self) + return end + local output = rss:print_subs(id, chat_name) + utilities.send_reply(self, msg, output, true) return end @@ -285,7 +344,7 @@ function rss:cron(self_plz) else content = '' end - text = text..'\n*'..title..'*\n'..content..' [Weiterlesen]('..link..')\n' + text = text..'\n[[#RSS]] *'..title..'*\n'..utilities.trim(utilities.markdown_escape_simple(content))..' [Weiterlesen]('..link..')\n' end if text ~= '' then local newlast = newentr[1].id @@ -299,4 +358,4 @@ function rss:cron(self_plz) end end -return rss +return rss \ No newline at end of file diff --git a/otouto/plugins/set.lua b/miku/plugins/set.lua similarity index 90% rename from otouto/plugins/set.lua rename to miku/plugins/set.lua index 86d53f4..b3c20a3 100644 --- a/otouto/plugins/set.lua +++ b/miku/plugins/set.lua @@ -1,7 +1,7 @@ local set = {} -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() set.command = 'set ' @@ -35,7 +35,7 @@ end function set:action(msg) local input = utilities.input(msg.text) - if not input:match('([^%s]+) (.+)') then + if not input or not input:match('([^%s]+) (.+)') then utilities.send_message(self, msg.chat.id, set.doc, true, msg.message_id, true) return end diff --git a/otouto/plugins/shell.lua b/miku/plugins/shell.lua similarity index 92% rename from otouto/plugins/shell.lua rename to miku/plugins/shell.lua index d97ace6..df48f12 100644 --- a/otouto/plugins/shell.lua +++ b/miku/plugins/shell.lua @@ -1,6 +1,6 @@ local shell = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') function shell:init(config) shell.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('sh', true).table @@ -10,6 +10,7 @@ function shell:action(msg, config) if msg.from.id ~= config.admin then utilities.send_reply(self, msg, config.errors.sudo) + return end local input = utilities.input(msg.text) diff --git a/miku/plugins/site_header.lua b/miku/plugins/site_header.lua new file mode 100644 index 0000000..9bab562 --- /dev/null +++ b/miku/plugins/site_header.lua @@ -0,0 +1,28 @@ +local site_header = {} + +local utilities = require('miku.utilities') + +function site_header:init(config) + site_header.triggers = { + "^/(head) ([%w-_%.%?%.:,/%+=&#!]+)$", + "^/(dig) ([%w-_%.%?%.:,/%+=&#!]+)$" + } +end + +function site_header:action(msg, config, matches) + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + end + + local url = matches[2] + if matches[1] == 'head' then + input = 'curl --head '..url + elseif matches[1] == 'dig' then + input = 'dig '..url..' ANY' + end + local output = io.popen(input):read('*all') + output = '```\n' .. output .. '\n```' + utilities.send_reply(self, msg, output, true) +end + +return site_header diff --git a/miku/plugins/soundcloud.lua b/miku/plugins/soundcloud.lua new file mode 100644 index 0000000..79c72fe --- /dev/null +++ b/miku/plugins/soundcloud.lua @@ -0,0 +1,41 @@ +local soundcloud = {} + +local http = require('socket.http') +local json = require('dkjson') +local utilities = require('miku.utilities') + +soundcloud.triggers = { + "soundcloud.com/([A-Za-z0-9-/-_-.]+)" +} + +local BASE_URL = 'http://api.soundcloud.com/resolve.json' +local client_id = cred_data.soundcloud_client_id + +function soundcloud:send_soundcloud_info(sc_url) + local url = BASE_URL..'?url=http://soundcloud.com/'..sc_url..'&client_id='..client_id + + local res,code = http.request(url) + if code ~= 200 then return nil end + local data = json.decode(res) + + local title = data.title + local description = data.description + local user = data.user.username + local user = 'Unbekannt' + local genre = data.genre + local playback_count = data.playback_count + local milliseconds = data.duration + local totalseconds = math.floor(milliseconds / 1000) + local duration = makeHumanTime(totalseconds) + + local text = '*'..title..'* von _'..user..'_\n_(Tag: '..genre..', '..duration..'; '..playback_count..' mal angehört)_\n'..description + return text +end + +function soundcloud:action(msg, config, matches) + local text = soundcloud:send_soundcloud_info(matches[1]) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + utilities.send_reply(self, msg, text, true) +end + +return soundcloud diff --git a/miku/plugins/speedtest.lua b/miku/plugins/speedtest.lua new file mode 100644 index 0000000..da76f6a --- /dev/null +++ b/miku/plugins/speedtest.lua @@ -0,0 +1,17 @@ +local speedtest = {} + +local utilities = require('miku.utilities') + +speedtest.triggers = { + "speedtest.net/my%-result/(%d+)", + "speedtest.net/my%-result/i/(%d+)" +} + +function speedtest:action(msg, config, matches) + local url = 'http://www.speedtest.net/result/'..matches[1]..'.png' + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) +end + +return speedtest \ No newline at end of file diff --git a/miku/plugins/spotify.lua b/miku/plugins/spotify.lua new file mode 100644 index 0000000..78f58b1 --- /dev/null +++ b/miku/plugins/spotify.lua @@ -0,0 +1,52 @@ +local spotify = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +spotify.triggers = { + "open.spotify.com/track/([A-Za-z0-9-]+)", + "play.spotify.com/track/([A-Za-z0-9-]+)" +} + +local BASE_URL = 'https://api.spotify.com/v1' + +function spotify:get_track_data(track) + local url = BASE_URL..'/tracks/'..track + local res,code = https.request(url) + if code ~= 200 then return nil end + local data = json.decode(res) + return data +end + +function spotify:send_track_data(data, self, msg) + local name = data.name + local album = data.album.name + local artist = data.artists[1].name + local preview = data.preview_url + local milliseconds = data.duration_ms + + -- convert s to mm:ss + local totalseconds = math.floor(milliseconds / 1000) + local duration = makeHumanTime(totalseconds) + + local text = '*'..name..'* von *'..artist..'* aus dem Album *'..album..'* _('..duration..')_' + if preview then + utilities.send_typing(self, msg.chat.id, 'upload_audio') + local file = download_to_file(preview, name..'.mp3') + utilities.send_audio(self, msg.chat.id, file, msg.message_id, totalseconds, artist, name) + return + else + utilities.send_reply(self, msg, text, true) + return + end +end + +function spotify:action(msg, config, matches) + local data = spotify:get_track_data(matches[1]) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + spotify:send_track_data(data, self, msg) + return +end + +return spotify diff --git a/miku/plugins/stats.lua b/miku/plugins/stats.lua new file mode 100644 index 0000000..00b7b5b --- /dev/null +++ b/miku/plugins/stats.lua @@ -0,0 +1,144 @@ +local stats = {} + +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() + +function stats:init(config) + stats.triggers = { + "^/([Ss]tats)$", + "^/([Ss]tats) (chat) (%-%d+)", + "^/([Ss]tats) (chat) (%d+)" + } + stats.doc = [[* +]]..config.cmd_pat..[[stats*: Zeigt Stats an +*]]..config.cmd_pat..[[stats* _chat_ __: Stats für Chat-ID (nur Superuser) +]] +end + +stats.command = 'stats' + +function stats:user_print_name(user) + if user.name then + return user.name + end + + local text = '' + if user.first_name then + text = user.last_name..' ' + end + if user.lastname then + text = text..user.last_name + end + + return text +end + +-- Returns a table with `name` and `msgs` +function stats:get_msgs_user_chat(user_id, chat_id) + local user_info = {} + local uhash = 'user:'..user_id + local user = redis:hgetall(uhash) + local um_hash = 'msgs:'..user_id..':'..chat_id + user_info.msgs = tonumber(redis:get(um_hash) or 0) + user_info.name = stats:user_print_name(user) + return user_info +end + +function stats:chat_stats(chat_id) + -- Users on chat + local hash = 'chat:'..chat_id..':users' + local users = redis:smembers(hash) + local users_info = {} + + -- Get user info + for i = 1, #users do + local user_id = users[i] + local user_info = stats:get_msgs_user_chat(user_id, chat_id) + table.insert(users_info, user_info) + end + + -- Sort users by msgs number + table.sort(users_info, function(a, b) + if a.msgs and b.msgs then + return a.msgs > b.msgs + end + end) + + local text = '' + for k,user in pairs(users_info) do + text = text..user.name..': '..user.msgs..'\n' + text = string.gsub(text, "%_", " ") -- Bot API doesn't use underscores anymore! Yippie! + end + if text:isempty() then return 'Keine Stats für diesen Chat verfügbar!'end + return text +end + +function stats:pre_process(msg, self) + -- Ignore service msg + if msg.service then -- check how Bot API handles service msgs, will update this + print('Service message') + return + end + + if msg.left_chat_member then + -- delete user from redis set, but keep message count + local hash = 'chat:'..msg.chat.id..':users' + local user_id_left = msg.left_chat_member.id + print('User '..user_id_left..' was kicked, deleting him/her from redis set '..hash) + redis:srem(hash, user_id_left) + return msg + end + + -- Save user on Redis + local hash = 'user:'..msg.from.id + -- print('Saving user', hash) -- remove comment to restore old behaviour + if msg.from.name then + redis:hset(hash, 'name', msg.from.name) + end + if msg.from.first_name then + redis:hset(hash, 'first_name', msg.from.first_name) + end + if msg.from.last_name then + redis:hset(hash, 'last_name', msg.from.last_name) + end + + -- Save stats on Redis + if msg.chat.type ~= 'private' then + -- User is on chat + local hash = 'chat:'..msg.chat.id..':users' + redis:sadd(hash, msg.from.id) + end + + -- Total user msgs + local hash = 'msgs:'..msg.from.id..':'..msg.chat.id + redis:incr(hash) + return msg +end + +function stats:action(msg, config, matches) + if matches[1]:lower() == "stats" then + + if not matches[2] then + if msg.chat.type == 'private' then + utilities.send_reply(self, msg, 'Stats funktionieren nur in Chats!') + return + else + local chat_id = msg.chat.id + utilities.send_reply(self, msg, stats:chat_stats(chat_id)) + return + end + end + + if matches[2] == "chat" then + if msg.from.id ~= config.admin then + utilities.send_reply(self, msg, config.errors.sudo) + return + else + utilities.send_reply(self, msg, stats:chat_stats(matches[3])) + return + end + end + end +end + +return stats \ No newline at end of file diff --git a/miku/plugins/steam.lua b/miku/plugins/steam.lua new file mode 100644 index 0000000..c6df8ea --- /dev/null +++ b/miku/plugins/steam.lua @@ -0,0 +1,63 @@ +local steam = {} + +local utilities = require('miku.utilities') +local http = require('socket.http') +local json = require('dkjson') +local bindings = require('miku.bindings') + +steam.triggers = { + "store.steampowered.com/app/([0-9]+)", + "steamcommunity.com/app/([0-9]+)" +} + +local BASE_URL = 'http://store.steampowered.com/api/appdetails/' +local DESC_LENTH = 400 + +function steam:get_steam_data(appid) + local url = BASE_URL + url = url..'?appids='..appid + url = url..'&l=german&cc=DE' + local res,code = http.request(url) + if code ~= 200 then return nil end + local data = json.decode(res)[appid].data + return data +end + +function steam:price_info(data) + local price = '' -- If no data is empty + + if data then + local initial = data.initial + local final = data.final or data.initial + local min = math.min(data.initial, data.final) + price = tostring(min/100) + if data.discount_percent and initial ~= final then + price = price..data.currency..' ('..data.discount_percent..'% OFF)' + end + price = price..' €' + end + + return price +end + +function steam:send_steam_data(data, self, msg) + local description = string.sub(unescape(data.about_the_game:gsub("%b<>", "")), 1, DESC_LENTH) .. '...' + local title = data.name + local price = steam:price_info(data.price_overview) + + local text = '*'..title..'* _'..price..'_\n'..description + local image_url = data.header_image + return text, image_url +end + +function steam:action(msg) + local data = steam:get_steam_data(matches[1]) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + + local text, image_url = steam:send_steam_data(data, self, msg) + utilities.send_typing(self, msg.chat.id, 'upload_photo') + utilities.send_photo(self, msg.chat.id, download_to_file(image_url, matches[1]..'.jpg'), nil, msg.message_id) + utilities.send_reply(self, msg, text, true) +end + +return steam \ No newline at end of file diff --git a/miku/plugins/streamable.lua b/miku/plugins/streamable.lua new file mode 100644 index 0000000..d779f9d --- /dev/null +++ b/miku/plugins/streamable.lua @@ -0,0 +1,53 @@ +local streamable = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +streamable.triggers = { + "streamable.com/([A-Za-z0-9-_-]+)", +} + +function streamable:send_streamable_video(shortcode, self, msg) + local BASE_URL = "https://api.streamable.com" + local url = BASE_URL..'/videos/'..shortcode + local res,code = https.request(url) + if code ~= 200 then return 'HTTP-Fehler' end + local data = json.decode(res) + if data.status ~= 2 then utilities.send_reply(self, msg, "Video ist (noch) nicht verfügbar.") return end + + if data.files.webm then + if data.title == "" then title = shortcode..'.webm' else title = data.title..'.webm' end + url = 'https:'..data.files.webm.url + width = data.files.webm.width + height = data.files.webm.height + if data.files.webm.size > 50000000 then + local size = math.floor(data.files.webm.size / 1000000) + utilities.send_reply(self, msg, '*Video ist größer als 50 MB* ('..size..' MB)!\n[Direktlink]('..url..')', true) + return + end + elseif data.files.mp4 then + if data.title == "" then title = shortcode..'.mp4' else title = data.title..'.mp4' end + url = 'https:'..data.files.mp4.url + width = data.files.mp4.width + height = data.files.mp4.height + if data.files.mp4.size > 50000000 then + local size = math.floor(data.files.mp4.size / 1000000) + utilities.send_reply(self, msg, '*Video ist größer als 50 MB* ('..size..' MB)!\n[Direktlink]('..url..')', true) + return + end + end + + utilities.send_typing(self, msg.chat.id, 'upload_video') + local file = download_to_file(url, title) + utilities.send_video(self, msg.chat.id, file, nil, msg.message_id, nil, width, height) + return +end + +function streamable:action(msg, config, matches) + local shortcode = matches[1] + streamable:send_streamable_video(shortcode, self, msg) + return +end + +return streamable diff --git a/miku/plugins/surrogate.lua b/miku/plugins/surrogate.lua new file mode 100644 index 0000000..a752781 --- /dev/null +++ b/miku/plugins/surrogate.lua @@ -0,0 +1,16 @@ +local surrogate = {} + +local utilities = require('miku.utilities') + +surrogate.triggers = { + "^/s (%-%d+) +(.+)$", + "^/s (%d+) +(.+)$" +} + +function surrogate:action(msg) + -- Supergroups don't work!? + utilities.send_message(self, matches[1], matches[2], true, nil, true) + return +end + +return surrogate diff --git a/miku/plugins/tagesschau.lua b/miku/plugins/tagesschau.lua new file mode 100644 index 0000000..28cf7af --- /dev/null +++ b/miku/plugins/tagesschau.lua @@ -0,0 +1,56 @@ +local tagesschau = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +tagesschau.triggers = { + "tagesschau.de/([A-Za-z0-9-_-_-/]+).html" +} + +local BASE_URL = 'https://www.tagesschau.de/api' + +local makeOurDate = function(dateString) + local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+)%:(%d+)%:(%d+)" + local year, month, day, hours, minutes, seconds = dateString:match(pattern) + return day..'.'..month..'.'..year..' um '..hours..':'..minutes..':'..seconds +end + +function tagesschau:get_tagesschau_article(article) + local url = BASE_URL..'/'..article..'.json' + local res,code = https.request(url) + local data = json.decode(res) + if code == 404 then return "Artikel nicht gefunden!" end + if code ~= 200 then return "HTTP-Fehler" end + if not data then return "HTTP-Fehler" end + if data.type ~= "story" then + print('Typ "'..data.type..'" wird nicht unterstützt') + return nil + end + + local title = data.topline..': '..data.headline + local news = data.shorttext + local posted_at = makeOurDate(data.date)..' Uhr' + + local text = '*'..title..'*\n_'..posted_at..'_\n'..news + if data.banner[1] then + return text, data.banner[1].variants[1].modPremium + else + return text + end +end + +function tagesschau:action(msg, config, matches) + local article = matches[1] + local text, image_url = tagesschau:get_tagesschau_article(article) + if image_url then + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) +end + +return tagesschau \ No newline at end of file diff --git a/otouto/plugins/tagesschau_eil.lua b/miku/plugins/tagesschau_eil.lua similarity index 89% rename from otouto/plugins/tagesschau_eil.lua rename to miku/plugins/tagesschau_eil.lua index 07cf81c..34a832c 100644 --- a/otouto/plugins/tagesschau_eil.lua +++ b/miku/plugins/tagesschau_eil.lua @@ -4,8 +4,8 @@ local http = require('socket.http') local https = require('ssl.https') local url = require('socket.url') local json = require('dkjson') -local utilities = require('otouto.utilities') -local redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local redis = (loadfile "./miku/redis.lua")() tagesschau_eil.command = 'eil ' @@ -88,22 +88,22 @@ function tagesschau_eil:cron(self_plz) -- print('EIL: Prüfe...') local last_eil = redis:get(hash..':last_entry') local res,code = http.request(url) - local data = json.decode(res) if code ~= 200 then return end + local data = json.decode(res) if not data then return end if data.breakingnews[1] then - if data.breakingnews[1].details ~= last_eil then + if data.breakingnews[1].date ~= last_eil then local title = '#EIL: *'..data.breakingnews[1].headline..'*' local news = data.breakingnews[1].shorttext local posted_at = makeOurDate(data.breakingnews[1].date)..' Uhr' local post_url = string.gsub(data.breakingnews[1].details, '/api/', '/') local post_url = string.gsub(post_url, '.json', '.html') - local eil = title..'\n_'..posted_at..'_\n'..news..'\n[Artikel aufrufen]('..post_url..')' - redis:set(hash..':last_entry', data.breakingnews[1].details) + local eil = title..'\n_'..posted_at..'_\n'..news + redis:set(hash..':last_entry', data.breakingnews[1].date) for _,user in pairs(redis:smembers(hash..':subs')) do local user = string.gsub(user, 'chat%#id', '') local user = string.gsub(user, 'user%#id', '') - utilities.send_message(self, user, eil, true, nil, true) + utilities.send_message(self, user, eil, true, nil, true, '{"inline_keyboard":[[{"text":"Eilmeldung aufrufen","url":"'..post_url..'"}]]}') end end end diff --git a/miku/plugins/tex.lua b/miku/plugins/tex.lua new file mode 100644 index 0000000..d4c7402 --- /dev/null +++ b/miku/plugins/tex.lua @@ -0,0 +1,34 @@ +local tex = {} + +local URL = require('socket.url') +local utilities = require('miku.utilities') + +tex.command = 'tex ' + +function tex:init(config) + tex.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('tex', true).table + tex.doc = [[* +]]..config.cmd_pat..[[tex* __: Konvertiert LaTeX in ein Bild]] +end + +function tex: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, tex.doc, true, msg.message_id, true) + return + end + end + + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local eq = URL.escape(input) + + local url = "http://latex.codecogs.com/png.download?" + .."\\dpi{300}%20\\LARGE%20"..eq + local file = download_to_file(url, 'latex.png') + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) +end + +return tex diff --git a/miku/plugins/thetvdb.lua b/miku/plugins/thetvdb.lua new file mode 100644 index 0000000..0aa7381 --- /dev/null +++ b/miku/plugins/thetvdb.lua @@ -0,0 +1,96 @@ +local tv = {} + +local http = require('socket.http') +local URL = require('socket.url') +local xml = require("xml") +local utilities = require('miku.utilities') + +tv.command = 'tv ' + +function tv:init(config) + tv.triggers = { + "^/tv (.+)$" + } + tv.doc = [[* +]]..config.cmd_pat..[[tv*_ _: Sendet Infos zur TV-Serie]] +end + +local BASE_URL = 'http://thetvdb.com/api' + +local makeOurDate = function(dateString) + local pattern = "(%d+)%-(%d+)%-(%d+)" + local year, month, day = dateString:match(pattern) + return day..'.'..month..'.'..year +end + + +function tv:get_tv_info(series) + local url = BASE_URL..'/GetSeries.php?seriesname='..series..'&language=de' + local res,code = http.request(url) + if code ~= 200 then return "HTTP-ERROR" end + local result = xml.load(res) + if not xml.find(result, 'seriesid') then return "NOTFOUND" end + return result +end + +function tv:send_tv_data(result, self, msg) + local title = xml.find(result, 'SeriesName')[1] + local id = xml.find(result, 'seriesid')[1] + + if xml.find(result, 'AliasNames') and xml.find(result, 'AliasNames')[1] ~= title then + alias = '\noder: '..xml.find(result, 'AliasNames')[1] + else + alias = '' + end + + if xml.find(result, 'Overview') then + desc = '\n_'..string.sub(xml.find(result, 'Overview')[1], 1, 250) .. '..._' + else + desc = '' + end + + if xml.find(result, 'FirstAired') then + aired = '\n*Erstausstrahlung:* '..makeOurDate(xml.find(result, 'FirstAired')[1]) + else + aired = '' + end + + + if xml.find(result, 'Network') then + publisher = '\n*Publisher:* '..xml.find(result, 'Network')[1] + else + publisher = '' + end + + if xml.find(result, 'IMDB_ID') then + imdb = '\n[IMDB-Seite](http://www.imdb.com/title/'..xml.find(result, 'IMDB_ID')[1]..')' + else + imdb = '' + end + + local text = '*'..title..'*'..alias..aired..publisher..imdb..desc..'\n[TVDB-Seite besuchen](http://thetvdb.com/?id='..id..'&tab=series)' + if xml.find(result, 'banner') then + local image_url = 'http://www.thetvdb.com/banners/'..xml.find(result, 'banner')[1] + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(image_url) + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + utilities.send_reply(self, msg, text, true) +end + + +function tv:action(msg, config, matches) + local series = URL.escape(matches[1]) + local tv_info = tv:get_tv_info(series) + if tv_info == "NOTFOUND" then + utilities.send_reply(self, msg, config.errors.results) + return + elseif tv_info == "HTTP-ERROR" then + utilities.send_reply(self, msg, config.errors.connection) + return + else + tv:send_tv_data(tv_info, self,msg) + end +end + +return tv diff --git a/otouto/plugins/time.lua b/miku/plugins/time.lua similarity index 98% rename from otouto/plugins/time.lua rename to miku/plugins/time.lua index 38d9ce1..6338241 100644 --- a/otouto/plugins/time.lua +++ b/miku/plugins/time.lua @@ -2,7 +2,7 @@ local time = {} local HTTPS = require('ssl.https') local JSON = require('dkjson') -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') time.command = 'time ' diff --git a/miku/plugins/translate.lua b/miku/plugins/translate.lua new file mode 100644 index 0000000..a6e9943 --- /dev/null +++ b/miku/plugins/translate.lua @@ -0,0 +1,141 @@ +local translate = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local json = require('dkjson') +local utilities = require('miku.utilities') +local mime = require("mime") +require("./miku/plugins/pasteee") + +translate.command = 'translate [Text]' + +function translate:init(config) + translate.triggers = { + "^/translate ([%w]+),([%a]+) (.+)", + "^/translate (to%:)([%w]+) (.+)", + "^/translate (.+)", + "^/getlanguages$", + "^/(whatlang) (.+)" + } + translate.doc = [[* +]]..config.cmd_pat..[[translate* _[Text]_: Übersetze Text in deutsch +*]]..config.cmd_pat..[[translate* to:Zielsprache _[Text]_: Übersetze Text in Zielsprache +*]]..config.cmd_pat..[[translate* Quellsprache,Zielsprache _[Text]_: Übersetze Text von beliebiger Sprache in beliebige Sprache +*]]..config.cmd_pat..[[getlanguages*: Postet alle verfügbaren Sprachcodes +*]]..config.cmd_pat..[[whatlang* _[Text]_: Gibt erkannte Sprache zurück]] +end + +local bing_key = cred_data.bing_key +local accountkey = mime.b64(bing_key..':'..bing_key) + +function translate:translate(source_lang, target_lang, text) + if not target_lang then target_lang = 'de' end + local url = 'https://api.datamarket.azure.com/Bing/MicrosoftTranslator/Translate?$format=json&Text=%27'..URL.escape(text)..'%27&To=%27'..target_lang..'%27&From=%27'..source_lang..'%27' + local response_body = {} + local request_constructor = { + url = url, + method = "GET", + sink = ltn12.sink.table(response_body), + headers = { + Authorization = "Basic "..accountkey + } + } + local ok, response_code, response_headers, response_status_line = https.request(request_constructor) + if not ok or response_code ~= 200 then return 'Ein Fehler ist aufgetreten.' end + + local trans = json.decode(table.concat(response_body)).d.results[1].Text + + return trans +end + +function translate:detect_language(text) + local url = 'https://api.datamarket.azure.com/Bing/MicrosoftTranslator/Detect?$format=json&Text=%27'..URL.escape(text)..'%27' + local response_body = {} + local request_constructor = { + url = url, + method = "GET", + sink = ltn12.sink.table(response_body), + headers = { + Authorization = "Basic "..accountkey + } + } + local ok, response_code, response_headers, response_status_line = https.request(request_constructor) + if not ok or response_code ~= 200 then return 'en' end + + local language = json.decode(table.concat(response_body)).d.results[1].Code + print('Erkannte Sprache: '..language) + return language +end + +function translate:get_all_languages() + local url = 'https://api.datamarket.azure.com/Bing/MicrosoftTranslator/GetLanguagesForTranslation?$format=json' + local response_body = {} + local request_constructor = { + url = url, + method = "GET", + sink = ltn12.sink.table(response_body), + headers = { + Authorization = "Basic "..accountkey + } + } + local ok, response_code, response_headers, response_status_line = https.request(request_constructor) + if not ok or response_code ~= 200 then return 'Ein Fehler ist aufgetreten.' end + + local lang_table = json.decode(table.concat(response_body)).d.results + + local languages = "" + for i in pairs(lang_table) do + languages = languages..lang_table[i].Code..'\n' + end + + local link = upload(languages) + return '[Sprachliste auf Paste.ee ansehen]('..link..')' +end + +function translate:action(msg, config, matches) + utilities.send_typing(self, msg.chat.id, 'typing') + + if matches[1] == '/getlanguages' then + utilities.send_reply(self, msg, translate:get_all_languages(), true) + return + end + + if matches[1] == 'whatlang' and matches[2] then + local text = matches[2] + local lang = translate:detect_language(text) + utilities.send_reply(self, msg, 'Erkannte Sprache: '..lang, true) + return + end + + -- Third pattern + if #matches == 1 then + print("First") + local text = matches[1] + local language = translate:detect_language(text) + utilities.send_reply(self, msg, translate:translate(language, nil, text)) + return + end + + -- Second pattern + if #matches == 3 and matches[1] == "to:" then + print("Second") + local target = matches[2] + local text = matches[3] + local language = translate:detect_language(text) + utilities.send_reply(self, msg, translate:translate(language, target, text)) + return + end + + -- First pattern + if #matches == 3 then + print("Third") + local source = matches[1] + local target = matches[2] + local text = matches[3] + utilities.send_reply(self, msg, translate:translate(source, target, text)) + return + end + +end + +return translate diff --git a/miku/plugins/tweet.lua b/miku/plugins/tweet.lua new file mode 100644 index 0000000..926c94d --- /dev/null +++ b/miku/plugins/tweet.lua @@ -0,0 +1,204 @@ +local tweet = {} + +local utilities = require('miku.utilities') +local HTTPS = require('ssl.https') +local JSON = require('dkjson') +local redis = (loadfile "./miku/redis.lua")() +local OAuth = (require "OAuth") +local bindings = require('miku.bindings') + +function tweet:init(config) + if not cred_data.tw_consumer_key then + print('Missing config value: tw_consumer_key.') + print('tweet.lua will not be enabled.') + return + elseif not cred_data.tw_consumer_secret then + print('Missing config value: tw_consumer_secret.') + print('tweet.lua will not be enabled.') + return + elseif not cred_data.tw_access_token then + print('Missing config value: tw_access_token.') + print('tweet.lua will not be enabled.') + return + elseif not cred_data.tw_access_token_secret then + print('Missing config value: tw_access_token_secret.') + print('tweet.lua will not be enabled.') + return + end + + tweet.triggers = { + "^/tweet (id) ([%w_%.%-]+)$", + "^/tweet (id) ([%w_%.%-]+) (last)$", + "^/tweet (id) ([%w_%.%-]+) (last) ([%d]+)$", + "^/tweet (name) ([%w_%.%-]+)$", + "^/tweet (name) ([%w_%.%-]+) (last)$", + "^/tweet (name) ([%w_%.%-]+) (last) ([%d]+)$" + } + tweet.doc = [[* +]]..config.cmd_pat..[[tweet* id _[id]_: Zufälliger Tweet vom User mit dieser ID +*]]..config.cmd_pat..[[tweet* id _[id]_ last: Aktuellster Tweet vom User mit dieser ID +*]]..config.cmd_pat..[[tweet* name _[Name]_: Zufälliger Tweet vom User mit diesem Namen +*]]..config.cmd_pat..[[tweet* name _[Name]_ last: Aktuellster Tweet vom User mit diesem Namen]] +end + +tweet.command = 'tweet name ' + +local consumer_key = cred_data.tw_consumer_key +local consumer_secret = cred_data.tw_consumer_secret +local access_token = cred_data.tw_access_token +local access_token_secret = cred_data.tw_access_token_secret + +local client = OAuth.new(consumer_key, consumer_secret, { + RequestToken = "https://api.twitter.com/oauth/request_token", + AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, + AccessToken = "https://api.twitter.com/oauth/access_token" +}, { + OAuthToken = access_token, + OAuthTokenSecret = access_token_secret +}) + +local twitter_url = "https://api.twitter.com/1.1/statuses/user_timeline.json" + +function tweet:analyze_tweet(tweet) + local header = "Tweet von " .. tweet.user.name .. " (@" .. tweet.user.screen_name .. ")\nhttps://twitter.com/statuses/" .. tweet.id_str + local text = tweet.text + + -- replace short URLs + if tweet.entities.urls then + for k, v in pairs(tweet.entities.urls) do + local short = v.url + local long = v.expanded_url + text = text:gsub(short, long) + end + end + + -- remove urls + local urls = {} + if tweet.extended_entities and tweet.extended_entities.media then + for k, v in pairs(tweet.extended_entities.media) do + if v.video_info and v.video_info.variants then -- If it's a video! + table.insert(urls, v.video_info.variants[1].url) + else -- If not, is an image + table.insert(urls, v.media_url) + end + text = text:gsub(v.url, "") -- Replace the URL in text + text = unescape(text) + end + end + + return header, text, urls +end + +function tweet:send_all_files(self, msg, urls) + local data = { + images = { + func = send_photos_from_url, + urls = {} + }, + gifs = { + func = send_gifs_from_url, + urls = {} + }, + videos = { + func = send_videos_from_url, + urls = {} + } + } + + local table_to_insert = nil + for i,url in pairs(urls) do + local _, _, extension = string.match(url, "(https?)://([^\\]-([^\\%.]+))$") + local mime_type = mimetype.get_content_type_no_sub(extension) + if extension == 'gif' then + table_to_insert = data.gifs.urls + elseif mime_type == 'image' then + table_to_insert = data.images.urls + elseif mime_type == 'video' then + table_to_insert = data.videos.urls + else + table_to_insert = nil + end + if table_to_insert then + table.insert(table_to_insert, url) + end + end + for k, v in pairs(data) do + if #v.urls > 0 then + end + v.func(receiver, v.urls) + end +end + +function tweet:sendTweet(self, msg, tweet) + local header, text, urls = tweet:analyze_tweet(tweet) + -- send the parts + local text = unescape(text) + send_reply(self, msg, header .. "\n" .. text) + tweet:send_all_files(self, msg, urls) + return nil +end + +function tweet:getTweet(self, msg, base, all) + local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", twitter_url, base) + + if response_code ~= 200 then + return "Konnte nicht verbinden, evtl. existiert der User nicht?" + end + + local response = JSON.decode(response_body) + if #response == 0 then + return "Konnte keinen Tweet bekommen, sorry" + end + if all then + for i,tweet in pairs(response) do + tweet:sendTweet(self, msg, tweet) + end + else + local i = math.random(#response) + local tweet = response[i] + tweet:sendTweet(self, msg, tweet) + end + + return nil +end + +function tweet:isint(n) + return n==math.floor(n) +end + +function tweet:action(msg, config, matches) + local base = {include_rts = 1} + + if matches[1] == 'id' then + local userid = tonumber(matches[2]) + if userid == nil or not tweet:isint(userid) then + utilities.send_reply(self, msg, "Die ID eines Users ist eine Zahl, du findest sie, indem du den Namen [auf dieser Webseite](http://gettwitterid.com/) eingibst.", true) + return + end + base.user_id = userid + elseif matches[1] == 'name' then + base.screen_name = matches[2] + else + return "" + end + + local count = 200 + local all = false + if #matches > 2 and matches[3] == 'last' then + count = 1 + if #matches == 4 then + local n = tonumber(matches[4]) + if n > 10 then + utilities.send_reply(self, msg, "Du kannst nur 10 Tweets auf einmal abfragen!") + return + end + count = matches[4] + all = true + end + end + base.count = count + + utilities.send_reply(self, msg, tweet:getTweet(self, msg, base, all)) +end + +return tweet diff --git a/miku/plugins/twitch.lua b/miku/plugins/twitch.lua new file mode 100644 index 0000000..c9e0a8a --- /dev/null +++ b/miku/plugins/twitch.lua @@ -0,0 +1,40 @@ +local twitch = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +twitch.triggers = { + "twitch.tv/([A-Za-z0-9-_-]+)" +} + +local BASE_URL = 'https://api.twitch.tv' + +function twitch:send_twitch_info(twitch_name) + local url = BASE_URL..'/kraken/channels/'..twitch_name + local res,code = https.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + local data = json.decode(res) + + local display_name = data.display_name + local name = data.name + if not data.game then + game = 'nichts' + else + game = data.game + end + local status = data.status + local views = comma_value(data.views) + local followers = comma_value(data.followers) + local text = '*'..display_name..'* ('..name..') streamt *'..game..'*\n'..status..'\n_'..views..' Zuschauer insgesamt und '..followers..' Follower_' + + return text +end + +function twitch:action(msg, config, matches) + local text = twitch:send_twitch_info(matches[1]) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + utilities.send_reply(self, msg, text, true) +end + +return twitch diff --git a/otouto/plugins/twitter.lua b/miku/plugins/twitter.lua similarity index 97% rename from otouto/plugins/twitter.lua rename to miku/plugins/twitter.lua index 92175cd..10e6173 100644 --- a/otouto/plugins/twitter.lua +++ b/miku/plugins/twitter.lua @@ -1,11 +1,11 @@ local twitter = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local HTTPS = require('ssl.https') local JSON = require('dkjson') -local redis = (loadfile "./otouto/redis.lua")() +local redis = (loadfile "./miku/redis.lua")() local OAuth = (require "OAuth") -local bindings = require('otouto.bindings') +local bindings = require('miku.bindings') function twitter:init(config) if not cred_data.tw_consumer_key then diff --git a/otouto/plugins/twitter_send.lua b/miku/plugins/twitter_send.lua similarity index 90% rename from otouto/plugins/twitter_send.lua rename to miku/plugins/twitter_send.lua index 67207be..0e519cd 100644 --- a/otouto/plugins/twitter_send.lua +++ b/miku/plugins/twitter_send.lua @@ -4,10 +4,10 @@ local http = require('socket.http') 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 utilities = require('miku.utilities') +local bindings = require('miku.bindings') local OAuth = require "OAuth" -local redis = (loadfile "./otouto/redis.lua")() +local redis = (loadfile "./miku/redis.lua")() function twitter_send:init(config) if not cred_data.tw_consumer_key then @@ -71,9 +71,9 @@ function twitter_send:do_twitter_authorization_flow(hash, is_chat) local auth_url = client:BuildAuthorizationUrl({ oauth_callback = callback_url, force_login = true }) if is_chat then - return 'Bitte schließe den Vorgang ab, indem du unten auf den Link klickst und mir die angezeigte PIN per `/tw auth PIN` *im Chat von gerade* übergibst.\n[Bei Twitter anmelden]('..auth_url..')' + return 'Bitte schließe den Vorgang ab, indem du unten auf den attraktiven Button klickst und mir die angezeigte PIN per `/tw auth PIN` *in der Gruppe von gerade eben* übergibst.', auth_url else - return 'Bitte schließe den Vorgang ab, indem du unten auf den Link klickst und mir die angezeigte PIN per `/tw auth PIN` übergibst.\n[Bei Twitter anmelden]('..auth_url..')' + return 'Bitte schließe den Vorgang ab, indem du unten auf den attraktiven Button klickst und mir die angezeigte PIN per `/tw auth PIN` übergibst.', auth_url end end @@ -283,19 +283,20 @@ function twitter_send:action(msg, config, matches) utilities.send_reply(self, msg, config.errors.sudo) return else - -- maybe we can edit the older message to update it to "logged in!"? - -- this should be interesting: https://core.telegram.org/bots/api#editmessagetext - local text = twitter_send:do_twitter_authorization_flow(hash, true) - local res = utilities.send_message(self, msg.from.id, text, true, nil, true) + local text, auth_url = twitter_send:do_twitter_authorization_flow(hash, true) + local res = utilities.send_message(self, msg.from.id, text, true, nil, true, '{"inline_keyboard":[[{"text":"Bei Twitter anmelden","url":"'..auth_url..'"}]]}') if not res then utilities.send_reply(self, msg, 'Bitte starte mich zuerst [privat](http://telegram.me/' .. self.info.username .. '?start).', true) elseif msg.chat.type ~= 'private' then - utilities.send_message(self, msg.chat.id, '_Bitte warten, der Administrator meldet sich an..._', true, nil, true) + local result = utilities.send_message(self, msg.chat.id, '_Bitte warten, der Administrator meldet sich an..._', true, nil, true) + redis:hset(hash, 'login_msg', result.result.message_id) end return end else - utilities.send_reply(self, msg, twitter_send:do_twitter_authorization_flow(hash), true) + local text, auth_url = twitter_send:do_twitter_authorization_flow(hash) + local result = utilities.send_reply(self, msg, text, true, '{"inline_keyboard":[[{"text":"Bei Twitter anmelden","url":"'..auth_url..'"}]]}') + redis:hset(hash, 'login_msg', result.result.message_id) return end end @@ -309,6 +310,9 @@ function twitter_send:action(msg, config, matches) end if string.len(matches[2]) > 7 then utilities.send_reply(self, msg, 'Invalide PIN!') return end utilities.send_reply(self, msg, twitter_send:get_twitter_access_token(hash, matches[2], oauth_token, oauth_token_secret)) + local message_id = redis:hget(hash, 'login_msg') + utilities.edit_message(self, msg.chat.id, message_id, '*Anmeldung abgeschlossen!*', true, true) + redis:hdel(hash, 'login_msg') return end diff --git a/miku/plugins/twitter_user.lua b/miku/plugins/twitter_user.lua new file mode 100644 index 0000000..7b01263 --- /dev/null +++ b/miku/plugins/twitter_user.lua @@ -0,0 +1,119 @@ +local twitter_user = {} + +local utilities = require('miku.utilities') +local http = require('socket.http') +local https = require('ssl.https') +local json = require('dkjson') +local OAuth = (require "OAuth") +local bindings = require('miku.bindings') + +function twitter_user:init(config) + if not cred_data.tw_consumer_key then + print('Missing config value: tw_consumer_key.') + print('twitter_user.lua will not be enabled.') + return + elseif not cred_data.tw_consumer_secret then + print('Missing config value: tw_consumer_secret.') + print('twitter_user.lua will not be enabled.') + return + elseif not cred_data.tw_access_token then + print('Missing config value: tw_access_token.') + print('twitter_user.lua will not be enabled.') + return + elseif not cred_data.tw_access_token_secret then + print('Missing config value: tw_access_token_secret.') + print('twitter_user.lua will not be enabled.') + return + end + + twitter_user.triggers = { + "twitter.com/([A-Za-z0-9-_-.-_-]+)$" + } +end + +local consumer_key = cred_data.tw_consumer_key +local consumer_secret = cred_data.tw_consumer_secret +local access_token = cred_data.tw_access_token +local access_token_secret = cred_data.tw_access_token_secret + +local client = OAuth.new(consumer_key, consumer_secret, { + RequestToken = "https://api.twitter.com/oauth/request_token", + AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, + AccessToken = "https://api.twitter.com/oauth/access_token" +}, { + OAuthToken = access_token, + OAuthTokenSecret = access_token_secret +}) + +function twitter_user:resolve_url(url) + local response_body = {} + local request_constructor = { + url = url, + method = "HEAD", + sink = ltn12.sink.table(response_body), + headers = {}, + redirect = false + } + + local ok, response_code, response_headers, response_status_line = http.request(request_constructor) + if ok and response_headers.location then + return response_headers.location + else + return url + end +end + +function twitter_user:action(msg) + local twitter_url = "https://api.twitter.com/1.1/users/show/"..matches[1]..".json" + local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", twitter_url) + local response = json.decode(response_body) + if response_code ~= 200 then return end + + local full_name = response.name + local user_name = response.screen_name + if response.verified then + user_name = user_name..' ✅' + end + if response.protected then + user_name = user_name..' 🔒' + end + local header = full_name.. " (@" ..user_name.. ")\n" + + local description = unescape(response.description) + if response.location then + location = response.location + else + location = '' + end + if response.url and response.location ~= '' then + url = ' | '..twitter_user:resolve_url(response.url)..'\n' + elseif response.url and response.location == '' then + url = twitter_user:resolve_url(response.url)..'\n' + else + url = '\n' + end + + local body = description..'\n'..location..url + + local favorites = comma_value(response.favourites_count) + local follower = comma_value(response.followers_count) + local following = comma_value(response.friends_count) + local statuses = comma_value(response.statuses_count) + local footer = statuses..' Tweets, '..follower..' Follower, '..following..' folge ich, '..favorites..' Tweets favorisiert' + + local pic_url = string.gsub(response.profile_image_url_https, "normal", "400x400") + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(pic_url) + + local text = header..body..footer + if string.len(text) > 199 then -- can only send captions with < 200 characters + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + utilities.send_reply(self, msg, text) + return + else + utilities.send_photo(self, msg.chat.id, file, text, msg.message_id) + return + end +end + +return twitter_user diff --git a/otouto/plugins/urbandictionary.lua b/miku/plugins/urbandictionary.lua similarity index 82% rename from otouto/plugins/urbandictionary.lua rename to miku/plugins/urbandictionary.lua index c206057..27753f8 100644 --- a/otouto/plugins/urbandictionary.lua +++ b/miku/plugins/urbandictionary.lua @@ -3,7 +3,7 @@ local urbandictionary = {} local HTTP = require('socket.http') local URL = require('socket.url') local JSON = require('dkjson') -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') urbandictionary.command = 'urbandictionary ' @@ -43,14 +43,14 @@ function urbandictionary:action(msg, config) return end - local output = '*' .. jdat.list[1].word .. '*\n\n' .. utilities.trim(jdat.list[1].definition) + local output = '*' .. jdat.list[1].word .. '*\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) .. '_' + output = output .. '_\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 diff --git a/miku/plugins/venue.lua b/miku/plugins/venue.lua new file mode 100644 index 0000000..45c5264 --- /dev/null +++ b/miku/plugins/venue.lua @@ -0,0 +1,31 @@ +local venue = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +venue.triggers = { + '/nil' +} + +local apikey = cred_data.google_apikey + +function venue:pre_process(msg, self) + if not msg.venue then return end -- Ignore + + local lat = msg.venue.location.latitude + local lng = msg.venue.location.longitude + local url = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='..lat..','..lng..'&result_type=street_address&language=de&key='..apikey + local res, code = https.request(url) + if code ~= 200 then return msg end + local data = json.decode(res).results[1] + local city = data.formatted_address + utilities.send_reply(self, msg, city) + + return msg +end + +function venue:action(msg) +end + +return venue diff --git a/miku/plugins/vimeo.lua b/miku/plugins/vimeo.lua new file mode 100644 index 0000000..83789ac --- /dev/null +++ b/miku/plugins/vimeo.lua @@ -0,0 +1,38 @@ +local vimeo = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +vimeo.triggers = { + "vimeo.com/(%d+)" +} + +local BASE_URL = 'https://vimeo.com/api/v2' + +function vimeo:send_vimeo_data (vimeo_code) + local url = BASE_URL..'/video/'..vimeo_code..'.json' + local res,code = https.request(url) + if code ~= 200 then return "HTTP FEHLER" end + local data = json.decode(res) + + local title = '*'..data[1].title..'*' + local uploader = data[1].user_name + local totalseconds = data[1].duration + local duration = makeHumanTime(totalseconds) + + if not data[1].stats_number_of_plays then + return title..'\n_(Hochgeladen von: '..uploader..', '..duration..')_' + else + local viewCount = ', '..comma_value(data[1].stats_number_of_plays)..' mal angsehen)' or "" + return title..'\n_(Hochgeladen von: '..uploader..', '..duration..viewCount..'_' + end +end + +function vimeo:action(msg, config, matches) + local text = vimeo:send_vimeo_data(matches[1]) + if not text then utilities.send_reply(self, msg, config.errors.connection) return end + utilities.send_reply(self, msg, text, true) +end + +return vimeo diff --git a/miku/plugins/vine.lua b/miku/plugins/vine.lua new file mode 100644 index 0000000..3cd83f0 --- /dev/null +++ b/miku/plugins/vine.lua @@ -0,0 +1,45 @@ +local vine = {} + +local https = require('ssl.https') +local json = require('dkjson') +local utilities = require('miku.utilities') + +vine.triggers = { + "vine.co/v/([A-Za-z0-9-_-]+)" +} + +local BASE_URL = 'https://vine.co' + +function vine:get_vine_data(vine_code) + local res, code = https.request(BASE_URL..'/v/'..vine_code..'/embed/simple') + if code ~= 200 then return nil end + local json_data = string.match(res, '') + local data = json.decode(json_data).post + return data +end + +function vine:send_vine_data(data) + local title = data.description + local author_name = data.user.username + local creation_date = data.createdPretty + local loops = data.loops.count + local video_url = data.videoUrls[1].videoUrl + local profile_name = string.gsub(data.user.profileUrl, '/', '') + local text = '"'..title..'", hochgeladen von '..author_name..' ('..profile_name..'), '..creation_date..', '..loops..'x angesehen' + if data.explicitContent == 1 then + text = text..' (🔞 NSFW 🔞)' + end + local file = download_to_file(video_url, data.shortId..'.mp4') + return text, file +end + +function vine:action(msg, config, matches) + local data = vine:get_vine_data(matches[1]) + if not data then utilities.send_reply(self, msg, config.errors.connection) return end + + utilities.send_typing(self, msg.chat.id, 'upload_video') + local text, file = vine:send_vine_data(data) + utilities.send_video(self, msg.chat.id, file, text, msg.message_id) +end + +return vine diff --git a/otouto/plugins/weather.lua b/miku/plugins/weather.lua similarity index 96% rename from otouto/plugins/weather.lua rename to miku/plugins/weather.lua index ee7399c..ee5defc 100644 --- a/otouto/plugins/weather.lua +++ b/miku/plugins/weather.lua @@ -3,9 +3,9 @@ local weather = {} 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 redis = (loadfile "./otouto/redis.lua")() +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') +local redis = (loadfile "./miku/redis.lua")() function weather:init(config) if not cred_data.forecastio_apikey then @@ -30,7 +30,7 @@ function weather:init(config) ]] end -weather.command = 'wetter' +weather.command = 'w [Ort]' local BASE_URL = "https://api.forecast.io/forecast" local apikey = cred_data.forecastio_apikey diff --git a/miku/plugins/webshot.lua b/miku/plugins/webshot.lua new file mode 100644 index 0000000..337d480 --- /dev/null +++ b/miku/plugins/webshot.lua @@ -0,0 +1,83 @@ +local webshot = {} + +local helpers = require('OAuth.helpers') +local utilities = require('miku.utilities') +local https = require('ssl.https') +local ltn12 = require('ltn12') +local json = require('dkjson') +local bindings = require('miku.bindings') + +local base = 'https://screenshotmachine.com/' +local url = base .. 'processor.php' + +function webshot:init(config) + webshot.triggers = { + "^/webshot ([T|t|S|s|E|e|N|n|M|m|L|l|X|x|F|f]) ([%w-_%.%?%.:,/%+=&#!]+)$", + "^/scrot ([T|t|S|s|E|e|N|n|M|m|L|l|X|x|F|f]) ([%w-_%.%?%.:,/%+=&#!]+)$", + "^/webshot ([%w-_%.%?%.:,/%+=&#!]+)$", + "^/scrot ([%w-_%.%?%.:,/%+=&#!]+)$" + } + webshot.doc = [[* +]]..config.cmd_pat..[[scrot* __: Fertigt Bild mit Größe 1024x768 (X) an +*]]..config.cmd_pat..[[scrot* _[T|S|E|N|M|L|X|F]_ __: Fertigt Bild mit bestimmter Größe an (T = tiny, F = full)]] +end + +webshot.command = 'scrot [T|S|E|N|M|L|X|F] ' + +function webshot:get_webshot_url(param, size) + local response_body = {} + local request_constructor = { + url = url, + method = "GET", + sink = ltn12.sink.table(response_body), + headers = { + referer = base, + dnt = "1", + origin = base, + ["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.41 Safari/537.36" + }, + redirect = false + } + + local arguments = { + urlparam = param, + size = size, + cacheLimit = "0" + } + + request_constructor.url = url .. "?" .. helpers.url_encode_arguments(arguments) + + local ok, response_code, response_headers, response_status_line = https.request(request_constructor) + if not ok or response_code ~= 200 then + return nil + end + + local response = table.concat(response_body) + return string.match(response, "href='(.-)'") +end + +function webshot:action(msg, config, matches) + if not matches[2] then + webshot_url = matches[1] + size = "X" + else + webshot_url = matches[2] + size = string.upper(matches[1]) + end + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local find = webshot:get_webshot_url(webshot_url, size) + if find then + local imgurl = base .. find + local file = download_to_file(imgurl) + if size == "F" then + utilities.send_document(self, msg.chat.id, file, nil, msg.message_id) + return + else + utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) + end + else + utilities.send_reply(self, msg, config.errors.connection) + end +end + +return webshot \ No newline at end of file diff --git a/miku/plugins/wiimmfi.lua b/miku/plugins/wiimmfi.lua new file mode 100644 index 0000000..617a647 --- /dev/null +++ b/miku/plugins/wiimmfi.lua @@ -0,0 +1,58 @@ +local wiimmfi = {} + +local http = require('socket.http') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +function wiimmfi:init(config) + wiimmfi.triggers = { + "^/(mkw)$", + "^/wiimmfi$", + "^/wfc$" + } + wiimmfi.doc = [[* +]]..config.cmd_pat..[[wfc*: Zeigt alle Wiimmfi-Spieler an +*]]..config.cmd_pat..[[mkw*: Zeigt alle Mario-Kart-Wii-Spieler an]] +end + +wiimmfi.command = 'wfc, /mkw' + +function wiimmfi:getplayer(game) + local url = 'http://wiimmfi.de/game' + local res,code = http.request(url) + if code ~= 200 then return "Fehler beim Abrufen von wiimmfi.de" end + if game == 'mkw' then + local players = string.match(res, "(.-)") + if players == nil then players = 0 end + text = 'Es spielen gerade '..players..' Spieler Mario Kart Wii' + else + local players = string.match(res, "(.-).-", ": ") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "Wii", "") + local players = string.gsub(players, "WiiWare", "") + local players = string.gsub(players, "NDS", "") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "", "") + local players = string.gsub(players, "", "") + if players == nil then players = 'Momentan spielt keiner auf Wiimmfi :(' end + text = players + end + return text +end + +function wiimmfi:action(msg, config, matches) + if matches[1] == "mkw" then + utilities.send_reply(self, msg, wiimmfi:getplayer('mkw')) + return + else + utilities.send_reply(self, msg, wiimmfi:getplayer()) + return + end +end + +return wiimmfi \ No newline at end of file diff --git a/miku/plugins/wikia.lua b/miku/plugins/wikia.lua new file mode 100644 index 0000000..2ce36af --- /dev/null +++ b/miku/plugins/wikia.lua @@ -0,0 +1,45 @@ +local wikia = {} + +local http = require('socket.http') +local json = require('dkjson') +local utilities = require('miku.utilities') +local bindings = require('miku.bindings') + +wikia.triggers = { + "https?://(.+).wikia.com/wiki/(.+)" +} + +local BASE_URL = '.wikia.com/api/v1/Articles/Details?abstract=400' + +function send_wikia_article(wikia, article) + local url = 'http://'..wikia..BASE_URL..'&titles='..article + local res,code = http.request(url) + if code ~= 200 then return "HTTP-FEHLER" end + if string.match(res, "Not a valid Wikia") then return 'Dieses Wikia existiert nicht!' end + local data = json.decode(res) + + local keyset={} + local n=0 + for id,_ in pairs(data.items) do + n=n+1 + keyset[n]=id + end + + local id = keyset[1] + if not id then return 'Diese Seite existiert nicht!' end + + local title = data.items[id].title + local abstract = data.items[id].abstract + local article_url = data.basepath..data.items[id].url + + local text = '*'..title..'*:\n'..abstract..' [Weiterlesen]('..article_url..')' + return text +end + +function wikia:action(msg, config, matches) + local wikia = matches[1] + local article = matches[2] + utilities.send_reply(self, msg, send_wikia_article(wikia, article), true) +end + +return wikia \ No newline at end of file diff --git a/miku/plugins/wikipedia.lua b/miku/plugins/wikipedia.lua new file mode 100644 index 0000000..6eae6c5 --- /dev/null +++ b/miku/plugins/wikipedia.lua @@ -0,0 +1,225 @@ +local wikipedia = {} + +local https = require('ssl.https') +local URL = require('socket.url') +local JSON = require('dkjson') +local socket = require('socket') +local utilities = require('miku.utilities') + +wikipedia.command = 'wiki ' + +function wikipedia:init(config) + wikipedia.triggers = { + "^/[Ww]iki(%w+) (search) (.+)$", + "^/[Ww]iki (search) ?(.*)$", + "^/[Ww]iki(%w+) (.+)$", + "^/[Ww]iki ?(.*)$", + "(%w+).wikipedia.org/wiki/(.+)" + } + wikipedia.inline_triggers = { + "^wiki(%w+) (.+)", + "^wiki (.+)" + } + wikipedia.doc = [[* +]]..config.cmd_pat..[[wiki* __: Gibt Wikipedia-Artikel aus +Alias: ]]..config.cmd_pat..[[wikipedia]] +end + +local decodetext +do + local char, gsub, tonumber = string.char, string.gsub, tonumber + local function _(hex) return char(tonumber(hex, 16)) end + + function decodetext(s) + s = gsub(s, '%%(%x%x)', _) + return s + end +end + +local server = { + -- http://meta.wikimedia.org/wiki/List_of_Wikipedias + wiki_server = "https://%s.wikipedia.org", + wiki_path = "/w/api.php", + wiki_load_params = { + action = "query", + prop = "extracts", + format = "json", + exchars = 350, + exsectionformat = "plain", + explaintext = "", + redirects = "" + }, + wiki_search_params = { + action = "query", + list = "search", + srlimit = 20, + format = "json", + }, + default_lang = "de", +} + +function wikipedia:getWikiServer(lang) + return string.format(server.wiki_server, lang or server.default_lang) +end + +--[[ +-- return decoded JSON table from Wikipedia +--]] +function wikipedia:loadPage(text, lang, intro, plain, is_search) + local request, sink = {}, {} + local query = "" + local parsed + + if is_search then + for k,v in pairs(server.wiki_search_params) do + query = query .. k .. '=' .. v .. '&' + end + parsed = URL.parse(wikipedia:getWikiServer(lang)) + parsed.path = server.wiki_path + parsed.query = query .. "srsearch=" .. URL.escape(text) + else + server.wiki_load_params.explaintext = plain and "" or nil + for k,v in pairs(server.wiki_load_params) do + query = query .. k .. '=' .. v .. '&' + end + parsed = URL.parse(wikipedia:getWikiServer(lang)) + parsed.path = server.wiki_path + parsed.query = query .. "titles=" .. URL.escape(text) + end + + -- HTTP request + request['url'] = URL.build(parsed) + request['method'] = 'GET' + request['sink'] = ltn12.sink.table(sink) + + local httpRequest = parsed.scheme == 'http' and http.request or https.request + local code, headers, status = socket.skip(1, httpRequest(request)) + + if not headers or not sink then + return nil + end + + local content = table.concat(sink) + if content ~= "" then + local ok, result = pcall(JSON.decode, content) + if ok and result then + return result + else + return nil + end + else + return nil + end +end + +-- extract intro passage in wiki page +function wikipedia:wikintro(text, lang) + local text = decodetext(text) + local result = self:loadPage(text, lang, true, true) + + if result and result.query then + + local query = result.query + if query and query.normalized then + text = query.normalized[1].to or text + end + + local page = query.pages[next(query.pages)] + + if page and page.extract then + local lang = lang or "de" + local title = page.title + local title_enc = URL.escape(title) + return '*'..title.."*:\n"..utilities.md_escape(page.extract), '{"inline_keyboard":[[{"text":"Artikel aufrufen","url":"https://'..lang..'.wikipedia.org/wiki/'..title_enc..'"}]]}' + else + local text = text.." nicht gefunden" + return text + end + else + return "Ein Fehler ist aufgetreten." + end +end + +-- search for term in wiki +function wikipedia:wikisearch(text, lang) + local result = wiki:loadPage(text, lang, true, true, true) + + if result and result.query then + local titles = "" + for i,item in pairs(result.query.search) do + titles = titles .. "\n" .. item["title"] + end + titles = titles ~= "" and titles or "Keine Ergebnisse gefunden" + return titles + else + return "Ein Fehler ist aufgetreten." + end + +end + +function wikipedia:snip_snippet(snippet) + local snippet = snippet:gsub("", "") + local snippet = snippet:gsub("", "") + return snippet +end + +function wikipedia:inline_callback(inline_query, config, matches) + if matches[2] then + lang = matches[1] + query = matches[2] + else + lang = 'de' + query = matches[1] + end + local url = 'https://'..lang..'.wikipedia.org/w/api.php?action=query&list=search&srsearch='..URL.escape(query)..'&format=json&prop=extracts&srprop=snippet' + local res, code = https.request(url) + if code ~= 200 then return end + local data = JSON.decode(res).query + + if data.searchinfo.totalhits == 0 then return end + + local results = '[' + for num in pairs(data.search) do + local title = data.search[num].title + results = results..'{"type":"article","id":"'..math.random(100000000000000000)..'","title":"'..title..'","description":"'..wikipedia:snip_snippet(data.search[num].snippet)..'","url":"https://'..lang..'.wikipedia.org/wiki/'..URL.escape(title)..'","thumb_url":"https://anditest.perseus.uberspace.de/inlineQuerys/wiki/logo.jpg","thumb_width":95,"thumb_height":86,"input_message_content":{"message_text":"https://'..lang..'.wikipedia.org/wiki/'..URL.escape(title)..'","disable_web_page_preview":true}}' + if num < #data.search then + results = results..',' + end + end + local results = results..']' + utilities.answer_inline_query(self, inline_query, results, 3600) +end + +function wikipedia:action(msg, config, matches) + local search, term, lang + if matches[1] == "search" then + search = true + term = matches[2] + lang = nil + elseif matches[2] == "search" then + search = true + term = matches[3] + lang = matches[1] + else + term = matches[2] + lang = matches[1] + end + if not term then + term = lang + lang = nil + end + if term == "" then + utilities.send_reply(msg, self, wikipedia.doc) + return + end + + local result + if search then + result = wikipedia:wikisearch(term, lang) + else + result, keyboard = wikipedia:wikintro(term, lang) + end + utilities.send_reply(self, msg, result, true, keyboard) +end + +return wikipedia diff --git a/miku/plugins/xkcd.lua b/miku/plugins/xkcd.lua new file mode 100644 index 0000000..c310b7e --- /dev/null +++ b/miku/plugins/xkcd.lua @@ -0,0 +1,37 @@ +local xkcd = {} + +local http = require('socket.http') +local json = require('dkjson') +local utilities = require('miku.utilities') + +xkcd.command = 'xkcd [i]' + +function xkcd:init(config) + xkcd.triggers = { + "^/xkcd (%d+)", + "xkcd.com/(%d+)" + } + xkcd.doc = [[* +]]..config.cmd_pat..[[xkcd* _[i]_: Gibt diesen XKCD-Comic aus]] +end + +function xkcd:get_xkcd(id) + local res,code = http.request("http://xkcd.com/"..id.."/info.0.json") + if code ~= 200 then return nil end + local data = json.decode(res) + local link_image = data.img + if link_image:sub(0,2) == '//' then + link_image = link_image:sub(3,-1) + end + return link_image, data.title, data.alt +end + +function xkcd:action(msg, config) + local url, title, alt = xkcd:get_xkcd(matches[1]) + if not url then utilities.send_reply(self, msg, config.errors.connection) return end + utilities.send_typing(self, msg.chat.id, 'upload_photo') + local file = download_to_file(url) + utilities.send_photo(self, msg.chat.id, file, title..'\n'..alt, msg.message_id) +end + +return xkcd diff --git a/miku/plugins/yourls.lua b/miku/plugins/yourls.lua new file mode 100644 index 0000000..67f1358 --- /dev/null +++ b/miku/plugins/yourls.lua @@ -0,0 +1,57 @@ +local yourls = {} + +local http = require('socket.http') +local https = require('ssl.https') +local utilities = require('miku.utilities') + +function yourls:init(config) + if not cred_data.yourls_site_url then + print('Missing config value: yourls_site_url.') + print('yourls.lua will not be enabled.') + return + elseif not cred_data.yourls_signature_token then + print('Missing config value: yourls_signature_token.') + print('yourls.lua will not be enabled.') + return + end + + yourls.triggers = { + "^/yourls (https?://[%w-_%.%?%.:/%+=&]+)" + } +end + +local SITE_URL = cred_data.yourls_site_url +local signature = cred_data.yourls_signature_token +local BASE_URL = SITE_URL..'/yourls-api.php' + +function yourls:prot_url(url) + local url, h = string.gsub(url, "http://", "") + local url, hs = string.gsub(url, "https://", "") + local protocol = "http" + if hs == 1 then + protocol = "https" + end + return url, protocol +end + +function yourls:create_yourls_link(long_url, protocol) + local url = BASE_URL..'?format=simple&signature='..signature..'&action=shorturl&url='..long_url + if protocol == "http" then + link,code = http.request(url) + else + link,code = https.request(url) + end + if code ~= 200 then + link = 'Ein Fehler ist aufgetreten. '..link + end + return link +end + +function yourls:action(msg, config, matches) + local long_url = matches[1] + local baseurl, protocol = yourls:prot_url(SITE_URL) + utilities.send_reply(self, msg, yourls:create_yourls_link(long_url, protocol)) + return +end + +return yourls diff --git a/otouto/plugins/youtube.lua b/miku/plugins/youtube.lua similarity index 50% rename from otouto/plugins/youtube.lua rename to miku/plugins/youtube.lua index cc48638..2fa3e59 100644 --- a/otouto/plugins/youtube.lua +++ b/miku/plugins/youtube.lua @@ -1,9 +1,10 @@ local youtube = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local https = require('ssl.https') +local URL = require('socket.url') local JSON = require('dkjson') -local bindings = require('otouto.bindings') +local bindings = require('miku.bindings') function youtube:init(config) if not cred_data.google_apikey then @@ -14,8 +15,12 @@ function youtube:init(config) youtube.triggers = { 'youtu.be/([A-Za-z0-9-_-]+)', + 'youtube.com/embed/([A-Za-z0-9-_-]+)', 'youtube.com/watch%?v=([A-Za-z0-9-_-]+)' } + youtube.inline_triggers = { + "^yt (.+)" + } youtube.doc = [[*YouTube-Link*: Postet Infos zu Video]] end @@ -23,15 +28,6 @@ local apikey = cred_data.google_apikey local BASE_URL = 'https://www.googleapis.com/youtube/v3' -function table.contains(table, element) - for _, value in pairs(table) do - if value == element then - return true - end - end - return false -end - local makeOurDate = function(dateString) local pattern = "(%d+)%-(%d+)%-(%d+)T" local year, month, day = dateString:match(pattern) @@ -47,7 +43,7 @@ function get_yt_data (yt_code) return data end -local function convertISO8601Time(duration) +function convertISO8601Time(duration) local a = {} for part in string.gmatch(duration, "%d+") do @@ -86,8 +82,25 @@ local function convertISO8601Time(duration) return duration end +function get_yt_thumbnail(data) + if data.snippet.thumbnails.maxres then + image_url = data.snippet.thumbnails.maxres.url + elseif data.snippet.thumbnails.high then + image_url = data.snippet.thumbnails.high.url + elseif data.snippet.thumbnails.medium then + image_url = data.snippet.thumbnails.medium.url + elseif data.snippet.thumbnails.standard then + image_url = data.snippet.thumbnails.standard.url + else + image_url = data.snippet.thumbnails.default.url + end + return image_url +end + function send_youtube_data(data, msg, self, link, sendpic) local title = data.snippet.localized.title + local title = title:gsub('%*', '\\*') + local title = title:gsub('`', '\\`') -- local description = data.snippet.localized.description local uploader = data.snippet.channelTitle local upload_date = makeOurDate(data.snippet.publishedAt) @@ -125,17 +138,7 @@ function send_youtube_data(data, msg, self, link, sendpic) end if sendpic then - if data.snippet.thumbnails.maxres then - image_url = data.snippet.thumbnails.maxres.url - elseif data.snippet.thumbnails.high then - image_url = data.snippet.thumbnails.high.url - elseif data.snippet.thumbnails.medium then - image_url = data.snippet.thumbnails.medium.url - elseif data.snippet.thumbnails.standard then - image_url = data.snippet.thumbnails.standard.url - else - image_url = data.snippet.thumbnails.default.url - end + local image_url = get_yt_thumbnail(data) -- need to change text, because Telegram captions can only be 200 characters long and don't support Markdown local text = link..'\n'..title..'\n('..uploader..' am '..upload_date..', '..viewCount..'x angesehen, Länge: '..duration..')' if blocked then @@ -148,13 +151,69 @@ function send_youtube_data(data, msg, self, link, sendpic) end end -function youtube:action(msg) - if not msg.text:match('youtu.be/([A-Za-z0-9-_-]+)') and not msg.text:match('youtube.com/watch%?v=([A-Za-z0-9-_-]+)') then - return - end - local yt_code = msg.text:match('youtu.be/([A-Za-z0-9-_-]+)') - if not yt_code then yt_code = msg.text:match('youtube.com/watch%?v=([A-Za-z0-9-_-]+)') end +function youtube:inline_callback(inline_query, config, matches) + local query = matches[1] + local url = BASE_URL..'/search?part=snippet&key='..apikey..'&maxResults=10&type=video&q='..URL.escape(query)..'&fields=items(id(videoId),snippet(publishedAt,title,thumbnails,channelTitle))' + local res,code = https.request(url) + if code ~= 200 then return end + + local data = JSON.decode(res) + if not data.items[1] then return end + local video_ids = "" + -- We get all videoIds from search... + for num in pairs(data.items) do + video_ids = video_ids..data.items[num].id.videoId + if num < #data.items then + video_ids = video_ids..',' + end + end + + -- ...and do a second query to get all video infos + local url = BASE_URL..'/videos?part=snippet,statistics,contentDetails&key='..apikey..'&id='..video_ids..'&fields=items(id,snippet(publishedAt,channelTitle,localized(title,description),thumbnails),statistics(viewCount,likeCount,dislikeCount,commentCount),contentDetails(duration,regionRestriction(blocked)))' + local res,code = https.request(url) + if code ~= 200 then return end + + local video_results = JSON.decode(res) + if not video_results.items[1] then return end + + local results = '[' + for num in pairs(video_results.items) do + local video_url = 'https://www.youtube.com/watch?v='..video_results.items[num].id + local thumb_url = get_yt_thumbnail(video_results.items[num]) + local video_duration = convertISO8601Time(video_results.items[num].contentDetails.duration) + local video_title = video_results.items[num].snippet.localized.title:gsub('"', '\\"') + + if video_results.items[num].statistics.likeCount then + likeCount = ', '..comma_value(video_results.items[num].statistics.likeCount)..' Likes, ' + dislikeCount = comma_value(video_results.items[num].statistics.dislikeCount)..' Dislikes' + else + likeCount = '' + dislikeCount = '' + end + + if video_results.items[num].statistics.commentCount then + commentCount = ', '..comma_value(video_results.items[num].statistics.commentCount)..' Kommentare' + else + commentCount = '' + end + + local readable_dur = makeHumanTime(video_duration) + local viewCount = comma_value(video_results.items[num].statistics.viewCount) + local uploader = video_results.items[num].snippet.channelTitle + local description = uploader..', '..viewCount..' Views, '..readable_dur..likeCount..dislikeCount..commentCount + + results = results..'{"type":"video","id":"'..math.random(100000000000000000)..'","video_url":"'..video_url..'","mime_type":"text/html","thumb_url":"'..thumb_url..'","title":"'..video_title..'","description":"'..description..'","video_duration":'..video_duration..',"input_message_content":{"message_text":"'..video_url..'"}}' + if num < #video_results.items then + results = results..',' + end + end + local results = results..']' + utilities.answer_inline_query(self, inline_query, results, 0) +end + +function youtube:action(msg, config, matches) + local yt_code = matches[1] local data = get_yt_data(yt_code) send_youtube_data(data, msg, self) return diff --git a/otouto/plugins/youtube_channel.lua b/miku/plugins/youtube_channel.lua similarity index 97% rename from otouto/plugins/youtube_channel.lua rename to miku/plugins/youtube_channel.lua index e45d11c..0d6fcdc 100644 --- a/otouto/plugins/youtube_channel.lua +++ b/miku/plugins/youtube_channel.lua @@ -1,6 +1,6 @@ local youtube_channel = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local https = require('ssl.https') local JSON = require('dkjson') diff --git a/miku/plugins/youtube_dl.lua b/miku/plugins/youtube_dl.lua new file mode 100644 index 0000000..5b4e6a1 --- /dev/null +++ b/miku/plugins/youtube_dl.lua @@ -0,0 +1,68 @@ +local youtube_dl = {} + +local utilities = require('miku.utilities') + +function youtube_dl:init(config) + youtube_dl.triggers = { + "^/(mp4) (https?://[%w-_%.%?%.:/%+=&]+)$", + "^/(mp3) (https?://[%w-_%.%?%.:/%+=&]+)$" + } + + youtube_dl.doc = [[* +]]..config.cmd_pat..[[mp3* __: Lädt Audio von [untersützten Seiten](https://rg3.github.io/youtube-dl/supportedsites.html) +*]]..config.cmd_pat..[[mp4* __: Lädt Video von [untersützten Seiten](https://rg3.github.io/youtube-dl/supportedsites.html) +]] +end + +youtube_dl.command = 'mp3 , /mp4 ' + +function youtube_dl:convert_video(link) + local output = io.popen('youtube-dl -f mp4 --max-filesize 49m -o "/tmp/%(title)s.%(ext)s" '..link):read('*all') + print(output) + if string.match(output, '.* File is larger .*') then + return 'TOOBIG' + end + local video = string.match(output, '%[download%] Destination: /tmp/(.*).mp4') + if not video then + video = string.match(output, '%[download%] /tmp/(.*).mp4 has already been downloaded') + end + return '/tmp/'..video..'.mp4' +end + +function youtube_dl:convert_audio(link) + local output = io.popen('youtube-dl --max-filesize 49m -o "/tmp/%(title)s.%(ext)s" --extract-audio --audio-format mp3 '..link):read('*all') + print(output) + if string.match(output, '.* File is larger .*') then + return 'TOOBIG' + end + local audio = string.match(output, '%[ffmpeg%] Destination: /tmp/(.*).mp3') + return '/tmp/'..audio..'.mp3' +end + +function youtube_dl:action(msg, config) + local link = matches[2] + + if matches[1] == 'mp4' then + utilities.send_typing(self, msg.chat.id, 'upload_video') + local file = youtube_dl:convert_video(link) + if file == 'TOOBIG' then + utilities.send_reply(self, msg, 'Das Video überschreitet die Grenze von 50 MB!') + return + end + utilities.send_video(self, msg.chat.id, file, nil, msg.message_id) + return + end + + if matches[1] == 'mp3' then + utilities.send_typing(self, msg.chat.id, 'upload_audio') + local file = youtube_dl:convert_audio(link) + if file == 'TOOBIG' then + utilities.send_reply(self, msg, 'Die MP3 überschreitet die Grenze von 50 MB!') + return + end + utilities.send_audio(self, msg.chat.id, file, msg.message_id) + return + end +end + +return youtube_dl diff --git a/otouto/plugins/youtube_playlist.lua b/miku/plugins/youtube_playlist.lua similarity index 96% rename from otouto/plugins/youtube_playlist.lua rename to miku/plugins/youtube_playlist.lua index 2722dcd..6a3e723 100644 --- a/otouto/plugins/youtube_playlist.lua +++ b/miku/plugins/youtube_playlist.lua @@ -1,6 +1,6 @@ local youtube_playlist = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local https = require('ssl.https') local JSON = require('dkjson') diff --git a/otouto/plugins/youtube_search.lua b/miku/plugins/youtube_search.lua similarity index 94% rename from otouto/plugins/youtube_search.lua rename to miku/plugins/youtube_search.lua index f94a392..4963163 100644 --- a/otouto/plugins/youtube_search.lua +++ b/miku/plugins/youtube_search.lua @@ -1,8 +1,8 @@ -require("./otouto/plugins/youtube") +require("./miku/plugins/youtube") local yt_search = {} -local utilities = require('otouto.utilities') +local utilities = require('miku.utilities') local https = require('ssl.https') local URL = require('socket.url') local JSON = require('dkjson') diff --git a/miku/redis-old.lua b/miku/redis-old.lua new file mode 100644 index 0000000..6bdf76f --- /dev/null +++ b/miku/redis-old.lua @@ -0,0 +1,39 @@ +local Redis = require 'redis' local FakeRedis = require 'fakeredis' +local params = { + host = os.getenv('REDIS_HOST') or '127.0.0.1', + port = tonumber(os.getenv('REDIS_PORT') or 6379) +} +local database = os.getenv('REDIS_DB') local password = +os.getenv('REDIS_PASSWORD') -- Overwrite HGETALL Redis.commands.hgetall += Redis.command('hgetall', { + response = function(reply, command, ...) + local new_reply = { } + for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end + return new_reply + end +}) +local redis = nil -- Won't launch an error if fails local ok = +pcall(function() + redis = Redis.connect(params) end) if not ok then + local fake_func = function() + print('\27[31mCan\'t connect with Redis, install/configure +it!\27[39m') + end + fake_func() + fake = FakeRedis.new() + print('\27[31mRedis addr: '..params.host..'\27[39m') + print('\27[31mRedis port: '..params.port..'\27[39m') + redis = setmetatable({fakeredis=true}, { + __index = function(a, b) + if b ~= 'data' and fake[b] then + fake_func(b) + end + return fake[b] or fake_func + end }) else + if password then + redis:auth(password) + end + if database then + redis:select(database) + end end +return redis diff --git a/otouto/redis.lua b/miku/redis.lua similarity index 100% rename from otouto/redis.lua rename to miku/redis.lua diff --git a/otouto/utilities.lua b/miku/utilities.lua similarity index 58% rename from otouto/utilities.lua rename to miku/utilities.lua index ea74f2e..fd5a211 100644 --- a/otouto/utilities.lua +++ b/miku/utilities.lua @@ -9,76 +9,103 @@ local HTTPS = require('ssl.https') local URL = require('socket.url') local JSON = require('dkjson') local http = require('socket.http') -local https = require('ssl.https') local serpent = require("serpent") -local bindings = require('otouto.bindings') -local redis = (loadfile "./otouto/redis.lua")() -local mimetype = (loadfile "./otouto/mimetype.lua")() +local bindings = require('miku.bindings') +local redis = (loadfile "./miku/redis.lua")() +local mimetype = (loadfile "./miku/mimetype.lua")() + +HTTP.timeout = 10 -- For the sake of ease to new contributors and familiarity to old contributors, -- we'll provide a couple of aliases to real bindings here. -function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown) +function utilities:send_message(chat_id, text, disable_web_page_preview, reply_to_message_id, use_markdown, reply_markup) 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 = use_markdown and 'Markdown' or nil, + reply_markup = reply_markup } ) end -function utilities:send_reply(old_msg, text, use_markdown) +-- https://core.telegram.org/bots/api#editmessagetext +function utilities:edit_message(chat_id, message_id, text, disable_web_page_preview, use_markdown, reply_markup) + return bindings.request(self, 'editMessageText', { + chat_id = chat_id, + message_id = message_id, + text = text, + disable_web_page_preview = disable_web_page_preview, + parse_mode = use_markdown and 'Markdown' or nil, + reply_markup = reply_markup + } ) +end + +function utilities:send_reply(old_msg, text, use_markdown, reply_markup) 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 + parse_mode = use_markdown and 'Markdown' or nil, + reply_markup = reply_markup } ) end -- NOTE: Telegram currently only allows file uploads up to 50 MB -- https://core.telegram.org/bots/api#sendphoto -function utilities:send_photo(chat_id, file, text, reply_to_message_id) - local output = bindings.request(self, 'sendPhoto', { +function utilities:send_photo(chat_id, file, text, reply_to_message_id, reply_markup) + if not file then return false end + local output, error = bindings.request(self, 'sendPhoto', { chat_id = chat_id, caption = text or nil, - reply_to_message_id = reply_to_message_id + reply_to_message_id = reply_to_message_id, + reply_markup = reply_markup }, {photo = file} ) - os.remove(file) - print("Deleted: "..file) + if string.match(file, '/tmp/') then + os.remove(file) + print("Deleted: "..file) + end +vardump(error) return output end -- https://core.telegram.org/bots/api#sendaudio -function utilities:send_audio(chat_id, file, text, reply_to_message_id, duration, performer, title) +function utilities:send_audio(chat_id, file, reply_to_message_id, duration, performer, title) + if not file then return false end local output = bindings.request(self, 'sendAudio', { chat_id = chat_id, - caption = text or nil, duration = duration or nil, performer = performer or nil, title = title or nil, reply_to_message_id = reply_to_message_id }, {audio = file} ) - os.remove(file) - print("Deleted: "..file) + if string.match(file, '/tmp/') then + os.remove(file) + print("Deleted: "..file) + end return output end -- https://core.telegram.org/bots/api#senddocument -function utilities:send_document(chat_id, file, text, reply_to_message_id) +function utilities:send_document(chat_id, file, text, reply_to_message_id, reply_markup) + if not file then return false end local output = bindings.request(self, 'sendDocument', { chat_id = chat_id, caption = text or nil, - reply_to_message_id = reply_to_message_id + reply_to_message_id = reply_to_message_id, + reply_markup = reply_markup }, {document = file} ) - os.remove(file) - print("Deleted: "..file) + if string.match(file, '/tmp/') then + os.remove(file) + print("Deleted: "..file) + end return output end -- https://core.telegram.org/bots/api#sendvideo function utilities:send_video(chat_id, file, text, reply_to_message_id, duration, width, height) + if not file then return false end local output = bindings.request(self, 'sendVideo', { chat_id = chat_id, caption = text or nil, @@ -87,21 +114,26 @@ function utilities:send_video(chat_id, file, text, reply_to_message_id, duration height = height or nil, reply_to_message_id = reply_to_message_id }, {video = file} ) - os.remove(file) - print("Deleted: "..file) + if string.match(file, '/tmp/') then + os.remove(file) + print("Deleted: "..file) + end return output end -- NOTE: Voice messages are .ogg files encoded with OPUS -- https://core.telegram.org/bots/api#sendvoice function utilities:send_voice(chat_id, file, text, reply_to_message_id, duration) + if not file then return false end local output = bindings.request(self, 'sendVoice', { chat_id = chat_id, duration = duration or nil, reply_to_message_id = reply_to_message_id }, {voice = file} ) - os.remove(file) - print("Deleted: "..file) + if string.match(file, '/tmp/') then + os.remove(file) + print("Deleted: "..file) + end return output end @@ -137,6 +169,42 @@ function utilities:send_typing(chat_id, action) } ) end +-- https://core.telegram.org/bots/api#answercallbackquery +function utilities:answer_callback_query(callback, text, show_alert) + return bindings.request(self, 'answerCallbackQuery', { + callback_query_id = callback.id, + text = text, + show_alert = show_alert + } ) +end + +-- https://core.telegram.org/bots/api#getchat +function utilities:get_chat_info(chat_id) + return bindings.request(self, 'getChat', { + chat_id = chat_id + } ) +end + +-- https://core.telegram.org/bots/api#getchatadministrators +function utilities:get_chat_administrators(chat_id) + return bindings.request(self, 'getChatAdministrators', { + chat_id = chat_id + } ) +end + +-- https://core.telegram.org/bots/api#answerinlinequery +function utilities:answer_inline_query(inline_query, results, cache_time, is_personal, next_offset, switch_pm_text, switch_pm_parameter) + return bindings.request(self, 'answerInlineQuery', { + inline_query_id = inline_query.id, + results = results, + cache_time = cache_time, + is_personal = is_personal, + next_offset = next_offset, + switch_pm_text = switch_pm_text, + switch_pm_parameter = switch_pm_parameter + } ) +end + -- get the indexed word in a string function utilities.get_word(s, i) s = s or '' @@ -178,41 +246,12 @@ function utilities.utf8_len(s) return chars end - -- I swear, I copied this from PIL, not yago! :) -function utilities.trim(str) -- Trims whitespace from a string. +-- Trims whitespace from a string. +function utilities.trim(str) local s = str:gsub('^%s*(.-)%s*$', '%1') return s end -local lc_list = { --- Latin = 'Cyrillic' - ['A'] = 'А', - ['B'] = 'В', - ['C'] = 'С', - ['E'] = 'Е', - ['I'] = 'І', - ['J'] = 'Ј', - ['K'] = 'К', - ['M'] = 'М', - ['H'] = 'Н', - ['O'] = 'О', - ['P'] = 'Р', - ['S'] = 'Ѕ', - ['T'] = 'Т', - ['X'] = 'Х', - ['Y'] = 'Ү', - ['a'] = 'а', - ['c'] = 'с', - ['e'] = 'е', - ['i'] = 'і', - ['j'] = 'ј', - ['o'] = 'о', - ['s'] = 'ѕ', - ['x'] = 'х', - ['y'] = 'у', - ['!'] = 'ǃ' -} - -- Retruns true if the string is empty function string:isempty() return self == nil or self == '' @@ -250,88 +289,40 @@ function string.starts(String, Start) return Start == string.sub(String,1,string.len(Start)) end -function get_http_file_name(url, headers) - -- Eg: fooo.var - local file_name = url:match("[^%w]+([%.%w]+)$") - -- Any delimited aphanumeric on the url - file_name = file_name or url:match("[^%w]+(%w+)[^%w]+$") - -- Random name, hope content-type works - file_name = file_name or str:random(5) - - local content_type = headers["content-type"] - - local extension = nil - if content_type then - extension = mimetype.get_mime_extension(content_type) - end - - if extension then - file_name = file_name.."."..extension - end - - local disposition = headers["content-disposition"] - if disposition then - -- attachment; filename=CodeCogsEqn.png - file_name = disposition:match('filename=([^;]+)') or file_name - file_name = string.gsub(file_name, "\"", "") - end - - return file_name -end - -- Saves file to $HOME/tmp/. If file_name isn't provided, -- will get the text after the last "/" for filename -- and content-type for extension function download_to_file(url, file_name) - print("url to download: "..url) - - local respbody = {} - local options = { - url = url, - sink = ltn12.sink.table(respbody), - redirect = true - } - - -- nil, code, headers, status - local response = nil - - if string.starts(url, 'https') then - options.redirect = false - response = {HTTPS.request(options)} - else - response = {HTTP.request(options)} - end - - local code = response[2] - local headers = response[3] - local status = response[4] - - if code ~= 200 then return nil end - - file_name = file_name or get_http_file_name(url, headers) - - local file_path = "/home/akamaru/Mikubot-V2/tmp/"..file_name - print("Saved to: "..file_path) - - file = io.open(file_path, "w+") - file:write(table.concat(respbody)) - file:close() - - return file_path + print('url to download: '..url) + if not file_name then + file_name = '/tmp/' .. url:match('.+/(.-)$') or '/tmp/' .. os.time() + else + file_name = '/tmp/' .. file_name + end + local body = {} + local doer = HTTP + local do_redir = true + if url:match('^https') then + doer = HTTPS + do_redir = false + end + local _, res = doer.request{ + url = url, + sink = ltn12.sink.table(body), + redirect = do_redir + } + if res ~= 200 then return false end + local file = io.open(file_name, 'w+') + file:write(table.concat(body)) + file:close() + print('Saved to: '..file_name) + return file_name end function vardump(value) print(serpent.block(value, {comment=false})) end - -- Replaces letters with corresponding Cyrillic characters. -function utilities.latcyr(str) - for k,v in pairs(lc_list) do - str = str:gsub(k, v) - end - return str -end - -- Loads a JSON file as a table. function utilities.load_data(filename) local f = io.open(filename) @@ -395,15 +386,46 @@ end function utilities:resolve_username(input) input = input:gsub('^@', '') - for _,v in pairs(self.database.users) do - if v.username and v.username:lower() == input:lower() then - return v + for _, user in pairs(self.database.users) do + if user.username and user.username:lower() == input:lower() then + local t = {} + for key, val in pairs(user) do + t[key] = val + end + return t + end + end +end + + -- 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 @@ -462,28 +484,9 @@ function utilities:handle_exception(err, message, config) end +-- MOVED TO DOWNLOAD_TO_FILE function utilities.download_file(url, filename) - if not filename then - filename = url:match('.+/(.-)$') or os.time() - filename = '/tmp/' .. filename - end - local body = {} - local doer = HTTP - local do_redir = true - if url:match('^https') then - doer = HTTPS - do_redir = false - end - local _, res = doer.request{ - url = url, - sink = ltn12.sink.table(body), - redirect = do_redir - } - if res ~= 200 then return false end - local file = io.open(filename, 'w+') - file:write(table.concat(body)) - file:close() - return filename + return download_to_file(url, filename) end function utilities.markdown_escape(text) @@ -494,6 +497,13 @@ function utilities.markdown_escape(text) text = text:gsub('`', '\\`') return text end + +function utilities.markdown_escape_simple(text) + text = text:gsub('_', '\\_') + text = text:gsub('%*', '\\*') + text = text:gsub('`', '\\`') + return text +end utilities.md_escape = utilities.markdown_escape @@ -564,21 +574,6 @@ function utilities.pretty_float(x) end end -function utilities:create_user_entry(user) - local id = tostring(user.id) - -- Clear things that may no longer exist, or create a user entry. - if self.database.users[id] then - self.database.users[id].username = nil - self.database.users[id].last_name = nil - else - self.database.users[id] = {} - end - -- Add all the user info to the entry. - for k,v in pairs(user) do - self.database.users[id][k] = v - end -end - -- This table will store unsavory characters that are not properly displayed, -- or are just not fun to type. utilities.char = { @@ -589,8 +584,40 @@ utilities.char = { em_dash = '—' } +-- taken from http://stackoverflow.com/a/11130774/3163199 +function scandir(directory) + local i, t, popen = 0, {}, io.popen + for filename in popen('ls -a "'..directory..'"'):lines() do + i = i + 1 + t[i] = filename + end + return t +end + +-- Returns at table of lua files inside plugins +function plugins_names() + local files = {} + for k, v in pairs(scandir("miku/plugins")) do + -- Ends with .lua + if (v:match(".lua$")) then + table.insert(files, v) + end + end + return files +end + +-- Function name explains what it does. +function file_exists(name) + local f = io.open(name,"r") + if f ~= nil then + io.close(f) + return true + else + return false + end +end + -- Returns a table with matches or nil ---function match_pattern(pattern, text, lower_case) function match_pattern(pattern, text) if text then local matches = { string.match(text, pattern) } @@ -601,6 +628,15 @@ function match_pattern(pattern, text) -- nil end +function is_sudo(msg, config) + local var = false + -- Check if user id is sudoer + if config.admin == msg.from.id then + var = true + end + return var +end + function post_petition(url, arguments, headers) local url, h = string.gsub(url, "http://", "") local url, hs = string.gsub(url, "https://", "") @@ -633,7 +669,7 @@ function post_petition(url, arguments, headers) if post_prot == "http" then ok, response_code, response_headers, response_status_line = http.request(request_constructor) else - ok, response_code, response_headers, response_status_line = https.request(request_constructor) + ok, response_code, response_headers, response_status_line = HTTPS.request(request_constructor) end if not ok then @@ -725,54 +761,90 @@ function cache_data(plugin, query, data, timeout, typ) end end ---[[ -Ordered table iterator, allow to iterate on the natural order of the keys of a -table. --- http://lua-users.org/wiki/SortedIteration -]] - -function __genOrderedIndex( t ) - local orderedIndex = {} - for key in pairs(t) do - table.insert( orderedIndex, key ) - end - table.sort( orderedIndex ) - return orderedIndex +-- Caches file_id and last_modified +-- result = result of send_X() (see media.lua) +function cache_file(result, url, last_modified) + local hash = 'telegram:cache:sent_file' + if result.result.video then + file_id = result.result.video.file_id + elseif result.result.audio then + file_id = result.result.audio.file_id + elseif result.result.voice then + file_id = result.result.voice.file_id + elseif result.result.document then + file_id = result.result.document.file_id + elseif result.result.photo then + local lv = #result.result.photo + file_id = result.result.photo[lv].file_id + end + print('Caching File...') + redis:hset(hash..':'..url, 'file_id', file_id) + redis:hset(hash..':'..url, 'last_modified', last_modified) + -- Why do we set a TTL? Because Telegram recycles outgoing file_id's + -- See: https://core.telegram.org/bots/faq#can-i-count-on-file-ids-to-be-persistent + redis:expire(hash..':'..url, 5259600) -- 2 months end -function orderedNext(t, state) - -- Equivalent of the next function, but returns the keys in the alphabetic - -- order. We use a temporary ordered key table that is stored in the - -- table being iterated. +function get_last_modified_header(url) + local doer = HTTP + local do_redir = true + if url:match('^https') then + doer = HTTPS + do_redir = false + end + local _, code, header = doer.request { + method = "HEAD", + url = url, + redirect = do_redir + } + if not header then return end + if header["last-modified"] then + last_modified = header["last-modified"] + elseif header["Last-Modified"] then + last_modified = header["Last-Modified"] + end + return last_modified, code +end - key = nil - --print("orderedNext: state = "..tostring(state) ) - if state == nil then - -- the first time, generate the index - t.__orderedIndex = __genOrderedIndex( t ) - key = t.__orderedIndex[1] - else - -- fetch the next value - for i = 1,table.getn(t.__orderedIndex) do - if t.__orderedIndex[i] == state then - key = t.__orderedIndex[i+1] - end - end - end +-- only url is needed! +function get_cached_file(url, file_name, receiver, chat_action, self) + local hash = 'telegram:cache:sent_file' + local cached_file_id = redis:hget(hash..':'..url, 'file_id') + local cached_last_modified = redis:hget(hash..':'..url, 'last_modified') - if key then - return key, t[key] - end - - -- no more value to return, cleanup - t.__orderedIndex = nil + -- get last-modified header + local last_modified, code = get_last_modified_header(url) + if code ~= 200 then + if cached_file_id then + redis:del(hash..':'..url) + end return -end - -function orderedPairs(t) - -- Equivalent of the pairs() function on tables. Allows to iterate - -- in order - return orderedNext, t, nil + end + + if not last_modified then + nocache = true + else + nocache = false + end + + if receiver and chat_action and self then + utilities.send_typing(self, receiver, chat_action) + end + + if not nocache then + if last_modified == cached_last_modified then + print('File not modified and already cached') + nocache = true + file = cached_file_id + else + print('File cached, but modified or not already cached. (Re)downloading...') + file = download_to_file(url, file_name) + end + else + print('No Last-Modified header!') + file = download_to_file(url, file_name) + end + return file, last_modified, nocache end -- converts total amount of seconds (e.g. 65 seconds) to human redable time (e.g. 1:05 minutes) @@ -802,25 +874,112 @@ function is_blacklisted(msg) end return var end + +function run_bash(str) + local cmd = io.popen(str) + local result = cmd:read('*all') + cmd:close() + return result +end + +function run_sh(msg) + name = get_name(msg) + text = '' + bash = msg.text:sub(4,-1) + text = run_bash(bash) + return text +end function unescape(str) - str = string.gsub( str, '<', '<' ) - str = string.gsub( str, '>', '>' ) - str = string.gsub( str, '"', '"' ) - str = string.gsub( str, ''', "'" ) - str = string.gsub( str, "Ä", "Ä") - str = string.gsub( str, "ä", "ä") - str = string.gsub( str, "Ö", "Ö") - str = string.gsub( str, "ö", "ö") - str = string.gsub( str, "Uuml;", "Ü") - str = string.gsub( str, "ü", "ü") - str = string.gsub( str, "ß", "ß") - str = string.gsub( str, '&#(%d+);', function(n) return string.char(n) end ) - str = string.gsub( str, '&#x(%d+);', function(n) return string.char(tonumber(n,16)) end ) + -- Character encoding + str = string.gsub(str, "´", "´") + str = string.gsub(str, "•", "•") + str = string.gsub(str, ">", ">") + str = string.gsub(str, "…", "…") + str = string.gsub(str, "<", "<") + str = string.gsub(str, "—", "—") + str = string.gsub(str, "∇", "∇") + str = string.gsub(str, " ", " ") + str = string.gsub(str, "–", "–") + str = string.gsub(str, "Ψ", "ψ") + str = string.gsub(str, "ψ", "ψ") + str = string.gsub(str, """, '"') + str = string.gsub(str, "»", "»") + str = string.gsub(str, "®", "®") + str = string.gsub(str, "ß", "ß") + str = string.gsub(str, "™", "™") + str = string.gsub(str, "&", "&") + str = string.gsub(str, "'", "'") + str = string.gsub(str, """, '"') + str = string.gsub(str, "'", "'") + str = string.gsub(str, "|", "|") + str = string.gsub(str, " ", " ") + str = string.gsub(str, "®", "®") + str = string.gsub(str, "»", "»") + str = string.gsub(str, "ß", "ß") + str = string.gsub(str, "–", "–") + str = string.gsub(str, "’", "'") + str = string.gsub(str, "“", "“") + str = string.gsub(str, "”", "”") + str = string.gsub(str, "„", "„") + str = string.gsub(str, "…", "…") + str = string.gsub(str, "‹", "‹") + str = string.gsub(str, "€", "€") + str = string.gsub(str, "♪", "♪") + + -- Ä Ö Ü + str = string.gsub(str, "ä", "ä") + str = string.gsub(str, "Ä", "Ä") + str = string.gsub(str, "ä", "ä") + str = string.gsub(str, "Ä", "Ä") + str = string.gsub(str, "ö", "ö") + str = string.gsub(str, "Ö", "Ö") + str = string.gsub(str, "ö", "ö") + str = string.gsub(str, "Ö", "Ö") + str = string.gsub(str, "ü", "ü") + str = string.gsub(str, "Ü", "Ü") + str = string.gsub(str, "ü", "ü") + str = string.gsub(str, "Ü", "Ü") + --str = string.gsub( str, '&#(%d+);', function(n) return string.char(n) end ) + --str = string.gsub( str, '&#x(%d+);', function(n) return string.char(tonumber(n,16)) end ) str = string.gsub( str, '&', '&' ) -- Be sure to do this after all others return str end +function gerRating(str) + str = string.gsub(str, "de/0", "FSK0") + str = string.gsub(str, "TV%-G", "FSK0") + str = string.gsub(str, "TV%-Y$", "FSK0") + str = string.gsub(str, "G %- All Ages", "FSK0") + str = string.gsub(str, "de/6", "FSK6") + str = string.gsub(str, "TV%-Y7", "FSK6") + str = string.gsub(str, "TV%-PG", "FSK6") + str = string.gsub(str, "PG %- Children", "FSK6") + str = string.gsub(str, "de/12", "FSK12") + str = string.gsub(str, "de/16", "FSK16") + str = string.gsub(str, "TV%-14", "FSK16") + str = string.gsub(str, "PG%-13 %- Teens 13 or older", "FSK16") + str = string.gsub(str, "de/18", "FSK18") + str = string.gsub(str, "TV%-MA", "FSK18") + str = string.gsub(str, "R %- 17%+ %(violence & profanity%)", "FSK18") + str = string.gsub(str, "R%+ %- Mild Nudity", "FSK18") + str = string.gsub(str, "Rx %- Hentai", "FSK18") + return str +end + +function convertNumbers(str) + str = string.gsub(str, "^1$", "01") + str = string.gsub(str, "^2$", "02") + str = string.gsub(str, "^3$", "03") + str = string.gsub(str, "^4$", "04") + str = string.gsub(str, "^5$", "05") + str = string.gsub(str, "^6$", "06") + str = string.gsub(str, "^7$", "07") + str = string.gsub(str, "^8$", "08") + str = string.gsub(str, "^9$", "09") + return str +end + function url_encode(str) if (str) then str = string.gsub (str, "\n", "\r\n") @@ -831,4 +990,13 @@ function url_encode(str) return str end +function table.contains(table, element) + for _, value in pairs(table) do + if value == element then + return true + end + end + return false +end + return utilities diff --git a/otouto/bot.lua b/otouto/bot.lua deleted file mode 100644 index bb1aa67..0000000 --- a/otouto/bot.lua +++ /dev/null @@ -1,198 +0,0 @@ -local bot = {} - --- Requires are moved to init to allow for reloads. -local bindings -- Load Telegram bindings. -local utilities -- Load miscellaneous and cross-plugin functions. -local redis = (loadfile "./otouto/redis.lua")() - -bot.version = '2.0' - -function bot:init(config) -- The function run when the bot is started or reloaded. - - bindings = require('otouto.bindings') - utilities = require('otouto.utilities') - redis = (loadfile "./otouto/redis.lua")() - cred_data = load_cred() - - assert( - config.bot_api_key and config.bot_api_key ~= '', - 'You did not set your bot token in the config!' - ) - self.BASE_URL = 'https://api.telegram.org/bot' .. config.bot_api_key .. '/' - - -- Fetch bot information. Try until it succeeds. - repeat - print('Fetching bot information...') - self.info = bindings.getMe(self) - until self.info - self.info = self.info.result - - -- Load the "database"! ;) - if not self.database then - self.database = utilities.load_data(self.info.username..'.db') - end - - self.database.users = self.database.users or {} -- Table to cache userdata. - 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 - 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.is_started = true -- and whether or not the bot should be running. - -end - -function bot:on_msg_receive(msg, config) -- The fn run whenever a message is received. - - -- Cache user info for those involved. - utilities.create_user_entry(self, msg.from) - if msg.forward_from and msg.forward_from.id ~= msg.from.id then - utilities.create_user_entry(self, msg.forward_from) - elseif msg.reply_to_message and msg.reply_to_message.from.id ~= msg.from.id then - utilities.create_user_entry(self, msg.reply_to_message.from) - end - - if msg.date < os.time() - 5 then return end -- Do not process old messages. - - msg = utilities.enrich_message(msg) - - if msg.text:match('^'..config.cmd_pat..'start .+') then - msg.text = config.cmd_pat .. utilities.input(msg.text) - msg.text_lower = msg.text:lower() - end - - for _, plugin in ipairs(self.plugins) do - for _, trigger in pairs(plugin.triggers) do - if string.match(msg.text_lower, trigger) then - local success, result = pcall(function() - -- trying to port matches to otouto - for k, pattern in pairs(plugin.triggers) do - matches = match_pattern(pattern, msg.text) - if matches then - break; - end - end - return plugin.action(self, msg, config, matches) - end) - if not success then - -- If the plugin has an error message, send it. If it does - -- not, use the generic one specified in config. If it's set - -- to false, do nothing. - if plugin.error then - utilities.send_reply(self, msg, plugin.error) - elseif plugin.error == nil then - utilities.send_reply(self, msg, config.errors.generic, true) - end - utilities.handle_exception(self, result, msg.from.id .. ': ' .. msg.text, config) - 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. - elseif result ~= true then - return - end - end - end - end - -end - -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 } ) - if res then - for _,v in ipairs(res.result) do -- Go through every new message. - self.last_update = v.update_id - if v.message then - bot.on_msg_receive(self, v.message, config) - end - end - else - print('Connection error while fetching updates.') - end - - if self.last_cron ~= os.date('%M') then -- Run cron jobs every minute. - self.last_cron = os.date('%M') - utilities.save_data(self.info.username..'.db', self.database) -- Save the database. - for i,v in ipairs(self.plugins) do - if v.cron then -- Call each plugin's cron function, if it has one. - local result, err = pcall(function() v.cron(self, config) end) - if not result then - utilities.handle_exception(self, err, 'CRON: ' .. i, config) - end - end - end - end - - end - - -- Save the database before exiting. - utilities.save_data(self.info.username..'.db', self.database) - print('Halted.') -end - -function load_cred() - if redis:exists("telegram:credentials") == false then - -- If credentials hash doesnt exists - print ("Created new credentials hash: telegram:credentials") - create_cred() - end - return redis:hgetall("telegram:credentials") -end - --- create credentials hash with redis -function create_cred() - cred = { - bitly_access_token = "", - cloudinary_apikey = "", - cloudinary_api_secret = "", - cloudinary_public_id = "", - derpibooru_apikey = "", - fb_access_token = "", - flickr_apikey = "", - ftp_site = "", - ftp_username = "", - ftp_password = "", - gender_apikey = "", - golem_apikey = "", - google_apikey = "", - google_cse_id = "", - gitlab_private_token = "", - gitlab_project_id = "", - instagram_access_token = "", - lyricsnmusic_apikey = "", - mal_username = "", - mal_pw = "", - neutrino_userid = "", - neutrino_apikey = "", - owm_apikey = "", - page2images_restkey = "", - soundcloud_client_id = "", - tw_consumer_key = "", - tw_consumer_secret = "", - tw_access_token = "", - tw_access_token_secret = "", - x_mashape_key = "", - yandex_translate_apikey = "", - yandex_rich_content_apikey = "", - yourls_site_url = "", - yourls_signature_token = "" - } - redis:hmset("telegram:credentials", cred) - print ('saved credentials into reds hash telegram:credentials') -end - -return bot diff --git a/otouto/plugins/about.lua b/otouto/plugins/about.lua deleted file mode 100644 index 6912b6b..0000000 --- a/otouto/plugins/about.lua +++ /dev/null @@ -1,33 +0,0 @@ -local about = {} - -local bot = require('otouto.bot') -local utilities = require('otouto.utilities') - -about.command = 'about' -about.doc = '`Sendet Informationen über den Bot.`' - -about.triggers = { - '' -} - -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 .. '\nBrawlbot v2, basierend auf Otouto v'..bot.version..' von topkecleon.' - - if (msg.new_chat_participant and msg.new_chat_participant.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') then - utilities.send_message(self, msg.chat.id, output, true) - return - end - - return true - -end - -return about diff --git a/otouto/plugins/administration.lua b/otouto/plugins/administration.lua deleted file mode 100644 index 62dbb3f..0000000 --- a/otouto/plugins/administration.lua +++ /dev/null @@ -1,1414 +0,0 @@ ---[[ - administration.lua - Version 1.10 - Part of the otouto project. - © 2016 topkecleon - GNU General Public License, version 2 - - This plugin provides self-hosted, single-realm group administration. - It requires tg (http://github.com/vysheng/tg) with supergroup support. - For more documentation, read the the manual (otou.to/rtfm). - - Remember to load this before blacklist.lua. - - Important notices about updates will be here! - - 1.9 - Added flag antihammer. Groups with antihammer enabled will not be - affected by global bans. However, users who are hammer'd from an anti- - hammer group will also be banned locally. Added autobanning after (default) - 3 autokicks. Threshold onfigurable with antiflood. Autokick counters reset - within twenty-four hours. Merged antisquig action into generic. There is no - automatic migration; simply add the following to database.administration: - autokick_timer = 0 - groups[*].flags[6] = false - groups[*].autoban = 3 - groups[*].autokicks = {}b - - 1.9.1 - Returned to non-toggled promotions/bans (too many complaints!). - - 1.10 - Added /ahelp $command support. No migration required. All actions - have been reworked to be more elegant. Style has been slightly changed (no - more weak-looking, italic group names). Added some (but not many) comments. - - 1.10.1 - Bug fixes and minor improvements. :^) - - 1.10.2 - Fixed bug in antibot. Further, ranks 2+ will be automatically made - group admins when they join a group. - - 1.10.3 - /gadd now supports arguments to enable flags immediately, eg: - "/gadd 1 4 5" will add a grouo with the unlisted, antibot, and antiflood - flags. - -]]-- - -local JSON = require('dkjson') -local drua = dofile('drua-tg.lua') -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - -local administration = {} - -function administration:init(config) - -- Build the administration db if nonexistent. - if not self.database.administration then - self.database.administration = { - admins = {}, - groups = {}, - activity = {}, - autokick_timer = os.date('%d') - } - end - - self.admin_temp = { - help = {}, - flood = {} - } - - drua.PORT = config.cli_port or 4567 - - administration.flags = administration.init_flags(config.cmd_pat) - administration.init_command(self, config) - - administration.doc = '`Returns a list of administrated groups.\nUse '..config.cmd_pat..'ahelp for more administrative commands.`' - - -- In the worst case, don't send errors in reply to random messages. - administration.error = false - -end - -function administration.init_flags(cmd_pat) return { - [1] = { - name = 'unlisted', - desc = 'Removes this group from the group listing.', - short = 'This group is unlisted.', - enabled = 'This group is no longer listed in '..cmd_pat..'groups.', - disabled = 'This group is now listed in '..cmd_pat..'groups.' - }, - [2] = { - name = 'antisquig', - desc = 'Automatically removes users who post Arabic script or RTL characters.', - short = 'This group does not allow Arabic script or RTL characters.', - enabled = 'Users will now be removed automatically for posting Arabic script and/or RTL characters.', - disabled = 'Users will no longer be removed automatically for posting Arabic script and/or RTL characters.', - kicked = 'You were automatically kicked from GROUPNAME for posting Arabic script and/or RTL characters.' - }, - [3] = { - name = 'antisquig++', - desc = 'Automatically removes users whose names contain Arabic script or RTL characters.', - short = 'This group does not allow users whose names contain Arabic script or RTL characters.', - enabled = 'Users whose names contain Arabic script and/or RTL characters will now be removed automatically.', - disabled = 'Users whose names contain Arabic script and/or RTL characters will no longer be removed automatically.', - kicked = 'You were automatically kicked from GROUPNAME for having a name which contains Arabic script and/or RTL characters.' - }, - [4] = { - name = 'antibot', - desc = 'Prevents the addition of bots by non-moderators.', - short = 'This group does not allow users to add bots.', - enabled = 'Non-moderators will no longer be able to add bots.', - disabled = 'Non-moderators will now be able to add bots.' - }, - [5] = { - name = 'antiflood', - desc = 'Prevents flooding by rate-limiting messages per user.', - short = 'This group automatically removes users who flood.', - enabled = 'Users will now be removed automatically for excessive messages. Use '..cmd_pat..'antiflood to configure limits.', - disabled = 'Users will no longer be removed automatically for excessive messages.', - kicked = 'You were automatically kicked from GROUPNAME for flooding.' - }, - [6] = { - name = 'antihammer', - desc = 'Allows globally banned users to enter this group. Note that users hammered in this group will also be banned locally.', - short = 'This group does not acknowledge global bans.', - enabled = 'This group will no longer remove users for being globally banned.', - disabled = 'This group will now remove users for being globally banned.' - } -} end - -administration.antiflood = { - text = 10, - voice = 10, - audio = 10, - contact = 10, - photo = 20, - video = 20, - location = 20, - document = 20, - sticker = 30 -} - -administration.ranks = { - [0] = 'Banned', - [1] = 'Users', - [2] = 'Moderators', - [3] = 'Governors', - [4] = 'Administrators', - [5] = 'Owner' -} - -function administration:get_rank(target, chat, config) - - target = tostring(target) - chat = tostring(chat) - - -- Return 5 if the target is the bot or its owner. - if tonumber(target) == config.admin or tonumber(target) == self.info.id then - return 5 - end - - -- Return 4 if the target is an administrator. - if self.database.administration.admins[target] then - return 4 - end - - if chat and self.database.administration.groups[chat] then - -- Return 3 if the target is the governor of the chat. - if self.database.administration.groups[chat].governor == tonumber(target) then - return 3 - -- Return 2 if the target is a moderator of the chat. - elseif self.database.administration.groups[chat].mods[target] then - return 2 - -- Return 0 if the target is banned from the chat. - elseif self.database.administration.groups[chat].bans[target] then - return 0 - -- Return 1 if antihammer is enabled. - elseif self.database.administration.groups[chat].flags[6] then - return 1 - end - end - - -- Return 0 if the target is blacklisted (and antihammer is not enabled). - if self.database.blacklist[target] then - return 0 - end - - -- Return 1 if the target is a regular user. - return 1 - -end - -function administration:get_target(msg, config) - local target = utilities.user_from_message(self, msg) - if target.id then - target.rank = administration.get_rank(self, target.id_str, msg.chat.id, config) - end - return target -end - -function administration:mod_format(id) - id = tostring(id) - local user = self.database.users[id] or { first_name = 'Unknown' } - local name = utilities.build_name(user.first_name, user.last_name) - name = utilities.markdown_escape(name) - local output = '• ' .. name .. ' `[' .. id .. ']`\n' - return output -end - -function administration:get_desc(chat_id, config) - - local group = self.database.administration.groups[tostring(chat_id)] - local t = {} - if group.link then - table.insert(t, '*Welcome to* [' .. group.name .. '](' .. group.link .. ')*!*') - else - table.insert(t, '*Welcome to ' .. group.name .. '!*') - end - if group.motd then - table.insert(t, '*Message of the Day:*\n' .. group.motd) - end - if #group.rules > 0 then - local rulelist = '*Rules:*\n' - for i = 1, #group.rules do - rulelist = rulelist .. '*' .. i .. '.* ' .. group.rules[i] .. '\n' - end - table.insert(t, utilities.trim(rulelist)) - end - local flaglist = '' - for i = 1, #administration.flags do - if group.flags[i] then - flaglist = flaglist .. '• ' .. administration.flags[i].short .. '\n' - end - end - if flaglist ~= '' then - table.insert(t, '*Flags:*\n' .. utilities.trim(flaglist)) - end - if group.governor then - local gov = self.database.users[tostring(group.governor)] - local s = utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`' - table.insert(t, '*Governor:* ' .. s) - end - local modstring = '' - for k,_ in pairs(group.mods) do - modstring = modstring .. administration.mod_format(self, k) - end - if modstring ~= '' then - table.insert(t, '*Moderators:*\n' .. utilities.trim(modstring)) - end - table.insert(t, 'Run '..config.cmd_pat..'ahelp@' .. self.info.username .. ' for a list of commands.') - return table.concat(t, '\n\n') - -end - -function administration:update_desc(chat, config) - local group = self.database.administration.groups[tostring(chat)] - local desc = 'Welcome to ' .. group.name .. '!\n' - if group.motd then desc = desc .. group.motd .. '\n' end - if group.governor then - local gov = self.database.users[tostring(group.governor)] - desc = desc .. '\nGovernor: ' .. utilities.build_name(gov.first_name, gov.last_name) .. ' [' .. gov.id .. ']\n' - end - local s = '\n'..config.cmd_pat..'desc@' .. self.info.username .. ' for more information.' - desc = desc:sub(1, 250-s:len()) .. s - drua.channel_set_about(chat, desc) -end - -function administration:kick_user(chat, target, reason, config) - drua.kick_user(chat, target) - local victim = target - if self.database.users[tostring(target)] then - victim = utilities.build_name( - self.database.users[tostring(target)].first_name, - self.database.users[tostring(target)].last_name - ) .. ' [' .. victim .. ']' - end - local group = self.database.administration.groups[tostring(chat)].name - utilities.handle_exception(self, victim..' kicked from '..group, reason, config) -end - -function administration.init_command(self_, config) - administration.commands = { - - { -- generic, mostly autokicks - triggers = { '' }, - - privilege = 0, - interior = true, - - action = function(self, msg, group, config) - - local rank = administration.get_rank(self, msg.from.id, msg.chat.id, config) - local user = {} - - if rank < 2 then - - -- banned - if rank == 0 then - user.do_kick = true - user.dont_unban = true - user.reason = 'banned' - user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' - elseif group.flags[2] and ( -- antisquig - msg.text:match(utilities.char.arabic) - or msg.text:match(utilities.char.rtl_override) - or msg.text:match(utilities.char.rtl_mark) - ) then - user.do_kick = true - user.reason = 'antisquig' - user.output = administration.flags[2].kicked:gsub('GROUPNAME', msg.chat.title) - elseif group.flags[3] and ( -- antisquig++ - msg.from.name:match(utilities.char.arabic) - or msg.from.name:match(utilities.char.rtl_override) - or msg.from.name:match(utilities.char.rtl_mark) - ) then - user.do_kick = true - user.reason = 'antisquig++' - user.output = administration.flags[3].kicked:gsub('GROUPNAME', msg.chat.title) - end - - -- antiflood - if group.flags[5] then - if not group.antiflood then - group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) - end - if not self.admin_temp.flood[msg.chat.id_str] then - self.admin_temp.flood[msg.chat.id_str] = {} - end - if not self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = 0 - end - if msg.sticker then -- Thanks Brazil for discarding switches. - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.sticker - elseif msg.photo then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.photo - elseif msg.document then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.document - elseif msg.audio then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.audio - elseif msg.contact then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.contact - elseif msg.video then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.video - elseif msg.location then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.location - elseif msg.voice then - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.voice - else - self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] = self.admin_temp.flood[msg.chat.id_str][msg.from.id_str] + group.antiflood.text - end - if self.admin_temp.flood[msg.chat.id_str][msg.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[msg.chat.id_str][msg.from.id_str] = nil - end - end - - end - - local new_user = user - local new_rank = rank - - if msg.new_chat_participant then - - -- I hate typing this out. - local noob = msg.new_chat_participant - - -- We'll make a new table for the new guy, unless he's also - -- the original guy. - if msg.new_chat_participant.id ~= msg.from.id then - new_user = {} - new_rank = administration.get_rank(self,noob.id, msg.chat.id, config) - end - - if new_rank == 0 then - new_user.do_kick = true - new_user.dont_unban = true - new_user.reason = 'banned' - new_user.output = 'Sorry, you are banned from ' .. msg.chat.title .. '.' - elseif new_rank == 1 then - if group.flags[3] and ( -- antisquig++ - noob.name:match(utilities.char.arabic) - or noob.name:match(utilities.char.rtl_override) - or noob.name:match(utilities.char.rtl_mark) - ) then - new_user.do_kick = true - new_user.reason = 'antisquig++' - new_user.output = administration.flags[3].kicked:gsub('GROUPNAME', msg.chat.title) - elseif ( -- antibot - group.flags[4] - and noob.username - and noob.username:match('bot$') - and rank < 2 - ) then - new_user.do_kick = true - new_user.reason = 'antibot' - end - else - -- Make the new user a group admin if he's a mod or higher. - if msg.chat.type == 'supergroup' then - drua.channel_set_admin(msg.chat.id, msg.new_chat_participant.id, 2) - end - end - - elseif msg.new_chat_title then - if rank < 3 then - drua.rename_chat(msg.chat.id, group.name) - else - group.name = msg.new_chat_title - if group.grouptype == 'supergroup' then - administration.update_desc(self, msg.chat.id, config) - end - end - elseif msg.new_chat_photo then - if group.grouptype == 'group' then - if rank < 3 then - drua.set_photo(msg.chat.id, group.photo) - else - group.photo = drua.get_photo(msg.chat.id) - end - else - group.photo = drua.get_photo(msg.chat.id) - end - elseif msg.delete_chat_photo then - if group.grouptype == 'group' then - if rank < 3 then - drua.set_photo(msg.chat.id, group.photo) - else - group.photo = nil - end - else - group.photo = nil - end - end - - if new_user ~= user and new_user.do_kick then - administration.kick_user(self, msg.chat.id, msg.new_chat_participant.id, new_user.reason, config) - if new_user.output then - utilities.send_message(self, msg.new_chat_participant.id, new_user.output) - end - if not new_user.dont_unban and msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) - end - end - - if group.flags[5] and user.do_kick and not user.dont_unban then - if group.autokicks[msg.from.id_str] then - group.autokicks[msg.from.id_str] = group.autokicks[msg.from.id_str] + 1 - else - group.autokicks[msg.from.id_str] = 1 - end - if group.autokicks[msg.from.id_str] >= group.autoban then - group.autokicks[msg.from.id_str] = 0 - group.bans[msg.from.id_str] = true - user.dont_unban = true - user.reason = 'antiflood autoban: ' .. user.reason - user.output = user.output .. '\nYou have been banned for being autokicked too many times.' - end - end - - if user.do_kick then - administration.kick_user(self, msg.chat.id, msg.from.id, user.reason, config) - if user.output then - utilities.send_message(self, msg.from.id, user.output) - end - if not user.dont_unban and msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) - end - end - - if msg.new_chat_participant and not new_user.do_kick then - local output = administration.get_desc(self, msg.chat.id, config) - utilities.send_message(self, msg.new_chat_participant.id, output, true, nil, true) - end - - -- Last active time for group listing. - if msg.text:len() > 0 then - for i,v in pairs(self.database.administration.activity) do - if v == msg.chat.id_str then - table.remove(self.database.administration.activity, i) - table.insert(self.database.administration.activity, 1, msg.chat.id_str) - end - end - end - - return true - - end - }, - - { -- /groups - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('groups').table, - - command = 'groups', - privilege = 1, - interior = false, - doc = 'Returns a list of administrated groups.', - - action = function(self, msg, group, config) - local output = '' - for _,v in ipairs(self.database.administration.activity) do - local group = self.database.administration.groups[v] - if not group.flags[1] then -- no unlisted groups - if group.link then - output = output .. '• [' .. utilities.md_escape(group.name) .. '](' .. group.link .. ')\n' - else - output = output .. '• ' .. group.name .. '\n' - end - end - end - if output == '' then - output = 'There are currently no listed groups.' - else - output = '*Groups:*\n' .. output - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /ahelp - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('ahelp', true).table, - - command = 'ahelp \\[command]', - privilege = 1, - interior = false, - doc = 'Returns a list of realm-related commands for your rank (in a private message), or command-specific help.', - - action = function(self, msg, group, config) - local rank = administration.get_rank(self, msg.from.id, msg.chat.id, config) - local input = utilities.get_word(msg.text_lower, 2) - if input then - input = input:gsub('^'..config.cmd_pat..'', '') - local doc - for _,action in ipairs(administration.commands) do - if action.keyword == input then - doc = ''..config.cmd_pat..'' .. action.command:gsub('\\','') .. '\n' .. action.doc - break - end - end - if doc then - local output = '*Help for* _' .. input .. '_ :\n```\n' .. doc .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - else - local output = 'Sorry, there is no help for that command.\n'..config.cmd_pat..'ahelp@'..self.info.username - utilities.send_reply(self, msg, output) - end - else - local output = '*Commands for ' .. administration.ranks[rank] .. ':*\n' - for i = 1, rank do - for _, val in ipairs(self.admin_temp.help[i]) do - output = output .. '• ' .. config.cmd_pat .. val .. '\n' - end - end - output = output .. 'Arguments: \\[optional]' - if utilities.send_message(self, msg.from.id, output, true, nil, true) then - if msg.from.id ~= msg.chat.id then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - else - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - end - end - }, - - { -- /ops - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('ops'):t('oplist').table, - - command = 'ops', - privilege = 1, - interior = true, - doc = 'Returns a list of moderators and the governor for the group.', - - action = function(self, msg, group, config) - local modstring = '' - for k,_ in pairs(group.mods) do - modstring = modstring .. administration.mod_format(self, k) - end - if modstring ~= '' then - modstring = '*Moderators for ' .. msg.chat.title .. ':*\n' .. modstring - end - local govstring = '' - if group.governor then - local gov = self.database.users[tostring(group.governor)] - govstring = '*Governor:* ' .. utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`' - end - local output = utilities.trim(modstring) ..'\n\n' .. utilities.trim(govstring) - if output == '\n\n' then - output = 'There are currently no moderators for this group.' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - - }, - - { -- /desc - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('desc'):t('description').table, - - command = 'description', - privilege = 1, - interior = true, - doc = 'Returns a description of the group (in a private message), including its motd, rules, flags, governor, and moderators.', - - action = function(self, msg, group, config) - local output = administration.get_desc(self, msg.chat.id, config) - if utilities.send_message(self, msg.from.id, output, true, nil, true) then - if msg.from.id ~= msg.chat.id then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - else - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - end - }, - - { -- /rules - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('rules?', true).table, - - command = 'rules \\[i]', - privilege = 1, - interior = true, - doc = 'Returns the group\'s list of rules, or a specific rule.', - - action = function(self, msg, group, config) - local output - local input = utilities.get_word(msg.text_lower, 2) - input = tonumber(input) - if #group.rules > 0 then - if input and group.rules[input] then - output = '*' .. input .. '.* ' .. group.rules[input] - else - output = '*Rules for ' .. msg.chat.title .. ':*\n' - for i,v in ipairs(group.rules) do - output = output .. '*' .. i .. '.* ' .. v .. '\n' - end - end - else - output = 'No rules have been set for ' .. msg.chat.title .. '.' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /motd - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('motd').table, - - command = 'motd', - privilege = 1, - interior = true, - doc = 'Returns the group\'s message of the day.', - - action = function(self, msg, group, config) - local output = 'No MOTD has been set for ' .. msg.chat.title .. '.' - if group.motd then - output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. group.motd - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /link - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('link').table, - - command = 'link', - privilege = 1, - interior = true, - doc = 'Returns the group\'s link.', - - action = function(self, msg, group, config) - local output = 'No link has been set for ' .. msg.chat.title .. '.' - if group.link then - output = '[' .. msg.chat.title .. '](' .. group.link .. ')' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /kickme - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('leave'):t('kickme').table, - - command = 'kickme', - privilege = 1, - interior = true, - doc = 'Removes the user from the group.', - - action = function(self, msg, group, config) - if administration.get_rank(self, msg.from.id, nil, config) == 5 then - utilities.send_reply(self, msg, 'I can\'t let you do that, '..msg.from.name..'.') - else - administration.kick_user(self, msg.chat.id, msg.from.id, 'kickme', config) - utilities.send_message(self, msg.chat.id, 'Goodbye, ' .. msg.from.name .. '!', true) - if msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = msg.from.id } ) - end - end - end - }, - - { -- /kick - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('kick', true).table, - - command = 'kick ', - privilege = 2, - interior = true, - doc = 'Removes a user from the group. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - elseif target.rank > 1 then - utilities.send_reply(self, msg, target.name .. ' is too privileged to be kicked.') - else - administration.kick_user(self, msg.chat.id, target.id, 'kicked by ' .. msg.from.name, config) - utilities.send_message(self, msg.chat.id, target.name .. ' has been kicked.') - if msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) - end - end - end - }, - - { -- /ban - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('ban', true).table, - - command = 'ban ', - privilege = 2, - interior = true, - doc = 'Bans a user from the group. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - elseif target.rank > 1 then - utilities.send_reply(self, msg, target.name .. ' is too privileged to be banned.') - elseif group.bans[target.id_str] then - utilities.send_reply(self, msg, target.name .. ' is already banned.') - else - administration.kick_user(self, msg.chat.id, target.id, 'banned by '..msg.from.name, config) - utilities.send_reply(self, msg, target.name .. ' has been banned.') - group.bans[target.id_str] = true - end - end - }, - - { -- /unban - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('unban', true).table, - - command = 'unban ', - privilege = 2, - interior = true, - doc = 'Unbans a user from the group. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - else - if not group.bans[target.id_str] then - utilities.send_reply(self, msg, target.name .. ' is not banned.') - else - group.bans[target.id_str] = nil - utilities.send_reply(self, msg, target.name .. ' has been unbanned.') - end - if msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) - end - end - end - }, - - { -- /setrules - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('setrules', true).table, - - command = 'setrules ', - privilege = 3, - interior = true, - doc = 'Sets the group\'s rules. Rules will be automatically numbered. Separate rules with a new line. Markdown is supported. Pass "--" to delete the rules.', - - action = function(self, msg, group, config) - local input = msg.text:match('^'..config.cmd_pat..'setrules[@'..self.info.username..']*(.+)') - if input == ' --' or input == ' ' .. utilities.char.em_dash then - group.rules = {} - utilities.send_reply(self, msg, 'The rules have been cleared.') - elseif input then - group.rules = {} - input = utilities.trim(input) .. '\n' - local output = '*Rules for ' .. msg.chat.title .. ':*\n' - local i = 1 - for l in input:gmatch('(.-)\n') do - output = output .. '*' .. i .. '.* ' .. l .. '\n' - i = i + 1 - table.insert(group.rules, utilities.trim(l)) - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - else - utilities.send_reply(self, msg, 'Please specify the new rules.') - end - end - }, - - { -- /changerule - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('changerule', true).table, - - command = 'changerule ', - privilege = 3, - interior = true, - doc = 'Changes a single rule. Pass "--" to delete the rule. If i is a number for which there is no rule, adds a rule by the next incremented number.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - local output = 'usage: `'..config.cmd_pat..'changerule `' - if input then - local rule_num = tonumber(input:match('^%d+')) - local new_rule = utilities.input(input) - if not rule_num then - output = 'Please specify which rule you want to change.' - elseif not new_rule then - output = 'Please specify the new rule.' - elseif new_rule == '--' or new_rule == utilities.char.em_dash then - if group.rules[rule_num] then - table.remove(group.rules, rule_num) - output = 'That rule has been deleted.' - else - output = 'There is no rule with that number.' - end - else - if not group.rules[rule_num] then - rule_num = #group.rules + 1 - end - group.rules[rule_num] = new_rule - output = '*' .. rule_num .. '*. ' .. new_rule - end - end - utilities.send_reply(self, msg, output, true) - end - }, - - { -- /setmotd - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('setmotd', true).table, - - command = 'setmotd ', - privilege = 2, - interior = true, - doc = 'Sets the group\'s message of the day. Markdown is supported. Pass "--" to delete the message.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - if not input and msg.reply_to_message and msg.reply_to_message.text:len() > 0 then - input = msg.reply_to_message.text - end - if input then - if input == '--' or input == utilities.char.em_dash then - group.motd = nil - utilities.send_reply(self, msg, 'The MOTD has been cleared.') - else - input = utilities.trim(input) - group.motd = input - local output = '*MOTD for ' .. msg.chat.title .. ':*\n' .. input - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - if group.grouptype == 'supergroup' then - administration.update_desc(self, msg.chat.id, config) - end - else - utilities.send_reply(self, msg, 'Please specify the new message of the day.') - end - end - }, - - { -- /setlink - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('setlink', true).table, - - command = 'setlink ', - privilege = 3, - interior = true, - doc = 'Sets the group\'s join link. Pass "--" to regenerate the link.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - if input == '--' or input == utilities.char.em_dash then - group.link = drua.export_link(msg.chat.id) - utilities.send_reply(self, msg, 'The link has been regenerated.') - elseif input then - group.link = input - local output = '[' .. msg.chat.title .. '](' .. input .. ')' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - else - utilities.send_reply(self, msg, 'Please specify the new link.') - end - end - }, - - { -- /alist - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('alist').table, - - command = 'alist', - privilege = 3, - interior = true, - doc = 'Returns a list of administrators. Owner is denoted with a star character.', - - action = function(self, msg, config) - local output = '*Administrators:*\n' - output = output .. administration.mod_format(self, config.admin):gsub('\n', ' ★\n') - for id,_ in pairs(self.database.administration.admins) do - output = output .. administration.mod_format(self, id) - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /flags - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('flags?', true).table, - - command = 'flag \\[i]', - privilege = 3, - interior = true, - doc = 'Returns a list of flags or toggles the specified flag.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - if input then - input = utilities.get_word(input, 1) - input = tonumber(input) - if not input or not administration.flags[input] then - input = false - end - end - if not input then - local output = '*Flags for ' .. msg.chat.title .. ':*\n' - for i,v in ipairs(administration.flags) do - local status = group.flags[i] or false - output = output .. '`[' .. i .. ']` *' .. v.name .. '*` = ' .. tostring(status) .. '`\n• ' .. v.desc .. '\n' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - elseif group.flags[input] == true then - group.flags[input] = false - utilities.send_reply(self, msg, administration.flags[input].disabled) - else - group.flags[input] = true - utilities.send_reply(self, msg, administration.flags[input].enabled) - end - end - }, - - { -- /antiflood - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('antiflood', true).table, - - command = 'antiflood \\[ ]', - privilege = 3, - interior = true, - doc = 'Returns a list of antiflood values or sets one.', - - action = function(self, msg, group, config) - if not group.flags[5] then - utilities.send_message(self, msg.chat.id, 'antiflood is not enabled. Use `'..config.cmd_pat..'flag 5` to enable it.', true, nil, true) - else - if not group.antiflood then - group.antiflood = JSON.decode(JSON.encode(administration.antiflood)) - end - local input = utilities.input(msg.text_lower) - local output - if input then - local key, val = input:match('(%a+) (%d+)') - if not key or not val or not tonumber(val) then - output = 'Not a valid message type or number.' - elseif key == 'autoban' then - group.autoban = tonumber(val) - output = 'Users will now be autobanned after *' .. val .. '* autokicks.' - else - group.antiflood[key] = tonumber(val) - output = '*' .. key:gsub('^%l', string.upper) .. '* messages are now worth *' .. val .. '* points.' - end - else - output = 'usage: `'..config.cmd_pat..'antiflood `\nexample: `'..config.cmd_pat..'antiflood text 5`\nUse this command to configure the point values for each message type. When a user reaches 100 points, he is kicked. The points are reset each minute. The current values are:\n' - for k,v in pairs(group.antiflood) do - output = output .. '*'..k..':* `'..v..'`\n' - end - output = output .. 'Users will be banned automatically after *' .. group.autoban .. '* autokicks. Configure this with the *autoban* keyword.' - end - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - end - end - }, - - { -- /mod - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('mod', true).table, - - command = 'mod ', - privilege = 3, - interior = true, - doc = 'Promotes a user to a moderator. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - else - if target.rank > 1 then - utilities.send_reply(self, msg, target.name .. ' is already a moderator or greater.') - else - group.bans[target.id_str] = nil - group.mods[target.id_str] = true - utilities.send_reply(self, msg, target.name .. ' is now a moderator.') - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 2) - end - end - end - }, - - { -- /demod - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('demod', true).table, - - command = 'demod ', - privilege = 3, - interior = true, - doc = 'Demotes a moderator to a user. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - else - if not group.mods[target.id_str] then - utilities.send_reply(self, msg, target.name .. ' is not a moderator.') - else - group.mods[target.id_str] = nil - utilities.send_reply(self, msg, target.name .. ' is no longer a moderator.') - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 0) - end - end - end - }, - - { -- /gov - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('gov', true).table, - - command = 'gov ', - privilege = 4, - interior = true, - doc = 'Promotes a user to the governor. The current governor will be replaced. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - else - if group.governor == target.id then - utilities.send_reply(self, msg, target.name .. ' is already the governor.') - else - group.bans[target.id_str] = nil - group.mods[target.id_str] = nil - group.governor = target.id - utilities.send_reply(self, msg, target.name .. ' is the new governor.') - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 2) - administration.update_desc(self, msg.chat.id, config) - end - end - end - }, - - { -- /degov - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('degov', true).table, - - command = 'degov ', - privilege = 4, - interior = true, - doc = 'Demotes the governor to a user. The administrator will become the new governor. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - else - if group.governor ~= target.id then - utilities.send_reply(self, msg, target.name .. ' is not the governor.') - else - group.governor = msg.from.id - utilities.send_reply(self, msg, target.name .. ' is no longer the governor.') - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 0) - administration.update_desc(self, msg.chat.id, config) - end - end - end - }, - - { -- /hammer - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('hammer', true).table, - - command = 'hammer ', - privilege = 4, - interior = false, - doc = 'Bans a user from all groups. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - elseif target.rank > 3 then - utilities.send_reply(self, msg, target.name .. ' is too privileged to be globally banned.') - elseif self.database.blacklist[target.id_str] then - utilities.send_reply(self, msg, target.name .. ' is already globally banned.') - else - administration.kick_user(self, msg.chat.id, target.id, 'hammered by '..msg.from.name, config) - self.database.blacklist[target.id_str] = true - for k,v in pairs(self.database.administration.groups) do - if not v.flags[6] then - v.mods[target.id_str] = nil - drua.kick_user(k, target.id) - end - end - local output = target.name .. ' has been globally banned.' - if group.flags[6] == true then - group.mods[target.id_str] = nil - group.bans[target.id_str] = true - output = target.name .. ' has been globally and locally banned.' - end - utilities.send_reply(self, msg, output) - end - end - }, - - { -- /unhammer - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('unhammer', true).table, - - command = 'unhammer ', - privilege = 4, - interior = false, - doc = 'Removes a global ban. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - elseif not self.database.blacklist[target.id_str] then - utilities.send_reply(self, msg, target.name .. ' is not globally banned.') - else - self.database.blacklist[target.id_str] = nil - utilities.send_reply(self, msg, target.name .. ' has been globally unbanned.') - end - if msg.chat.type == 'supergroup' then - bindings.unbanChatMember(self, { chat_id = msg.chat.id, user_id = target.id } ) - end - end - }, - - { -- /admin - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('admin', true).table, - - command = 'admin ', - privilege = 5, - interior = false, - doc = 'Promotes a user to an administrator. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - elseif target.rank >= 4 then - utilities.send_reply(self, msg, target.name .. ' is already an administrator or greater.') - else - for _,g in pairs(self.database.administration.groups) do - g.mods[target.id_str] = nil - end - self.database.administration.admins[target.id_str] = true - utilities.send_reply(self, msg, target.name .. ' is now an administrator.') - end - if group.grouptype == 'supergroup' then - drua.channel_set_admin(msg.chat.id, target.id, 2) - end - end - }, - - { -- /deadmin - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('deadmin', true).table, - - command = 'deadmin ', - privilege = 5, - interior = false, - doc = 'Demotes an administrator to a user. The target may be specified via reply, username, or ID.', - - action = function(self, msg, group, config) - local target = administration.get_target(self, msg, config) - if target.err then - utilities.send_reply(self, msg, target.err) - else - for chat_id, group in pairs(self.database.administration.groups) do - if group.grouptype == 'supergroup' then - drua.channel_set_admin(chat_id, target.id, 0) - end - end - if target.rank ~= 4 then - utilities.send_reply(self, msg, target.name .. ' is not an administrator.') - else - self.database.administration.admins[target.id_str] = nil - utilities.send_reply(self, msg, target.name .. ' is no longer an administrator.') - end - end - end - }, - - { -- /gadd - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('gadd', true).table, - - command = 'gadd [i] ...', - privilege = 5, - interior = false, - doc = 'Adds a group to the administration system. Pass numbers as arguments to enable those flags immediately. For example, this would add the group and enable the unlisted flag, antibot, and antiflood:\n/gadd 1 4 5', - - action = function(self, msg, group, config) - if msg.chat.id == msg.from.id then - utilities.send_message(self, msg.chat.id, 'No.') - elseif self.database.administration.groups[msg.chat.id_str] then - utilities.send_reply(self, msg, 'I am already administrating this group.') - else - local flags = {} - for i = 1, #administration.flags do - flags[i] = false - end - local input = utilities.input(msg.text) - if input then - local index = utilities.index(input) - for _, i in ipairs(index) do - i = tonumber(i) - if i and i < #administration.flags and i > 0 then - flags[i] = true - end - end - end - self.database.administration.groups[msg.chat.id_str] = { - mods = {}, - governor = msg.from.id, - bans = {}, - flags = flags, - rules = {}, - grouptype = msg.chat.type, - name = msg.chat.title, - link = drua.export_link(msg.chat.id), - photo = drua.get_photo(msg.chat.id), - founded = os.time(), - autokicks = {}, - autoban = 3 - } - administration.update_desc(self, msg.chat.id, config) - table.insert(self.database.administration.activity, msg.chat.id_str) - utilities.send_reply(self, msg, 'I am now administrating this group.') - drua.channel_set_admin(msg.chat.id, self.info.id, 2) - end - end - }, - - { -- /grem - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('grem', true):t('gremove', true).table, - - command = 'gremove \\[chat]', - privilege = 5, - interior = false, - doc = 'Removes a group from the administration system.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) or msg.chat.id_str - local output - if self.database.administration.groups[input] then - local chat_name = self.database.administration.groups[input].name - self.database.administration.groups[input] = nil - for i,v in ipairs(self.database.administration.activity) do - if v == input then - table.remove(self.database.administration.activity, i) - end - end - output = 'I am no longer administrating _' .. utilities.md_escape(chat_name) .. '_.' - else - if input == msg.chat.id_str then - output = 'I do not administrate this group.' - else - output = 'I do not administrate that group.' - end - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - }, - - { -- /glist - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('glist', false).table, - - command = 'glist', - privilege = 5, - interior = false, - doc = 'Returns a list (in a private message) of all administrated groups with their governors and links.', - - action = function(self, msg, group, config) - local output = '' - if utilities.table_size(self.database.administration.groups) > 0 then - for k,v in pairs(self.database.administration.groups) do - output = output .. '[' .. utilities.md_escape(v.name) .. '](' .. v.link .. ') `[' .. k .. ']`\n' - if v.governor then - local gov = self.database.users[tostring(v.governor)] - output = output .. '★ ' .. utilities.md_escape(utilities.build_name(gov.first_name, gov.last_name)) .. ' `[' .. gov.id .. ']`\n' - end - end - else - output = 'There are no groups.' - end - if utilities.send_message(self, msg.from.id, output, true, nil, true) then - if msg.from.id ~= msg.chat.id then - utilities.send_reply(self, msg, 'I have sent you the requested information in a private message.') - end - end - end - }, - - { -- /broadcast - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('broadcast', true).table, - - command = 'broadcast ', - privilege = 5, - interior = false, - doc = 'Broadcasts a message to all administrated groups.', - - action = function(self, msg, group, config) - local input = utilities.input(msg.text) - if not input then - utilities.send_reply(self, msg, 'Give me something to broadcast.') - else - input = '*Admin Broadcast:*\n' .. input - for id,_ in pairs(self.database.administration.groups) do - utilities.send_message(self, id, input, true, nil, true) - end - end - end - }, - - { -- /buildwall :^) - triggers = utilities.triggers(self_.info.username, config.cmd_pat):t('buildwall').table, - privilege = 3, - interior = true, - action = function(self, msg, group, config) - for i = 2, 5 do - group.flags[i] = true - end - utilities.send_message(self, msg.chat.id, 'antisquig, antisquig++, antibot, and antiflood have been enabled.') - end - } - - } - - -- These could be merged, but load time doesn't matter. - - -- Generate trigger table. - administration.triggers = {} - for _, command in ipairs(administration.commands) do - for _, trigger in ipairs(command.triggers) do - table.insert(administration.triggers, trigger) - end - end - - -- Generate help messages. - self_.database.administration.help = {} - for i,_ in ipairs(administration.ranks) do - self_.admin_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) - end - end - - -- Generate ahelp keywords. - for _,v in ipairs(administration.commands) do - if v.command and v.doc then - v.keyword = utilities.get_word(v.command, 1) - end - end -end - -function administration:action(msg, config) - for _,command in ipairs(administration.commands) do - for _,trigger in pairs(command.triggers) do - if msg.text_lower:match(trigger) then - if command.interior and not self.database.administration.groups[msg.chat.id_str] then - break - end - if administration.get_rank(self, msg.from.id, msg.chat.id, config) < command.privilege then - break - end - local res = command.action(self, msg, self.database.administration.groups[msg.chat.id_str], config) - if res ~= true then - return res - end - end - end - end - return true -end - -function administration:cron() - self.admin_temp.flood = {} - if os.date('%d') ~= self.database.administration.autokick_timer then - self.database.administration.autokick_timer = os.date('%d') - for _,v in pairs(self.database.administration.groups) do - v.autokicks = {} - end - end -end - -administration.command = 'groups' - -return administration diff --git a/otouto/plugins/apod.lua b/otouto/plugins/apod.lua deleted file mode 100644 index d0d187f..0000000 --- a/otouto/plugins/apod.lua +++ /dev/null @@ -1,87 +0,0 @@ - -- Credit to Heitor (tg:Wololo666; gh:heitorPB) for this plugin. - -local apod = {} - -local HTTPS = require('ssl.https') -local JSON = require('dkjson') -local URL = require('socket.url') -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] -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. -]]..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 -```]] -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 - - if input 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 - end - else - date = date .. os.date("%F") - end - - date = date .. '*\n' - - local jstr, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - local jdat = JSON.decode(jstr) - - if jdat.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) - -end - -return apod diff --git a/otouto/plugins/bandersnatch.lua b/otouto/plugins/bandersnatch.lua deleted file mode 100644 index 1cdcc50..0000000 --- a/otouto/plugins/bandersnatch.lua +++ /dev/null @@ -1,35 +0,0 @@ -local bandersnatch = {} - -local utilities = require('otouto.utilities') - -bandersnatch.command = 'bandersnatch' - -function bandersnatch:init(config) - bandersnatch.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bandersnatch'):t('bc').table - bandersnatch.doc = [[``` -Shun the frumious Bandersnatch. -Alias: ]]..config.cmd_pat..[[bc -```]] -end - -local fullnames = { "Wimbledon Tennismatch", "Rinkydink Curdlesnoot", "Butawhiteboy Cantbekhan", "Benadryl Claritin", "Bombadil Rivendell", "Wanda's Crotchfruit", "Biblical Concubine", "Syphilis Cankersore", "Buckminster Fullerene", "Bourgeoisie Capitalist" } - -local firstnames = { "Bumblebee", "Bandersnatch", "Broccoli", "Rinkydink", "Bombadil", "Boilerdang", "Bandicoot", "Fragglerock", "Muffintop", "Congleton", "Blubberdick", "Buffalo", "Benadryl", "Butterfree", "Burberry", "Whippersnatch", "Buttermilk", "Beezlebub", "Budapest", "Boilerdang", "Blubberwhale", "Bumberstump", "Bulbasaur", "Cogglesnatch", "Liverswort", "Bodybuild", "Johnnycash", "Bendydick", "Burgerking", "Bonaparte", "Bunsenburner", "Billiardball", "Bukkake", "Baseballmitt", "Blubberbutt", "Baseballbat", "Rumblesack", "Barister", "Danglerack", "Rinkydink", "Bombadil", "Honkytonk", "Billyray", "Bumbleshack", "Snorkeldink", "Anglerfish", "Beetlejuice", "Bedlington", "Bandicoot", "Boobytrap", "Blenderdick", "Bentobox", "Anallube", "Pallettown", "Wimbledon", "Buttercup", "Blasphemy", "Snorkeldink", "Brandenburg", "Barbituate", "Snozzlebert", "Tiddleywomp", "Bouillabaisse", "Wellington", "Benetton", "Bendandsnap", "Timothy", "Brewery", "Bentobox", "Brandybuck", "Benjamin", "Buckminster", "Bourgeoisie", "Bakery", "Oscarbait", "Buckyball", "Bourgeoisie", "Burlington", "Buckingham", "Barnoldswick" } - -local lastnames = { "Coddleswort", "Crumplesack", "Curdlesnoot", "Calldispatch", "Humperdinck", "Rivendell", "Cuttlefish", "Lingerie", "Vegemite", "Ampersand", "Cumberbund", "Candycrush", "Clombyclomp", "Cragglethatch", "Nottinghill", "Cabbagepatch", "Camouflage", "Creamsicle", "Curdlemilk", "Upperclass", "Frumblesnatch", "Crumplehorn", "Talisman", "Candlestick", "Chesterfield", "Bumbersplat", "Scratchnsniff", "Snugglesnatch", "Charizard", "Carrotstick", "Cumbercooch", "Crackerjack", "Crucifix", "Cuckatoo", "Cockletit", "Collywog", "Capncrunch", "Covergirl", "Cumbersnatch", "Countryside", "Coggleswort", "Splishnsplash", "Copperwire", "Animorph", "Curdledmilk", "Cheddarcheese", "Cottagecheese", "Crumplehorn", "Snickersbar", "Banglesnatch", "Stinkyrash", "Cameltoe", "Chickenbroth", "Concubine", "Candygram", "Moldyspore", "Chuckecheese", "Cankersore", "Crimpysnitch", "Wafflesmack", "Chowderpants", "Toodlesnoot", "Clavichord", "Cuckooclock", "Oxfordshire", "Cumbersome", "Chickenstrips", "Battleship", "Commonwealth", "Cunningsnatch", "Custardbath", "Kryptonite", "Curdlesnoot", "Cummerbund", "Coochyrash", "Crackerdong", "Crackerdong", "Curdledong", "Crackersprout", "Crumplebutt", "Colonist", "Coochierash", "Thundersnatch" } - -function bandersnatch:action(msg) - - local output - - if math.random(10) == 10 then - output = fullnames[math.random(#fullnames)] - else - output = firstnames[math.random(#firstnames)] .. ' ' .. lastnames[math.random(#lastnames)] - end - - utilities.send_message(self, msg.chat.id, '_'..output..'_', true, nil, true) - -end - -return bandersnatch diff --git a/otouto/plugins/bible.lua b/otouto/plugins/bible.lua deleted file mode 100644 index 1daddc2..0000000 --- a/otouto/plugins/bible.lua +++ /dev/null @@ -1,53 +0,0 @@ -local bible = {} - -local HTTP = require('socket.http') -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 - - bible.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('bible', true):t('b', true).table - bible.doc = [[``` -]]..config.cmd_pat..[[bible -Returns a verse from the American Standard Version of the Bible, or an apocryphal verse from the King James Version. Results from biblia.com. -Alias: ]]..config.cmd_pat..[[b -```]] -end - -bible.command = 'bible ' - -function bible:action(msg, config) - - local input = utilities.input(msg.text) - if not input then - utilities.send_message(self, msg.chat.id, bible.doc, true, msg.message_id, true) - return - end - - local url = 'http://api.biblia.com/v1/bible/content/ASV.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) - - local output, res = HTTP.request(url) - - if not output or res ~= 200 or output:len() == 0 then - url = 'http://api.biblia.com/v1/bible/content/KJVAPOC.txt?key=' .. config.biblia_api_key .. '&passage=' .. URL.escape(input) - output, res = HTTP.request(url) - end - - if not output or res ~= 200 or output:len() == 0 then - output = config.errors.results - end - - if output:len() > 4000 then - output = 'The text is too long to post here. Try being more specific.' - end - - utilities.send_reply(self, msg, output) - -end - -return bible diff --git a/otouto/plugins/bing.lua b/otouto/plugins/bing.lua deleted file mode 100644 index f362bc2..0000000 --- a/otouto/plugins/bing.lua +++ /dev/null @@ -1,70 +0,0 @@ - -- Credit to Juan (tg:JuanPotato; gh:JuanPotato) for this plugin. - -- Or rather, the seven lines that actually mean anything. - -local bing = {} - -local URL = require('socket.url') -local JSON = require('dkjson') -local mime = require('mime') -local https = require('ssl.https') -local ltn12 = require('ltn12') -local utilities = require('otouto.utilities') - -bing.command = 'bing ' -bing.doc = [[``` -/bing -Returns the top web search results from Bing. -Aliases: /g, /google -```]] - -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 -end - -function bing: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_reply(self, msg, bing.doc, true) - return - end - end - local url = bing.search_url:format(URL.escape(input)) - local resbody = {} - local _,b,_ = https.request{ - url = url, - headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. config.bing_api_key) }, - sink = ltn12.sink.table(resbody), - } - if b ~= 200 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) - 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) -end - -return bing diff --git a/otouto/plugins/blacklist.lua b/otouto/plugins/blacklist.lua deleted file mode 100644 index ac83e74..0000000 --- a/otouto/plugins/blacklist.lua +++ /dev/null @@ -1,45 +0,0 @@ - -- 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 blacklist = {} - -local utilities = require('otouto.utilities') - -function blacklist:init() - if not self.database.blacklist then - self.database.blacklist = {} - end -end - -blacklist.triggers = { - '' -} - -function blacklist:action(msg, config) - - if self.database.blacklist[msg.from.id_str] then return end - if self.database.blacklist[msg.chat.id_str] then return end - if not msg.text:match('^'..config.cmd_pat..'blacklist') then return true end - if msg.from.id ~= config.admin then return end - - local target = utilities.user_from_message(self, msg) - if target.err then - utilities.send_reply(self, msg, target.err) - return - end - - if tonumber(target.id) < 0 then - target.name = 'Group' - end - - if self.database.blacklist[tostring(target.id)] then - self.database.blacklist[tostring(target.id)] = nil - utilities.send_reply(self, msg, target.name .. ' has been removed from the blacklist.') - else - self.database.blacklist[tostring(target.id)] = true - utilities.send_reply(self, msg, target.name .. ' has been added to the blacklist.') - end - - end - - return blacklist diff --git a/otouto/plugins/calc.lua b/otouto/plugins/calc.lua deleted file mode 100644 index e9cfd99..0000000 --- a/otouto/plugins/calc.lua +++ /dev/null @@ -1,43 +0,0 @@ -local calc = {} - -local URL = require('socket.url') -local HTTPS = require('ssl.https') -local utilities = require('otouto.utilities') - -calc.command = 'calc ' - -function calc:init(config) - calc.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('calc', true).table - calc.doc = [[``` -]]..config.cmd_pat..[[calc -Returns solutions to mathematical expressions and conversions between common units. Results provided by mathjs.org. -```]] -end - -function calc:action(msg, config) - - local input = utilities.input(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, calc.doc, true, msg.message_id, true) - return - end - end - - local url = 'https://api.mathjs.org/v1/?expr=' .. URL.escape(input) - - local output = HTTPS.request(url) - if not output then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - output = '`' .. output .. '`' - - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - -end - -return calc diff --git a/otouto/plugins/cats.lua b/otouto/plugins/cats.lua deleted file mode 100644 index 99f6049..0000000 --- a/otouto/plugins/cats.lua +++ /dev/null @@ -1,38 +0,0 @@ -local cats = {} - -local HTTP = require('socket.http') -local utilities = require('otouto.utilities') - -function cats:init(config) - if not config.thecatapi_key then - print('Missing config value: thecatapi_key.') - print('cats.lua will be enabled, but there are more features with a key.') - end - - cats.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cat').table -end - -cats.command = 'cat' -cats.doc = '`Returns a cat!`' - -function cats:action(msg, config) - - local url = 'http://thecatapi.com/api/images/get?format=html&type=jpg' - if config.thecatapi_key then - url = url .. '&api_key=' .. config.thecatapi_key - end - - local str, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - str = str:match('') - local output = '[Cat!]('..str..')' - - utilities.send_message(self, msg.chat.id, output, false, nil, true) - -end - -return cats diff --git a/otouto/plugins/channel.lua b/otouto/plugins/channel.lua deleted file mode 100644 index cf51e4b..0000000 --- a/otouto/plugins/channel.lua +++ /dev/null @@ -1,65 +0,0 @@ -local channel = {} - -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - ---channel.command = 'ch \\n ' -channel.doc = [[``` -/ch - - -Sends a message to a channel. Channel may be specified via ID or username. Messages are markdown-enabled. Users may only send messages to channels for which they are the owner or an administrator. - -The following markdown syntax is supported: - *bold text* - _italic text_ - [text](URL) - `inline fixed-width code` - `‌`‌`pre-formatted fixed-width code block`‌`‌` - -Due to the frequent dysfunction and incompletion of the API method used to determine the administrators of a channel, this command may not work for the owners of some channels. -```]] - -function channel:init(config) - channel.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ch', true).table -end - -function channel:action(msg, config) - -- An exercise in using zero early returns. :) - local input = utilities.input(msg.text) - local output - if input then - local chat_id = utilities.get_word(input, 1) - local admin_list, t = bindings.getChatAdministrators(self, { chat_id = chat_id } ) - if admin_list then - local is_admin = false - for _, admin in ipairs(admin_list.result) do - if admin.user.id == msg.from.id then - is_admin = true - end - end - if is_admin then - local text = input:match('\n(.+)') - if text then - local success, result = utilities.send_message(self, chat_id, text, true, nil, true) - if success then - output = 'Your message has been sent!' - else - output = 'Sorry, I was unable to send your message.\n`' .. result.description .. '`' - end - else - output = 'Please enter a message to be sent. Markdown is supported.' - end - else - output = 'Sorry, you do not appear to be an administrator for that channel.\nThere is currently a known bug in the getChatAdministrators method, where administrator lists will often not show a channel\'s owner.' - end - else - output = 'Sorry, I was unable to retrieve a list of administrators for that channel.\n`' .. t.description .. '`' - end - else - output = channel.doc - end - utilities.send_reply(self, msg, output, true) -end - -return channel diff --git a/otouto/plugins/chatter.lua b/otouto/plugins/chatter.lua deleted file mode 100644 index 1e60875..0000000 --- a/otouto/plugins/chatter.lua +++ /dev/null @@ -1,80 +0,0 @@ - -- Put this absolutely at the end, even after greetings.lua. - -local chatter = {} - -local HTTP = require('socket.http') -local URL = require('socket.url') -local JSON = require('dkjson') -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - -function chatter:init(config) - if not config.simsimi_key then - print('Missing config value: simsimi_key.') - print('chatter.lua will not be enabled.') - return - end - - chatter.triggers = { - '' - } -end - -chatter.base_url = 'http://%sapi.simsimi.com/request.p?key=%s&lc=%s&ft=1.0&text=%s' - -function chatter:action(msg, config) - - if msg.text == '' then return true end - - if ( - not ( - msg.text_lower:match('^'..self.info.first_name:lower()..',') - or msg.text_lower:match('^@'..self.info.username:lower()..',') - or msg.from.id == msg.chat.id - --Uncomment the following line for Al Gore-like conversation. - --or (msg.reply_to_message and msg.reply_to_message.from.id == self.info.id) - ) - or msg.text:match('^'..config.cmd_pat) - or msg.text == '' - ) then - return true - end - - bindings.sendChatAction(self, { action = 'typing' } ) - - local input = msg.text_lower:gsub(self.info.first_name, 'simsimi') - input = input:gsub('@'..self.info.username, 'simsimi') - - local sandbox = config.simsimi_trial and 'sandbox.' or '' - - local url = chatter.base_url:format(sandbox, config.simsimi_key, config.lang, URL.escape(input)) - - local jstr, res = HTTP.request(url) - if res ~= 200 then - utilities.send_message(self, msg.chat.id, config.errors.chatter_connection) - return - end - - local jdat = JSON.decode(jstr) - if not jdat.response or jdat.response:match('^I HAVE NO RESPONSE.') then - utilities.send_message(self, msg.chat.id, config.errors.chatter_response) - return - end - local output = jdat.response - - -- Clean up the response here. - output = utilities.trim(output) - -- Simsimi will often refer to itself. Replace "simsimi" with the bot name. - output = output:gsub('%aimi?%aimi?', self.info.first_name) - -- Self-explanatory. - output = output:gsub('USER', msg.from.first_name) - -- Capitalize the first letter. - output = output:gsub('^%l', string.upper) - -- Add a period if there is no punctuation. - output = output:gsub('%P$', '%1.') - - utilities.send_message(self, msg.chat.id, output) - -end - -return chatter diff --git a/otouto/plugins/commit.lua b/otouto/plugins/commit.lua deleted file mode 100644 index 0701385..0000000 --- a/otouto/plugins/commit.lua +++ /dev/null @@ -1,430 +0,0 @@ - -- Commits from https://github.com/ngerakines/commitment. - -local commit = {} - -local utilities = require('otouto.utilities') - -commit.command = 'commit' -commit.doc = '`Returns a commit message from whatthecommit.com.`' - -function commit:init(config) - commit.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('commit').table -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
    all good", - "debug suff", - "more debug... who overwrote!", - "these confounded tests drive me nuts", - "For great justice.", - "QuickFix.", - "oops - thought I got that one.", - "removed echo and die statements, lolz.", - "somebody keeps erasing my changes.", - "doh.", - "pam anderson is going to love me.", - "added security.", - "arrgghh... damn this thing for not working.", - "jobs... steve jobs", - "and a comma", - "this is my quickfix branch and i will use to do my quickfixes", - "Fix my stupidness", - "and so the crazy refactoring process sees the sunlight after some months in the dark!", - "gave up and used tables.", - "[Insert your commit message here. Be sure to make it descriptive.]", - "Removed test case since code didn't pass QA", - "removed tests since i can't make them green", - "stuff", - "more stuff", - "Become a programmer, they said. It'll be fun, they said.", - "Same as last commit with changes", - "foo", - "just checking if git is working properly...", - "fixed some minor stuff, might need some additional work.", - "just trolling the repo", - "All your codebase are belong to us.", - "Somebody set up us the bomb.", - "should work I guess...", - "To be honest, I do not quite remember everything I changed here today. But it is all good, I tell ya.", - "well crap.", - "herpderp (redux)", - "herpderp", - "Derp", - "derpherp", - "Herping the derp", - "sometimes you just herp the derp so hard it herpderps", - "Derp. Fix missing constant post rename", - "Herping the fucking derp right here and now.", - "Derp, asset redirection in dev mode", - "mergederp", - "Derp search/replace fuckup", - "Herpy dooves.", - "Derpy hooves", - "derp, helper method rename", - "Herping the derp derp (silly scoping error)", - "Herp derp I left the debug in there and forgot to reset errors.", - "Reset error count between rows. herpderp", - "hey, what's that over there?!", - "hey, look over there!", - "It worked for me...", - "Does not work.", - "Either Hot Shit or Total Bollocks", - "Arrrrgggg", - "Don’t mess with Voodoo", - "I expected something different.", - "Todo!!!", - "This is supposed to crash", - "No changes after this point.", - "I know, I know, this is not how I’m supposed to do it, but I can't think of something better.", - "Don’t even try to refactor it.", - "(c) Microsoft 1988", - "Please no changes this time.", - "Why The Fuck?", - "We should delete this crap before shipping.", - "Shit code!", - "ALL SORTS OF THINGS", - "Herpderp, shoulda check if it does really compile.", - "I CAN HAZ PYTHON, I CAN HAZ INDENTS", - "Major fixup.", - "less french words", - "breathe, =, breathe", - "IEize", - "this doesn't really make things faster, but I tried", - "this should fix it", - "forgot to save that file", - "Glue. Match sticks. Paper. Build script!", - "Argh! About to give up :(", - "Blaming regex.", - "oops", - "it's friday", - "yo recipes", - "Not sure why", - "lol digg", - "grrrr", - "For real, this time.", - "Feed. You. Stuff. No time.", - "I don't give a damn 'bout my reputation", - "DEAL WITH IT", - "commit", - "tunning", - "I really should've committed this when I finished it...", - "It's getting hard to keep up with the crap I've trashed", - "I honestly wish I could remember what was going on here...", - "I must enjoy torturing myself", - "For the sake of my sanity, just ignore this...", - "That last commit message about silly mistakes pales in comparision to this one", - "My bad", - "Still can't get this right...", - "Nitpicking about alphabetizing methods, minor OCD thing", - "Committing fixes in the dark, seriously, who killed my power!?", - "You can't see it, but I'm making a very angry face right now", - "Fix the fixes", - "It's secret!", - "Commit committed....", - "No time to commit.. My people need me!", - "Something fixed", - "I'm hungry", - "asdfasdfasdfasdfasdfasdfadsf", - "hmmm", - "formatted all", - "Replace all whitespaces with tabs.", - "s/ / /g", - "I'm too foo for this bar", - "Things went wrong...", - "??! what the ...", - "This solves it.", - "Working on tests (haha)", - "fixed conflicts (LOL merge -s ours; push -f)", - "last minute fixes.", - "fuckup.", - "Revert \"fuckup\".", - "should work now.", - "final commit.", - "done. going to bed now.", - "buenas those-things.", - "Your commit is writing checks your merge can't cash.", - "This branch is so dirty, even your mom can't clean it.", - "wip", - "Revert \"just testing, remember to revert\"", - "bla", - "harharhar", - "restored deleted entities just to be sure", - "added some filthy stuff", - "bugger", - "lol", - "oopsie B|", - "Copy pasta fail. still had a instead of a", - "Now added delete for real", - "grmbl", - "move your body every every body", - "Trying to fake a conflict", - "And a commit that I don't know the reason of...", - "ffs", - "that's all folks", - "Fucking submodule bull shit", - "apparently i did something…", - "bump to 0.0.3-dev:wq", - "pep8 - cause I fell like doing a barrel roll", - "pep8 fixer", - "it is hump day _^_", - "happy monday _ bleh _", - "after of this commit remember do a git reset hard", - "someday I gonna kill someone for this shit...", - "magic, have no clue but it works", - "I am sorry", - "dirty hack, have a better idea ?", - "Code was clean until manager requested to fuck it up", - " - Temporary commit.", - ":(:(", - "...", - "GIT :/", - "stopped caring 10 commits ago", - "Testing in progress ;)", - "Fixed Bug", - "Fixed errors", - "Push poorly written test can down the road another ten years", - "commented out failing tests", - "I'm human", - "TODO: write meaningful commit message", - "Pig", - "SOAP is a piece of shit", - "did everything", - "project lead is allergic to changes...", - "making this thing actually usable.", - "I was told to leave it alone, but I have this thing called OCD, you see", - "Whatever will be, will be 8{", - "It's 2015; why are we using ColdFusion?!", - "#GrammarNazi", - "Future self, please forgive me and don't hit me with the baseball bat again!", - "Hide those navs, boi!", - "Who knows...", - "Who knows WTF?!", - "I should get a raise for this.", - "Done, to whoever merges this, good luck.", - "Not one conflict, today was a good day.", - "First Blood", - "Fixed the fuck out of #526!", - "I'm too old for this shit!", - "One little whitespace gets its very own commit! Oh, life is so erratic!", - "please dont let this be the problem", - "good: no crash. bad: nothing happens", - "trying", - "trying harder", - "i tried", - "fml" -} - -function commit:action(msg) - - local output = '`'..commits[math.random(#commits)]..'`' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - -end - -return commit diff --git a/otouto/plugins/currency.lua b/otouto/plugins/currency.lua deleted file mode 100644 index 16160d2..0000000 --- a/otouto/plugins/currency.lua +++ /dev/null @@ -1,61 +0,0 @@ -local currency = {} - -local HTTPS = require('ssl.https') -local utilities = require('otouto.utilities') - -currency.command = 'cash [amount] to ' - -function currency:init(config) - currency.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('cash', true).table - currency.doc = [[``` -]]..config.cmd_pat..[[cash [amount] to -Example: ]]..config.cmd_pat..[[cash 5 USD to EUR -Returns exchange rates for various currencies. -Source: Google Finance. -```]] -end - -function currency:action(msg, config) - - local input = msg.text:upper() - if not input:match('%a%a%a TO %a%a%a') then - utilities.send_message(self, msg.chat.id, currency.doc, true, msg.message_id, true) - return - end - - local from = input:match('(%a%a%a) TO') - local to = input:match('TO (%a%a%a)') - local amount = utilities.get_word(input, 2) - amount = tonumber(amount) or 1 - local result = 1 - - local url = 'https://www.google.com/finance/converter' - - if from ~= to then - - url = url .. '?from=' .. from .. '&to=' .. to .. '&a=' .. amount - local str, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - str = str:match('(.*) %u+') - if not str then - utilities.send_reply(self, msg, config.errors.results) - return - end - - result = string.format('%.2f', str) - - end - - local output = amount .. ' ' .. from .. ' = ' .. result .. ' ' .. to .. '\n\n' - output = output .. os.date('!%F %T UTC') .. '\nSource: Google Finance`' - output = '```\n' .. output .. '\n```' - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - -end - -return currency diff --git a/otouto/plugins/dice.lua b/otouto/plugins/dice.lua deleted file mode 100644 index 032d3bf..0000000 --- a/otouto/plugins/dice.lua +++ /dev/null @@ -1,56 +0,0 @@ -local dice = {} - -local utilities = require('otouto.utilities') - -dice.command = 'roll ' - -function dice:init(config) - dice.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('roll', true).table - dice.doc = [[``` -]]..config.cmd_pat..[[roll -Returns a set of dice rolls, where n is the number of rolls and r is the range. If only a range is given, returns only one roll. -```]] -end - -function dice:action(msg) - - local input = utilities.input(msg.text_lower) - if not input then - utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) - return - end - - local count, range - if input:match('^[%d]+d[%d]+$') then - count, range = input:match('([%d]+)d([%d]+)') - elseif input:match('^d?[%d]+$') then - count = 1 - range = input:match('^d?([%d]+)$') - else - utilities.send_message(self, msg.chat.id, dice.doc, true, msg.message_id, true) - return - end - - count = tonumber(count) - range = tonumber(range) - - if range < 2 then - utilities.send_reply(self, msg, 'The minimum range is 2.') - return - end - if range > 1000 or count > 1000 then - utilities.send_reply(self, msg, 'The maximum range and count are 1000.') - return - end - - local output = '*' .. count .. 'd' .. range .. '*\n`' - for _ = 1, count do - output = output .. math.random(range) .. '\t' - end - output = output .. '`' - - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - -end - -return dice diff --git a/otouto/plugins/dilbert.lua b/otouto/plugins/dilbert.lua deleted file mode 100644 index 63416a6..0000000 --- a/otouto/plugins/dilbert.lua +++ /dev/null @@ -1,51 +0,0 @@ -local dilbert = {} - -local HTTP = require('socket.http') -local URL = require('socket.url') -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - -dilbert.command = 'dilbert [date]' - -function dilbert:init(config) - dilbert.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('dilbert', true).table - dilbert.doc = [[``` -]]..config.cmd_pat..[[dilbert [YYYY-MM-DD] -Returns the latest Dilbert strip or that of the provided date. -Dates before the first strip will return the first strip. Dates after the last trip will return the last strip. -Source: dilbert.com -```]] -end - -function dilbert:action(msg, config) - - bindings.sendChatAction(self, { chat_id = msg.chat.id, action = 'upload_photo' } ) - - local input = utilities.input(msg.text) - if not input then input = os.date('%F') end - if not input:match('^%d%d%d%d%-%d%d%-%d%d$') then input = os.date('%F') end - - local url = 'http://dilbert.com/strip/' .. URL.escape(input) - local str, res = HTTP.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - local strip_filename = '/tmp/' .. input .. '.gif' - local strip_file = io.open(strip_filename) - if strip_file then - strip_file:close() - strip_file = strip_filename - else - local strip_url = str:match('') - strip_file = utilities.download_file(strip_url, '/tmp/' .. input .. '.gif') - end - - local strip_title = str:match('') - - bindings.sendPhoto(self, { chat_id = msg.chat.id, caption = strip_title }, { photo = strip_file } ) - -end - -return dilbert diff --git a/otouto/plugins/echo.lua b/otouto/plugins/echo.lua deleted file mode 100644 index 10228b4..0000000 --- a/otouto/plugins/echo.lua +++ /dev/null @@ -1,34 +0,0 @@ -local echo = {} - -local utilities = require('otouto.utilities') - -echo.command = 'echo ' - -function echo:init(config) - echo.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('echo', true).table - echo.doc = [[``` -]]..config.cmd_pat..[[echo -Repeats a string of text. -```]] -end - -function echo:action(msg) - - local input = utilities.input(msg.text) - - 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) .. '"' - else - output = utilities.md_escape(utilities.char.zwnj..input) - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - end - - -end - -return echo diff --git a/otouto/plugins/eightball.lua b/otouto/plugins/eightball.lua deleted file mode 100644 index 5db3972..0000000 --- a/otouto/plugins/eightball.lua +++ /dev/null @@ -1,58 +0,0 @@ -local eightball = {} - -local utilities = require('otouto.utilities') - -eightball.command = '8ball' -eightball.doc = '`Returns an answer from a magic 8-ball!`' - -function eightball:init(config) - eightball.triggers = utilities.triggers(self.info.username, config.cmd_pat, - {'[Yy]/[Nn]%p*$'}):t('8ball', true).table -end - -local ball_answers = { - "It is certain.", - "It is decidedly so.", - "Without a doubt.", - "Yes, definitely.", - "You may rely on it.", - "As I see it, yes.", - "Most likely.", - "Outlook: good.", - "Yes.", - "Signs point to yes.", - "Reply hazy try again.", - "Ask again later.", - "Better not tell you now.", - "Cannot predict now.", - "Concentrate and ask again.", - "Don't count on it.", - "My reply is no.", - "My sources say no.", - "Outlook: not so good.", - "Very doubtful.", - "There is a time and place for everything, but not now." -} - -local yesno_answers = { - 'Absolutely.', - 'In your dreams.', - 'Yes.', - 'No.' -} - -function eightball:action(msg) - - local output - - if msg.text_lower:match('y/n%p?$') then - output = yesno_answers[math.random(#yesno_answers)] - else - output = ball_answers[math.random(#ball_answers)] - end - - utilities.send_reply(self, msg, output) - -end - -return eightball diff --git a/otouto/plugins/fortune.lua b/otouto/plugins/fortune.lua deleted file mode 100644 index c47871e..0000000 --- a/otouto/plugins/fortune.lua +++ /dev/null @@ -1,31 +0,0 @@ - -- Requires that the "fortune" program is installed on your computer. - -local fortune = {} - -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 - - fortune.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('fortune').table -end - -fortune.command = 'fortune' -fortune.doc = '`Returns a UNIX fortune.`' - -function fortune:action(msg) - - local fortunef = io.popen('fortune') - local output = fortunef:read('*all') - output = '```\n' .. output .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, nil, true) - fortunef:close() - -end - -return fortune diff --git a/otouto/plugins/gImages.lua b/otouto/plugins/gImages.lua deleted file mode 100644 index 3fab5b3..0000000 --- a/otouto/plugins/gImages.lua +++ /dev/null @@ -1,74 +0,0 @@ - -- You need a Google API key and a Google Custom Search Engine set up to use this, in config.google_api_key and config.google_cse_key, respectively. - -- You must also sign up for the CSE in the Google Developer Console, and enable image results. - -local gImages = {} - -local HTTPS = require('ssl.https') -local URL = require('socket.url') -local JSON = require('dkjson') -local utilities = require('otouto.utilities') -local bindings = require('otouto.bindings') - -function gImages:init(config) - if not cred_data.google_apikey then - print('Missing config value: google_apikey.') - print('gImages.lua will not be enabled.') - return - elseif not cred_data.google_cse_id then - print('Missing config value: google_cse_id.') - print('gImages.lua will not be enabled.') - return - end - - gImages.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('img', true):t('i', true):t('insfw', true).table - gImages.doc = [[* -]]..config.cmd_pat..[[img* __ -Sucht Bild mit Google und versendet es (SafeSearch aktiv) -Alias: *]]..config.cmd_pat..[[i*]] -end - -gImages.command = 'img ' - -function gImages: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, gImages.doc, true, msg.message_id, true) - return - end - end - - print ('Checking if search contains blacklisted word: '..input) - if is_blacklisted(input) then - utilities.send_reply(self, msg, 'Vergiss es! ._.') - return - end - - utilities.send_typing(self, msg.chat.id, 'upload_photo') - local apikey = cred_data.google_apikey - local cseid = cred_data.google_cse_id - local BASE_URL = 'https://www.googleapis.com/customsearch/v1' - local url = BASE_URL..'/?searchType=image&alt=json&num=10&key='..apikey..'&cx='..cseid..'&safe=high'..'&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 jdat.searchInformation.totalResults == '0' then - utilities.send_reply(self, msg, config.errors.results) - return - end - - local i = math.random(jdat.queries.request[1].count) - local img_url = jdat.items[i].link - - local file = download_to_file(img_url) - utilities.send_photo(self, msg.chat.id, file, img_url) -end - -return gImages diff --git a/otouto/plugins/gMaps.lua b/otouto/plugins/gMaps.lua deleted file mode 100644 index d3719e9..0000000 --- a/otouto/plugins/gMaps.lua +++ /dev/null @@ -1,44 +0,0 @@ -local gMaps = {} - -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - -gMaps.command = 'location ' - -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 -Returns a location from Google Maps. -Alias: ]]..config.cmd_pat..[[loc -```]] -end - -function gMaps: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, gMaps.doc, true, msg.message_id, true) - return - end - end - - local coords = utilities.get_coords(input, config) - if type(coords) == 'string' then - utilities.send_reply(self, msg, coords) - return - end - - bindings.sendLocation(self, { - chat_id = msg.chat.id, - latitude = coords.lat, - longitude = coords.lon, - reply_to_message_id = msg.message_id - } ) - -end - -return gMaps diff --git a/otouto/plugins/hackernews.lua b/otouto/plugins/hackernews.lua deleted file mode 100644 index b0a9f69..0000000 --- a/otouto/plugins/hackernews.lua +++ /dev/null @@ -1,65 +0,0 @@ -local hackernews = {} - -local HTTPS = require('ssl.https') -local JSON = require('dkjson') -local bindings = require('otouto.bindings') -local utilities = require('otouto.utilities') - -hackernews.command = 'hackernews' - -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 -```]] -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 - 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' - for i = 1, res_count do - local res_url = 'https://hacker-news.firebaseio.com/v0/item/' .. jdat[i] .. '.json' - jstr, res = HTTPS.request(res_url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local res_jdat = JSON.decode(jstr) - local title = res_jdat.title:gsub('%[.+%]', ''):gsub('%(.+%)', ''):gsub('&', '&') - if title:len() > 48 then - title = title:sub(1, 45) .. '...' - end - local url = res_jdat.url - if not url then - utilities.send_reply(self, msg, config.errors.connection) - return - end - if url:find('%(') then - output = output .. '• ' .. title .. '\n' .. url:gsub('_', '\\_') .. '\n' - else - output = output .. '• [' .. title .. '](' .. url .. ')\n' - end - - end - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - -end - -return hackernews diff --git a/otouto/plugins/hearthstone.lua b/otouto/plugins/hearthstone.lua deleted file mode 100644 index 6f8d075..0000000 --- a/otouto/plugins/hearthstone.lua +++ /dev/null @@ -1,130 +0,0 @@ - -- Plugin for the Hearthstone database provided by hearthstonejson.com. - -local hearthstone = {} - ---local HTTPS = require('ssl.https') -local JSON = require('dkjson') -local utilities = require('otouto.utilities') - -function hearthstone:init(config) - 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 - print('Error connecting to hearthstonejson.com.') - print('hearthstone.lua will not be enabled.') - return - end - - self.database.hearthstone = d - 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 -Returns Hearthstone card info. -Alias: ]]..config.cmd_pat..[[hs -```]] -end - -hearthstone.command = 'hearthstone ' - -local function format_card(card) - - local ctype = card.type - if card.race then - ctype = card.race - end - if card.rarity then - ctype = card.rarity .. ' ' .. ctype - end - if card.playerClass then - ctype = ctype .. ' (' .. card.playerClass .. ')' - elseif card.faction then - ctype = ctype .. ' (' .. card.faction .. ')' - end - - local stats - if card.cost then - stats = card.cost .. 'c' - if card.attack then - stats = stats .. ' | ' .. card.attack .. 'a' - end - if card.health then - stats = stats .. ' | ' .. card.health .. 'h' - end - if card.durability then - stats = stats .. ' | ' .. card.durability .. 'd' - end - elseif card.health then - stats = card.health .. 'h' - end - - -- unused? - local info - if card.text then - info = card.text:gsub('',''):gsub('%$','') - if card.flavor then - info = info .. '\n_' .. card.flavor .. '_' - end - elseif card.flavor then - info = card.flavor - else - info = nil - end - - local s = '*' .. card.name .. '*\n' .. ctype - if stats then - s = s .. '\n' .. stats - end - if info then - s = s .. '\n' .. info - end - - return s - -end - -function hearthstone:action(msg, config) - - local input = utilities.input(msg.text_lower) - if not input then - utilities.send_message(self, msg.chat.id, hearthstone.doc, true, msg.message_id, true) - return - end - - local output = '' - for _,v in pairs(self.database.hearthstone) do - if type(v) == 'table' and string.lower(v.name):match(input) then - output = output .. format_card(v) .. '\n\n' - end - end - - output = utilities.trim(output) - if output:len() == 0 then - utilities.send_reply(self, msg, config.errors.results) - return - end - - utilities.send_message(self, msg.chat.id, output, true, msg.message_id, true) - -end - -return hearthstone diff --git a/otouto/plugins/images.lua b/otouto/plugins/images.lua deleted file mode 100644 index a546ba4..0000000 --- a/otouto/plugins/images.lua +++ /dev/null @@ -1,16 +0,0 @@ -local images = {} - -local utilities = require('otouto.utilities') -images.triggers = { - "(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Pp][Nn][Gg])$", - "(https?://[%w-_%%%.%?%.:,/%+=~&%[%]]+%.[Jj][Pp][Ee]?[Gg])$" -} - -function images:action(msg) - utilities.send_typing(self, msg.chat.id, 'upload_photo') - local url = matches[1] - local file = download_to_file(url) - utilities.send_photo(self, msg.chat.id, file, nil, msg.message_id) -end - -return images diff --git a/otouto/plugins/lastfm.lua b/otouto/plugins/lastfm.lua deleted file mode 100644 index 316b3e0..0000000 --- a/otouto/plugins/lastfm.lua +++ /dev/null @@ -1,111 +0,0 @@ - -- TODO: Add support for librefm API. - -- Just kidding, nobody actually uses that. - -local lastfm = {} - -local HTTP = require('socket.http') -local URL = require('socket.url') -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 - - lastfm.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('lastfm', true):t('np', true):t('fmset', true).table - lastfm.doc = [[``` -]]..config.cmd_pat..[[np [username] -Returns what you are or were last listening to. If you specify a username, info will be returned for that username. - -]]..config.cmd_pat..[[fmset -Sets your last.fm username. Otherwise, ]]..config.cmd_pat..[[np will use your Telegram username. Use "]]..config.cmd_pat..[[fmset --" to delete it. -```]] -end - -lastfm.command = 'lastfm' - -function lastfm:action(msg, config) - - local input = utilities.input(msg.text) - - if string.match(msg.text, '^'..config.cmd_pat..'lastfm') then - utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) - return - elseif string.match(msg.text, '^'..config.cmd_pat..'fmset') then - if not input then - utilities.send_message(self, msg.chat.id, lastfm.doc, true, msg.message_id, true) - elseif input == '--' or input == utilities.char.em_dash then - self.database.users[msg.from.id_str].lastfm = nil - utilities.send_reply(self, msg, 'Your last.fm username has been forgotten.') - else - self.database.users[msg.from.id_str].lastfm = input - utilities.send_reply(self, msg, 'Your last.fm username has been set to "' .. input .. '".') - end - return - end - - local url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' .. config.lastfm_api_key .. '&user=' - - local username - local alert = '' - if input then - username = input - elseif self.database.users[msg.from.id_str].lastfm then - username = self.database.users[msg.from.id_str].lastfm - elseif msg.from.username then - username = msg.from.username - alert = '\n\nYour username has been set to ' .. username .. '.\nTo change it, use '..config.cmd_pat..'fmset .' - self.database.users[msg.from.id_str].lastfm = username - else - utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') - return - end - - url = url .. URL.escape(username) - - local jstr, res - utilities.with_http_timeout( - 1, function () - jstr, res = HTTP.request(url) - end) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - local jdat = JSON.decode(jstr) - if jdat.error then - utilities.send_reply(self, msg, 'Please specify your last.fm username or set it with '..config.cmd_pat..'fmset.') - return - end - - jdat = jdat.recenttracks.track[1] or jdat.recenttracks.track - if not jdat then - utilities.send_reply(self, msg, 'No history for this user.' .. alert) - return - end - - local output = input or msg.from.first_name - output = '🎵 ' .. output - - if jdat['@attr'] and jdat['@attr'].nowplaying then - output = output .. ' is currently listening to:\n' - else - output = output .. ' last listened to:\n' - end - - local title = jdat.name or 'Unknown' - local artist = 'Unknown' - if jdat.artist then - artist = jdat.artist['#text'] - end - - output = output .. title .. ' - ' .. artist .. alert - utilities.send_message(self, msg.chat.id, output) - -end - -return lastfm diff --git a/otouto/plugins/me.lua b/otouto/plugins/me.lua deleted file mode 100644 index f11769c..0000000 --- a/otouto/plugins/me.lua +++ /dev/null @@ -1,29 +0,0 @@ -local me = {} - -local utilities = require('otouto.utilities') - -function me:init(config) - me.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('me', true).table -end - -function me:action(msg, config) - - local target = self.database.users[msg.from.id_str] - - if msg.from.id == config.admin and (msg.reply_to_message or utilities.input(msg.text)) then - target = utilities.user_from_message(self, msg, true) - if target.err then - utilities.send_reply(self, msg, target.err) - return - end - end - - local output = '' - for k,v in pairs(target) do - output = output .. '*' .. k .. ':* `' .. tostring(v) .. '`\n' - end - utilities.send_message(self, msg.chat.id, output, true, nil, true) - -end - -return me diff --git a/otouto/plugins/nick.lua b/otouto/plugins/nick.lua deleted file mode 100644 index 50a60b8..0000000 --- a/otouto/plugins/nick.lua +++ /dev/null @@ -1,51 +0,0 @@ -local nick = {} - -local utilities = require('otouto.utilities') - -nick.command = 'nick ' - -function nick:init(config) - nick.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('nick', true).table - nick.doc = [[``` -]]..config.cmd_pat..[[nick -Set your nickname. Use "]]..config.cmd_pat..[[nick --" to delete it. -```]] -end - -function nick:action(msg, config) - - local target = msg.from - - if msg.from.id == config.admin and msg.reply_to_message then - target = msg.reply_to_message.from - target.id_str = tostring(target.id) - target.name = target.first_name - if target.last_name then - target.name = target.first_name .. ' ' .. target.last_name - end - end - - local output - local input = utilities.input(msg.text) - if not input then - if self.database.users[target.id_str].nickname then - output = target.name .. '\'s nickname is "' .. self.database.users[target.id_str].nickname .. '".' - else - output = target.name .. ' currently has no nickname.' - end - elseif utilities.utf8_len(input) > 32 then - output = 'The character limit for nicknames is 32.' - elseif input == '--' or input == utilities.char.em_dash then - self.database.users[target.id_str].nickname = nil - output = target.name .. '\'s nickname has been deleted.' - else - input = input:gsub('\n', ' ') - self.database.users[target.id_str].nickname = input - output = target.name .. '\'s nickname has been set to "' .. input .. '".' - end - - utilities.send_reply(self, msg, output) - -end - -return nick diff --git a/otouto/plugins/patterns.lua b/otouto/plugins/patterns.lua deleted file mode 100644 index 82eb8fd..0000000 --- a/otouto/plugins/patterns.lua +++ /dev/null @@ -1,33 +0,0 @@ -local patterns = {} - -local utilities = require('otouto.utilities') - -patterns.triggers = { - '^/?s/.-/.-$' -} - -function patterns:action(msg) - if not msg.reply_to_message then return true end - local output = msg.reply_to_message.text - if msg.reply_to_message.from.id == self.info.id then - output = output:gsub('Did you mean:\n"', '') - output = output:gsub('"$', '') - end - local m1, m2 = msg.text:match('^/?s/(.-)/(.-)/?$') - if not m2 then return true end - local res - res, output = pcall( - function() - return output:gsub(m1, m2) - end - ) - if res == false then - utilities.send_reply(self, msg, 'Malformed pattern!') - else - output = output:sub(1, 4000) - output = 'Did you mean:\n"' .. output .. '"' - utilities.send_reply(self, msg.reply_to_message, output) - end -end - -return patterns diff --git a/otouto/plugins/ping.lua b/otouto/plugins/ping.lua deleted file mode 100644 index c368c33..0000000 --- a/otouto/plugins/ping.lua +++ /dev/null @@ -1,16 +0,0 @@ - -- Actually the simplest plugin ever! - -local ping = {} - -local utilities = require('otouto.utilities') - -function ping:init(config) - ping.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('ping'):t('annyong').table -end - -function ping:action(msg, config) - local output = msg.text_lower:match('^'..config.cmd_pat..'ping') and 'Pong!' or 'Annyong.' - utilities.send_message(self, msg.chat.id, output) -end - -return ping diff --git a/otouto/plugins/pun.lua b/otouto/plugins/pun.lua deleted file mode 100644 index fc84689..0000000 --- a/otouto/plugins/pun.lua +++ /dev/null @@ -1,144 +0,0 @@ -local pun = {} - -local utilities = require('otouto.utilities') - -pun.command = 'pun' -pun.doc = '`Returns a pun.`' - -function pun:init(config) - pun.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('pun').table -end - -local puns = { - "The person who invented the door-knock won the No-bell prize.", - "I couldn't work out how to fasten my seatbelt. Then it clicked.", - "Never trust atoms; they make up everything.", - "Singing in the shower is all fun and games until you get shampoo in your mouth - Then it becomes a soap opera.", - "I can't believe I got fired from the calendar factory. All I did was take a day off.", - "To the guy who invented zero: Thanks for nothing!", - "Enough with the cripple jokes! I just can't stand them.", - "I've accidentally swallowed some Scrabble tiles. My next crap could spell disaster.", - "How does Moses make his tea? Hebrews it.", - "Did you hear about the guy who got hit in the head with a can of soda? He was lucky it was a soft drink.", - "When William joined the army he disliked the phrase 'fire at will'.", - "There was a sign on the lawn at a rehab center that said 'Keep off the Grass'.", - "I wondered why the baseball was getting bigger. Then it hit me.", - "I can hear music coming out of my printer. I think the paper's jamming again.", - "I have a few jokes about unemployed people, but none of them work", - "Want to hear a construction joke? I'm working on it", - "I always take a second pair of pants when I go golfing, in case I get a hole in one.", - "I couldn't remember how to throw a boomerang, but then it came back to me.", - "I've decided that my wifi will be my valentine. IDK, we just have this connection.", - "A prisoner's favorite punctuation mark is the period. It marks the end of his sentence.", - "I used to go fishing with Skrillex, but he kept dropping the bass.", - "Two antennae met on a roof and got married. The wedding was okay, but the reception was incredible.", - "A book just fell on my head. I've only got my shelf to blame.", - "I dropped my steak on the floor. Now it's ground beef.", - "I used to have a fear of hurdles, but I got over it.", - "The outcome of war does not prove who is right, but only who is left.", - "Darth Vader tries not to burn his food, but it always comes out a little on the dark side.", - "The store keeps calling me to buy more furniture, but all I wanted was a one night stand.", - "This girl said she recognized me from the vegetarian club, but I'd never met herbivore.", - "Police arrested two kids yesterday, one was drinking battery acid, the other was eating fireworks. They charged one and let the other one off...", - "No more Harry Potter jokes guys. I'm Sirius.", - "It was hard getting over my addiction to hokey pokey, but I've turned myself around.", - "It takes a lot of balls to golf the way I do.", - "Why did everyone want to hang out with the mushroom? Because he was a fungi.", - "How much does a hipster weigh? An instagram.", - "I used to be addicted to soap, but I'm clean now.", - "When life gives you melons, you’re probably dyslexic.", - "What's with all the blind jokes? I just don't see the point.", - "If Apple made a car, would it have Windows?", - "Need an ark? I Noah guy.", - "The scarecrow won an award because he was outstanding in his field.", - "What's the difference between a man in a tux on a bicycle, and a man in a sweatsuit on a trycicle? A tire.", - "What do you do with a sick chemist? If you can't helium, and you can't curium, you'll just have to barium.", - "I'm reading a book about anti-gravity. It's impossible to put down.", - "Trying to write with a broken pencil is pointless.", - "When TVs go on vacation, they travel to remote islands.", - "I was going to tell a midget joke, but it's too short.", - "Jokes about German sausage are the wurst.", - "How do you organize a space party? You planet.", - "Sleeping comes so naturally to me, I could do it with my eyes closed.", - "I'm glad I know sign language; it's pretty handy.", - "Atheism is a non-prophet organization.", - "Velcro: What a rip-off!", - "If they made a Minecraft movie, it would be a blockbuster.", - "I don't trust people with graph paper. They're always plotting something", - "I had a friend who was addicted to brake fluid. He says he can stop anytime.", - "The form said I had Type A blood, but it was a Type O.", - "I went to to the shop to buy eight Sprites - I came home and realised I'd picked 7Up.", - "There was an explosion at a pie factory. 3.14 people died.", - "A man drove his car into a tree and found out how a Mercedes bends.", - "The experienced carpenter really nailed it, but the new guy screwed everything up.", - "I didn't like my beard at first, but then it grew on me.", - "Smaller babies may be delivered by stork, but the heavier ones need a crane.", - "What's the definition of a will? It's a dead giveaway.", - "I was going to look for my missing watch, but I could never find the time.", - "I hate elevators, and I often take steps to avoid them.", - "Did you hear about the guy whose whole left side was cut off? He's all right now.", - "It's not that the man did not know how to juggle, he just didn't have the balls to do it.", - "I used to be a loan shark, but I lost interest", - "I don't trust these stairs; they're always up to something.", - "My friend's bakery burned down last night. Now his business is toast.", - "Don't trust people that do acupuncture; they're back stabbers.", - "The man who survived mustard gas and pepper spray is now a seasoned veteran.", - "Police were called to a daycare where a three-year-old was resisting a rest.", - "When Peter Pan punches, they Neverland", - "The shoemaker did not deny his apprentice anything he needed. He gave him his awl.", - "I did a theatrical performance about puns. It was a play on words.", - "Show me a piano falling down a mineshaft and I'll show you A-flat minor.", - "Have you ever tried to eat a clock? It's very time consuming.", - "There was once a cross-eyed teacher who couldn't control his pupils.", - "A new type of broom came out and it is sweeping the nation.", - "I relish the fact that you've mustard the strength to ketchup to me.", - "I knew a woman who owned a taser. Man, was she stunning!", - "What did the grape say when it got stepped on? Nothing - but it let out a little whine.", - "It was an emotional wedding. Even the cake was in tiers.", - "When a clock is hungry it goes back four seconds.", - "The dead batteries were given out free of charge.", - "Why are there no knock-knock jokes about America? Because freedom rings.", - "When the cannibal showed up late to dinner, they gave him the cold shoulder.", - "I should have been sad when my flashlight died, but I was delighted.", - "Why don't tennis players ever get married? Love means nothing to them.", - "Pterodactyls can't be heard going to the bathroom because the P is silent.", - "Mermaids make calls on their shell phones.", - "What do you call an aardvark with three feet? A yaardvark.", - "Captain Kirk has three ears: A right ear, a left ear, and a final front ear.", - "How do celebrities stay cool? They have a lot of fans.", - "Without geometry, life is pointless.", - "Did you hear about the cow who tried to jump over a barbed-wire fence? It ended in udder destruction.", - "The truth may ring like a bell, but it is seldom ever tolled.", - "I used to work for the IRS, but my job was too taxing.", - "I used to be a programmer, but then I lost my drive.", - "Pediatricians are doctors with little patients.", - "I finally fired my masseuse today. She always rubbed me the wrong way.", - "I stayed up all night wondering where the sun went. Then it dawned on me.", - "What's the difference between a man and his dog? The man wears a suit; the dog just pants.", - "A psychic midget who escapes from prison is a small medium at large.", - "I've been to the dentist several times, so I know the drill.", - "The roundest knight at King Arthur's round table was Sir Cumference. He acquired his size from too much pi.", - "She was only a whiskey maker, but he loved her still.", - "Male deer have buck teeth.", - "Whiteboards are remarkable.", - "Visitors in Cuba are always Havana good time.", - "Why does electricity shock people? It doesn't know how to conduct itself.", - "Lancelot had a scary dream about his horse. It was a knight mare.", - "A tribe of cannibals captured a missionary and ate him. Afterward, they all had violent food poisoning. This just goes to show that you can't keep a good man down.", - "Heaven for gamblers is a paradise.", - "Old wheels aren't thrown away, they're just retired.", - "Horses are very stable animals.", - "Banks don't crash, they just lose their balance.", - "The career of a skier can go downhill very fast.", - "In democracy, it's your vote that counts. In feudalism, it's your count that votes.", - "A sea lion is nothing but an ionized seal.", - "The vegetables from my garden aren't that great. I guess you could say they're mediokra." -} - -function pun:action(msg) - - utilities.send_reply(self, msg, puns[math.random(#puns)]) - -end - -return pun diff --git a/otouto/plugins/reactions.lua b/otouto/plugins/reactions.lua deleted file mode 100644 index e594d50..0000000 --- a/otouto/plugins/reactions.lua +++ /dev/null @@ -1,52 +0,0 @@ - -- Never change this plugin. It was not meant to be changed. - -- You may add reactions. You must never remove reactions. - -- You must never restructure. You must never disable this plugin. - -- - Drew, creator, a year later. - - -- Nevermind, Brayden changed it. - -- - Drew, just now. - -local reactions = {} - -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) - -- Generate a "help" message triggered by "/reactions". - help = 'Reactions:\n' - reactions.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('reactions').table - for trigger,reaction in pairs(mapping) do - help = help .. '• ' .. config.cmd_pat .. trigger:gsub('.%?', '') .. ': ' .. reaction .. '\n' - table.insert(reactions.triggers, config.cmd_pat..trigger) - table.insert(reactions.triggers, config.cmd_pat..trigger..'@'..self.info.username:lower()) - end -end - -function reactions:action(msg, config) - if string.match(msg.text_lower, config.cmd_pat..'reactions') then - utilities.send_message(self, msg.chat.id, help) - return - end - for trigger,reaction in pairs(mapping) do - if string.match(msg.text_lower, config.cmd_pat..trigger) then - utilities.send_message(self, msg.chat.id, reaction) - return - end - end -end - -return reactions diff --git a/otouto/plugins/shout.lua b/otouto/plugins/shout.lua deleted file mode 100644 index 9f68327..0000000 --- a/otouto/plugins/shout.lua +++ /dev/null @@ -1,52 +0,0 @@ -local shout = {} - -local utilities = require('otouto.utilities') - -shout.command = 'shout ' - -function shout:init(config) - shout.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('shout', true).table - shout.doc = [[``` -]]..config.cmd_pat..[[shout -Shouts something. Input may be the replied-to message. -```]] -end - -function shout:action(msg) - - local input = utilities.input(msg.text) - 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 - end - input = utilities.trim(input) - - if input:len() > 20 then - input = input:sub(1,20) - end - - input = input:upper() - local output = '' - local inc = 0 - for match in input:gmatch('([%z\1-\127\194-\244][\128-\191]*)') do - output = output .. match .. ' ' - end - output = output .. '\n' - for match in input:sub(2):gmatch('([%z\1-\127\194-\244][\128-\191]*)') do - local spacing = '' - for _ = 1, inc do - spacing = spacing .. ' ' - end - inc = inc + 1 - output = output .. match .. ' ' .. spacing .. match .. '\n' - end - output = '```\n' .. utilities.trim(output) .. '\n```' - utilities.send_message(self, msg.chat.id, output, true, false, true) - -end - -return shout diff --git a/otouto/plugins/slap.lua b/otouto/plugins/slap.lua deleted file mode 100644 index 5128434..0000000 --- a/otouto/plugins/slap.lua +++ /dev/null @@ -1,130 +0,0 @@ -local slap = {} - -local utilities = require('otouto.utilities') - -slap.command = 'slap [target]' - -function slap:init(config) - slap.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('slap', true).table - slap.doc = [[``` -]]..config.cmd_pat..[[slap [target] -Slap somebody. -```]] -end - -local slaps = { - 'VICTIM was shot by VICTOR.', - 'VICTIM was pricked to death.', - 'VICTIM walked into a cactus while trying to escape VICTOR.', - 'VICTIM drowned.', - 'VICTIM drowned whilst trying to escape VICTOR.', - 'VICTIM blew up.', - 'VICTIM was blown up by VICTOR.', - 'VICTIM hit the ground too hard.', - 'VICTIM fell from a high place.', - 'VICTIM fell off a ladder.', - 'VICTIM fell into a patch of cacti.', - 'VICTIM was doomed to fall by VICTOR.', - 'VICTIM was blown from a high place by VICTOR.', - 'VICTIM was squashed by a falling anvil.', - 'VICTIM went up in flames.', - 'VICTIM burned to death.', - 'VICTIM was burnt to a crisp whilst fighting VICTOR.', - 'VICTIM walked into a fire whilst fighting VICTOR.', - 'VICTIM tried to swim in lava.', - 'VICTIM tried to swim in lava while trying to escape VICTOR.', - 'VICTIM was struck by lightning.', - 'VICTIM was slain by VICTOR.', - 'VICTIM got finished off by VICTOR.', - 'VICTIM was killed by magic.', - 'VICTIM was killed by VICTOR using magic.', - 'VICTIM starved to death.', - 'VICTIM suffocated in a wall.', - 'VICTIM fell out of the world.', - 'VICTIM was knocked into the void by VICTOR.', - 'VICTIM withered away.', - 'VICTIM was pummeled by VICTOR.', - 'VICTIM was fragged by VICTOR.', - 'VICTIM was desynchronized.', - 'VICTIM was wasted.', - 'VICTIM was busted.', - 'VICTIM\'s bones are scraped clean by the desolate wind.', - 'VICTIM has died of dysentery.', - 'VICTIM fainted.', - 'VICTIM is out of usable Pokemon! VICTIM whited out!', - 'VICTIM is out of usable Pokemon! VICTIM blacked out!', - 'VICTIM whited out!', - 'VICTIM blacked out!', - 'VICTIM says goodbye to this cruel world.', - 'VICTIM got rekt.', - 'VICTIM was sawn in half by VICTOR.', - 'VICTIM died. I blame VICTOR.', - 'VICTIM was axe-murdered by VICTOR.', - 'VICTIM\'s melon was split by VICTOR.', - 'VICTIM was slice and diced by VICTOR.', - 'VICTIM was split from crotch to sternum by VICTOR.', - 'VICTIM\'s death put another notch in VICTOR\'s axe.', - 'VICTIM died impossibly!', - 'VICTIM died from VICTOR\'s mysterious tropical disease.', - 'VICTIM escaped infection by dying.', - 'VICTIM played hot-potato with a grenade.', - 'VICTIM was knifed by VICTOR.', - 'VICTIM fell on his sword.', - 'VICTIM ate a grenade.', - 'VICTIM practiced being VICTOR\'s clay pigeon.', - 'VICTIM is what\'s for dinner!', - 'VICTIM was terminated by VICTOR.', - 'VICTIM was shot before being thrown out of a plane.', - 'VICTIM was not invincible.', - 'VICTIM has encountered an error.', - 'VICTIM died and reincarnated as a goat.', - 'VICTOR threw VICTIM off a building.', - 'VICTIM is sleeping with the fishes.', - 'VICTIM got a premature burial.', - 'VICTOR replaced all of VICTIM\'s music with Nickelback.', - 'VICTOR spammed VICTIM\'s email.', - 'VICTOR made VICTIM a knuckle sandwich.', - 'VICTOR slapped VICTIM with pure nothing.', - 'VICTOR hit VICTIM with a small, interstellar spaceship.', - 'VICTIM was quickscoped by VICTOR.', - 'VICTOR put VICTIM in check-mate.', - 'VICTOR RSA-encrypted VICTIM and deleted the private key.', - 'VICTOR put VICTIM in the friendzone.', - 'VICTOR slaps VICTIM with a DMCA takedown request!', - 'VICTIM became a corpse blanket for VICTOR.', - 'Death is when the monsters get you. Death comes for VICTIM.', - 'Cowards die many times before their death. VICTIM never tasted death but once.', - 'VICTIM died of hospital gangrene.', - 'VICTIM got a house call from Doctor VICTOR.', - 'VICTOR beheaded VICTIM.', - 'VICTIM got stoned...by an angry mob.', - 'VICTOR sued the pants off VICTIM.', - 'VICTIM was impeached.', - 'VICTIM was one-hit KO\'d by VICTOR.', - 'VICTOR sent VICTIM to /dev/null.', - 'VICTOR sent VICTIM down the memory hole.' -} - -function slap:action(msg) - - local victor = self.database.users[msg.from.id_str] - local victim = utilities.user_from_message(self, msg, true) - local input = utilities.input(msg.text) - - local victim_name = victim.nickname or victim.first_name or input - local victor_name = victor.nickname or victor.first_name - if not victim_name or victim_name == victor_name then - victim_name = victor_name - victor_name = self.info.first_name - end - - local output = slaps[math.random(#slaps)] - output = output:gsub('VICTIM', victim_name) - output = output:gsub('VICTOR', victor_name) - output = utilities.char.zwnj .. output - - utilities.send_message(self, msg.chat.id, output) - -end - -return slap diff --git a/otouto/plugins/translate.lua b/otouto/plugins/translate.lua deleted file mode 100644 index bcff57e..0000000 --- a/otouto/plugins/translate.lua +++ /dev/null @@ -1,51 +0,0 @@ -local translate = {} - -local HTTPS = require('ssl.https') -local URL = require('socket.url') -local JSON = require('dkjson') -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 - translate.doc = [[``` -]]..config.cmd_pat..[[translate [text] -Translates input or the replied-to message into the bot's language. -```]] -end - -function translate: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, translate.doc, true, msg.message_id, true) - return - end - end - - local url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. config.yandex_key .. '&lang=' .. config.lang .. '&text=' .. URL.escape(input) - - local str, res = HTTPS.request(url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - local jdat = JSON.decode(str) - if jdat.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) - -end - -return translate diff --git a/otouto/plugins/whoami.lua b/otouto/plugins/whoami.lua deleted file mode 100644 index 52b8ae5..0000000 --- a/otouto/plugins/whoami.lua +++ /dev/null @@ -1,51 +0,0 @@ -local whoami = {} - -local utilities = require('otouto.utilities') - -whoami.command = 'whoami' - -function whoami:init(config) - whoami.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('who', true):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 - msg.from.name = utilities.build_name(msg.from.first_name, msg.from.last_name) - end - - 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), msg.from.name, msg.from.id) - else - user = 'You are *%s* `[%s]`,' - user = user:format(msg.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) - -end - -return whoami diff --git a/otouto/plugins/wikipedia.lua b/otouto/plugins/wikipedia.lua deleted file mode 100644 index b15fdbc..0000000 --- a/otouto/plugins/wikipedia.lua +++ /dev/null @@ -1,112 +0,0 @@ -local wikipedia = {} - -local HTTPS = require('ssl.https') -local URL = require('socket.url') -local JSON = require('dkjson') -local utilities = require('otouto.utilities') - -wikipedia.command = 'wiki ' - -function wikipedia:init(config) - wikipedia.triggers = utilities.triggers(self.info.username, config.cmd_pat):t('wikipedia', true):t('wiki', true).table - wikipedia.doc = [[* -]]..config.cmd_pat..[[wiki* __: Gibt Wikipedia-Artikel aus -Alias: ]]..config.cmd_pat..[[wikipedia]] -end - -local get_title = function(search) - for _,v in ipairs(search) do - if not v.snippet:match('steht für') then - return v.title - end - end - return false -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) - 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 - 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://de.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=' - - jstr, res = HTTPS.request(search_url .. URL.escape(input)) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - - jdat = JSON.decode(jstr) - if jdat.query.searchinfo.totalhits == 0 then - utilities.send_reply(self, msg, config.errors.results) - return - end - - local title = get_title(jdat.query.search) - if not title then - utilities.send_reply(self, msg, config.errors.results) - return - end - - local res_url = 'https://de.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 - utilities.send_reply(self, msg, config.errors.connection) - return - end - - local _ - local text = JSON.decode(jstr).query.pages - _, text = next(text) - 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('', '') - 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://de.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) - else - output = '*' .. title:gsub('%(.+%)', '') .. '*\n' .. text:gsub('%[.+%]','') - end - output = output .. '\n[Wikipedia - '..title..'](' .. url:gsub('%)', '\\)') .. ')' - - utilities.send_message(self, msg.chat.id, output, true, nil, true) - -end - -return wikipedia diff --git a/otouto/plugins/xkcd.lua b/otouto/plugins/xkcd.lua deleted file mode 100644 index bd16ef8..0000000 --- a/otouto/plugins/xkcd.lua +++ /dev/null @@ -1,58 +0,0 @@ -local xkcd = {} - -local HTTP = require('socket.http') -local JSON = require('dkjson') -local utilities = require('otouto.utilities') - -xkcd.command = 'xkcd [i]' - -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. -```]] -end - -function xkcd:action(msg, config) - - local jstr, res = HTTP.request('http://xkcd.com/info.0.json') - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local latest = JSON.decode(jstr).num - local strip_num = latest - - local input = utilities.input(msg.text) - if input then - if input == '404' then - utilities.send_message(self, msg.chat.id, '*404*\nNot found.', false, nil, true) - return - elseif tonumber(input) then - if tonumber(input) > latest then - strip_num = latest - else - strip_num = input - end - elseif input == 'r' then - strip_num = math.random(latest) - end - end - - local res_url = 'http://xkcd.com/' .. strip_num .. '/info.0.json' - - jstr, res = HTTP.request(res_url) - if res ~= 200 then - utilities.send_reply(self, msg, config.errors.connection) - return - end - local jdat = JSON.decode(jstr) - - local output = '*' .. jdat.safe_title .. ' (*[' .. jdat.num .. '](' .. jdat.img .. ')*)*\n_' .. jdat.alt:gsub('_', '\\_') .. '_' - - utilities.send_message(self, msg.chat.id, output, false, nil, true) - -end - -return xkcd diff --git a/tg-install.sh b/tg-install.sh deleted file mode 100644 index a78b911..0000000 --- a/tg-install.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# Will download lua-tg and will download and build tg's "test" branch. -# Written for Ubuntu/Debian. If you're running Arch (the only acceptable -# alternative), figure it out yourself. - -echo 'Requesting root privileges to install necessary packages:' -echo 'libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev libjansson-dev libpython-dev make' -sudo apt-get install libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev libjansson-dev libpython-dev make -echo 'Compiling tg, test branch.' -git clone http://github.com/vysheng/tg --recursive -b test -cd tg -./configure -make -echo 'All done! Use ./tg-launch.sh to launch tg. Be sure to log in with your Telegram account.' diff --git a/tg-launch.sh b/tg-launch.sh deleted file mode 100644 index 03c6205..0000000 --- a/tg-launch.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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. - -while true; do - tg/bin/telegram-cli -P 4567 -E - rm ~/.telegram-cli/state - echo 'tg has stopped. ^C to exit.' - sleep 5s -done