1

49 Commits

Author SHA1 Message Date
eleksir
cec2f2f875 Use ->canTalk() instead of ::canTalk() in Message.pm as it is more reliable way to check this 2022-02-03 02:03:50 +03:00
eleksir
a4d0afab5d Use JSON::PP explicitly rather than implicitly 2022-02-03 02:00:06 +03:00
eleksir
75d0e07c6d Fix error handling in canDeleteMessage 2022-02-03 00:42:13 +03:00
eleksir
b503fec769 Fix error handling in canTalk() 2022-02-03 00:29:26 +03:00
eleksir
c9e664b4e5 Update readmes about runtime environment 2022-01-29 18:10:56 +03:00
eleksir
e552ecbf00 Pin versions in cpanfile 2022-01-29 18:06:23 +03:00
eleksir
ea634710f1 In newcommer greeting treat space-char-only first_name or last_name as non-existing 2022-01-29 17:32:25 +03:00
eleksir
e784a78979 Use all types of greet phrases to omit answer if newcommer respond to our greet with quotation 2022-01-29 17:21:04 +03:00
eleksir
b992698b9b Add easter egg phrases in private and group conversations, fix previous addition of private phrases a bit 2021-12-25 15:41:20 +03:00
eleksir
8d21991af6 Put disclaimer about feature freeze and update help message with relevant software version text 2021-12-20 00:01:43 +03:00
eleksir
f223ba04ae Extend weather command list, add !weather, !погода, !погадка, !погодка, also allow oboobs and obutts plugins in private conversation 2021-12-18 17:24:21 +03:00
eleksir
8d35bb2452 Update api with fields from v5.4 and v5.5 2021-12-07 20:11:56 +03:00
eleksir
71a2471725 Couple of grammar and coding style mistakes fixes here and there 2021-12-04 03:10:41 +03:00
eleksir
e93c9f9131 Use different hello-phrases to greet newcommers 2021-12-03 20:25:51 +03:00
eleksir
1abb95622a Fix bunch of grammar mistakes 2021-10-17 01:14:38 +03:00
eleksir
bb399a5051 Add undocumented commands !kde and !кде 2021-10-12 10:54:30 +03:00
eleksir
c5014a8f77 Weather.pm: Hardcode popular russian short city names 2021-10-01 13:23:00 +03:00
eleksir
f83a2d3436 Anek.pm: fix rare case when fetched response is unparsable 2021-08-16 00:03:38 +03:00
eleksir
2134edd841 Implemented !proverb and !пословица commands 2021-08-11 16:54:21 +03:00
eleksir
3dd565c658 Implemented !coin !монетка !roll !dies !кости commands 2021-08-11 13:19:56 +03:00
eleksir
7147b785e7 Add !anek !анек !анекдот commands 2021-07-29 21:26:59 +03:00
eleksir
f1ad32d5ae Add migration scripts and doc.
When I move bot from perl-5.30.0 to perl-5.32.1 I encounter with BDB0091
DB_VERSION_MISMATCH error And it looks like I have to export essential
data on older perl to some portable format and then import it on newer
perl environment.
2021-07-05 16:56:01 +03:00
eleksir
66f64da5c6 Fix incorrect error message when unable to create gamestatdir 2021-07-05 13:54:27 +03:00
eleksir
cf71cd7f6f Apply perlcritic 2021-07-03 11:20:13 +03:00
eleksir
eddbcc6277 Prep for gamestat implementation
* Move artifacts to separate files
+ Implement db seed with artifacts
+ Add gamedata_seed.pl script to seed artifacts to db
2021-07-02 16:03:20 +03:00
eleksir
30fb568de0 Use "от ... до ..." instead of simple dash in Weather.pm 2021-06-28 11:01:40 +03:00
eleksir
9c9e67e0a2 Enhance Weather.pm
- Fix incomplete message when t.min==t.max
- Print t.max - t.min if both of them are < 0
2021-06-28 10:50:53 +03:00
eleksir
47e82449e7 Update present api implementation to botapi v5.3 2021-06-26 10:00:51 +03:00
eleksir
b6ec09c5de Now bot triggers on phrase "кто все эти люди?" correctly even if it mentioned first 2021-06-24 01:21:51 +03:00
eleksir
dca58a3deb Add undocumented voice field to ChatMemberUpdated object 2021-06-12 14:09:40 +03:00
eleksir
6d439a1b44 Implement !horse !лошадка !frog !лягушка !snail !улитка commands 2021-05-06 11:00:44 +03:00
eleksir
096877bee2 Make logging use features of Log::Any and log as it should be done via this module. Cleanup sample config. 2021-05-04 18:45:30 +03:00
eleksir
095cb57a18 Implemented !хелп, !хэлп, answer 'кто все эти люди?' question, hide notext log message to debug lever 2021-05-04 13:30:11 +03:00
eleksir
36c79eb46c Add BotLib changes to all remain scripts in bin dir 2021-05-01 01:29:02 +03:00
eleksir
dd693bf5fb Update Flickr API registration procedure in README_RU.md 2021-05-01 01:08:41 +03:00
eleksir
b8c3f4957c Update objects of Teapot::Bot according Bot API docs. Add missing fields.
+ Add missing fields some of existing objects
* Fix copy-paste errors in Teapot::Bot::Object::PollAnswer
2021-05-01 01:05:31 +03:00
eleksir
ec6c63a954 Add 'text' field to ChatMemberUpdated, looks like they miss to document it 2021-04-30 23:53:51 +03:00
eleksir
62650bd8ad Update to api 5.2 changes (no payments-2.0) 2021-04-30 23:42:54 +03:00
eleksir
8d5b5df194 FortuneToggleList() was misspelled in periodic job so fortune message was not shown 2021-04-26 11:06:16 +03:00
eleksir
0befdf6327 Set log verbosity of CHI::Driver to warn to avoid excessive logging flood 2021-04-25 01:50:01 +03:00
eleksir
8e2414288b Fix some errors of tg api v5.1 transition 2021-04-25 01:34:58 +03:00
eleksir
fbab0a4d88 1-st run fixes after refactor 2021-04-24 16:12:30 +03:00
eleksir
a3f8b43f07 Merge branch 'master' into chi 2021-04-24 11:53:26 +03:00
eleksir
615213ee41 Merge branch 'master' into update_teapot_bot_lib_to_api_v5.1 2021-04-24 11:47:44 +03:00
eleksir
3c721b905f Sometimes xkcd.ru returns redirect to 404, retry to get correct content in this case. 2021-04-22 19:02:08 +03:00
eleksir
8665a6ddda Refactor some parts of bot
- Move most modules to subdirs
- Rename functions according perlcritic guidelines
- Fix and add some bugs
2021-04-22 17:36:50 +03:00
eleksir
729489236f move admin to CHI 2021-04-07 13:43:34 +03:00
eleksir
cec3ed66fb Introduce cachedir config pragma, move karma to CHI
* Move karma from homebrew storage to CHI
* Use cachedir for CHI file storage heap
* Use cachedir in drink.pm
* Use cachedir in weather.pm
2021-04-07 13:39:04 +03:00
eleksir
5d17018490 Update Teapot::Bot to v5.1 api data types, add some debug 2021-04-04 22:44:28 +03:00
69 changed files with 4546 additions and 1357 deletions

9
.gitignore vendored
View File

@@ -1,13 +1,16 @@
_Deparsed_XSubs.pm _Deparsed_XSubs.pm
.idea/** .idea/**
.vscode/** .vscode/**
.vstags
aleesa-telegram-bot.pid aleesa-telegram-bot.pid
data/cache/** data/cache/**
data/config.json data/config.json
data/fortune/** data/fortune/**
data/friday/** data/friday/**
data/karma/** data/gamestat/**
data/telegrambot-brains/**
data/lat/**
data/image/** data/image/**
data/karma/**
data/lat/**
data/proverb/**
data/telegrambot-brains/**
vendor_perl/** vendor_perl/**

View File

@@ -52,7 +52,7 @@ verbose = 8
[-ValuesAndExpressions::ProhibitEmptyQuotes] [-ValuesAndExpressions::ProhibitEmptyQuotes]
# cause we consider following correct and useful: my $var = ''; # cause we consider following correct and useful: my $var = '';
# the recomended use of q{} looks too complex: my $var = q{}; # the recommended use of q{} looks too complex: my $var = q{};
[-ValuesAndExpressions::ProhibitNoisyQuotes] [-ValuesAndExpressions::ProhibitNoisyQuotes]
# Other people, other opinions: "," is better readable than qw(,) # Other people, other opinions: "," is better readable than qw(,)
@@ -92,4 +92,3 @@ lib_sections = NAME | VERSION | DESCRIPTION | AUTHOR | LICENSE AND COPYRIGHT
[-ControlStructures::ProhibitDeepNests] [-ControlStructures::ProhibitDeepNests]
# Have to use it, so fuckoff # Have to use it, so fuckoff

30
Migration.txt Normal file
View File

@@ -0,0 +1,30 @@
Migration procedure.
When you require to perform this procedure?
- When perl version is changed
- When migration to other OS or distribution occurs
Why do i have to perform this procedure?
- Some modules are vendored with bot
- BDB can vary its on-disk format depending on dbd version that linked with
perl interpreter itself and its appropriate module.
How can i guess that i miss to perform this procedure?
Most notable symptom is 'BDB0091 DB_VERSION_MISMATCH' in logs and bot admin settings are lost.
Procedure steps.
On source system perform following steps:
1. stop the bot, to unlock and close all files.
2. make a backup
3. prepare target system and copy bot to target system
On target system perform these steps:
1. remove vendor_perl dir in bot' directory
2. run "bash bootstrap.sh" (no quotes) and wait until it successfully build and
install required modules.
3. run perl bin/cache_import.pl script. It should create cache and exit
silently.
Now it is possible to run bot without settings loss.

View File

@@ -1,29 +1,32 @@
# Aleesa-Telegram-bot - Telegram chatty bot # Aleesa-Telegram-bot - Telegram chatty bot
## Disclaimer
This version - 3 (something) - considered stable. It means that further development of new features kind of frosen.
Most effort will be targeted to future 4-th version development. If you have some ideas or fixes to this version
feel free PR them, they will be merged as soon as being reviewed.
## About ## About
It is based on Perl modules [Telegram::Bot][1] and [Hailo][2] as conversation It is based on Perl modules [Telegram::Bot][1] and [Hailo][2] as conversation generator.
generator.
I have to fork [Telegram::Bot][1] and update it according to features appeared I have to fork [Telegram::Bot][1] and update it according to features appeared in [Telegram Bot API v5][3]. Also,
in [Telegram Bot API v5][3]. Also, I decide to rename it to avoid collision in I decide to rename it to avoid collision in the future, now it is Teapot::Bot and is bundled with bot. When lib becomes
the future, now it is Teapot::Bot and is bundled with bot. When lib becomes
mature enough, maybe I release it separately. mature enough, maybe I release it separately.
Bot config located in **data/config.json**, sample config provided as Bot config located in **data/config.json**, sample config provided as **data/sample_config.json**.
**data/sample_config.json**.
Bot can be run via **bin/aleesa-telegram-bot** and acts as daemon (double fork() Bot can be run via **bin/aleesa-telegram-bot** and acts as daemon (double fork() and detaches stdio).
and detaches stdio).
## Installation ## Installation
In order to run this application, you need to "bootstrap" it - download and build Application have been tested on alpine linux x86_64 3.12, with perl-5.32.1 builtin.
all required dependencies and libraries.
You'll need perl-5.18 or newer, "Development Tools" or similar group of In order to run this application, you need to "bootstrap" it - download and build all required dependencies and
packages, perl, perl-devel, perl-local-lib, perl-app-cpanm, sqlite-devel, libraries.
zlib-devel, openssl-devel, libdb4-devel (Berkeley DB devel), make.
You'll need perl-5.18 or newer, "Development Tools" or similar group of packages, perl, perl-devel, perl-local-lib,
perl-app-cpanm, sqlite-devel, zlib-devel, openssl-devel, libdb4-devel (Berkeley DB devel), make.
After installing required dependencies it is possible to run: After installing required dependencies it is possible to run:
@@ -37,56 +40,49 @@ and all libraries should be downloaded, built, tested and installed.
## Running ## Running
This bot does not [ultilize][4] webhooks, so in order to run it only well This bot does not [ultilize][4] webhooks, so in order to run it only well internet connection is required, no
internet connection is requied, no public-available ip address is needed. public-available ip address is needed.
### Additional Settings to be performed in order to run bot as it designed ### Additional Settings to be performed in order to run bot as it designed
There are some "plugins" that requires registration on thrird party services There are some "plugins" that requires registration on third party services and API keys and secrets in order to work.
and API keys and secrets in order to work.
#### OpenWeatherMap #### OpenWeatherMap
You should [register][5], [login][6] and [create][7] api key. Free plan have You should [register][5], [login][6] and [create][7] api key. Free plan have all required features to run this "plugin".
all required features to run this "plugin".
Once key is created you should put it in **config.json** under openweathermap Once key is created you should put it in **config.json** under openweathermap section as appid. Thats it.
section as appid. Thats it.
It is still impossible to disable this plugin, but "I'm working on it". It is still impossible to disable this plugin, but "I'm working on it".
#### Imgur #### Imgur
This plugin "just exists" and is actually used nowhere around the code. This plugin "just exists" and is actually used nowhere around the code. Anyway, to make use of it besides registering
Anyway, to make use of it besides registering apropriate handler in code, you appropriate handler in code, you should sign in imgur and get access token.
should sign in imgur and get access token.
Actually at this time this "plugin" can be stub-ed with default config values. Actually at this time this "plugin" can be stub-ed with default config values.
Anyway, to fetch access token you should [login][8] and perform [registration][9] Anyway, to fetch access token you should [login][8] and perform [registration][9] of your app. Then you should
of your app. Then you should authorise your app according to [this][10] manual. Or authorize your app according to [this][10] manual. Or [this][11] one.
[this][11] one.
#### Flickr #### Flickr
This "plugin" is really used to implement random picture link retrival plugins. This "plugin" is really used to implement random picture link retrival plugins.
There is helper script in **bin/flickr_init.pl** that helps to get all required keys There is helper script in **bin/flickr_init.pl** that helps to get all required keys and secrets from flickr api.
and secrets from flickr api.
Before using it bot must be bootstrapped via **`bootstrap.sh`**. Before using it bot must be bootstrapped via **`bootstrap.sh`**.
[Login][11] and create an [app][12] in your flickr account. Then *logout* flickr account. [Login][11] and create an [app][12] in your flickr account. Then *logout* flickr account. (It is required in order to
(It is required in order to properly get "verfier" parameter later). properly get "verifier" parameter later).
Put your [app key and secret][13] to **config.json**. Ensure that "verifier" parameter Put your [app key and secret][13] to **config.json**. Ensure that "verifier" parameter in Flickr-related part of config
in Flickr-related part of config either absent or commented out. either absent or commented out.
Run **bin/flickr_init.pl** first time and follow on screen instuctions - copy and Run **bin/flickr_init.pl** first time and follow on screen instructions - copy and paste given link to browser login to
paste given link to browser login to flickr account adn yo'll be redirected to flickr account and you'll be redirected to example.com. Check address bar in browser to get so-called "verifier", put
example.com. Check address bar in browser to get so-called "verifier", put it in it in **config.json**. Then run **bin/flickr_init.pl** again to store API access token in db under data/image directory
**config.json**. Then run **bin/flickr_init.pl** again to store API access token in db (in default config). You're done.
under data/image directory (in default config). You're done.
[1]: https://metacpan.org/pod/Telegram::Bot [1]: https://metacpan.org/pod/Telegram::Bot
[2]: https://metacpan.org/pod/Hailo [2]: https://metacpan.org/pod/Hailo

View File

@@ -1,32 +1,38 @@
# Aleesa-Telegram-bot - Болтливый бот для Telegram # Aleesa-Telegram-bot - Болтливый бот для Telegram
## Дисклеймер
Эта версия бота за номером 3 (с мелочью) стабильная. Сие означает, что дальнейшего развития функций в этой версии не
предполагается. Конечно, возможны исключения, но основной план - переключиться на 4-ю версию, куда планируется
перенести большинство функций 3-й версии.
Если у кого-то возникнут идеи относительно этой версии бота - ставьте PR, я только рад. Т.к. несмотря на то, что
развитие этой версии "слегка" заморожено, поддерживать её планируется как минимум до выхода в свет 4-й версии бота.
## Что это такое ## Что это такое
Это бот основанный на перловых модулях [Telegram::Bot][1] для работы в Это бот, основанный на перловых модулях [Telegram::Bot][1] для работы в мессенджере Telegram и [Hailo][2] для генерации
мессенджере Telegram и [Hailo][2] для генерации "умной" беседы. "умной" беседы.
Для корректной работы пришлось форкнуть [Telegram::Bot][1] чтобы обновить Для корректной работы, пришлось форкнуть [Telegram::Bot][1], чтобы обновить интерфейс API до [Telegram Bot API v5][3].
интерфейс API до [Telegram Bot API v5][3]. Для избежания неоднозначностей модуль Для избежания неоднозначностей, модуль пришлось переименовать и теперь он идёт в составе бота. Возможно, когда модуль
пришлось переименовать и теперь он идёт в составе бота. Возможно, когда модуль отлежится и будет похож на стабильный, он стнет самостоятельным и поселится на metacpan.org
отлежится и будет похож на стабильный, он стнет самостоятельным и поселится на
metacpan.org
Конфиг бота должен лежать в **data/config.json**, пример конфига расположен в Конфиг бота должен лежать в **data/config.json**. Пример конфига расположен в **data/sample_config.json**.
**data/sample_config.json**.
Бота можно запустить через **bin/aleesa-telegram-bot** и он взлетит как юниксовый Бота можно запустить через **bin/aleesa-telegram-bot** и он взлетит, как юниксовый демон (сделает двойной fork() и
демон (сделает двойной fork() и отцепится от stdio). отцепится от stdio).
## Установка ## Установка
Чтобы запустить приложение, его надо "забурстрапить" - загрузить и собрать все Приложение тестировалось на alpine linux x86_64 3.12, со встроенным perl-5.32.1.
необходимые зависимости.
Понадобится perl-5.18 а желательно новее, "Development Tools" или подобная Чтобы запустить приложение, его надо "забурстрапить" - загрузить и собрать все необходимые зависимости.
группа пакетов , perl, perl-devel, perl-local-lib, perl-app-cpanm, sqlite-devel,
zlib-devel, openssl-devel, libdb4-devel (Berkeley DB devel), make.
После установки этих пакетов можно будет запустить: Понадобится perl-5.18, а желательно новее, "Development Tools" или подобная группа пакетов , perl, perl-devel,
perl-local-lib, perl-app-cpanm, sqlite-devel, zlib-devel, openssl-devel, libdb4-devel (Berkeley DB devel), make.
После установки этих пакетов, можно будет запустить:
```bash ```bash
@@ -34,59 +40,54 @@ bash bootstrap.sh
``` ```
и по идее всё что нужно будет выкачено, собрано и разложено куда надо. и, по идее, всё что нужно будет выкачено, собрано и разложено куда надо.
## Запуск и работа ## Запуск и работа
Бот не использует [вебхуки][4], поэтому ничего дополнительного, кроме хорошего Бот не использует [вебхуки][4], поэтому ничего дополнительного, кроме хорошего подключения к интернету ему не нужно.
подключения к интернету ему не нужно.
### Дополнительные настройки ### Дополнительные настройки
Некоторые "плагины" требуют регистрации в сторонних сервисах (API keys, secrets Некоторые "плагины" требуют регистрации в сторонних сервисах (API keys, secrets итд), чтобы бот не сыпал ошибками в
итд), чтобы бот не сыпал ошибками в логи (возможно, в дальнейшем возможно будет логи (возможно, в дальнейшем возможно будет отключить эти плагины).
отключить эти плагины).
#### OpenWeatherMap #### OpenWeatherMap
Для работы погодного плагина надо [заргистрироваться][5], [залогиниться][6] и Для работы погодного плагина, надо [заргистрироваться][5], [залогиниться][6] и [создать][7] api key. Free plan
[создать][7] api key. Free plan предлагает всё необхдимое для работы "плагина". предлагает всё необхдимое для работы "плагина".
Когда api key будет создан, его нужно вписать в **config.json** "секцию" Когда api key будет создан, его нужно вписать в **config.json** "секцию" openweathermap в качестве appid. На этом всё.
openweathermap в качестве appid. На этом всё.
#### Imgur #### Imgur
Этот плагин "просто есть" и его на самом деле ничто и никто не использует. И в Этот плагин "просто есть" и его на самом деле ничто и никто не использует. И в этой связи все настройки плагина можно
этой связи все настройки плагина можно оставить "как есть" в примере конфига. оставить "как есть" в примере конфига.
Тем не менее, если есть большое желание, то можно [залогиниться][8] и Тем не менее, если есть большое желание, то можно [залогиниться][8] и [зарегистрировать][9] бота как приложение, а
[зарегистрировать][9] бота как приложение, а потом разрешить приложению потом разрешить приложению пользоваться API-шкой, как описано в [этом][10] или [этом][11] мануале.
пользоваться API-шкой, как описано в [этом][10] или [этом][11] мануале.
#### Flickr #### Flickr
Этот "плагин" действительно используется для добычи ссылок на картинки в разных Этот "плагин" действительно используется для добычи ссылок на картинки в разных "картиночных" командах бота.
"картиночных" командах бота.
Есть скрипт, который поможет с регистрацией приложения в api Flickr - Есть скрипт, который поможет с регистрацией приложения в api Flickr - **bin/flickr_init.pl**, он достанет все нужные
**bin/flickr_init.pl** он достанет все нужные ключи и секреты к ним. ключи и секреты к ним.
До запуска этого скрипта, бот должен быть забутстраплен через **`bootstrap.sh`**. До запуска этого скрипта, бот должен быть забутстраплен через **`bootstrap.sh`**.
[Залогиньтесь][11] и создайте [app][12] in своём Flicker-овском аккаунте. Далее, [Залогиньтесь][11] и создайте [app][12] in своём Flicker-овском аккаунте. Далее, *разлогиньтесь*, это нужно, чтобы
*разлогиньтесь*, это нужно, чтобы потом проще было достать параметр verifier. потом проще было достать параметр verifier.
Запишите [app key и secret][13] в **config.json**. Удостоверьтесь, что параметр Запишите [app key и secret][13] в **config.json**. Удостоверьтесь, что параметр "verifier" в конфиге либо закоменчен,
"verifier" в конфиге либо закоменчен либо отсутствует. либо отсутствует.
Запустите **bin/flickr_init.pl** и следуте предлагаемым инструкциям - в итоге Запустите **bin/flickr_init.pl** и следуте предлагаемым инструкциям - в итоге будет дана ссылка, которую надо
будет дана ссылка, которую надо скопипастить в браузер и через один-два скопипастить в браузер. По ссылке предложат залогиниться, далее надо будет разрешить приложению просматривать профиль
редиректа вы попадёте на example.com. Из адресной строки браузера надо и, через один-два редиректа, вы попадёте на example.com. Из адресной строки браузера надо скопировать параметр
скопировать параметр "verifier" и вписать его в **config.json**. "verifier" и вписать его в **config.json**.
Чтобы сохранить в бд плагина API access token для нормальной работы "плагина", Чтобы сохранить в бд плагина API access token для нормальной работы "плагина", второй раз запустите
второй раз запустите **bin/flickr_init.pl**. На этом всё. **bin/flickr_init.pl**. На этом всё.
[1]: https://metacpan.org/pod/Telegram::Bot [1]: https://metacpan.org/pod/Telegram::Bot
[2]: https://metacpan.org/pod/Hailo [2]: https://metacpan.org/pod/Hailo

View File

@@ -29,11 +29,12 @@ no Cwd;
use Cwd::utf8 qw (chdir abs_path); use Cwd::utf8 qw (chdir abs_path);
use File::Basename qw (basename dirname); use File::Basename qw (basename dirname);
use File::Path qw (make_path); use File::Path qw (make_path);
use Log::Any qw ($log);
use Log::Any::Adapter; use Log::Any::Adapter;
use Log::Any::For::Std; use Log::Any::For::Std;
use POSIX qw (setgid setuid setsid); use POSIX qw (setgid setuid setsid);
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use telegrambot qw (run_telegrambot); use TelegramBot qw (RunTelegramBot);
sub daemonize; sub daemonize;
sub __fork; sub __fork;
@@ -43,7 +44,8 @@ local $OUTPUT_AUTOFLUSH = 1;
my $runAsUid = $UID; my $runAsUid = $UID;
my $piddir = '/var/run/' . basename (abs_path ($PROGRAM_NAME)); my $piddir = '/var/run/' . basename (abs_path ($PROGRAM_NAME));
my $C = loadConf (); my $C = LoadConf ();
my $loglevel = $C->{'loglevel'} // 'notice';
my $logfile; my $logfile;
if (defined $C->{'log'}) { if (defined $C->{'log'}) {
@@ -92,7 +94,19 @@ if ($UID == 0) {
} }
if (defined $logfile) { if (defined $logfile) {
Log::Any::Adapter->set ('File', $logfile); Log::Any::Adapter->set ('File', $logfile, log_level => $loglevel);
# Some annoying CHI logging events come with category set to CHI::Driver and log_level set to debug.
# Until we use start using Log::Any native features and set whole loglevel accordingly, lets lower
# verbosity paticulary for CHI.
Log::Any::Adapter->set (
{
category => 'CHI::Driver'
},
'File',
$logfile,
log_level => 'warn'
);
} else { } else {
Log::Any::Adapter->set ('File', '/dev/null'); Log::Any::Adapter->set ('File', '/dev/null');
} }
@@ -104,14 +118,24 @@ daemonize ();
my $apath = abs_path $PROGRAM_NAME; my $apath = abs_path $PROGRAM_NAME;
unless (defined $apath) { unless (defined $apath) {
die "[FATAL] Unable to get abs_path: $OS_ERROR\n"; $log->fatal ("[FATAL] Unable to get abs_path: $OS_ERROR\n");
exit 1;
}; };
# write pidfile # write pidfile
my $pidfile = sprintf '%s/%s.pid', $piddir, basename $apath; my $pidfile = sprintf '%s/%s.pid', $piddir, basename $apath;
warn "[INFO] Creating pidfile at $pidfile\n"; $log->info ("[INFO] Creating pidfile at $pidfile");
open my $MYPID, '>', $pidfile or die "[FATAL] Error opening $pidfile: $OS_ERROR\n";
print {$MYPID} $PID or die "[FATAL] Error writing to $pidfile: $OS_ERROR\n"; open my $MYPID, '>', $pidfile or do {
$log->fatal ("[FATAL] Error opening $pidfile: $OS_ERROR");
exit 1;
};
print {$MYPID} $PID or do {
$log->fatal ("[FATAL] Error writing to $pidfile: $OS_ERROR");
exit 1;
};
close $MYPID; ## no critic (InputOutput::RequireCheckedSyscalls close $MYPID; ## no critic (InputOutput::RequireCheckedSyscalls
local $SIG{INT} = \&__signal_handler; local $SIG{INT} = \&__signal_handler;
@@ -121,7 +145,7 @@ local $SIG{TERM} = \&__signal_handler;
$PROGRAM_NAME = abs_path $PROGRAM_NAME; ## no critic (Variables::RequireLocalizedPunctuationVars) $PROGRAM_NAME = abs_path $PROGRAM_NAME; ## no critic (Variables::RequireLocalizedPunctuationVars)
# run bot, at last # run bot, at last
run_telegrambot (); RunTelegramBot ();
if (-f $pidfile) { if (-f $pidfile) {
unlink $pidfile; unlink $pidfile;
@@ -145,7 +169,8 @@ sub __fork {
my $pid = fork; my $pid = fork;
unless (defined $pid) { unless (defined $pid) {
die "[FATAL] Can't fork: $OS_ERROR\n"; $log->fatal ("[FATAL] Can't fork: $OS_ERROR");
exit 1;
} }
if ($pid != 0) { if ($pid != 0) {
@@ -156,7 +181,7 @@ sub __fork {
} }
sub __signal_handler { sub __signal_handler {
warn "[INFO] Caught a signal $OS_ERROR\n"; $log->info ("[INFO] Caught a signal $OS_ERROR");
if (-f $pidfile) { if (-f $pidfile) {
unlink $pidfile; unlink $pidfile;

106
bin/cache_export.pl Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/perl
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use version; our $VERSION = qw (1.0);
my $workdir;
# before we run, change working dir
BEGIN {
use Cwd qw (chdir abs_path);
my @CWD = split /\//xms, abs_path ($PROGRAM_NAME);
if ($#CWD > 1) { $#CWD = $#CWD - 2; }
$workdir = join '/', @CWD;
chdir $workdir;
}
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use CHI;
use CHI::Driver::BerkeleyDB;
use File::Basename qw (dirname);
use File::Path qw (make_path);
use JSON::XS;
use Encode;
use BotLib::Conf qw (LoadConf);
my $c = LoadConf ();
my $cachedir = $c->{cachedir};
my $datadir = dirname $cachedir;
my $export = 0;
my $exportInst = 0;
sub dumpCache {
my $namespace = shift;
my $cacheExportDir = sprintf '%s/cacheexport', $datadir;
unless (-d $cacheExportDir) {
make_path ($cacheExportDir) || die "Unable to create $cacheExportDir: $OS_ERROR\n";
}
my $filename = sprintf '%s/%s', $cacheExportDir, $exportInst;
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => $namespace
);
my $j;
my $writefile = 0;
foreach my $k ($cache->get_keys ()) {
$k = decode ('UTF-8', $k);
$j->{$k} = decode ('UTF-8', $cache->get ($k));
$writefile = 1;
}
if ($writefile) {
say "Dumping $namespace"; ## no critic (InputOutput::RequireCheckedSyscalls)
my $jsonencoder = JSON::XS->new->pretty ();
open (my $FILE, '>', $filename) or die "Unable to dump cache namespace $namespace to $filename: $OS_ERROR\n";
print $FILE $jsonencoder->encode ($j) or die "Unable to dump cache namespace $namespace to $filename: $OS_ERROR\n";;
close $FILE; ## no critic (InputOutput::RequireCheckedSyscalls)
my $EXPORTFD;
if ($export) {
open ($EXPORTFD, '>>', "$cacheExportDir/exportfiles.txt") or die "Unable to open for appending $cacheExportDir/exportfiles.txt: $OS_ERROR\n";
} else {
open ($EXPORTFD, '>', "$cacheExportDir/exportfiles.txt") or die "Unable to create $cacheExportDir/exportfiles.txt: $OS_ERROR\n";
$export = 1;
}
printf ($EXPORTFD "%s,%s\n", $namespace, $filename) or die "Unable to write to $cacheExportDir/exportfiles.txt: $OS_ERROR\n";
close $EXPORTFD; ## no critic (InputOutput::RequireCheckedSyscalls)
$exportInst++;
}
return;
}
opendir (my $DIR, $cachedir) or die "Unable to open $cachedir: $OS_ERROR\n";
while (readdir $DIR) {
if (/^BotLib\+3a\+3aAdmin_censor_(.+)\.db$/) {
dumpCache ("BotLib::Admin_censor_$1");
} elsif (/^BotLib\+3a\+3aAdmin_fortune.db$/) {
dumpCache ('BotLib::Admin_fortune');
} elsif (/BotLib\+3a\+3aAdmin_plugin_(.+)\.db$/) {
dumpCache ("BotLib::Admin_plugin_$1");
} elsif (/BotLib\+3a\+3aAdmin_(.+)\.db$/) {
dumpCache ("BotLib::Admin_$1");
} elsif (/^BotLib\+3a\+3aImage\+3a\+3aFlickr\.db$/) {
dumpCache ('BotLib::Image::Flickr');
} elsif (/^BotLib\+3a\+3aKarma_(.+)\.db$/) {
dumpCache ("BotLib::Karma_$1");
}
}
closedir $DIR;
exit 0;

86
bin/cache_import.pl Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/perl
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use version; our $VERSION = qw (1.0);
my $workdir;
# before we run, change working dir
BEGIN {
use Cwd qw (chdir abs_path);
my @CWD = split /\//xms, abs_path ($PROGRAM_NAME);
if ($#CWD > 1) { $#CWD = $#CWD - 2; }
$workdir = join '/', @CWD;
chdir $workdir;
}
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use CHI;
use CHI::Driver::BerkeleyDB;
use File::Basename qw (dirname);
use File::Path qw (make_path);
use JSON::XS;
use Encode;
use BotLib::Conf qw (LoadConf);
my $c = LoadConf ();
my $cachedir = $c->{cachedir};
my $datadir = dirname $cachedir;
my $export = 0;
sub createCache {
my $namespace = shift;
my $filename = shift;
say "Creating $namespace from $filename"; ## no critic (InputOutput::RequireCheckedSyscalls)
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => $namespace
);
open my $FH, '<', $filename or die "Unable to open $filename: $OS_ERROR\n";
binmode $FH;
my $len = (stat $filename) [7];
my $json;
use bytes;
my $readlen = read $FH, $json, $len;
no bytes;
unless ($readlen) {
close $FH; ## no critic (InputOutput::RequireCheckedSyscalls
die "Unable to read $filename: $OS_ERROR\n";
}
if ($readlen != $len) {
close $FH; ## no critic (InputOutput::RequireCheckedSyscalls
die "File $filename is $len bytes on disk, but we read only $readlen bytes\n";
}
close $FH; ## no critic (InputOutput::RequireCheckedSyscalls
my $j = JSON::XS->new->utf8->relaxed;
my $dump = $j->decode ($json);
foreach my $key (keys (%{$dump})) {
$cache->set ($key, $dump->{$key}, 'never');
}
return;
}
open (my $FH, '<', "$datadir/cacheexport/exportfiles.txt") or die "Unable to open $datadir/cacheexport/exportfiles.txt: $OS_ERROR\n";
while (<$FH>) {
chomp;
my ($namespace, $filename) = split /\,/, $_, 2;
createCache ($namespace, $filename);
}
close $FH; ## no critic (InputOutput::RequireCheckedSyscalls)
exit 0;

View File

@@ -20,9 +20,9 @@ BEGIN {
} }
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5"); use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use flickr qw (flickr_by_tags); use BotLib::Image::Flickr qw (FlickrByTags);
my $url = flickr_by_tags ('bird,owl'); my $url = FlickrByTags ('bird,owl');
if ($url) { if ($url) {
say $url; ## no critic (InputOutput::RequireCheckedSyscalls) say $url; ## no critic (InputOutput::RequireCheckedSyscalls)

View File

@@ -20,9 +20,9 @@ BEGIN {
} }
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5"); use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use flickr qw (flickr_init); use BotLib::Image::Flickr qw (FlickrInit);
if (flickr_init ()) { if (FlickrInit ()) {
exit 0; exit 0;
} else { } else {
exit 1; exit 1;

View File

@@ -20,9 +20,9 @@ BEGIN {
} }
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5"); use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use flickr qw (flickr_test_login); use BotLib::Image::Flickr qw (FlickrTestLogin);
if (flickr_test_login ()) { if (FlickrTestLogin ()) {
exit 0; exit 0;
} else { } else {
exit 1; exit 1;

View File

@@ -20,9 +20,7 @@ BEGIN {
} }
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5"); use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use fortune qw (seed); use BotLib::Fortune qw (Seed);
use Carp;
local $SIG{__DIE__} = sub { Carp::confess @_ };
seed (); Seed ();
exit 0; exit 0;

View File

@@ -20,7 +20,7 @@ BEGIN {
} }
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5"); use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use friday qw (seed); use BotLib::Friday qw (Seed);
seed (); Seed ();
exit 0; exit 0;

26
bin/gamedata_seed.pl Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/perl
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use version; our $VERSION = qw (1.0);
my $workdir;
# before we run, change working dir
BEGIN {
use Cwd qw (chdir abs_path);
my @CWD = split /\//xms, abs_path ($PROGRAM_NAME);
if ($#CWD > 1) { $#CWD = $#CWD - 2; }
$workdir = join '/', @CWD;
chdir $workdir;
}
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use BotLib::Gamestat qw (SeedArtifacts);
SeedArtifacts ();
exit 0;

26
bin/proverb_seed.pl Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/perl
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use version; our $VERSION = qw (1.0);
my $workdir;
# before we run, change working dir
BEGIN {
use Cwd qw (chdir abs_path);
my @CWD = split /\//xms, abs_path ($PROGRAM_NAME);
if ($#CWD > 1) { $#CWD = $#CWD - 2; }
$workdir = join '/', @CWD;
chdir $workdir;
}
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use BotLib::Proverb qw (Seed);
Seed ();
exit 0;

View File

@@ -20,7 +20,7 @@ BEGIN {
} }
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5"); use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use lat qw (train); use BotLib::Lat qw (Train);
train (); Train ();
exit 0; exit 0;

View File

@@ -1,31 +1,39 @@
requires 'Carp'; ## no critic (TestingAndDebugging::RequireUseStrict,Modules::RequireVersionVar)
requires 'CHI'; ## no critic (TestingAndDebugging::RequireUseWarnings,Modules::RequireExplicitPackage)
requires 'CHI::Driver::BerkeleyDB'; ## no critic (Modules::RequireEndWithOne)
requires 'Cwd::utf8'; requires 'Carp', '==1.50';
requires 'DateTime'; requires 'CHI', '==0.61';
requires 'DB_File'; requires 'CHI::Driver::BerkeleyDB', '==0.05';
requires 'SQLite_File'; requires 'Cwd::utf8', '==0.011';
requires 'Digest::HMAC_SHA1'; requires 'DateTime', '==1.55';
requires 'Digest::MD5'; requires 'DB_File', '==1.856';
requires 'Data::Dumper'; requires 'SQLite_File', '==0.1006';
requires 'Digest::SHA'; requires 'DBI', '==1.643';
requires 'File::Basename'; requires 'DBD::SQLite', '==1.70';
requires 'File::Path'; requires 'Digest::HMAC_SHA1', '==1.04';
requires 'Hailo'; requires 'Digest::MD5', '==2.58';
requires 'HTML::TokeParser'; requires 'Data::Dumper', '==2.183';
requires 'IO::Socket::SSL'; requires 'Digest::SHA', '==6.02';
requires 'JSON'; requires 'File::Basename', '==2.85';
requires 'JSON::XS'; requires 'File::Path', '==2.18';
requires 'List::Util'; requires 'Hailo', '==0.75';
requires 'local::lib'; requires 'HTML::TokeParser', '==3.76';
requires 'Log::Any'; requires 'IO::Socket::SSL', '==2.074';
requires 'Log::Any::Adapter'; requires 'JSON', '==4.05';
requires 'Log::Any::For::Std'; requires 'JSON::PP', '==4.07';
requires 'Math::Random::Secure'; requires 'JSON::XS', '==4.03';
requires 'Mojo::Base'; requires 'List::Util', '==1.60';
requires 'Mojo::IOLoop'; requires 'local::lib', '==2.000028';
requires 'Mojo::UserAgent'; requires 'Log::Any', '==1.710';
requires 'Mojo::UserAgent::Cached'; requires 'Log::Any::Adapter', '==1.710';
requires 'Text::Fuzzy'; requires 'Log::Any::For::Std', '==0.05';
requires 'URI::Encode::XS'; requires 'Math::Random::Secure', '==0.080001';
requires 'URI::URL'; requires 'Mojolicious', '==9.22';
requires 'Mojo::Base', '0';
requires 'Mojo::IOLoop', '0';
requires 'Mojo::UserAgent', '0';
requires 'Mojo::UserAgent::Cached', '==1.18';
requires 'String::Random', '==0.32';
requires 'Text::Fuzzy', '==0.29';
requires 'URI::Encode::XS', '==0.11';
requires 'URI::URL', '==5.10';

View File

@@ -0,0 +1,54 @@
амулет Мерлина
библиотеку Ивана Грозного
деревянную плошку
диплока
доски Ноева Ковчега
древний презерватив из бараньей кишки
икону
инсталятор Windows 3.11
истлевший гроб
каменный топор
клад со златом-серебром
кривой нож
ледяную глыбу
лунный грунт
мамонта
мамонтёнка
метеорит
мумию фараона Тутанхамона
носки прапорщика (поставит в угол)
обломки грузинской керамики
птеродактиля
рубль с Лениным
рыцарские доспехи
синий мох
скелет
скелет собаки по кличке Беня
тиранозавтра
тотем индейцев
трубу с туркменским газом
скорбь еврейского народа
залежи активированного угля
не очень новый завет
мать леннарта поттеринга
руководство "начинающему археологу"
слово о полку игореве и дело о полку игореве, вместе с игорем
телепрограмму канала A-one
могилу искателя могил
заплесневевший кусок хлеба
преемник радиоприемника
глушитель от ВАЗ-2106
чувство удовлетворения
крота
воспоминания о былой любви
грязистор
египетское мумиё
окаменелый камень
кладку яиц рептилоида
яму
чеснок и стрелы
доллар по 30
несмазанные скрижали
четырелобита
чучело тролля
целое ничего

47
data/fisher_artifacts.txt Normal file
View File

@@ -0,0 +1,47 @@
семикрылого чешуйчатого пятихуя
разумного конденсатора
сальмонеллу
селёдку
желтоперого тунца
розовую сальмонеллу
голавля
барбуса
окуня
верхоплавку
коричневую форель
полярника
плотву
зубатку
солнечную рыбу
старую шину
скользкую корягу
лампу джина
любовное послание в бутылке
старое бревно
резиновый сапог
труп
Лох-Несское чудовище
старую рыбацкую приманку
кусок Титаника
обломок Атлантиды
кита
кальмара
дельфина
ската
подводную лодку
медузу
морскую звезду
электрического угря
большую белую акулу
аквалангиста
монстра
вируса
сырую пачку сигарет
водоросли
якорь от лодки
водяного
русалку
палтуса
труп собаки по кличке Беня
олигофрена Проню
огеймера Фишкина

1719
data/proverb_src/proverb.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,10 @@
{ {
"admin" : { # Cachedir used not only for caches but for some settings and stats too.
# store per-channel (per-conversation) settings "cachedir" : "data/cache",
"dir" : "data/admin"
"gamestat" : {
"dir" : "data",
"gamestatdir" : "data/gamestat"
}, },
"fortune" : { "fortune" : {
@@ -9,57 +12,62 @@
"srcdir" : "data/fortune_src" "srcdir" : "data/fortune_src"
}, },
"proverb" : {
"dir" : "data/proverb",
"srcdir" : "data/proverb_src"
},
"friday" : { "friday" : {
"dir" : "data/friday", "dir" : "data/friday",
"src" : "data/friday.txt" "src" : "data/friday.txt"
}, },
"image": { "image": {
"dir" : "data/image",
"imgur" : { "imgur" : {
# see README.md about these settings - auth'ed search settings # See README.md about these settings - auth'ed search settings.
"client-id" : "imgur api client id", "client-id" : "imgur api client id",
"secret" : "secret for client id", "secret" : "secret for client id",
"access_token" : "access token for querying imgur api", "access_token" : "access token for querying imgur api",
"refresh_token" : "token for refreshing access token" "refresh_token" : "token for refreshing access token"
}, },
"flickr" : { "flickr" : {
# see README.md about these settings - auth'ed search settings # See README.md about these settings - auth'ed search settings.
"consumer_key" : "md5-like_hash", "consumer_key" : "md5-like_hash",
"consumer_secret" : "some_hash-like_string", "consumer_secret" : "some_hash-like_string",
"verifier": "some_hash-like_string" "verifier": "some_hash-like_string"
} }
}, },
"karma": {
"dir": "data/karma"
},
"lat" : { "lat" : {
"brain": "data/lat/brain.sqlite", "brain": "data/lat/brain.sqlite",
"src": "data/lat.txt" "src": "data/lat.txt"
}, },
# required for debugging reasons, if commented out or not exist, no debug logging appears at all # Required for debugging reasons, if commented out or not exist, no debug logging appears at all.
"log" : "/var/log/aleesa-telegram-bot/debug.log", "log" : "/var/log/aleesa-telegram-bot/aleesa.log",
# openwatermap plugin # Verbosity can be trace, debug, info (inform), notice, warning (warn), error (err), critical (crit, fatal), alert, emergency.
# Currently implemented debug, info, notice, warn, error, fatal. Notice is default logging level.
"loglevel": "notice",
# Openwatermap plugin.
"openweathermap" : { "openweathermap" : {
"appid" : "md5sum-like_string", "appid" : "md5sum-like_string",
# result cache, stores data for 4 hours
"cachedir" : "data/cache",
}, },
# stuff for telegram itself # Stuff for telegram itself.
"telegrambot" : { "telegrambot" : {
# directory with per-conversation "brains"
# Directory with per-conversation "brains".
"braindir" : "data/telegrambot-brains", "braindir" : "data/telegrambot-brains",
# prefix-sign for all commands. in many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!"
# in telegram commands begin with "/" on the start of first (and only) line. # Prefix-sign for all commands. In many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!".
# In telegram commands begin with "/" on the start of first (and only) line. In case of this bot you can define what to use.
"csign" : "!", "csign" : "!",
"token" : "Long_string_given_by_@BotFather" "token" : "Long_string_given_by_@BotFather"
}, },
# unprivileged user to run this bot from # Unprivileged user to run this bot from.
"user" : "unit", "user" : "unit",
} }

View File

@@ -1,4 +1,4 @@
package botlib; package BotLib;
# store here utility functions that are not protocol-specified # store here utility functions that are not protocol-specified
use 5.018; use 5.018;
@@ -10,30 +10,34 @@ use English qw ( -no_match_vars );
use Carp qw (carp); use Carp qw (carp);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use admin qw (@forbiddenMessageTypes getForbiddenTypes addForbiddenType delForbiddenType listForbidden fortune_toggle fortune_status plugin_toggle plugin_status pluginEnabled); use BotLib::Admin qw (@ForbiddenMessageTypes GetForbiddenTypes AddForbiddenType
use archeologist qw (dig); DelForbiddenType ListForbidden FortuneToggle FortuneStatus
use buni qw (buni); PluginToggle PluginStatus PluginEnabled);
use drink qw (drink); use BotLib::Anek qw (Anek);
use fisher qw (fish); use BotLib::Archeologist qw (Dig);
use fortune qw (fortune); use BotLib::Buni qw (Buni);
use friday qw (friday); use BotLib::Drink qw (Drink);
use image qw (kitty fox oboobs obutts rabbit owl); use BotLib::Fisher qw (Fish);
use karma qw (karmaSet karmaGet); use BotLib::Fortune qw (Fortune);
use lat qw (latAnswer); use BotLib::Friday qw (Friday);
use monkeyuser qw (monkeyuse); use BotLib::Image qw (Kitty Fox Oboobs Obutts Rabbit Owl Frog Horse Snail);
use util qw (trim); use BotLib::Karma qw (KarmaSet KarmaGet);
use weather qw (weather); use BotLib::Lat qw (Lat);
use xkcd qw (xkcd); use BotLib::Monkeyuser qw (Monkeyuser);
use BotLib::Proverb qw (Proverb);
use BotLib::Util qw (trim);
use BotLib::Weather qw (Weather);
use BotLib::Xkcd qw (Xkcd);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (randomCommonPhrase command highlight botsleep isCensored); our @EXPORT_OK = qw (RandomCommonPhrase Command Highlight BotSleep IsCensored);
my $c = loadConf (); my $c = LoadConf ();
my $csign = $c->{telegrambot}->{csign}; my $csign = $c->{telegrambot}->{csign};
sub randomCommonPhrase { sub RandomCommonPhrase {
my @myphrase = ( my @myphrase = (
'Так, блядь...', 'Так, блядь...',
'*Закатывает рукава* И ради этого ты меня позвал?', '*Закатывает рукава* И ради этого ты меня позвал?',
@@ -41,7 +45,7 @@ sub randomCommonPhrase {
'Повтори свой вопрос, не поняла', 'Повтори свой вопрос, не поняла',
'Выйди и зайди нормально', 'Выйди и зайди нормально',
'Я подумаю', 'Я подумаю',
'Даже не знаю что на это ответить', 'Даже не знаю, что на это ответить',
'Ты упал такие вопросы девочке задавать?', 'Ты упал такие вопросы девочке задавать?',
'Можно и так, но не уверена', 'Можно и так, но не уверена',
'А как ты думаешь?', 'А как ты думаешь?',
@@ -51,13 +55,13 @@ sub randomCommonPhrase {
return $myphrase[irand ($#myphrase + 1)]; return $myphrase[irand ($#myphrase + 1)];
} }
sub command { sub Command {
my $self = shift; my $self = shift;
my $msg = shift; my $msg = shift;
my $text = shift; my $text = shift;
my $chatid = shift; my $chatid = shift;
my $reply; my $reply;
my ($userid, $username, $fullname, $highlight, $visavi) = highlight ($msg); my ($userid, $username, $fullname, $highlight, $visavi) = Highlight ($msg);
if (substr ($text, 1) eq 'ping') { if (substr ($text, 1) eq 'ping') {
$reply = 'Pong.'; $reply = 'Pong.';
@@ -67,31 +71,81 @@ sub command {
$reply = 'Wat?'; $reply = 'Wat?';
} elsif (substr ($text, 1) eq 'понг') { } elsif (substr ($text, 1) eq 'понг') {
$reply = 'Шта?'; $reply = 'Шта?';
} elsif (substr ($text, 1) eq 'coin' || substr ($text, 1) eq 'монетка') {
if (rand (101) < 0.016) {
$reply = 'ребро';
} else {
if (irand (2) == 0) {
if (irand (2) == 0) {
$reply = 'орёл';
} else {
$reply = 'аверс';
}
} else {
if (irand (2) == 0) {
$reply = 'решка';
} else {
$reply = 'реверс';
}
}
}
} elsif (substr ($text, 1) eq 'roll' || substr ($text, 1) eq 'dice' || substr ($text, 1) eq 'кости') {
$reply = sprintf 'На первой кости выпало %d, а на второй — %d.', irand (6) + 1, irand (6) + 1;
} elsif (substr ($text, 1) eq 'ver' || substr ($text, 1) eq 'version' || substr ($text, 1) eq 'версия') { } elsif (substr ($text, 1) eq 'ver' || substr ($text, 1) eq 'version' || substr ($text, 1) eq 'версия') {
$reply = 'Версия: Нуль.Чего-то_там.Чего-то_там'; $reply = 'Версия: Три.Чего-то_там.С_мелочью';
} elsif (substr ($text, 1) eq 'anek' || substr ($text, 1) eq 'анек' || substr ($text, 1) eq 'анекдот') {
$msg->typing ();
$reply = "```\n" . Anek () . "\n```";
sleep (irand (2) + 1);
$msg->replyMd ($reply);
return;
} elsif (length ($text) >= 2 && (substr ($text, 1, 2) eq 'w ' || substr ($text, 1, 2) eq 'п ')) { } elsif (length ($text) >= 2 && (substr ($text, 1, 2) eq 'w ' || substr ($text, 1, 2) eq 'п ')) {
my $city = substr $text, 3; my $city = substr $text, 3;
$reply = weather $city; $reply = Weather $city;
} elsif (substr ($text, 1, 7) eq 'погода ') {
my $city = substr $text, 7;
$reply = Weather $city;
} elsif ((substr ($text, 1, 8) eq 'погодка ') || (substr ($text, 1, 8) eq 'погадка ') || (substr ($text, 1, 8) eq 'weather ')){
my $city = substr $text, 8;
$reply = Weather $city;
} elsif (substr ($text, 1) eq 'buni') { } elsif (substr ($text, 1) eq 'buni') {
$msg->typing (); $msg->typing ();
$reply = buni (); $reply = Buni ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'monkeyuser') { } elsif (substr ($text, 1) eq 'monkeyuser') {
$msg->typing (); $msg->typing ();
$reply = monkeyuser (); $reply = Monkeyuser ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'cat' || substr ($text, 1) eq 'кис') { } elsif (substr ($text, 1) eq 'cat' || substr ($text, 1) eq 'кис') {
$msg->typing (); $msg->typing ();
$reply = kitty (); $reply = Kitty ();
sleep (irand (2) + 1);
$msg->replyMd ($reply);
return;
} elsif (substr ($text, 1) eq 'frog' || substr ($text, 1) eq 'toad' || substr ($text, 1) eq 'лягушка') {
$msg->typing ();
$reply = Frog ();
sleep (irand (2) + 1);
$msg->replyMd ($reply);
return;
} elsif (substr ($text, 1) eq 'horse' || substr ($text, 1) eq 'лошадка') {
$msg->typing ();
$reply = Horse ();
sleep (irand (2) + 1);
$msg->replyMd ($reply);
return;
} elsif (substr ($text, 1) eq 'snail' || substr ($text, 1) eq 'улитка') {
$msg->typing ();
$reply = Snail ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'lat' || substr ($text, 1) eq 'лат') { } elsif (substr ($text, 1) eq 'lat' || substr ($text, 1) eq 'лат') {
$reply = latAnswer (); $reply = Lat ();
} elsif ( } elsif (
(length ($text) >= 6 && (substr ($text, 1, 6) eq 'karma ' || substr ($text, 1, 6) eq 'карма ')) || (length ($text) >= 6 && (substr ($text, 1, 6) eq 'karma ' || substr ($text, 1, 6) eq 'карма ')) ||
substr ($text, 1) eq 'karma' || substr ($text, 1) eq 'карма' substr ($text, 1) eq 'karma' || substr ($text, 1) eq 'карма'
@@ -106,40 +160,40 @@ sub command {
$mytext = ''; $mytext = '';
} }
$reply = karmaGet ($chatid, $mytext); $reply = KarmaGet ($chatid, $mytext);
} elsif (substr ($text, 1) eq 'xkcd') { } elsif (substr ($text, 1) eq 'xkcd') {
$msg->typing (); $msg->typing ();
$reply = xkcd (); $reply = Xkcd ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'fox' || substr ($text, 1) eq 'лис') { } elsif (substr ($text, 1) eq 'fox' || substr ($text, 1) eq 'лис') {
$msg->typing (); $msg->typing ();
$reply = fox (); $reply = Fox ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'rabbit' || substr ($text, 1) eq 'bunny' || substr ($text, 1) eq 'кролик') { } elsif (substr ($text, 1) eq 'rabbit' || substr ($text, 1) eq 'bunny' || substr ($text, 1) eq 'кролик') {
$msg->typing (); $msg->typing ();
$reply = rabbit (); $reply = Rabbit ();
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'owl' || substr ($text, 1) eq 'сова') { } elsif (substr ($text, 1) eq 'owl' || substr ($text, 1) eq 'сова') {
$msg->typing (); $msg->typing ();
$reply = owl (); $reply = Owl ();
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif ( } elsif (
substr ($text, 1) eq 'tits' || substr ($text, 1) eq 'tits' ||
substr ($text, 1) eq 'boobs' || substr ($text, 1) eq 'boobs' ||
substr ($text, 1) eq 'tities' || substr ($text, 1) eq 'tities' ||
substr ($text, 1) eq 'boobies' || substr ($text, 1) eq 'boobies' ||
substr ($text, 1) eq 'сиси' || substr ($text, 1) eq 'сиси' ||
substr ($text, 1) eq 'сисечки' substr ($text, 1) eq 'сисечки'
) { ) {
if (pluginEnabled $chatid, 'oboobs') { if (PluginEnabled $chatid, 'oboobs') {
$msg->typing (); $msg->typing ();
$reply = oboobs (); $reply = Oboobs ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
} }
@@ -147,48 +201,63 @@ sub command {
return; return;
} elsif ( } elsif (
substr ($text, 1) eq 'butt' || substr ($text, 1) eq 'butt' ||
substr ($text, 1) eq 'booty' || substr ($text, 1) eq 'booty' ||
substr ($text, 1) eq 'ass' || substr ($text, 1) eq 'ass' ||
substr ($text, 1) eq 'попа' || substr ($text, 1) eq 'попа' ||
substr ($text, 1) eq 'попка' substr ($text, 1) eq 'попка'
) { ) {
if (pluginEnabled $chatid, 'obutts') { if (PluginEnabled $chatid, 'obutts') {
$msg->typing (); $msg->typing ();
$reply = obutts (); $reply = Obutts ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
} }
return; return;
} elsif (substr ($text, 1) eq 'friday' || substr ($text, 1) eq 'пятница') { } elsif (substr ($text, 1) eq 'friday' || substr ($text, 1) eq 'пятница') {
$reply = friday (); $reply = Friday ();
} elsif (substr ($text, 1) eq 'proverb' || substr ($text, 1) eq 'пословица') {
$reply = Proverb ();
} elsif (substr ($text, 1) eq 'fortune' || substr ($text, 1) eq 'фортунка' || substr ($text, 1) eq 'f' || substr ($text, 1) eq 'ф') { } elsif (substr ($text, 1) eq 'fortune' || substr ($text, 1) eq 'фортунка' || substr ($text, 1) eq 'f' || substr ($text, 1) eq 'ф') {
$reply = sprintf "```\n%s\n```\n", trim (fortune ()); $reply = sprintf "```\n%s\n```\n", trim (Fortune ());
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'drink' || substr ($text, 1) eq 'праздник') { } elsif (substr ($text, 1) eq 'drink' || substr ($text, 1) eq 'праздник') {
$msg->typing (); $msg->typing ();
$reply = drink (); $reply = Drink ();
sleep (irand (2) + 1); sleep (irand (2) + 1);
} elsif (substr ($text, 1) eq 'dig' || substr ($text, 1) eq 'копать') { } elsif (substr ($text, 1) eq 'dig' || substr ($text, 1) eq 'копать') {
$msg->typing (); $msg->typing ();
$reply = dig $highlight; $reply = Dig $highlight;
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'fish' || substr ($text, 1) eq 'fishing' || substr ($text, 1) eq 'рыба' || substr ($text, 1) eq 'рыбка' || substr ($text, 1) eq 'рыбалка' ) { } elsif (substr ($text, 1) eq 'fish' || substr ($text, 1) eq 'fishing' || substr ($text, 1) eq 'рыба' || substr ($text, 1) eq 'рыбка' || substr ($text, 1) eq 'рыбалка' ) {
$msg->typing (); $msg->typing ();
$reply = fish $highlight; $reply = Fish $highlight;
sleep (irand (2) + 1); sleep (irand (2) + 1);
$msg->replyMd ($reply); $msg->replyMd ($reply);
return; return;
} elsif (substr ($text, 1) eq 'хэлп' || substr ($text, 1) eq 'halp') {
$reply = 'HALP!!!11';
} elsif (substr ($text, 1) eq 'kde' || substr ($text, 1) eq 'кде') {
my @phrases = (
'Нет, я не буду поднимать вам плазму.',
'Повторяйте эту мантру по утрам не менее 5 раз: "Плазма не падает." И, возможно, она перестанет у вас падать.',
'Плазма не падает, она просто выходит отдохнуть.',
'Плазма это не агрегатное состояние, это суперпозиция. Во время работы она находится сразу в жидком, мягком и тёплом состояниях.',
);
$reply = $phrases[irand ($#phrases + 1)];
} elsif (substr ($text, 1) eq 'help' || substr ($text, 1) eq 'помощь') { } elsif (substr ($text, 1) eq 'help' || substr ($text, 1) eq 'помощь') {
$reply = << "MYHELP"; $reply = << "MYHELP";
``` ```
${csign}help | ${csign}помощь - список команд ${csign}help | ${csign}помощь - список команд
${csign}anek | ${csign}анек | ${csign}анекдот - рандомный анекдот с anekdot.ru
${csign}buni - рандомный стрип hapi buni ${csign}buni - рандомный стрип hapi buni
${csign}bunny | ${csign}rabbit | ${csign}кролик - кролик ${csign}bunny | ${csign}rabbit | ${csign}кролик - кролик
${csign}cat | ${csign}кис - кошечка ${csign}cat | ${csign}кис - кошечка
${csign}coin | ${csign}монетка - подбросить монетку - орёл или решка?
${csign}dig | ${csign}копать - заняться археологией ${csign}dig | ${csign}копать - заняться археологией
${csign}drink | ${csign}праздник - какой сегодня праздник? ${csign}drink | ${csign}праздник - какой сегодня праздник?
${csign}fish | ${csign}рыба | ${csign}рыбка - порыбачить ${csign}fish | ${csign}рыба | ${csign}рыбка - порыбачить
@@ -197,12 +266,21 @@ ${csign}f | ${csign}ф - рандомная фраза из
${csign}fortune | ${csign}фортунка - рандомная фраза из сборника цитат fortune_mod ${csign}fortune | ${csign}фортунка - рандомная фраза из сборника цитат fortune_mod
${csign}fox | ${csign}лис - лисичка ${csign}fox | ${csign}лис - лисичка
${csign}friday | ${csign}пятница - а не пятница ли сегодня? ${csign}friday | ${csign}пятница - а не пятница ли сегодня?
${csign}frog | ${csign}лягушка - лягушка
${csign}horse | ${csign}лошадка - лошадка
${csign}lat | ${csign}лат - сгенерить фразу из крылатых латинских выражений ${csign}lat | ${csign}лат - сгенерить фразу из крылатых латинских выражений
${csign}monkeyuser - рандомный стрип MonkeyUser ${csign}monkeyuser - рандомный стрип MonkeyUser
${csign}owl | ${csign}сова - сова ${csign}owl | ${csign}сова - сова
${csign}proverb | ${csign}пословица - рандомная русская пословица
${csign}ping | ${csign}пинг - попинговать бота ${csign}ping | ${csign}пинг - попинговать бота
${csign}roll | ${csign}dice | ${csign}кости - бросить кости
${csign}snail | ${csign}улитка - улитка
${csign}ver | ${csign}version | ${csign}версия - что-то про версию ПО ${csign}ver | ${csign}version | ${csign}версия - что-то про версию ПО
${csign}w город | ${csign}п город - погода в указанном городе ${csign}w город | ${csign}п город - погода в указанном городе
${csign}weather город - погода в указанном городе
${csign}погода город - погода в указанном городе
${csign}погодка город - погода в указанном городе
${csign}погадка город - погода в указанном городе
${csign}xkcd - рандомный стрип с сайта xkcd.ru ${csign}xkcd - рандомный стрип с сайта xkcd.ru
${csign}karma фраза | ${csign}карма фраза - посмотреть карму фразы ${csign}karma фраза | ${csign}карма фраза - посмотреть карму фразы
фраза-- | фраза++ - убавить или добавить карму фразе фраза-- | фраза++ - убавить или добавить карму фразе
@@ -255,50 +333,50 @@ MYADMIN
my ($msgType, $toggle) = split /\s/, $args, 2; my ($msgType, $toggle) = split /\s/, $args, 2;
if (defined $toggle) { if (defined $toggle) {
foreach (@forbiddenMessageTypes) { foreach (@ForbiddenMessageTypes) {
if ($msgType eq $_) { if ($msgType eq $_) {
if ($toggle == 1) { if ($toggle == 1) {
addForbiddenType ($chatid, $msgType); AddForbiddenType ($chatid, $msgType);
$reply = "Теперь сообщения с $msgType будут автоматически удаляться"; $reply = "Теперь сообщения с $msgType будут автоматически удаляться";
} elsif ($toggle == 0) { } elsif ($toggle == 0) {
delForbiddenType ($chatid, $msgType); DelForbiddenType ($chatid, $msgType);
$reply = "Теперь сообщения с $msgType будут оставаться"; $reply = "Теперь сообщения с $msgType будут оставаться";
} }
} }
} }
} }
} else { } else {
$reply = listForbidden ($chatid); $reply = ListForbidden ($chatid);
} }
} elsif ($cmd eq 'fortune' || $cmd eq 'фортунка') { } elsif ($cmd eq 'fortune' || $cmd eq 'фортунка') {
if (defined $args) { if (defined $args) {
if ($args == 1) { if ($args == 1) {
$reply = fortune_toggle ($chatid, 1); $reply = FortuneToggle ($chatid, 1);
} elsif ($args == 0) { } elsif ($args == 0) {
$reply = fortune_toggle ($chatid, 0); $reply = FortuneToggle ($chatid, 0);
} }
} else { } else {
$reply = fortune_status ($chatid); $reply = FortuneStatus ($chatid);
} }
} elsif ($cmd eq 'oboobs') { } elsif ($cmd eq 'oboobs') {
if (defined $args) { if (defined $args) {
if ($args == 1) { if ($args == 1) {
$reply = plugin_toggle ($chatid, 'oboobs', 1); $reply = PluginToggle ($chatid, 'oboobs', 1);
} elsif ($args == 0) { } elsif ($args == 0) {
$reply = plugin_toggle ($chatid, 'oboobs', 0); $reply = PluginToggle ($chatid, 'oboobs', 0);
} }
} else { } else {
$reply = plugin_status ($chatid, 'oboobs'); $reply = PluginStatus ($chatid, 'oboobs');
} }
} elsif ($cmd eq 'obutts') { } elsif ($cmd eq 'obutts') {
if (defined $args) { if (defined $args) {
if ($args == 1) { if ($args == 1) {
$reply = plugin_toggle ($chatid, 'obutts', 1); $reply = PluginToggle ($chatid, 'obutts', 1);
} elsif ($args == 0) { } elsif ($args == 0) {
$reply = plugin_toggle ($chatid, 'obutts', 0); $reply = PluginToggle ($chatid, 'obutts', 0);
} }
} else { } else {
$reply = plugin_status ($chatid, 'obutts'); $reply = PluginStatus ($chatid, 'obutts');
} }
} }
} }
@@ -307,7 +385,7 @@ MYADMIN
return $reply; return $reply;
} }
sub highlight { sub Highlight {
my $msg = shift; my $msg = shift;
my $fullname; my $fullname;
@@ -349,7 +427,7 @@ sub highlight {
return ($userid, $username, $fullname, $highlight, $visavi); return ($userid, $username, $fullname, $highlight, $visavi);
} }
sub botsleep { sub BotSleep {
# TODO: Parametrise this with fuzzy sleep time in seconds # TODO: Parametrise this with fuzzy sleep time in seconds
my $msg = shift; my $msg = shift;
# let's emulate real human and delay answer # let's emulate real human and delay answer
@@ -365,10 +443,10 @@ sub botsleep {
return; return;
} }
sub isCensored { sub IsCensored {
my $msg = shift; my $msg = shift;
my $forbidden = getForbiddenTypes ($msg->chat->id); my $forbidden = GetForbiddenTypes ($msg->chat->id);
# voice messages are special # voice messages are special
if (defined ($msg->voice) && defined ($msg->voice->duration) && ($msg->voice->duration > 0)) { if (defined ($msg->voice) && defined ($msg->voice->duration) && ($msg->voice->duration > 0)) {

266
lib/BotLib/Admin.pm Normal file
View File

@@ -0,0 +1,266 @@
package BotLib::Admin;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Carp qw (cluck croak);
use CHI;
use CHI::Driver::BerkeleyDB;
use BotLib::Conf qw (LoadConf);
use BotLib::Util qw (utf2sha1);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
# to export array we need @ISA here
our @ISA = qw / Exporter /; ## no critic (ClassHierarchies::ProhibitExplicitISA)
our @EXPORT_OK = qw (@ForbiddenMessageTypes @PluginList GetForbiddenTypes AddForbiddenType DelForbiddenType
ListForbidden FortuneToggle FortuneToggleList FortuneStatus PluginToggle PluginStatus
PluginEnabled);
my $c = LoadConf ();
my $cachedir = $c->{cachedir};
# this list is not yet used
our @PluginList = qw (oboobs obutts); ## no critic (Variables::ProhibitPackageVars)
our @ForbiddenMessageTypes = qw (audio voice photo video video_note animation sticker dice game poll document); ## no critic (Variables::ProhibitPackageVars)
sub GetForbiddenTypes {
my $chatid = shift;
my $messageTypes;
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'censor' . '_' . utf2sha1 ($chatid)
);
foreach (@ForbiddenMessageTypes) {
my $type = $cache->get ($_);
if ($type) {
$messageTypes->{$_} = 1;
} else {
unless (defined $type) {
$cache->set ($_, 0, 'never');
}
$messageTypes->{$_} = 0;
}
}
return $messageTypes;
}
sub AddForbiddenType {
my $chatid = shift;
my $type = shift;
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'censor' . '_' . utf2sha1 ($chatid)
);
$cache->set ($type, 1, 'never');
return;
}
sub DelForbiddenType {
my $chatid = shift;
my $type = shift;
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'censor' . '_' . utf2sha1 ($chatid)
);
$cache->remove ($type);
return;
}
sub ListForbidden {
my $chatid = shift;
my @list;
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'censor' . '_' . utf2sha1 ($chatid)
);
foreach (@ForbiddenMessageTypes) {
my $type = $cache->get ($_);
if ($type) {
push @list, sprintf 'Тип сообщения %s удаляется', $_;
} else {
unless (defined $type) {
$cache->set ($_, 0, 'never');
}
push @list, sprintf 'Тип сообщения %s не удаляется', $_;
}
}
return join "\n", @list;
}
sub FortuneToggle (@) {
my $chatid = shift;
my $action = shift // undef;
my $phrase = 'Фортунка с утра ';
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'fortune'
);
my $state = $cache->get ($chatid);
if (defined $action) {
if ($action) {
$cache->set ($chatid, 1, 'never');
$phrase .= 'будет показываться.';
} else {
if (defined $state) {
$cache->remove ($chatid);
}
$phrase .= 'не будет показываться.';
}
} else {
if (defined $state && $state) {
$cache->remove ($chatid);
$phrase .= 'не будет показываться.';
} else {
$cache->set ($chatid, 1, 'never');
$phrase .= 'будет показываться.';
}
}
return $phrase;
}
sub FortuneStatus ($) {
my $chatid = shift;
my $phrase = 'Фортунка с утра ';
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'fortune'
);
my $state = $cache->get ($chatid);
if (defined $state && $state) {
$phrase .= 'показывается.';
} else {
$phrase .= 'не показывается.';
}
return $phrase;
}
sub FortuneToggleList () {
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'fortune'
);
return $cache->get_keys ();
}
sub PluginStatus (@) {
my $chatid = shift;
my $plugin = shift;
my $phrase = "Плагин $plugin ";
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'plugin' . '_' . utf2sha1 ($chatid)
);
my $state = $cache->get ($plugin);
if ($state) {
$phrase .= 'включён.';
} else {
unless (defined $state) {
$cache->set ($plugin, 0, 'never');
}
$phrase .= 'выключен.';
}
return $phrase;
}
sub PluginEnabled (@) {
my $chatid = shift;
my $plugin = shift;
# seems that we have point to return always true if chat is private
if ($chatid > 0) {
return 1;
}
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'plugin' . '_' . utf2sha1 ($chatid)
);
my $state = $cache->get ($plugin);
if (defined $state && $state) {
return 1;
} else {
return 0;
}
}
sub PluginToggle (@) {
my $chatid = shift;
my $plugin = shift;
my $action = shift // undef;
my $phrase = "Плагин $plugin ";
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . 'plugin' . '_' . utf2sha1 ($chatid)
);
my $state = $cache->get ($plugin);
if (defined $action) {
if ($action) {
$cache->set ($plugin, 1, 'never');
$phrase .= 'включён';
} else {
$cache->set ($plugin, 0, 'never');
$phrase .= 'выключен';
}
} else {
if (defined ($state) && $state) {
$cache->set ($plugin, 0, 'never');
$phrase .= 'включён';
} else {
$cache->set ($plugin, 1, 'never');
$phrase .= 'выключен';
}
}
return $phrase;
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

88
lib/BotLib/Anek.pm Normal file
View File

@@ -0,0 +1,88 @@
package BotLib::Anek;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Encode;
use HTML::TokeParser;
use JSON::XS;
use Log::Any qw ($log);
use Mojo::UserAgent;
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (Anek);
sub Anek {
my $r;
my $ret = 'Все рассказчики анекдотов отдыхают';
my $got_anek = 0;
for (1..3) {
for (1..3) {
my $ua = Mojo::UserAgent->new->connect_timeout (3);
$ua->max_redirects(3);
$r = $ua->get ('https://www.anekdot.ru/rss/randomu.html')->result;
if ($r->is_success) {
last;
}
sleep 2;
}
if ($r->is_success) {
my $json;
my $response_text = decode ('UTF-8', $r->body);
my @text = split /\n/, $response_text;
while (my $str = pop @text) {
if ($str =~ /^var anekdot_texts \= JSON/) {
$str = (split /JSON\.parse\(\'/, $str, 2)[1];
if (defined $str && length ($str) > 10) {
$json = (split /\';\)/, $str, 2)[0];
last;
}
}
}
if (defined $json && length ($json) > 10) {
$json =~ s/\\"/"/g;
$json =~ s/\\\\"/\\"/g;
$json = substr $json, 0, -3;
my $anek = eval { ${JSON::XS->new->relaxed->decode ($json)}[0] };
if (defined $anek) {
my @anek = split /<br>/, $anek;
$ret = join "\n", @anek;
if (length ($ret) > 1) {
$got_anek = 1;
}
} else {
$log->warn (sprintf '[WARN] anekdot.ru server returns incorrect json, full response text message: %s', $response_text);
}
} else {
$log->warn (sprintf '[WARN] anekdot.ru server returns unexpected response text: %s', $r->body);
}
} else {
$log->warn (sprintf '[WARN] anekdot.ru server return status %s with message: %s', $r->code, $r->message);
}
if ($got_anek) {
last;
}
}
return $ret;
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

108
lib/BotLib/Archeologist.pm Normal file
View File

@@ -0,0 +1,108 @@
package BotLib::Archeologist;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use DBI;
use Log::Any qw ($log);
use Math::Random::Secure qw (irand);
use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (Dig);
my $c = LoadConf ();
my $gamestatdir = $c->{gamestat}->{gamestatdir};
my $dbfile = sprintf '%s/gamestat.sqlite', $gamestatdir;
sub getRandomArtifact {
my $dbh;
$dbh = DBI->connect ("dbi:SQLite:$dbfile", '', '') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
$dbh->{sqlite_unicode} = 1;
my $rows_affected = $dbh->do ('PRAGMA foreign_keys=ON') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
$rows_affected = $dbh->do ('PRAGMA journal_mode=WAL') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
my $ary_ref = $dbh->selectall_arrayref ('SELECT name FROM artifacts WHERE id > 0 AND id < 10000') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
$dbh->disconnect or $log->warn (sprintf '[WARN] Unable to close gamestat db, sqlite error: %s', $dbh->errstr);
return $ary_ref->[irand ($#{ $ary_ref } + 1)]->[0];
}
sub Dig {
my $name = shift;
my @location = (
'в архивах',
'в гималаях',
'в интернете',
'в кармане',
'в клумбе',
'в лунном кратере',
'во льдах арктики',
'в морге',
'в окрестностях погоста',
'в песочнице',
'в пустыне Гоби',
'в пустыне Сахаре',
'в русле высохшей реки',
'в собственной памяти',
'в старом сортире',
'в углу',
'в цветочном горшке',
'на бабушкином огороде',
'на винчестере',
'на горе Арарат',
'на горном плато',
'на дне моря',
'на необитаемом острове',
'на первом слое сумрака',
'на просторах Украины',
);
my $find_probability = irand 100;
my $success_probability = irand 100;
my $artifact_age = irand 15000;
if ($find_probability <= 20) {
return sprintf 'По уши закопавшись %s, %s, нифига вы не выкопали! Может повезет в другом месте?', $location[irand ($#location + 1)], $name;
}
my $phrase = sprintf "Вы начали раскопки %s и усиленно роете лопатами, экскаватором...\n", $location[irand ($#location + 1)];
$phrase .= "Вам кажется, что ваш совочек ударился обо что-то твердое. Может, это клад?!\n\n";
my $artifact = getRandomArtifact();
if ($success_probability <= 25) {
$phrase .= sprintf '%s стал лучшим археологом, выкопав %s! возраст артефакта - %s лет!', $name, $artifact, $artifact_age;
} else {
$phrase .= sprintf "Поздравляю, %s! Вы только что выкопали %s, возраст - %s лет!\n", $name, $artifact, $artifact_age;
$phrase .= sprintf 'Извини, %s, но на артефакт это не тянет! Однако, попытка хорошая!', $name;
}
return $phrase;
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,4 +1,4 @@
package buni; package BotLib::Buni;
use 5.018; use 5.018;
use strict; use strict;
@@ -8,18 +8,20 @@ use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck); use Carp qw (cluck);
use HTML::TokeParser; use HTML::TokeParser;
use Log::Any qw ($log);
use Mojo::UserAgent; use Mojo::UserAgent;
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (buni); our @EXPORT_OK = qw (Buni);
sub buni { sub Buni {
my $r; my $r;
my $ret = 'Нету Buni'; my $ret = 'Нету Buni';
for (1..3) { for (1..3) {
my $ua = Mojo::UserAgent->new->connect_timeout (3); my $ua = Mojo::UserAgent->new->connect_timeout (3);
$ua->max_redirects(3);
$r = $ua->get ('http://www.bunicomic.com/?random&nocache=1')->result; $r = $ua->get ('http://www.bunicomic.com/?random&nocache=1')->result;
if ($r->is_success) { if ($r->is_success) {
@@ -47,7 +49,7 @@ sub buni {
} while ($#{$a[0]} > 1); } while ($#{$a[0]} > 1);
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Bunicomic server return status %s with message: %s', $r->code, $r->message);
} }
return $ret; return $ret;

View File

@@ -1,4 +1,4 @@
package conf; package BotLib::Conf;
# loads config # loads config
use 5.018; use 5.018;
@@ -11,9 +11,9 @@ use JSON::XS;
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (loadConf); our @EXPORT_OK = qw (LoadConf);
sub loadConf { sub LoadConf {
my $c = 'data/config.json'; my $c = 'data/config.json';
open my $CH, '<', $c or die "[FATAL] No conf at $c: $OS_ERROR\n"; open my $CH, '<', $c or die "[FATAL] No conf at $c: $OS_ERROR\n";
my $len = (stat $c) [7]; my $len = (stat $c) [7];

View File

@@ -1,4 +1,4 @@
package drink; package BotLib::Drink;
use 5.018; use 5.018;
use strict; use strict;
@@ -12,17 +12,21 @@ use English qw ( -no_match_vars );
use Encode; use Encode;
use Carp qw (cluck); use Carp qw (cluck);
use HTML::TokeParser; use HTML::TokeParser;
use Log::Any qw ($log);
use Mojo::Log; use Mojo::Log;
use Mojo::UserAgent::Cached; use Mojo::UserAgent::Cached;
use POSIX qw (strftime); use POSIX qw (strftime);
use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (drink); our @EXPORT_OK = qw (Drink);
my @MONTH = qw (yanvar fevral mart aprel may iyun iyul avgust sentyabr oktyabr noyabr dekabr); my @MONTH = qw (yanvar fevral mart aprel may iyun iyul avgust sentyabr oktyabr noyabr dekabr);
my $c = LoadConf ();
my $cachedir = $c->{cachedir};
sub drink { sub Drink {
my $r; my $r;
my $ret = 'Не знаю праздников - вджобываю весь день на шахтах, как проклятая.'; my $ret = 'Не знаю праздников - вджобываю весь день на шахтах, как проклятая.';
my ($dayNum, $monthNum) = (localtime ())[3, 4]; my ($dayNum, $monthNum) = (localtime ())[3, 4];
@@ -59,7 +63,7 @@ sub drink {
$ua->cache_agent( $ua->cache_agent(
CHI->new ( CHI->new (
driver => 'BerkeleyDB', driver => 'BerkeleyDB',
root_dir => 'data/cache', root_dir => $cachedir,
namespace => __PACKAGE__, namespace => __PACKAGE__,
expires_at => $expirationDate, expires_at => $expirationDate,
expires_on_backend => 1, expires_on_backend => 1,
@@ -100,7 +104,7 @@ sub drink {
$ret = join "\n", @holyday; $ret = join "\n", @holyday;
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Kakoysegodnyaprazdnik server return status %s with message: %s', $r->code, $r->message);
} }
return $ret; return $ret;

View File

@@ -1,4 +1,4 @@
package fisher; package BotLib::Fisher;
use 5.018; use 5.018;
use strict; use strict;
@@ -6,66 +6,54 @@ use warnings;
use utf8; use utf8;
use open qw (:std :utf8); use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use DBI;
use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (fish); our @EXPORT_OK = qw (Fish);
my $c = LoadConf ();
my $gamestatdir = $c->{gamestat}->{gamestatdir};
my $dbfile = sprintf '%s/gamestat.sqlite', $gamestatdir;
sub fish { sub getRandomArtifact {
my $dbh;
$dbh = DBI->connect ("dbi:SQLite:$dbfile", '', '') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
$dbh->{sqlite_unicode} = 1;
my $rows_affected = $dbh->do ('PRAGMA foreign_keys=ON') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
$rows_affected = $dbh->do ('PRAGMA journal_mode=WAL') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
my $ary_ref = $dbh->selectall_arrayref ('SELECT name FROM artifacts WHERE id > 9999 AND id < 20000') or do {
$log->error (sprintf '[ERROR] Unable to get random artifact, sqlite error: %s', $dbh->errstr);
return undef;
};
$dbh->disconnect or $log->warn (sprintf '[WARN] Unable to close gamestat db, sqlite error: %s', $dbh->errstr);
return $ary_ref->[irand ($#{ $ary_ref } + 1)]->[0];
}
sub Fish {
my $name = shift; my $name = shift;
my @artifact = (
'семикрылого чешуйчатого пятихуя',
'разумного конденсатора',
'сальмонеллу',
'селёдку',
'желтоперого тунца',
'розовую сальмонеллу',
'голавля',
'барбуса',
'окуня',
'верхоплавку',
'коричневую форель',
'полярника',
'плотву',
'зубатку',
'солнечную рыбу',
'старую шину',
'скользкую корягу',
'лампу джина',
'любовное послание в бутылке',
'старое бревно',
'резиновый сапог',
'труп',
'Лох-Несское чудовище',
'старую рыбацкую приманку',
'кусок Титаника',
'обломок Атлантиды',
'кита',
'кальмара',
'дельфина',
'ската',
'подводную лодку',
'медузу',
'морскую звезду',
'электрического угря',
'большую белую акулу',
'аквалангиста',
'монстра',
'вируса',
'сырую пачку сигарет',
'водоросли',
'якорь от лодки',
'водяного',
'русалку',
'палтуса',
'труп собаки по кличке Беня',
'олигофрена Проню',
'огеймера Фишкина',
);
my @location = ( my @location = (
'в поток', 'в поток',
'в озеро', 'в озеро',
@@ -117,7 +105,8 @@ sub fish {
return $phrase; return $phrase;
} }
$phrase .= sprintf "Поздравляю, %s! Вы только что поймали %s! аж на %d кило!\n\n", $name, $artifact[irand ($#artifact + 1)], $weight; my $artifact = getRandomArtifact ();
$phrase .= sprintf "Поздравляю, %s! Вы только что поймали %s! аж на %d кило!\n\n", $name, $artifact, $weight;
if ($success_probability <= 25) { if ($success_probability <= 25) {
$phrase .= sprintf 'Ё-мое!!! А это ведь новый рекорд! Продолжай в том же духе, %s!', $name; $phrase .= sprintf 'Ё-мое!!! А это ведь новый рекорд! Продолжай в том же духе, %s!', $name;

View File

@@ -1,4 +1,4 @@
package fortune; package BotLib::Fortune;
# Any fortune_mod sources are suitable. Text file with "\n%\n" sentence delimeter. # Any fortune_mod sources are suitable. Text file with "\n%\n" sentence delimeter.
use 5.018; use 5.018;
@@ -9,19 +9,20 @@ use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck croak); use Carp qw (cluck croak);
use File::Path qw (make_path); use File::Path qw (make_path);
use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use SQLite_File; use SQLite_File;
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (seed fortune); our @EXPORT_OK = qw (Seed Fortune);
my $c = loadConf (); my $c = LoadConf ();
my $dir = $c->{fortune}->{dir}; my $dir = $c->{fortune}->{dir};
my $srcdir = $c->{fortune}->{srcdir}; my $srcdir = $c->{fortune}->{srcdir};
sub seed () { sub Seed () {
unless (-d $dir) { unless (-d $dir) {
make_path ($dir) || croak "Unable to create $dir: $OS_ERROR"; make_path ($dir) || croak "Unable to create $dir: $OS_ERROR";
} }
@@ -65,11 +66,11 @@ sub seed () {
} }
# just return answer # just return answer
sub fortune () { sub Fortune () {
my $backingfile = sprintf '%s/fortune.sqlite', $dir; my $backingfile = sprintf '%s/fortune.sqlite', $dir;
tie my @array, 'SQLite_File', $backingfile || do { tie my @array, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR"; $log->error ("[ERROR] Unable to tie to $backingfile: $OS_ERROR");
return ''; return '';
}; };

View File

@@ -1,4 +1,4 @@
package friday; package BotLib::Friday;
# for friday data stolen from https://raw.githubusercontent.com/isida/vi/master/data/friday.txt # for friday data stolen from https://raw.githubusercontent.com/isida/vi/master/data/friday.txt
use 5.018; use 5.018;
@@ -9,20 +9,21 @@ use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck croak); use Carp qw (cluck croak);
use File::Path qw (make_path); use File::Path qw (make_path);
use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use SQLite_File; use SQLite_File;
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (seed friday); our @EXPORT_OK = qw (Seed Friday);
my $c = loadConf (); my $c = LoadConf ();
my $dir = $c->{friday}->{dir}; my $dir = $c->{friday}->{dir};
my $srcfile = $c->{friday}->{src}; my $srcfile = $c->{friday}->{src};
my @dow = qw (monday tuesday wednesday thursday friday saturday sunday); my @dow = qw (monday tuesday wednesday thursday friday saturday sunday);
sub seed () { sub Seed () {
unless (-d $dir) { unless (-d $dir) {
make_path ($dir) || croak "Unable to create $dir: $OS_ERROR"; make_path ($dir) || croak "Unable to create $dir: $OS_ERROR";
} }
@@ -75,12 +76,12 @@ sub seed () {
} }
# just return answer # just return answer
sub friday () { sub Friday () {
my $today = (localtime (time))[6] - 1; my $today = (localtime (time))[6] - 1;
my $backingfile = sprintf '%s/%s.sqlite', $dir, $dow[$today]; my $backingfile = sprintf '%s/%s.sqlite', $dir, $dow[$today];
tie my @array, 'SQLite_File', $backingfile || do { tie my @array, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR\n"; $log->error ("[ERROR] Unable to tie to $backingfile: $OS_ERROR\n");
return ''; return '';
}; };

115
lib/BotLib/Gamestat.pm Normal file
View File

@@ -0,0 +1,115 @@
package BotLib::Gamestat;
# Stores and calculates player stats for games
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use DBI;
use English qw ( -no_match_vars );
use File::Path qw (make_path);
use String::Random qw (random_string);
use BotLib::Conf qw (LoadConf);
use BotLib::Util qw (trim utf2sha1);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (SeedArtifacts);
my $c = LoadConf ();
my $dir = $c->{gamestat}->{dir};
my $gamestatdir = $c->{gamestat}->{gamestatdir};
my $dbfile = sprintf '%s/gamestat.sqlite', $gamestatdir;
# assume that there is not too much artifacts.
sub SeedArtifacts {
open (my $AFH, '<', "$dir/archeologic_artifacts.txt") || die "No $dir/archeologic_artifacts.txt\n";
my @arch_artifacts;
while (my $artifact = <$AFH>) {
next if ($artifact =~ /^\s*$/);
$artifact = trim ($artifact);
push @arch_artifacts, $artifact;
}
close $AFH; ## no critic (InputOutput::RequireCheckedSyscalls)
open ($AFH, '<', "$dir/fisher_artifacts.txt") || die "No $dir/fisher_artifacts.txt\n";
my @fisher_artifacts;
while (my $artifact = <$AFH>) {
next if ($artifact =~ /^\s*$/);
$artifact = trim ($artifact);
push @fisher_artifacts, $artifact;
}
close $AFH; ## no critic (InputOutput::RequireCheckedSyscalls)
undef $AFH;
my $tablename = random_string ('..........');
$tablename = utf2sha1 ($tablename);
while ($tablename =~ /^\d/) {
$tablename = utf2sha1 ($tablename);
}
$tablename =~ s/[\/_\+]/a/g;
$tablename = lc ($tablename);
unless (-d $gamestatdir) {
make_path ($gamestatdir) || die "Unable to create $gamestatdir: $OS_ERROR\n";
}
my $dbh = DBI->connect ("dbi:SQLite:$dbfile", '', '');
$dbh->{sqlite_unicode} = 1;
my $rows_affected = $dbh->do ('PRAGMA foreign_keys=ON') or die $dbh->errstr . "\n";
$rows_affected = $dbh->do ('PRAGMA journal_mode=WAL') or die $dbh->errstr . "\n";
# Open transaction
$dbh->begin_work or die $dbh->errstr . "\n";
my $query = <<"QUERY";
CREATE TABLE IF NOT EXISTS $tablename (
id INT UNIQUE NOT NULL, -- artifact id
name TEXT PRIMARY KEY -- artifact name
);
QUERY
$rows_affected = $dbh->do ($query) or die $dbh->errstr . "\n";
my $sth = $dbh->prepare ("REPLACE INTO $tablename(id,name) VALUES (?,?)") or die $dbh->errstr . "\n";
# Insert our archeological artifacts
my $counter = 0;
foreach my $artifact (@arch_artifacts) {
last if ($counter == 10000);
$sth->execute ($c, $artifact);
$counter++;
}
# Insert our fishing artifacts
$counter = 10000;
foreach my $artifact (@fisher_artifacts) {
last if ($counter == 20000);
$sth->execute ($c, $artifact);
$counter++;
}
# Add indexes
$rows_affected = $dbh->do ("CREATE INDEX IF NOT EXISTS myartifactid ON $tablename(id)") or die $dbh->errstr . "\n";
# Drop target table and replace it with our
$rows_affected = $dbh->do ('DROP TABLE IF EXISTS artifacts') or die $dbh->errstr . "\n";
$rows_affected = $dbh->do ("ALTER TABLE $tablename RENAME TO artifacts") or die $dbh->errstr . "\n";
# Close transaction
$dbh->commit or die $dbh->errstr . "\n";
$dbh->disconnect or warn (sprintf '[WARN] Unable to close gamestat db, sqlite error: %s', $dbh->errstr) . "\n";
return;
}
1;

View File

@@ -1,4 +1,4 @@
package image; package BotLib::Image;
use 5.018; use 5.018;
use strict; use strict;
@@ -7,23 +7,18 @@ use utf8;
use open qw (:std :utf8); use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck); use Carp qw (cluck);
use File::Path qw (make_path); use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use Mojo::UserAgent; use Mojo::UserAgent;
use SQLite_File; use BotLib::Image::Flickr qw (FlickrByTags);
use conf qw (loadConf); use BotLib::Image::Imgur qw (Imgur);
use flickr qw (flickr_by_tags); use BotLib::Util qw (urlencode);
use imgur qw (imgur);
use util qw (urlencode);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (kitty fox oboobs obutts rabbit owl); our @EXPORT_OK = qw (Kitty Fox Oboobs Obutts Rabbit Owl Frog Horse Snail);
my $c = loadConf (); sub Kitty {
my $dir = $c->{image}->{dir};
sub kitty {
my $r; my $r;
my $ret = 'Нету кошечек, все разбежались.'; my $ret = 'Нету кошечек, все разбежались.';
@@ -44,21 +39,21 @@ sub kitty {
}; };
unless (defined $jcat) { unless (defined $jcat) {
cluck "[ERROR] Unable to decode JSON: $EVAL_ERROR"; $log->warn ("[WARN] Unable to decode JSON from thecatapi: $EVAL_ERROR");
} else { } else {
if ($jcat->[0]->{url}) { if ($jcat->[0]->{url}) {
my @cats = ('龴ↀ◡ↀ龴', '=^..^=', '≧◔◡◔≦ ','^ↀᴥↀ^' ); my @cats = ('龴ↀ◡ↀ龴', '=^..^=', '≧◔◡◔≦ ','^ↀᴥↀ^');
$ret = sprintf '[%s](%s)', $cats[irand ($#cats + 1)], $jcat->[0]->{url}; $ret = sprintf '[%s](%s)', $cats[irand ($#cats + 1)], $jcat->[0]->{url};
} }
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Thecatapi server return status %s with message: %s', $r->code, $r->message);
} }
return $ret; return $ret;
} }
sub fox { sub Fox {
my $r; my $r;
my $ret = 'Нету лисичек, все разбежались.'; my $ret = 'Нету лисичек, все разбежались.';
@@ -79,7 +74,7 @@ sub fox {
}; };
unless (defined $jfox) { unless (defined $jfox) {
cluck "[ERROR] Unable to decode JSON: $EVAL_ERROR"; $log->warn ("[WARN] Unable to decode JSON from randomfox: $EVAL_ERROR");
} else { } else {
if ($jfox->{image}) { if ($jfox->{image}) {
$jfox->{image} =~ s/\\//xmsg; $jfox->{image} =~ s/\\//xmsg;
@@ -87,13 +82,13 @@ sub fox {
} }
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Randomfox server return status %s with message: %s', $r->code, $r->message);
} }
return $ret; return $ret;
} }
sub oboobs { sub Oboobs {
my $r; my $r;
my $ret = 'Нету cисичек, все разбежались.'; my $ret = 'Нету cисичек, все разбежались.';
@@ -114,7 +109,7 @@ sub oboobs {
}; };
unless (defined $joboobs) { unless (defined $joboobs) {
cluck "[ERROR] Unable to decode JSON: $EVAL_ERROR"; $log->warn ("[WARN] Unable to decode JSON from oboobs: $EVAL_ERROR");
} else { } else {
if ($joboobs->[0]->{preview}) { if ($joboobs->[0]->{preview}) {
my @boobs = ('(. )( .)', '( . Y . )', '(o)(o)', '( @ )( @ )', '(.)(.)'); my @boobs = ('(. )( .)', '( . Y . )', '(o)(o)', '( @ )( @ )', '(.)(.)');
@@ -122,13 +117,13 @@ sub oboobs {
} }
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Oboobs server return status %s with message: %s', $r->code, $r->message);
} }
return $ret; return $ret;
} }
sub obutts { sub Obutts {
my $r; my $r;
my $ret = 'Нету попок, все разбежались.'; my $ret = 'Нету попок, все разбежались.';
@@ -149,7 +144,7 @@ sub obutts {
}; };
unless (defined $jobutts) { unless (defined $jobutts) {
cluck "[ERROR] Unable to decode JSON: $EVAL_ERROR"; $log->warn ("[ERROR] Unable to decode JSON from obutts: $EVAL_ERROR");
} else { } else {
if ($jobutts->[0]->{preview}) { if ($jobutts->[0]->{preview}) {
my @butts = ('(__(__)', '(_!_)', '(__.__)'); my @butts = ('(__(__)', '(_!_)', '(__.__)');
@@ -157,13 +152,13 @@ sub obutts {
} }
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Obutts server return status %s with message: %s', $r->code, $r->message);
} }
return $ret; return $ret;
} }
# It works but results are not particulary relevant, partially. about 20-30% miss # It works but results are not particularly relevant, partially. about 20-30% miss
# So i prefer to use flickr rabbits selection :) # So i prefer to use flickr rabbits selection :)
sub rabbit_imgur { sub rabbit_imgur {
my @terms = ( my @terms = (
@@ -174,7 +169,7 @@ sub rabbit_imgur {
urlencode 'score?q_not=cat OR kitten OR kittens OR dog OR cats OR dogs OR puppy OR puppies&q_tags=bunnies,bunny,rabbits,rabbit', urlencode 'score?q_not=cat OR kitten OR kittens OR dog OR cats OR dogs OR puppy OR puppies&q_tags=bunnies,bunny,rabbits,rabbit',
); );
my $rabbit = imgur ($terms[irand ($#terms + 1)]); my $rabbit = Imgur ($terms[irand ($#terms + 1)]);
# return just large thumbnail instead of real photo (that can be really huge) # return just large thumbnail instead of real photo (that can be really huge)
$rabbit = sprintf '%sl%s', substr ($rabbit, 0, -4), substr ($rabbit, -4); $rabbit = sprintf '%sl%s', substr ($rabbit, 0, -4), substr ($rabbit, -4);
@@ -185,9 +180,9 @@ sub rabbit_imgur {
} }
} }
sub rabbit { sub Rabbit {
# rabbit, but bunny # rabbit, but bunny
my $url = flickr_by_tags ('animal,bunny'); my $url = FlickrByTags ('animal,bunny');
if (defined $url) { if (defined $url) {
return sprintf '[(\_/)](%s)', $url; return sprintf '[(\_/)](%s)', $url;
@@ -196,8 +191,8 @@ sub rabbit {
} }
} }
sub owl { sub Owl {
my $url = flickr_by_tags ('bird,owl'); my $url = FlickrByTags ('bird,owl');
if (defined $url) { if (defined $url) {
return sprintf '[{ O v O }](%s)', $url; return sprintf '[{ O v O }](%s)', $url;
@@ -206,6 +201,41 @@ sub owl {
} }
} }
sub Frog {
# unable to find any one-line ascii art for frog :(
my @art = qw (frog toad лягушка);
my $url = FlickrByTags ('frog,toad,amphibian');
if (defined $url) {
return sprintf '[%s](%s)', $art [irand ($#art + 1)], $url;
} else {
return 'Нету лягушек, все свалили.';
}
}
sub Horse {
# unable to find any one-line ascii art for frog :(
my @art = qw (horse лошадь лошадка);
my $url = FlickrByTags ('horse,equine,mammal');
if (defined $url) {
return sprintf '[%s](%s)', $art [irand ($#art + 1)], $url;
} else {
return 'Нету коняшек, все разбежались.';
}
}
sub Snail {
# unable to find any one-line ascii art for frog :(
my @art = qw ('-'_@_ @╜ @_'-');
my $url = FlickrByTags ('snail,slug');
if (defined $url) {
return sprintf '[%s](%s)', $art [irand ($#art + 1)], $url;
} else {
return 'Нету улиток, все расползлись.';
}
}
1; 1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4: # vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,4 +1,4 @@
package flickr; package BotLib::Image::Flickr;
use 5.018; use 5.018;
use strict; use strict;
@@ -7,25 +7,26 @@ use utf8;
use open qw (:std :utf8); use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck); use Carp qw (cluck);
use CHI;
use CHI::Driver::BerkeleyDB;
use Digest::HMAC_SHA1 qw (hmac_sha1); use Digest::HMAC_SHA1 qw (hmac_sha1);
use File::Path qw (make_path); use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use MIME::Base64; use MIME::Base64;
use Mojo::UserAgent; use Mojo::UserAgent;
use SQLite_File;
use URI::Encode::XS qw (uri_encode); use URI::Encode::XS qw (uri_encode);
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use util qw (urlencode); use BotLib::Util qw (urlencode);
use Data::Dumper; use Data::Dumper;
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (flickr_init flickr_by_tags flickr_by_text flickr_test_login); our @EXPORT_OK = qw (FlickrInit FlickrByTags FlickrByText FlickrTestLogin);
my $c = loadConf (); my $c = LoadConf ();
my $dir = $c->{image}->{dir}; my $cachedir = $c->{cachedir};
my $flickr_consumer_key = $c->{image}->{flickr}->{consumer_key}; my $flickr_consumer_key = $c->{image}->{flickr}->{consumer_key};
my $flickr_consumer_secret = $c->{image}->{flickr}->{consumer_secret}; my $flickr_consumer_secret = $c->{image}->{flickr}->{consumer_secret};
@@ -185,10 +186,10 @@ sub flickrAuthorization {
my $ua = Mojo::UserAgent->new->connect_timeout (5); my $ua = Mojo::UserAgent->new->connect_timeout (5);
my $r = $ua->get ($url)->result; my $r = $ua->get ($url)->result;
if ($r->is_success) { if ($r->code == 302) {
return $r->url; return $r->content->headers->location;
} else { } else {
say "Something went wrong: $r->code $r->message"; ## no critic (InputOutput::RequireCheckedSyscalls) printf "Something went wrong: %s %s\n", $r->code, $r->message;
return; return;
} }
} }
@@ -335,19 +336,19 @@ sub flickrSearchByText {
if (defined $response) { if (defined $response) {
return $response; return $response;
} else { } else {
cluck "Flickr api returns incorrect json: $r->message"; $log->warn ("[WARN] Flickr api returns incorrect json: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "Flickr api returns incorrect json: $r->message"; $log->warn ("[WARN] Flickr api returns incorrect json: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "Flickr api returns incorrect json: $r->message"; $log->warn ("[WARN] Flickr api returns incorrect json: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "HTTP status code not 200: $r->code, $r->message"; $log->warn ("[WARN] Flickr api HTTP status code not 200: $r->code, $r->message");
return undef; return undef;
} }
} }
@@ -442,105 +443,85 @@ sub flickrSearchByTags {
if (defined $response) { if (defined $response) {
return $response; return $response;
} else { } else {
cluck "Flickr api returns incorrect json: $r->message"; $log->warn ("[WARN] Flickr api returns incorrect json: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "Flickr api returns incorrect json: $r->message"; $log->warn ("[WARN] Flickr api returns incorrect json: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "Flickr api returns incorrect json: $r->message"; $log->warn ("[WARN] Flickr api returns incorrect json: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "HTTP status code not 200: $r->code, $r->message"; $log->warn ("[WARN] Flickr api HTTP status code not 200: $r->code, $r->message");
return undef; return undef;
} }
} }
sub flickr_init { sub FlickrInit {
my $backingfile = sprintf '%s/secrets.sqlite', $dir; my $cache = CHI->new (
driver => 'BerkeleyDB',
unless (-d $dir) { root_dir => $cachedir,
make_path ($dir) or do { namespace => __PACKAGE__
say "Unable to create $dir: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls) );
return 0;
};
}
tie my %secret, 'SQLite_File', $backingfile || do {
say "[ERROR] Unable to tie to $backingfile: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls)
return 0;
};
if (defined $flickr_verifier && $flickr_verifier) { if (defined $flickr_verifier && $flickr_verifier) {
my $flickr_request_token = $secret{flickr_request_token}; my $flickr_request_token = $cache->get ('flickr_request_token');
my $flickr_request_token_secret = $secret{flickr_request_token_secret}; my $flickr_request_token_secret = $cache->get ('flickr_request_token_secret');
unless (defined $flickr_request_token || defined $flickr_request_token_secret) { unless (defined $flickr_request_token || defined $flickr_request_token_secret) {
say "Looks like there is no request token or request token secret in $backingfile"; ## no critic (InputOutput::RequireCheckedSyscalls) say 'Looks like there is no request token or request token secret in config db.'; ## no critic (InputOutput::RequireCheckedSyscalls)
say 'Please remove verifier settings from config.json and run this script again'; ## no critic (InputOutput::RequireCheckedSyscalls) say 'Please remove verifier settings from config.json and run this script again.'; ## no critic (InputOutput::RequireCheckedSyscalls)
untie %secret;
return 0; return 0;
} }
my %accessToken = flickrAccessToken ($flickr_request_token, $flickr_request_token_secret, $flickr_verifier); my %accessToken = flickrAccessToken ($flickr_request_token, $flickr_request_token_secret, $flickr_verifier);
if (defined ($accessToken{oauth_token}) && defined ($accessToken{oauth_token_secret})) { if (defined ($accessToken{oauth_token}) && defined ($accessToken{oauth_token_secret})) {
$secret{flickr_access_token} = $accessToken{oauth_token}; # TODO: check set
$secret{flickr_access_token_secret} = $accessToken{oauth_token_secret}; $cache->set ('flickr_access_token', $accessToken{oauth_token}, 'never');
$cache->set ('flickr_access_token_secret', $accessToken{oauth_token_secret}, 'never');
say sprintf ( ## no critic (InputOutput::RequireCheckedSyscalls) say sprintf ( ## no critic (InputOutput::RequireCheckedSyscalls)
'Access token (%s) and access token secret (%s) are in %s', 'Access token (%s) and access token secret (%s) are config db',
$accessToken{oauth_token}, $accessToken{oauth_token},
$accessToken{oauth_token_secret}, $accessToken{oauth_token_secret},
$backingfile
); );
untie %secret;
return 1; return 1;
} }
} else { } else {
my %req = flickrRequestToken (); my %req = flickrRequestToken ();
if (defined ($req{oauth_token}) && defined ($req{oauth_token_secret}) && defined ($req{oauth_token})) { if (defined ($req{oauth_token}) && defined ($req{oauth_token_secret}) && defined ($req{oauth_token})) {
$secret{flickr_request_token} = $req{oauth_token}; # TODO: check set
$secret{flickr_request_token_secret} = $req{oauth_token_secret}; $cache->set ('flickr_request_token', $req{oauth_token}, 'never');
$cache->set ('flickr_request_token_secret', $req{oauth_token_secret}, 'never');
my $confirm_url = flickrAuthorization ($req{oauth_token}); my $confirm_url = flickrAuthorization ($req{oauth_token});
if (defined $confirm_url) { if (defined $confirm_url) {
say "Please open this url in your browser and grant access for this app:\n$confirm_url"; ## no critic (InputOutput::RequireCheckedSyscalls) say "Please open this url in your browser and grant access for this app:\n$confirm_url"; ## no critic (InputOutput::RequireCheckedSyscalls)
say 'Do not forget to put oauth_verifier to config.json file and re-run this script to get access token'; ## no critic (InputOutput::RequireCheckedSyscalls) say 'Do not forget to put oauth_verifier to config.json file and re-run this script to get access token'; ## no critic (InputOutput::RequireCheckedSyscalls)
say 'Note that you should be logged off from flickr account.'; ## no critic (InputOutput::RequireCheckedSyscalls) say 'Note that you should be logged off from flickr account.'; ## no critic (InputOutput::RequireCheckedSyscalls)
untie %secret;
return 1; return 1;
} }
} }
} }
untie %secret;
return 0; return 0;
} }
sub flickr_test_login { sub FlickrTestLogin {
my $text = shift; my $text = shift;
my $backingfile = sprintf '%s/secrets.sqlite', $dir; my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__
);
unless (-d $dir) { my $flickr_access_token = $cache->get ('flickr_access_token');
make_path ($dir) or do { my $flickr_access_token_secret = $cache->get ('flickr_access_token_secret');
say "Unable to create $dir: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls)
return 0;
};
}
tie my %secret, 'SQLite_File', $backingfile || do {
say "[ERROR] Unable to tie to $backingfile: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls)
return 0;
};
my $flickr_access_token = $secret{flickr_access_token};
my $flickr_access_token_secret = $secret{flickr_access_token_secret};
untie %secret;
if (flickrTestLogin ($flickr_access_token, $flickr_access_token_secret)) { if (flickrTestLogin ($flickr_access_token, $flickr_access_token_secret)) {
return 1; return 1;
@@ -549,25 +530,17 @@ sub flickr_test_login {
} }
} }
sub flickr_by_tags { sub FlickrByTags {
my $text = shift; my $text = shift;
my $backingfile = sprintf '%s/secrets.sqlite', $dir;
unless (-d $dir) { my $cache = CHI->new (
make_path ($dir) or do { driver => 'BerkeleyDB',
say "Unable to create $dir: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls) root_dir => $cachedir,
return 0; namespace => __PACKAGE__
}; );
}
tie my %secret, 'SQLite_File', $backingfile || do { my $flickr_access_token = $cache->get ('flickr_access_token');
say "[ERROR] Unable to tie to $backingfile: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls) my $flickr_access_token_secret = $cache->get ('flickr_access_token_secret');
return 0;
};
my $flickr_access_token = $secret{flickr_access_token};
my $flickr_access_token_secret = $secret{flickr_access_token_secret};
untie %secret;
# NB: I honestly query random page (among number of pages) of results from api, but always got first page, # NB: I honestly query random page (among number of pages) of results from api, but always got first page,
# no matter what is shown in response' "page" parameter. Looks like bug in API. Try to little bit mitigate # no matter what is shown in response' "page" parameter. Looks like bug in API. Try to little bit mitigate
@@ -582,7 +555,7 @@ sub flickr_by_tags {
$item = sprintf 'https://live.staticflickr.com/%s/%s_%s_z.jpg', $item->{server}, $item->{id}, $item->{secret}; $item = sprintf 'https://live.staticflickr.com/%s/%s_%s_z.jpg', $item->{server}, $item->{id}, $item->{secret};
return $item; return $item;
} else { } else {
cluck 'Flickr api returns empty search result list'; $log->notice ('[NOTICE] Flickr api returns empty search result list');
return; return;
} }
} else { } else {
@@ -590,25 +563,17 @@ sub flickr_by_tags {
} }
} }
sub flickr_by_text { sub FlickrByText {
my $text = shift; my $text = shift;
my $backingfile = sprintf '%s/secrets.sqlite', $dir;
unless (-d $dir) { my $cache = CHI->new (
make_path ($dir) or do { driver => 'BerkeleyDB',
say "Unable to create $dir: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls) root_dir => $cachedir,
return 0; namespace => __PACKAGE__
}; );
}
tie my %secret, 'SQLite_File', $backingfile || do { my $flickr_access_token = $cache->get ('flickr_access_token');
say "[ERROR] Unable to tie to $backingfile: $OS_ERROR"; ## no critic (InputOutput::RequireCheckedSyscalls) my $flickr_access_token_secret = $cache->get ('flickr_access_token_secret');
return 0;
};
my $flickr_access_token = $secret{flickr_access_token};
my $flickr_access_token_secret = $secret{flickr_access_token_secret};
untie %secret;
# NB: I honestly query random page (among number of pages) of results from api, but always got first page, # NB: I honestly query random page (among number of pages) of results from api, but always got first page,
# no matter what is shown in response' "page" parameter. Looks like bug in API. Try to little bit mitigate # no matter what is shown in response' "page" parameter. Looks like bug in API. Try to little bit mitigate
@@ -623,7 +588,7 @@ sub flickr_by_text {
$item = sprintf 'https://live.staticflickr.com/%s/%s_%s_z.jpg', $item->{server}, $item->{id}, $item->{secret}; $item = sprintf 'https://live.staticflickr.com/%s/%s_%s_z.jpg', $item->{server}, $item->{id}, $item->{secret};
return $item; return $item;
} else { } else {
cluck 'Flickr api returns empty search result list'; $log->notice ('[NOTICE] Flickr api returns empty search result list');
return; return;
} }
} else { } else {

View File

@@ -1,4 +1,4 @@
package imgur; package Botlib::Image::Imgur;
use 5.018; use 5.018;
use strict; use strict;
@@ -7,56 +7,63 @@ use utf8;
use open qw (:std :utf8); use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck); use Carp qw (cluck);
use File::Path qw (make_path); use CHI;
use CHI::Driver::BerkeleyDB;
use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use Mojo::UserAgent; use Mojo::UserAgent;
use SQLite_File;
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (imgur); our @EXPORT_OK = qw (Imgur);
my $c = loadConf (); my $c = LoadConf ();
my $dir = $c->{image}->{dir}; my $cachedir = $c->{cachedir};
my $imgur_client_id = $c->{image}->{imgur}->{client_id};
my $imgur_client_secret = $c->{image}->{imgur}->{client_secret};
my $imgur_access_token = $c->{image}->{imgur}->{access_token};;
my $imgur_refresh_token = $c->{image}->{imgur}->{refresh_token};
sub imgur { sub Imgur {
my $tag = shift; my $tag = shift;
my $refresh_url = 'https://api.imgur.com/oauth2/token'; my $refresh_url = 'https://api.imgur.com/oauth2/token';
my $search_url = 'https://api.imgur.com/3/gallery/search/'; my $search_url = 'https://api.imgur.com/3/gallery/search/';
# load all tokens from api secrets db my $imgur_client_id = $c->{image}->{imgur}->{client_id} // do {
my $backingfile = sprintf '%s/secrets.sqlite', $dir; $log->error ('[ERROR] No client_id specified in config for imgur module');
unless (-d $dir) {
make_path ($dir) or do {
cluck "Unable to create $dir: $OS_ERROR";
return undef;
};
}
tie my %secret, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR";
return undef; return undef;
}; };
my $imgur_client_secret = $c->{image}->{imgur}->{client_secret} // do {
$log->error ('[ERROR] No client_secret specified in config for imgur module');
return undef;
};
my $imgur_access_token = $c->{image}->{imgur}->{access_token} // do {
$log->error ('[ERROR] No access_token specified in config for imgur module');
return undef;
};
my $imgur_refresh_token = $c->{image}->{imgur}->{refresh_token} // do {
$log->warn ('[WARN] No refresh_token specified in config for imgur module');
};
# N.B. Both imgur_access_token and imgur_refresh_token time to time can change, so we need
# some place where we can store new values
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__
);
$imgur_access_token = $cache->get ('imgur_access_token');
# if there is no access_token in db - put there one from config # if there is no access_token in db - put there one from config
if (defined $secret{imgur_access_token}) { unless (defined $imgur_access_token) {
$imgur_access_token = $secret{imgur_access_token}; $cache->set ('imgur_access_token', $c->{image}->{imgur}->{access_token}, 'never');
} else {
$secret{imgur_access_token} = $imgur_access_token;
} }
$imgur_refresh_token = $cache->get ('imgur_refresh_token');
# if there is no refresh_token in db - put there one from config # if there is no refresh_token in db - put there one from config
if (defined $secret{imgur_refresh_token}) { unless (defined $imgur_refresh_token) {
$imgur_refresh_token = $secret{imgur_refresh_token}; $cache->set ('imgur_refresh_token', $c->{image}->{imgur}->{refresh_token}, 'never');
} else {
$secret{imgur_refresh_token} = $imgur_refresh_token;
} }
# query api # query api
@@ -69,14 +76,13 @@ sub imgur {
$refresh_url => form { $refresh_url => form {
refresh_token => $imgur_refresh_token, refresh_token => $imgur_refresh_token,
client_id => $imgur_client_id, client_id => $imgur_client_id,
clinent_secret => $imgur_client_secret, client_secret => $imgur_client_secret,
grant_type => 'refresh_token', grant_type => 'refresh_token',
} }
); );
unless ($r->is_success) { unless ($r->is_success) {
cluck "Unable to refresh imgur token, please re-authorise app! or imgur experiencing api outage: $r->code $r->message"; $log->error ("[ERROR] Unable to refresh imgur token, please re-authorise app! or imgur experiencing api outage: $r->code $r->message");
untie %secret;
return undef; return undef;
} }
@@ -84,12 +90,11 @@ sub imgur {
if (defined $refresh) { if (defined $refresh) {
$imgur_refresh_token = $refresh->{refresh_token}; $imgur_refresh_token = $refresh->{refresh_token};
$secret{imgur_refresh_token} = $refresh->{refresh_token}; $cache->set ('refresh_token', $refresh->{refresh_token}, 'never');
$imgur_access_token = $refresh->{access_token}; $imgur_access_token = $refresh->{access_token};
$secret{imgur_access_token} = $refresh->{access_token}; $cache->set ('imgur_access_token', $refresh->{access_token}, 'never');
} else { } else {
cluck "Unable to refresh imgur access_token, unable to decode json: $EVAL_ERROR"; $log->warn ("[ERROR] Unable to refresh imgur access_token, unable to decode json: $EVAL_ERROR");
untie %secret;
return undef; return undef;
} }
@@ -97,8 +102,6 @@ sub imgur {
$r = $ua->get ($search_url . $tag => {Authorization => "Bearer $imgur_access_token"})->result; $r = $ua->get ($search_url . $tag => {Authorization => "Bearer $imgur_access_token"})->result;
} }
untie %secret;
if ($r->is_success) { if ($r->is_success) {
my $searchResult = eval { return $r->json; }; my $searchResult = eval { return $r->json; };
my @urls; my @urls;
@@ -128,19 +131,19 @@ sub imgur {
if ($#urls > 0) { if ($#urls > 0) {
return $urls[irand ($#urls + 1)]; return $urls[irand ($#urls + 1)];
} else { } else {
cluck 'Imgur api returned empty result.'; $log->notice ('[NOTICE] Imgur api returned empty result.');
return undef; return undef;
} }
} else { } else {
cluck "Unable to get results from imgur api: $r->message"; $log->warn ("[WARN] Unable to get results from imgur api: $r->message");
return undef; return undef;
} }
} else { } else {
cluck "Unable to decode imgur api json: $EVAL_ERROR"; $log->warn ("[WARN] Unable to decode imgur api json: $EVAL_ERROR");
return undef; return undef;
} }
} else { } else {
cluck "Unable to get results from imgur api: $r->code, $r->message"; $log->warn ("[WARN] Unable to get results from imgur api: $r->code, $r->message");
return undef; return undef;
} }
} }

104
lib/BotLib/Karma.pm Normal file
View File

@@ -0,0 +1,104 @@
package BotLib::Karma;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Carp qw (cluck);
use CHI;
use CHI::Driver::BerkeleyDB;
use Log::Any qw ($log);
use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (KarmaSet KarmaGet);
my $c = LoadConf ();
my $cachedir = $c->{cachedir};
my $max = 5;
# swallow phrase and return answer
sub KarmaSet (@) {
my $chatid = shift;
my $phrase = shift;
my $action = shift;
if (($phrase eq '') || ($phrase =~ /^ +$/)) {
$phrase = ' ';
}
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . $chatid
);
my $score = $cache->get ($phrase);
if (defined $score) {
if ($action eq '++') {
$score++;
} else {
$score--;
}
} else {
if ($action eq '++') {
$score = 1;
} else {
$score = -1;
}
}
if ($cache->set ($phrase, $score, 'never') != $score) {
$log->error ('[ERROR] Cache error: unable to set karma.');
}
if ($score < -1 && (($score % (0 - $max)) + 1) == 0) {
if ($phrase eq ' ') {
return sprintf 'Зарегистрировано пробитие дна, карма пустоты составляет %d', $score;
} else {
return sprintf 'Зарегистрировано пробитие дна, карма %s составляет %d', $phrase, $score;
}
} else {
if ($phrase eq ' ') {
return sprintf 'Карма пустоты составляет %d', $score;
} else {
return sprintf 'Карма %s составляет %d', $phrase, $score;
}
}
}
# just return answer
sub KarmaGet (@) {
my $chatid = shift;
my $phrase = shift;
if (($phrase eq '') || ($phrase =~ /^ +$/)) {
$phrase = ' ';
}
my $cache = CHI->new (
driver => 'BerkeleyDB',
root_dir => $cachedir,
namespace => __PACKAGE__ . '_' . $chatid
);
my $score = $cache->get ($phrase);
unless (defined $score) {
$score = 0;
}
if ($phrase eq ' ') {
return sprintf 'Карма пустоты составляет %d', $score;
} else {
return sprintf 'Карма %s составляет %d', $phrase, $score;
}
}
1;
# vim: ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,4 +1,4 @@
package lat; package BotLib::Lat;
# famous latin phases taken from https://ru.wikipedia.org/wiki/Список_крылатых_латинских_выражений # famous latin phases taken from https://ru.wikipedia.org/wiki/Список_крылатых_латинских_выражений
use 5.018; use 5.018;
@@ -11,17 +11,18 @@ use Carp qw (cluck);
use File::Basename qw (dirname); use File::Basename qw (dirname);
use File::Path qw (make_path); use File::Path qw (make_path);
use Hailo; use Hailo;
use conf qw (loadConf); use Log::Any qw ($log);
use BotLib::Conf qw (LoadConf);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (train latAnswer); our @EXPORT_OK = qw (Train Lat);
my $c = loadConf (); my $c = LoadConf ();
my $brain = $c->{lat}->{brain}; my $brain = $c->{lat}->{brain};
my $srcfile = $c->{lat}->{src}; my $srcfile = $c->{lat}->{src};
sub train () { sub Train () {
my $braindir = dirname $brain; my $braindir = dirname $brain;
unless (-d $braindir) { unless (-d $braindir) {
@@ -45,16 +46,16 @@ sub train () {
} }
# just return answer # just return answer
sub latAnswer () { sub Lat () {
my $braindir = dirname $brain; my $braindir = dirname $brain;
unless (-d $braindir) { unless (-d $braindir) {
cluck "[ERROR] No lat module data: $braindir is absent! Train lat first."; $log->error ("[ERROR] No lat module data: $braindir is absent! Train lat first.");
return ''; return '';
} }
unless (-f $brain) { unless (-f $brain) {
cluck "[ERROR] No lat module data: $brain is absent! Train lat first."; $log->error ("[ERROR] No lat module data: $brain is absent! Train lat first.");
return ''; return '';
} }

View File

@@ -1,4 +1,4 @@
package monkeyuser; package BotLib::Monkeyuser;
use 5.018; use 5.018;
use strict; use strict;
@@ -8,14 +8,15 @@ use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck); use Carp qw (cluck);
use HTML::TokeParser; use HTML::TokeParser;
use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use Mojo::UserAgent; use Mojo::UserAgent;
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (monkeyuser); our @EXPORT_OK = qw (Monkeyuser);
sub monkeyuser { sub Monkeyuser {
my $r; my $r;
my $ret = 'Нету Monkey User-ов, они все спрятались.'; my $ret = 'Нету Monkey User-ов, они все спрятались.';
my @link; my @link;
@@ -51,7 +52,7 @@ sub monkeyuser {
$ret = sprintf '[MonkeyUser](https://www.monkeyuser.com%s)', $link [irand (1 + $#link)]; $ret = sprintf '[MonkeyUser](https://www.monkeyuser.com%s)', $link [irand (1 + $#link)];
} }
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->{status}, $r->{reason}; $log->warn (sprintf '[WARN] MonkeyUser server return status %s with message: %s', $r->{status}, $r->{reason});
} }
return $ret; return $ret;

88
lib/BotLib/Proverb.pm Normal file
View File

@@ -0,0 +1,88 @@
package BotLib::Proverb;
# Any fortune_mod sources are suitable. Text file with "\n%\n" sentence delimeter.
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Carp qw (cluck croak);
use File::Path qw (make_path);
use Log::Any qw ($log);
use Math::Random::Secure qw (irand);
use SQLite_File;
use BotLib::Conf qw (LoadConf);
use BotLib::Util qw (trim);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (Seed Proverb);
my $c = LoadConf ();
my $dir = $c->{proverb}->{dir};
my $srcdir = $c->{proverb}->{srcdir};
sub Seed () {
unless (-d $dir) {
make_path ($dir) || croak "Unable to create $dir: $OS_ERROR";
}
my $backingfile = sprintf '%s/proverb.sqlite', $dir;
if (-f $backingfile) {
unlink $backingfile || croak "Unable to remove $backingfile: $OS_ERROR";
}
tie my @proverb, 'SQLite_File', $backingfile || croak "Unable to tie to $backingfile: $OS_ERROR";
opendir (my $srcdirhandle, $srcdir) || croak "Unable to open $srcdir: $OS_ERROR";
while (my $proverbfile = readdir ($srcdirhandle)) {
my $srcfile = sprintf '%s/%s', $srcdir, $proverbfile;
unless (-f $srcfile) {
next;
}
if ($proverbfile =~ m/^\.+$/) {
next;
}
open (my $fh, '<', $srcfile) || croak "Unable to open $srcfile, $OS_ERROR";
while (readline $fh) {
chomp;
my $str = trim ($_);
if ($str ne '') {
push @proverb, $str;
}
}
close $fh; ## no critic (InputOutput::RequireCheckedSyscalls, InputOutput::RequireCheckedOpen)
}
closedir $srcdirhandle;
untie @proverb;
return;
}
# just return answer
sub Proverb () {
my $backingfile = sprintf '%s/proverb.sqlite', $dir;
tie my @array, 'SQLite_File', $backingfile || do {
$log->error ("[ERROR] Unable to tie to $backingfile: $OS_ERROR");
return '';
};
my $phrase = $array[irand ($#array + 1)];
# decode?
utf8::decode $phrase;
untie @array;
return $phrase;
}
1;
# vim: ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,4 +1,4 @@
package util; package BotLib::Util;
use 5.018; use 5.018;
use strict; use strict;
@@ -77,7 +77,6 @@ sub utf2sha1 {
return sha1_base64 $bytes; return sha1_base64 $bytes;
} }
1; 1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4: # vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,4 +1,4 @@
package weather; package BotLib::Weather;
use 5.018; use 5.018;
use strict; use strict;
@@ -9,21 +9,20 @@ use English qw ( -no_match_vars );
use Carp qw (carp cluck); use Carp qw (carp cluck);
use CHI; use CHI;
use CHI::Driver::BerkeleyDB; use CHI::Driver::BerkeleyDB;
use DB_File; use Log::Any qw ($log);
use Digest::MD5 qw (md5_base64);
use File::Path qw (make_path);
use Mojo::Log; use Mojo::Log;
use Mojo::UserAgent::Cached; use Mojo::UserAgent::Cached;
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use util qw (trim urlencode); use BotLib::Util qw (trim urlencode);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (weather); our @EXPORT_OK = qw (Weather);
my $c = loadConf (); my $c = LoadConf ();
my $cachedir = $c->{cachedir};
sub weather { sub Weather {
my $city = shift; my $city = shift;
$city = trim $city; $city = trim $city;
@@ -31,13 +30,22 @@ sub weather {
return 'Длинновато для названия города.' if (length ($city) > 80); return 'Длинновато для названия города.' if (length ($city) > 80);
$city = ucfirst $city; $city = ucfirst $city;
if ($city eq 'Мск' || $city eq 'Default' || $city eq 'Dc' || $city eq 'Msk' || $city eq 'Dc-universe') {
$city = 'Москва';
} elsif ($city eq 'Спб' || $city eq 'Spb') {
$city = 'Санкт-Петербург';
} elsif ($city eq 'Ект' || $city eq 'Ебург' || $city eq 'Ёбург' || $city eq 'Екат' || $city eq 'Ekt' || $city eq 'Eburg' || $city eq 'Ekat') {
$city = 'Екатеринбург';
}
my $w = __weather ($city); my $w = __weather ($city);
my $reply; my $reply;
if ($w) { if ($w) {
if ($w->{temperature_min} == $w->{temperature_max}) { if ($w->{temperature_min} == $w->{temperature_max}) {
$reply = sprintf ( $reply = sprintf (
"Погода в городе %s, %s:\n%s, ветер %s %s м/c, температура %s°C, ощущается как %s°C, относительная влажность %s%%, давление %s мм.рт.с", "Погода в городе %s, %s:\n%s, ветер %s %s м/c, температура %s°C, ощущается как %s°C, относительная влажность %s%%, давление %s мм.рт.ст",
$w->{name}, $w->{name},
$w->{country}, $w->{country},
ucfirst $w->{description}, ucfirst $w->{description},
@@ -48,9 +56,23 @@ sub weather {
$w->{humidity}, $w->{humidity},
$w->{pressure} $w->{pressure}
); );
} elsif ($w->{temperature_min} < 0 && $w->{temperature_max} <= 0) {
$reply = sprintf (
"Погода в городе %s, %s:\n%s, ветер %s %s м/c, температура от %s до %s°C, ощущается как %s°C, относительная влажность %s%%, давление %s мм.рт.ст",
$w->{name},
$w->{country},
ucfirst $w->{description},
$w->{wind_direction},
$w->{wind_speed},
$w->{temperature_max},
$w->{temperature_min},
$w->{temperature_feelslike},
$w->{humidity},
$w->{pressure}
);
} else { } else {
$reply = sprintf ( $reply = sprintf (
"Погода в городе %s, %s:\n%s, ветер %s %s м/c, температура %s-%s°C, ощущается как %s°C, относительная влажность %s%%, давление %s мм.рт.ст", "Погода в городе %s, %s:\n%s, ветер %s %s м/c, температура от %s до %s°C, ощущается как %s°C, относительная влажность %s%%, давление %s мм.рт.ст",
$w->{name}, $w->{name},
$w->{country}, $w->{country},
ucfirst $w->{description}, ucfirst $w->{description},
@@ -74,7 +96,6 @@ sub __weather {
my $city = shift; my $city = shift;
$city = urlencode $city; $city = urlencode $city;
my $appid = $c->{openweathermap}->{appid}; my $appid = $c->{openweathermap}->{appid};
my $cachedir = $c->{openweathermap}->{cachedir};
my $now = time (); my $now = time ();
my $fc; my $fc;
my $w; my $w;
@@ -112,11 +133,11 @@ sub __weather {
}; };
unless ($fc) { unless ($fc) {
cluck "[WARN] openweathermap returns corrupted json: $EVAL_ERROR"; $log->warn ("[WARN] openweathermap returns corrupted json: $EVAL_ERROR");
return undef; return undef;
}; };
} else { } else {
cluck sprintf 'Server return status %s with message: %s', $r->code, $r->message; $log->warn (sprintf '[WARN] Server return status %s with message: %s', $r->code, $r->message);
return undef; return undef;
} }

47
lib/BotLib/Xkcd.pm Normal file
View File

@@ -0,0 +1,47 @@
package BotLib::Xkcd;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use Mojo::UserAgent;
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (Xkcd);
sub Xkcd {
my $ua = Mojo::UserAgent->new->connect_timeout (5)->max_redirects (0);
my $location;
my $status = 400;
my $c = 0;
do {
my $r = $ua->get ('https://xkcd.ru/random/')->result;
if (
defined $r->content &&
defined $r->content->headers &&
defined $r->content->headers->location &&
$r->content->headers->location ne ''
) {
$location = substr $r->content->headers->location, 1, -1;
$location = sprintf 'https://xkcd.ru/i/%s_v1.png', $location;
$r = $ua->head ($location)->result;
}
$c++;
$status = $r->code;
} while ($c < 3 || $status >= 404);
if ($status == 200) {
return sprintf '[xkcd.ru](%s)', $location;
}
return 'Комикс-стрип нарисовать не так-то просто :(';
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -18,6 +18,13 @@ use Log::Any;
use Data::Dumper; use Data::Dumper;
use Teapot::Bot::Object::Message; use Teapot::Bot::Object::Message;
use Teapot::Bot::Object::Poll;
use Teapot::Bot::Object::PollAnswer;
use Teapot::Bot::Object::ChatMemberUpdated;
use Teapot::Bot::Object::User;
use Teapot::Bot::Object::Chat;
use Teapot::Bot::Object::ChatMember;
use Teapot::Bot::Object::ChatInviteLink;
$Teapot::Bot::Brain::VERSION = '0.022'; $Teapot::Bot::Brain::VERSION = '0.022';
@@ -132,6 +139,61 @@ sub getChatMember {
} }
} }
sub banChatMember {
my $self = shift;
my $args = shift || {};
my $token = $self->token || croak 'No token supplied to banChatMember?';
my $send_args = {};
unless ($args->{chat_id}) {
cluck 'No chat_id supplied to banChatMember()';
return {'error' => 1};
}
unless ($args->{user_id}) {
cluck 'No user_id supplied to banChatMember()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{user_id} = $args->{user_id};
$send_args->{until_date} = $args->{until_date} if exists $args->{until_date};
$send_args->{revoke_messages} = $args->{revoke_messages} if exists $args->{revoke_messages};
my $url = "https://api.telegram.org/bot${token}/banChatMember";
my $api_response = $self->_post_request($url, $send_args);
return;
}
sub unbanChatMember {
my $self = shift;
my $args = shift || {};
my $token = $self->token || croak 'No token supplied to unbanChatMember?';
my $send_args = {};
unless ($args->{chat_id}) {
cluck 'No chat_id supplied to unbanChatMember()';
return {'error' => 1};
}
unless ($args->{user_id}) {
cluck 'No user_id supplied to unbanChatMember()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{user_id} = $args->{user_id};
$send_args->{only_if_banned} = $args->{only_if_banned} if exists $args->{only_if_banned};
my $url = "https://api.telegram.org/bot${token}/unbanChatMember";
my $api_response = $self->_post_request($url, $send_args);
return;
}
sub getChat { sub getChat {
my $self = shift; my $self = shift;
@@ -160,6 +222,28 @@ sub getChat {
} }
sub leaveChat {
my $self = shift;
my $args = shift || {};
my $send_args = {};
unless ($args->{chat_id}) {
cluck 'No chat_id supplied to getChat()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
my $token = $self->token || croak 'No token supplied to getChat()?';
my $url = "https://api.telegram.org/bot${token}/leaveChat";
my $api_response = $self->_post_request($url, $send_args);
return;
}
sub sendMessage { sub sendMessage {
my $self = shift; my $self = shift;
my $args = shift || {}; my $args = shift || {};
@@ -277,7 +361,7 @@ sub deleteMessage {
my $token = $self->token || croak 'No token supplied to deleteMessage()?'; my $token = $self->token || croak 'No token supplied to deleteMessage()?';
if (Teapot::Bot::Object::ChatMember::canDeleteMessage($self, $args->{chat_id})) { if (Teapot::Bot::Object::ChatMember->canDeleteMessage($self, $args->{chat_id})) {
my $url = "https://api.telegram.org/bot${token}/deleteMessage"; my $url = "https://api.telegram.org/bot${token}/deleteMessage";
$self->_post_request($url, $send_args); $self->_post_request($url, $send_args);
} }
@@ -356,11 +440,11 @@ sub sendChatAction {
elsif ($args->{action} eq 'upload_video') { elsif ($args->{action} eq 'upload_video') {
$send_args->{action} = 'upload_video'; $send_args->{action} = 'upload_video';
} }
elsif ($args->{action} eq 'record_audio') { elsif ($args->{action} eq 'record_voice') {
$send_args->{action} = 'record_audio'; $send_args->{action} = 'record_voice';
} }
elsif ($args->{action} eq 'upload_audio') { elsif ($args->{action} eq 'upload_voice') {
$send_args->{action} = 'upload_audio'; $send_args->{action} = 'upload_voice';
} }
elsif ($args->{action} eq 'upload_document') { elsif ($args->{action} eq 'upload_document') {
$send_args->{action} = 'upload_document'; $send_args->{action} = 'upload_document';
@@ -374,6 +458,9 @@ sub sendChatAction {
elsif ($args->{action} eq 'upload_video_note') { elsif ($args->{action} eq 'upload_video_note') {
$send_args->{action} = 'upload_video_note'; $send_args->{action} = 'upload_video_note';
} }
elsif ($args->{action} eq 'choose_sticker') {
$send_args->{action} = 'choose_sticker';
}
else { else {
$send_args->{action} = 'typing'; $send_args->{action} = 'typing';
} }
@@ -385,6 +472,121 @@ sub sendChatAction {
return; return;
} }
sub exportChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $token = $self->token || croak 'No token supplied to createChatInviteLink()?';
unless ($args->{chat_id}) {
cluck 'No chat_id supplied in createChatInviteLink()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
my $url = "https://api.telegram.org/bot${token}/exportChatInviteLink";
my $api_response = $self->_post_request($url);
if ($api_response) {
return $api_response;
}
else {
return {'error' => 1};
}
}
sub createChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $token = $self->token || croak 'No token supplied to createChatInviteLink()?';
unless ($args->{chat_id}) {
cluck 'No chat_id supplied in createChatInviteLink()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{expire_date} = $args->{expire_date} if exists $args->{expire_date};
$send_args->{member_limit} = $args->{member_limit} if exists $args->{member_limit};
my $url = "https://api.telegram.org/bot${token}/createChatInviteLink";
my $api_response = $self->_post_request($url);
if ($api_response) {
return Teapot::Bot::Object::ChatInviteLink->create_from_hash($api_response, $self);
}
else {
return {'error' => 1};
}
}
sub editChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $token = $self->token || croak 'No token supplied to editChatInviteLink()?';
unless ($args->{chat_id}) {
cluck 'No chat_id supplied in editChatInviteLink()';
return {'error' => 1};
}
unless ($args->{invite_link}) {
cluck 'No invite_link supplied in editChatInviteLink()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{invite_link} = $args->{invite_link};
$send_args->{expire_date} = $args->{expire_date} if exists $args->{expire_date};
$send_args->{member_limit} = $args->{member_limit} if exists $args->{member_limit};
my $url = "https://api.telegram.org/bot${token}/editChatInviteLink";
my $api_response = $self->_post_request($url);
if ($api_response) {
return Teapot::Bot::Object::ChatInviteLink->create_from_hash($api_response, $self);
}
else {
return {'error' => 1};
}
}
sub revokeChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $token = $self->token || croak 'No token supplied to revokeChatInviteLink()?';
unless ($args->{chat_id}) {
cluck 'No chat_id supplied in revokeChatInviteLink()';
return {'error' => 1};
}
unless ($args->{invite_link}) {
cluck 'No invite_link supplied in revokeChatInviteLink()';
return {'error' => 1};
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{invite_link} = $args->{invite_link};
my $url = "https://api.telegram.org/bot${token}/revokeChatInviteLink";
my $api_response = $self->_post_request($url);
if ($api_response) {
return Teapot::Bot::Object::ChatInviteLink->create_from_hash($api_response, $self);
}
else {
return {'error' => 1};
}
}
sub _add_getUpdates_handler { sub _add_getUpdates_handler {
my $self = shift; my $self = shift;
@@ -414,7 +616,7 @@ sub _add_getUpdates_handler {
$self->_process_message($item); $self->_process_message($item);
} }
} else { } else {
cluck "Something nasty happen while handling updates from $updateURL : $EVAL_ERROR"; cluck "Unable to get update from API: $EVAL_ERROR";
} }
$http_active = 0; $http_active = 0;
@@ -433,11 +635,24 @@ sub _process_message {
# There can be several types of responses. But only one response. # There can be several types of responses. But only one response.
# https://core.telegram.org/bots/api#update # https://core.telegram.org/bots/api#update
my $update; my $update;
$update = Teapot::Bot::Object::Message->create_from_hash($item->{message}, $self) if $item->{message}; $update = Teapot::Bot::Object::Message->create_from_hash($item->{message}, $self) if $item->{message};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{edited_message}, $self) if $item->{edited_message}; $update = Teapot::Bot::Object::Message->create_from_hash($item->{edited_message}, $self) if $item->{edited_message};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{channel_post}, $self) if $item->{channel_post}; $update = Teapot::Bot::Object::Message->create_from_hash($item->{channel_post}, $self) if $item->{channel_post};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{edited_channel_post}, $self) if $item->{edited_channel_post}; $update = Teapot::Bot::Object::Message->create_from_hash($item->{edited_channel_post}, $self) if $item->{edited_channel_post};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{poll}, $self) if $item->{poll}; # Not yet implemented, we have no InlineQuery object yet
#$update = Teapot::Bot::Object::InlineQuery->create_from_hash($tem->{inline_query}, $self) if $item->{inline_query};
# Not yet implemented, we have no ChosenInlineResult object yet
#$update = Teapot::Bot::Object::ChosenInlineResult->create_from_hash($tem->{chosen_inline_result}, $self) if $item->{chosen_inline_result};
# Not yet implemented, we have no callback_query object yet, because we do not use this mechanism, s API should not gi us that shit
#$update = Teapot::Bot::Object::CallbackQuery->create_from_hash($tem->{callback_query}, $self) if $item->{callback_query};
# Not yet implemented, we have no ShippingQuery object yet
#$update = Teapot::Bot::Object::ShippingQuery->create_from_hash($tem->{shipping_query}, $self) if $item->{shipping_query};
# Not yet implemented, we have no PreCheckoutQuery object yet
#$update = Teapot::Bot::Object::PreCheckoutQuery->create_from_hash($tem->{pre_checkout_query}, $self) if $item->{pre_checkout_query};
$update = Teapot::Bot::Object::Poll->create_from_hash($item->{poll}, $self) if $item->{poll};
$update = Teapot::Bot::Object::PollAnswer->create_from_hash($item->{poll_answer}, $self) if $item->{poll_answer};
$update = Teapot::Bot::Object::ChatMemberUpdated->create_from_hash($item->{my_chat_member}, $self) if $item->{my_chat_member};
$update = Teapot::Bot::Object::ChatMemberUpdated->create_from_hash($item->{chat_member}, $self) if $item->{chat_member};
# if we got to this point without creating a response, it must be a type we # if we got to this point without creating a response, it must be a type we
# don't handle yet # don't handle yet
@@ -593,6 +808,25 @@ of Chat User.
On error returns hash reference with error set to 1 On error returns hash reference with error set to 1
=head2 banChatMember
This is the wrapper around the C<banChatMember> API method. See
L<https://core.telegram.org/bots/api#banchatmember>.
Takes chat_id, and user_id as arguments. And optionally until_date and
revoke_messages.
Returns nothing, it's send-only method (Telegram API returns true on success)
=head2 unbanChatMember
This is the wrapper around the C<unbanChatMember> API method. See
L<https://core.telegram.org/bots/api#unbanchatmember>.
Takes chat_id, and user_id as arguments. And optionally only_if_banned.
Returns nothing, it's send-only method (Telegram API returns true on success)
=head2 getChat =head2 getChat
This is the wrapper around the C<getChat> API method. See This is the wrapper around the C<getChat> API method. See
@@ -604,6 +838,15 @@ Returns the L<Teapot::Bot::Object::Chat> that represents properties of Chat.
On error returns hash reference with error set to 1 On error returns hash reference with error set to 1
=head2 leaveChat
This is the wrapper around the C<leaveChat> API method. See
L<https://core.telegram.org/bots/api#leavechat>.
Takes chat_id as argument.
Returns nothing, it's send-only method (Telegram API returns true on success)
=head2 sendMessage =head2 sendMessage
See L<https://core.telegram.org/bots/api#sendmessage>. See L<https://core.telegram.org/bots/api#sendmessage>.
@@ -641,6 +884,47 @@ See L<https://core.telegram.org/bots/api#sendchataction>.
Returns nothing, it's send-only method (Telegram API returns true on success) Returns nothing, it's send-only method (Telegram API returns true on success)
=head2 exportChatInviteLink
See L<https://core.telegram.org/bots/api#exportchatinvitelink>
Takes chat_id as argument.
Returns a raw api answer.
On error returns hash reference with error set to 1
=head2 createChatInviteLink
See L<https://core.telegram.org/bots/api#createchatinvitelink>
Takes chat_id as argument. And optionally expire_date and member_limit.
Returns a L<Teapot::Bot::Object::ChatInviteLink> object.
On error returns hash reference with error set to 1
=head2 editChatInviteLink
See L<https://core.telegram.org/bots/api#editchatinvitelink>
Takes chat_id and invite_link as arguments. And optionally expire_date
and member_limit.
Returns a L<Teapot::Bot::Object::ChatInviteLink> object.
On error returns hash reference with error set to 1
=head2 revokeChatInviteLink
See L<https://core.telegram.org/bots/api#revokechatinvitelink>
Takes chat_id and invite_link as arguments.
Returns a L<Teapot::Bot::Object::ChatInviteLink> object.
On error returns hash reference with error set to 1
=head1 SEE ALSO =head1 SEE ALSO
=over 4 =over 4

View File

@@ -8,6 +8,7 @@ use Carp qw/croak/;
$Teapot::Bot::Object::Animation::VERSION = '0.022'; $Teapot::Bot::Object::Animation::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'width'; has 'width';
has 'height'; has 'height';
has 'duration'; has 'duration';
@@ -17,7 +18,8 @@ has 'mime_type';
has 'file_size'; has 'file_size';
sub fields { sub fields {
return { scalar => [qw/file_id width height duration file_name mime_type file_size/], return { scalar => [qw/file_id file_unique_id width height duration file_name mime_type
file_size/],
'Teapot::Bot::Object::PhotoSize' => [qw/thumb/], 'Teapot::Bot::Object::PhotoSize' => [qw/thumb/],
}; };
} }

View File

@@ -6,15 +6,18 @@ use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::Audio::VERSION = '0.022'; $Teapot::Bot::Object::Audio::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'duration'; has 'duration';
has 'performer'; has 'performer'; # Optional. Performer of the audio as defined by sender or by audio tags
has 'title'; has 'title'; # Optional. Title of the audio as defined by sender or by audio tags
has 'mime_type'; has 'file_name'; # Optional. Original filename as defined by sender
has 'file_size'; has 'mime_type'; # Optional. MIME type of the file as defined by sender
has 'thumb'; #PhotoSize has 'file_size'; # Optional. File size
has 'thumb'; # Optional. Thumbnail of the album cover to which the music file belongs
sub fields { sub fields {
return { scalar => [qw/file_id duration performer title mime_type file_size/], return { scalar => [qw/file_id file_unique_id duration performer title file_name mime_type
file_size/],
'Teapot::Bot::Object::PhotoSize' => [qw/thumb/], 'Teapot::Bot::Object::PhotoSize' => [qw/thumb/],
}; };
} }

View File

@@ -5,6 +5,7 @@ use Mojo::Base -base;
use List::Util qw/any/; use List::Util qw/any/;
use Carp qw/croak cluck/; use Carp qw/croak cluck/;
use Data::Dumper; use Data::Dumper;
use JSON::PP ();
$Teapot::Bot::Object::Base::VERSION = '0.022'; $Teapot::Bot::Object::Base::VERSION = '0.022';
@@ -42,6 +43,28 @@ sub _field_is_array_of_arrays {
return; return;
} }
sub create_from_array {
my $class = shift;
my $array = shift;
my $brain = shift || croak 'No brain supplied to create_from_array()';
my $obj = shift || $class->new(_brain => $brain);
unless (defined $array) {
cluck 'Array is undef';
return $obj;
}
unless (ref ($array) eq 'ARRAY') {
cluck 'Not array ' . Dumper ($array);
return $obj;
}
cluck "Dumping class: " . Dumper ($class);
cluck "Dumping array: " . Dumper ($array);
# it's stub, return $obj
return $obj;
}
# create an object from a hash. Needs to deal with the nested types, and # create an object from a hash. Needs to deal with the nested types, and
# arrays # arrays
@@ -57,6 +80,10 @@ sub create_from_hash {
} }
unless (ref ($hash) eq 'HASH') { unless (ref ($hash) eq 'HASH') {
if (ref ($hash) eq 'ARRAY') {
return create_from_array($class, $hash, $brain);
}
cluck 'Not a hash ' . Dumper ($hash); cluck 'Not a hash ' . Dumper ($hash);
return $obj; return $obj;
} }

View File

@@ -11,27 +11,36 @@ $Teapot::Bot::Object::Chat::VERSION = '0.022';
has 'id'; has 'id';
has 'type'; has 'type';
has 'title'; # Optional. has 'title'; # Optional.
has 'username'; # Optional. has 'username'; # Optional.
has 'first_name'; # Optional. has 'first_name'; # Optional.
has 'last_name'; # Optional. has 'last_name'; # Optional.
has 'photo'; # Teapot::Bot::Object::ChatPhoto. Optional. Returned only in getChat. has 'photo'; # Teapot::Bot::Object::ChatPhoto. Optional. Returned only in getChat.
has 'description'; # Optional. Returned only in getChat. has 'bio'; # Optional. Bio of the other party in a private chat. Returned only in getChat.
has 'invite_link'; # Optional. Returned only in getChat. has 'has_private_forwards'; # Optional. True, if privacy settings of the other party in the private chat allows to
has 'pinned_message'; # Teapot::Bot::Object::Message # use tg://user?id=<user_id> links only in chats with the user.
has 'permissions'; # Teapot::Bot::Object::ChatPermissions. Optional. Default chat member permissions, for # Returned only in getChat.
# groups and supergroups. Returned only in getChat. has 'description'; # Optional. Returned only in getChat.
has 'slow_mode_delay'; # Optional. Returned only in getChat. has 'invite_link'; # Optional. Returned only in getChat.
has 'sticker_set_name'; # Optional. Returned only in getChat. has 'pinned_message'; # Teapot::Bot::Object::Message
has 'can_set_sticker_set'; # Optional. Returned only in getChat. has 'permissions'; # Teapot::Bot::Object::ChatPermissions. Optional. Default chat member permissions, for
has 'linked_chat_id'; # Optional. greater than 32 bits, smaller than 52 bits # groups and supergroups. Returned only in getChat.
has 'location'; # Optional. Returned only in getChat. has 'slow_mode_delay'; # Optional. Returned only in getChat.
has 'message_auto_delete_time'; # Optional. The time after which all messages sent to the chat will be automatically
# deleted; in seconds. Returned only in getChat.
has 'has_protected_content'; # Optional. True, if messages from the chat can't be forwarded to other chats.
# Returned only in getChat.
has 'sticker_set_name'; # Optional. Returned only in getChat.
has 'can_set_sticker_set'; # Optional. Returned only in getChat.
has 'linked_chat_id'; # Optional. greater than 32 bits, smaller than 52 bits
has 'location'; # Optional. Returned only in getChat.
sub fields { sub fields {
return { return {
'scalar' => [qw/id type title username first_name last_name description 'scalar' => [qw/id type title username first_name last_name bio
invite_link slow_mode_delay sticker_set_name has_private_forwards description invite_link slow_mode_delay
can_set_sticker_set linked_chat_id/], message_auto_delete_time has_protected_content
sticker_set_name can_set_sticker_set linked_chat_id/],
'Teapot::Bot::Object::ChatPhoto' => [qw/photo/], 'Teapot::Bot::Object::ChatPhoto' => [qw/photo/],
'Teapot::Bot::Object::Message' => [qw/pinned_message/], 'Teapot::Bot::Object::Message' => [qw/pinned_message/],
'Teapot::Bot::Object::ChatPermissions' => [qw/permissions/], 'Teapot::Bot::Object::ChatPermissions' => [qw/permissions/],

View File

@@ -0,0 +1,61 @@
package Teapot::Bot::Object::ChatInviteLink;
# ABSTRACT: The base class for Telegram message 'ChatInviteLink' type.
use Mojo::Base 'Teapot::Bot::Object::Base';
use Teapot::Bot::Object::User;
$Teapot::Bot::Object::ChatInviteLink::VERSION = '0.022';
has 'invite_link';
has 'creator';
has 'creates_join_request';
has 'is_primary';
has 'is_revoked';
has 'name';
has 'expire_date'; # Optional. Point in time (Unix timestamp) when the link will expire or has been expired
has 'member_limit'; # Optional. Maximum number of users that can be members of the chat simultaneously after joining
# the chat via this invite link; 1-99999
has 'pending_join_request_count';
sub fields {
return {
'scalar' => [qw/invite_link creates_join_request is_primary is_revoked name expire_date
member_limit pending_join_request_count/],
'Teapot::Bot::Object::User' => [qw/creator/],
};
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::ChatInviteLink - The base class for Telegram message 'ChatInviteLink' type
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram message 'ChatInviteLink' type.
See L<https://core.telegram.org/bots/api#chatinvitelink> for details of the
attributes available for L<Teapot::Bot::Object::ChatInviteLink> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -7,34 +7,66 @@ use Carp qw(cluck);
$Teapot::Bot::Object::ChatMember::VERSION = '0.022'; $Teapot::Bot::Object::ChatMember::VERSION = '0.022';
has 'user'; # ChatMemberOwner
has 'status'; has 'user';
has 'custom_title'; # optional. Owner and administrators only. has 'status';
has 'is_anonymous'; # Optional. Owner and administrators only. has 'custom_title'; # optional. Owner and administrators only.
has 'is_anonymous'; # Optional. Owner and administrators only.
has 'can_be_edited'; # optional. Administrators only. # ChatMemberAdministrator
has 'can_post_messages'; # optional. Administrators only. # has 'user';
has 'can_edit_messages'; # optional. Administrators only. # has 'status';
has 'can_delete_messages'; # optional. Administrators only. has 'can_be_edited'; # optional. Administrators only.
has 'can_restrict_members'; # optional. Administrators only. # has 'custom_title'; # optional. Owner and administrators only.
has 'can_promote_members'; # optional. Administrators only. # has 'is_anonymous'; # Optional. Owner and administrators only.
has 'can_change_info'; # optional. Administrators and restricted only. has 'can_manage_chat'; # Optional. Administrators only. True, if the administrator can access the chat event
has 'can_invite_users'; # optional. Administrators and restricted only. # log, chat statistics, message statistics in channels, see channel members, see
has 'can_pin_messages'; # optional. Administrators and restricted only. # anonymous administrators in supergroups and ignore slow mode. Implied by any other
has 'is_member'; # optional. Restricted only. # administrator privilege
has 'can_send_messages'; # optional. Restricted only. has 'can_post_messages'; # optional. Administrators only.
has 'can_send_media_messages'; # optional. Restricted only. has 'can_edit_messages'; # optional. Administrators only.
has 'can_send_polls'; # optional. Restricted only. has 'can_delete_messages'; # optional. Administrators only.
has 'can_send_other_messages'; # optional. Restricted only. has 'can_manage_voice_chats'; # Optional. Administrators only. True, if the administrator can manage voice chats
has 'can_add_web_page_previews'; # optional. Restricted only. has 'can_restrict_members'; # optional. Administrators only.
has 'until_date'; # optional. Restricted and kicked only. has 'can_promote_members'; # optional. Administrators only.
has 'can_change_info'; # optional. Administrators and restricted only.
has 'can_invite_users'; # optional. Administrators and restricted only.
has 'can_pin_messages'; # optional. Administrators and restricted only.
# ChatMemberMember
# has 'status';
# has 'user';
# ChatMemberRestricted
# has 'status';
# has 'user';
has 'is_member'; # optional. Restricted only.
# has 'can_change_info'; # optional. Administrators and restricted only.
# has 'can_invite_users'; # optional. Administrators and restricted only.
# has 'can_pin_messages'; # optional. Administrators and restricted only.
has 'can_send_messages'; # optional. Restricted only.
has 'can_send_media_messages'; # optional. Restricted only.
has 'can_send_polls'; # optional. Restricted only.
has 'can_send_other_messages'; # optional. Restricted only.
has 'can_add_web_page_previews'; # optional. Restricted only.
has 'until_date'; # optional. Restricted and kicked only.
# ChatMemberLeft
# has 'status';
# has 'user';
# ChatMemberBanned
# has 'status';
# has 'user';
# has 'until_date'; # optional. Restricted and kicked only.
sub fields { sub fields {
return { return {
'scalar' => [qw/status custom_title is_anonymous can_be_edited can_post_messages can_edit_messages 'scalar' => [qw/status custom_title is_anonymous can_be_edited can_manage_chat can_post_messages
can_delete_messages can_restrict_members can_promote_members can_change_info can_edit_messages can_delete_messages can_manage_voice_chats can_restrict_members
can_invite_users can_pin_messages is_member can_send_messages can_promote_members can_change_info can_invite_users can_pin_messages is_member
can_send_media_messages can_send_polls can_send_other_messages can_add_web_page_previews until_date/], can_send_messages can_send_media_messages can_send_polls can_send_other_messages
can_add_web_page_previews until_date/],
'Teapot::Bot::Object::User' => [qw/user/], 'Teapot::Bot::Object::User' => [qw/user/],
}; };
} }
@@ -51,7 +83,7 @@ sub canDeleteMessage {
my $chatobj = $self->getChat ({ 'chat_id' => $chatid }); my $chatobj = $self->getChat ({ 'chat_id' => $chatid });
# on api error, keep silence # on api error, keep silence
unless ($chatobj) { if ($chatobj == 0 || $chatobj->{error}) {
cluck "Unable to get chat info for $chatid from telegram API"; cluck "Unable to get chat info for $chatid from telegram API";
return 0; return 0;
} }
@@ -59,7 +91,7 @@ sub canDeleteMessage {
my $myObj = $self->getMe (); my $myObj = $self->getMe ();
# on api error, keep silence # on api error, keep silence
unless ($chatobj) { if ($myObj == 0 || $myObj->{error}) {
cluck "Unable to get chat info for $chatid from telegram API"; cluck "Unable to get chat info for $chatid from telegram API";
return 0; return 0;
} }
@@ -68,7 +100,7 @@ sub canDeleteMessage {
my $me = $self->getChatMember ({ 'chat_id' => $chatid, 'user_id' => $myid }); my $me = $self->getChatMember ({ 'chat_id' => $chatid, 'user_id' => $myid });
# on api error, keep silence # on api error, keep silence
unless ($me) { if ($me == 0 || $me->{error}) {
cluck 'Unable to get chat info for bot itself from telegram API'; cluck 'Unable to get chat info for bot itself from telegram API';
return 0; return 0;
} }

View File

@@ -0,0 +1,67 @@
package Teapot::Bot::Object::ChatMemberUpdated;
# ABSTRACT: The base class for Telegram 'ChatMemberUpdated' type objects
use Mojo::Base 'Teapot::Bot::Object::Base';
use Teapot::Bot::Object::Chat;
use Teapot::Bot::Object::User;
use Teapot::Bot::Object::ChatMember;
use Teapot::Bot::Object::ChatInviteLink;
use Teapot::Bot::Object::Voice;
$Teapot::Bot::Object::ChatMemberUpdated::VERSION = '0.022';
has 'chat';
has 'from';
has 'date';
has 'old_chat_member';
has 'new_chat_member';
has 'text'; # Undocumented as of Bot API 5.2
has 'voice'; # Undocumented as of Bot API 5.2
has 'invite_link'; # Optional. Chat invite link, which was used by the user to join the chat; for joining by
# invite link events only.
sub fields {
return {
'Teapot::Bot::Object::Chat' => [qw/chat/],
'Teapot::Bot::Object::User' => [qw/from/],
'scalar' => [qw/date text/],
'Teapot::Bot::Object::ChatMember' => [qw/old_chat_member new_chat_member/],
'Teapot::Bot::Object::Voice' => [qw/voice/],
'Teapot::Bot::Object::ChatInviteLink' => [qw/invite_link/]
};
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::ChatMemberUpdated - The base class for Telegram 'ChatMemberUpdated' type objects
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram 'ChatMemberUpdated' type objects.
See L<https://core.telegram.org/bots/api#chatmemberupdated> for details of the
attributes available for L<Teapot::Bot::Object::ChatMemberUpdated> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -39,26 +39,26 @@ sub canTalk {
my $chatobj = $self->_brain->getChat ({ 'chat_id' => $chatid }); my $chatobj = $self->_brain->getChat ({ 'chat_id' => $chatid });
# on api error, keep silence # on api error, keep silence
unless ($chatobj) { if ($chatobj == 0 || $chatobj->{error}) {
carp "Unable to get chat info for $chatid from telegram API"; carp "Unable to get chat info for $chatid from telegram API";
return 0; return $can_talk;
} }
my $myObj = $self->_brain->getMe (); my $myObj = $self->_brain->getMe ();
# on api error, keep silence # on api error, keep silence
unless ($chatobj) { if ($myObj == 0 || $myObj->{error}) {
carp "Unable to get chat info for $chatid from telegram API"; carp "Unable to get chat info for $chatid from telegram API";
return 0; return $can_talk;
} }
my $myid = $myObj->id; my $myid = $myObj->id;
my $me = $self->_brain->getChatMember ({ 'chat_id' => $chatid, 'user_id' => $myid }); my $me = $self->_brain->getChatMember ({ 'chat_id' => $chatid, 'user_id' => $myid });
# on api error, keep silence # on api error, keep silence
unless ($me) { if ($me == 0 || $me->{error}) {
carp 'Unable to get chat info for bot itself from telegram API'; carp 'Unable to get chat info for bot itself from telegram API';
return 0; return $can_talk;
} }
if ($me->{'status'} eq 'administrator') { if ($me->{'status'} eq 'administrator') {

View File

@@ -9,10 +9,11 @@ has 'phone_number';
has 'first_name'; has 'first_name';
has 'last_name'; has 'last_name';
has 'user_id'; has 'user_id';
has 'vcard';
sub fields { sub fields {
return { return {
scalar => [qw/phone_number first_name last_name user_id/], scalar => [qw/phone_number first_name last_name user_id vcard/],
}; };
} }

View File

@@ -8,6 +8,7 @@ use Teapot::Bot::Object::PhotoSize;
$Teapot::Bot::Object::Document::VERSION = '0.022'; $Teapot::Bot::Object::Document::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'thumb'; #PhotoSize has 'thumb'; #PhotoSize
has 'file_name'; has 'file_name';
has 'mime_type'; has 'mime_type';
@@ -15,7 +16,7 @@ has 'file_size';
sub fields { sub fields {
return { return {
scalar => [qw/file_id file_name mime_type file_size/], scalar => [qw/file_id file_unique_id file_name mime_type file_size/],
'Teapot::Bot::Object::PhotoSize' => [qw/thumb/] 'Teapot::Bot::Object::PhotoSize' => [qw/thumb/]
}; };
} }

View File

@@ -29,6 +29,11 @@ use Teapot::Bot::Object::PassportData;
use Teapot::Bot::Object::InlineKeyboardMarkup; use Teapot::Bot::Object::InlineKeyboardMarkup;
use Teapot::Bot::Object::ProximityAlertTriggered; use Teapot::Bot::Object::ProximityAlertTriggered;
use Teapot::Bot::Object::Dice; use Teapot::Bot::Object::Dice;
use Teapot::Bot::Object::MessageAutoDeleteTimerChanged;
use Teapot::Bot::Object::VoiceChatScheduled;
use Teapot::Bot::Object::VoiceChatStarted;
use Teapot::Bot::Object::VoiceChatEnded;
use Teapot::Bot::Object::VoiceChatParticipantsInvited;
use Data::Dumper; use Data::Dumper;
use Carp qw(cluck); use Carp qw(cluck);
@@ -47,9 +52,11 @@ has 'forward_from_message_id';
has 'forward_signature'; has 'forward_signature';
has 'forward_sender_name'; has 'forward_sender_name';
has 'forward_date'; has 'forward_date';
has 'is_automatic_forward';
has 'reply_to_message'; # Message has 'reply_to_message'; # Message
has 'via_bot'; # User has 'via_bot'; # User
has 'edit_date'; has 'edit_date';
has 'has_protected_content';
has 'media_group_id'; has 'media_group_id';
has 'author_signature'; has 'author_signature';
has 'text'; has 'text';
@@ -78,6 +85,7 @@ has 'delete_chat_photo';
has 'group_chat_created'; has 'group_chat_created';
has 'supergroup_chat_created'; has 'supergroup_chat_created';
has 'channel_chat_created'; has 'channel_chat_created';
has 'message_auto_delete_timer_changed'; # Optional. Service message: auto-delete timer settings changed in the chat
has 'migrate_to_chat_id'; has 'migrate_to_chat_id';
has 'migrate_from_chat_id'; has 'migrate_from_chat_id';
has 'pinned_message'; # Message has 'pinned_message'; # Message
@@ -86,14 +94,18 @@ has 'successful_payment'; # SuccessfulPayment
has 'connected_website'; has 'connected_website';
has 'passport_data'; # PassportData has 'passport_data'; # PassportData
has 'proximity_alert_triggered'; has 'proximity_alert_triggered';
has 'voice_chat_scheduled'; # Optional. Service message: voice chat scheduled
has 'voice_chat_started'; # Optional. Service message: voice chat started
has 'voice_chat_ended'; # Optional. Service message: voice chat ended
has 'voice_chat_participants_invited'; # Optional. Service message: new participants invited to a voice chat
has 'reply_markup'; # Array of InlineKeyboardMarkup has 'reply_markup'; # Array of InlineKeyboardMarkup
sub fields { sub fields {
return { return {
'scalar' => [qw/message_id date forward_from_message_id 'scalar' => [qw/message_id date forward_from_message_id
forward_signature forward_sender_name forward_signature forward_sender_name
forward_date edit_date media_group_id forward_date is_automatic_forward edit_date has_protected_content
author_signature text caption media_group_id author_signature text caption
new_chat_title delete_chat_photo new_chat_title delete_chat_photo
group_chat_created supergroup_chat_created group_chat_created supergroup_chat_created
channel_chat_created migrate_to_chat_id channel_chat_created migrate_to_chat_id
@@ -126,6 +138,11 @@ sub fields {
'Teapot::Bot::Object::InlineKeyboardMarkup' => [qw/reply_markup/], 'Teapot::Bot::Object::InlineKeyboardMarkup' => [qw/reply_markup/],
'Teapot::Bot::Object::ProximityAlertTriggered' => [qw/proximity_alert_triggered/], 'Teapot::Bot::Object::ProximityAlertTriggered' => [qw/proximity_alert_triggered/],
'Teapot::Bot::Object::Dice' => [qw/dice/], 'Teapot::Bot::Object::Dice' => [qw/dice/],
'Teapot::Bot::Object::MessageAutoDeleteTimerChanged' => [qw/message_auto_delete_timer_changed/],
'Teapot::Bot::Object::VoiceChatScheduled' => [qw/voice_chat_scheduled/],
'Teapot::Bot::Object::VoiceChatStarted' => [qw/voice_chat_started/],
'Teapot::Bot::Object::VoiceChatEnded' => [qw/voice_chat_ended/],
'Teapot::Bot::Object::VoiceChatParticipantsInvited' => [qw/voice_chat_participants_invited/],
}; };
} }
@@ -140,7 +157,7 @@ sub reply {
my $chatid = $self->chat->id; my $chatid = $self->chat->id;
if (Teapot::Bot::Object::ChatPermissions::canTalk($self, $chatid)) { if (Teapot::Bot::Object::ChatPermissions->canTalk($self, $chatid)) {
return $self->_brain->sendMessage({chat_id => $chatid, text => $text}); return $self->_brain->sendMessage({chat_id => $chatid, text => $text});
} }
@@ -153,7 +170,7 @@ sub replyMd {
my $chatid = $self->chat->id; my $chatid = $self->chat->id;
if (Teapot::Bot::Object::ChatPermissions::canTalk ($self, $chatid)) { if (Teapot::Bot::Object::ChatPermissions->canTalk ($self, $chatid)) {
my $send_args; my $send_args;
$send_args->{text} = $text; $send_args->{text} = $text;
$send_args->{parse_mode} = 'Markdown'; $send_args->{parse_mode} = 'Markdown';
@@ -170,7 +187,7 @@ sub typing {
my $chatid = $self->chat->id; my $chatid = $self->chat->id;
if (Teapot::Bot::Object::ChatPermissions::canTalk ($self, $chatid)) { if (Teapot::Bot::Object::ChatPermissions->canTalk ($self, $chatid)) {
return $self->_brain->sendChatAction({chat_id => $chatid}); return $self->_brain->sendChatAction({chat_id => $chatid});
} }

View File

@@ -0,0 +1,49 @@
package Teapot::Bot::Object::MessageAutoDeleteTimerChanged;
# ABSTRACT: The base class for Telegram 'MessageAutoDeleteTimerChanged' type objects
use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::MessageAutoDeleteTimerChanged::VERSION = '0.022';
has 'message_auto_delete_time';
sub fields {
return {
'scalar' => [qw/message_auto_delete_time/],
};
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::MessageAutoDeleteTimerChanged - The base class for Telegram 'MessageAutoDeleteTimerChanged' type objects
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram 'MessageAutoDeleteTimerChanged' type objects.
See L<https://core.telegram.org/bots/api#messageautodeletetimerchanged> for details of the
attributes available for L<Teapot::Bot::Object::MessageAutoDeleteTimerChanged> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -7,12 +7,13 @@ use Carp qw/croak/;
$Teapot::Bot::Object::PhotoSize::VERSION = '0.022'; $Teapot::Bot::Object::PhotoSize::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'width'; has 'width';
has 'height'; has 'height';
has 'file_size'; has 'file_size';
sub fields { sub fields {
return { scalar => [qw/file_id width height file_size/] }; return { scalar => [qw/file_id file_unique_id width height file_size/] };
} }
1; 1;

View File

@@ -1,9 +1,9 @@
package Teapot::Bot::Object::PollOption; package Teapot::Bot::Object::PollAnswer;
# ABSTRACT: The base class for Telegram 'PollOption' type objects # ABSTRACT: The base class for Telegram 'PollOption' type objects
use Mojo::Base 'Teapot::Bot::Object::Base'; use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::PollOption::VERSION = '0.022'; $Teapot::Bot::Object::PollAnswer::VERSION = '0.022';
has 'poll_id'; has 'poll_id';
has 'user'; has 'user';
@@ -30,17 +30,17 @@ __END__
=head1 NAME =head1 NAME
Teapot::Bot::Object::PollOption - The base class for Telegram 'PollOption' type objects Teapot::Bot::Object::PollAnswer - The base class for Telegram 'PollAnswer' type objects
=head1 VERSION =head1 VERSION
version 0.022 version 0.022
=head1 DESCRIPTION =head1 DESCRIPTION
The base class for Telegram 'PollOption' type objects. The base class for Telegram 'PollAnswer' type objects.
See L<https://core.telegram.org/bots/api#polloption> for details of the See L<https://core.telegram.org/bots/api#pollanswer> for details of the
attributes available for L<Teapot::Bot::Object::PollOption> objects. attributes available for L<Teapot::Bot::Object::PollAnswer> objects.
=head1 AUTHOR =head1 AUTHOR
@@ -48,7 +48,7 @@ Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT =head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2020 Sergei Fedosov <eleksir@gmail.com>. This software is copyright (c) 2021 Sergei Fedosov <eleksir@gmail.com>.
This is free software; you can redistribute it and/or modify it under This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself. the same terms as the Perl 5 programming language system itself.

View File

@@ -7,16 +7,19 @@ use Teapot::Bot::Object::PhotoSize;
$Teapot::Bot::Object::Video::VERSION = '0.022'; $Teapot::Bot::Object::Video::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'width'; has 'width';
has 'height'; has 'height';
has 'duration'; has 'duration';
has 'thumb'; #PhotoSize has 'thumb'; # Optional. Video thumbnail
has 'mime_type'; has 'file_name'; # Optional. Original filename as defined by sender
has 'file_size'; has 'mime_type'; # Optional. Mime type of a file as defined by sender
has 'file_size'; # Optional. File size
sub fields { sub fields {
return { return {
scalar => [qw/file_id width height duration mime_type file_size/], scalar => [qw/file_id file_unique_id width height duration file_name mime_type
file_size/],
'Teapot::Bot::Object::PhotoSize' => [qw/thumb/] 'Teapot::Bot::Object::PhotoSize' => [qw/thumb/]
}; };
} }

View File

@@ -8,6 +8,7 @@ use Teapot::Bot::Object::PhotoSize;
$Teapot::Bot::Object::VideoNote::VERSION = '0.022'; $Teapot::Bot::Object::VideoNote::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'length'; has 'length';
has 'duration'; has 'duration';
has 'mime_type'; has 'mime_type';
@@ -16,7 +17,7 @@ has 'file_size';
sub fields { sub fields {
return { return {
scalar => [qw/file_id length duration mime_type file_size/], scalar => [qw/file_id file_unique_id length duration mime_type file_size/],
'Teapot::Bot::Object::PhotoSize' => [qw/thumb /], 'Teapot::Bot::Object::PhotoSize' => [qw/thumb /],
}; };
} }

View File

@@ -6,12 +6,13 @@ use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::Voice::VERSION = '0.022'; $Teapot::Bot::Object::Voice::VERSION = '0.022';
has 'file_id'; has 'file_id';
has 'file_unique_id';
has 'duration'; has 'duration';
has 'mime_type'; has 'mime_type';
has 'file_size'; has 'file_size';
sub fields { sub fields {
return { scalar => [qw/file_id duration mime_type file_size/] }; return { scalar => [qw/file_id file_unique_id duration mime_type file_size/] };
} }
1; 1;

View File

@@ -0,0 +1,48 @@
package Teapot::Bot::Object::VoiceChatEnded;
# ABSTRACT: The base class for Telegram message 'VoiceChatEnded' type.
use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::VoiceChatEnded::VERSION = '0.022';
# This object represents a service message about a voice chat started in the chat. Currently holds no information.
has 'duration';
sub fields {
return { scalar => [qw/duration/] };
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::VoiceChatEnded - The base class for Telegram message 'VoiceChatEnded' type
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram message 'VoiceChatEnded' type.
See L<https://core.telegram.org/bots/api#voicechatended> for details of the
attributes available for L<Teapot::Bot::Object::VoiceChatEnded> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -0,0 +1,53 @@
package Teapot::Bot::Object::VoiceChatParticipantsInvited;
# ABSTRACT: The base class for Telegram message 'VoiceChatParticipantsInvited' type.
use Mojo::Base 'Teapot::Bot::Object::Base';
use Teapot::Bot::Object::User;
$Teapot::Bot::Object::VoiceChatParticipantsInvited::VERSION = '0.022';
# This object represents a service message about a voice chat started in the chat. Currently holds no information.
has 'users';
sub fields {
return { 'Teapot::Bot::Object::User' => [qw/users/] };
}
sub arrays {
return qw/users/;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::VoiceChatParticipantsInvited - The base class for Telegram message 'VoiceChatParticipantsInvited' type
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram message 'VoiceChatParticipantsInvited' type.
See L<https://core.telegram.org/bots/api#voicechatparticipantsinvited> for details of the
attributes available for L<Teapot::Bot::Object::VoiceChatParticipantsInvited> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -0,0 +1,48 @@
package Teapot::Bot::Object::VoiceChatScheduled;
# ABSTRACT: The base class for Telegram message 'VoiceChatEnded' type.
use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::VoiceChatScheduled::VERSION = '0.022';
# This object represents a service message about a voice chat started in the chat. Currently holds no information.
has 'start_date';
sub fields {
return { scalar => [qw/start_date/] };
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::VoiceChatScheduled - The base class for Telegram message 'VoiceChatScheduled' type
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram message 'VoiceChatScheduled' type.
See L<https://core.telegram.org/bots/api#voicechatscheduled> for details of the
attributes available for L<Teapot::Bot::Object::VoiceChatScheduled> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -0,0 +1,48 @@
package Teapot::Bot::Object::VoiceChatStarted;
# ABSTRACT: The base class for Telegram message 'VoiceChatStarted' type.
use Mojo::Base 'Teapot::Bot::Object::Base';
$Teapot::Bot::Object::VoiceChatStarted::VERSION = '0.022';
# This object represents a service message about a voice chat started in the chat. Currently holds no information.
has 'stub';
sub fields {
return { scalar => [qw/stub/] };
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Teapot::Bot::Object::VoiceChatStarted - The base class for Telegram message 'VoiceChatStarted' type
=head1 VERSION
version 0.022
=head1 DESCRIPTION
The base class for Telegram message 'VoiceChatStarted' type.
See L<https://core.telegram.org/bots/api#voicechatstarted> for details of the
attributes available for L<Teapot::Bot::Object::VoiceChatStarted> objects.
=head1 AUTHOR
Sergei Fedosov <eleksir@gmail.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2021 by Sergei Fedosov <eleksir@gmail.com>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut

View File

@@ -1,30 +1,31 @@
package telegrambot; package TelegramBot;
# main bot gears are here # main bot gears are here
use 5.018; use 5.018;
use strict; use strict;
use warnings; use warnings;
use utf8; use utf8;
use Data::Dumper;
use open qw (:std :utf8); use open qw (:std :utf8);
use English qw ( -no_match_vars ); use English qw ( -no_match_vars );
use Carp qw (cluck carp); use Carp qw (cluck carp);
use File::Path qw (make_path); use File::Path qw (make_path);
use Hailo; use Hailo;
use Log::Any qw ($log);
use Math::Random::Secure qw (irand); use Math::Random::Secure qw (irand);
use Mojo::Base 'Teapot::Bot::Brain'; use Mojo::Base 'Teapot::Bot::Brain';
use admin qw (fortune_toggle_list); use BotLib::Admin qw (FortuneToggleList);
use botlib qw (randomCommonPhrase command highlight botsleep isCensored); use BotLib qw (RandomCommonPhrase Command Highlight BotSleep IsCensored);
use conf qw (loadConf); use BotLib::Conf qw (LoadConf);
use fortune qw (fortune); use BotLib::Fortune qw (Fortune);
use karma qw (karmaSet); use BotLib::Image qw (Oboobs Obutts);
use util qw (trim fmatch); use BotLib::Karma qw (KarmaSet);
use BotLib::Util qw (trim fmatch);
use version; our $VERSION = qw (1.0); use version; our $VERSION = qw (1.0);
use Exporter qw (import); use Exporter qw (import);
our @EXPORT_OK = qw (run_telegrambot); our @EXPORT_OK = qw (RunTelegramBot);
my $c = loadConf (); my $c = LoadConf ();
my $hailo; my $hailo;
my $myid; my $myid;
my $myusername; my $myusername;
@@ -47,9 +48,9 @@ sub __cron {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime (time); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime (time);
if ($hour == 8 && ($min >= 0 && $min <= 14)) { if ($hour == 8 && ($min >= 0 && $min <= 14)) {
foreach my $enabledfortunechat (fortune_toggle_list ()) { foreach my $enabledfortunechat (FortuneToggleList ()) {
my $send_args; my $send_args;
$send_args->{text} = sprintf "%s\n```\n%s\n```\n", $intro[irand ($#intro + 1)], trim (fortune ()); $send_args->{text} = sprintf "%s\n```\n%s\n```\n", $intro[irand ($#intro + 1)], trim (Fortune ());
$send_args->{chat_id} = $enabledfortunechat; $send_args->{chat_id} = $enabledfortunechat;
$send_args->{parse_mode} = 'Markdown'; $send_args->{parse_mode} = 'Markdown';
$self->sendMessage ($send_args); $self->sendMessage ($send_args);
@@ -65,14 +66,14 @@ sub __on_msg {
my $chatid; my $chatid;
my $chatname = 'Noname chat'; my $chatname = 'Noname chat';
# user sending message info # user sending message info
my ($userid, $username, $fullname, $highlight, $vis_a_vi) = highlight $msg; my ($userid, $username, $fullname, $highlight, $vis_a_vi) = BotLib::Highlight ($msg);
my $csign = $c->{telegrambot}->{csign}; my $csign = $c->{telegrambot}->{csign};
unless ($myid) { unless ($myid) {
my $myObj = $self->getMe (); my $myObj = $self->getMe ();
unless ($myObj) { unless ($myObj) {
carp '[ERROR] Unable to get chatid, API Error?'; $log->error ('[ERROR] Unable to get chatid, API Error?');
return; return;
} }
@@ -105,7 +106,7 @@ sub __on_msg {
} }
} }
} else { } else {
carp '[INFO] Unable to get chatid'; $log->warn ('[INFO] Unable to get chatid');
return; return;
} }
@@ -114,21 +115,33 @@ sub __on_msg {
# Newcommer event, greet our new member and suggest to introduce themself. # Newcommer event, greet our new member and suggest to introduce themself.
if ($msg->can ('new_chat_members') && defined ($msg->new_chat_members)) { if ($msg->can ('new_chat_members') && defined ($msg->new_chat_members)) {
my @members; my @members;
my @introduce = (
'Дратути',
'Дарована',
'Доброе утро, день или вечер',
'Добро пожаловать в наше скромное коммунити',
'Наше вам с кисточкой тут, на канальчике',
);
foreach my $member (@{$msg->new_chat_members}) { foreach my $member (@{$msg->new_chat_members}) {
my $member_str = ''; my $member_str = '';
if ($member->can ('first_name') && defined ($member->first_name)) { # avoid naming people by spacing symbols, or empty names
if ($member->can ('first_name') && defined ($member->first_name) && $member->first_name !~ /^\s+$/ui) {
$member_str .= $member->first_name; $member_str .= $member->first_name;
if ($member->can ('last_name') && defined ($member->last_name)) { if ($member->can ('last_name') && defined ($member->last_name) && $member->last_name !~ /^\s+$/ui) {
$member_str .= ' ' . $member->last_name; $member_str .= ' ' . $member->last_name;
} }
} else { } else {
if ($member->can ('last_name') && defined ($member->last_name)) { if ($member->can ('last_name') && defined ($member->last_name) && $member->last_name !~ /^\s+$/ui) {
$member_str .= ' ' . $member->last_name; $member_str .= ' ' . $member->last_name;
} else { # username must be uniq and should contain only english letters and numbers, so...
} elsif ($member->can ('username') && defined ($member->username)) {
$member_str .= '@' . $member->username; $member_str .= '@' . $member->username;
# fallback to id if uose trying to be smartass and use only spacing chars in it's firstname and/or lastname
} else {
$member_str .= $member->id;
} }
} }
@@ -137,12 +150,12 @@ sub __on_msg {
if ($#members > 1) { if ($#members > 1) {
my $lastone = pop @members; my $lastone = pop @members;
$phrase = sprintf 'Дратути, %s и %s. Представьтес, пожалуйста, и расскажите, что вас сюда привело.', join (', ', @members), $lastone; $phrase = sprintf '%s, %s и %s. Представьтес, пожалуйста, и расскажите, что вас сюда привело.', $introduce[irand ($#introduce + 1)], join (', ', @members), $lastone;
} else { } else {
$phrase = sprintf 'Дратути, %s. Представьтес, пожалуйста, и расскажите, что вас сюда привело.', $members[0]; $phrase = sprintf '%s, %s. Представьтес, пожалуйста, и расскажите, что вас сюда привело.', $introduce[irand ($#introduce + 1)], $members[0];
} }
botsleep $msg; BotSleep $msg;
$msg->replyMd ($phrase); $msg->replyMd ($phrase);
return; return;
} }
@@ -158,9 +171,9 @@ sub __on_msg {
); );
if ($chatid < 0) { if ($chatid < 0) {
carp "[INFO] Initialized brain for public chat $chatname ($chatid): $brainname"; $log->info ("[INFO] Initialized brain for public chat $chatname ($chatid): $brainname");
} else { } else {
carp "[INFO] Initialized brain for private chat $chatname ($chatid): $brainname"; $log->info ("[INFO] Initialized brain for private chat $chatname ($chatid): $brainname");
} }
} }
@@ -171,11 +184,11 @@ sub __on_msg {
} }
my $text = $msg->text; my $text = $msg->text;
carp sprintf ('[DEBUG] Private chat %s say to bot: %s', $vis_a_vi, $text) if $c->{debug}; $log->debug (sprintf ('[DEBUG] Private chat %s say to bot: %s', $vis_a_vi, $text));
my $reply = 'Давайте ещё пообщаемся, а то я ещё не научилась от вас плохому.'; my $reply = 'Давайте ещё пообщаемся, а то я ещё не научилась от вас плохому.';
if (substr ($text, 0, 1) eq $csign) { if (substr ($text, 0, 1) eq $csign) {
$reply = command ($self, $msg, $text, $userid); $reply = Command ($self, $msg, $text, $userid);
} else { } else {
my $just_message_in_chat = 0; my $just_message_in_chat = 0;
@@ -184,27 +197,45 @@ sub __on_msg {
my @arr = split /\n/, $text; my @arr = split /\n/, $text;
if ($#arr < 1) { if ($#arr < 1) {
$reply = karmaSet ($chatid, trim (substr ($text, 0, -2)), substr ($text, -2)); $reply = KarmaSet ($chatid, trim (substr ($text, 0, -2)), substr ($text, -2));
} else { } else {
$just_message_in_chat = 1; $just_message_in_chat = 1;
} }
} else {
$just_message_in_chat = 1;
} }
if ($just_message_in_chat) { if ($just_message_in_chat) {
my $str = $hailo->{$msg->chat->id}->learn_reply ($text); if ($text =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Какие "люди"? Мы здесь вдвоём, только ты и я.';
} elsif ($text =~ /^ *кто +я *\?$/ui) {
$reply = 'Где?';
} elsif ($text =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Ты да я, да мы с тобой.';
} elsif ($text =~ /покажи +(сиськи|сиси|титьки) *$/ui) {
$reply = Oboobs ();
} elsif ($text =~ /show +(titts|tities|boobs) *$/i) {
$reply = Oboobs ();
} elsif ($text =~ /покажи +(попку|попу) *$/ui) {
$reply = Obutts ();
} elsif ($text =~ /show +(ass|butt|butty) *$/i) {
$reply = Obutts ();
} else {
my $str = $hailo->{$msg->chat->id}->learn_reply ($text);
if (defined ($str) && $str ne '') { if (defined ($str) && $str ne '') {
$reply = $str; $reply = $str;
$phrase = trim $text; $phrase = trim $text;
while ($phrase =~ /[\.|\,|\?|\!]$/) { while ($phrase =~ /[\.|\,|\?|\!]$/) {
chop $phrase; chop $phrase;
} }
$phrase = lc $phrase; $phrase = lc $phrase;
if (fmatch (lc ($reply), $phrase)) { if (fmatch (lc ($reply), $phrase)) {
$reply = randomCommonPhrase (); $reply = RandomCommonPhrase ();
}
} }
} }
} }
@@ -213,53 +244,64 @@ sub __on_msg {
if (defined $reply) { if (defined $reply) {
$msg->typing (); $msg->typing ();
sleep 1; sleep 1;
carp sprintf ('[DEBUG] Private chat bot reply to %s: %s', $vis_a_vi, $reply) if $c->{debug}; $log->debug (sprintf ('[DEBUG] Private chat bot reply to %s: %s', $vis_a_vi, $reply));
$msg->reply ($reply); $msg->reply ($reply);
} }
# group chat # group chat
} elsif (($msg->chat->type eq 'supergroup') or ($msg->chat->type eq 'group')) { } elsif (($msg->chat->type eq 'supergroup') or ($msg->chat->type eq 'group')) {
my $reply; my $reply;
if (isCensored $msg) { if (IsCensored $msg) {
carp sprintf '[INFO] In public chat %s (%s) message from %s was censored', $chatname, $chatid, $vis_a_vi; $log->info (sprintf '[INFO] In public chat %s (%s) message from %s was censored', $chatname, $chatid, $vis_a_vi);
$self->deleteMessage ({chat_id => $chatid, message_id => $msg->{message_id}}); $self->deleteMessage ({chat_id => $chatid, message_id => $msg->{message_id}});
} }
# detect and log messages without text, noop here # detect and log messages without text, noop here
unless (defined $msg->text) { unless (defined $msg->text) {
carp sprintf ('[INFO] No text in message from %s', $vis_a_vi); $log->debug (sprintf ('[DEBUG] No text in message from %s', $vis_a_vi));
return; return;
} }
# we have text here! so potentially we can chit-chat # we have text here! so potentially we can chit-chat
my $text = $msg->text; my $text = $msg->text;
carp sprintf ('[DEBUG] In public chat %s (%s) %s say: %s', $chatname, $chatid, $vis_a_vi, $text) if $c->{debug}; $log->debug (sprintf ('[DEBUG] In public chat %s (%s) %s say: %s', $chatname, $chatid, $vis_a_vi, $text));
# are they quote something, maybe, us? # are they quote something, maybe, us?
if (defined ($msg->reply_to_message) && if (defined ($msg->reply_to_message) &&
defined ($msg->reply_to_message->from) && defined ($msg->reply_to_message->from) &&
defined ($msg->reply_to_message->from->username) && defined ($msg->reply_to_message->from->username) &&
($msg->reply_to_message->from->username eq $myusername)) { ($msg->reply_to_message->from->username eq $myusername)) {
carp sprintf ('[DEBUG] In public chat %s (%s) %s quote us!', $chatname, $chatid, $vis_a_vi) if $c->{debug}; $log->debug (sprintf ('[DEBUG] In public chat %s (%s) %s quote us!', $chatname, $chatid, $vis_a_vi));
# do not answer back if someone quote our new member greet # do not answer back if someone quote our new member greet
if ((substr ($msg->reply_to_message->text, 0, 9) eq 'Дратути, ') && if (substr ($msg->reply_to_message->text, -61) eq 'Представьтес, пожалуйста, и расскажите, что вас сюда привело.') {
(substr ($msg->reply_to_message->text, -61) eq 'Представьтес, пожалуйста, и расскажите, что вас сюда привело.')) { my @hello = (
return; 'Дратути, ',
} else { 'Дарована, ',
# remove our name from users reply, just in case 'Доброе утро, день или вечер, ',
my $pat1 = quotemeta ('@' . $myusername); 'Добро пожаловать в наше скромное коммунити, ',
my $pat2 = quotemeta $myfullname; 'Наше вам с кисточкой тут, на канальчике, ',
$phrase = $text; );
$phrase =~ s/$pat1//g;
$phrase =~ s/$pat2//g; foreach my $hi (@hello) {
if (substr ($msg->reply_to_message->text, 0, length $hi) eq $hi) {
return;
}
}
}
# remove our name from users reply, just in case
my $pat1 = quotemeta ('@' . $myusername);
my $pat2 = quotemeta $myfullname;
$phrase = $text;
$phrase =~ s/$pat1//g;
$phrase =~ s/$pat2//g;
# figure out reply :) # figure out reply :)
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase) if (length ($phrase) > 3); $reply = $hailo->{$msg->chat->id}->learn_reply ($phrase) if (length ($phrase) > 3);
}
# simple commands # simple commands
} elsif (substr ($text, 0, 1) eq $csign) { } elsif (substr ($text, 0, 1) eq $csign) {
$reply = command ($self, $msg, $text, $chatid); $reply = Command ($self, $msg, $text, $chatid);
} elsif ( } elsif (
($text eq $myusername) or ($text eq $myusername) or
($text eq '@' . $myusername) or ($text eq '@' . $myusername) or
@@ -275,28 +317,63 @@ sub __on_msg {
# phrase directed to bot # phrase directed to bot
if ((lc ($text) =~ /^${qname}[\,|\:]? (.+)/) or (lc ($text) =~ /^${qtname}[\,|\:]? (.+)/)){ if ((lc ($text) =~ /^${qname}[\,|\:]? (.+)/) or (lc ($text) =~ /^${qtname}[\,|\:]? (.+)/)){
$phrase = $1; $phrase = $1;
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase);
if ($phrase =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Кто здесь?';
} elsif ($phrase =~ /^ *кто +я *?$/ui){
$reply = 'Где?';
} elsif ($phrase =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Здесь все: Никита, Стас, Гена, Турбо и Дюша Метёлкин.';
} else {
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase);
}
# bot mention by name # bot mention by name
} elsif ((lc ($text) =~ /.+ ${qname}[\,|\!|\?|\.| ]/) or (lc ($text) =~ / $qname$/)) { } elsif ((lc ($text) =~ /.+ ${qname}[\,|\!|\?|\.| ]/) or (lc ($text) =~ / $qname$/)) {
$phrase = $text; $phrase = $text;
$reply = $hailo->{$msg->chat->id}->reply($phrase);
if ($phrase =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Кто здесь?';
} elsif ($phrase =~ /^ *кто +я *?$/ui){
$reply = 'Где?';
} elsif ($phrase =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Здесь все: Никита, Стас, Гена, Турбо и Дюша Метёлкин.';
} else {
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase);
}
# bot mention by telegram name # bot mention by telegram name
} elsif ((lc ($text) =~ /.+ ${qtname}[\,|\!|\?|\.| ]/) or (lc ($text) =~ / $qtname$/)) { } elsif ((lc ($text) =~ /.+ ${qtname}[\,|\!|\?|\.| ]/) or (lc ($text) =~ / $qtname$/)) {
$phrase = $text; $phrase = $text;
$reply = $hailo->{$msg->chat->id}->reply ($phrase);
if ($phrase =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Кто здесь?';
} elsif ($phrase =~ /^ *кто +я *?$/ui){
$reply = 'Где?';
} elsif ($phrase =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Здесь все: Никита, Стас, Гена, Турбо и Дюша Метёлкин.';
} else {
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase);
}
# karma adjustment # karma adjustment
} elsif (substr ($text, -2) eq '++' || substr ($text, -2) eq '--') { } elsif (substr ($text, -2) eq '++' || substr ($text, -2) eq '--') {
my @arr = split /\n/, $text; my @arr = split /\n/, $text;
if ($#arr < 1) { if ($#arr < 1) {
$reply = karmaSet ($chatid, trim (substr ($text, 0, -2)), substr ($text, -2)); $reply = KarmaSet ($chatid, trim (substr ($text, 0, -2)), substr ($text, -2));
} else { } else {
# just message in chat # just message in chat
$hailo->{$msg->chat->id}->learn ($text); $hailo->{$msg->chat->id}->learn ($text);
} }
# just message in chat # just message in chat
} else { } else {
$hailo->{$msg->chat->id}->learn ($text); if ($text =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Кто здесь?';
} elsif ($phrase =~ /^ *кто +я *?$/ui){
$reply = 'Где?';
} elsif ($phrase =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Здесь все: Никита, Стас, Гена, Турбо и Дюша Метёлкин.';
} else {
$hailo->{$msg->chat->id}->learn ($text);
}
} }
} }
@@ -311,15 +388,15 @@ sub __on_msg {
$phrase = lc $phrase; $phrase = lc $phrase;
if (fmatch (lc ($reply), $phrase)) { if (fmatch (lc ($reply), $phrase)) {
$reply = randomCommonPhrase (); $reply = RandomCommonPhrase ();
} }
$msg->typing (); $msg->typing ();
sleep (irand 2); sleep (irand 2);
carp sprintf ('[DEBUG] In public chat %s (%s) bot reply to %s: %s', $chatname, $chatid, $vis_a_vi, $reply) if $c->{debug}; $log->debug (sprintf ('[DEBUG] In public chat %s (%s) bot reply to %s: %s', $chatname, $chatid, $vis_a_vi, $reply));
$msg->reply ($reply); $msg->reply ($reply);
} else { } else {
carp sprintf ('[DEBUG] In public chat %s (%s) bot is not required to reply to %s', $chatname, $chatid, $vis_a_vi) if $c->{debug}; $log->debug (sprintf ('[DEBUG] In public chat %s (%s) bot is not required to reply to %s', $chatname, $chatid, $vis_a_vi));
} }
# should be channel, so we can't talk # should be channel, so we can't talk
@@ -337,7 +414,7 @@ sub init {
unless (-d $braindir) { unless (-d $braindir) {
make_path ($braindir, 0, 0755) or do { ## no critic (ValuesAndExpressions::ProhibitLeadingZeros) make_path ($braindir, 0, 0755) or do { ## no critic (ValuesAndExpressions::ProhibitLeadingZeros)
cluck "Unable to create $braindir: $OS_ERROR"; $log->fatal ("[FATAL] Unable to create $braindir: $OS_ERROR");
exit 1; exit 1;
}; };
} }
@@ -347,10 +424,10 @@ sub init {
return; return;
} }
sub run_telegrambot { sub RunTelegramBot {
while (sleep 3) { while (sleep 3) {
eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval) eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
telegrambot->new->think; TelegramBot->new->think;
} }
} }

View File

@@ -1,392 +0,0 @@
package admin;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Carp qw (cluck croak);
use File::Path qw (make_path);
use SQLite_File;
use conf qw (loadConf);
use util qw (utf2sha1);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
# to export array we need @ISA here
our @ISA = qw / Exporter /; ## no critic (ClassHierarchies::ProhibitExplicitISA)
our @EXPORT_OK = qw (@forbiddenMessageTypes @pluginList getForbiddenTypes addForbiddenType delForbiddenType listForbidden fortune_toggle fortune_toggle_list fortune_status plugin_toggle plugin_status pluginEnabled);
my $c = loadConf ();
my $dir = $c->{admin}->{dir};
# this list is not yet used
our @pluginList = qw (oboobs obutts); ## no critic (Variables::ProhibitPackageVars)
our @forbiddenMessageTypes = qw (audio voice photo video video_note animation sticker dice game poll document); ## no critic (Variables::ProhibitPackageVars)
sub initCensorDB {
my $backingfile = shift;
tie my %censor, 'SQLite_File', $backingfile || cluck "Unable to tie to $backingfile: $OS_ERROR";
foreach (@forbiddenMessageTypes) {
$censor{$_} = 0;
}
untie %censor;
return;
}
sub getForbiddenTypes {
my $chatid = shift;
unless (-d $dir) {
unless (make_path $dir) {
cluck "Unable to create $dir: $OS_ERROR";
return;
}
}
my $messageTypes;
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/censor_%s.db', $dir, $backingfile;
my %censor;
unless (-f $backingfile) {
initCensorDB ($backingfile);
}
unless (tie %censor, 'SQLite_File', $backingfile) {
cluck "Unable to tie to $backingfile: $OS_ERROR";
return $messageTypes;
}
foreach (keys %censor) {
if ($censor{$_}) {
$messageTypes->{$_} = 1;
} else {
$messageTypes->{$_} = 0;
}
}
untie %censor;
return $messageTypes;
}
sub addForbiddenType {
my $chatid = shift;
my $type = shift;
unless (-d $dir) {
unless (make_path $dir) {
cluck "Unable to create $dir: $OS_ERROR";
return;
}
}
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/censor_%s.db', $dir, $backingfile;
unless (-f $backingfile) {
initCensorDB ($backingfile);
}
my %censor;
unless (tie %censor, 'SQLite_File', $backingfile) {
cluck "Unable to tie to $backingfile: $OS_ERROR";
return;
}
$censor{$type} = 1;
untie %censor;
return;
}
sub delForbiddenType {
my $chatid = shift;
my $type = shift;
unless (-d $dir) {
unless (make_path $dir) {
cluck "Unable to create $dir: $OS_ERROR";
return;
}
}
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/censor_%s.db', $dir, $backingfile;
unless (-f $backingfile) {
initCensorDB ($backingfile);
return;
}
my %censor;
unless (tie %censor, 'SQLite_File', $backingfile) {
cluck "Unable to tie to $backingfile: $OS_ERROR";
return;
}
$censor{$type} = 0;
untie %censor;
return;
}
sub listForbidden {
my $chatid = shift;
unless (-d $dir) {
unless (make_path $dir) {
cluck "Unable to create $dir: $OS_ERROR";
return;
}
}
my @list;
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/censor_%s.db', $dir, $backingfile;
unless (-f $backingfile) {
initCensorDB ($backingfile);
}
my %censor;
# FIXME: return list with all types alowed
unless (tie %censor, 'SQLite_File', $backingfile) {
cluck "Unable to tie to $backingfile: $OS_ERROR";
return '';
}
foreach (keys %censor) {
if ($censor{$_}) {
push @list, sprintf 'Тип сообщения %s удаляется', $_;
} else {
push @list, sprintf 'Тип сообщения %s не удаляется', $_;
}
}
untie %censor;
return join "\n", @list;
}
sub fortune_toggle (@) {
my $chatid = shift;
my $action = shift // undef;
my $phrase = 'Фортунка с утра ';
unless (-d $dir) {
make_path ($dir) || do {
cluck "[ERROR] Unable to create $dir: $OS_ERROR";
return;
};
}
my $backingfile = sprintf '%s/fortune_morning.sqlite', $dir;
tie my %toggle, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR";
return;
};
my $state = $toggle{$chatid};
if (defined $action) {
if ($action) {
$toggle{$chatid} = 1;
$phrase .= 'будет показываться.';
} else {
if (defined $state) {
delete $toggle{$chatid};
}
$phrase .= 'не будет показываться.';
}
} else {
if (defined $state && $state) {
delete $toggle{$chatid};
$phrase .= 'не будет показываться.';
} else {
$toggle{$chatid} = 1;
$phrase .= 'будет показываться.';
}
}
untie %toggle;
return $phrase;
}
sub fortune_status ($) {
my $chatid = shift;
my $phrase = 'Фортунка с утра ';
unless (-d $dir) {
make_path ($dir) || do {
cluck "[ERROR] Unable to create $dir: $OS_ERROR";
return;
};
}
my $backingfile = sprintf '%s/fortune_morning.sqlite', $dir;
tie my %toggle, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR\n";
return;
};
my $state = $toggle{$chatid};
if (defined $state && $state) {
$phrase .= 'показывается.';
} else {
$phrase .= 'не показывается.';
}
untie %toggle;
return $phrase;
}
sub fortune_toggle_list () {
unless (-d $dir) {
make_path ($dir) || do {
cluck "[ERROR] Unable to create $dir: $OS_ERROR";
my @empty = ();
return @empty;
}
}
my $backingfile = sprintf '%s/fortune_morning.sqlite', $dir;
tie my %toggle, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR";
my @empty = ();
return @empty;
};
my @list = keys %toggle;
untie %toggle;
return @list;
}
sub plugin_status (@) {
my $chatid = shift;
my $plugin = shift;
my $phrase = "Плагин $plugin ";
unless (-d $dir) {
make_path ($dir) || do {
cluck "[ERROR] Unable to create $dir: $OS_ERROR";
return;
};
}
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/%s.sqlite', $dir, $backingfile;
tie my %toggle, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR\n";
return;
};
my $state = $toggle{$plugin};
untie %toggle;
if (defined $state && $state) {
$phrase .= 'включён.';
} else {
$phrase .= 'выключен.';
}
return $phrase;
}
sub pluginEnabled (@) {
my $chatid = shift;
my $plugin = shift;
unless (-d $dir) {
make_path ($dir) || do {
cluck "[ERROR] Unable to create $dir: $OS_ERROR";
return;
};
}
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/%s.sqlite', $dir, $backingfile;
tie my %toggle, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR\n";
return;
};
my $state = $toggle{$plugin};
untie %toggle;
if (defined $state && $state) {
return 1;
} else {
return 0;
}
}
sub plugin_toggle (@) {
my $chatid = shift;
my $plugin = shift;
my $action = shift // undef;
my $phrase = "Плагин $plugin ";
unless (-d $dir) {
make_path ($dir) || do {
cluck "[ERROR] Unable to create $dir: $OS_ERROR";
my @empty = ();
return @empty;
}
}
my $backingfile = utf2sha1 $chatid;
$backingfile =~ s/\//-/xmsg;
$backingfile = sprintf '%s/%s.sqlite', $dir, $backingfile;
tie my %toggle, 'SQLite_File', $backingfile || do {
cluck "[ERROR] Unable to tie to $backingfile: $OS_ERROR";
my @empty = ();
return @empty;
};
if (defined $action) {
if ($action) {
$toggle{$plugin} = 1;
$phrase .= 'включён';
} else {
$toggle{$plugin} = 0;
$phrase .= 'выключен';
}
} else {
if (defined ($toggle{$plugin}) && $toggle{$plugin}) {
$toggle{$plugin} = 0;
$phrase .= 'включён';
} else {
$toggle{$plugin} = 1;
$phrase .= 'выключен';
}
}
untie %toggle;
return $phrase;
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,126 +0,0 @@
package archeologist;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Math::Random::Secure qw (irand);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (dig);
sub dig {
my $name = shift;
my @artifact = (
'амулет Мерлина',
'библиотеку Ивана Грозного',
'деревянную плошку',
'диплока',
'доски Ноева Ковчега',
'древний презерватив из бараньей кишки',
'икону',
'инсталятор Windows 3.11',
'истлевший гроб',
'каменный топор',
'клад со златом-серебром',
'кривой нож',
'ледяную глыбу',
'лунный грунт',
'мамонта',
'мамонтёнка',
'метеорит',
'мумию фараона Тутанхамона',
'носки прапорщика (поставит в угол)',
'обломки грузинской керамики',
'птеродактиля',
'рубль с Лениным',
'рыцарские доспехи',
'синий мох',
'скелет',
'скелет собаки по кличке Беня',
'тиранозавтра',
'тотем индейцев',
'трубу с туркменским газом',
'скорбь еврейского народа',
'залежи активированного угля',
'не очень новый завет',
'мать леннарта поттеринга',
'руководство "начинающему археологу"',
'слово о полку игореве и дело о полку игореве, вместе с игорем',
'телепрограмму канала A-one',
'могилу искателя могил',
'заплесневевший кусок хлеба',
'преемник радиоприемника',
'глушитель от ВАЗ-2106',
'чувство удовлетворения',
'крота',
'воспоминания о былой любви',
'грязистор',
'египетское мумиё',
'окаменелый камень',
'кладку яиц рептилоида',
'яму',
'чеснок и стрелы',
'доллар по 30',
'несмазанные скрижали',
'четырелобита',
'чучело тролля',
'целое ничего',
);
my @location = (
'в архивах',
'в гималаях',
'в интернете',
'в кармане',
'в клумбе',
'в лунном кратере',
'во льдах арктики',
'в морге',
'в окрестностях погоста',
'в песочнице',
'в пустыне Гоби',
'в пустыне Сахаре',
'в русле высохшей реки',
'в собственной памяти',
'в старом сортире',
'в углу',
'в цветочном горшке',
'на бабушкином огороде',
'на винчестере',
'на горе Арарат',
'на горном плато',
'на дне моря',
'на необитаемом острове',
'на первом слое сумрака',
'на просторах Украины',
);
my $find_probability = irand 100;
my $success_probability = irand 100;
my $artifact_age = irand 15000;
if ($find_probability <= 20) {
return sprintf 'По уши закопавшись %s, %s, нифига вы не выкопали! Может повезет в другом месте?', $location[irand ($#location + 1)], $name;
}
my $phrase = sprintf "Вы начали раскопки %s и усиленно роете лопатами, экскаватором...\n", $location[irand ($#location + 1)];
$phrase .= "Вам кажется что ваш совочек ударился обо что-то твердое. Может, это клад?!\n\n";
if ($success_probability <= 25) {
$phrase .= sprintf '%s стал лучшим археологом, выкопав %s! возраст артефакта - %s лет!', $name, $artifact[irand ($#artifact + 1)], $artifact_age;
} else {
$phrase .= sprintf "Поздравляю, %s! Вы только что выкопали %s, возраст - %s лет!\n", $name, $artifact[irand ($#artifact + 1)], $artifact_age;
$phrase .= sprintf 'Извини, %s, но на артефакт это не тянет! Однако, попытка хорошая!', $name;
}
return $phrase;
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,137 +0,0 @@
package karma;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Carp qw (cluck);
use Digest::SHA qw (sha1_base64);
use DB_File;
use File::Path qw (make_path);
use conf qw (loadConf);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (karmaSet karmaGet);
my $c = loadConf ();
my $karmadir = $c->{karma}->{dir};
my $max = 5;
# swallow phrase and return answer
sub karmaSet (@) {
my $chatid = shift;
my $phrase = shift;
my $action = shift;
my $score = 0;
# sha1* does not understand utf8, so explicitly construct string cost of decimal numbers only
my $karmafile = join ('', unpack ('C*', $chatid));
$karmafile = sha1_base64 $karmafile;
# sha1 string can contain slashes, so make a bit longer string consist of only decimal numbers
$karmafile = join ('', unpack ('C*', $karmafile));
$karmafile = sprintf '%s/%s.db', $karmadir, $karmafile;
if (($phrase eq '') || ($phrase =~ /^ +$/)) {
$phrase = '';
}
unless (-d $karmadir) {
make_path ($karmadir) or do {
cluck "Unable to create $karmadir: $OS_ERROR";
return sprintf 'Карма %s составляет 0', $phrase;
};
}
# init hash, store phrase and score
tie my %karma, 'DB_File', $karmafile || do {
cluck "[ERROR] Something nasty happen when cachedata ties to its data: $OS_ERROR";
return sprintf 'Карма %s составляет 0', $phrase;
};
# bdb does not understand utf8, so tied hash too, we do not want store original phrase anyway
my $sha1_phrase = sha1_base64 (join ('', unpack ('C*', $phrase)));
if (defined $karma{$sha1_phrase}) {
if ($action eq '++') {
$score = $karma{$sha1_phrase} + 1;
$karma{$sha1_phrase} = $score;
} else {
$score = $karma{$sha1_phrase} - 1;
$karma{$sha1_phrase} = $score;
}
} else {
if ($action eq '++') {
$karma{$sha1_phrase} = 1;
$score = 1;
} else {
$karma{$sha1_phrase} = -1;
$score = -1;
}
}
untie %karma;
if ($score < -1 && (($score % (0 - $max)) + 1) == 0) {
if ($phrase eq '') {
return sprintf 'Зарегистрировано пробитие дна, карма пустоты составляет %d', $score;
} else {
return sprintf 'Зарегистрировано пробитие дна, карма %s составляет %d', $phrase, $score;
}
} else {
if ($phrase eq '') {
return sprintf 'Карма пустоты составляет %d', $score;
} else {
return sprintf 'Карма %s составляет %d', $phrase, $score;
}
}
}
# just return answer
sub karmaGet (@) {
my $chatid = shift;
my $phrase = shift;
# sha1* does not understand utf8, so explicitly construct string cost of decimal numbers only
my $karmafile = join ('', unpack ('C*', $chatid));
$karmafile = sha1_base64 $karmafile;
# sha1 string can contain slashes, so make a bit longer string consist of only decimal numbers
$karmafile = join ('', unpack ('C*', $karmafile));
$karmafile = sprintf '%s/%s.db', $karmadir, $karmafile;
if (($phrase eq '') || ($phrase =~ /^ +$/)) {
$phrase = '';
}
unless (-d $karmadir) {
make_path ($karmadir) or do {
cluck "Unable to create $karmadir: $OS_ERROR";
return sprintf 'Карма %s составляет 0', $phrase;
};
}
# init hash, store phrase and score
tie my %karma, 'DB_File', $karmafile || do {
cluck "[ERROR] Something nasty happen when karma ties to its data: $OS_ERROR";
return sprintf 'Карма %s составляет 0', $phrase;
};
# bdb does not understand utf8, so tied hash too, we do not want store original phrase anyway
my $sha1_phrase = sha1_base64 (join ('', unpack ('C*', $phrase)));
my $score = $karma{$sha1_phrase};
untie %karma;
unless (defined $score) {
$score = 0;
}
if ($phrase eq '') {
return sprintf 'Карма пустоты составляет %d', $score;
} else {
return sprintf 'Карма %s составляет %d', $phrase, $score;
}
}
1;
# vim: ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,32 +0,0 @@
package xkcd;
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use Mojo::UserAgent;
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (xkcd);
sub xkcd {
my $ua = Mojo::UserAgent->new->connect_timeout (5)->max_redirects (0);
my $r = $ua->get ('https://xkcd.ru/random/')->result;
if (
defined $r->content &&
defined $r->content->headers &&
defined $r->content->headers->location &&
$r->content->headers->location ne ''
) {
return sprintf '[xkcd.ru](https://xkcd.ru/i/%s_v1.png)', substr ($r->content->headers->location, 1, -1);
}
return 'Комикс-стрип нарисовать не так-то просто :(';
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4: