diff --git a/CHANGELOG.md b/CHANGELOG.md index 756aa81f..f0f1d760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,13 @@ ### [v0.40 (X.X.XXXX)] +#### Added +- `BotActionHandledEvent` and `BotActionFailedEvent`. + #### Changed - Add index to column `bot_actions.handler` +- Moved logic for executing action handlers to `ProcessMessageTrigger` action class. +- When a bot handler fails / throws exception, the exception will not be reported. The action and exception will be dispatched in the new `BotActionFailedEvent` that can be listened to by the end user. --- diff --git a/src/Actions/Bots/ProcessMessageTriggers.php b/src/Actions/Bots/ProcessMessageTriggers.php index 4558cb62..e66b7e70 100644 --- a/src/Actions/Bots/ProcessMessageTriggers.php +++ b/src/Actions/Bots/ProcessMessageTriggers.php @@ -5,6 +5,8 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Collection; use RTippin\Messenger\Actions\BaseMessengerAction; +use RTippin\Messenger\Events\BotActionFailedEvent; +use RTippin\Messenger\Events\BotActionHandledEvent; use RTippin\Messenger\Exceptions\FeatureDisabledException; use RTippin\Messenger\Messenger; use RTippin\Messenger\MessengerBots; @@ -72,7 +74,8 @@ public function __construct(Messenger $messenger, } /** - * Create a new thread bot! + * Process all matching actions that the message + * body matches through the action triggers. * * @param mixed ...$parameters * @param Thread[0] @@ -119,7 +122,8 @@ private function matchActionTriggers(BotAction $action): void /** * Check if we should execute the actions handler. When executing, * set the proper data into the handler, and start the actions - * cooldown, if any. + * cooldown, if any. Fire events when the action is handled + * or failed. * * @param BotAction $action * @param string $trigger @@ -134,14 +138,12 @@ private function handleAction(BotAction $action, string $trigger): void ->initializeHandler($action->handler) ->setAction($action) ->setThread($this->getThread()) - ->setMessage( - $this->getMessage(), - $trigger, - $this->senderIp - ) + ->setMessage($this->getMessage(), $trigger, $this->senderIp) ->handle(); + + $this->fireHandledEvent($action, $trigger); } catch (Throwable $e) { - report($e); + $this->fireFailedEvent($action, $e); } $this->botActionEnding($action); @@ -226,4 +228,33 @@ private function startTriggeredBotCooldowns(): void ->unique('id') ->each(fn (Bot $bot) => $bot->startCooldown()); } + + /** + * @param BotAction $action + * @param string $trigger + */ + private function fireHandledEvent(BotAction $action, string $trigger): void + { + if ($this->shouldFireEvents()) { + $this->dispatcher->dispatch(new BotActionHandledEvent( + $action, + $this->getMessage(true), + $trigger + )); + } + } + + /** + * @param BotAction $action + * @param Throwable $exception + */ + private function fireFailedEvent(BotAction $action, Throwable $exception): void + { + if ($this->shouldFireEvents()) { + $this->dispatcher->dispatch(new BotActionFailedEvent( + $action, + $exception + )); + } + } } diff --git a/src/Events/BotActionFailedEvent.php b/src/Events/BotActionFailedEvent.php new file mode 100644 index 00000000..b25c8c57 --- /dev/null +++ b/src/Events/BotActionFailedEvent.php @@ -0,0 +1,34 @@ +action = $action; + $this->exception = $exception; + } +} diff --git a/src/Events/BotActionHandledEvent.php b/src/Events/BotActionHandledEvent.php new file mode 100644 index 00000000..d5c435e7 --- /dev/null +++ b/src/Events/BotActionHandledEvent.php @@ -0,0 +1,43 @@ +action = $action; + $this->message = $message; + $this->trigger = $trigger; + } +} diff --git a/tests/Actions/ProcessMessageTriggersTest.php b/tests/Actions/ProcessMessageTriggersTest.php new file mode 100644 index 00000000..cee31985 --- /dev/null +++ b/tests/Actions/ProcessMessageTriggersTest.php @@ -0,0 +1,336 @@ +group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(); + + $this->expectException(FeatureDisabledException::class); + $this->expectExceptionMessage('Bots are currently disabled.'); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + } + + /** @test */ + public function it_executes_handle() + { + MessengerBots::setHandlers([FunBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertDatabaseHas('messages', [ + 'body' => 'Testing Fun.' + ]); + } + + /** @test */ + public function it_executes_handle_if_admin() + { + MessengerBots::setHandlers([FunBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->admin() + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertDatabaseHas('messages', [ + 'body' => 'Testing Fun.' + ]); + } + + /** @test */ + public function it_doesnt_execute_handle_if_not_admin() + { + MessengerBots::setHandlers([FunBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->admin() + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, false); + + $this->assertDatabaseMissing('messages', [ + 'body' => 'Testing Fun.' + ]); + } + + /** @test */ + public function it_forwards_sender_ip_to_handler() + { + MessengerBots::setHandlers([SillyBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(SillyBotHandler::class) + ->triggers('!test') + ->admin() + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true, '127.0.0.1'); + + $this->assertDatabaseHas('messages', [ + 'body' => 'Testing Silly. 127.0.0.1' + ]); + } + + /** @test */ + public function it_sets_action_and_bot_cooldowns() + { + MessengerBots::setHandlers([FunBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $bot = Bot::factory()->for($thread)->owner($this->tippin)->create(['cooldown' => 30]); + $action = BotAction::factory() + ->for($bot) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->create(['cooldown' => 30]); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertTrue(Cache::has("bot:$bot->id:cooldown")); + $this->assertTrue(Cache::has("bot:$bot->id:$action->id:cooldown")); + } + + /** @test */ + public function it_can_release_action_cooldown() + { + MessengerBots::setHandlers([SillyBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $bot = Bot::factory()->for($thread)->owner($this->tippin)->create(['cooldown' => 30]); + $action = BotAction::factory() + ->for($bot) + ->owner($this->tippin) + ->handler(SillyBotHandler::class) + ->triggers('!test') + ->create(['cooldown' => 30]); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertTrue(Cache::has("bot:$bot->id:cooldown")); + $this->assertFalse(Cache::has("bot:$bot->id:$action->id:cooldown")); + } + + /** @test */ + public function it_executes_multiple_handles() + { + MessengerBots::setHandlers([ + FunBotHandler::class, + SillyBotHandler::class, + ]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $bot = Bot::factory()->for($thread)->owner($this->tippin)->create(); + BotAction::factory() + ->for($bot) + ->owner($this->tippin) + ->state(new Sequence( + ['handler' => FunBotHandler::class], + ['handler' => SillyBotHandler::class], + )) + ->triggers('!test') + ->count(2) + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertDatabaseCount('messages', 3); + } + + /** @test */ + public function it_does_nothing_if_bot_on_cooldown() + { + MessengerBots::setHandlers([FunBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $bot = Bot::factory()->for($thread)->owner($this->tippin)->create(); + BotAction::factory() + ->for($bot) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->create(); + Cache::put("bot:$bot->id:cooldown", true, now()->addSeconds(30)); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertDatabaseCount('messages', 1); + } + + /** @test */ + public function it_does_nothing_if_action_on_cooldown() + { + MessengerBots::setHandlers([FunBotHandler::class]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $action = BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->create(); + Cache::put("bot:$action->bot_id:$action->id:cooldown", true, now()->addSeconds(30)); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertDatabaseCount('messages', 1); + } + + /** @test */ + public function it_does_nothing_if_no_valid_handlers_found() + { + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->triggers('!test') + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + $this->assertDatabaseCount('messages', 1); + } + + /** @test */ + public function it_fires_handled_event() + { + BaseMessengerAction::enableEvents(); + MessengerBots::setHandlers([FunBotHandler::class]); + Event::fake([ + BotActionHandledEvent::class, + BotActionFailedEvent::class, + ]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $action = BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(FunBotHandler::class) + ->triggers('!test') + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + Event::assertNotDispatched(BotActionFailedEvent::class); + Event::assertDispatched(function (BotActionHandledEvent $event) use ($action, $message) { + $this->assertSame($action->id, $event->action->id); + $this->assertSame($message->id, $event->message->id); + $this->assertSame('!test', $event->trigger); + + return true; + }); + } + + /** @test */ + public function it_fires_multiple_event() + { + BaseMessengerAction::enableEvents(); + MessengerBots::setHandlers([ + FunBotHandler::class, + SillyBotHandler::class, + BrokenBotHandler::class, + ]); + Event::fake([ + BotActionHandledEvent::class, + BotActionFailedEvent::class, + ]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $bot = Bot::factory()->for($thread)->owner($this->tippin)->create(); + BotAction::factory() + ->for($bot) + ->owner($this->tippin) + ->state(new Sequence( + ['handler' => FunBotHandler::class], + ['handler' => SillyBotHandler::class], + ['handler' => BrokenBotHandler::class], + )) + ->triggers('!test') + ->count(3) + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + Event::assertDispatchedTimes(BotActionHandledEvent::class, 2); + Event::assertDispatchedTimes(BotActionFailedEvent::class, 1); + } + + /** @test */ + public function it_fires_failed_event_if_handler_throws_exception() + { + BaseMessengerAction::enableEvents(); + MessengerBots::setHandlers([BrokenBotHandler::class]); + Event::fake([ + BotActionHandledEvent::class, + BotActionFailedEvent::class, + ]); + $thread = Thread::factory()->group()->create(); + $message = Message::factory()->for($thread)->owner($this->tippin)->create(['body' => '!test']); + $action = BotAction::factory() + ->for(Bot::factory()->for($thread)->owner($this->tippin)->create()) + ->owner($this->tippin) + ->handler(BrokenBotHandler::class) + ->triggers('!test') + ->create(); + + app(ProcessMessageTriggers::class)->execute($thread, $message, true); + + Event::assertNotDispatched(BotActionHandledEvent::class); + Event::assertDispatched(function (BotActionFailedEvent $event) use ($action) { + $this->assertSame($action->id, $event->action->id); + $this->assertInstanceOf(BotException::class, $event->exception); + + return true; + }); + } +} diff --git a/tests/Fixtures/BrokenBotHandler.php b/tests/Fixtures/BrokenBotHandler.php new file mode 100644 index 00000000..58706a9a --- /dev/null +++ b/tests/Fixtures/BrokenBotHandler.php @@ -0,0 +1,24 @@ + 'broken_bot', + 'description' => 'This is a broken bot.', + 'name' => 'Broken Bot', + 'unique' => true, + ]; + } + + public function handle(): void + { + throw new BotException('Busted.'); + } +} diff --git a/tests/Fixtures/FunBotHandler.php b/tests/Fixtures/FunBotHandler.php index 1da8e70e..b7b34033 100644 --- a/tests/Fixtures/FunBotHandler.php +++ b/tests/Fixtures/FunBotHandler.php @@ -19,7 +19,7 @@ public static function getSettings(): array public function handle(): void { - // + $this->composer()->message('Testing Fun.'); } public function rules(): array diff --git a/tests/Fixtures/SillyBotHandler.php b/tests/Fixtures/SillyBotHandler.php index 29bf77e2..8781263a 100644 --- a/tests/Fixtures/SillyBotHandler.php +++ b/tests/Fixtures/SillyBotHandler.php @@ -18,7 +18,9 @@ public static function getSettings(): array public function handle(): void { - // + $this->composer()->message('Testing Silly. '.$this->senderIp); + + $this->releaseCooldown(); } public function authorize(): bool