From 3b8b6d3732956f3b4bcd6ca3b8268039293749ee Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Thu, 5 Jan 2017 23:17:19 +0300 Subject: [PATCH] Telegram account linking added. Telegram bot services refactored. Legacy abstract API client POST requests fixed. --- .../Version20170105191821.php | 38 +++ .../Controller/Telegram/WebHookController.php | 21 +- .../Bundle/PointToolsBundle/DTO/Api/Auth.php | 96 ++++++ .../Entity/Telegram/Account.php | 264 +++++++++++++++ .../Telegram/CommandProcessingException.php | 9 + .../SubscriptionEventRepository.php | 27 +- .../Resources/config/services.yml | 57 +++- .../Resources/views/Telegram/error.md.twig | 7 + .../Resources/views/Telegram/help.md.twig | 3 + .../Telegram/last_user_subscriptions.md.twig | 12 + .../views/Telegram/user_subscribers.md.twig | 11 + .../Resources/views/User/show.html.twig | 4 +- .../PointToolsBundle/Service/AbstractApi.php | 4 +- .../Factory/Telegram/AccountFactory.php | 46 +++ .../Telegram/IncomingUpdateDispatcher.php | 53 +++ .../Telegram/IncomingUpdateProcessor.php | 170 ---------- .../Service/Telegram/InlineQueryProcessor.php | 68 ++++ .../Telegram/PrivateMessageProcessor.php | 307 ++++++++++++++++++ .../PointToolsBundle/Service/UserApi.php | 61 +++- 19 files changed, 1065 insertions(+), 193 deletions(-) create mode 100644 app/DoctrineMigrations/Version20170105191821.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/DTO/Api/Auth.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Entity/Telegram/Account.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Exception/Telegram/CommandProcessingException.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/error.md.twig create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/last_user_subscriptions.md.twig create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/user_subscribers.md.twig create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Service/Factory/Telegram/AccountFactory.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateDispatcher.php delete mode 100644 src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateProcessor.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/InlineQueryProcessor.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/PrivateMessageProcessor.php diff --git a/app/DoctrineMigrations/Version20170105191821.php b/app/DoctrineMigrations/Version20170105191821.php new file mode 100644 index 0000000..5eb5559 --- /dev/null +++ b/app/DoctrineMigrations/Version20170105191821.php @@ -0,0 +1,38 @@ +abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE TABLE users.telegram_accounts (account_id INT NOT NULL, user_id INT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, linked_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, first_name TEXT NOT NULL, last_name TEXT DEFAULT NULL, username TEXT DEFAULT NULL, private_chat_id BIGINT DEFAULT NULL, subscriber_notification BOOLEAN NOT NULL, rename_notification BOOLEAN NOT NULL, PRIMARY KEY(account_id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1EDB9B25A76ED395 ON users.telegram_accounts (user_id)'); + $this->addSql('CREATE INDEX subscriber_notification_idx ON users.telegram_accounts (subscriber_notification) WHERE subscriber_notification = TRUE'); + $this->addSql('CREATE INDEX rename_notification_idx ON users.telegram_accounts (rename_notification) WHERE rename_notification = TRUE'); + $this->addSql('ALTER TABLE users.telegram_accounts ADD CONSTRAINT FK_1EDB9B25A76ED395 FOREIGN KEY (user_id) REFERENCES users.users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP TABLE users.telegram_accounts'); + } +} diff --git a/src/Skobkin/Bundle/PointToolsBundle/Controller/Telegram/WebHookController.php b/src/Skobkin/Bundle/PointToolsBundle/Controller/Telegram/WebHookController.php index 666ac6e..8bd13e9 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Controller/Telegram/WebHookController.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Controller/Telegram/WebHookController.php @@ -18,14 +18,31 @@ class WebHookController extends Controller throw $this->createNotFoundException(); } + $logger = $this->get('logger'); + $content = json_decode($request->getContent(), true); $update = new Update( $content, - $this->get('logger') + $logger ); - $this->get('point_tools.telegram.update_processor')->process($update); + try { + $this->get('point_tools.telegram.update_dispatcher')->process($update); + } catch (\Exception $e) { + if ($this->getParameter('kernel.debug')) { + throw $e; + } + + $logger->addError('Telegram bot error', [ + 'exception' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } return new JsonResponse('received'); } diff --git a/src/Skobkin/Bundle/PointToolsBundle/DTO/Api/Auth.php b/src/Skobkin/Bundle/PointToolsBundle/DTO/Api/Auth.php new file mode 100644 index 0000000..51bf3e6 --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/DTO/Api/Auth.php @@ -0,0 +1,96 @@ +token; + } + + /** + * @param string|null $token + * + * @return Auth + */ + public function setToken(string $token = null): Auth + { + $this->token = $token; + + return $this; + } + + /** + * @return string|null + */ + public function getCsRfToken() + { + return $this->csRfToken; + } + + /** + * @param string $csRfToken + * + * @return Auth + */ + public function setCsRfToken(string $csRfToken = null) + { + $this->csRfToken = $csRfToken; + + return $this; + } + + /** + * @return string|null + */ + public function getError() + { + return $this->error; + } + + /** + * @param string|null $error + * + * @return Auth + */ + public function setError(string $error = null): Auth + { + $this->error = $error; + + return $this; + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Entity/Telegram/Account.php b/src/Skobkin/Bundle/PointToolsBundle/Entity/Telegram/Account.php new file mode 100644 index 0000000..20d4b48 --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Entity/Telegram/Account.php @@ -0,0 +1,264 @@ +id = $id; + } + + /** + * @ORM\PrePersist() + */ + public function prePersist() + { + $this->createdAt = new \DateTime(); + } + + /** + * @ORM\PreUpdate() + */ + public function preUpdate() + { + $this->updatedAt = new \DateTime(); + } + + public function getId(): int + { + return $this->id; + } + + /** + * @return \DateTime + */ + public function getCreatedAt(): \DateTime + { + return $this->createdAt; + } + + /** + * @return \DateTime + */ + public function getUpdatedAt(): \DateTime + { + return $this->updatedAt; + } + + /** + * @return \DateTime + */ + public function getLinkedAt(): \DateTime + { + return $this->linkedAt; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): Account + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function setLastName(string $lastName = null): Account + { + $this->lastName = $lastName; + + return $this; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setUsername(string $username = null): Account + { + $this->username = $username; + + return $this; + } + + public function setChatId(int $chatId): self + { + $this->chatId = $chatId; + + return $this; + } + + public function getChatId(): int + { + return $this->chatId; + } + + /** + * @return User|null + */ + public function getUser() + { + return $this->user; + } + + public function setUser(User $user): Account + { + if (!$this->user && $user) { + $this->linkedAt = new \DateTime(); + } + + $this->user = $user; + + return $this; + } + + public function getUserId(): int + { + return $this->user->getId(); + } + + /** + * Disables all notifications + */ + public function disableNotifications(): self + { + $this->subscriberNotification = false; + $this->renameNotification = false; + + return $this; + } + + public function setSubscriberNotification(bool $subscriberNotification): self + { + $this->subscriberNotification = $subscriberNotification; + + return $this; + } + + public function isSubscriberNotification(): bool + { + return $this->subscriberNotification; + } + + public function setRenameNotification(bool $renameNotification): self + { + $this->renameNotification = $renameNotification; + + return $this; + } + + public function isRenameNotification(): bool + { + return $this->renameNotification; + } +} diff --git a/src/Skobkin/Bundle/PointToolsBundle/Exception/Telegram/CommandProcessingException.php b/src/Skobkin/Bundle/PointToolsBundle/Exception/Telegram/CommandProcessingException.php new file mode 100644 index 0000000..ae742c7 --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Exception/Telegram/CommandProcessingException.php @@ -0,0 +1,9 @@ +createQueryBuilder('se'); @@ -34,7 +33,7 @@ class SubscriptionEventRepository extends EntityRepository * * @return QueryBuilder */ - public function createUserLastSubscribersEventsQuery(User $user) + public function createUserLastSubscribersEventsQuery(User $user): QueryBuilder { $qb = $this->createQueryBuilder('se'); @@ -47,12 +46,28 @@ class SubscriptionEventRepository extends EntityRepository ; } + /** + * Get last user subscriber events + * + * @param User $user + * @param int $limit + * + * @return SubscriptionEvent[] + */ + public function getUserLastSubscribersEvents(User $user, int $limit = 20): array + { + $qb = $this->createUserLastSubscribersEventsQuery($user); + $qb->setMaxResults($limit); + + return $qb->getQuery()->getResult(); + } + /** * Get last global subscriptions QueryBuilder for pagination * * @return QueryBuilder */ - public function createLastSubscriptionEventsQuery() + public function createLastSubscriptionEventsQuery(): QueryBuilder { $qb = $this->createQueryBuilder('se'); @@ -71,7 +86,7 @@ class SubscriptionEventRepository extends EntityRepository * * @return SubscriptionEvent[] */ - public function getLastSubscriptionEvents($limit = 20) + public function getLastSubscriptionEvents(int $limit = 20): array { $qb = $this->createLastSubscriptionEventsQuery(); $qb->setMaxResults($limit); diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml b/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml index 935bceb..61bbe9d 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml @@ -14,6 +14,7 @@ services: tags: - { name: guzzle.client } + skobkin_point_tools.api_user: class: Skobkin\Bundle\PointToolsBundle\Service\UserApi arguments: @@ -21,6 +22,7 @@ services: - "%point_use_https%" - "%point_api_base_url%" - @doctrine.orm.entity_manager + - @serializer skobkin_point_tools.api_post: class: Skobkin\Bundle\PointToolsBundle\Service\PostApi @@ -30,26 +32,34 @@ services: - "%point_api_base_url%" - @skobkin__point_tools.service_factory.post_factory + skobkin_point_tools.subscriptions_manager: class: Skobkin\Bundle\PointToolsBundle\Service\SubscriptionsManager arguments: [ @doctrine.orm.entity_manager ] + + # Factories + # User factory skobkin__point_tools.service_factory.user_factory: class: Skobkin\Bundle\PointToolsBundle\Service\Factory\UserFactory arguments: [ @doctrine.orm.entity_manager ] + # Comment factory skobkin__point_tools.service_factory.comment_factory: class: Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\CommentFactory arguments: [ @doctrine.orm.entity_manager, @skobkin__point_tools.service_factory.user_factory ] + # Tag factory skobkin__point_tools.service_factory.tag_factory: class: Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\TagFactory arguments: [ @logger, @doctrine.orm.entity_manager ] + # File factory skobkin__point_tools.service_factory.file_factory: class: Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\FileFactory arguments: [ @logger, @doctrine.orm.entity_manager ] + # Post factory skobkin__point_tools.service_factory.post_factory: class: Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\PostFactory arguments: @@ -60,21 +70,27 @@ services: - @skobkin__point_tools.service_factory.comment_factory - @skobkin__point_tools.service_factory.tag_factory + # Telegram accounts factory + point_tools.factory.telegram_account: + class: Skobkin\Bundle\PointToolsBundle\Service\Factory\Telegram\AccountFactory + arguments: [@doctrine.orm.entity_manager] + + # Custom Markdown parser markdown.parser.point: class: Skobkin\Bundle\PointToolsBundle\Service\Markdown\PointParser - arguments: - - [] - - @router + arguments: [[], @router] tags: - { name: markdown.parser } + # Event listener point_tools.event_listener.user_rename_subscriber: class: Skobkin\Bundle\PointToolsBundle\EventListener\UserRenameSubscriber tags: - { name: doctrine.event_subscriber, connection: default } + # Twig extensions point_tools.twig.point_avatar_extension: class: Skobkin\Bundle\PointToolsBundle\Twig\PointUserExtension @@ -83,17 +99,40 @@ services: tags: - { name: twig.extension } - # Telegram API client + + # Telegram services + # API client point_tools.telegram.api_client: class: unreal4u\TelegramAPI\TgLog arguments: [%telegram_token%, @logger, @point_tools.http.telegram_client] - # Telegram simple message sender + # Simple message sender point_tools.telegram.simple_sender: class: Skobkin\Bundle\PointToolsBundle\Service\Telegram\SimpleSender arguments: [@point_tools.telegram.api_client] - # Telegram command processor - point_tools.telegram.update_processor: - class: Skobkin\Bundle\PointToolsBundle\Service\Telegram\IncomingUpdateProcessor - arguments: [%point_id%, @point_tools.telegram.api_client, @doctrine.orm.entity_manager, @twig] \ No newline at end of file + # Common incoming message processor + point_tools.telegram.update_dispatcher: + class: Skobkin\Bundle\PointToolsBundle\Service\Telegram\IncomingUpdateDispatcher + arguments: + - @point_tools.telegram.private_message_processor + - @point_tools.telegram.inline_query_processor + + # InlineQuery processor + point_tools.telegram.inline_query_processor: + class: Skobkin\Bundle\PointToolsBundle\Service\Telegram\InlineQueryProcessor + lazy: true + arguments: [@doctrine.orm.entity_manager, @point_tools.telegram.api_client] + + # Private message processor + point_tools.telegram.private_message_processor: + class: Skobkin\Bundle\PointToolsBundle\Service\Telegram\PrivateMessageProcessor + lazy: true + arguments: + - @point_tools.telegram.api_client + - @skobkin_point_tools.api_user + - @point_tools.factory.telegram_account + - @doctrine.orm.entity_manager + - @twig + - %point_id% + - %point_login% \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/error.md.twig b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/error.md.twig new file mode 100644 index 0000000..05f3344 --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/error.md.twig @@ -0,0 +1,7 @@ +*{{ title }}* +{% if text %} + +{{ text }} +{% endif %} + +[Send bug report](https://bitbucket.org/skobkin/point-tools/issues?status=new&status=open). \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/help.md.twig b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/help.md.twig index 562b7ad..807a7d6 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/help.md.twig +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/help.md.twig @@ -1,7 +1,10 @@ *Help*: +/me - Show your last subscriber events (/link needed) +/link %login% %password% - Link your Telegram account with Point.im account /last - shows last global subscription events /last %user% - shows last user subscribers events +/sub - Show user subscribers /help shows this message Visit [Point Tools](https://point.skobk.in/) for more info. \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/last_user_subscriptions.md.twig b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/last_user_subscriptions.md.twig new file mode 100644 index 0000000..6f1bead --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/last_user_subscriptions.md.twig @@ -0,0 +1,12 @@ +{# @var user \Skobkin\Bundle\PointToolsBundle\Entity\User #} +*Last [@{{ user.login }}]({{ user.login|point_user_url(true) }}):* + +{% set subscription = constant('\\Skobkin\\Bundle\\PointToolsBundle\\Entity\\SubscriptionEvent::ACTION_SUBSCRIBE') %} +{# @var event \Skobkin\Bundle\PointToolsBundle\Entity\SubscriptionEvent #} +{% for event in events %} +{% set sub_login = event.subscriber.login %} +{{ event.date|date('d M y H:i') }} {% if subscription == event.action %} + {% else %} - {% endif %} [@{{ sub_login }}]({{ sub_login|point_user_url(true) }}) +{% endfor %} + +{# @todo remove hardcoded URL #} +See more events on [Point Tools](https://point.skobk.in{{ path('user_show', {'login': user.login}) }}) site. \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/user_subscribers.md.twig b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/user_subscribers.md.twig new file mode 100644 index 0000000..47bd43e --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Telegram/user_subscribers.md.twig @@ -0,0 +1,11 @@ +{# @var user \Skobkin\Bundle\PointToolsBundle\Entity\User #} +*@{{ user.login }} subscribers:* + +{% if subscribers|length > 0 %} +{{ subscribers|join(', ') }} +{% else %} +No subscribers +{% endif %} + +{# @todo remove hardcoded URL #} +See more events on [Point Tools](https://point.skobk.in{{ path('user_show', {'login': user.login}) }}) site. \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/User/show.html.twig b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/User/show.html.twig index 75ebe28..625ddda 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/User/show.html.twig +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/User/show.html.twig @@ -23,7 +23,7 @@
@@ -94,7 +94,7 @@ {% for event in subscriptions_log %} - @{{ event.subscriber.login }} + @{{ event.subscriber.login }} diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php b/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php index f8f2594..cec0234 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php @@ -80,9 +80,7 @@ class AbstractApi public function sendPostRequest($path, array $parameters = []) { /** @var GuzzleRequest $request */ - $request = $this->client->post($path, null, null, [ - 'form_params' => $parameters, - ]); + $request = $this->client->post($path, null, $parameters); return $request->send(); } diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/Factory/Telegram/AccountFactory.php b/src/Skobkin/Bundle/PointToolsBundle/Service/Factory/Telegram/AccountFactory.php new file mode 100644 index 0000000..8cb3e38 --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/Factory/Telegram/AccountFactory.php @@ -0,0 +1,46 @@ +em = $em; + $this->accountRepo = $em->getRepository('SkobkinPointToolsBundle:Telegram\Account'); + } + + public function findOrCreateFromMessage(Message $message): Account + { + if (null === $account = $this->accountRepo->findOneBy(['id' => $message->from->id])) { + $account = new Account($message->from->id); + $this->em->persist($account); + } + + // Setting/updating account data + $account + ->setFirstName($message->from->first_name) + ->setLastName($message->from->last_name) + ->setUsername($message->from->username) + ->setChatId($message->chat->id) + ; + + return $account; + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateDispatcher.php b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateDispatcher.php new file mode 100644 index 0000000..225f54e --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateDispatcher.php @@ -0,0 +1,53 @@ +privateMessageProcessor = $privateMessageProcessor; + $this->inlineQueryProcessor = $inlineQueryProcessor; + } + + /** + * Processes update and delegates it to corresponding service + * + * @param Update $update + */ + public function process(Update $update) + { + if ($update->message && $update->message instanceof Message) { + $chatType = $update->message->chat->type; + + if (self::CHAT_TYPE_PRIVATE === $chatType) { + $this->privateMessageProcessor->process($update->message); + } elseif (self::CHAT_TYPE_GROUP === $chatType) { + // @todo implement + } + } elseif ($update->inline_query && $update->inline_query instanceof Query) { + $this->inlineQueryProcessor->process($update->inline_query); + } + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateProcessor.php b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateProcessor.php deleted file mode 100644 index 2315798..0000000 --- a/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/IncomingUpdateProcessor.php +++ /dev/null @@ -1,170 +0,0 @@ -client = $client; - $this->em = $em; - $this->twig = $twig; - $this->pointUserId = $pointUserId; - } - - /** - * Processes update and delegates it to corresponding service - * - * @param Update $update - */ - public function process(Update $update) - { - if ($update->message && $update->message instanceof Message) { - $chatType = $update->message->chat->type; - - if (self::CHAT_TYPE_PRIVATE === $chatType) { - $this->processPrivateMessage($update); - } elseif (self::CHAT_TYPE_GROUP === $chatType) { - - } - } elseif ($update->inline_query && $update->inline_query instanceof Query) { - $this->processInlineQuery($update); - } - - } - - /** - * @todo refactor - * - * @param Update $update - */ - private function processPrivateMessage(Update $update) - { - $chatId = $update->message->chat->id; - $text = $update->message->text; - - $sendMessage = new SendMessage(); - $sendMessage->chat_id = $chatId; - $sendMessage->parse_mode = self::PARSE_MODE_MARKDOWN; - $sendMessage->disable_web_page_preview = true; - - $words = explode(' ', $text, 3); - - if (0 === count($words)) { - return; - } - - switch ($words[0]) { - case 'l': - case '/last': - case 'last': - if (array_key_exists(1, $words)) { - $sendMessage->text = 'Not implemented yet :('; - } else { - $events = $this->em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(10); - $sendMessage->text = $this->twig->render('@SkobkinPointTools/Telegram/last_global_subscriptions.md.twig', ['events' => $events]); - } - - break; - - case 'sub': - case '/sub': - case 'subscribers': - $sendMessage->text = 'Subscribers list here...'; - break; - - case 'stats': - case '/stats': - $stats = [ - 'total_users' => $this->em->getRepository('SkobkinPointToolsBundle:User')->getUsersCount(), - 'active_users' => $this->em->getRepository('SkobkinPointToolsBundle:Subscription')->getUserSubscribersCountById($this->pointUserId), - 'today_events' => $this->em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastDayEventsCount(), - ]; - - $sendMessage->text = $this->twig->render('@SkobkinPointTools/Telegram/stats.md.twig', $stats); - - break; - - case '/help': - default: - $sendMessage->text = $this->twig->render('@SkobkinPointTools/Telegram/help.md.twig'); - break; - } - - $this->client->performApiRequest($sendMessage); - } - - private function processInlineQuery(Update $update) - { - $queryId = $update->inline_query->id; - $text = $update->inline_query->query; - - if (mb_strlen($text) < 2) { - return; - } - - $answerInlineQuery = new AnswerInlineQuery(); - $answerInlineQuery->inline_query_id = $queryId; - - foreach ($this->em->getRepository('SkobkinPointToolsBundle:User')->findUsersLikeLogin($text) as $user) { - $article = new Query\Result\Article(); - $article->title = $user->getLogin(); - - $contentText = new Text(); - $contentText->message_text = sprintf( - "@%s:\nName: %s\nSubscribers: %d", - $user->getLogin(), - $user->getName(), - $user->getSubscribers()->count() - ); - - $article->input_message_content = $contentText; - $article->id = md5($user->getId()); - - $answerInlineQuery->addResult($article); - } - - $this->client->performApiRequest($answerInlineQuery); - } -} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/InlineQueryProcessor.php b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/InlineQueryProcessor.php new file mode 100644 index 0000000..032d51e --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/InlineQueryProcessor.php @@ -0,0 +1,68 @@ +em = $em; + $this->client = $client; + + $this->userRepo = $em->getRepository('SkobkinPointToolsBundle:User'); + } + + public function process(Query $inlineQuery) + { + if (mb_strlen($inlineQuery->query) < 2) { + return; + } + + $answerInlineQuery = new AnswerInlineQuery(); + $answerInlineQuery->inline_query_id = $inlineQuery->id; + + foreach ($this->em->getRepository('SkobkinPointToolsBundle:User')->findUsersLikeLogin($inlineQuery->query) as $user) { + $article = new Query\Result\Article(); + $article->title = $user->getLogin(); + + $contentText = new Text(); + $contentText->message_text = sprintf( + "@%s:\nName: %s\nSubscribers: %d", + $user->getLogin(), + $user->getName(), + $user->getSubscribers()->count() + ); + + $article->input_message_content = $contentText; + $article->id = md5($user->getId()); + + $answerInlineQuery->addResult($article); + } + + $this->client->performApiRequest($answerInlineQuery); + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/PrivateMessageProcessor.php b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/PrivateMessageProcessor.php new file mode 100644 index 0000000..c900c3c --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/Telegram/PrivateMessageProcessor.php @@ -0,0 +1,307 @@ +client = $client; + $this->userApi = $userApi; + $this->accountFactory = $accountFactory; + $this->em = $em; + $this->twig = $twig; + $this->pointUserId = $pointUserId; + $this->pointUserLogin = $pointUserLogin; + + $this->userRepo = $em->getRepository('SkobkinPointToolsBundle:User'); + $this->subscriptionRepo = $em->getRepository('SkobkinPointToolsBundle:Subscription'); + $this->subscriptionEventRepo = $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent'); + } + + public function process(Message $message) + { + if (!IncomingUpdateDispatcher::CHAT_TYPE_PRIVATE === $message->chat->type) { + throw new \InvalidArgumentException('This service can process only private chat messages'); + } + + // Registering Telegram user + /** @var Account $account */ + $account = $this->accountFactory->findOrCreateFromMessage($message); + $this->em->flush(); + + // Creating blank response for later use + $sendMessage = $this->createResponseMessage($message, self::PARSE_MODE_MARKDOWN, true); + + $words = explode(' ', $message->text, 4); + + if (0 === count($words)) { + return; + } + + try { + switch ($words[0]) { + case '/link': + case 'link': + if (array_key_exists(2, $words)) { + if ($this->linkAccount($account, $words[1], $words[2])) { + // Saving linking status + $this->em->flush(); + $this->sendAccountLinked($sendMessage); + } else { + $this->sendError($sendMessage, 'Account linking error', 'Check login and password or try again later.'); + } + } else { + $this->sendError($sendMessage, 'Login/Password error', 'You need to specify login and password separated by space after /link (example: `/link mylogin MypASSw0rd`)'); + } + break; + + case '/me': + case 'me': + if ($user = $account->getUser()) { + $this->sendUserEvents($sendMessage, $user); + } else { + $this->sendError($sendMessage, 'Account not linked', 'You must /link your account first to be able to use this command.'); + } + break; + + case '/last': + case 'l': + case 'last': + if (array_key_exists(1, $words)) { + if (null !== $user = $this->userRepo->findUserByLogin($words[1])) { + $this->sendUserEvents($sendMessage, $user); + } else { + $this->sendError($sendMessage, 'User not found'); + } + } else { + $this->sendGlobalEvents($sendMessage); + } + + break; + + case '/sub': + case 'sub': + case 'subscribers': + if (array_key_exists(1, $words)) { + if (null !== $user = $this->userRepo->findUserByLogin($words[1])) { + $this->sendUserSubscribers($sendMessage, $user); + } else { + $this->sendError($sendMessage, 'User not found'); + } + } else { + $user = $this->userRepo->findUserByLogin($this->pointUserLogin); + $this->sendUserSubscribers($sendMessage, $user); + } + + break; + + case '/stats': + case 'stats': + $this->sendStats($sendMessage); + + break; + + case '/help': + default: + $this->sendHelp($sendMessage); + break; + } + } catch (CommandProcessingException $e) { + $this->sendError($sendMessage, 'Processing error', $e->getMessage()); + + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + } catch (\Exception $e) { + $this->sendError($sendMessage, 'Unknown error'); + + throw $e; + } + } + + private function linkAccount(Account $account, string $login, string $password): bool + { + if ($this->userApi->isAuthDataValid($login, $password)) { + /** @var User $user */ + if (null === $user = $this->userRepo->findOneBy(['login' => $login])) { + throw new CommandProcessingException('User not found in Point Tools database. Please try again later.'); + } + + $account->setUser($user); + + return true; + } + + return false; + } + + private function sendAccountLinked(SendMessage $sendMessage) + { + $sendMessage->text = 'Account linked. Try using /me now.'; + + $this->client->performApiRequest($sendMessage); + } + + private function sendUserSubscribers(SendMessage $sendMessage, User $user) + { + $subscribers = []; + + foreach ($user->getSubscribers() as $subscription) { + $subscribers[] = '@'.$subscription->getSubscriber()->getLogin(); + } + + $sendMessage->text = $this->twig->render(self::TEMPLATE_USER_SUBSCRIBERS, [ + 'user' => $user, + 'subscribers' => $subscribers, + ]); + + $this->client->performApiRequest($sendMessage); + } + + /** + * @param SendMessage $sendMessage + * @param User $user + */ + private function sendUserEvents(SendMessage $sendMessage, User $user) + { + $events = $this->subscriptionEventRepo->getUserLastSubscribersEvents($user, 10); + $sendMessage->text = $this->twig->render(self::TEMPLATE_LAST_USER_SUB_EVENTS, [ + 'user' => $user, + 'events' => $events, + ]); + + $this->client->performApiRequest($sendMessage); + } + + private function sendGlobalEvents(SendMessage $sendMessage) + { + $events = $this->subscriptionEventRepo->getLastSubscriptionEvents(10); + $sendMessage->text = $this->twig->render(self::TEMPLATE_LAST_EVENTS, ['events' => $events]); + + $this->client->performApiRequest($sendMessage); + } + + private function sendStats(SendMessage $sendMessage) + { + $sendMessage->text = $this->twig->render(self::TEMPLATE_STATS, [ + 'total_users' => $this->userRepo->getUsersCount(), + 'active_users' => $this->subscriptionRepo->getUserSubscribersCountById($this->pointUserId), + 'today_events' => $this->subscriptionEventRepo->getLastDayEventsCount(), + ]); + + $this->client->performApiRequest($sendMessage); + } + + private function sendHelp(SendMessage $sendMessage) + { + $sendMessage->text = $this->twig->render(self::TEMPLATE_HELP); + + $this->client->performApiRequest($sendMessage); + } + + private function sendError(SendMessage $sendMessage, string $title, string $text = '') + { + $sendMessage->text = $this->twig->render(self::TEMPLATE_ERROR, [ + 'title' => $title, + 'text' => $text, + ]); + + $this->client->performApiRequest($sendMessage); + } + + private function createResponseMessage(Message $message, string $parseMode = self::PARSE_MODE_MARKDOWN, bool $disableWebPreview = false): SendMessage + { + $sendMessage = new SendMessage(); + $sendMessage->chat_id = $message->chat->id; + $sendMessage->parse_mode = $parseMode; + $sendMessage->disable_web_page_preview = $disableWebPreview; + + return $sendMessage; + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php b/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php index 544f742..a7166e0 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php @@ -6,6 +6,8 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Guzzle\Http\Exception\ClientErrorResponseException; use Guzzle\Service\Client; +use JMS\Serializer\Serializer; +use Skobkin\Bundle\PointToolsBundle\DTO\Api\Auth; use Skobkin\Bundle\PointToolsBundle\Entity\User; use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\ApiException; use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\InvalidResponseException; @@ -36,12 +38,18 @@ class UserApi extends AbstractApi */ protected $userRepository; + /** + * @var Serializer + */ + private $serializer; - public function __construct(Client $httpClient, $https = true, $baseUrl = null, EntityManager $entityManager) + + public function __construct(Client $httpClient, $https = true, $baseUrl = null, EntityManager $entityManager, Serializer $serializer) { parent::__construct($httpClient, $https, $baseUrl); $this->em = $entityManager; + $this->serializer = $serializer; $this->userRepository = $this->em->getRepository('SkobkinPointToolsBundle:User'); } @@ -50,6 +58,57 @@ class UserApi extends AbstractApi return 'skobkin_point_tools_api_user'; } + public function isAuthDataValid(string $login, string $password): bool + { + $auth = $this->authenticate($login, $password); + + if (!$auth->getError() && $auth->getToken()) { + $this->logout($auth); + + return true; + } + + return false; + } + + public function authenticate(string $login, string $password): Auth + { + try { + $authData = $this->getPostRequestData( + '/api/login', + [ + 'login' => $login, + 'password' => $password, + ] + ); + + return $this->serializer->deserialize($authData, Auth::class, 'json'); + } catch (ClientErrorResponseException $e) { + if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) { + throw new InvalidResponseException('API method not found', 0, $e); + } else { + throw $e; + } + } + } + + public function logout(Auth $auth): bool + { + try { + $this->getPostRequestData('/api/logout', ['csrf_token' => $auth->getCsRfToken()]); + + return true; + } catch (ClientErrorResponseException $e) { + if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) { + throw new InvalidResponseException('API method not found', 0, $e); + } elseif (Response::HTTP_FORBIDDEN === $e->getResponse()->getStatusCode()) { + return true; + } else { + throw $e; + } + } + } + /** * Get user subscribers by user login *