diff --git a/README.md b/README.md index 9b76b8a..e90a946 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ To add a bot you need to link your app with the bot twitch account. 1. Open `${APP_URL}/connection/bot/redirect` with your laravel-app. 2. Login into your Twitch-Bot account with your Twitch Application. -3. After redirect you need to manually connect your laravel-Account with a bot. +3. After redirect, you need to manually connect your laravel-Account with a bot. 4. Open Your Database table "bot_connections" and connect your bot with your user. 5. Restart the Bot Artisan Bot diff --git a/composer.json b/composer.json index 7a0f50f..b822a89 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "homepage": "https://github.com/redbeed/openoverlay", "keywords": ["Laravel", "OpenOverlay", "twitch", "Eventsub", "Bot", "IRC"], "require": { + "php": "^8.0", "illuminate/support": "~8|~9", "guzzlehttp/guzzle": "^7.2", "ext-json": "*", @@ -23,9 +24,13 @@ "require-dev": { "phpunit/phpunit": "~9.0", "orchestra/testbench": "~5|~6", - "nunomaduro/phpinsights": "^1.14" + "nunomaduro/phpinsights": "^1.14", + "laravel/pint": "^0.2.3" }, "autoload": { + "files": [ + "src/Support/helpers.php" + ], "psr-4": { "Redbeed\\OpenOverlay\\": "src/", "Redbeed\\OpenOverlay\\Database\\": "database/" @@ -47,5 +52,10 @@ } }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/config/openoverlay.php b/config/openoverlay.php index 8efce82..18f9429 100644 --- a/config/openoverlay.php +++ b/config/openoverlay.php @@ -33,12 +33,12 @@ * You can use :username, :twitchUrl and :gameName for your message. */ \Redbeed\OpenOverlay\Listeners\AutoShoutOutRaid::class => [ - 'message' => 'Follow :username over at :twitchUrl. They were last playing :gameName' + 'message' => 'Follow :username over at :twitchUrl. They were last playing :gameName', ], \Redbeed\OpenOverlay\Support\ViewerInChat::class => [ - 'reset' => -1 - ] + 'reset' => -1, + ], ], 'webhook' => [ @@ -55,7 +55,7 @@ /** * Your personal and unique secret is used to validate a twitch callback - * If you change your secret all previous configures webhook callbacks will be end as invalid + * If you change your secret all previous configures webhook callbacks will be ended as invalid */ 'secret' => env('OVERLAY_SECRET'), @@ -72,20 +72,8 @@ ], 'bot' => [ - 'commands' => [ - - 'simple' => [ - '!hello' => 'Hello %username%! How are you doing?', - ], - - 'advanced' => [ - \Redbeed\OpenOverlay\ChatBot\Commands\HelloWorldBotCommand::class, - \Redbeed\OpenOverlay\ChatBot\Commands\ShoutOutBotCommand::class, - ] - ], - 'schedules' => [ \Redbeed\OpenOverlay\Console\Scheduling\MadeWithChatBotScheduling::class, - ] - ] + ], + ], ]; diff --git a/database/Factories/EventSubEventsFactory.php b/database/Factories/EventSubEventsFactory.php index 895de5e..0d895ae 100644 --- a/database/Factories/EventSubEventsFactory.php +++ b/database/Factories/EventSubEventsFactory.php @@ -7,11 +7,10 @@ class EventSubEventsFactory extends Factory { - protected $model = EventSubEvents::class; /** - * @inheritDoc + * {@inheritDoc} */ public function definition() { diff --git a/database/migrations/2020_12_17_000001_create_user_bots_enabled_table.php b/database/migrations/2020_12_17_000001_create_user_bots_enabled_table.php index 44f7d76..ced6730 100644 --- a/database/migrations/2020_12_17_000001_create_user_bots_enabled_table.php +++ b/database/migrations/2020_12_17_000001_create_user_bots_enabled_table.php @@ -14,7 +14,6 @@ class CreateUserBotsEnabledTable extends Migration public function up() { Schema::create('users_bots_enabled', function (Blueprint $table) { - $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('bot_id'); $table->timestamps(); diff --git a/routes/openoverlay.php b/routes/openoverlay.php index 78e8990..ae67d12 100644 --- a/routes/openoverlay.php +++ b/routes/openoverlay.php @@ -1,7 +1,6 @@ group(function () { - Route::middleware(['web', 'auth'])->group(function () { - Route::get('/redirect')->uses([AuthController::class, 'redirect']) ->name('connection.redirect'); @@ -27,7 +24,6 @@ Route::get('/callback')->uses([AppTokenController::class, 'handleProviderCallback']) ->name('connection.app-token.callback'); - }); // prefix: /connection/bot @@ -37,7 +33,6 @@ Route::get('/callback')->uses([BotAuthController::class, 'handleProviderCallback']) ->name('connection.bot.callback'); - }); }); @@ -45,6 +40,5 @@ Route::any('/webhook')->uses([WebhookController::class, 'handleProviderCallback']) ->name('connection.webhook'); }); - }); }); diff --git a/src/Actions/RegisterUserTwitchWebhooks.php b/src/Actions/RegisterUserTwitchWebhooks.php index d4f479f..30b4921 100644 --- a/src/Actions/RegisterUserTwitchWebhooks.php +++ b/src/Actions/RegisterUserTwitchWebhooks.php @@ -36,7 +36,7 @@ public static function registerAll(Connection $connection, bool $clearBeforeRegi public function clearBroadcasterSubscriptions() { - $this->apiClient->deleteSubByBroadcasterId((string)$this->connection->service_user_id); + $this->apiClient->deleteSubByBroadcasterId((string) $this->connection->service_user_id); } public function register(string $type): bool @@ -61,7 +61,7 @@ public function register(string $type): bool private function registerCondition($type): array { - $broadcasterId = (string)$this->connection->service_user_id; + $broadcasterId = (string) $this->connection->service_user_id; if ($type === 'channel.raid') { return ['to_broadcaster_user_id' => $broadcasterId]; diff --git a/src/Automations/Actions/TwitchChatBotMessage.php b/src/Automations/Actions/TwitchChatBotMessage.php new file mode 100644 index 0000000..7ae3653 --- /dev/null +++ b/src/Automations/Actions/TwitchChatBotMessage.php @@ -0,0 +1,35 @@ +message = $message; + } + + public function handle() + { + Artisan::queue(SendMessageCommand::class, [ + 'userId' => $this->getUser()->id, + '--botId' => $this->getBot()->id, + 'message' => $this->replaceInString($this->message), + ]); + } +} diff --git a/src/Automations/Actions/TwitchRandomChatBotMessage.php b/src/Automations/Actions/TwitchRandomChatBotMessage.php new file mode 100644 index 0000000..c827cba --- /dev/null +++ b/src/Automations/Actions/TwitchRandomChatBotMessage.php @@ -0,0 +1,37 @@ +messages = $messages; + } + + public function handle() + { + Artisan::queue(SendMessageCommand::class, [ + 'userId' => $this->getUser()->id, + '--botId' => $this->getBot()->id, + 'message' => $this->replaceInString(Arr::random($this->messages)), + ]); + } +} diff --git a/src/Automations/Actions/UseTwitchChatMessage.php b/src/Automations/Actions/UseTwitchChatMessage.php new file mode 100644 index 0000000..178d9d4 --- /dev/null +++ b/src/Automations/Actions/UseTwitchChatMessage.php @@ -0,0 +1,41 @@ +chatMessage = $chatMessage; + } + + public function setBotConnection(BotConnection $botConnection): void + { + $this->botConnection = $botConnection; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + protected function getBot(): ?BotConnection + { + return $this->botConnection ?: $this->chatMessage?->bot; + } + + protected function getUser(): ?User + { + return $this->user ?: $this->chatMessage?->channelUser; + } +} diff --git a/src/Automations/Actions/UseVariables.php b/src/Automations/Actions/UseVariables.php new file mode 100644 index 0000000..039c13d --- /dev/null +++ b/src/Automations/Actions/UseVariables.php @@ -0,0 +1,60 @@ +variables = array_merge_recursive($this->variables, $variables); + } + + public function getVariables($filterName = ''): array + { + if ($filterName) { + return $this->filterVariables($filterName); + } + + return $this->variables; + } + + private function filterVariables(string $filterName): array + { + $filtered = []; + foreach ($this->variables as $name => $value) { + if (str_contains($name, $filterName)) { + $filtered[$name] = $value; + } + } + + return $filtered; + } + + protected function replaceInString(string $string): string + { + return strtr($string, $this->makeReplacements($string)); + } + + protected function makeReplacements(string $string): array + { + $replacements = []; + foreach ($this->variables as $key => $value) { + $keyPattern = ':'.$key; + + if (Str::contains($string, $keyPattern)) { + if ($this->variables[$key] instanceof \Closure) { + // If the variable is a closure, we will execute it and replace the key with the result. + $this->variables[$key] = $this->variables[$key](); + } + + $replacements[$keyPattern] = $this->variables[$key]; + } + } + + return $replacements; + } +} diff --git a/src/Automations/AutomationDispatcher.php b/src/Automations/AutomationDispatcher.php new file mode 100644 index 0000000..bdf088e --- /dev/null +++ b/src/Automations/AutomationDispatcher.php @@ -0,0 +1,56 @@ +each(function ($handler) use ($trigger) { + $this->add($trigger, $handler); + }); + + return; + } + + $this->automations[$trigger][] = $handlerClass; + } + + public function getAutomations(?string $triggerClass = null): array + { + if ($triggerClass) { + return $this->automations[$triggerClass] ?? []; + } + + return $this->automations; + } + + public function trigger(mixed $trigger) + { + ray('fire '.get_class($trigger)); + + if (empty($this->automations[$trigger::class])) { + return; + } + + foreach ($this->automations[$trigger::class] as $automations) { + collect($automations)->each(function ($automation) use ($trigger) { + // If it's a closure, execute it + if ($automation instanceof Closure) { + $automation = $automation($trigger); + $automation->handle(); + + return; + } + + $automation = new $automation($trigger); + $automation->handle(); + }); + } + } +} diff --git a/src/Automations/AutomationHandler.php b/src/Automations/AutomationHandler.php new file mode 100644 index 0000000..1f03c16 --- /dev/null +++ b/src/Automations/AutomationHandler.php @@ -0,0 +1,89 @@ +trigger = $trigger; + } + + /** + * @return Filter[] + */ + public function filters(): array + { + return []; + } + + /** + * @return array + */ + public function actions(): array + { + return []; + } + + public function handle() + { + $variables = []; + + foreach ($this->filters() as $filter) { + $response = $filter->handle($this->trigger); + + ray('did i passed? ', $response, $filter->settings()); + + if ($response === false) { + // Filter failed stop the automation + return; + } + + $variables = array_merge_recursive( + $variables, $filter->variables() + ); + } + + foreach ($this->actions() as $action) { + $traits = class_uses($action); + + // Check if the action uses the UsesVariables trait and if so, add the variables to the action + if (in_array(UseVariables::class, $traits)) { + $action->addVariables($variables); + } + + // Check if the action uses the UsesTwitchChatMessage trait and if so, add the message to the action + if (in_array(UseTwitchChatMessage::class, $traits) && $this->trigger instanceof TwitchChatMessageTrigger) { + $action->setChatMessage($this->trigger->message); + } + + $action->handle(); + } + } + + #[ArrayShape(['trigger' => 'string', 'options' => 'array'])] + public static function triggerConfig(string $triggerClass, array $options = []) + { + return ['trigger' => $triggerClass, 'options' => $options]; + } + + #[ArrayShape(['action' => 'string', 'options' => 'array'])] + public static function actionConfig(string $actionClass, array $options = []) + { + return ['action' => $actionClass, 'options' => $options]; + } +} diff --git a/src/Automations/AutomationsServiceProvider.php b/src/Automations/AutomationsServiceProvider.php new file mode 100644 index 0000000..023abac --- /dev/null +++ b/src/Automations/AutomationsServiceProvider.php @@ -0,0 +1,42 @@ +app->singleton('automations', function () { + return new AutomationDispatcher(); + }); + + $this->booting(function () { + $automations = $this->getAutomations(); + + foreach ($automations as $trigger => $handler) { + Automation::add($trigger, $handler); + } + }); + } + + public function getAutomations(): array + { + return $this->automations; + } + + public function boot() + { + $this->callAfterResolving(Schedule::class, function (Schedule $schedule) { + $schedule->call(function () { + \automation(new ScheduleTrigger()); + })->everyMinute(); + }); + } +} diff --git a/src/Automations/Filters/ChatMessage/ChatMessageContainsFilter.php b/src/Automations/Filters/ChatMessage/ChatMessageContainsFilter.php new file mode 100644 index 0000000..2fd459d --- /dev/null +++ b/src/Automations/Filters/ChatMessage/ChatMessageContainsFilter.php @@ -0,0 +1,82 @@ +needle = $needle; + $this->caseSensitive = $caseSensitive; + } + + #[Pure] + public function validate(): bool + { + $message = $this->trigger->message->message; + $needle = $this->needle; + + if (! $this->caseSensitive) { + $message = Str::lower($message); + $needle = Str::lower($needle); + } + + return Str::contains($message, $needle); + } + + /** + * @throws AutomationFilterNotValid + */ + public function validTrigger() + { + parent::validTrigger(); + + if (! ($this->trigger instanceof TwitchChatMessageTrigger)) { + throw new AutomationFilterNotValid('Trigger is not valid. Trigger must be instance of TwitchChatMessageTrigger but is '.get_class($this->trigger)); + } + } + + public function variables(): array + { + return [ + 'username' => $this->trigger->message->username, + 'twitchUrl' => 'https://www.twitch.tv/'.$this->trigger->message->username, + 'game' => function () { + try { + return (new UsersClient())->lastGame($this->trigger->message->username); + } catch (ClientException) { + return ''; + } + }, + ]; + } + + public function settings(): array + { + return [ + 'needle' => $this->needle, + 'caseSensitive' => $this->caseSensitive, + ]; + } +} diff --git a/src/Automations/Filters/ChatMessage/ChatMessageContainsWithPatternFilter.php b/src/Automations/Filters/ChatMessage/ChatMessageContainsWithPatternFilter.php new file mode 100644 index 0000000..f8c4f3d --- /dev/null +++ b/src/Automations/Filters/ChatMessage/ChatMessageContainsWithPatternFilter.php @@ -0,0 +1,108 @@ +needle = $needle; + $this->regexPatterns = $regexPatterns; + $this->caseSensitive = $needleCaseSensitive; + } + + public function validate(): bool + { + $message = $this->trigger->message->message; + $needle = $this->needle; + + if (! $this->caseSensitive) { + $message = Str::lower($message); + $needle = Str::lower($needle); + } + + if (! Str::contains($message, $needle)) { + return false; + } + + return preg_match($this->regex(), $this->trigger->message->message); + } + + private function regex(): string + { + $regex = collect($this->regexPatterns) + ->mapWithKeys(function ($regex, $key) { + return [$key => '(?<'.$key.'>'.$regex.')']; + }) + ->prepend($this->needle) + ->implode(' '); + + return '/'.$regex.'/'; + } + + private function matches(): array + { + $matches = []; + preg_match($this->regex(), $this->trigger->message->message, $matches); + + return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); + } + + /** + * @throws AutomationFilterNotValid + */ + public function validTrigger() + { + parent::validTrigger(); + + if (! ($this->trigger instanceof TwitchChatMessageTrigger)) { + throw new AutomationFilterNotValid('Trigger is not valid. Trigger must be instance of TwitchChatMessageTrigger but is '.get_class($this->trigger)); + } + } + + public function variables(): array + { + return array_merge_recursive([ + 'username' => $this->trigger->message->username, + 'game' => function () { + try { + return (new UsersClient())->lastGame($this->trigger->message->username); + } catch (ClientException) { + return ''; + } + }, + ], $this->matches()); + } + + public function settings(): array + { + return [ + 'needle' => $this->needle, + 'regexPatterns' => $this->regexPatterns, + 'caseSensitive' => $this->caseSensitive, + ]; + } +} diff --git a/src/Automations/Filters/Filter.php b/src/Automations/Filters/Filter.php new file mode 100644 index 0000000..50387bf --- /dev/null +++ b/src/Automations/Filters/Filter.php @@ -0,0 +1,57 @@ +trigger = $trigger; + $this->validTrigger(); + + return $this->validate(); + } + + public function validate(): bool + { + return true; + } + + public function settings(): array + { + return []; + } + + /** + * @throws AutomationFilterNotValid + */ + public function validTrigger() + { + if (! ($this->trigger instanceof Trigger)) { + throw new AutomationFilterNotValid('Trigger is not valid. Trigger must be instance of Trigger but is '.get_class($this->trigger)); + } + } + + public function variables(): array + { + return []; + } +} diff --git a/src/Automations/Filters/FrequencyFilter.php b/src/Automations/Filters/FrequencyFilter.php new file mode 100644 index 0000000..22ad499 --- /dev/null +++ b/src/Automations/Filters/FrequencyFilter.php @@ -0,0 +1,32 @@ +trigger instanceof ScheduleTrigger)) { + throw new AutomationFilterNotValid('Trigger is not valid. Trigger must be instance of ScheduleTrigger but is '.get_class($this->trigger)); + } + } + + public function validate(): bool + { + return (new CronExpression($this->expression))->isDue($this->trigger->date->toDateTimeString()); + } +} diff --git a/src/Automations/Filters/Twitch/ChannelStatus.php b/src/Automations/Filters/Twitch/ChannelStatus.php new file mode 100644 index 0000000..d28f0a2 --- /dev/null +++ b/src/Automations/Filters/Twitch/ChannelStatus.php @@ -0,0 +1,70 @@ +connection = $userConnection; + } + + private function setOnlineStatus(string $status): self + { + $this->onlineStatus = $status; + + return $this; + } + + public function isOnline(): self + { + return $this->setOnlineStatus(self::IS_ONLINE); + } + + public function isOffline(): self + { + return $this->setOnlineStatus(self::IS_OFFLINE); + } + + protected function validateOnlineStatus(): bool + { + if ($this->onlineStatus === null) { + return true; + } + + $currentStatus = StreamerOnline::isOnline($this->connection->service_user_id) ? self::IS_ONLINE : self::IS_OFFLINE; + + return $currentStatus === $this->onlineStatus; + } + + public function validate(): bool + { + if (! $this->validateOnlineStatus()) { + return false; + } + + return true; + } + + public function settings(): array + { + return [ + 'onlineStatus' => $this->onlineStatus, + ]; + } +} diff --git a/src/Automations/Triggers/ScheduleTrigger.php b/src/Automations/Triggers/ScheduleTrigger.php new file mode 100644 index 0000000..6c69602 --- /dev/null +++ b/src/Automations/Triggers/ScheduleTrigger.php @@ -0,0 +1,19 @@ +date = now(); + } +} diff --git a/src/Automations/Triggers/Trigger.php b/src/Automations/Triggers/Trigger.php new file mode 100644 index 0000000..3273484 --- /dev/null +++ b/src/Automations/Triggers/Trigger.php @@ -0,0 +1,27 @@ +options = $options; + } +} diff --git a/src/Automations/Triggers/TwitchChatMessageTrigger.php b/src/Automations/Triggers/TwitchChatMessageTrigger.php new file mode 100644 index 0000000..ce543b4 --- /dev/null +++ b/src/Automations/Triggers/TwitchChatMessageTrigger.php @@ -0,0 +1,29 @@ +message = $message; + } + + public function valid(): bool + { + if (empty($this->options['message'])) { + return true; + } + + return Str::contains($this->message->message, $this->options['message']); + } +} diff --git a/src/ChatBot/Commands/BotCommand.php b/src/ChatBot/Commands/BotCommand.php deleted file mode 100644 index 71955b7..0000000 --- a/src/ChatBot/Commands/BotCommand.php +++ /dev/null @@ -1,126 +0,0 @@ -connection = $connectionHandler; - } - - public function handle(ChatMessage $chatMessage) - { - if ($this->messageValid($chatMessage->message) === false) { - return; - } - - // build & check parameters - $this->buildParameters($chatMessage->message); - if ($this->parametersValid() === false) { - return; - } - - $this->connection->sendChatMessage( - $chatMessage->channel, - $this->response($chatMessage) - ); - } - - protected function parametersValid(): bool - { - $keys = $this->parametersKeys(); - - return count($keys) === count($this->parameters); - } - - protected function buildParameters(string $message) - { - $keys = $this->parametersKeys(); - if (count($keys) <= 0) { - return; - } - - $valuesOnly = explode(' ', $message, 2); - if (count($valuesOnly) !== 2) { - return; - } - - $values = explode(' ', $valuesOnly[1], count($keys)); - foreach ($values as $valueKey => $value) { - $this->parameters[$keys[$valueKey]] = $value; - } - } - - protected function parameter(string $key): ?string - { - if (isset($this->parameters[$key])) { - return $this->parameters[$key]; - } - - return null; - } - - public function parametersKeys(): array - { - preg_match_all("/\{(.+?)\}/m", $this->signature, $matches); - if (count($matches) < 2) { - return []; - } - - return $matches[1]; - } - - public function response(ChatMessage $chatMessage): string - { - return ''; - } - - protected function command(): string - { - return head(explode(' ', $this->signature)); - } - - protected function messageValid(string $message): bool - { - if ($this->messageStartsWith($message, $this->command())) { - return true; - } - - if (is_array($this->aliasCommands) && count($this->aliasCommands)) { - foreach ($this->aliasCommands as $aliasCommand) { - if ($this->messageStartsWith($message, $aliasCommand)) { - return true; - } - } - } - - return false; - } - - protected function messageStartsWith(string $message, string $command): bool - { - // perfect match - if (trim($command) === trim($message)) { - return true; - } - - // match with space - return substr(trim($message), 0, strlen(trim($command) . ' ')) === trim($command) . ' '; - } -} diff --git a/src/ChatBot/Commands/HelloWorldBotCommand.php b/src/ChatBot/Commands/HelloWorldBotCommand.php deleted file mode 100644 index 1e49dd2..0000000 --- a/src/ChatBot/Commands/HelloWorldBotCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -username, - 'It is ' . Carbon::now()->toString() . '... I think', - ]); - } -} diff --git a/src/ChatBot/Commands/ShoutOutBotCommand.php b/src/ChatBot/Commands/ShoutOutBotCommand.php deleted file mode 100644 index e1ff204..0000000 --- a/src/ChatBot/Commands/ShoutOutBotCommand.php +++ /dev/null @@ -1,48 +0,0 @@ -parameter('username'), '@'); - - $usersClient = new UsersClient(); - try { - $users = $usersClient->byUsername($username); - } catch (ClientException $exception) { - return ''; - } - - $user = head($users['data']); - if ($user['login'] !== strtolower($username)) { - return ''; - } - - $response = [ - 'Don“t forget to checkout ' . $user['display_name'] . ' www.twitch.tv/' . $user['login'] - ]; - - try { - $channelClient = new ChannelsClient(); - $channels = $channelClient->get($user['id']); - $channel = head($channels['data']); - - if (!empty($channel['game_id'])) { - $response[] = '- last playing "' . $channel['game_name'] . '"'; - } - } catch (ClientException $exception) { - // ignore - } - - return implode(' ', $response); - } -} diff --git a/src/ChatBot/Commands/SimpleBotCommands.php b/src/ChatBot/Commands/SimpleBotCommands.php deleted file mode 100644 index e97c0e4..0000000 --- a/src/ChatBot/Commands/SimpleBotCommands.php +++ /dev/null @@ -1,49 +0,0 @@ -simpleCommands = config('openoverlay.bot.commands.simple'); - } - - public function handle(ChatMessage $chatMessage) - { - foreach ($this->simpleCommands as $command => $responseMessage) { - $this->handleSimpleCommand($chatMessage, $command, $responseMessage); - } - } - - public function handleSimpleCommand(ChatMessage $chatMessage, string $command, string $responseMessage): void - { - if ($this->messageStartsWith($chatMessage->message, $command) === false) { - return; - } - - $this->connection->sendChatMessage( - $chatMessage->channel, - $this->responseSimpleCommand($chatMessage, $responseMessage) - ); - } - - public function responseSimpleCommand(ChatMessage $chatMessage, string $message): string - { - $replace = [ - '%username%' => $chatMessage->username, - ]; - - return str_replace(array_keys($replace), array_values($replace), $message); - } -} diff --git a/src/ChatBot/Twitch/ChatMessage.php b/src/ChatBot/Twitch/ChatMessage.php index 98e9dd7..349c0a7 100644 --- a/src/ChatBot/Twitch/ChatMessage.php +++ b/src/ChatBot/Twitch/ChatMessage.php @@ -2,38 +2,46 @@ namespace Redbeed\OpenOverlay\ChatBot\Twitch; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; +use Redbeed\OpenOverlay\Models\BotConnection; use Redbeed\OpenOverlay\Models\Twitch\Emote; class ChatMessage { - /** @var string */ - public $username; + public string $username; - /** @var string */ - public $channel; + public string $channel; - /** @var string */ - public $message; + public string $message; /** @var Emote[] */ - public $possibleEmotes; + public array $possibleEmotes; - public function __construct(string $channel, string $username, string $message) + public ?\Illuminate\Foundation\Auth\User $channelUser; + + public ?BotConnection $bot; + + public function __construct(string $channel, string $username, string $message, ?BotConnection $bot = null) { $this->channel = $channel; $this->username = trim($username); $this->message = trim($message); + + $this->bot = $bot; + if ($this->bot) { + $this->channelUser = $this->bot->users()->where('name', $this->channel)->first(); + } } - public static function parseIRCMessage(string $message): ?ChatMessage + public static function parseIRCMessage(BotConnection $bot, string $message): ?ChatMessage { try { preg_match("/:(.*)\!.*#(\S+) :(.*)/", $message, $matches); - return new ChatMessage($matches[2], $matches[1], $matches[3]); + return new ChatMessage($matches[2], $matches[1], $matches[3], $bot); } catch (\Exception $exception) { - echo $exception->getMessage() . "\r\n"; + Log::error($exception); } return null; @@ -44,16 +52,17 @@ public function toHtml(string $emoteSize = Emote::IMAGE_SIZE_MD): string $emoteList = collect($this->possibleEmotes) ->map(function (Emote $emote) use ($emoteSize) { $name = htmlspecialchars_decode($emote->name); - $regex = '/'.preg_quote($name, '/') . '(\s|$)/'; + $regex = '/'.preg_quote($name, '/').'(\s|$)/'; if (@preg_match($regex, null) === false) { - echo "Emote Regex '" . $regex . "' is invalid \r\n"; + echo "Emote Regex '".$regex."' is invalid \r\n"; + return null; } return [ 'name' => $regex, - 'image' => '' . Str::slug($emote->name) . ' ', + 'image' => ''.Str::slug($emote->name).' ', ]; }); diff --git a/src/ChatBot/Twitch/ConnectionHandler.php b/src/ChatBot/Twitch/ConnectionHandler.php index cced429..6a97e79 100644 --- a/src/ChatBot/Twitch/ConnectionHandler.php +++ b/src/ChatBot/Twitch/ConnectionHandler.php @@ -1,15 +1,12 @@ connection = $connection; - $this->connection->on('message', function ($message) use ($connection) { + $this->connection->on('message', function ($message) { $this->basicMessageHandler($message); }); } @@ -62,48 +55,49 @@ public static function withPrivateMessageHandler(WebSocket $connection): Connect public function privateMessageHandler(string $message): void { // if is chat message - if (strpos($message, 'PRIVMSG') !== false) { + if (str_contains($message, 'PRIVMSG')) { $this->chatMessageReceived($message); } } public function basicMessageHandler(string $message): void { - // ignore for basic handler - if (strpos($message, 'PRIVMSG') !== false) { + // if is chat message starts with PRIVMSG ignore basic handler + if (str_contains($message, 'PRIVMSG')) { return; } - // get join message - if (strpos($message, 'NOTICE * :Login authentication failed') !== false) { - $this->write("LOGIN | " . $message); + // if this message contains "Login authentication" reset bot connection + if (str_contains($message, 'NOTICE * :Login authentication failed')) { + $this->write('LOGIN | '.$message); event(new BotTokenExpires($this->bot)); $this->connection->close(); + return; } - // get join message - if (strpos($message, 'PING') !== false) { + // handle ping message from twitch + if (str_contains($message, 'PING')) { $this->pingReceived($message); return; } - // get join message - if (strpos($message, 'JOIN') !== false) { + // handle join confirmation + if (str_contains($message, 'JOIN')) { $this->joinMessageReceived($message); return; } - $this->write("UNKOWN | " . $message . PHP_EOL, ''); + $this->write('UNKOWN | '.$message.PHP_EOL, ''); } public function pingReceived(string $message): void { $this->send('PONG :tmi.twitch.tv'); - $this->write("PING PONG done"); + $this->write('PING PONG done'); } public function joinMessageReceived(string $message): void @@ -111,7 +105,7 @@ public function joinMessageReceived(string $message): void try { preg_match("/:(.*)\!.*#(.*)/", $message, $matches); - $this->write("BOT (" . $matches[1] . ") joined " . $matches[2]); + $this->write('BOT ('.$matches[1].') joined '.$matches[2]); $channelName = trim(strtolower($matches[2])); @@ -119,9 +113,8 @@ public function joinMessageReceived(string $message): void $this->runChannelQueue($channelName); $this->afterJoinCallBacks($channelName); - } catch (\Exception $exception) { - $this->write($exception->getMessage() . ' ' . $exception->getLine() . PHP_EOL, 'ERROR'); + $this->write($exception->getMessage().' '.$exception->getLine().PHP_EOL, 'ERROR'); } } @@ -130,10 +123,8 @@ private function afterJoinCallBacks(string $channelName) $channelName = strtolower($channelName); if (isset($this->joinedCallBack[$channelName])) { - - $this->write('CALL CALLBACK FOR ' . $channelName); + $this->write('CALL CALLBACK FOR '.$channelName); $this->joinedCallBack[$channelName](); - } } @@ -142,7 +133,7 @@ public function addJoinedCallBack(string $channelName, callable $callback): void $channelName = strtolower($channelName); $this->joinedCallBack[$channelName] = $callback; - $this->write('Callback added for ' . $channelName); + $this->write('Callback added for '.$channelName); // channel already joined if (in_array($channelName, $this->joinedChannel)) { @@ -152,7 +143,7 @@ public function addJoinedCallBack(string $channelName, callable $callback): void public function chatMessageReceived(string $message): void { - $model = ChatMessage::parseIRCMessage($message); + $model = ChatMessage::parseIRCMessage($this->bot, $message); if ($model === null) { return; @@ -160,24 +151,13 @@ public function chatMessageReceived(string $message): void $model->possibleEmotes = $this->emoteSets[$model->channel] ?? []; - $this->write($model->channel . ' | ' . $model->username . ': ' . $model->message, 'Twitch'); - - try { - // Check commands - foreach ($this->customCommands as $commandHandler) { - $commandHandler->handle($model); - } - } catch (\Exception $exception) { - $this->write($exception->getMessage(), 'ERROR'); - $this->write($exception->getFile() . ' #' . $exception->getLine(), 'ERROR'); - } - - $this->write($model->channel . ' | ' . $model->username . ': ' . $model->message . ' HANDLED'); + $this->write($model->channel.' | '.$model->username.': '.$model->message, 'Twitch'); try { event(new ChatMessageReceived($model)); } catch (\Exception $exception) { - $this->write(" -> EVENT ERROR: " . $exception->getMessage(), 'ERROR'); + Log::error($exception); + $this->write(' -> EVENT ERROR: '.$exception->getMessage(), 'ERROR'); } } @@ -185,8 +165,8 @@ public function auth(BotConnection $bot) { $this->bot = $bot; - $this->send('PASS oauth:' . $this->bot->service_token); - $this->send('NICK ' . strtolower($this->bot->bot_username)); + $this->send('PASS oauth:'.$this->bot->service_token); + $this->send('NICK '.strtolower($this->bot->bot_username)); } public function send(string $message): void @@ -194,7 +174,6 @@ public function send(string $message): void $this->connection->send($message); } - public function joinChannel(Connection $channel): void { $channelName = strtolower($channel->service_username); @@ -202,8 +181,8 @@ public function joinChannel(Connection $channel): void $this->channelQueue[$channelName] = []; $this->loadEmotes($channel); - $this->send('JOIN #' . strtolower($channelName)); - $this->write('JOIN #' . strtolower($channelName)); + $this->send('JOIN #'.strtolower($channelName)); + $this->write('JOIN #'.strtolower($channelName)); } private function loadEmotes(Connection $channel) @@ -221,7 +200,7 @@ private function runChannelQueue(string $channelName): void { $channelName = trim(strtolower($channelName)); - if (!empty($this->channelQueue[$channelName])) { + if (! empty($this->channelQueue[$channelName])) { foreach ($this->channelQueue[$channelName] as $item) { $this->send($item); } @@ -233,10 +212,10 @@ private function runChannelQueue(string $channelName): void public function sendChatMessage(string $channelName, string $message): void { $lowerChannelName = strtolower($channelName); - $message = 'PRIVMSG #' . $lowerChannelName . ' :' . $message . PHP_EOL; + $message = 'PRIVMSG #'.$lowerChannelName.' :'.$message.PHP_EOL; // send message after channel joined - if (!in_array($lowerChannelName, $this->joinedChannel)) { + if (! in_array($lowerChannelName, $this->joinedChannel)) { $this->channelQueue[$lowerChannelName][] = $message; return; @@ -246,23 +225,9 @@ public function sendChatMessage(string $channelName, string $message): void $this->write($message); } - public function initCustomCommands(): void - { - /** @var BotCommand[] $commandClasses */ - $commandClasses = config('openoverlay.bot.commands.advanced'); - - // add simple command handler - $commandClasses[] = SimpleBotCommands::class; - - foreach ($commandClasses as $commandClass) { - $this->customCommands[] = new $commandClass($this); - } - } - protected function write(string $output, string $title = 'OpenOverlay', $newLine = true) { - $title = !empty($title) ? '[' . $title . ']' : ''; - echo trim($title . ' ' . $output) . ($newLine ? PHP_EOL : ''); + $title = ! empty($title) ? '['.$title.']' : ''; + echo trim($title.' '.$output).($newLine ? PHP_EOL : ''); } - } diff --git a/src/Console/Commands/BroadcastFaker/ChannelCheerFake.php b/src/Console/Commands/BroadcastFaker/ChannelCheerFake.php index bee8ec6..c7c2913 100644 --- a/src/Console/Commands/BroadcastFaker/ChannelCheerFake.php +++ b/src/Console/Commands/BroadcastFaker/ChannelCheerFake.php @@ -4,24 +4,23 @@ class ChannelCheerFake extends Fake { - protected $eventData = [ - "is_anonymous" => false, - "user_id" => "1234", - "user_login" => null, - "user_name" => null, - "broadcaster_user_id" => "1337", - "broadcaster_user_login" => "cooler_user", - "broadcaster_user_name" => "Cooler_User", - "message" => "This is a bit cheer for you!", - "bits" => 1000, + 'is_anonymous' => false, + 'user_id' => '1234', + 'user_login' => null, + 'user_name' => null, + 'broadcaster_user_id' => '1337', + 'broadcaster_user_login' => 'cooler_user', + 'broadcaster_user_name' => 'Cooler_User', + 'message' => 'This is a bit cheer for you!', + 'bits' => 1000, ]; protected function randomizeEventData(): array { $array = parent::randomizeEventData(); - $anonymous = (bool)random_int(0, 1); + $anonymous = (bool) random_int(0, 1); if ($anonymous === false) { $username = Fake::fakeUsername(); diff --git a/src/Console/Commands/BroadcastFaker/ChannelFollowFake.php b/src/Console/Commands/BroadcastFaker/ChannelFollowFake.php index 1250e84..b4b6467 100644 --- a/src/Console/Commands/BroadcastFaker/ChannelFollowFake.php +++ b/src/Console/Commands/BroadcastFaker/ChannelFollowFake.php @@ -2,14 +2,13 @@ namespace Redbeed\OpenOverlay\Console\Commands\BroadcastFaker; -class ChannelFollowFake extends Fake +class ChannelFollowFake extends Fake { - protected $eventData = [ - "user_id" => "1234", - "user_name" => "cool_user", - "broadcaster_user_id" => "1337", - "broadcaster_user_name" => "cooler_user", + 'user_id' => '1234', + 'user_name' => 'cool_user', + 'broadcaster_user_id' => '1337', + 'broadcaster_user_name' => 'cooler_user', ]; protected function randomizeEventData(): array diff --git a/src/Console/Commands/BroadcastFaker/ChannelRaidFake.php b/src/Console/Commands/BroadcastFaker/ChannelRaidFake.php index c2a8149..86b5cac 100644 --- a/src/Console/Commands/BroadcastFaker/ChannelRaidFake.php +++ b/src/Console/Commands/BroadcastFaker/ChannelRaidFake.php @@ -4,15 +4,14 @@ class ChannelRaidFake extends Fake { - protected $eventData = [ - "from_broadcaster_user_id" => "1234", - "from_broadcaster_user_login" => "cool_user", - "from_broadcaster_user_name" => "Cool_User", - "to_broadcaster_user_id" => "1337", - "to_broadcaster_user_login" => "cooler_user", - "to_broadcaster_user_name" => "Cooler_User", - "viewers" => 9001 + 'from_broadcaster_user_id' => '1234', + 'from_broadcaster_user_login' => 'cool_user', + 'from_broadcaster_user_name' => 'Cool_User', + 'to_broadcaster_user_id' => '1337', + 'to_broadcaster_user_login' => 'cooler_user', + 'to_broadcaster_user_name' => 'Cooler_User', + 'viewers' => 9001, ]; protected function randomizeEventData(): array diff --git a/src/Console/Commands/BroadcastFaker/ChannelSubscribeFake.php b/src/Console/Commands/BroadcastFaker/ChannelSubscribeFake.php index 917db2c..8251cae 100644 --- a/src/Console/Commands/BroadcastFaker/ChannelSubscribeFake.php +++ b/src/Console/Commands/BroadcastFaker/ChannelSubscribeFake.php @@ -5,12 +5,12 @@ class ChannelSubscribeFake extends Fake { protected $eventData = [ - "user_id" => "1234", - "user_name" => "cool_user", - "broadcaster_user_id" => "1337", - "broadcaster_user_name" => "cooler_user", - "tier" => "1000", - "is_gift" => false, + 'user_id' => '1234', + 'user_name' => 'cool_user', + 'broadcaster_user_id' => '1337', + 'broadcaster_user_name' => 'cooler_user', + 'tier' => '1000', + 'is_gift' => false, ]; protected function randomizeEventData(): array diff --git a/src/Console/Commands/BroadcastFaker/ChannelUpdateFake.php b/src/Console/Commands/BroadcastFaker/ChannelUpdateFake.php index 797340b..183075f 100644 --- a/src/Console/Commands/BroadcastFaker/ChannelUpdateFake.php +++ b/src/Console/Commands/BroadcastFaker/ChannelUpdateFake.php @@ -6,15 +6,14 @@ class ChannelUpdateFake extends Fake { - protected $eventData = [ - "user_id" => "1337", - "user_name" => "open_overlay_user", - "title" => "Best Stream Ever", - "language" => "en", - "category_id" => "21779", - "category_name" => "Fortnite", - "is_mature" => false, + 'user_id' => '1337', + 'user_name' => 'open_overlay_user', + 'title' => 'Best Stream Ever', + 'language' => 'en', + 'category_id' => '21779', + 'category_name' => 'Fortnite', + 'is_mature' => false, ]; protected function randomizeEventData(): array @@ -23,7 +22,7 @@ protected function randomizeEventData(): array $array['title'] = implode(' ', [ $array['title'], - '('.Carbon::now()->format('H:i:s').')' + '('.Carbon::now()->format('H:i:s').')', ]); $array['user_name'] = Fake::fakeUsername(); diff --git a/src/Console/Commands/BroadcastFaker/Fake.php b/src/Console/Commands/BroadcastFaker/Fake.php index c92e019..321b6ea 100644 --- a/src/Console/Commands/BroadcastFaker/Fake.php +++ b/src/Console/Commands/BroadcastFaker/Fake.php @@ -1,14 +1,11 @@ randomizeEventData(); } - public static function fakeUsername(): string { + public static function fakeUsername(): string + { return Arr::random([ 'Chris', 'redbeed', @@ -34,7 +32,7 @@ public static function fakeUsername(): string { 'Lethinium', 'kekub', 'Laravel_user', - 'Twitch_user' + 'Twitch_user', ]); } } diff --git a/src/Console/Commands/BroadcastFaker/StreamOffline.php b/src/Console/Commands/BroadcastFaker/StreamOffline.php index 061b0e8..6b98ae2 100644 --- a/src/Console/Commands/BroadcastFaker/StreamOffline.php +++ b/src/Console/Commands/BroadcastFaker/StreamOffline.php @@ -5,8 +5,8 @@ class StreamOffline extends Fake { protected $eventData = [ - "broadcaster_user_id" => "u1337", - "broadcaster_user_login" => "cooler_user", - "broadcaster_user_name" => "Cooler_user", + 'broadcaster_user_id' => 'u1337', + 'broadcaster_user_login' => 'cooler_user', + 'broadcaster_user_name' => 'Cooler_user', ]; } diff --git a/src/Console/Commands/BroadcastFaker/StreamOnline.php b/src/Console/Commands/BroadcastFaker/StreamOnline.php index 1f626fd..a1e2316 100644 --- a/src/Console/Commands/BroadcastFaker/StreamOnline.php +++ b/src/Console/Commands/BroadcastFaker/StreamOnline.php @@ -7,12 +7,12 @@ class StreamOnline extends Fake { protected $eventData = [ - "id" => "1337", - "broadcaster_user_id" => "u1337", - "broadcaster_user_login" => "cooler_user", - "broadcaster_user_name" => "Cooler_user", - "type" => "live", - "started_at" => null, + 'id' => '1337', + 'broadcaster_user_id' => 'u1337', + 'broadcaster_user_login' => 'cooler_user', + 'broadcaster_user_name' => 'Cooler_user', + 'type' => 'live', + 'started_at' => null, ]; protected function randomizeEventData(): array diff --git a/src/Console/Commands/ChatBot/RestartServerCommand.php b/src/Console/Commands/ChatBot/RestartServerCommand.php index b9e5ae3..543bf2f 100644 --- a/src/Console/Commands/ChatBot/RestartServerCommand.php +++ b/src/Console/Commands/ChatBot/RestartServerCommand.php @@ -26,7 +26,6 @@ class RestartServerCommand extends Command public function handle(): void { - Cache::forever( StartCommand::RESTART_CACHE_KEY, $this->currentTime() diff --git a/src/Console/Commands/ChatBot/RuntimeCommand.php b/src/Console/Commands/ChatBot/RuntimeCommand.php index 5464b07..50f5269 100644 --- a/src/Console/Commands/ChatBot/RuntimeCommand.php +++ b/src/Console/Commands/ChatBot/RuntimeCommand.php @@ -24,7 +24,6 @@ protected function softShutdown() { $this->loop->stop(); - echo "Chatbot Service will shutdown." . PHP_EOL; + echo 'Chatbot Service will shutdown.'.PHP_EOL; } - } diff --git a/src/Console/Commands/ChatBot/SendMessageCommand.php b/src/Console/Commands/ChatBot/SendMessageCommand.php index af51cc5..9e9d8e0 100644 --- a/src/Console/Commands/ChatBot/SendMessageCommand.php +++ b/src/Console/Commands/ChatBot/SendMessageCommand.php @@ -2,13 +2,13 @@ namespace Redbeed\OpenOverlay\Console\Commands\ChatBot; +use function Ratchet\Client\connect; use Ratchet\Client\WebSocket; use Redbeed\OpenOverlay\ChatBot\Twitch\ConnectionHandler; use Redbeed\OpenOverlay\Models\BotConnection; use Redbeed\OpenOverlay\Models\User\Connection; use Redbeed\OpenOverlay\Models\User\UserOpenOverlay; use Redbeed\OpenOverlay\OpenOverlay; -use function Ratchet\Client\connect; class SendMessageCommand extends RuntimeCommand { @@ -35,11 +35,13 @@ public function handle(): void if (trim($message) === null) { $this->error('Message not filled'); + return; } if ($user === null) { $this->error('User not found'); + return; } @@ -47,6 +49,7 @@ public function handle(): void if ($bot === null) { $this->error('Bot not found'); + return; } @@ -59,7 +62,6 @@ public function handle(): void $twitchUsers = $user->connections()->where('service', 'twitch')->get(); foreach ($twitchUsers as $twitchUser) { - $connectionHandler->joinChannel($twitchUser); $connectionHandler->sendChatMessage($twitchUser->service_username, $message); @@ -72,7 +74,6 @@ public function handle(): void }); }); } - }, function ($e) { echo "Could not connect: {$e->getMessage()}\n"; }); @@ -90,6 +91,7 @@ private function configureMaxRuntime() private function getUser() { $userId = $this->argument('userId'); + return (OpenOverlay::userModel())::find($userId); } diff --git a/src/Console/Commands/ChatBot/StartCommand.php b/src/Console/Commands/ChatBot/StartCommand.php index 7118b6a..bffa5d4 100644 --- a/src/Console/Commands/ChatBot/StartCommand.php +++ b/src/Console/Commands/ChatBot/StartCommand.php @@ -3,14 +3,13 @@ namespace Redbeed\OpenOverlay\Console\Commands\ChatBot; use Illuminate\Support\Facades\Cache; +use function Ratchet\Client\connect; use Ratchet\Client\WebSocket; use Redbeed\OpenOverlay\ChatBot\Twitch\ConnectionHandler; use Redbeed\OpenOverlay\Models\BotConnection; -use function Ratchet\Client\connect; class StartCommand extends RuntimeCommand { - const RESTART_CACHE_KEY = 'redbeed:open-overlay:chat-bot:restart'; /** @@ -28,13 +27,10 @@ class StartCommand extends RuntimeCommand protected $description = 'Chat Bot worker (loop service)'; /** - * * Timestamp of the last restart - * */ private int $lastRestart; - public function handle(): void { $this->configureRestartTimer(); @@ -65,8 +61,6 @@ private function configureChatbot() foreach ($bot->users as $user) { $twitchUsers = $user->connections()->where('service', 'twitch')->get(); - $connectionHandler->initCustomCommands(); - foreach ($twitchUsers as $twitchUser) { $connectionHandler->joinChannel($twitchUser); $connectionHandler->sendChatMessage($twitchUser->service_username, 'Hello'); @@ -76,7 +70,6 @@ private function configureChatbot() $conn->on('close', function ($code = null, $reason = null) { echo "Connection closed ({$code} - {$reason})"; }); - }, function ($e) { echo "Could not connect: {$e->getMessage()}\n"; }); diff --git a/src/Console/Commands/EventBroadcastFaker.php b/src/Console/Commands/EventBroadcastFaker.php index 5fd28fa..0be4583 100644 --- a/src/Console/Commands/EventBroadcastFaker.php +++ b/src/Console/Commands/EventBroadcastFaker.php @@ -2,14 +2,12 @@ namespace Redbeed\OpenOverlay\Console\Commands; -use Carbon\Carbon; -use Illuminate\Database\Eloquent\Model; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\ChannelCheerFake; +use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\ChannelFollowFake; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\ChannelRaidFake; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\ChannelSubscribeFake; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\ChannelUpdateFake; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\Fake; -use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\ChannelFollowFake; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\StreamOffline; use Redbeed\OpenOverlay\Console\Commands\BroadcastFaker\StreamOnline; use Redbeed\OpenOverlay\Events\Twitch\EventReceived; @@ -62,7 +60,7 @@ public function handle(): void return; } - if (!array_key_exists($type, $this->types)) { + if (! array_key_exists($type, $this->types)) { $this->error('Type is not provided'); return; @@ -77,6 +75,6 @@ public function handle(): void ]); broadcast(new EventReceived($fakeEvent)); - $this->info('Event ' . $type . ' for ' . $twitchUserId . ' fired'); + $this->info('Event '.$type.' for '.$twitchUserId.' fired'); } } diff --git a/src/Console/Commands/EventSubDeleteCommand.php b/src/Console/Commands/EventSubDeleteCommand.php index 72088b3..c321b5a 100644 --- a/src/Console/Commands/EventSubDeleteCommand.php +++ b/src/Console/Commands/EventSubDeleteCommand.php @@ -88,10 +88,9 @@ public function handle(): void $this->subscriptionsTable($subscriptions); } - $this->info($subscriptionsCount . ' subscriptions matching your options'); + $this->info($subscriptionsCount.' subscriptions matching your options'); if ($this->confirm('Do you wish to delete them?')) { - $deleteProgress = $this->output->createProgressBar($subscriptionsCount); $deleteProgress->start(); @@ -99,12 +98,10 @@ public function handle(): void foreach ($subscriptions as $subscription) { try { - $eventSubClient->deleteSubscription($subscription['id']); $deleted++; - } catch (RequestException $exception) { - $this->error($subscription['id'] . ' could not deleted'); + $this->error($subscription['id'].' could not deleted'); } $deleteProgress->advance(); @@ -113,7 +110,7 @@ public function handle(): void $deleteProgress->finish(); $this->newLine(2); - $this->info('Total EventSub deleted: ' . $deleted . '/' . $subscriptionsCount); + $this->info('Total EventSub deleted: '.$deleted.'/'.$subscriptionsCount); } } diff --git a/src/Console/Commands/EventSubListingCommand.php b/src/Console/Commands/EventSubListingCommand.php index 5804808..94807b0 100644 --- a/src/Console/Commands/EventSubListingCommand.php +++ b/src/Console/Commands/EventSubListingCommand.php @@ -3,7 +3,6 @@ namespace Redbeed\OpenOverlay\Console\Commands; use Illuminate\Console\Command; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Redbeed\OpenOverlay\Models\Twitch\EventSubscription; use Redbeed\OpenOverlay\Service\Twitch\EventSubClient; @@ -34,11 +33,8 @@ public function __construct() parent::__construct(); } - /** * Execute the console command. - * - * @return int */ public function handle() { diff --git a/src/Console/Commands/Make/MakeBotSchedulingCommand.php b/src/Console/Commands/Make/MakeBotSchedulingCommand.php index b267ae1..15346b7 100644 --- a/src/Console/Commands/Make/MakeBotSchedulingCommand.php +++ b/src/Console/Commands/Make/MakeBotSchedulingCommand.php @@ -3,9 +3,7 @@ namespace Redbeed\OpenOverlay\Console\Commands\Make; use Illuminate\Console\GeneratorCommand; -use Illuminate\Support\Str; use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; class MakeBotSchedulingCommand extends GeneratorCommand { diff --git a/src/Console/Commands/Make/Stubs/BotCommand.stub b/src/Console/Commands/Make/Stubs/BotCommand.stub deleted file mode 100644 index 25c91b6..0000000 --- a/src/Console/Commands/Make/Stubs/BotCommand.stub +++ /dev/null @@ -1,18 +0,0 @@ -option('force')) { + if (! empty($currentSecret) && ! $this->option('force')) { $this->warn('You already have a secret'); + return; } @@ -44,7 +44,7 @@ private function writeSecretKeyInEnvironmentFile($key): void if (preg_match($secretKeyPattern, $envFileContent) === 0) { file_put_contents( $envFilePath, - self::ENV_KEY . '=' . $key . PHP_EOL, + self::ENV_KEY.'='.$key.PHP_EOL, FILE_APPEND ); @@ -57,7 +57,7 @@ private function writeSecretKeyInEnvironmentFile($key): void $envFilePath, preg_replace( $secretKeyPattern, - self::ENV_KEY . '=' . $key, + self::ENV_KEY.'='.$key, $envFileContent ) ); @@ -69,13 +69,14 @@ private function showOption(string $key): void { if ($this->option('show')) { $this->info('New Secret Key:'); - $this->info(self::ENV_KEY . '=' . $key); + $this->info(self::ENV_KEY.'='.$key); } } protected function keyReplacementPattern(): string { - $escaped = preg_quote('=' . env(self::ENV_KEY, ''), '/'); - return "/^" . self::ENV_KEY . "{$escaped}/m"; + $escaped = preg_quote('='.config('openoverlay.webhook.twitch.secret'), '/'); + + return '/^'.self::ENV_KEY."{$escaped}/m"; } } diff --git a/src/Console/Commands/Twitch/OnlineStatusCommand.php b/src/Console/Commands/Twitch/OnlineStatusCommand.php new file mode 100644 index 0000000..acfcb54 --- /dev/null +++ b/src/Console/Commands/Twitch/OnlineStatusCommand.php @@ -0,0 +1,46 @@ +option('all') && $this->argument('twitchUserId')) { + $connections = $connections->where('service_user_id', $this->argument('twitchUserId')); + } + + $connections = $connections->get(); + $streamsResponse = (new StreamsClient())->byUserIds($connections->pluck('service_user_id')->toArray()); + $streamsData = collect($streamsResponse['data'] ?? []); + + foreach ($connections as $connection) { + $stream = $streamsData->firstWhere('user_id', $connection->service_user_id); + + if ($stream !== null) { + StreamerOnline::setOnline( + $connection->service_user_id, + Carbon::parse($stream['created_at'] ?? null, 'UTC') + ); + + $this->info('Streamer '.$connection->service_username.' is online'); + continue; + } + + $this->info('Streamer '.$connection->service_username.' is offline'); + StreamerOnline::setOffline($connection->service_user_id); + } + } +} diff --git a/src/Console/Commands/Twitch/RefresherCommand.php b/src/Console/Commands/Twitch/RefresherCommand.php index 57eeba6..73fe88c 100644 --- a/src/Console/Commands/Twitch/RefresherCommand.php +++ b/src/Console/Commands/Twitch/RefresherCommand.php @@ -8,7 +8,6 @@ class RefresherCommand extends Command { - protected $signature = 'overlay:twitch:refresher'; protected $description = 'Refresh followers and subscriber for all twitch connections'; @@ -18,7 +17,7 @@ public function handle(): void $connections = Connection::where('service', 'twitch')->get(); foreach ($connections as $connection) { - $this->info('Start for ' . $connection->service_username . ' (' . $connection->service_user_id . ')'); + $this->info('Start for '.$connection->service_username.' ('.$connection->service_user_id.')'); event(new RefresherEvent($connection)); } diff --git a/src/Console/ConsoleServiceProvider.php b/src/Console/ConsoleServiceProvider.php index abfcc21..901e9d3 100644 --- a/src/Console/ConsoleServiceProvider.php +++ b/src/Console/ConsoleServiceProvider.php @@ -4,19 +4,17 @@ use Illuminate\Support\ServiceProvider; use Redbeed\OpenOverlay\Console\Commands\ChatBot\RestartServerCommand; -use Redbeed\OpenOverlay\Console\Commands\ChatBot\StartCommand; use Redbeed\OpenOverlay\Console\Commands\ChatBot\SendMessageCommand; +use Redbeed\OpenOverlay\Console\Commands\ChatBot\StartCommand; use Redbeed\OpenOverlay\Console\Commands\EventBroadcastFaker; use Redbeed\OpenOverlay\Console\Commands\EventSubDeleteCommand; use Redbeed\OpenOverlay\Console\Commands\EventSubListingCommand; -use Redbeed\OpenOverlay\Console\Commands\Make\MakeBotCommandCommand; -use Redbeed\OpenOverlay\Console\Commands\Make\MakeBotSchedulingCommand; use Redbeed\OpenOverlay\Console\Commands\SecretCommand; +use Redbeed\OpenOverlay\Console\Commands\Twitch\OnlineStatusCommand; use Redbeed\OpenOverlay\Console\Commands\Twitch\RefresherCommand; class ConsoleServiceProvider extends ServiceProvider { - public function boot(): void { $this->registerGlobalCommands(); @@ -36,10 +34,8 @@ protected function registerConsoleCommands(): void StartCommand::class, RestartServerCommand::class, - MakeBotCommandCommand::class, - MakeBotSchedulingCommand::class, - RefresherCommand::class, + OnlineStatusCommand::class, ]); } diff --git a/src/Console/Scheduling/ChatBotScheduling.php b/src/Console/Scheduling/ChatBotScheduling.php index 5f6405a..261665b 100644 --- a/src/Console/Scheduling/ChatBotScheduling.php +++ b/src/Console/Scheduling/ChatBotScheduling.php @@ -35,11 +35,10 @@ protected function schedule(Event $event): Event public function getJob(Schedule $schedule, $user): ?Event { - if (!$this->valid($user)) { + if (! $this->valid($user)) { return null; } return $this->schedule($schedule->command(SendMessageCommand::class, [$user->id, $this->message()])); } - } diff --git a/src/EventServiceProvider.php b/src/EventServiceProvider.php index 9e636fc..99d3af5 100644 --- a/src/EventServiceProvider.php +++ b/src/EventServiceProvider.php @@ -2,18 +2,21 @@ namespace Redbeed\OpenOverlay; -use \Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use Illuminate\Support\Facades\Event; +use Redbeed\OpenOverlay\Automations\Triggers\TwitchChatMessageTrigger; use Redbeed\OpenOverlay\Events\Twitch\BotTokenExpires; +use Redbeed\OpenOverlay\Events\Twitch\ChatMessageReceived; use Redbeed\OpenOverlay\Events\Twitch\EventReceived; use Redbeed\OpenOverlay\Events\Twitch\RefresherEvent; use Redbeed\OpenOverlay\Events\UserConnectionChanged; +use Redbeed\OpenOverlay\Listeners\AutoShoutOutRaid; use Redbeed\OpenOverlay\Listeners\Twitch\NewFollowerListener; use Redbeed\OpenOverlay\Listeners\Twitch\NewSubscriberListener; -use Redbeed\OpenOverlay\Listeners\AutoShoutOutRaid; +use Redbeed\OpenOverlay\Listeners\Twitch\Refresher\NewConnectionRefresher; use Redbeed\OpenOverlay\Listeners\Twitch\Refresher\StandardRefresher; -use Redbeed\OpenOverlay\Listeners\TwitchSplitReceivedEvents; use Redbeed\OpenOverlay\Listeners\Twitch\UpdateBotToken; -use Redbeed\OpenOverlay\Listeners\Twitch\Refresher\NewConnectionRefresher; +use Redbeed\OpenOverlay\Listeners\TwitchSplitReceivedEvents; use Redbeed\OpenOverlay\Listeners\UpdateUserWebhookCalls; use Redbeed\OpenOverlay\Sociallite\TwitchClientCredentialsExtendSocialite; use SocialiteProviders\Manager\SocialiteWasCalled; @@ -21,15 +24,14 @@ class EventServiceProvider extends ServiceProvider { - protected $listen = [ SocialiteWasCalled::class => [ TwitchExtendSocialite::class, TwitchClientCredentialsExtendSocialite::class, ], - BotTokenExpires::class => [ - UpdateBotToken::class - ] + BotTokenExpires::class => [ + UpdateBotToken::class, + ], ]; public function listens(): array @@ -53,6 +55,10 @@ public function listens(): array $listen[EventReceived::class][] = NewSubscriberListener::class; } + Event::listen(function (ChatMessageReceived $event) { + automation(new TwitchChatMessageTrigger($event->message)); + }); + return $listen; } diff --git a/src/Events/Twitch/ChatMessageReceived.php b/src/Events/Twitch/ChatMessageReceived.php index f3169de..70a3ce0 100644 --- a/src/Events/Twitch/ChatMessageReceived.php +++ b/src/Events/Twitch/ChatMessageReceived.php @@ -2,6 +2,7 @@ namespace Redbeed\OpenOverlay\Events\Twitch; +use function config; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; @@ -11,7 +12,6 @@ use Redbeed\OpenOverlay\Models\Twitch\Emote; use Redbeed\OpenOverlay\Models\User\Connection; use Redbeed\OpenOverlay\Support\ViewerInChat; -use function config; class ChatMessageReceived implements ShouldBroadcastNow { @@ -44,7 +44,7 @@ public function viewerInChatListener() public function broadcastOn(): Channel { - return new Channel('twitch.' . $this->twitchUser->service_user_id); + return new Channel('twitch.'.$this->twitchUser->service_user_id); } public function broadcastAs(): string diff --git a/src/Events/Twitch/StreamOffline.php b/src/Events/Twitch/StreamOffline.php index 243cd0d..97e69ce 100644 --- a/src/Events/Twitch/StreamOffline.php +++ b/src/Events/Twitch/StreamOffline.php @@ -35,7 +35,7 @@ public function __construct(EventSubEvents $twitchEvent) public function broadcastOn(): Channel { - return new Channel('twitch.' . $this->twitchUser->service_user_id); + return new Channel('twitch.'.$this->twitchUser->service_user_id); } public function broadcastAs(): string @@ -47,7 +47,7 @@ public function broadcastWith() { return [ 'started' => $this->streamStarted, - 'ended' => Carbon::now(), + 'ended' => Carbon::now(), ]; } } diff --git a/src/Events/Twitch/StreamOnline.php b/src/Events/Twitch/StreamOnline.php index 79651a8..d97c446 100644 --- a/src/Events/Twitch/StreamOnline.php +++ b/src/Events/Twitch/StreamOnline.php @@ -31,7 +31,7 @@ public function __construct(EventSubEvents $twitchEvent) public function broadcastOn(): Channel { - return new Channel('twitch.' . $this->twitchUser->service_user_id); + return new Channel('twitch.'.$this->twitchUser->service_user_id); } public function broadcastAs(): string diff --git a/src/Events/ViewerEnteredChat.php b/src/Events/ViewerEnteredChat.php index 7eea08d..9d26ce8 100644 --- a/src/Events/ViewerEnteredChat.php +++ b/src/Events/ViewerEnteredChat.php @@ -34,6 +34,6 @@ public function broadcastWith() public function broadcastOn() { - return new Channel('twitch.' . $this->streamer->service_user_id); + return new Channel('twitch.'.$this->streamer->service_user_id); } } diff --git a/src/Exceptions/AutomationFilterNotValid.php b/src/Exceptions/AutomationFilterNotValid.php new file mode 100644 index 0000000..bc15c20 --- /dev/null +++ b/src/Exceptions/AutomationFilterNotValid.php @@ -0,0 +1,7 @@ +get('event'); - if (!empty($event) && in_array($messageType, config('openoverlay.webhook.twitch.subscribe'), true)) { - + if (! empty($event) && in_array($messageType, config('openoverlay.webhook.twitch.subscribe'), true)) { return $this->receiveNotification( $messageId, $messageType, $messageTimestamp, $event ); - } return $request->get('challenge'); @@ -62,6 +60,5 @@ private function receiveNotification(string $eventId, string $eventType, string } return \response('Event received', $newEvent->wasRecentlyCreated ? Response::HTTP_CREATED : Response::HTTP_OK); - } } diff --git a/src/Http/Controllers/Connection/AppTokenController.php b/src/Http/Controllers/Connection/AppTokenController.php index aacc623..0866f4c 100644 --- a/src/Http/Controllers/Connection/AppTokenController.php +++ b/src/Http/Controllers/Connection/AppTokenController.php @@ -4,7 +4,6 @@ class AppTokenController extends SocialiteController { - protected $socialiteDriver = 'twitch_client_credentials'; public function __construct() diff --git a/src/Http/Controllers/Connection/AuthController.php b/src/Http/Controllers/Connection/AuthController.php index 3da4a4a..c2c5c55 100644 --- a/src/Http/Controllers/Connection/AuthController.php +++ b/src/Http/Controllers/Connection/AuthController.php @@ -6,11 +6,9 @@ use Illuminate\Support\Facades\Auth; use Redbeed\OpenOverlay\Events\UserConnectionChanged; use Redbeed\OpenOverlay\Models\User\Connection; -use Redbeed\OpenOverlay\Service\Twitch\UsersClient; class AuthController extends SocialiteController { - protected function callbackUrl(): string { return route('open_overlay.connection.callback'); diff --git a/src/Http/Controllers/Connection/SocialiteController.php b/src/Http/Controllers/Connection/SocialiteController.php index 4f03bc0..9c29c8d 100644 --- a/src/Http/Controllers/Connection/SocialiteController.php +++ b/src/Http/Controllers/Connection/SocialiteController.php @@ -1,6 +1,5 @@ setScopes($this->scopes()); } - protected function scopes(): array { + protected function scopes(): array + { return config('openoverlay.service.twitch.scopes'); } @@ -26,12 +26,11 @@ public function redirect(): RedirectResponse { $callbackUrl = $this->callbackUrl(); - if (!empty($callbackUrl)) { + if (! empty($callbackUrl)) { /** @var RedirectResponse $redirect */ $redirect = $this->socialite()->redirect(); - $redirectUrl = Url::fromString($redirect->getTargetUrl()); $redirectUrl = $redirectUrl->withQueryParameter('redirect_uri', $callbackUrl); diff --git a/src/Listeners/AutoShoutOutRaid.php b/src/Listeners/AutoShoutOutRaid.php index 3274c34..9cc5427 100644 --- a/src/Listeners/AutoShoutOutRaid.php +++ b/src/Listeners/AutoShoutOutRaid.php @@ -23,12 +23,12 @@ public function handle(EventReceived $event) $connection = Connection::where('service_user_id', $event->event->event_user_id) ->first(); - if (!$connection) { + if (! $connection) { return; } $chatMessage = config( - 'openoverlay.modules' . AutoShoutOutRaid::class . 'message', + 'openoverlay.modules'.AutoShoutOutRaid::class.'message', 'Follow :username over at :twitchUrl. They were last playing :gameName' ); @@ -40,7 +40,7 @@ public function handle(EventReceived $event) $channels = $channelClient->get($eventData['from_broadcaster_user_id']); $channel = head($channels['data']); - if (!empty($channel['game_id'])) { + if (! empty($channel['game_id'])) { $gameName = $channel['game_name']; } } catch (ClientException $exception) { @@ -52,7 +52,7 @@ public function handle(EventReceived $event) 'userId' => $connection->user->id, 'message' => __($chatMessage, [ 'username' => $eventData['from_broadcaster_user_name'], - 'twitchUrl' => 'https://www.twitch.tv/' . $eventData['from_broadcaster_user_login'], + 'twitchUrl' => 'https://www.twitch.tv/'.$eventData['from_broadcaster_user_login'], 'gameName' => $gameName, ]), ]); diff --git a/src/Listeners/Twitch/EventListener.php b/src/Listeners/Twitch/EventListener.php index 5667ea4..6512454 100644 --- a/src/Listeners/Twitch/EventListener.php +++ b/src/Listeners/Twitch/EventListener.php @@ -7,7 +7,6 @@ abstract class EventListener implements ShouldQueue { - protected function eventType(): string { return 'event.type'; // https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types @@ -27,6 +26,5 @@ public function handle(EventReceived $event) $this->handleEvent($event); } - abstract public function handleEvent(EventReceived $event): void; } diff --git a/src/Listeners/Twitch/NewFollowerListener.php b/src/Listeners/Twitch/NewFollowerListener.php index d5aefba..7d698f1 100644 --- a/src/Listeners/Twitch/NewFollowerListener.php +++ b/src/Listeners/Twitch/NewFollowerListener.php @@ -5,11 +5,9 @@ use Carbon\Carbon; use Redbeed\OpenOverlay\Events\Twitch\EventReceived; use Redbeed\OpenOverlay\Models\Twitch\UserFollowers; -use Redbeed\OpenOverlay\Models\Twitch\UserSubscriber; class NewFollowerListener extends EventListener { - protected function eventType(): string { return 'channel.follow'; @@ -26,11 +24,11 @@ public function handleEvent(EventReceived $event): void if (empty($followerModal)) { UserFollowers::create([ - 'twitch_user_id' => $event->event->event_user_id, - 'follower_user_id' => $followerData['user_id'], + 'twitch_user_id' => $event->event->event_user_id, + 'follower_user_id' => $followerData['user_id'], 'follower_username' => $followerData['user_name'], - 'followed_at' => Carbon::parse($followerData['followed_at']), - 'deleted_at' => null, + 'followed_at' => Carbon::parse($followerData['followed_at']), + 'deleted_at' => null, ]); return; diff --git a/src/Listeners/Twitch/NewSubscriberListener.php b/src/Listeners/Twitch/NewSubscriberListener.php index c05ea9c..7586b1e 100644 --- a/src/Listeners/Twitch/NewSubscriberListener.php +++ b/src/Listeners/Twitch/NewSubscriberListener.php @@ -17,13 +17,13 @@ public function handleEvent(EventReceived $event): void $subscriberData = $event->event->event_data; UserSubscriber::firstOrCreate([ - 'twitch_user_id' => $event->event->event_user_id, + 'twitch_user_id' => $event->event->event_user_id, 'subscriber_user_id' => $subscriberData['user_id'], ], [ 'subscriber_username' => $subscriberData['user_name'], - 'tier' => $subscriberData['user_name'], - 'tier_name' => $subscriberData['plan_name'], - 'is_gift' => $subscriberData['is_gift'], + 'tier' => $subscriberData['user_name'], + 'tier_name' => $subscriberData['plan_name'] ?? '', + 'is_gift' => $subscriberData['is_gift'], ]); } } diff --git a/src/Listeners/Twitch/Refresher/LoginRefresher.php b/src/Listeners/Twitch/Refresher/LoginRefresher.php index 88a9efb..c0d8e44 100644 --- a/src/Listeners/Twitch/Refresher/LoginRefresher.php +++ b/src/Listeners/Twitch/Refresher/LoginRefresher.php @@ -2,6 +2,7 @@ namespace Redbeed\OpenOverlay\Listeners\Twitch\Refresher; +use Exception; use Illuminate\Auth\Events\Login; use Illuminate\Contracts\Queue\ShouldQueue; use Redbeed\OpenOverlay\Exceptions\WrongConnectionTypeException; @@ -27,7 +28,13 @@ public function handle(Login $event) } if (parent::saveSubscriber()) { - $this->refreshSubscriber($twitchConnection); + try { + $this->refreshSubscriber($twitchConnection); + } catch (Exception $e) { + // ignore exception as it is not critical + // user auth token is not valid + report($e); + } } } } diff --git a/src/Listeners/Twitch/Refresher/Refresher.php b/src/Listeners/Twitch/Refresher/Refresher.php index 4e122bc..3ad0d88 100644 --- a/src/Listeners/Twitch/Refresher/Refresher.php +++ b/src/Listeners/Twitch/Refresher/Refresher.php @@ -3,6 +3,7 @@ namespace Redbeed\OpenOverlay\Listeners\Twitch\Refresher; use Carbon\Carbon; +use function head; use Illuminate\Support\Arr; use Redbeed\OpenOverlay\Exceptions\WrongConnectionTypeException; use Redbeed\OpenOverlay\Models\Twitch\UserFollowers; @@ -10,7 +11,6 @@ use Redbeed\OpenOverlay\Models\User\Connection; use Redbeed\OpenOverlay\Service\Twitch\SubscriptionsClient; use Redbeed\OpenOverlay\Service\Twitch\UsersClient; -use function head; abstract class Refresher { @@ -19,7 +19,7 @@ public static function saveFollowers(): bool return config('openoverlay.service.twitch.save.follower', false) === true; } - public static function saveSubscriber(): bool + public static function saveSubscriber(): bool { return config('openoverlay.service.twitch.save.subscriber', false) === true; } @@ -49,11 +49,11 @@ protected function refreshFollowers(Connection $twitchConnection) if ($followerModal === null) { UserFollowers::create([ - 'twitch_user_id' => $twitchConnection->service_user_id, - 'follower_user_id' => $followerData['from_id'], + 'twitch_user_id' => $twitchConnection->service_user_id, + 'follower_user_id' => $followerData['from_id'], 'follower_username' => $followerData['from_name'], - 'followed_at' => Carbon::parse($followerData['followed_at']), - 'deleted_at' => null, + 'followed_at' => Carbon::parse($followerData['followed_at']), + 'deleted_at' => null, ]); continue; @@ -107,16 +107,16 @@ protected function refreshSubscriber(Connection $twitchConnection) UserSubscriber::create([ 'twitch_user_id' => $twitchConnection->service_user_id, - 'subscriber_user_id' => $subscriberData['user_id'], - 'subscriber_username' => $subscriberData['user_name'], + 'subscriber_user_id' => $subscriberData['user_id'], + 'subscriber_username' => $subscriberData['user_name'], 'subscriber_login_name' => $subscriberData['user_login'], - 'tier' => $subscriberData['tier'], + 'tier' => $subscriberData['tier'], 'tier_name' => $subscriberData['plan_name'], - 'is_gift' => $subscriberData['is_gift'], - 'gifter_user_id' => $subscriberData['gifter_id'], - 'gifter_username' => $subscriberData['gifter_name'], + 'is_gift' => $subscriberData['is_gift'], + 'gifter_user_id' => $subscriberData['gifter_id'], + 'gifter_username' => $subscriberData['gifter_name'], 'gifter_login_name' => $subscriberData['gifter_login'], ]); @@ -149,7 +149,7 @@ protected function refreshSubscriber(Connection $twitchConnection) private function twitchUser(string $broadcasterId): array { $userClient = new UsersClient(); + return head(Arr::get($userClient->byId($broadcasterId), 'data', [])); } - } diff --git a/src/Listeners/Twitch/Refresher/StandardRefresher.php b/src/Listeners/Twitch/Refresher/StandardRefresher.php index a30efcc..235db64 100644 --- a/src/Listeners/Twitch/Refresher/StandardRefresher.php +++ b/src/Listeners/Twitch/Refresher/StandardRefresher.php @@ -2,6 +2,7 @@ namespace Redbeed\OpenOverlay\Listeners\Twitch\Refresher; +use GuzzleHttp\Exception\ClientException; use Illuminate\Contracts\Queue\ShouldQueue; use Redbeed\OpenOverlay\Events\Twitch\RefresherEvent; use Redbeed\OpenOverlay\Exceptions\WrongConnectionTypeException; @@ -22,7 +23,13 @@ public function handle(RefresherEvent $event) } if (parent::saveSubscriber()) { - $this->refreshSubscriber($event->twitchConnection); + try { + $this->refreshSubscriber($event->twitchConnection); + } catch (ClientException $e) { + // ignore exception as it is not critical + // user auth token is not valid + report($e); + } } } } diff --git a/src/Listeners/Twitch/UpdateBotToken.php b/src/Listeners/Twitch/UpdateBotToken.php index d1e269b..d75acbe 100644 --- a/src/Listeners/Twitch/UpdateBotToken.php +++ b/src/Listeners/Twitch/UpdateBotToken.php @@ -20,7 +20,7 @@ public function handle(BotTokenExpires $event) $client = AuthClient::http(); $response = $client->refreshToken($event->botModel->service_refresh_token); } catch (\Exception $exception) { - Log::error("Bot Connection deleted"); + Log::error('Bot Connection deleted'); Log::error($exception); return; diff --git a/src/Listeners/TwitchSplitReceivedEvents.php b/src/Listeners/TwitchSplitReceivedEvents.php index fbb4a46..2543d58 100644 --- a/src/Listeners/TwitchSplitReceivedEvents.php +++ b/src/Listeners/TwitchSplitReceivedEvents.php @@ -12,11 +12,13 @@ public function handle(EventReceived $twitchEvent) { if ($twitchEvent->event->event_type === 'stream.online') { broadcast(new StreamOnline($twitchEvent->event)); + return; } if ($twitchEvent->event->event_type === 'stream.offline') { broadcast(new StreamOffline($twitchEvent->event)); + return; } } diff --git a/src/Models/BotConnection.php b/src/Models/BotConnection.php index 1f43a4a..1efa4ea 100644 --- a/src/Models/BotConnection.php +++ b/src/Models/BotConnection.php @@ -42,11 +42,13 @@ public function setServiceRefreshTokenAttribute($value): void $this->attributes['service_refresh_token'] = $this->encryptString($value); } - private function decryptString(string $key): string { + private function decryptString(string $key): string + { return Crypt::decryptString($key); } - private function encryptString(string $value): string { + private function encryptString(string $value): string + { return Crypt::encryptString($value); } diff --git a/src/Models/Twitch/Emote.php b/src/Models/Twitch/Emote.php index 0afcf7f..56801ef 100644 --- a/src/Models/Twitch/Emote.php +++ b/src/Models/Twitch/Emote.php @@ -5,7 +5,9 @@ class Emote { const IMAGE_SIZE_SM = 'url_1x'; + const IMAGE_SIZE_MD = 'url_2x'; + const IMAGE_SIZE_LG = 'url_4x'; /** @var string */ @@ -34,15 +36,15 @@ public static function fromJson(array $emoteData): Emote $emote->name = $emoteData['name']; $emote->images = $emoteData['images']; - if (!empty($emoteData['tier'])) { + if (! empty($emoteData['tier'])) { $emote->tier = $emoteData['tier']; } - if (!empty($emoteData['emote_type'])) { + if (! empty($emoteData['emote_type'])) { $emote->emoteType = $emoteData['emote_type']; } - if (!empty($emoteData['emote_set_id'])) { + if (! empty($emoteData['emote_set_id'])) { $emote->emoteSetId = $emoteData['emote_set_id']; } @@ -53,5 +55,4 @@ public function image(string $size = Emote::IMAGE_SIZE_MD): string { return $this->images[$size]; } - } diff --git a/src/Models/Twitch/EventSubEvents.php b/src/Models/Twitch/EventSubEvents.php index c747b40..a4c58e8 100644 --- a/src/Models/Twitch/EventSubEvents.php +++ b/src/Models/Twitch/EventSubEvents.php @@ -5,8 +5,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Redbeed\OpenOverlay\Database\Factories\EventSubEventsFactory; -use Redbeed\OpenOverlay\Models\User\Connection; -use Redbeed\OpenOverlay\OpenOverlay; class EventSubEvents extends Model { diff --git a/src/Models/Twitch/EventSubscription.php b/src/Models/Twitch/EventSubscription.php index 44faaef..ef263b3 100644 --- a/src/Models/Twitch/EventSubscription.php +++ b/src/Models/Twitch/EventSubscription.php @@ -1,9 +1,7 @@ attributes['service_refresh_token'] = $this->encryptString($value); } - private function decryptString(string $key): string { + private function decryptString(string $key): string + { return Crypt::decryptString($key); } - private function encryptString(string $value): string { + private function encryptString(string $value): string + { return Crypt::encryptString($value); } diff --git a/src/OpenOverlay.php b/src/OpenOverlay.php index 1d76f05..db72a1e 100644 --- a/src/OpenOverlay.php +++ b/src/OpenOverlay.php @@ -2,10 +2,8 @@ namespace Redbeed\OpenOverlay; - class OpenOverlay { - public static function userModel(): string { return config('auth.providers.users.model'); diff --git a/src/OpenOverlayServiceProvider.php b/src/OpenOverlayServiceProvider.php index db6b249..dc2b513 100644 --- a/src/OpenOverlayServiceProvider.php +++ b/src/OpenOverlayServiceProvider.php @@ -4,6 +4,8 @@ use Illuminate\Console\Scheduling\Schedule; use Illuminate\Support\ServiceProvider; +use Redbeed\OpenOverlay\Automations\AutomationsServiceProvider; +use Redbeed\OpenOverlay\Console\Commands\Twitch\OnlineStatusCommand; use Redbeed\OpenOverlay\Console\ConsoleServiceProvider; use Redbeed\OpenOverlay\Console\Scheduling\ChatBotScheduling; use Redbeed\OpenOverlay\Models\BotConnection; @@ -17,16 +19,21 @@ class OpenOverlayServiceProvider extends ServiceProvider */ public function boot(): void { - // $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'redbeed'); - // $this->loadViewsFrom(__DIR__.'/../resources/views', 'redbeed'); - - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); - $this->loadRoutesFrom(__DIR__ . '/../routes/openoverlay.php'); + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); + $this->loadRoutesFrom(__DIR__.'/../routes/openoverlay.php'); // Publishing is only necessary when using the CLI. if ($this->app->runningInConsole()) { $this->bootForConsole(); } + + // Register schedule + $this->callAfterResolving(Schedule::class, function (Schedule $schedule) { + // check streamer status every hour + $schedule + ->command(OnlineStatusCommand::class, ['--all' => true])->hourly() + ->withoutOverlapping(); + }); } /** @@ -38,8 +45,9 @@ public function register(): void { $this->app->register(EventServiceProvider::class); $this->app->register(ConsoleServiceProvider::class); + $this->app->register(AutomationsServiceProvider::class); - $this->mergeConfigFrom(__DIR__ . '/../config/openoverlay.php', 'openoverlay'); + $this->mergeConfigFrom(__DIR__.'/../config/openoverlay.php', 'openoverlay'); // Register the service the package provides. $this->app->singleton('openoverlay', function ($app) { @@ -57,7 +65,6 @@ public function provides() return ['openoverlay']; } - /** * Console-specific booting. * @@ -67,7 +74,7 @@ protected function bootForConsole(): void { // Publishing the configuration file. $this->publishes([ - __DIR__ . '/../config/openoverlay.php' => config_path('openoverlay.php'), + __DIR__.'/../config/openoverlay.php' => config_path('openoverlay.php'), ], 'openoverlay.config'); $this->callAfterResolving(Schedule::class, function (Schedule $schedule) { @@ -85,9 +92,7 @@ private function registerSchedule(Schedule $schedule): void foreach ($bots as $bot) { foreach ($bot->users as $user) { foreach ($scheduledMessages as $message) { - (new $message())->getJob($schedule, $user); - } } } diff --git a/src/Service/Twitch/ApiClient.php b/src/Service/Twitch/ApiClient.php index d217348..e6350de 100644 --- a/src/Service/Twitch/ApiClient.php +++ b/src/Service/Twitch/ApiClient.php @@ -26,7 +26,7 @@ public function __construct() RequestOptions::HEADERS => [ 'Client-ID' => $clientId, 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . $authCode, + 'Authorization' => 'Bearer '.$authCode, ], ]); } @@ -41,6 +41,7 @@ public static function http() /** * @return static + * * @throws AppTokenMissing */ public function addAppToken() @@ -53,28 +54,26 @@ public function addAppToken() return $this->withOptions([ RequestOptions::HEADERS => [ - 'Authorization' => 'Bearer ' . $appToken, + 'Authorization' => 'Bearer '.$appToken, ], ]); } /** - * @param string $appToken - * + * @param string $appToken * @return static */ public function withAppToken(string $appToken) { return $this->setOptions([ RequestOptions::HEADERS => [ - 'Authorization' => 'Bearer ' . $appToken, + 'Authorization' => 'Bearer '.$appToken, ], ]); } /** - * @param array $options - * + * @param array $options * @return static */ public function withOptions(array $options) @@ -86,8 +85,7 @@ public function withOptions(array $options) } /** - * @param array $options - * + * @param array $options * @return static */ public function setOptions(array $options): self @@ -99,16 +97,16 @@ public function setOptions(array $options): self } /** - * @param string $method - * @param string $url - * + * @param string $method + * @param string $url * @return array + * * @throws \GuzzleHttp\Exception\GuzzleException */ public function request(string $method, string $url) { $response = $this->httpClient->request($method, $url, $this->options); - $json = (string)$response->getBody(); + $json = (string) $response->getBody(); return json_decode($json, true); } diff --git a/src/Service/Twitch/ChannelsClient.php b/src/Service/Twitch/ChannelsClient.php index 2a007b4..ff34f92 100644 --- a/src/Service/Twitch/ChannelsClient.php +++ b/src/Service/Twitch/ChannelsClient.php @@ -17,4 +17,12 @@ public function get(string $broadcasterId): array ]) ->request('GET', 'channels'); } + + public function lastGame(string $broadcasterId) + { + $channels = (new self)->get($broadcasterId); + $channel = head($channels['data']); + + return $channel['game_name']; + } } diff --git a/src/Service/Twitch/ChatEmotesClient.php b/src/Service/Twitch/ChatEmotesClient.php index 02e1947..7187e33 100644 --- a/src/Service/Twitch/ChatEmotesClient.php +++ b/src/Service/Twitch/ChatEmotesClient.php @@ -2,20 +2,18 @@ namespace Redbeed\OpenOverlay\Service\Twitch; -use Exception; -use GuzzleHttp\Exception\ClientException; use GuzzleHttp\RequestOptions; use Redbeed\OpenOverlay\Exceptions\TwitchEmoteSetIdException; use Redbeed\OpenOverlay\Models\Twitch\Emote; class ChatEmotesClient extends ApiClient { - const MAX_SET_ID = 25; /** - * @param string $broadcasterId + * @param string $broadcasterId * @return Emote[] + * * @throws \GuzzleHttp\Exception\GuzzleException * @throws \Redbeed\OpenOverlay\Exceptions\AppTokenMissing */ @@ -38,8 +36,9 @@ public function get(string $broadcasterId): array } /** - * @param string $broadcasterId + * @param string $broadcasterId * @return Emote[] + * * @throws \GuzzleHttp\Exception\GuzzleException * @throws \Redbeed\OpenOverlay\Exceptions\AppTokenMissing */ @@ -57,8 +56,9 @@ public function global(): array } /** - * @param int $setId + * @param int $setId * @return array + * * @throws TwitchEmoteSetIdException * @throws \GuzzleHttp\Exception\GuzzleException * @throws \Redbeed\OpenOverlay\Exceptions\AppTokenMissing @@ -66,7 +66,7 @@ public function global(): array public function set(int $setId): array { if ($setId > ChatEmotesClient::MAX_SET_ID || $setId < 1) { - throw new TwitchEmoteSetIdException('Set Id minimum: 1 / maximum: ' . ChatEmotesClient::MAX_SET_ID); + throw new TwitchEmoteSetIdException('Set Id minimum: 1 / maximum: '.ChatEmotesClient::MAX_SET_ID); } $json = $this @@ -91,7 +91,6 @@ public function allSets(): array $bulkSize = 10; foreach (range(1, (ChatEmotesClient::MAX_SET_ID / $bulkSize)) as $bulk) { - $to = min(($bulkSize * $bulk), ChatEmotesClient::MAX_SET_ID); $from = ($bulkSize * $bulk) - 9; diff --git a/src/Service/Twitch/DateTime.php b/src/Service/Twitch/DateTime.php index ed52186..00cf663 100644 --- a/src/Service/Twitch/DateTime.php +++ b/src/Service/Twitch/DateTime.php @@ -9,7 +9,7 @@ class DateTime public static function parse($dateString): Carbon { // twitch timestamps sometimes (randomly) to long - $timestamp = substr(trim($dateString, 'Z'), 0, 23) . 'Z'; + $timestamp = substr(trim($dateString, 'Z'), 0, 23).'Z'; return Carbon::createFromFormat(\DateTime::RFC3339_EXTENDED, $timestamp); } diff --git a/src/Service/Twitch/EventSubClient.php b/src/Service/Twitch/EventSubClient.php index 42b43e1..efb2a26 100644 --- a/src/Service/Twitch/EventSubClient.php +++ b/src/Service/Twitch/EventSubClient.php @@ -16,6 +16,7 @@ class EventSubClient extends ApiClient /** * @return EventSubClient + * * @throws AppTokenMissing */ public static function http() @@ -28,7 +29,7 @@ public static function http() return (new self())->setOptions([ RequestOptions::HEADERS => [ - 'Authorization' => 'Bearer ' . $appToken, + 'Authorization' => 'Bearer '.$appToken, ], ]); } @@ -38,14 +39,13 @@ public static function verifySignature( string $messageId, string $messageTimestamp, string $requestBody - ): bool - { + ): bool { if (empty($messageId) || empty($messageSignature) || empty($messageTimestamp) || empty($requestBody)) { throw new WebhookTwitchSignatureMissing('Twitch Eventsub Header infomation missing'); } - $message = $messageId . $messageTimestamp . $requestBody; - $hash = 'sha256=' . hash_hmac('sha256', $message, config('openoverlay.webhook.twitch.secret')); + $message = $messageId.$messageTimestamp.$requestBody; + $hash = 'sha256='.hash_hmac('sha256', $message, config('openoverlay.webhook.twitch.secret')); return $hash === $messageSignature; } @@ -70,7 +70,7 @@ public function subscribe(string $type, string $webhookCallback, array $conditio 'condition' => $condition, 'transport' => [ 'method' => 'webhook', - 'callback' => $webhookCallback . '?' . time(), + 'callback' => $webhookCallback.'?'.time(), 'secret' => $secret, ], ], @@ -84,16 +84,15 @@ public function deleteSubByBroadcasterId(string $broadcasterUserId) ->subscriptions() ->filter(function ($subscription) use ($broadcasterUserId) { /** @var EventSubscription $subscription */ - if (empty($subscription->condition)) { return false; } - if (!empty($subscription->condition['broadcaster_user_id']) && $subscription->condition['broadcaster_user_id'] !== $broadcasterUserId) { + if (! empty($subscription->condition['broadcaster_user_id']) && $subscription->condition['broadcaster_user_id'] !== $broadcasterUserId) { return false; } - if (!empty($subscription->condition['to_broadcaster_user_id']) && $subscription->condition['to_broadcaster_user_id'] !== $broadcasterUserId) { + if (! empty($subscription->condition['to_broadcaster_user_id']) && $subscription->condition['to_broadcaster_user_id'] !== $broadcasterUserId) { return false; } diff --git a/src/Service/Twitch/UsersClient.php b/src/Service/Twitch/UsersClient.php index bcbc944..e2c6ac7 100644 --- a/src/Service/Twitch/UsersClient.php +++ b/src/Service/Twitch/UsersClient.php @@ -2,11 +2,17 @@ namespace Redbeed\OpenOverlay\Service\Twitch; -use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\RequestOptions; +use Illuminate\Support\Arr; +use Redbeed\OpenOverlay\Exceptions\AppTokenMissing; class UsersClient extends ApiClient { + /** + * @throws AppTokenMissing + * @throws GuzzleException + */ public function byId(string $id): array { return $this @@ -19,6 +25,10 @@ public function byId(string $id): array ->request('GET', 'users'); } + /** + * @throws AppTokenMissing + * @throws GuzzleException + */ public function byUsername(string $username): array { return $this @@ -31,6 +41,10 @@ public function byUsername(string $username): array ->request('GET', 'users'); } + /** + * @throws AppTokenMissing + * @throws GuzzleException + */ public function followers(string $twitchUserId): array { return $this @@ -43,6 +57,10 @@ public function followers(string $twitchUserId): array ->request('GET', 'users/follows'); } + /** + * @throws AppTokenMissing + * @throws GuzzleException + */ public function allFollowers(string $twitchUserId): array { $firstResponse = $this @@ -78,4 +96,23 @@ public function allFollowers(string $twitchUserId): array return $firstResponse; } + + /** + * @throws AppTokenMissing + * @throws GuzzleException + */ + public static function lastGame(string $username): string + { + $users = (new self)->byUsername($username); + if (empty($users['data']) || count($users['data']) === 0) { + return ''; + } + + $user = Arr::first($users['data']); + if (empty($user) || empty($user['id'])) { + return ''; + } + + return (new ChannelsClient())->lastGame($user['id']); + } } diff --git a/src/Sociallite/TwitchClientCredentialsExtendSocialite.php b/src/Sociallite/TwitchClientCredentialsExtendSocialite.php index 227b3df..2d3d382 100644 --- a/src/Sociallite/TwitchClientCredentialsExtendSocialite.php +++ b/src/Sociallite/TwitchClientCredentialsExtendSocialite.php @@ -9,7 +9,7 @@ class TwitchClientCredentialsExtendSocialite /** * Register the provider. * - * @param \SocialiteProviders\Manager\SocialiteWasCalled $socialiteWasCalled + * @param \SocialiteProviders\Manager\SocialiteWasCalled $socialiteWasCalled */ public function handle(SocialiteWasCalled $socialiteWasCalled) { diff --git a/src/Support/Facades/Automation.php b/src/Support/Facades/Automation.php new file mode 100644 index 0000000..fe0ed5b --- /dev/null +++ b/src/Support/Facades/Automation.php @@ -0,0 +1,17 @@ +service_user_id . '.viewer.' . StreamerOnline::onlineTime($streamer->service_user_id, $platform); + return $platform.'.streamer.'.$streamer->service_user_id.'.viewer.'.StreamerOnline::onlineTime($streamer->service_user_id, $platform); } public static function list(Connection $streamer, string $platform = 'twitch'): array diff --git a/src/Support/helpers.php b/src/Support/helpers.php new file mode 100644 index 0000000..aed3c3b --- /dev/null +++ b/src/Support/helpers.php @@ -0,0 +1,14 @@ +trigger(...$args); + } +}