Telegram account linking added. Telegram bot services refactored. Legacy abstract API client POST requests fixed.

This commit is contained in:
Alexey Skobkin 2017-01-05 23:17:19 +03:00
parent 1145e4da45
commit 3b8b6d3732
19 changed files with 1065 additions and 193 deletions

View File

@ -0,0 +1,38 @@
<?php
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Telegram accounts added
*/
class Version20170105191821 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() 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('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');
}
}

View File

@ -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');
}

View File

@ -0,0 +1,96 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
use JMS\Serializer\Annotation as JMSS;
/**
* @JMSS\ExclusionPolicy("none")
* @JMSS\AccessType("public_method")
*/
class Auth
{
/**
* @var string
*
* @JMSS\SerializedName("token")
* @JMSS\Type("string")
*/
private $token;
/**
* @var string
*
* @JMSS\SerializedName("csrf_token")
* @JMSS\Type("string")
*/
private $csRfToken;
/**
* @var string
*
* @JMSS\SerializedName("error")
* @JMSS\Type("string")
*/
private $error;
/**
* @return string|null
*/
public function getToken()
{
return $this->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;
}
}

View File

@ -0,0 +1,264 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Entity\Telegram;
use Doctrine\ORM\Mapping as ORM;
use Skobkin\Bundle\PointToolsBundle\Entity\User;
/**
* Account
*
* @ORM\Table(name="telegram_accounts", schema="users", indexes={
* @ORM\Index(name="subscriber_notification_idx", columns={"subscriber_notification"}, options={"where": "subscriber_notification = TRUE"}),
* @ORM\Index(name="rename_notification_idx", columns={"rename_notification"}, options={"where": "rename_notification = TRUE"}),
* })
* @ORM\Entity
* @ORM\HasLifecycleCallbacks()
*/
class Account
{
/**
* Telegram user ID
*
* @var int
*
* @ORM\Id()
* @ORM\Column(name="account_id", type="integer")
*/
private $id;
/**
* @var \DateTime
*
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* @var \DateTime
*
* @ORM\Column(name="updated_at", type="datetime", nullable=true)
*/
private $updatedAt;
/**
* @var \DateTime
*
* @ORM\Column(name="linked_at", type="datetime", nullable=true)
*/
private $linkedAt;
/**
* @var string
*
* @ORM\Column(name="first_name", type="text")
*/
private $firstName;
/**
* @var string|null
*
* @ORM\Column(name="last_name", type="text", nullable=true)
*/
private $lastName;
/**
* @var string|null
*
* @ORM\Column(name="username", type="text", nullable=true)
*/
private $username;
/**
* ID of private chat with user
*
* @var int
*
* @ORM\Column(name="private_chat_id", type="bigint", nullable=true)
*/
private $chatId;
/**
* @var User
*
* @ORM\OneToOne(targetEntity="Skobkin\Bundle\PointToolsBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", nullable=true, onDelete="CASCADE")
*/
private $user;
/**
* Notifications about new subscribers
*
* @var bool
*
* @ORM\Column(name="subscriber_notification", type="boolean")
*/
private $subscriberNotification = false;
/**
* Notifications about user renaming
*
* @var bool
*
* @ORM\Column(name="rename_notification", type="boolean")
*/
private $renameNotification = false;
public function __construct(int $id)
{
$this->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;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Exception\Telegram;
class CommandProcessingException extends \Exception
{
}

View File

@ -3,7 +3,6 @@
namespace Skobkin\Bundle\PointToolsBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use Skobkin\Bundle\PointToolsBundle\Entity\SubscriptionEvent;
use Skobkin\Bundle\PointToolsBundle\Entity\User;
@ -11,9 +10,9 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
class SubscriptionEventRepository extends EntityRepository
{
/**
* @return integer
* @return int
*/
public function getLastDayEventsCount()
public function getLastDayEventsCount(): int
{
$qb = $this->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);

View File

@ -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]
# 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%

View File

@ -0,0 +1,7 @@
*{{ title }}*
{% if text %}
{{ text }}
{% endif %}
[Send bug report](https://bitbucket.org/skobkin/point-tools/issues?status=new&status=open).

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -23,7 +23,7 @@
<div class="panel-body">
<ul class="users mosaic">
{% for user in subscribers %}
<li><a href="{{ url('user_show', {login: user.login}) }}">@{{ user.login }}</a></li>
<li><a href="{{ path('user_show', {login: user.login}) }}">@{{ user.login }}</a></li>
{% endfor %}
</ul>
</div>
@ -94,7 +94,7 @@
{% for event in subscriptions_log %}
<tr>
<td>
<a href="{{ url('user_show', {login: event.subscriber.login}) }}">@{{ event.subscriber.login }}</a>
<a href="{{ path('user_show', {login: event.subscriber.login}) }}">@{{ event.subscriber.login }}</a>
</td>
<td>
<span class="glyphicon {% if event.action == 'subscribe' %}glyphicon-plus{% elseif event.action == 'unsubscribe' %}glyphicon-minus{% endif %}"></span>

View File

@ -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();
}

View File

@ -0,0 +1,46 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\Factory\Telegram;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Skobkin\Bundle\PointToolsBundle\Entity\Telegram\Account;
use unreal4u\TelegramAPI\Telegram\Types\Message;
class AccountFactory
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var EntityRepository
*/
private $accountRepo;
public function __construct(EntityManagerInterface $em)
{
$this->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;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\Telegram;
use unreal4u\TelegramAPI\Telegram\Types\Inline\Query;
use unreal4u\TelegramAPI\Telegram\Types\Message;
use unreal4u\TelegramAPI\Telegram\Types\Update;
/**
* Dispatches incoming messages processing to corresponding services
*/
class IncomingUpdateDispatcher
{
const CHAT_TYPE_PRIVATE = 'private';
const CHAT_TYPE_GROUP = 'group';
/**
* @var InlineQueryProcessor
*/
private $inlineQueryProcessor;
/**
* @var PrivateMessageProcessor
*/
private $privateMessageProcessor;
public function __construct(PrivateMessageProcessor $privateMessageProcessor, InlineQueryProcessor $inlineQueryProcessor)
{
$this->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);
}
}
}

View File

@ -1,170 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\Telegram;
use Doctrine\ORM\EntityManagerInterface;
use unreal4u\TelegramAPI\Telegram\Methods\AnswerInlineQuery;
use unreal4u\TelegramAPI\Telegram\Methods\SendMessage;
use unreal4u\TelegramAPI\Telegram\Types\Inline\Query;
use unreal4u\TelegramAPI\Telegram\Types\InputMessageContent\Text;
use unreal4u\TelegramAPI\Telegram\Types\Message;
use unreal4u\TelegramAPI\Telegram\Types\Update;
use unreal4u\TelegramAPI\TgLog;
/**
* @todo refactor
*/
class IncomingUpdateProcessor
{
const CHAT_TYPE_PRIVATE = 'private';
const CHAT_TYPE_GROUP = 'group';
const PARSE_MODE_MARKDOWN = 'Markdown';
const PARSE_MODE_HTML5 = 'HTML';
/**
* @var TgLog
*/
private $client;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var \Twig_Environment
*/
private $twig;
/**
* @var int
*/
private $pointUserId;
/**
* @param TgLog $client
*/
public function __construct(int $pointUserId, TgLog $client, EntityManagerInterface $em, \Twig_Environment $twig)
{
$this->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);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\Telegram;
use Doctrine\ORM\EntityManagerInterface;
use Skobkin\Bundle\PointToolsBundle\Repository\UserRepository;
use unreal4u\TelegramAPI\Telegram\Methods\AnswerInlineQuery;
use unreal4u\TelegramAPI\Telegram\Types\Inline\Query;
use unreal4u\TelegramAPI\Telegram\Types\InputMessageContent\Text;
use unreal4u\TelegramAPI\TgLog;
class InlineQueryProcessor
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var UserRepository
*/
private $userRepo;
/**
* @var TgLog
*/
private $client;
public function __construct(EntityManagerInterface $em, TgLog $client)
{
$this->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);
}
}

View File

@ -0,0 +1,307 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\Telegram;
use Doctrine\ORM\EntityManagerInterface;
use Skobkin\Bundle\PointToolsBundle\Entity\Telegram\Account;
use Skobkin\Bundle\PointToolsBundle\Entity\User;
use Skobkin\Bundle\PointToolsBundle\Exception\Telegram\CommandProcessingException;
use Skobkin\Bundle\PointToolsBundle\Repository\SubscriptionEventRepository;
use Skobkin\Bundle\PointToolsBundle\Repository\SubscriptionRepository;
use Skobkin\Bundle\PointToolsBundle\Repository\UserRepository;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\Telegram\AccountFactory;
use Skobkin\Bundle\PointToolsBundle\Service\UserApi;
use unreal4u\TelegramAPI\Telegram\Methods\SendMessage;
use unreal4u\TelegramAPI\Telegram\Types\Message;
use unreal4u\TelegramAPI\TgLog;
/**
* Processes all private messages
*/
class PrivateMessageProcessor
{
const TEMPLATE_ERROR = '@SkobkinPointTools/Telegram/error.md.twig';
const TEMPLATE_STATS = '@SkobkinPointTools/Telegram/stats.md.twig';
const TEMPLATE_HELP = '@SkobkinPointTools/Telegram/help.md.twig';
const TEMPLATE_LAST_EVENTS = '@SkobkinPointTools/Telegram/last_global_subscriptions.md.twig';
const TEMPLATE_LAST_USER_SUB_EVENTS = '@SkobkinPointTools/Telegram/last_user_subscriptions.md.twig';
const TEMPLATE_USER_SUBSCRIBERS = '@SkobkinPointTools/Telegram/user_subscribers.md.twig';
const PARSE_MODE_MARKDOWN = 'Markdown';
const PARSE_MODE_HTML5 = 'HTML';
/**
* @var TgLog
*/
private $client;
/**
* @var UserApi
*/
private $userApi;
/**
* @var AccountFactory
*/
private $accountFactory;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var \Twig_Environment
*/
private $twig;
/**
* @var UserRepository
*/
private $userRepo;
/**
* @var SubscriptionRepository
*/
private $subscriptionRepo;
/**
* @var SubscriptionEventRepository
*/
private $subscriptionEventRepo;
/**
* @var int
*/
private $pointUserId;
/**
* @var string
*/
private $pointUserLogin;
public function __construct(
TgLog $client,
UserApi $userApi,
AccountFactory $accountFactory,
EntityManagerInterface $em,
\Twig_Environment $twig,
int $pointUserId,
string $pointUserLogin
)
{
$this->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;
}
}

View File

@ -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
*