Skip to content

Instantly share code, notes, and snippets.

@denistouch
Last active December 30, 2022 07:12
Show Gist options
  • Save denistouch/f75f0a50d7b0da34296c1f52dec87027 to your computer and use it in GitHub Desktop.
Save denistouch/f75f0a50d7b0da34296c1f52dec87027 to your computer and use it in GitHub Desktop.
Первичная обработка Update из телеграма с целью выбора сервиса, который будет отвечать за его выполнение и получения запроса описывающего его требования
<?php
namespace App\Model\Telegram;
use App\Service\TelegramBot\Actions\CommonAction;
class Action
{
public function __construct(
public readonly ActionRequest $request,
public readonly CommonAction $command
)
{
}
}
<?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;
}
<?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 = '',
)
{
}
}
<?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 = '',
) {
}
}
<?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;
}
}
<?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'));
}
}
<?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;
}
}
<?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;
}
}
<?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