├── app ├── migrations │ ├── .gitignore │ ├── Version20240326004348.php │ └── Version20240327122025.php ├── supervisord.pid ├── src │ ├── PresentationSystem │ │ ├── Http │ │ │ └── Controller │ │ │ │ └── .gitignore │ │ └── CommandLine │ │ │ ├── SubscribersFeature │ │ │ ├── GithubSyncSubscribersCommand.php │ │ │ ├── GithubSubscribersBalancingCommand.php │ │ │ └── GithubCheckUnfollowingCommand.php │ │ │ └── SubscriptionFeature │ │ │ ├── GithubSyncSubscriptionsCommand.php │ │ │ └── GithubSubscriptionsBalancingCommand.php │ ├── InfrastructureSystem │ │ ├── InternalFollowersFeatureApi │ │ │ ├── .gitkeep │ │ │ ├── Exception │ │ │ │ └── OutOfRangeException.php │ │ │ ├── DataObject │ │ │ │ ├── SubscriberInterface.php │ │ │ │ └── SubscriptionInterface.php │ │ │ └── Manager │ │ │ │ └── InternalSubscribersManagerInterface.php │ │ ├── EventDispatcherFeature │ │ │ ├── EventInterface.php │ │ │ ├── EventDispatcherInterface.php │ │ │ ├── Event.php │ │ │ └── EventDispatcher.php │ │ ├── LoggerFeature │ │ │ ├── LoggerInterface.php │ │ │ └── Logger.php │ │ ├── MessageBusFeatureApi │ │ │ └── MessageBusInterface.php │ │ ├── SymfonyMessengerFeature │ │ │ ├── SymfonyMessageBusInterface.php │ │ │ └── MessageBus.php │ │ ├── DoctrineFeature │ │ │ ├── DoctrineEntityManagerInterface.php │ │ │ └── DoctrineEntityManager.php │ │ ├── GithubFollowersFeature │ │ │ ├── Interfaces │ │ │ │ └── GithubSubscribersManagerInterface.php │ │ │ └── DataObject │ │ │ │ ├── Subscriber.php │ │ │ │ └── Subscription.php │ │ ├── DatabaseFeatureApi │ │ │ └── DatabaseEntityManagerInterface.php │ │ ├── SerializerFeature │ │ │ ├── DeserializerInterface.php │ │ │ └── Deserializer.php │ │ ├── SubscribersFeature │ │ │ ├── Storage │ │ │ │ └── SubscriberStorage.php │ │ │ ├── Factory │ │ │ │ └── SubscriberFactory.php │ │ │ ├── Scheduler │ │ │ │ └── SubscribersBalancingActionScheduler.php │ │ │ ├── Repository │ │ │ │ └── SubscriberRepository.php │ │ │ └── Entity │ │ │ │ └── Subscriber.php │ │ └── SubscriptionFeature │ │ │ ├── Factory │ │ │ └── SubscriptionFactory.php │ │ │ ├── Storage │ │ │ └── SubscriptionStorage.php │ │ │ ├── Repository │ │ │ └── SubscriptionRepository.php │ │ │ └── Entity │ │ │ └── Subscription.php │ ├── Kernel.php │ ├── MessageBusSystem │ │ └── SubscribersFeature │ │ │ ├── Interfaces │ │ │ └── SubscribersFeatureActionHookMessageInterface.php │ │ │ ├── UnsubscribeAction │ │ │ ├── Interfaces │ │ │ │ ├── UnsubscribeActionMessageHandlerInterface.php │ │ │ │ ├── UnsubscribeActionMessageFactoryInterface.php │ │ │ │ ├── UnsubscribeAfterCheckDefinedEventListenerInterface.php │ │ │ │ └── UnsubscribeActionMessageInterface.php │ │ │ ├── MessageFactory.php │ │ │ ├── Message.php │ │ │ ├── UnsubscribeAfterCheckDefinedEventListener.php │ │ │ └── MessageHandler.php │ │ │ ├── SubscribersBalancingAction │ │ │ ├── Interfaces │ │ │ │ ├── SubscribersBalancingActionMessageHandlerInterface.php │ │ │ │ ├── SubscribersBalancingActionMessageInterface.php │ │ │ │ └── SubscribersBalancingActionMessageFactoryInterface.php │ │ │ ├── MessageFactory.php │ │ │ ├── Message.php │ │ │ └── MessageHandler.php │ │ │ └── CheckUnsubscribedHandledHookAction │ │ │ ├── Interfaces │ │ │ ├── CheckUnsubscribedHandledHookActionMessageHandlerInterface.php │ │ │ ├── CheckUnsubscribedHandledHookActionListenerInterface.php │ │ │ ├── CheckUnsubscribedHandledHookActionMessageFactoryInterface.php │ │ │ └── CheckUnsubscribedHandledHookActionMessageInterface.php │ │ │ ├── Message.php │ │ │ ├── MessageFactory.php │ │ │ ├── MessageHandler.php │ │ │ └── Listener.php │ ├── ApplicationSystem │ │ ├── SynchronizationFeature │ │ │ ├── SynchronizationSubscribersManagerInterface.php │ │ │ ├── SynchronizationSubscriptionsManagerInterface.php │ │ │ ├── SynchronizationSubscribersManager.php │ │ │ └── SynchronizationSubscriptionsManager.php │ │ ├── SubscribersFeature │ │ │ ├── Actions │ │ │ │ ├── RemoveAllSubscribersAction │ │ │ │ │ ├── Interfaces │ │ │ │ │ │ └── RemoveAllSubscribersHandlerInterface.php │ │ │ │ │ └── Handler.php │ │ │ │ ├── SyncSubscribersAction │ │ │ │ │ ├── Interfaces │ │ │ │ │ │ └── SyncSubscribersActionHandlerInterface.php │ │ │ │ │ └── Handler.php │ │ │ │ ├── SubscribersBalancingAction │ │ │ │ │ ├── Interfaces │ │ │ │ │ │ └── SubscribersBalancingActionHandlerInterface.php │ │ │ │ │ └── Handler.php │ │ │ │ ├── UnsubscribeAction │ │ │ │ │ ├── Interfaces │ │ │ │ │ │ └── UnsubscribeActionHandlerInterface.php │ │ │ │ │ └── Handler.php │ │ │ │ ├── CheckUnsubscribedHandledHookAction │ │ │ │ │ ├── Interfaces │ │ │ │ │ │ ├── CheckUnsubscribedHandledHookActionHandlerInterface.php │ │ │ │ │ │ └── UnsubscribeAfterCheckDefinedEventInterface.php │ │ │ │ │ ├── UnsubscribeAfterCheckDefinedEvent.php │ │ │ │ │ └── Handler.php │ │ │ │ └── CheckUnsubscribedAction │ │ │ │ │ ├── Interfaces │ │ │ │ │ └── CheckUnsubscribedActionHandlerInterface.php │ │ │ │ │ └── Handler.php │ │ │ ├── Enums │ │ │ │ └── ActionsEnum.php │ │ │ └── Interfaces │ │ │ │ └── Manager │ │ │ │ └── SubscribersManagerInterface.php │ │ └── SubscriptionFeature │ │ │ ├── Actions │ │ │ ├── RemoveAllSubscriptionsAction │ │ │ │ ├── Interfaces │ │ │ │ │ └── RemoveAllSubscriptionsHandlerInterface.php │ │ │ │ └── Handler.php │ │ │ ├── SyncSubscriptionsAction │ │ │ │ ├── Interfaces │ │ │ │ │ └── SyncSubscriptionsActionHandlerInterface.php │ │ │ │ └── Handler.php │ │ │ └── SubscriptionsBalancingAction │ │ │ │ ├── Interfaces │ │ │ │ └── SubscriptionsBalancingActionHandlerInterface.php │ │ │ │ └── Handler.php │ │ │ └── Interfaces │ │ │ └── Manager │ │ │ └── SubscriptionManagerInterface.php │ ├── CommandQueryBusSystem │ │ ├── SubscribersFeature │ │ │ ├── SyncSubscribersAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── SyncSubscribersActionHandlerInterface.php │ │ │ │ │ ├── SyncSubscribersActionCommandValidatorInterface.php │ │ │ │ │ ├── SyncSubscribersActionCommandInterface.php │ │ │ │ │ └── SyncSubscribersActionCommandFactoryInterface.php │ │ │ │ ├── CommandFactory.php │ │ │ │ ├── Command.php │ │ │ │ ├── CommandValidator.php │ │ │ │ └── Handler.php │ │ │ ├── UnsubscribeAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── UnsubscribeActionHandlerInterface.php │ │ │ │ │ ├── UnsubscribeActionCommandValidatorInterface.php │ │ │ │ │ ├── UnsubscribeActionCommandInterface.php │ │ │ │ │ └── UnsubscribeActionCommandFactoryInterface.php │ │ │ │ ├── CommandFactory.php │ │ │ │ ├── CommandValidator.php │ │ │ │ ├── Command.php │ │ │ │ └── Handler.php │ │ │ ├── SubscribersBalancingAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── SubscribersBalancingActionHandlerInterface.php │ │ │ │ │ ├── SubscribersBalancingActionCommandInterface.php │ │ │ │ │ ├── SubscribersBalancingActionCommandValidatorInterface.php │ │ │ │ │ └── SubscribersBalancingActionCommandFactoryInterface.php │ │ │ │ ├── CommandFactory.php │ │ │ │ ├── Command.php │ │ │ │ ├── CommandValidator.php │ │ │ │ └── Handler.php │ │ │ ├── SubscribersManagerInterface.php │ │ │ ├── CheckUnsubscribedAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── CheckUnsubscribedActionQueryValidatorInterface.php │ │ │ │ │ ├── CheckUnsubscribedActionHandlerInterface.php │ │ │ │ │ ├── CheckUnsubscribedActionResponseInterface.php │ │ │ │ │ ├── CheckUnsubscribedActionQueryFactoryInterface.php │ │ │ │ │ ├── CheckUnsubscribedActionQueryInterface.php │ │ │ │ │ ├── CheckUnsubscribedActionResponseFactoryInterface.php │ │ │ │ │ └── CheckUnsubscribedActionHandledEventInterface.php │ │ │ │ ├── Response.php │ │ │ │ ├── ResponseFactory.php │ │ │ │ ├── QueryFactory.php │ │ │ │ ├── Query.php │ │ │ │ ├── HandledEvent.php │ │ │ │ ├── QueryValidator.php │ │ │ │ └── Handler.php │ │ │ ├── CheckUnsubscribedHandledHookAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── CheckUnsubscribedHandledHookActionHandlerInterface.php │ │ │ │ │ ├── CheckUnsubscribedHandledHookActionCommandValidatorInterface.php │ │ │ │ │ ├── CheckUnsubscribedHandledHookActionCommandInterface.php │ │ │ │ │ └── CheckUnsubscribedHandledHookActionCommandFactoryInterface.php │ │ │ │ ├── CommandFactory.php │ │ │ │ ├── Command.php │ │ │ │ ├── CommandValidator.php │ │ │ │ └── Handler.php │ │ │ └── SubscribersManager.php │ │ ├── SubscriptionFeature │ │ │ ├── SyncSubscriptionsAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── SyncSubscriptionsActionHandlerInterface.php │ │ │ │ │ ├── SyncSubscriptionsActionCommandInterface.php │ │ │ │ │ ├── SyncSubscriptionsActionCommandValidatorInterface.php │ │ │ │ │ └── SyncSubscriptionsActionCommandFactoryInterface.php │ │ │ │ ├── CommandFactory.php │ │ │ │ ├── Command.php │ │ │ │ ├── CommandValidator.php │ │ │ │ └── Handler.php │ │ │ ├── SubscriptionsBalancingAction │ │ │ │ ├── Interfaces │ │ │ │ │ ├── SubscriptionsBalancingActionHandlerInterface.php │ │ │ │ │ ├── SubscriptionsBalancingActionCommandInterface.php │ │ │ │ │ ├── SubscriptionsBalancingActionCommandValidatorInterface.php │ │ │ │ │ └── SubscriptionsBalancingActionCommandFactoryInterface.php │ │ │ │ ├── CommandFactory.php │ │ │ │ ├── Command.php │ │ │ │ ├── CommandValidator.php │ │ │ │ └── Handler.php │ │ │ ├── SubscriptionManagerInterface.php │ │ │ └── SubscriptionManager.php │ │ └── Exception │ │ │ └── CommandQueryValidatorException.php │ └── DomainSystem │ │ ├── SubscriptionFeature │ │ ├── SubscriptionManagerInterface.php │ │ ├── Repository │ │ │ └── SubscriptionRepositoryInterface.php │ │ ├── Storage │ │ │ └── SubscriptionStorageInterface.php │ │ ├── Factory │ │ │ └── SubscriptionFactoryInterface.php │ │ ├── SubscriptionManager.php │ │ └── Entity │ │ │ └── SubscriptionInterface.php │ │ ├── UserFeature │ │ ├── Interfaces │ │ │ ├── DataObject │ │ │ │ └── UserInterface.php │ │ │ └── Factory │ │ │ │ └── UserFactoryInterface.php │ │ ├── Factory │ │ │ └── UserFactory.php │ │ └── DataObject │ │ │ └── User.php │ │ └── SubscribersFeature │ │ ├── Storage │ │ └── SubscriberStorageInterface.php │ │ ├── SubscribersManagerInterface.php │ │ ├── Repository │ │ └── SubscriberRepositoryInterface.php │ │ ├── Factory │ │ └── SubscriberFactoryInterface.php │ │ ├── SubscribersManager.php │ │ └── Entity │ │ └── SubscriberInterface.php ├── .gitignore ├── config │ ├── routes │ │ └── framework.yaml │ ├── routes.yaml │ ├── preload.php │ ├── packages │ │ ├── doctrine_migrations.yaml │ │ ├── routing.yaml │ │ ├── validator.yaml │ │ ├── messenger.yaml │ │ ├── cache.yaml │ │ ├── framework.yaml │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── bundles.php │ └── services.yaml ├── public │ └── index.php ├── .docker │ └── php │ │ └── Dockerfile ├── .env ├── .supervisord │ ├── conf.d │ │ ├── supervisord.conf │ │ └── program │ │ │ └── scheduler-subscribers-balancing.conf │ └── Dockerfile ├── bin │ └── console ├── composer.json └── symfony.lock ├── .gitignore ├── beanstalkd └── Dockerfile ├── postgresql └── Dockerfile ├── .env-sample ├── Makefile ├── docker-compose.yml └── README.md /app/migrations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/supervisord.pid: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.idea 3 | /.env 4 | -------------------------------------------------------------------------------- /app/src/PresentationSystem/Http/Controller/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /beanstalkd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM schickling/beanstalkd 2 | -------------------------------------------------------------------------------- /postgresql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:alpine3.19 2 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/InternalFollowersFeatureApi/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /.env.local 2 | /.env.local.php 3 | /.env.*.local 4 | /config/secrets/prod/prod.decrypt.private.php 5 | /public/bundles/ 6 | /var/ 7 | /vendor/ 8 | -------------------------------------------------------------------------------- /app/config/routes/framework.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | _errors: 3 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 4 | prefix: /_error 5 | -------------------------------------------------------------------------------- /app/config/routes.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: 3 | path: ../src/PresentationSystem//Http/Controller/ 4 | namespace: App\PresentationSystem\Http\Controller 5 | type: attribute 6 | -------------------------------------------------------------------------------- /app/config/preload.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 9 | ]; 10 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscriptionFeature/SubscriptionsBalancingAction/Interfaces/SubscriptionsBalancingActionCommandInterface.php: -------------------------------------------------------------------------------- 1 | errors)[0], 400); 11 | } 12 | 13 | public function getErrors(): array 14 | { 15 | return $this->errors; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Interfaces/CheckUnsubscribedHandledHookActionCommandValidatorInterface.php: -------------------------------------------------------------------------------- 1 | messageBus->dispatch($message, $stamps); 17 | 18 | return true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. 4 | # failure_transport: failed 5 | 6 | transports: 7 | 8 | routing: 9 | 10 | # when@test: 11 | # framework: 12 | # messenger: 13 | # transports: 14 | # # replace with your transport name here (e.g., my_transport: 'in-memory://') 15 | # # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test 16 | # async: 'in-memory://' 17 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedAction/Interfaces/CheckUnsubscribedActionQueryFactoryInterface.php: -------------------------------------------------------------------------------- 1 | items; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/EventDispatcherFeature/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch($event); 17 | 18 | return true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/DomainSystem/SubscribersFeature/Factory/SubscriberFactoryInterface.php: -------------------------------------------------------------------------------- 1 | value; 20 | }, 21 | self::cases(), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedAction/Interfaces/CheckUnsubscribedActionHandledEventInterface.php: -------------------------------------------------------------------------------- 1 | subscriptionStorage->save($subscriber, $flush); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Interfaces/CheckUnsubscribedHandledHookActionMessageFactoryInterface.php: -------------------------------------------------------------------------------- 1 | subscribersManager->removeByTargetUsername($targetUsername); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/SubscribersBalancingAction/CommandFactory.php: -------------------------------------------------------------------------------- 1 | login; 21 | } 22 | 23 | public function isSubscribed(): ?bool 24 | { 25 | return $this->subscribed; 26 | } 27 | 28 | public function setSubscribed(?bool $subscribed): void 29 | { 30 | $this->subscribed = $subscribed; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/SyncSubscribersAction/Command.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 18 | } 19 | 20 | public function getTargetUsername(): string 21 | { 22 | return $this->targetUsername; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/UnsubscribeAction/MessageFactory.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 18 | } 19 | 20 | public function getTargetUsername(): string 21 | { 22 | return $this->targetUsername; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/SubscribersBalancingAction/Message.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 18 | } 19 | 20 | public function getTargetUsername(): string 21 | { 22 | return $this->targetUsername; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/UnsubscribeAction/CommandFactory.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 18 | } 19 | 20 | public function getTargetUsername(): string 21 | { 22 | return $this->targetUsername; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Interfaces/CheckUnsubscribedHandledHookActionCommandFactoryInterface.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 18 | } 19 | 20 | public function getTargetUsername(): string 21 | { 22 | return $this->targetUsername; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SerializerFeature/Deserializer.php: -------------------------------------------------------------------------------- 1 | serializer->deserialize($data, $type, $format, $context); 17 | } 18 | 19 | public function deserializeAsArray(mixed $data, string $type, string $format, array $context = []): mixed 20 | { 21 | return $this->serializer->deserialize($data, $type . '[]', $format, $context); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/UnsubscribeAction/CommandValidator.php: -------------------------------------------------------------------------------- 1 | getUser()->isSubscribed()) { 17 | return ['user' => 'User already following target user.']; 18 | } 19 | 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/UnsubscribeAction/Message.php: -------------------------------------------------------------------------------- 1 | user; 22 | } 23 | 24 | public function getTargetUserToken(): string 25 | { 26 | return $this->targetUserToken; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/UnsubscribeAction/Command.php: -------------------------------------------------------------------------------- 1 | user; 22 | } 23 | 24 | public function getTargetUserToken(): string 25 | { 26 | return $this->targetUserToken; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/DomainSystem/SubscribersFeature/SubscribersManager.php: -------------------------------------------------------------------------------- 1 | subscriberStorage->save($subscriber, $flush); 18 | } 19 | 20 | public function removeByTargetUsername(string $targetUsername): void 21 | { 22 | $this->subscriberStorage->removeBy(['targetUsername' => $targetUsername]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Message.php: -------------------------------------------------------------------------------- 1 | command; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedAction/QueryFactory.php: -------------------------------------------------------------------------------- 1 | logger->debug($this->prepareMessage($message), $this->prepareContext($context)); 19 | } 20 | 21 | private function prepareMessage(string $message): string 22 | { 23 | return self::PREFIX . $message; 24 | } 25 | 26 | private function prepareContext(array $context): array 27 | { 28 | return array_merge([ 29 | '__time' => microtime(true), 30 | ],$context); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Actions/UnsubscribeAction/Handler.php: -------------------------------------------------------------------------------- 1 | internalSubscribersManager->unsubscribe($targetUserToken, $user->getLogin()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Actions/CheckUnsubscribedAction/Interfaces/CheckUnsubscribedActionHandlerInterface.php: -------------------------------------------------------------------------------- 1 | getTargetUsername()) { 14 | return ['targetUsername' => 'TargetUsername обязательный параметр']; 15 | } 16 | 17 | if (!$command->getTargetUserToken()) { 18 | return ['targetUserToken' => 'TargetUserToken обязательный параметр']; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/MessageFactory.php: -------------------------------------------------------------------------------- 1 | entityManager->save($subscriber, $flush); 20 | } 21 | 22 | public function removeBy(array $criteria): void 23 | { 24 | $this->entityManager->removeBy(Subscriber::class, $criteria); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Actions/CheckUnsubscribedHandledHookAction/UnsubscribeAfterCheckDefinedEvent.php: -------------------------------------------------------------------------------- 1 | users; 21 | } 22 | 23 | public function getTargetUserToken(): string 24 | { 25 | return $this->targetUserToken; 26 | } 27 | 28 | public function getName(): string 29 | { 30 | return self::class; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscriptionFeature/SyncSubscriptionsAction/CommandValidator.php: -------------------------------------------------------------------------------- 1 | getTargetUsername()) { 14 | return ['targetUsername' => 'TargetUsername обязательный параметр']; 15 | } 16 | 17 | if (!$command->getTargetUserToken()) { 18 | return ['targetUserToken' => 'TargetUserToken обязательный параметр']; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/SubscribersBalancingAction/CommandValidator.php: -------------------------------------------------------------------------------- 1 | getTargetUsername()) { 14 | return ['targetUsername' => 'TargetUsername обязательный параметр']; 15 | } 16 | 17 | if (!$command->getTargetUserToken()) { 18 | return ['targetUserToken' => 'TargetUserToken обязательный параметр']; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscriptionFeature/SubscriptionsBalancingAction/CommandValidator.php: -------------------------------------------------------------------------------- 1 | getTargetUsername()) { 14 | return ['targetUsername' => 'TargetUsername обязательный параметр']; 15 | } 16 | 17 | if (!$command->getTargetUserToken()) { 18 | return ['targetUserToken' => 'TargetUserToken обязательный параметр']; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/MessageHandler.php: -------------------------------------------------------------------------------- 1 | handler->handle($message->getCommand()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/migrations/Version20240326004348.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE SEQUENCE subscribers_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); 20 | $this->addSql('CREATE TABLE subscribers (id INT NOT NULL, target_username VARCHAR(255) NOT NULL, login VARCHAR(255) NOT NULL, internal_id BIGINT NOT NULL, url VARCHAR(255) NOT NULL, repositories_url VARCHAR(255) NOT NULL, subscriptions_url VARCHAR(255) NOT NULL, starred_url VARCHAR(255) NOT NULL, followers_url VARCHAR(255) NOT NULL, following_url VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 21 | } 22 | 23 | public function down(Schema $schema): void 24 | { 25 | $this->addSql('DROP SEQUENCE subscribers_id_seq CASCADE'); 26 | $this->addSql('DROP TABLE subscribers'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/migrations/Version20240327122025.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); 20 | $this->addSql('CREATE TABLE subscriptions (id INT NOT NULL, target_username VARCHAR(255) NOT NULL, login VARCHAR(255) NOT NULL, internal_id BIGINT NOT NULL, url VARCHAR(255) NOT NULL, repositories_url VARCHAR(255) NOT NULL, subscriptions_url VARCHAR(255) NOT NULL, starred_url VARCHAR(255) NOT NULL, followers_url VARCHAR(255) NOT NULL, following_url VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 21 | } 22 | 23 | public function down(Schema $schema): void 24 | { 25 | $this->addSql('DROP SEQUENCE subscriptions_id_seq CASCADE'); 26 | $this->addSql('DROP TABLE subscriptions'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SubscribersFeature/Factory/SubscriberFactory.php: -------------------------------------------------------------------------------- 1 | value, $actions, true)) { 26 | $this->eventDispatcher->dispatch(new UnsubscribeAfterCheckDefinedEvent($users, $targetUserToken)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/CommandFactory.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 24 | } 25 | 26 | public function getTargetUsername(): ?string 27 | { 28 | return $this->targetUsername; 29 | } 30 | 31 | public function getActions(): array 32 | { 33 | return $this->actions; 34 | } 35 | 36 | public function getPage(): int 37 | { 38 | return $this->page; 39 | } 40 | 41 | public function getLimit(): int 42 | { 43 | return $this->limit; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SubscriptionFeature/Storage/SubscriptionStorage.php: -------------------------------------------------------------------------------- 1 | entityManager->save($subscription, $flush); 20 | } 21 | 22 | public function removeByTargetUsername(string $targetUsername): void 23 | { 24 | $this->entityManager->removeBy(Subscription::class, [ 25 | 'targetUsername' => $targetUsername, 26 | ]); 27 | } 28 | 29 | public function remove(SubscriptionInterface $subscription, bool $flush = false): void 30 | { 31 | $this->entityManager->remove($subscription, $flush); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/UnsubscribeAction/UnsubscribeAfterCheckDefinedEventListener.php: -------------------------------------------------------------------------------- 1 | getUsers() as $user) { 24 | $this->messageBus->dispatch($this->messageFactory->create($user, $event->getTargetUserToken())); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/DomainSystem/SubscribersFeature/Entity/SubscriberInterface.php: -------------------------------------------------------------------------------- 1 | add(RecurringMessage::cron( 25 | $this->subscribersBalancingSchedule, 26 | $this->messageFactory->create( 27 | $this->githubPersonalAccessToken, 28 | $this->githubTargetUsername, 29 | ), 30 | )); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/UnsubscribeAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($command); 25 | if (!empty($errors)) { 26 | throw new CommandQueryValidatorException($errors); 27 | } 28 | 29 | $this->subscribersManager->unsubscribe($command->getUser(), $command->getTargetUserToken()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Command.php: -------------------------------------------------------------------------------- 1 | response; 24 | } 25 | 26 | public function getTargetUserToken(): ?string 27 | { 28 | return $this->targetUserToken; 29 | } 30 | 31 | public function getTargetUsername(): ?string 32 | { 33 | return $this->targetUsername; 34 | } 35 | 36 | public function getActions(): ?array 37 | { 38 | return $this->actions; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/SyncSubscribersAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($command); 22 | if ($errors) { 23 | throw new CommandQueryValidatorException($errors); 24 | } 25 | 26 | $this->subscribersManager->syncSubscribers($command->getTargetUserToken(), $command->getTargetUsername()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedAction/HandledEvent.php: -------------------------------------------------------------------------------- 1 | targetUserToken; 24 | } 25 | 26 | public function getTargetUsername(): string 27 | { 28 | return $this->targetUsername; 29 | } 30 | 31 | public function getActions(): array 32 | { 33 | return $this->actions; 34 | } 35 | 36 | public function getName(): string 37 | { 38 | return self::class; 39 | } 40 | 41 | public function getResponse(): CheckUnsubscribedActionResponseInterface 42 | { 43 | return $this->response; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscriptionFeature/SyncSubscriptionsAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($command); 22 | if ($errors) { 23 | throw new CommandQueryValidatorException($errors); 24 | } 25 | 26 | $this->subscriptionManager->syncSubscriptions($command->getTargetUserToken(), $command->getTargetUsername()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/SubscribersBalancingAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($command); 22 | if ($errors) { 23 | throw new CommandQueryValidatorException($errors); 24 | } 25 | 26 | $this->subscribersManager->subscribersBalancing($command->getTargetUserToken(), $command->getTargetUsername()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SubscribersFeature/Repository/SubscriberRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('subscriber'); 23 | 24 | return $queryBuilder->leftJoin( 25 | Subscription::class, 26 | 'subscription', 27 | Join::WITH, 28 | 'subscription.login = subscriber.login', 29 | ) 30 | ->andWhere('subscription.login IS NULL') 31 | ->andWhere('subscriber.targetUsername = :targetUsername') 32 | ->setParameter('targetUsername', $targetUsername) 33 | ->getQuery() 34 | ->getResult() 35 | ; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscriptionFeature/SubscriptionsBalancingAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($command); 22 | if ($errors) { 23 | throw new CommandQueryValidatorException($errors); 24 | } 25 | 26 | $this->subscriptionManager->subscriptionsBalancing($command->getTargetUserToken(), $command->getTargetUsername()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SubscriptionFeature/Repository/SubscriptionRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('subscription'); 23 | 24 | return $queryBuilder->leftJoin( 25 | Subscriber::class, 26 | 'subscriber', 27 | Join::WITH, 28 | 'subscription.login = subscriber.login', 29 | ) 30 | ->andWhere('subscriber.login IS NULL') 31 | ->andWhere('subscription.targetUsername = :targetUsername') 32 | ->setParameter('targetUsername', $targetUsername) 33 | ->getQuery() 34 | ->getResult() 35 | ; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/UnsubscribeAction/MessageHandler.php: -------------------------------------------------------------------------------- 1 | handler->handle($this->commandFactory->create($message->getUser(), $message->getTargetUserToken())); 26 | 27 | $this->logger->debug(sprintf('Отписались от пользователя %s', $message->getUser()->getLogin()), [ 28 | 'class' => __CLASS__, 29 | 'method' => __METHOD__, 30 | 'line' => __LINE__, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscriptionFeature/Actions/RemoveAllSubscriptionsAction/Handler.php: -------------------------------------------------------------------------------- 1 | subscriptionStorage->removeByTargetUsername($targetUserName); 24 | } catch (\Throwable $exception) { 25 | $this->logger->debug('При удалении подписок произошла непредвиденная ошибка.', [ 26 | 'arguments' => func_get_args(), 27 | 'exception' => [ 28 | 'message' => $exception->getMessage(), 29 | 'class' => get_class($exception), 30 | 'code' => $exception->getCode(), 31 | ], 32 | 'class' => __CLASS__, 33 | 'method' => __METHOD__, 34 | 'line' => __LINE__, 35 | ]); 36 | throw $exception; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedAction/QueryValidator.php: -------------------------------------------------------------------------------- 1 | getTargetUserToken()) { 18 | return ['targetUserToken' => 'TargetUserToken обязательный параметр']; 19 | } 20 | 21 | if (!$query->getTargetUsername()) { 22 | return ['targetUsername' => 'TargetUsername обязательный параметр']; 23 | } 24 | 25 | if (!empty($query->getActions())) { 26 | $diff = array_diff($query->getActions(), ActionsEnum::getCases()); 27 | if (!empty($diff)) { 28 | return [$diff[0] => sprintf('Неизвестное действие "%s".', $diff[0])]; 29 | } 30 | } 31 | 32 | if ($query->getPage() < 1) { 33 | return ['page' => 'Cannot have negative value.']; 34 | } 35 | 36 | if ($query->getLimit() < 1) { 37 | return ['page' => 'Cannot have negative value.']; 38 | } 39 | 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/GithubFollowersFeature/DataObject/Subscriber.php: -------------------------------------------------------------------------------- 1 | login; 24 | } 25 | 26 | public function getId(): int 27 | { 28 | return $this->id; 29 | } 30 | 31 | public function getUrl(): string 32 | { 33 | return $this->url; 34 | } 35 | 36 | public function getReposUrl(): string 37 | { 38 | return $this->repos_url; 39 | } 40 | 41 | public function getSubscriptionsUrl(): string 42 | { 43 | return $this->subscriptions_url; 44 | } 45 | 46 | public function getStarredUrl(): string 47 | { 48 | return $this->starred_url; 49 | } 50 | 51 | public function getFollowersUrl(): string 52 | { 53 | return $this->followers_url; 54 | } 55 | 56 | public function getFollowingUrl(): string 57 | { 58 | return $this->following_url; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/GithubFollowersFeature/DataObject/Subscription.php: -------------------------------------------------------------------------------- 1 | login; 24 | } 25 | 26 | public function getId(): int 27 | { 28 | return $this->id; 29 | } 30 | 31 | public function getUrl(): string 32 | { 33 | return $this->url; 34 | } 35 | 36 | public function getReposUrl(): string 37 | { 38 | return $this->repos_url; 39 | } 40 | 41 | public function getSubscriptionsUrl(): string 42 | { 43 | return $this->subscriptions_url; 44 | } 45 | 46 | public function getStarredUrl(): string 47 | { 48 | return $this->starred_url; 49 | } 50 | 51 | public function getFollowersUrl(): string 52 | { 53 | return $this->followers_url; 54 | } 55 | 56 | public function getFollowingUrl(): string 57 | { 58 | return $this->following_url; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscriptionFeature/Interfaces/Manager/SubscriptionManagerInterface.php: -------------------------------------------------------------------------------- 1 | getResponse()?->getItems())) { 18 | return ['response.items' => 'Список не подписанных пользователей пуст.']; 19 | } 20 | 21 | if (!$command->getTargetUserToken()) { 22 | return ['targetUserToken' => 'TargetUserToken обязательный параметр']; 23 | } 24 | 25 | if (!$command->getTargetUsername()) { 26 | return ['targetUsername' => 'TargetUsername обязательный параметр']; 27 | } 28 | 29 | if (!empty($command->getActions())) { 30 | $diff = array_diff($command->getActions(), ActionsEnum::getCases()); 31 | if (!empty($diff)) { 32 | return [$diff[0] => sprintf('Неизвестное действие "%s".', $diff[0])]; 33 | } 34 | } 35 | 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/SubscribersBalancingAction/MessageHandler.php: -------------------------------------------------------------------------------- 1 | logger->debug(sprintf('%s method started', __METHOD__), [ 24 | 'arguments' => func_get_args(), 25 | 'class' => __CLASS__, 26 | 'method' => __METHOD__, 27 | 'line' => __LINE__, 28 | ]); 29 | 30 | $this->subscribersManager->subscribersBalancing($message->getTargetUserToken(), $message->getTargetUsername()); 31 | 32 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 33 | 'class' => __CLASS__, 34 | 'method' => __METHOD__, 35 | 'line' => __LINE__, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($command); 25 | if ($errors) { 26 | throw new CommandQueryValidatorException($errors); 27 | } 28 | 29 | $this->subscribersManager->handleCheckUnsubscribedHandledHook( 30 | $command->getResponse()->getItems(), 31 | $command->getTargetUserToken(), 32 | $command->getTargetUsername(), 33 | $command->getActions(), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | 5 | # IMPORTANT: You MUST configure your server version, 6 | # either here or in the DATABASE_URL env var (see .env file) 7 | #server_version: '16' 8 | 9 | profiling_collect_backtrace: '%kernel.debug%' 10 | use_savepoints: true 11 | orm: 12 | auto_generate_proxy_classes: true 13 | enable_lazy_ghost_objects: true 14 | report_fields_where_declared: true 15 | validate_xml_mapping: true 16 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 17 | auto_mapping: true 18 | mappings: 19 | App: 20 | type: attribute 21 | is_bundle: false 22 | dir: '%kernel.project_dir%/src/InfrastructureSystem' 23 | prefix: 'App\InfrastructureSystem' 24 | alias: App 25 | 26 | when@test: 27 | doctrine: 28 | dbal: 29 | # "TEST_TOKEN" is typically set by ParaTest 30 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 31 | 32 | when@prod: 33 | doctrine: 34 | orm: 35 | auto_generate_proxy_classes: false 36 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 37 | query_cache_driver: 38 | type: pool 39 | pool: doctrine.system_cache_pool 40 | result_cache_driver: 41 | type: pool 42 | pool: doctrine.result_cache_pool 43 | 44 | framework: 45 | cache: 46 | pools: 47 | doctrine.result_cache_pool: 48 | adapter: cache.app 49 | doctrine.system_cache_pool: 50 | adapter: cache.system 51 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/DoctrineFeature/DoctrineEntityManager.php: -------------------------------------------------------------------------------- 1 | entityManager->persist($entity); 19 | if ($flush) { 20 | $this->entityManager->flush(); 21 | } 22 | } 23 | 24 | public function removeBy(string $table, array $criteria): void 25 | { 26 | $queryString = sprintf('DELETE FROM %s entity', $table); 27 | 28 | $wherePart = []; 29 | foreach ($criteria as $attribute => $value) { 30 | $wherePart[] = sprintf('entity.%s = :%s', $attribute, $attribute); 31 | } 32 | 33 | if (!empty($wherePart)) { 34 | $queryString .= ' WHERE ' . implode(' AND ', $wherePart); 35 | } 36 | 37 | $query = $this->entityManager->createQuery($queryString); 38 | $response = $query->execute($criteria); 39 | 40 | $this->logger->debug(sprintf('Удалено %s записей', $response), [ 41 | 'arguments' => func_get_args(), 42 | 'class' => __CLASS__, 43 | 'method' => __METHOD__, 44 | 'line' => __LINE__, 45 | ]); 46 | } 47 | 48 | public function remove(object $entity, bool $flush = false): void 49 | { 50 | $this->entityManager->remove($entity); 51 | if ($flush) { 52 | $this->entityManager->flush(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: ./app/.docker/php 7 | dockerfile: ./Dockerfile 8 | container_name: ${CONTAINER_PREFIX}_app 9 | restart: unless-stopped 10 | tty: true 11 | networks: 12 | - github-booster-default 13 | volumes: 14 | - ./app:/app:rw 15 | depends_on: 16 | - postgresql 17 | - beanstalkd 18 | 19 | app_supervisord: 20 | build: 21 | context: ./app/.supervisord 22 | dockerfile: ./Dockerfile 23 | container_name: ${CONTAINER_PREFIX}_app_supervisord 24 | restart: unless-stopped 25 | tty: true 26 | networks: 27 | - github-booster-default 28 | volumes: 29 | - ./app/.supervisord/conf.d:/etc/supervisor/conf.d:rw 30 | - ./app:/app:rw 31 | environment: 32 | - SUPERVISOR_USERNAME=${SUPERVISOR_USERNAME} 33 | - SUPERVISOR_PASSWORD=${SUPERVISOR_PASSWORD} 34 | - SUPERVISOR_PORT=${SUPERVISOR_PORT} 35 | ports: 36 | - ${SUPERVISOR_PORT}:${SUPERVISOR_PORT} 37 | depends_on: 38 | - postgresql 39 | - beanstalkd 40 | 41 | postgresql: 42 | build: 43 | context: ./postgresql 44 | dockerfile: ./Dockerfile 45 | container_name: ${CONTAINER_PREFIX}_postgresql 46 | restart: unless-stopped 47 | tty: true 48 | networks: 49 | - github-booster-default 50 | environment: 51 | - POSTGRES_DB=${POSTGRES_DB} 52 | - POSTGRES_USER=${POSTGRES_USER} 53 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 54 | - PGDATA=/var/lib/postgresql/data 55 | ports: 56 | - ${POSTGRES_EXT_PORT}:${POSTGRES_PORT} 57 | 58 | beanstalkd: 59 | build: 60 | context: ./beanstalkd 61 | dockerfile: ./Dockerfile 62 | container_name: ${CONTAINER_PREFIX}_beanstalkd 63 | restart: unless-stopped 64 | tty: true 65 | networks: 66 | - github-booster-default 67 | 68 | networks: 69 | github-booster-default: 70 | driver: bridge 71 | -------------------------------------------------------------------------------- /app/config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration 6 | parameters: 7 | 8 | services: 9 | # default configuration for services in *this* file 10 | _defaults: 11 | autowire: true # Automatically injects dependencies in your services. 12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 13 | bind: 14 | $githubPersonalAccessToken: "%env(GITHUB_PERSONAL_TOKEN)%" 15 | $githubTargetUsername: "%env(GITHUB_TARGET_USERNAME)%" 16 | $subscribersBalancingSchedule: "%env(SUBSCRIBERS_BALANCING_SCHEDULE)%" 17 | 18 | # makes classes in src/ available to be used as services 19 | # this creates a service per class whose id is the fully-qualified class name 20 | App\: 21 | resource: '../src/' 22 | exclude: 23 | - '../src/DependencyInjection/' 24 | - '../src/Entity/' 25 | - '../src/Kernel.php' 26 | 27 | # LISTENERS 28 | App\MessageBusSystem\SubscribersFeature\CheckUnsubscribedHandledHookAction\Listener: 29 | tags: 30 | - { name: kernel.event_listener, event: App\CommandQueryBusSystem\SubscribersFeature\CheckUnsubscribedAction\HandledEvent } 31 | 32 | App\MessageBusSystem\SubscribersFeature\UnsubscribeAction\UnsubscribeAfterCheckDefinedEventListener: 33 | tags: 34 | - { name: kernel.event_listener, event: App\ApplicationSystem\SubscribersFeature\Actions\CheckUnsubscribedHandledHookAction\UnsubscribeAfterCheckDefinedEvent } 35 | 36 | # MESSAGE HANDLERS 37 | App\MessageBusSystem\SubscribersFeature\SubscribersBalancingAction\MessageHandler: 38 | tags: 39 | - { name: messenger.message_handler } 40 | -------------------------------------------------------------------------------- /app/src/MessageBusSystem/SubscribersFeature/CheckUnsubscribedHandledHookAction/Listener.php: -------------------------------------------------------------------------------- 1 | getResponse()->getItems())) { 27 | return; 28 | } 29 | 30 | $this->messageBus->dispatch( 31 | $this->messageFactory->create( 32 | $this->commandFactory->create( 33 | $event->getResponse(), 34 | $event->getTargetUserToken(), 35 | $event->getTargetUsername(), 36 | $event->getActions(), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/PresentationSystem/CommandLine/SubscribersFeature/GithubSyncSubscribersCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 33 | self::GITHUB_PERSONAL_TOKEN_OPTION, 34 | null, 35 | InputOption::VALUE_REQUIRED, 36 | 'Персональный токен Github.', 37 | ) 38 | ->addOption( 39 | self::GITHUB_TARGET_USERNAME_OPTION, 40 | null, 41 | InputOption::VALUE_REQUIRED, 42 | 'Пользователь, для которого надо проверить подписчиков и подписки.', 43 | ) 44 | ; 45 | } 46 | 47 | protected function execute(InputInterface $input, OutputInterface $output): int 48 | { 49 | $targetUserToken = $input->getOption(self::GITHUB_PERSONAL_TOKEN_OPTION); 50 | $targetUsername = $input->getOption(self::GITHUB_TARGET_USERNAME_OPTION); 51 | 52 | $this->subscribersManager->syncSubscribers($targetUserToken, $targetUsername); 53 | 54 | return self::SUCCESS; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/PresentationSystem/CommandLine/SubscriptionFeature/GithubSyncSubscriptionsCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 33 | self::GITHUB_PERSONAL_TOKEN_OPTION, 34 | null, 35 | InputOption::VALUE_REQUIRED, 36 | 'Персональный токен Github.', 37 | ) 38 | ->addOption( 39 | self::GITHUB_TARGET_USERNAME_OPTION, 40 | null, 41 | InputOption::VALUE_REQUIRED, 42 | 'Пользователь, для которого надо проверить подписчиков и подписки.', 43 | ) 44 | ; 45 | } 46 | 47 | protected function execute(InputInterface $input, OutputInterface $output): int 48 | { 49 | $targetUserToken = $input->getOption(self::GITHUB_PERSONAL_TOKEN_OPTION); 50 | $targetUsername = $input->getOption(self::GITHUB_TARGET_USERNAME_OPTION); 51 | 52 | $this->subscriptionManager->syncSubscriptions($targetUserToken, $targetUsername); 53 | 54 | return self::SUCCESS; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/PresentationSystem/CommandLine/SubscribersFeature/GithubSubscribersBalancingCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 34 | self::GITHUB_PERSONAL_TOKEN_OPTION, 35 | null, 36 | InputOption::VALUE_REQUIRED, 37 | 'Персональный токен Github.', 38 | ) 39 | ->addOption( 40 | self::GITHUB_TARGET_USERNAME_OPTION, 41 | null, 42 | InputOption::VALUE_REQUIRED, 43 | 'Пользователь, для которого надо проверить подписчиков и подписки.', 44 | ) 45 | ; 46 | } 47 | 48 | protected function execute(InputInterface $input, OutputInterface $output): int 49 | { 50 | $targetUserToken = $input->getOption(self::GITHUB_PERSONAL_TOKEN_OPTION); 51 | $targetUsername = $input->getOption(self::GITHUB_TARGET_USERNAME_OPTION); 52 | 53 | $this->subscribersManager->subscribersBalancing($targetUserToken, $targetUsername); 54 | 55 | return self::SUCCESS; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SynchronizationFeature/SynchronizationSubscribersManager.php: -------------------------------------------------------------------------------- 1 | logger->debug(sprintf('%s method started', __METHOD__), [ 21 | 'arguments' => func_get_args(), 22 | 'class' => __CLASS__, 23 | 'method' => __METHOD__, 24 | 'line' => __LINE__, 25 | ]); 26 | 27 | $this->logger->debug('Удаление списка подписчиков целевого пользователя из БД.', [ 28 | 'arguments' => func_get_args(), 29 | 'class' => __CLASS__, 30 | 'method' => __METHOD__, 31 | 'line' => __LINE__, 32 | ]); 33 | 34 | $this->removeAllSubscribersHandler->handle($targetUsername); 35 | 36 | $this->logger->debug('Получение нового списка подписчиков целевого пользователя и сохранение его в БД.', [ 37 | 'arguments' => func_get_args(), 38 | 'class' => __CLASS__, 39 | 'method' => __METHOD__, 40 | 'line' => __LINE__, 41 | ]); 42 | 43 | $this->syncSubscribersActionHandler->handle($targetUserToken, $targetUsername); 44 | 45 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 46 | 'class' => __CLASS__, 47 | 'method' => __METHOD__, 48 | 'line' => __LINE__, 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/PresentationSystem/CommandLine/SubscriptionFeature/GithubSubscriptionsBalancingCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 34 | self::GITHUB_PERSONAL_TOKEN_OPTION, 35 | null, 36 | InputOption::VALUE_REQUIRED, 37 | 'Персональный токен Github.', 38 | ) 39 | ->addOption( 40 | self::GITHUB_TARGET_USERNAME_OPTION, 41 | null, 42 | InputOption::VALUE_REQUIRED, 43 | 'Пользователь, для которого надо проверить подписчиков и подписки.', 44 | ) 45 | ; 46 | } 47 | 48 | protected function execute(InputInterface $input, OutputInterface $output): int 49 | { 50 | $targetUserToken = $input->getOption(self::GITHUB_PERSONAL_TOKEN_OPTION); 51 | $targetUsername = $input->getOption(self::GITHUB_TARGET_USERNAME_OPTION); 52 | 53 | $this->subscriptionManager->subscriptionsBalancing($targetUserToken, $targetUsername); 54 | 55 | return self::SUCCESS; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: 3 | - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists 4 | 5 | when@dev: 6 | monolog: 7 | handlers: 8 | main: 9 | type: stream 10 | path: "php://stdout" 11 | level: debug 12 | channels: ["!event", "!doctrine"] 13 | # uncomment to get logging in your browser 14 | # you may have to allow bigger header sizes in your Web server configuration 15 | #firephp: 16 | # type: firephp 17 | # level: info 18 | #chromephp: 19 | # type: chromephp 20 | # level: info 21 | console: 22 | type: console 23 | process_psr_3_messages: false 24 | channels: ["!event", "!doctrine", "!console"] 25 | 26 | when@test: 27 | monolog: 28 | handlers: 29 | main: 30 | type: fingers_crossed 31 | action_level: error 32 | handler: nested 33 | excluded_http_codes: [404, 405] 34 | channels: ["!event"] 35 | nested: 36 | type: stream 37 | path: "%kernel.logs_dir%/%kernel.environment%.log" 38 | level: debug 39 | 40 | when@prod: 41 | monolog: 42 | handlers: 43 | main: 44 | type: fingers_crossed 45 | action_level: error 46 | handler: nested 47 | excluded_http_codes: [404, 405] 48 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 49 | nested: 50 | type: stream 51 | path: php://stderr 52 | level: debug 53 | formatter: monolog.formatter.json 54 | console: 55 | type: console 56 | process_psr_3_messages: false 57 | channels: ["!event", "!doctrine"] 58 | deprecation: 59 | type: stream 60 | channels: [deprecation] 61 | path: php://stderr 62 | formatter: monolog.formatter.json 63 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SynchronizationFeature/SynchronizationSubscriptionsManager.php: -------------------------------------------------------------------------------- 1 | logger->debug(sprintf('%s method started', __METHOD__), [ 21 | 'arguments' => func_get_args(), 22 | 'class' => __CLASS__, 23 | 'method' => __METHOD__, 24 | 'line' => __LINE__, 25 | ]); 26 | 27 | $this->logger->debug('Удаление списка подписок целевого пользователя из БД.', [ 28 | 'arguments' => func_get_args(), 29 | 'class' => __CLASS__, 30 | 'method' => __METHOD__, 31 | 'line' => __LINE__, 32 | ]); 33 | 34 | $this->removeAllSubscriptionsHandler->handle($targetUsername); 35 | 36 | $this->logger->debug('Получение нового списка подписок целевого пользователя и сохранение его в БД.', [ 37 | 'arguments' => func_get_args(), 38 | 'class' => __CLASS__, 39 | 'method' => __METHOD__, 40 | 'line' => __LINE__, 41 | ]); 42 | 43 | $this->syncSubscriptionsActionHandler->handle($targetUserToken, $targetUsername); 44 | 45 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 46 | 'class' => __CLASS__, 47 | 'method' => __METHOD__, 48 | 'line' => __LINE__, 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/CheckUnsubscribedAction/Handler.php: -------------------------------------------------------------------------------- 1 | validator->validate($query); 30 | if ($errors) { 31 | throw new CommandQueryValidatorException($errors); 32 | } 33 | 34 | $unfollowingUsers = $this->subscribersManager->checkUnsubscribed( 35 | $query->getTargetUserToken(), 36 | $query->getTargetUsername(), 37 | $query->getPage(), 38 | $query->getLimit(), 39 | ); 40 | 41 | $response = $this->responseFactory->create($unfollowingUsers); 42 | 43 | $this->eventDispatcher->dispatch(new HandledEvent( 44 | $response, 45 | $query->getTargetUserToken(), 46 | $query->getTargetUsername(), 47 | $query->getActions(), 48 | )); 49 | 50 | return $response; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "minimum-stability": "stable", 5 | "prefer-stable": true, 6 | "require": { 7 | "php": ">=8.1", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "doctrine/dbal": "^3", 11 | "doctrine/doctrine-bundle": "^2.11", 12 | "doctrine/doctrine-migrations-bundle": "^3.3", 13 | "doctrine/orm": "^3.1", 14 | "dragonmantank/cron-expression": "^3.3", 15 | "guzzlehttp/guzzle": "^7.0", 16 | "symfony/beanstalkd-messenger": "6.4.*", 17 | "symfony/console": "6.4.*", 18 | "symfony/dotenv": "6.4.*", 19 | "symfony/event-dispatcher": "6.4.*", 20 | "symfony/flex": "^2", 21 | "symfony/framework-bundle": "6.4.*", 22 | "symfony/maker-bundle": "^1.56", 23 | "symfony/messenger": "6.4.*", 24 | "symfony/monolog-bundle": "^3.10", 25 | "symfony/property-access": "6.4.*", 26 | "symfony/runtime": "6.4.*", 27 | "symfony/scheduler": "6.4.*", 28 | "symfony/serializer": "6.4.*", 29 | "symfony/validator": "6.4.*", 30 | "symfony/yaml": "6.4.*" 31 | }, 32 | "config": { 33 | "allow-plugins": { 34 | "php-http/discovery": true, 35 | "symfony/flex": true, 36 | "symfony/runtime": true 37 | }, 38 | "sort-packages": true 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "App\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "App\\Tests\\": "tests/" 48 | } 49 | }, 50 | "replace": { 51 | "symfony/polyfill-ctype": "*", 52 | "symfony/polyfill-iconv": "*", 53 | "symfony/polyfill-php72": "*", 54 | "symfony/polyfill-php73": "*", 55 | "symfony/polyfill-php74": "*", 56 | "symfony/polyfill-php80": "*", 57 | "symfony/polyfill-php81": "*" 58 | }, 59 | "scripts": { 60 | "auto-scripts": { 61 | "cache:clear": "symfony-cmd", 62 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 63 | }, 64 | "post-install-cmd": [ 65 | "@auto-scripts" 66 | ], 67 | "post-update-cmd": [ 68 | "@auto-scripts" 69 | ] 70 | }, 71 | "conflict": { 72 | "symfony/symfony": "*" 73 | }, 74 | "extra": { 75 | "symfony": { 76 | "allow-contrib": false, 77 | "require": "6.4.*" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscriptionFeature/Actions/SubscriptionsBalancingAction/Handler.php: -------------------------------------------------------------------------------- 1 | subscriptionRepository->findUnsubscribedByTargetUsername($targetUsername); 22 | 23 | foreach ($subscriptions as $subscription) { 24 | try { 25 | $this->logger->debug('Вызов метода отписки от пользователя.', [ 26 | 'subscription' => [ 27 | 'targetUsername' => $subscription->getTargetUsername(), 28 | 'login' => $subscription->getLogin(), 29 | ], 30 | 'class' => __CLASS__, 31 | 'method' => __METHOD__, 32 | 'line' => __LINE__, 33 | ]); 34 | 35 | $this->internalSubscribersManager->unsubscribe($targetUserToken, $subscription->getLogin()); 36 | } catch (\Throwable $throwable) { 37 | $this->logger->debug('Не удалось вызвать метод отписки.', [ 38 | 'exception' => [ 39 | 'message' => $throwable->getMessage(), 40 | 'class' => get_class($throwable), 41 | ], 42 | 'subscription' => [ 43 | 'targetUsername' => $subscription->getTargetUsername(), 44 | 'login' => $subscription->getLogin(), 45 | ], 46 | 'class' => __CLASS__, 47 | 'method' => __METHOD__, 48 | 'line' => __LINE__, 49 | ]); 50 | throw $throwable; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Actions/SubscribersBalancingAction/Handler.php: -------------------------------------------------------------------------------- 1 | subscriberRepository->findSubscribedByTargetUsername($targetUsername); 22 | 23 | foreach ($subscribers as $subscriber) { 24 | try { 25 | $this->logger->debug('Вызов метода подписки на пользователя.', [ 26 | 'subscription' => [ 27 | 'targetUsername' => $subscriber->getTargetUsername(), 28 | 'login' => $subscriber->getLogin(), 29 | ], 30 | 'class' => __CLASS__, 31 | 'method' => __METHOD__, 32 | 'line' => __LINE__, 33 | ]); 34 | 35 | $this->internalSubscribersManager->subscribe($targetUserToken, $subscriber->getLogin()); 36 | } catch (\Throwable $throwable) { 37 | $this->logger->debug('Не удалось вызвать метод подписки.', [ 38 | 'exception' => [ 39 | 'message' => $throwable->getMessage(), 40 | 'class' => get_class($throwable), 41 | ], 42 | 'subscription' => [ 43 | 'targetUsername' => $subscriber->getTargetUsername(), 44 | 'login' => $subscriber->getLogin(), 45 | ], 46 | 'class' => __CLASS__, 47 | 'method' => __METHOD__, 48 | 'line' => __LINE__, 49 | ]); 50 | 51 | if ($throwable->getCode() === 404) { 52 | continue; 53 | } 54 | 55 | throw $throwable; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Actions/CheckUnsubscribedAction/Handler.php: -------------------------------------------------------------------------------- 1 | internalSubscribersManager->getSubscriptions($targetUserToken, $page, $limit); 25 | 26 | $unfollowingUsers = []; 27 | 28 | $this->logger->debug('Получили список подписчиков.', [ 29 | 'page' => $page, 30 | 'limit' => $limit, 31 | 'count' => count($subscriptions), 32 | 'class' => __CLASS__, 33 | 'method' => __METHOD__, 34 | 'line' => __LINE__, 35 | ]); 36 | 37 | foreach ($subscriptions as $subscription) { 38 | $isSubscribed = $this->internalSubscribersManager->subscriptionCheck( 39 | $targetUserToken, 40 | $targetUsername, 41 | $subscription->getLogin(), 42 | ); 43 | 44 | if (!$isSubscribed) { 45 | $user = $this->userFactory->create($subscription->getLogin(), false); 46 | $unfollowingUsers[] = $user; 47 | 48 | $this->logger->debug('Найден не подписанный пользователь.', [ 49 | 'username' => $user->getLogin(), 50 | 'class' => __CLASS__, 51 | 'method' => __METHOD__, 52 | 'line' => __LINE__, 53 | ]); 54 | } 55 | 56 | $this->logger->debug(sprintf('Проверили подписан ли пользователь на "%s".', $targetUsername), [ 57 | 'username' => $subscription->getLogin(), 58 | 'class' => __CLASS__, 59 | 'method' => __METHOD__, 60 | 'line' => __LINE__, 61 | ]); 62 | } 63 | 64 | return $unfollowingUsers; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscribersFeature/SubscribersManager.php: -------------------------------------------------------------------------------- 1 | logger->debug(sprintf('%s method started', __METHOD__), [ 25 | 'class' => __CLASS__, 26 | 'method' => __METHOD__, 27 | 'line' => __LINE__, 28 | ]); 29 | 30 | $this->syncSubscribersActionHandler->handle($this->syncSubscribersActionCommandFactory->create( 31 | $targetUserToken, 32 | $targetUsername, 33 | )); 34 | 35 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 36 | 'class' => __CLASS__, 37 | 'method' => __METHOD__, 38 | 'line' => __LINE__, 39 | ]); 40 | } 41 | 42 | public function subscribersBalancing(string $targetUserToken, string $targetUsername): void 43 | { 44 | $this->logger->debug(sprintf('%s method started', __METHOD__), [ 45 | 'class' => __CLASS__, 46 | 'method' => __METHOD__, 47 | 'line' => __LINE__, 48 | ]); 49 | 50 | $this->subscribersBalancingActionHandler->handle($this->subscribersBalancingActionCommandFactory->create( 51 | $targetUserToken, 52 | $targetUsername, 53 | )); 54 | 55 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 56 | 'class' => __CLASS__, 57 | 'method' => __METHOD__, 58 | 'line' => __LINE__, 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/CommandQueryBusSystem/SubscriptionFeature/SubscriptionManager.php: -------------------------------------------------------------------------------- 1 | logger->debug(sprintf('%s method started', __METHOD__), [ 25 | 'arguments' => func_get_args(), 26 | 'class' => __CLASS__, 27 | 'method' => __METHOD__, 28 | 'line' => __LINE__, 29 | ]); 30 | 31 | $this->syncSubscriptionsActionHandler->handle($this->syncSubscriptionsActionCommandFactory->create( 32 | $targetUserToken, 33 | $targetUsername, 34 | )); 35 | 36 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 37 | 'class' => __CLASS__, 38 | 'method' => __METHOD__, 39 | 'line' => __LINE__, 40 | ]); 41 | } 42 | 43 | public function subscriptionsBalancing(string $targetUserToken, string $targetUsername): void 44 | { 45 | $this->logger->debug(sprintf('%s method started', __METHOD__), [ 46 | 'arguments' => func_get_args(), 47 | 'class' => __CLASS__, 48 | 'method' => __METHOD__, 49 | 'line' => __LINE__, 50 | ]); 51 | 52 | $this->subscriptionsBalancingActionHandler->handle($this->subscriptionsBalancingActionCommandFactory->create( 53 | $targetUserToken, 54 | $targetUsername, 55 | )); 56 | 57 | $this->logger->debug(sprintf('%s method ended', __METHOD__), [ 58 | 'class' => __CLASS__, 59 | 'method' => __METHOD__, 60 | 'line' => __LINE__, 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Actions/SyncSubscribersAction/Handler.php: -------------------------------------------------------------------------------- 1 | internalSubscribersManager->getSubscribers($targetUserToken, $page, $limit); 34 | } catch (OutOfRangeException $exception) { 35 | $this->logger->debug($exception->getMessage(), [ 36 | 'username' => $targetUsername, 37 | 'class' => __CLASS__, 38 | 'method' => __METHOD__, 39 | 'line' => __LINE__, 40 | ]); 41 | 42 | return; 43 | } 44 | 45 | $count = count($subscribers); 46 | $flush = false; 47 | $index = 0; 48 | 49 | foreach ($subscribers as $subscriber) { 50 | if ($index >= ($count - 1)) { 51 | $flush = true; 52 | } 53 | 54 | $entity = $this->subscriberFactory->create( 55 | targetUsername: $targetUsername, 56 | login: $subscriber->getLogin(), 57 | internalId: $subscriber->getId(), 58 | url: $subscriber->getUrl(), 59 | repositoriesUrl: $subscriber->getReposUrl(), 60 | subscriptionsUrl: $subscriber->getSubscriptionsUrl(), 61 | starredUrl: $subscriber->getStarredUrl(), 62 | followersUrl: $subscriber->getFollowersUrl(), 63 | followingUrl: $subscriber->getFollowingUrl(), 64 | ); 65 | $this->subscribersManager->saveSubscriber($entity, $flush); 66 | 67 | $index++; 68 | } 69 | 70 | $page++; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/ApplicationSystem/SubscribersFeature/Interfaces/Manager/SubscribersManagerInterface.php: -------------------------------------------------------------------------------- 1 | internalSubscribersManager->getSubscriptions($targetUserToken, $page, $limit); 34 | } catch (OutOfRangeException $exception) { 35 | $this->logger->debug($exception->getMessage(), [ 36 | 'username' => $targetUsername, 37 | 'class' => __CLASS__, 38 | 'method' => __METHOD__, 39 | 'line' => __LINE__, 40 | ]); 41 | 42 | return; 43 | } 44 | 45 | $count = count($subscriptions); 46 | $flush = false; 47 | $index = 0; 48 | 49 | foreach ($subscriptions as $subscription) { 50 | if ($index >= ($count - 1)) { 51 | $flush = true; 52 | } 53 | 54 | $entity = $this->subscriptionFactory->create( 55 | targetUsername: $targetUsername, 56 | login: $subscription->getLogin(), 57 | internalId: $subscription->getId(), 58 | url: $subscription->getUrl(), 59 | repositoriesUrl: $subscription->getReposUrl(), 60 | subscriptionsUrl: $subscription->getSubscriptionsUrl(), 61 | starredUrl: $subscription->getStarredUrl(), 62 | followersUrl: $subscription->getFollowersUrl(), 63 | followingUrl: $subscription->getFollowingUrl(), 64 | ); 65 | $this->subscriptionManager->saveSubscription($entity, $flush); 66 | 67 | $index++; 68 | } 69 | 70 | $page++; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github booster 2 | 3 | ## Описание 4 | 5 | Идея приложения в том, чтобы иметь под рукой функционал для автоматизированных действий на Github. Также с его помощью планируется сбор метрик и сегментация пользователей, с которыми можно взаимодействовать для достижения конечной цели. 6 | 7 | Для системы выделяются следующие задачи: 8 | 9 | 1. Автоматизировать балансировку подписчиков/подписок. Необходимо отписываться от людей, на которых ранее был подписан пользователь, но которые не подписались в ответ. 10 | 2. Собрать метрики, по которым можно сегментировать пользователей. Например, тех кто охотно подписывается в ответ или ставит звезды. 11 | 3. Автоматизировать длительный процесс выполнения стратегии "Зуб за Зуб". Принцип стратегии в контексте Github состоит из набора подписчиков за счет взаимной подписки, а также получении звезд за счет взаимного обмена. 12 | 13 | ## Зависимости проекта 14 | 15 | Для работы с проектом требуется наличие на используемой машине следующих инструментов: 16 | 17 | 1. Docker 18 | 2. Docker Compose 19 | 3. Bash / Shell 20 | 4. Make* 21 | 22 | * Необязательные инструменты. 23 | 24 | ## Установка 25 | 26 | При установленном сборщике проектов Make: 27 | ```bash 28 | make init 29 | ``` 30 | 31 | Запуск через Docker Compose: 32 | ```bash 33 | docker compose build \ 34 | && docker compose up -d --remove-orphans \ 35 | && docker compose exec app composer i 36 | ``` 37 | 38 | ## Использование 39 | 40 | ### Создание Personal Access Token 41 | 42 | Перед использованием команд создайте Personal Access Token с [помощью инструкции](https://docs.github.com/ru/enterprise-cloud@latest/authentication/authenticating-with-saml-single-sign-on/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on). 43 | 44 | `TODO: Описать минимальный набор доступов для токена.` 45 | 46 | ### github:subscribers:check-unfollowing 47 | 48 | Проверка пользователей, которые не подписались в ответ 49 | 50 | ```bash 51 | docker compose exec app php bin/console github:subscribers:check-unfollowing \ 52 | --token='your github personal access token' \ 53 | --username='your username' 54 | ``` 55 | 56 | ### github:subscribers:sync 57 | 58 | Синхронизирует список подписчиков целевого пользователя. 59 | 60 | (!) ВАЖНО: метод перед работой очищает таблицу базы данных, чтобы данные всегда были актуальны. 61 | 62 | ```bash 63 | docker compose exec app php bin/console github:subscribers:sync \ 64 | --token='' \ 65 | --username='' 66 | ``` 67 | 68 | ### github:subscriptions:sync 69 | 70 | Синхронизирует список подписок целевого пользователя. 71 | 72 | (!) ВАЖНО: метод перед работой очищает таблицу базы данных, чтобы данные всегда были актуальны. 73 | 74 | ```bash 75 | docker compose exec app php bin/console github:subscriptions:sync \ 76 | --token='' \ 77 | --username='' 78 | ``` 79 | 80 | ### github:subscriptions:balancing 81 | 82 | (!) ВАЖНО: метод перед работой очищает таблицу базы данных, чтобы данные всегда были актуальны. 83 | 84 | ```bash 85 | docker compose exec app php bin/console github:subscriptions:balancing \ 86 | --token='' \ 87 | --username='' 88 | ``` 89 | 90 | ### github:subscribers:balancing 91 | 92 | (!) ВАЖНО: метод перед работой очищает таблицу базы данных, чтобы данные всегда были актуальны. 93 | 94 | ```bash 95 | docker compose exec app php bin/console github:subscribers:balancing \ 96 | --token='' \ 97 | --username='' 98 | ``` 99 | -------------------------------------------------------------------------------- /app/symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "doctrine/doctrine-bundle": { 3 | "version": "2.11", 4 | "recipe": { 5 | "repo": "github.com/symfony/recipes", 6 | "branch": "main", 7 | "version": "2.10", 8 | "ref": "c170ded8fc587d6bd670550c43dafcf093762245" 9 | }, 10 | "files": [ 11 | "config/packages/doctrine.yaml", 12 | "src/Entity/.gitignore", 13 | "src/Repository/.gitignore" 14 | ] 15 | }, 16 | "doctrine/doctrine-migrations-bundle": { 17 | "version": "3.3", 18 | "recipe": { 19 | "repo": "github.com/symfony/recipes", 20 | "branch": "main", 21 | "version": "3.1", 22 | "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" 23 | }, 24 | "files": [ 25 | "config/packages/doctrine_migrations.yaml", 26 | "migrations/.gitignore" 27 | ] 28 | }, 29 | "symfony/console": { 30 | "version": "6.4", 31 | "recipe": { 32 | "repo": "github.com/symfony/recipes", 33 | "branch": "main", 34 | "version": "5.3", 35 | "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" 36 | }, 37 | "files": [ 38 | "bin/console" 39 | ] 40 | }, 41 | "symfony/flex": { 42 | "version": "2.4", 43 | "recipe": { 44 | "repo": "github.com/symfony/recipes", 45 | "branch": "main", 46 | "version": "1.0", 47 | "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" 48 | }, 49 | "files": [ 50 | ".env" 51 | ] 52 | }, 53 | "symfony/framework-bundle": { 54 | "version": "6.4", 55 | "recipe": { 56 | "repo": "github.com/symfony/recipes", 57 | "branch": "main", 58 | "version": "6.4", 59 | "ref": "a91c965766ad3ff2ae15981801643330eb42b6a5" 60 | }, 61 | "files": [ 62 | "config/packages/cache.yaml", 63 | "config/packages/framework.yaml", 64 | "config/preload.php", 65 | "config/routes/framework.yaml", 66 | "config/services.yaml", 67 | "public/index.php", 68 | "src/Controller/.gitignore", 69 | "src/Kernel.php" 70 | ] 71 | }, 72 | "symfony/maker-bundle": { 73 | "version": "1.56", 74 | "recipe": { 75 | "repo": "github.com/symfony/recipes", 76 | "branch": "main", 77 | "version": "1.0", 78 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 79 | } 80 | }, 81 | "symfony/messenger": { 82 | "version": "6.4", 83 | "recipe": { 84 | "repo": "github.com/symfony/recipes", 85 | "branch": "main", 86 | "version": "6.0", 87 | "ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee" 88 | }, 89 | "files": [ 90 | "config/packages/messenger.yaml" 91 | ] 92 | }, 93 | "symfony/monolog-bundle": { 94 | "version": "3.10", 95 | "recipe": { 96 | "repo": "github.com/symfony/recipes", 97 | "branch": "main", 98 | "version": "3.7", 99 | "ref": "aff23899c4440dd995907613c1dd709b6f59503f" 100 | }, 101 | "files": [ 102 | "config/packages/monolog.yaml" 103 | ] 104 | }, 105 | "symfony/routing": { 106 | "version": "6.4", 107 | "recipe": { 108 | "repo": "github.com/symfony/recipes", 109 | "branch": "main", 110 | "version": "6.2", 111 | "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6" 112 | }, 113 | "files": [ 114 | "config/packages/routing.yaml", 115 | "config/routes.yaml" 116 | ] 117 | }, 118 | "symfony/validator": { 119 | "version": "6.4", 120 | "recipe": { 121 | "repo": "github.com/symfony/recipes", 122 | "branch": "main", 123 | "version": "5.3", 124 | "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" 125 | }, 126 | "files": [ 127 | "config/packages/validator.yaml" 128 | ] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SubscribersFeature/Entity/Subscriber.php: -------------------------------------------------------------------------------- 1 | targetUsername = $targetUsername; 59 | $this->login = $login; 60 | $this->internalId = $internalId; 61 | $this->url = $url; 62 | $this->repositoriesUrl = $repositoriesUrl; 63 | $this->subscriptionsUrl = $subscriptionsUrl; 64 | $this->starredUrl = $starredUrl; 65 | $this->followersUrl = $followersUrl; 66 | $this->followingUrl = $followingUrl; 67 | } 68 | 69 | public function getId(): ?int 70 | { 71 | return $this->id; 72 | } 73 | 74 | public function getTargetUsername(): string 75 | { 76 | return $this->targetUsername; 77 | } 78 | 79 | public function setTargetUsername(string $targetUsername): void 80 | { 81 | $this->targetUsername = $targetUsername; 82 | } 83 | 84 | public function getLogin(): string 85 | { 86 | return $this->login; 87 | } 88 | 89 | public function setLogin(string $login): void 90 | { 91 | $this->login = $login; 92 | } 93 | 94 | public function getInternalId(): int|float 95 | { 96 | return $this->internalId; 97 | } 98 | 99 | public function setInternalId(int|float $internalId): void 100 | { 101 | $this->internalId = $internalId; 102 | } 103 | 104 | public function getUrl(): string 105 | { 106 | return $this->url; 107 | } 108 | 109 | public function setUrl(string $url): void 110 | { 111 | $this->url = $url; 112 | } 113 | 114 | public function getRepositoriesUrl(): string 115 | { 116 | return $this->repositoriesUrl; 117 | } 118 | 119 | public function setRepositoriesUrl(string $repositoriesUrl): void 120 | { 121 | $this->repositoriesUrl = $repositoriesUrl; 122 | } 123 | 124 | public function getSubscriptionsUrl(): string 125 | { 126 | return $this->subscriptionsUrl; 127 | } 128 | 129 | public function setSubscriptionsUrl(string $subscriptionsUrl): void 130 | { 131 | $this->subscriptionsUrl = $subscriptionsUrl; 132 | } 133 | 134 | public function getStarredUrl(): string 135 | { 136 | return $this->starredUrl; 137 | } 138 | 139 | public function setStarredUrl(string $starredUrl): void 140 | { 141 | $this->starredUrl = $starredUrl; 142 | } 143 | 144 | public function getFollowersUrl(): string 145 | { 146 | return $this->followersUrl; 147 | } 148 | 149 | public function setFollowersUrl(string $followersUrl): void 150 | { 151 | $this->followersUrl = $followersUrl; 152 | } 153 | 154 | public function getFollowingUrl(): string 155 | { 156 | return $this->followingUrl; 157 | } 158 | 159 | public function setFollowingUrl(string $followingUrl): void 160 | { 161 | $this->followingUrl = $followingUrl; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/InfrastructureSystem/SubscriptionFeature/Entity/Subscription.php: -------------------------------------------------------------------------------- 1 | targetUsername = $targetUsername; 59 | $this->login = $login; 60 | $this->internalId = $internalId; 61 | $this->url = $url; 62 | $this->repositoriesUrl = $repositoriesUrl; 63 | $this->subscriptionsUrl = $subscriptionsUrl; 64 | $this->starredUrl = $starredUrl; 65 | $this->followersUrl = $followersUrl; 66 | $this->followingUrl = $followingUrl; 67 | } 68 | 69 | public function getId(): ?int 70 | { 71 | return $this->id; 72 | } 73 | 74 | public function getTargetUsername(): string 75 | { 76 | return $this->targetUsername; 77 | } 78 | 79 | public function setTargetUsername(string $targetUsername): void 80 | { 81 | $this->targetUsername = $targetUsername; 82 | } 83 | 84 | public function getLogin(): string 85 | { 86 | return $this->login; 87 | } 88 | 89 | public function setLogin(string $login): void 90 | { 91 | $this->login = $login; 92 | } 93 | 94 | public function getInternalId(): int|float 95 | { 96 | return $this->internalId; 97 | } 98 | 99 | public function setInternalId(int|float $internalId): void 100 | { 101 | $this->internalId = $internalId; 102 | } 103 | 104 | public function getUrl(): string 105 | { 106 | return $this->url; 107 | } 108 | 109 | public function setUrl(string $url): void 110 | { 111 | $this->url = $url; 112 | } 113 | 114 | public function getRepositoriesUrl(): string 115 | { 116 | return $this->repositoriesUrl; 117 | } 118 | 119 | public function setRepositoriesUrl(string $repositoriesUrl): void 120 | { 121 | $this->repositoriesUrl = $repositoriesUrl; 122 | } 123 | 124 | public function getSubscriptionsUrl(): string 125 | { 126 | return $this->subscriptionsUrl; 127 | } 128 | 129 | public function setSubscriptionsUrl(string $subscriptionsUrl): void 130 | { 131 | $this->subscriptionsUrl = $subscriptionsUrl; 132 | } 133 | 134 | public function getStarredUrl(): string 135 | { 136 | return $this->starredUrl; 137 | } 138 | 139 | public function setStarredUrl(string $starredUrl): void 140 | { 141 | $this->starredUrl = $starredUrl; 142 | } 143 | 144 | public function getFollowersUrl(): string 145 | { 146 | return $this->followersUrl; 147 | } 148 | 149 | public function setFollowersUrl(string $followersUrl): void 150 | { 151 | $this->followersUrl = $followersUrl; 152 | } 153 | 154 | public function getFollowingUrl(): string 155 | { 156 | return $this->followingUrl; 157 | } 158 | 159 | public function setFollowingUrl(string $followingUrl): void 160 | { 161 | $this->followingUrl = $followingUrl; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/PresentationSystem/CommandLine/SubscribersFeature/GithubCheckUnfollowingCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 43 | self::GITHUB_PERSONAL_TOKEN_OPTION, 44 | null, 45 | InputOption::VALUE_REQUIRED, 46 | 'Персональный токен Github.', 47 | ) 48 | ->addOption( 49 | self::GITHUB_TARGET_USERNAME_OPTION, 50 | null, 51 | InputOption::VALUE_REQUIRED, 52 | 'Пользователь, для которого надо проверить подписчиков и подписки.', 53 | ) 54 | ->addOption( 55 | self::ACTION_STRATEGY_OPTION, 56 | null, 57 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 58 | 'Действия, которые нужно выполнить с теми, кто не подписан на целевого пользователя.' 59 | . ' Доступные действия: ' . ActionsEnum::getCasesAsString(), 60 | ) 61 | ->addOption( 62 | self::PAGE_OPTION, 63 | null, 64 | InputOption::VALUE_REQUIRED, 65 | 'Страница, с которой нужно начать поиск.', 66 | 1, 67 | ) 68 | ->addOption( 69 | self::LIMIT_OPTION, 70 | null, 71 | InputOption::VALUE_REQUIRED, 72 | 'Количество пользователей, проверяемых за одну итерацию', 73 | 30, 74 | ) 75 | ; 76 | } 77 | 78 | protected function execute(InputInterface $input, OutputInterface $output): int 79 | { 80 | throw new \RuntimeException('Deprecated port blocked.'); 81 | 82 | $targetUserToken = $input->getOption(self::GITHUB_PERSONAL_TOKEN_OPTION); 83 | $targetUsername = $input->getOption(self::GITHUB_TARGET_USERNAME_OPTION); 84 | $actions = $input->getOption(self::ACTION_STRATEGY_OPTION); 85 | $page = filter_var($input->getOption(self::PAGE_OPTION), FILTER_VALIDATE_INT) ?: 1; 86 | $limit = filter_var($input->getOption(self::LIMIT_OPTION), FILTER_VALIDATE_INT) ?: 30; 87 | 88 | $totalCount = 0; 89 | $isEmpty = false; 90 | 91 | while ($isEmpty === false) { 92 | try { 93 | $unfollowingUsers = $this->handler->handle($this->queryFactory->create($targetUserToken, $targetUsername, $actions, $page, $limit)) 94 | ->getItems(); 95 | 96 | if (!empty($unfollowingUsers)) { 97 | $page = 1; 98 | $totalCount += count($unfollowingUsers); 99 | continue; 100 | } 101 | 102 | $page += 1; 103 | } catch (OutOfRangeException $exception) { 104 | $isEmpty = true; 105 | } 106 | } 107 | 108 | $table = new Table($output); 109 | $table->setHeaders(['Количество не подписавшихся пользователей']); 110 | $table->addRow([$totalCount]); 111 | 112 | $table->render(); 113 | 114 | return self::SUCCESS; 115 | } 116 | } 117 | --------------------------------------------------------------------------------