Last active
December 30, 2022 07:12
-
-
Save denistouch/f75f0a50d7b0da34296c1f52dec87027 to your computer and use it in GitHub Desktop.
Первичная обработка Update из телеграма с целью выбора сервиса, который будет отвечать за его выполнение и получения запроса описывающего его требования
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Model\Telegram; | |
use App\Service\TelegramBot\Actions\CommonAction; | |
class Action | |
{ | |
public function __construct( | |
public readonly ActionRequest $request, | |
public readonly CommonAction $command | |
) | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Service\TelegramBot\Actions; | |
use App\Model\Telegram\ActionRequest; | |
use App\Model\Telegram\ActionResponse; | |
interface ActionInterface | |
{ | |
public const CALLBACK_SEPARATOR = '__'; | |
public const CMD_SYMBOL = '/'; | |
public function processingRequest(ActionRequest $request): ?ActionResponse; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Model\Telegram; | |
use App\Entity\Client; | |
use TelegramBot\Api\Types\Message; | |
use TelegramBot\Api\Types\User; | |
class ActionRequest | |
{ | |
public function __construct( | |
public readonly User $user, | |
public readonly bool $isAction, | |
public readonly ?Client $client, | |
public readonly ?Message $message = null, | |
public readonly string $data = '', | |
public readonly string $actionToBack = '', | |
) | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Model\Telegram; | |
use App\Utility\Telegram\Keyboard\Keyboard; | |
use Symfony\Component\Translation\TranslatableMessage; | |
class ActionResponse | |
{ | |
public function __construct( | |
public readonly int $toUser, | |
public readonly ?TranslatableMessage $message = null, | |
public readonly ?Keyboard $menu = null, | |
public readonly string $backToAction = '', | |
) { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Service\TelegramBot\Actions; | |
use App\Entity\Client; | |
use App\Model\Telegram\ActionRequest; | |
use App\Model\Telegram\ActionResponse; | |
use App\Utility\Telegram\Keyboard\Button; | |
use Symfony\Component\Translation\TranslatableMessage; | |
abstract class CommonAction implements ActionInterface | |
{ | |
public const PREFIX = 'default'; | |
public const NAME = ''; | |
public static function getExecuteButton(TranslatableMessage $text = null): Button | |
{ | |
return new Button( | |
$text ?? new TranslatableMessage('telegram.bot.text.'.static::PREFIX), | |
static::CMD_SYMBOL.static::PREFIX | |
); | |
} | |
public static function getShowMenuButton(TranslatableMessage $text = null): Button | |
{ | |
return new Button( | |
$text ?? new TranslatableMessage('telegram.bot.text.'.static::PREFIX), | |
static::PREFIX | |
); | |
} | |
public abstract function processingRequest( | |
ActionRequest $request, | |
ActionResponse $previousResponse = null | |
): ActionResponse; | |
protected function isNew(?Client $client): bool | |
{ | |
if (!$client) { | |
return true; | |
} | |
return $client->getStatus() === Client::STATUS_NEW; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Service\TelegramBot\Actions; | |
use App\Model\Telegram\ActionRequest; | |
use App\Model\Telegram\ActionResponse; | |
use Symfony\Component\Translation\TranslatableMessage; | |
class DefaultAction extends CommonAction | |
{ | |
public const PREFIX = 'default'; | |
public function processingRequest(ActionRequest $request, ActionResponse $previousResponse = null): ActionResponse | |
{ | |
return new ActionResponse($request->user->getId(), new TranslatableMessage('telegram.bot.text.default')); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Service\TelegramBot; | |
use App\Model\Telegram\Action; | |
use App\Model\Telegram\ActionRequest; | |
use App\Service\ClientService; | |
use App\Service\TelegramBot\Actions\ActionInterface; | |
use App\Service\TelegramBot\Actions\CommonAction; | |
use App\Service\TelegramBot\Actions\DefaultAction; | |
use App\Service\TelegramBot\Actions\LanguageAction; | |
use App\Service\TelegramBot\Actions\LevelAction; | |
use App\Service\TelegramBot\Actions\SettingsAction; | |
use App\Service\TelegramBot\Actions\StartAction; | |
use App\Service\TelegramBot\Actions\StopAction; | |
use App\Service\TelegramBot\Actions\SubscriptionAction; | |
use App\Service\TelegramBot\Actions\WorkoutAction; | |
use Psr\Container\ContainerInterface; | |
use Symfony\Contracts\Service\ServiceSubscriberInterface; | |
use TelegramBot\Api\Types\CallbackQuery; | |
use TelegramBot\Api\Types\Message; | |
use TelegramBot\Api\Types\Update; | |
use TelegramBot\Api\Types\User; | |
class TelegramActionSelectorService implements ServiceSubscriberInterface | |
{ | |
public function __construct( | |
private readonly ContainerInterface $locator, | |
private readonly ClientService $clientService, | |
) { | |
} | |
public function getActionFromUpdate(Update $update): Action | |
{ | |
/** @var CommonAction $action */ | |
$action = $this->locator->get(DefaultAction::PREFIX); | |
$message = $update->getMessage(); | |
$query = $update->getCallbackQuery(); | |
if ($message) { | |
$action = $this->getActionFromMessage($message) ?? $action; | |
} elseif ($query) { | |
$action = $this->getActionFromQuery($query) ?? $action; | |
} | |
$request = $this->getRequest($action, $update); | |
return new Action($request, $action); | |
} | |
public function getService(string $serviceAlias): ?CommonAction | |
{ | |
$actionClass = collect(array_keys(self::getSubscribedServices())) | |
->filter(function ($item) use ($serviceAlias) { | |
return $item === $serviceAlias; | |
}) | |
->first(); | |
if ($actionClass) { | |
return $this->locator->get($actionClass); | |
} | |
return null; | |
} | |
public static function getSubscribedServices(): array | |
{ | |
return [ | |
DefaultAction::PREFIX => DefaultAction::class, | |
StartAction::PREFIX => StartAction::class, | |
LevelAction::PREFIX => LevelAction::class, | |
SubscriptionAction::PREFIX => SubscriptionAction::class, | |
WorkoutAction::PREFIX => WorkoutAction::class, | |
SettingsAction::PREFIX => SettingsAction::class, | |
LanguageAction::PREFIX => LanguageAction::class, | |
]; | |
} | |
private function getActionsList(): array | |
{ | |
return collect(array_keys(self::getSubscribedServices())) | |
->filter(function ($value) { | |
return $value !== DefaultAction::PREFIX; | |
}) | |
->all(); | |
} | |
private function getActionFromMessage(Message $message): ?ActionInterface | |
{ | |
$actionsAsMessage = [ | |
StartAction::PREFIX, | |
]; | |
$cmd = $message->getText(); | |
$actionClass = collect($actionsAsMessage) | |
->filter(function ($actionPrefix) use ($cmd) { | |
return ActionInterface::CMD_SYMBOL.$actionPrefix === $cmd; | |
}) | |
->first(); | |
if ($actionClass) { | |
return $this->locator->get($actionClass); | |
} | |
return null; | |
} | |
private function getActionFromQuery(CallbackQuery $query): ?ActionInterface | |
{ | |
$cmd = $query->getData(); | |
$actionClass = collect($this->getActionsList()) | |
->filter(function ($action) use ($cmd) { | |
return (ActionInterface::CMD_SYMBOL.$action === $cmd) || | |
(str_starts_with($cmd, $action.ActionInterface::CALLBACK_SEPARATOR)) || | |
($action === $cmd); | |
}) | |
->first(); | |
if ($actionClass) { | |
return $this->locator->get($actionClass); | |
} | |
return null; | |
} | |
private function getRequest(CommonAction $action, Update $update): ActionRequest | |
{ | |
$user = $this->getUserFromContext($update); | |
$isAction = $this->isAction($action::PREFIX, $update); | |
$data = $this->getData($action::PREFIX, $update); | |
$message = $this->getMessage($update); | |
$client = $this->clientService->getByTelegramId($user->getId()); | |
return new ActionRequest($user, $isAction, $client, $message, $data ?? ''); | |
} | |
private function getUserFromContext(Update $context): ?User | |
{ | |
$message = $context->getMessage(); | |
$query = $context->getCallbackQuery(); | |
if ($message) { | |
return $message->getFrom(); | |
} elseif ($query) { | |
return $query->getFrom(); | |
} | |
return null; | |
} | |
private function isAction(string $prefix, Update $update): bool | |
{ | |
$message = $update->getMessage(); | |
$query = $update->getCallbackQuery(); | |
if ($message) { | |
return $message->getText() === ActionInterface::CMD_SYMBOL.$prefix; | |
} elseif ($query) { | |
return str_contains($query->getData(), $prefix.ActionInterface::CALLBACK_SEPARATOR); | |
} else { | |
return false; | |
} | |
} | |
private function getData(string $prefix, Update $update): ?string | |
{ | |
$query = $update->getCallbackQuery(); | |
if ($query) { | |
return collect(explode(ActionInterface::CALLBACK_SEPARATOR, $query->getData())) | |
->filter(function ($value) use ($prefix) { | |
return $value !== $prefix; | |
}) | |
->first(); | |
} | |
return null; | |
} | |
private function getMessage(Update $context): ?Message | |
{ | |
$message = $context->getMessage(); | |
$query = $context->getCallbackQuery(); | |
if ($message) { | |
return $message; | |
} elseif ($query) { | |
return $query->getMessage(); | |
} | |
return null; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Service\TelegramBot; | |
use ReflectionClass; | |
use ReflectionMethod; | |
use TelegramBot\Api\Types\Message; | |
use TelegramBot\Api\Types\Update; | |
class TelegramSkipService | |
{ | |
public function isNeedToSkipUpdate(int $botId, Update $update): bool | |
{ | |
$callbackQuery = $update->getCallbackQuery(); | |
if ($callbackQuery) { | |
return false; | |
} | |
$message = $update->getMessage(); | |
if ($message) { | |
return $this->isNeedToSkipMessage($botId, $message); | |
} else { | |
return true; | |
} | |
} | |
public function isNeedToSkipMessage(int $botId, Message $message): bool | |
{ | |
$entitiesFieldName = 'entities'; | |
$supportedFields = ['text', 'date', 'chat', 'from', 'messageId']; | |
$user = $message->getFrom(); | |
if ($user && $user->getId() === $botId) { | |
return false; | |
} | |
$reflectionClass = new ReflectionClass($message); | |
$content = collect($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) | |
->filter(function ($method) { | |
/** @var ReflectionMethod $method */ | |
return !$method->isStatic() && str_contains($method->getName(), 'get'); | |
}) | |
->mapWithKeys(function ($method) use ($message) { | |
/** @var ReflectionMethod $method */ | |
$fieldName = lcfirst(str_replace('get', '', $method->getName())); | |
try { | |
$value = $method->invoke($message); | |
} catch (\ReflectionException $exception) { | |
$value = null; | |
} | |
return [$fieldName => $value]; | |
}) | |
->filter(function ($value) { | |
return $value !== null; | |
}); | |
if ($content->count() === 0) { | |
return true; | |
} | |
$badContent = $content | |
->filter(function ($value, $propertyName) use ($entitiesFieldName){ | |
if ($propertyName === $entitiesFieldName) { | |
return count($value) !== 1; | |
} else { | |
return true; | |
} | |
}) | |
->filter(function ($value, $propertyName) use ($supportedFields) { | |
return !in_array($propertyName, $supportedFields); | |
}) | |
; | |
return $badContent->count() !== 0; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Tests\Service\TelegramBot; | |
use App\Service\TelegramBot\TelegramSkipService; | |
use PHPUnit\Framework\TestCase; | |
use TelegramBot\Api\Types\Animation; | |
use TelegramBot\Api\Types\Audio; | |
use TelegramBot\Api\Types\CallbackQuery; | |
use TelegramBot\Api\Types\Chat; | |
use TelegramBot\Api\Types\Contact; | |
use TelegramBot\Api\Types\Dice; | |
use TelegramBot\Api\Types\Document; | |
use TelegramBot\Api\Types\Inline\InlineKeyboardMarkup; | |
use TelegramBot\Api\Types\Location; | |
use TelegramBot\Api\Types\Message; | |
use TelegramBot\Api\Types\MessageEntity; | |
use TelegramBot\Api\Types\Payments\Invoice; | |
use TelegramBot\Api\Types\Payments\SuccessfulPayment; | |
use TelegramBot\Api\Types\PhotoSize; | |
use TelegramBot\Api\Types\Poll; | |
use TelegramBot\Api\Types\Sticker; | |
use TelegramBot\Api\Types\Update; | |
use TelegramBot\Api\Types\User; | |
use TelegramBot\Api\Types\Venue; | |
use TelegramBot\Api\Types\Video; | |
use TelegramBot\Api\Types\Voice; | |
class TelegramSkipServiceTest extends TestCase | |
{ | |
private int $botId; | |
protected function setUp(): void | |
{ | |
$this->botId = 1656; | |
} | |
private function getTestableService(): TelegramSkipService | |
{ | |
return new TelegramSkipService(); | |
} | |
public function testNeedToSkipCallbackQueryCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$update = new Update(); | |
$update->setCallbackQuery(new CallbackQuery()); | |
$this->assertFalse($service->isNeedToSkipUpdate($this->botId, $update)); | |
} | |
public function testNeedToSkipEmptyUpdateCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$update = new Update(); | |
$this->assertTrue($service->isNeedToSkipUpdate($this->botId, $update)); | |
} | |
public function testNeedToSkipMessageEmptyCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageOnlyTextCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setText('text'); | |
$this->assertFalse($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageForwardCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setForwardFrom(new User()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
$message = new Message(); | |
$message->setForwardFromChat(new Chat()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
$message = new Message(); | |
$message->setForwardSenderName('name'); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
$message = new Message(); | |
$message->setForwardDate(6464646); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageReplyCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setReplyToMessage(new Message()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageMediaGroupCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setMediaGroupId(6412321); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
//public function testNeedToSkipMessageCaptionedEntitiesCase(): void | |
//{ | |
// $service = $this->getTestableService(); | |
// | |
// $message = new Message(); | |
// //TODO затрудняюсь протестировать | |
// $message->setCaptionEntities(ArrayOfMessageEntity::fromResponse([])); | |
// $this->assertFalse($service->isNeedToSkipMessage($message)); | |
//} | |
public function testNeedToSkipMessageAudioCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setAudio(new Audio()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageDocumentCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setDocument(new Document()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageAnimationCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setAnimation(new Animation()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessagePhotoCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setPhoto([new PhotoSize()]); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageStickerCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setSticker(new Sticker()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageVideoCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setVideo(new Video()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageVoiceCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setVoice(new Voice()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageCaptionCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setCaption('asdad'); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageContactCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setContact(new Contact()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageLocationCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setLocation(new Location()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageVenueCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setVenue(new Venue()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessagePollCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setPoll(new Poll()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageDiceCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setDice(new Dice()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessagePinnedMessageCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setPinnedMessage(new Message()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageInvoiceCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setInvoice(new Invoice()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageSuccessfulPaymentCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setSuccessfulPayment(new SuccessfulPayment()); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageConnectedWebsiteCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setConnectedWebsite('string'); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageReplyMarkupCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$message = new Message(); | |
$message->setReplyMarkup(new InlineKeyboardMarkup([])); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageOneCommandCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$messageEntity = new MessageEntity(); | |
$messageEntity->setType(MessageEntity::TYPE_BOT_COMMAND); | |
$message = new Message(); | |
$message->setEntities([$messageEntity]); | |
$this->assertFalse($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
public function testNeedToSkipMessageSomeCommandCase(): void | |
{ | |
$service = $this->getTestableService(); | |
$firstEntity = new MessageEntity(); | |
$firstEntity->setType(MessageEntity::TYPE_BOT_COMMAND); | |
$secondEntity = new MessageEntity(); | |
$secondEntity->setType(MessageEntity::TYPE_BOT_COMMAND); | |
$message = new Message(); | |
$message->setEntities([$firstEntity, $secondEntity]); | |
$this->assertTrue($service->isNeedToSkipMessage($this->botId, $message)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment