nutgram/nutgram

View on GitHub
src/Nutgram.php

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
<?php


namespace SergiX44\Nutgram;

use GuzzleHttp\Client as Guzzle;
use InvalidArgumentException;
use Laravel\SerializableClosure\SerializableClosure;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use SergiX44\Container\Container;
use SergiX44\Nutgram\Cache\ConversationCache;
use SergiX44\Nutgram\Cache\GlobalCache;
use SergiX44\Nutgram\Cache\UserCache;
use SergiX44\Nutgram\Conversations\Conversation;
use SergiX44\Nutgram\Handlers\FireHandlers;
use SergiX44\Nutgram\Handlers\ResolveHandlers;
use SergiX44\Nutgram\Handlers\Type\Command;
use SergiX44\Nutgram\Hydrator\Hydrator;
use SergiX44\Nutgram\Proxies\GlobalCacheProxy;
use SergiX44\Nutgram\Proxies\UpdateDataProxy;
use SergiX44\Nutgram\Proxies\UserCacheProxy;
use SergiX44\Nutgram\RunningMode\Polling;
use SergiX44\Nutgram\RunningMode\RunningMode;
use SergiX44\Nutgram\Support\BulkMessenger;
use SergiX44\Nutgram\Support\HandleLogging;
use SergiX44\Nutgram\Support\ValidatesWebData;
use SergiX44\Nutgram\Telegram\Client;
use SergiX44\Nutgram\Telegram\Types\Common\Update;
use SergiX44\Nutgram\Testing\FakeNutgram;
use Throwable;

class Nutgram extends ResolveHandlers
{
    use Client, UpdateDataProxy, GlobalCacheProxy, UserCacheProxy, FireHandlers, ValidatesWebData, HandleLogging;

    /**
     * @var string
     */
    private string $token;

    /**
     * @var Configuration
     */
    private Configuration $config;

    /**
     * @var Guzzle
     */
    private Guzzle $http;

    /**
     * @var Hydrator
     */
    protected Hydrator $hydrator;

    /**
     * @var LoggerInterface
     */
    private LoggerInterface $logger;

    /**
     * @var Container
     */
    protected Container $container;

    /**
     * Nutgram constructor.
     * @param string $token
     * @param Configuration|null $config
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function __construct(string $token, ?Configuration $config = null)
    {
        if (empty($token)) {
            throw new InvalidArgumentException('The token cannot be empty.');
        }

        $this->bootstrap($token, $config ?? new Configuration());
    }

    /**
     * Initializes the current instance
     * @param string $token
     * @param Configuration $config
     * @return void
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function bootstrap(string $token, Configuration $config): void
    {
        $this->token = $token;
        $this->config = $config;
        $this->container = new Container();
        if ($config->container !== null) {
            $this->container->delegate($config->container);
        }

        SerializableClosure::setSecretKey($this->token);

        $this->http = new Guzzle([
            'base_uri' => sprintf(
                '%s/bot%s/%s',
                $config->apiUrl,
                $this->token,
                $config->testEnv ?? false ? 'test/' : ''
            ),
            'timeout' => $config->clientTimeout,
            'version' => $config->enableHttp2 ? '2.0' : '1.1',
            ...$config->clientOptions,
        ]);
        $botId = $config->botId ?? (int)explode(':', $this->token)[0];
        $this->container->set(ClientInterface::class, $this->http);
        $this->container->singleton(Hydrator::class, $config->hydrator);
        $this->container->singleton(CacheInterface::class, $config->cache);
        $this->container->singleton(LoggerInterface::class, $config->logger);
        $this->container->singleton(
            ConversationCache::class,
            fn (ContainerInterface $c) => new ConversationCache($c->get(CacheInterface::class), $botId)
        );
        $this->container->singleton(
            GlobalCache::class,
            fn (ContainerInterface $c) => new GlobalCache($c->get(CacheInterface::class), $botId)
        );
        $this->container->singleton(
            UserCache::class,
            fn (ContainerInterface $c) => new UserCache($c->get(CacheInterface::class), $botId)
        );

        $this->hydrator = $this->container->get(Hydrator::class);
        $this->conversationCache = $this->container->get(ConversationCache::class);
        $this->globalCache = $this->container->get(GlobalCache::class);
        $this->userCache = $this->container->get(UserCache::class);
        $this->logger = $this->container->get(LoggerInterface::class);

        $this->container->singleton(RunningMode::class, Polling::class);
        $this->container->set(__CLASS__, $this);
    }

    /**
     * @return array
     */
    public function __serialize(): array
    {
        return [
            'token' => $this->token,
            'config' => $this->config,
        ];
    }

    /**
     * @param array $data
     * @return void
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function __unserialize(array $data): void
    {
        $this->bootstrap($data['token'], $data['config']);
    }

    /**
     * @param mixed $update
     * @param array $responses
     * @return FakeNutgram
     */
    public static function fake(mixed $update = null, array $responses = [], ?Configuration $config = null): FakeNutgram
    {
        return FakeNutgram::instance($update, $responses, $config);
    }

    /**
     * @param string|RunningMode $classOrInstance
     */
    public function setRunningMode(string|RunningMode $classOrInstance): void
    {
        $this->container->bind(RunningMode::class, $classOrInstance);
    }

    protected function preflight(): void
    {
        if (!$this->finalized) {
            $this->resolveGroups();
            $this->applyGlobalMiddleware();
            $this->finalized = true;
        }
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function run(): void
    {
        $this->preflight();
        $this->container->get(RunningMode::class)->processUpdates($this);
    }

    /**
     * @param Update $update
     * @throws Throwable
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function processUpdate(Update $update): void
    {
        $this->update = $update;

        $conversation = $this->currentConversation($this->userId(), $this->chatId());

        if ($conversation !== null) {
            $handlers = $this->continueConversation($conversation);
        } else {
            $handlers = $this->resolveHandlers();
        }

        if (empty($handlers) && !empty($this->handlers[self::FALLBACK])) {
            $this->addHandlersBy($handlers, self::FALLBACK, value: $this->update->getType()->value);
        }

        if (empty($handlers)) {
            $this->addHandlersBy($handlers, self::FALLBACK);
        }

        $this->fireHandlers($handlers);
    }

    /**
     * @param Conversations\Conversation|callable $callable
     * @param int|null $userId
     * @param int|null $chatId
     * @return $this
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function stepConversation(
        Conversations\Conversation|callable $callable,
        ?int $userId = null,
        ?int $chatId = null
    ): self {
        $userId = $userId ?? $this->userId();
        $chatId = $chatId ?? $this->chatId();

        if ($this->update === null && ($userId === null || $chatId === null)) {
            throw new InvalidArgumentException('You cannot step a conversation without userId and chatId.');
        }

        $this->conversationCache->set($userId, $chatId, $callable);

        return $this;
    }

    /**
     * @param int|null $userId
     * @param int|null $chatId
     * @return $this
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function endConversation(?int $userId = null, ?int $chatId = null): self
    {
        $userId = $userId ?? $this->userId();
        $chatId = $chatId ?? $this->chatId();

        if ($this->update === null && ($userId === null || $chatId === null)) {
            throw new InvalidArgumentException('You cannot end a conversation without userId and chatId.');
        }

        $this->conversationCache->delete($userId, $chatId);

        return $this;
    }

    public function getContainer(): ContainerInterface
    {
        return $this->container;
    }

    /**
     * @return Configuration
     */
    public function getConfig(): Configuration
    {
        return $this->config;
    }

    /**
     * @return string
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function getRunningMode(): string
    {
        return $this->container->get(RunningMode::class)::class;
    }

    /**
     * Set my commands call to Telegram using all the registered commands
     */
    public function registerMyCommands(): void
    {
        $this->preflight();

        $myCommands = [];
        array_walk_recursive($this->handlers, static function ($handler) use (&$myCommands) {
            if ($handler instanceof Command && !$handler->isHidden()) {
                // scopes
                foreach ($handler->scopes() as $scope) {
                    $hashCode = crc32(serialize(get_object_vars($scope)));

                    // language_code
                    foreach ($handler->getAllDescriptions() as $language => $description) {
                        $myCommands[$hashCode]['scopes'][$language] = $scope;
                        $myCommands[$hashCode]['commands'][$language][] = $handler->toBotCommand($language);
                    }
                }
            }
        });

        // set commands for each scope
        foreach ($myCommands as $myCommand) {
            // set commands for each language
            foreach ($myCommand['scopes'] as $language => $scope) {
                $this->setMyCommands(
                    commands: array_values(array_unique($myCommand['commands'][$language], SORT_REGULAR)),
                    scope: $scope,
                    language_code: $language === '*' ? null : $language
                );
            }
        }
    }

    /**
     * @return BulkMessenger
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function getBulkMessenger(): BulkMessenger
    {
        return $this->container->get(BulkMessenger::class);
    }

    public function invoke(callable|array|string $callable, array $params = []): mixed
    {
        return $this->container->call($callable, $params);
    }
}