mirror of
https://github.com/maxpozdeev/mytinytodo.git
synced 2026-03-11 08:55:27 +00:00
+ add extension for Notifications
This commit is contained in:
parent
0512a5d1a6
commit
5370d36b30
10 changed files with 785 additions and 0 deletions
1
src/ext/notifications/.htaccess
Normal file
1
src/ext/notifications/.htaccess
Normal file
|
|
@ -0,0 +1 @@
|
|||
deny from all
|
||||
116
src/ext/notifications/class.controller.php
Normal file
116
src/ext/notifications/class.controller.php
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
namespace Notify;
|
||||
|
||||
use NotificationsExtension;
|
||||
use Config;
|
||||
|
||||
class Controller extends \ApiController
|
||||
{
|
||||
function postDeactivateAll()
|
||||
{
|
||||
$prefs = Config::requestDomain(NotificationsExtension::domain);
|
||||
if (isset($prefs['chats'])) {
|
||||
$prefs['chats'] = [];
|
||||
Config::saveDomain(NotificationsExtension::domain, $prefs);
|
||||
}
|
||||
$this->response->data = [ 'total' => 1, 'msg' => __("notifications.all_chats_deactivated") ];
|
||||
}
|
||||
|
||||
function postCheck()
|
||||
{
|
||||
$prefs = Config::requestDomain(NotificationsExtension::domain);
|
||||
if (!($prefs['validToken'] ?? false)) {
|
||||
$this->response->data = [ 'total' => 0, 'msg' => __("notifications.bot_not_configured") ];
|
||||
return;
|
||||
}
|
||||
if (!isset($prefs['chats']) || !is_array($prefs['chats'])) {
|
||||
$prefs['chats'] = [];
|
||||
}
|
||||
$code = $prefs['code'] ?? null;
|
||||
$codeExpires = $prefs['codeExpires'] ?? 0;
|
||||
$token = $prefs['token'] ?? '';
|
||||
|
||||
$this->response->data = [ 'total' => 0, 'msg' => __("notifications.no_new_chats") ];
|
||||
|
||||
// Read messages since last check
|
||||
$maxId = $prefs['lastUpdateId'] ?? 0;
|
||||
$api = new TelegramApi($token);
|
||||
$updates = $api->getUpdates([
|
||||
'offset' => $maxId + 1,
|
||||
'allowed_updates' => ['message']
|
||||
]);
|
||||
if (!is_array($updates) || count($updates) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select last message in every chat
|
||||
$messages = array();
|
||||
foreach ($updates as $update) {
|
||||
$message = $update['message'] ?? [];
|
||||
$chatId = (string)($message['chat']['id'] ?? 0);
|
||||
$prefs['lastUpdateId'] = max($maxId, $update['update_id'] ?? 0);
|
||||
$messages[$chatId] = $message;
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
foreach ($messages as $chatId => $message) {
|
||||
$chatId = (int) $chatId;
|
||||
$text = $message['text'] ?? '';
|
||||
$msgId = (int) ($message['message_id'] ?? 0);
|
||||
if (in_array($chatId, $prefs['chats'])) {
|
||||
$api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'text' => __("notifications.already_active")
|
||||
]);
|
||||
}
|
||||
else if ($text === '/start') {
|
||||
$api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'text' => __("notifications.please_send")
|
||||
]);
|
||||
}
|
||||
else if ($code === null) {
|
||||
$api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'text' => __("notifications.code_not_set")
|
||||
]);
|
||||
}
|
||||
else if ($codeExpires < time()) {
|
||||
$api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'text' => __("notifications.code_expired")
|
||||
]);
|
||||
}
|
||||
else if ($text == $code) {
|
||||
$prefs['chats'][] = $chatId;
|
||||
$api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'reply_to_message_id' => $msgId,
|
||||
'text' => __("notifications.activated")
|
||||
]);
|
||||
$total++;
|
||||
$this->response->data = [
|
||||
'total' => $total,
|
||||
'msg' => __("notifications.activated")
|
||||
];
|
||||
}
|
||||
else {
|
||||
$api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'reply_to_message_id' => $msgId,
|
||||
'text' => __("notifications.code_wrong")
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Config::saveDomain(NotificationsExtension::domain, $prefs);
|
||||
}
|
||||
|
||||
}
|
||||
65
src/ext/notifications/class.observer.php
Normal file
65
src/ext/notifications/class.observer.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
namespace Notify;
|
||||
|
||||
use NotificationsExtension;
|
||||
use MTTNotification;
|
||||
use MTTNotificationCenter;
|
||||
use DBConnection;
|
||||
|
||||
|
||||
class NotificationObserver implements \MTTNotificationObserverInterface
|
||||
{
|
||||
private $prefs = null;
|
||||
private $delayedNotifications = [];
|
||||
|
||||
public function notification(string $notification, $object)
|
||||
{
|
||||
if (!$this->prefs) {
|
||||
$this->init();
|
||||
}
|
||||
if (count($this->prefs['chats']) == 0 && count($this->prefs['emails']) == 0) {
|
||||
return; // nobody to notify
|
||||
}
|
||||
|
||||
$db = DBConnection::instance();
|
||||
switch ($notification) {
|
||||
case MTTNotification::didFinishRequest:
|
||||
$this->processDelayed();
|
||||
break;
|
||||
case MTTNotification::didCreateTask:
|
||||
case MTTNotification::didCreateList:
|
||||
// Get list name
|
||||
$list = $db->sqa( "SELECT name FROM {$db->prefix}lists WHERE id=?", array($object['listId'] ?? 0) );
|
||||
$object['listName'] = htmlspecialchars($list['name'] ?? '');
|
||||
$this->delayedNotifications[] = [
|
||||
'notification' => $notification,
|
||||
'object' => $object
|
||||
];
|
||||
MTTNotificationCenter::addObserverForNotification(MTTNotification::didFinishRequest, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function processDelayed()
|
||||
{
|
||||
$db = DBConnection::instance();
|
||||
$useCli = !function_exists('fastcgi_finish_request');
|
||||
$sender = new Sender( $this->prefs, $useCli );
|
||||
foreach ($this->delayedNotifications as $item) {
|
||||
$sender->notify($item);
|
||||
}
|
||||
}
|
||||
|
||||
private function init()
|
||||
{
|
||||
$this->prefs = NotificationsExtension::preferences();
|
||||
$this->token = $this->prefs['token'] ?? '';
|
||||
}
|
||||
|
||||
}
|
||||
191
src/ext/notifications/class.sender.php
Normal file
191
src/ext/notifications/class.sender.php
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
namespace Notify;
|
||||
|
||||
use NotificationsExtension;
|
||||
use MTTNotification;
|
||||
|
||||
|
||||
class Sender
|
||||
{
|
||||
private $prefs;
|
||||
private $cli = false;
|
||||
|
||||
function __construct(array $prefs, bool $useCli = false)
|
||||
{
|
||||
$this->prefs = $prefs;
|
||||
if ($useCli && function_exists('pcntl_fork')) {
|
||||
$this->cli = true;
|
||||
}
|
||||
}
|
||||
|
||||
function notify(array $item)
|
||||
{
|
||||
$notification = $item['notification'] ?? '';
|
||||
$object = $item['object'] ?? null;
|
||||
switch ($notification) {
|
||||
case MTTNotification::didCreateTask: $this->notifyTaskCreated($object); break;
|
||||
case MTTNotification::didCreateList: $this->notifyListCreated($object); break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
$task['title'], $task['tags'], $task['duedate'], $task['listName'] are already escaped
|
||||
*/
|
||||
private function notifyTaskCreated($task)
|
||||
{
|
||||
$link = get_mttinfo('url'). '?task='. $task['id'];
|
||||
|
||||
//email
|
||||
if (count($this->prefs['emails']) > 0) {
|
||||
$aText = [];
|
||||
$aText[] = "New task in ". htmlspecialchars_decode($task['listName']). ":";
|
||||
$aText[] = htmlspecialchars_decode($task['title']);
|
||||
$aText[] = "";
|
||||
if ($task['duedate'] != '') {
|
||||
$aText[] = "Due: ". htmlspecialchars_decode($task['duedate']);
|
||||
}
|
||||
if ($task['tags'] != '') {
|
||||
$aText[] = "Tags: ". implode(", ", preg_split("/,\s*/", htmlspecialchars_decode($task['tags']), -1, PREG_SPLIT_NO_EMPTY));
|
||||
}
|
||||
if ($aText[count($aText)-1] != '') {
|
||||
$aText[] = "";
|
||||
}
|
||||
$aText[] = "Link: $link";
|
||||
$text = implode("\r\n", $aText);
|
||||
|
||||
$this->sendEmails( $text, "New task #". $task['id']);
|
||||
}
|
||||
|
||||
// telegram
|
||||
if (count($this->prefs['chats']) > 0) {
|
||||
$text = "New task <a href=\"$link\">#". $task['id']. "</a> in ". $task['listName'] .": ". $task['title'];
|
||||
if ($task['duedate'] != '') {
|
||||
$text .= "\nDue: ". $task['duedate'];
|
||||
}
|
||||
if ($task['tags'] != '') {
|
||||
$text .= "\nTags: ". implode(", ", preg_split("/,\s*/", $task['tags'], -1, PREG_SPLIT_NO_EMPTY));
|
||||
}
|
||||
|
||||
$this->sendTelegrams($text);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
$list['name'] is already escaped
|
||||
*/
|
||||
private function notifyListCreated($list)
|
||||
{
|
||||
$link = get_mttinfo('url'). '?list='. $list['id'];
|
||||
|
||||
//email
|
||||
if (count($this->prefs['emails']) > 0) {
|
||||
$aText = [];
|
||||
$aText[] = "New list:";
|
||||
$aText[] = htmlspecialchars_decode($list['name']);
|
||||
$aText[] = "";
|
||||
$aText[] = "Link: $link";
|
||||
$text = implode("\r\n", $aText);
|
||||
|
||||
$this->sendEmails( $text, "New list");
|
||||
}
|
||||
|
||||
// telegram
|
||||
if (count($this->prefs['chats']) > 0) {
|
||||
$text = "New list: <a href=\"$link\">". $list['name']. "</a>";
|
||||
|
||||
$this->sendTelegrams($text);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendEmails(string $text, string $subject)
|
||||
{
|
||||
$host = parse_url(get_unsafe_mttinfo('url'), PHP_URL_HOST);
|
||||
$host = preg_replace('/^(www\.)/', '', $host);
|
||||
$fromAddr = "mytinytodo@$host";
|
||||
$from = "myTinyTodo <$fromAddr>";
|
||||
$mttTitle = str_replace( ["\r","\n"], '', get_unsafe_mttinfo('title') );
|
||||
$subject = "[$mttTitle] $subject";
|
||||
if (!mb_check_encoding($subject, 'ASCII')) {
|
||||
$subject = mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n");
|
||||
}
|
||||
$headers = [
|
||||
'From: '. $from
|
||||
];
|
||||
if (mb_check_encoding($text, 'ASCII')) {
|
||||
$headers[] = 'Content-Type: text/plain';
|
||||
}
|
||||
else {
|
||||
$headers[] = 'Content-Type: text/plain; charset=UTF-8';
|
||||
$headers[] = 'Content-Transfer-Encoding: 8bit';
|
||||
}
|
||||
foreach ($this->prefs['emails'] as $email) {
|
||||
mail($email, $subject, $text, implode("\r\n", $headers), "-f$fromAddr");
|
||||
}
|
||||
}
|
||||
|
||||
private function sendTelegrams(string $text)
|
||||
{
|
||||
if ($this->cli) {
|
||||
$this->sendTelegramsInBackground($text);
|
||||
}
|
||||
else {
|
||||
$this->sendTelegramsWithApi($text);
|
||||
}
|
||||
}
|
||||
|
||||
// public!
|
||||
function sendTelegramsWithApi(string $text)
|
||||
{
|
||||
if (!isset($this->prefs['token'])) {
|
||||
return;
|
||||
}
|
||||
$api = new TelegramApi($this->prefs['token']);
|
||||
$blockedChats = [];
|
||||
foreach ($this->prefs['chats'] as $chatId) {
|
||||
// try-catch?
|
||||
$result = $api->sendMessage([
|
||||
'chat_id' => $chatId,
|
||||
'parse_mode' => 'HTML', //or MarkdownV2
|
||||
'text' => $text,
|
||||
'disable_web_page_preview' => true
|
||||
]);
|
||||
if (!$result && $api->lastError) {
|
||||
if ($api->lastError['error_code'] == 403) {
|
||||
// User has blocked the bot
|
||||
$blockedChats[] = $chatId;
|
||||
error_log("Bot is blocked in chat $chatId, chat will be deactivated");
|
||||
}
|
||||
else {
|
||||
error_log("Telegram API Error ". $api->lastError['error_code']. ": ". $api->lastError['description']);
|
||||
}
|
||||
}
|
||||
}
|
||||
//We can remove blocked chats from settings
|
||||
if (count($blockedChats) > 0) {
|
||||
$this->prefs['chats'] = array_diff($this->prefs['chats'], $blockedChats);
|
||||
\Config::saveDomain(NotificationsExtension::domain, $this->prefs);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendTelegramsInBackground(string $text)
|
||||
{
|
||||
$hash = password_hash($this->prefs['token'], PASSWORD_DEFAULT);
|
||||
$dir = __DIR__;
|
||||
$outfile = ''; # or '> /dev/null 2>&1';
|
||||
//$outfile = '> /dev/null 2>&1';
|
||||
// if (MTT_DEBUG) {
|
||||
// $outfile = "> $dir/../../db/cli-notify.log 2>&1";
|
||||
// }
|
||||
$fh = popen("php -f $dir/cli-notify.php $outfile", 'w');
|
||||
fwrite($fh, $hash."\n".$text);
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
}
|
||||
86
src/ext/notifications/class.telegramapi.php
Normal file
86
src/ext/notifications/class.telegramapi.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
namespace Notify;
|
||||
|
||||
class TelegramApi
|
||||
{
|
||||
private $token = '';
|
||||
/** @var ?array $lastError */
|
||||
public $lastError = null;
|
||||
|
||||
function __construct(string $token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
function getMe(): ?array
|
||||
{
|
||||
return $this->makeGetRequest('getMe');
|
||||
}
|
||||
|
||||
function getUpdates(?array $params = null): ?array
|
||||
{
|
||||
return $this->makePostRequest('getUpdates', $params ?? []);
|
||||
}
|
||||
|
||||
function sendMessage(array $params): ?array
|
||||
{
|
||||
return $this->makePostRequest('sendMessage', $params);
|
||||
}
|
||||
|
||||
private function makeGetRequest(string $method): ?array
|
||||
{
|
||||
$this->lastError = null;
|
||||
$body = @file_get_contents('https://api.telegram.org/bot'. $this->token .'/'. $method, false);
|
||||
if ($body === false) {
|
||||
throw new \Exception("Failed to make request to Telegram API");
|
||||
}
|
||||
$decodedBody = $this->decodeBody($body);
|
||||
return $decodedBody['result'] ?? [];
|
||||
}
|
||||
|
||||
private function makePostRequest(string $method, array $params): ?array
|
||||
{
|
||||
$json = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
$options = array(
|
||||
'http' => array(
|
||||
'header' => "Content-type: application/json\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => $json,
|
||||
'ignore_errors' => true
|
||||
)
|
||||
);
|
||||
$context = stream_context_create($options);
|
||||
$this->lastError = null;
|
||||
$body = @file_get_contents('https://api.telegram.org/bot'. $this->token .'/'. $method, false, $context);
|
||||
if ($body === false) {
|
||||
throw new \Exception("Failed to make request to Telegram API");
|
||||
}
|
||||
$decodedBody = $this->decodeBody($body);
|
||||
return $decodedBody['result'] ?? [];
|
||||
}
|
||||
|
||||
private function decodeBody(string $body): array
|
||||
{
|
||||
$decodedBody = json_decode($body, true);
|
||||
if (!is_array($decodedBody)) {
|
||||
$decodedBody = [];
|
||||
}
|
||||
if (!isset($decodedBody['ok'])) {
|
||||
throw new \Exception("Telegram API Error");
|
||||
}
|
||||
if ($decodedBody['ok'] === false) {
|
||||
$this->lastError = [
|
||||
'error_code' => $decodedBody['error_code'] ?? 0,
|
||||
'description' => ($decodedBody['description'] ?? '')
|
||||
];
|
||||
}
|
||||
return $decodedBody;
|
||||
}
|
||||
}
|
||||
59
src/ext/notifications/cli-notify.php
Normal file
59
src/ext/notifications/cli-notify.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
set_time_limit(30);
|
||||
|
||||
if (php_sapi_name() != 'cli') {
|
||||
error_log("Supports cli only");
|
||||
exit(-1);
|
||||
}
|
||||
if (!function_exists('pcntl_fork')) {
|
||||
error_log("Required PHP module is not found: pcntl");
|
||||
exit(-2);
|
||||
}
|
||||
$dontStartSession = 1;
|
||||
require(__DIR__.'/../../init.php');
|
||||
|
||||
$hash = fgets(STDIN);
|
||||
if ($hash === false) {
|
||||
error_log("No input");
|
||||
exit(-3);
|
||||
}
|
||||
$hash = trim($hash);
|
||||
$text = stream_get_contents(STDIN);
|
||||
|
||||
// Wi will fork a child to do a long work
|
||||
$pid = pcntl_fork();
|
||||
if ($pid == -1) {
|
||||
error_log("Failed to fork a child");
|
||||
exit(-1);
|
||||
}
|
||||
else if ($pid) {
|
||||
// parent will not wait for child's exit
|
||||
exit;
|
||||
}
|
||||
|
||||
// Child is here, detach it
|
||||
if (posix_setsid() < 0) {
|
||||
error_log("posix_setsid() failed");
|
||||
exit;
|
||||
}
|
||||
|
||||
$prefs = NotificationsExtension::preferences();
|
||||
if (!isset($prefs['token'])) {
|
||||
error_log("No telegram token");
|
||||
exit(-4);
|
||||
}
|
||||
$token = $prefs['token'] ?? '';
|
||||
if (!password_verify($prefs['token'], $hash)) {
|
||||
error_log("Not authorized");
|
||||
exit(-5);
|
||||
}
|
||||
|
||||
$sender = new Notify\Sender($prefs);
|
||||
$sender->sendTelegramsWithApi($text);
|
||||
6
src/ext/notifications/extension.json
Normal file
6
src/ext/notifications/extension.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"bundleId": "notifications",
|
||||
"name": "Notifications",
|
||||
"version": "0.9",
|
||||
"description": "Notify about new tasks and lists on e-mail or telegram"
|
||||
}
|
||||
26
src/ext/notifications/lang/en.json
Normal file
26
src/ext/notifications/lang/en.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"ext.notifications.name": "Notifications",
|
||||
"notifications.check": "Check",
|
||||
"notifications.bot_not_configured": "Bot is not configured",
|
||||
"notifications.h_email": "E-Mail:",
|
||||
"notifications.d_email": "Separate multiple addresses with comma.",
|
||||
"notifications.h_telegram": "Telegram",
|
||||
"notifications.h_token": "Bot token:",
|
||||
"notifications.d_token": "Telegram Bot API token from @BotFather.",
|
||||
"notifications.h_active_chats": "Active chats:",
|
||||
"notifications.d_active_chats": "Number of chats where bot sends notifications.",
|
||||
"notifications.deactivate_all": "Deactivate all",
|
||||
"notifications.h_new_chat": "New chat:",
|
||||
"notifications.d_new_chat": "Start new conversation with the bot, send this code to the chat and click \"Check\" here.",
|
||||
"notifications.saved": "Saved",
|
||||
"notifications.invalid_email": "Invalid email address",
|
||||
"notifications.no_bot_info": "Can not get bot info, seems token is invalid",
|
||||
"notifications.all_chats_deactivated": "All chats deactivated",
|
||||
"notifications.no_new_chats": "No new chats",
|
||||
"notifications.already_active": "Already active",
|
||||
"notifications.please_send": "Please send a code to activate",
|
||||
"notifications.code_not_set": "Code is not set",
|
||||
"notifications.code_expired": "Code has expired",
|
||||
"notifications.code_wrong": "Wrong code",
|
||||
"notifications.activated": "Activated"
|
||||
}
|
||||
26
src/ext/notifications/lang/ru.json
Normal file
26
src/ext/notifications/lang/ru.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"ext.notifications.name": "Уведомления",
|
||||
"notifications.check": "Проверить",
|
||||
"notifications.bot_not_configured": "Бот не настроен",
|
||||
"notifications.h_email": "E-Mail:",
|
||||
"notifications.d_email": "Разделите несколько адресов c помощью запятой.",
|
||||
"notifications.h_telegram": "Телеграм",
|
||||
"notifications.h_token": "Токен для бота:",
|
||||
"notifications.d_token": "Токен для телеграм-бота, полученный от @BotFather.",
|
||||
"notifications.h_active_chats": "Активные чаты:",
|
||||
"notifications.d_active_chats": "Количество чатов, куда бот присылает уведомления.",
|
||||
"notifications.deactivate_all": "Отключить все",
|
||||
"notifications.h_new_chat": "Новый чат:",
|
||||
"notifications.d_new_chat": "Начните чат с ботом, отправьте ему этот код и нажмите \"Проверить\"",
|
||||
"notifications.saved": "Сохранено",
|
||||
"notifications.invalid_email": "Некорректный адрес e-mail",
|
||||
"notifications.no_bot_info": "Ошибка в подключении бота; возможно, токен некорректный",
|
||||
"notifications.all_chats_deactivated": "All chats deactivated",
|
||||
"notifications.no_new_chats": "Нет новых чатов",
|
||||
"notifications.already_active": "Уже активирован",
|
||||
"notifications.please_send": "Пожалуйста, отправьте код для активации",
|
||||
"notifications.code_not_set": "Код не установлен",
|
||||
"notifications.code_expired": "Истек срок действия кода",
|
||||
"notifications.code_wrong": "Неправильный код",
|
||||
"notifications.activated": "Активирован"
|
||||
}
|
||||
209
src/ext/notifications/loader.php
Normal file
209
src/ext/notifications/loader.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
if (!defined('MTTPATH')) {
|
||||
die("Unexpected usage.");
|
||||
}
|
||||
|
||||
if (!function_exists('mb_internal_encoding')) {
|
||||
throw new Exception("Required PHP module is not found: mbstring");
|
||||
}
|
||||
if (strtoupper(mb_internal_encoding()) != 'UTF-8') {
|
||||
throw new Exception("mb_internal_encoding is not UTF-8");
|
||||
}
|
||||
|
||||
require_once('class.observer.php');
|
||||
require_once('class.controller.php');
|
||||
require_once('class.sender.php');
|
||||
require_once('class.telegramapi.php');
|
||||
|
||||
|
||||
// on PHP-FPM we can send telegrams on didFinishRequest without delay
|
||||
|
||||
// folder is the bundleId of extension
|
||||
// name of function for extension loader has format "mtt_ext_${bundleId}_loader"
|
||||
|
||||
function mtt_ext_notifications_instance(): MTTExtension
|
||||
{
|
||||
return new NotificationsExtension();
|
||||
}
|
||||
|
||||
use Notify\NotificationObserver;
|
||||
use Notify\Controller;
|
||||
use Notify\TelegramApi;
|
||||
|
||||
class NotificationsExtension extends MTTExtension implements MTTHttpApiExtender, MTTExtensionSettingsInterface
|
||||
{
|
||||
//the same as dir name
|
||||
const bundleId = 'notifications';
|
||||
|
||||
// settings domain
|
||||
const domain = "ext.notifications.json";
|
||||
|
||||
function init()
|
||||
{
|
||||
// subscribe for notifications
|
||||
MTTNotificationCenter::addObserverForNotifications(
|
||||
[ MTTNotification::didCreateTask, MTTNotification::didCreateList ],
|
||||
new NotificationObserver()
|
||||
);
|
||||
}
|
||||
|
||||
// produces smth like like <API_PATH>/ext/notifications/deactivate
|
||||
function extendHttpApi(): array
|
||||
{
|
||||
return array(
|
||||
'/deactivate' => [
|
||||
'POST' => [ Controller::class , 'postDeactivateAll' ],
|
||||
],
|
||||
'/check' => [
|
||||
'POST' => [ Controller::class , 'postCheck' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function settingsPage(): string
|
||||
{
|
||||
$e = function($s) { return __($s, true); };
|
||||
$ext = htmlspecialchars(self::bundleId);
|
||||
$prefs = self::preferences();
|
||||
$emails = htmlspecialchars( implode(', ', $prefs['emails']) );
|
||||
$numberOfChats = count($prefs['chats']);
|
||||
|
||||
$token = $prefs['token'] ?? '';
|
||||
if (defined('MTT_DEMO') && $token != '') {
|
||||
$token = "<demo>";
|
||||
}
|
||||
$token = htmlspecialchars($token);
|
||||
|
||||
$botname = $prefs['botname'] ?? '';
|
||||
$botLink = '';
|
||||
if ($botname != '') {
|
||||
$botname = htmlspecialchars($botname);
|
||||
$botLink = "<a href='https://t.me/$botname' target='_blank'>@$botname</a>";
|
||||
|
||||
$code = $prefs['code'] ?? null;
|
||||
$codeExpires = $prefs['codeExpires'] ?? 0;
|
||||
if ($code === null || $codeExpires < time()) {
|
||||
$prefs['code'] = $code = randomString(6, '0123456789');
|
||||
$prefs['codeExpires'] = $codeExpires = time() + 60*15; // 15 min
|
||||
Config::saveDomain(self::domain, $prefs);
|
||||
}
|
||||
$newChat = "$botLink <br><br>$code <a href=\"#\" data-ext-settings-action=\"post:check\" data-ext=\"$ext\">{$e('notifications.check')}</a>";
|
||||
}
|
||||
else {
|
||||
$newChat = $e('notifications.bot_not_configured');
|
||||
}
|
||||
|
||||
//$e = function($s) { return __($s, true); };
|
||||
//$c = function($key) { return htmlspecialchars(Config::get($key)); };
|
||||
|
||||
return
|
||||
<<<EOD
|
||||
<div class="tr">
|
||||
<div class="th"> {$e('notifications.h_email')}
|
||||
<div class="descr">{$e('notifications.d_email')}</div>
|
||||
</div>
|
||||
<div class="td"> <input name="emails" value="$emails" class="in350" autocomplete="off" /> </div>
|
||||
</div>
|
||||
<div class="tr">
|
||||
<div class="th"> {$e('notifications.h_telegram')} </div>
|
||||
</div>
|
||||
<div class="tr">
|
||||
<div class="th"> {$e('notifications.h_token')}
|
||||
<div class="descr">{$e('notifications.d_token')}</div>
|
||||
</div>
|
||||
<div class="td"> <input name="token" value="$token" class="in350" autocomplete="off" /> </div>
|
||||
</div>
|
||||
<div class="tr">
|
||||
<div class="th"> {$e('notifications.h_active_chats')}
|
||||
<div class="descr">{$e('notifications.d_active_chats')}</div>
|
||||
</div>
|
||||
<div class="td"> $numberOfChats <a href="#" data-ext-settings-action="post:deactivate" data-ext="$ext">{$e('notifications.deactivate_all')}</a> </div>
|
||||
</div>
|
||||
<div class="tr">
|
||||
<div class="th"> {$e('notifications.h_new_chat')}
|
||||
<div class="descr">{$e('notifications.d_new_chat')}</div>
|
||||
</div>
|
||||
<div class="td"> $newChat </div>
|
||||
</div>
|
||||
EOD;
|
||||
}
|
||||
|
||||
function saveSettings(array $params, ?string &$outMessage): bool
|
||||
{
|
||||
if (defined('MTT_DEMO')) {
|
||||
$outMessage = "Demo";
|
||||
return true;
|
||||
}
|
||||
$token = $params['token'] ?? '';
|
||||
$emails = $params['emails'] ?? '';
|
||||
if (!is_string($token) || !is_string($emails)) {
|
||||
throw new Exception("Invalid format");
|
||||
}
|
||||
|
||||
$prefs = Config::requestDomain(self::domain);
|
||||
if ($token !== ($prefs['token'] ?? '')) {
|
||||
$prefs['botname'] = '';
|
||||
$prefs['chats'] = [];
|
||||
$prefs['validToken'] = false;
|
||||
}
|
||||
$prefs['token'] = $token;
|
||||
$prefs['code'] = null;
|
||||
$prefs['emails'] = [];
|
||||
|
||||
// validate emails
|
||||
if ($emails != '') {
|
||||
$a = explode(',', $emails);
|
||||
foreach ($a as $email) {
|
||||
$email = trim($email);
|
||||
if (preg_match('/^[^\s\@\|]+@[^\s\@\|]+$/', $email)) {
|
||||
$prefs['emails'][] = $email;
|
||||
}
|
||||
else {
|
||||
$outMessage = __('notifications.invalid_email');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// validate token
|
||||
if ($token != '' && !$prefs['validToken']) {
|
||||
$api = new TelegramApi($token);
|
||||
try {
|
||||
$result = $api->getMe();
|
||||
if ($result && isset($result['username'])) {
|
||||
$prefs['botname'] = $result['username'];
|
||||
}
|
||||
$prefs['validToken'] = true;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
error_log($e->getMessage());
|
||||
$outMessage = __('notifications.no_bot_info');;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Config::saveDomain(self::domain, $prefs);
|
||||
$outMessage = __('notifications.saved');
|
||||
return true;
|
||||
}
|
||||
|
||||
static function preferences(): array
|
||||
{
|
||||
$prefs = Config::requestDomain(self::domain);
|
||||
if (!isset($prefs['chats']) || !is_array($prefs['chats'])) {
|
||||
$prefs['chats'] = [];
|
||||
}
|
||||
if (!isset($prefs['emails']) || !is_array($prefs['emails'])) {
|
||||
$prefs['emails'] = [];
|
||||
}
|
||||
return $prefs;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue