Repository: def-studio/telegraph (php)

Issue:
New anonymous chats created
Hello,

With the latest upgrade, the package started to create chats from webhooks that can not be identified which group they belong to because the name is null.

I have two questions:
1. Is it possible to fill in the name in the database with the chat title instead it being null?
2. Is it possible to disable storing unknown chats like the previous behavior was with a config change?

Thank you!

Pull request description:
[feat] unknown chats webhook handling
revert #141 

fix #160 

Patch:
diff --git a/config/telegraph.php b/config/telegraph.php
index 0e414ea4..05edcadf 100644
--- a/config/telegraph.php
+++ b/config/telegraph.php
@@ -1,6 +1,5 @@
 <?php
 
-use DefStudio\Telegraph\Handlers\EmptyWebhookHandler;
 use DefStudio\Telegraph\Telegraph;
 
 return [
@@ -22,7 +21,7 @@ return [
      *
      * For reference, see https://def-studio.github.io/telegraph/webhooks/overview
      */
-    'webhook_handler' => EmptyWebhookHandler::class,
+    'webhook_handler' => DefStudio\Telegraph\Handlers\EmptyWebhookHandler::class,
 
     /*
      * If enabled, Telegraph dumps received
@@ -36,6 +35,23 @@ return [
      */
     'report_unknown_webhook_commands' => true,
 
+    'security' => [
+        /*
+         * if enabled, allows callback queries from unregistered chats
+         */
+        'allow_callback_queries_from_unknown_chats' => false,
+
+        /*
+         * if enabled, allows messages and commands from unregistered chats
+         */
+        'allow_messages_from_unknown_chats' => false,
+
+        /*
+         * if enabled, store unknown chats as new TelegraphChat models
+         */
+        'store_unknown_chats_in_db' => false,
+    ],
+
     /*
      * Set model class for both TelegraphBot and TelegraphChat,
      * to allow more customization.
diff --git a/docs/content/en/installation.md b/docs/content/en/installation.md
index 013abba7..11178313 100644
--- a/docs/content/en/installation.md
+++ b/docs/content/en/installation.md
@@ -67,6 +67,24 @@ return [
      * reported as exception in application logs
      */
     'report_unknown_webhook_commands' => true,
+    
+    'security' => [
+        /*
+         * if enabled, allows callback queries from unregistered chats
+         */
+        'allow_callback_queries_from_unknown_chats' => false,
+
+        /*
+         * if enabled, allows messages and commands from unregistered chats
+         */
+        'allow_messages_from_unknown_chats' => false,
+
+        /*
+         * if enabled, store unknown chats as
+         * new TelegraphChat models
+         */
+        'store_unknown_chats_in_db' => false,
+    ],
 
     /*
      * Set model class for both TelegraphBot and TelegraphChat,
diff --git a/docs/content/en/webhooks/overview.md b/docs/content/en/webhooks/overview.md
index 2723f002..986974ac 100644
--- a/docs/content/en/webhooks/overview.md
+++ b/docs/content/en/webhooks/overview.md
@@ -9,6 +9,9 @@ position: 60
 
 Telegram bots can interact with chats and users through a webhook system that enables it to be updated about chats changes, new commands and user interactions without continuously polling Telegram APIs for updates.
 
+<alert type="alert">By default webhooks can handle incoming requests from "known" chats (the one stored in database as TelegraphChat models) and will reject all others. In order to handle unknown chats see [below](webhooks/overview#handle-requests-from-unknown-chats)</alert>
+
+
 ## Default Handler
 
 A default "do nothing" handler is shipped with Telegraph installation, it can only handle a single chat command:
@@ -39,3 +42,28 @@ class MyWebhookHandler extends \DefStudio\Telegraph\Handlers\WebhookHandler
 <alert type="alert">A custom webhook handler must extend `DefStudio\Telegraph\Handlers\WebhookHandler` and has to be registered in `config('telegraph.webhook_handler')`</alert>
 
 A detailed description of how WebhookHandlers work can be found in the next sections
+
+## Handle requests from unknown chats
+
+By default webhooks can handle incoming requests from "known" chats (the one stored in database as TelegraphChat models) and will reject all others.
+
+Callback queries, commands and messages handling from unknown chats can be enabled from telegraph config in security settings:
+
+```php
+ 'security' => [
+    /*
+     * if enabled, allows callback queries from unregistered chats
+     */
+    'allow_callback_queries_from_unknown_chats' => true,
+
+    /*
+     * if enabled, allows messages and commands from unregistered chats
+     */
+    'allow_messages_from_unknown_chats' => true,
+    
+     /*
+     * if enabled, store unknown chats as new TelegraphChat models
+     */
+    'store_unknown_chats_in_db' => true,
+],
+```
diff --git a/src/DTO/Chat.php b/src/DTO/Chat.php
index b033a9ac..7f1b07c0 100644
--- a/src/DTO/Chat.php
+++ b/src/DTO/Chat.php
@@ -23,7 +23,7 @@ class Chat implements Arrayable
     }
 
     /**
-     * @param array{id:int, type:string, title?:string} $data
+     * @param array{id:int, type:string, title?:string, username?: string} $data
      */
     public static function fromArray(array $data): Chat
     {
@@ -31,7 +31,7 @@ class Chat implements Arrayable
 
         $chat->id = $data['id'];
         $chat->type = $data['type'];
-        $chat->title = $data['title'] ?? '';
+        $chat->title = $data['title'] ?? $data['username'] ?? '';
 
         return $chat;
     }
diff --git a/src/Handlers/WebhookHandler.php b/src/Handlers/WebhookHandler.php
index 9e7cd89a..7c3e9745 100644
--- a/src/Handlers/WebhookHandler.php
+++ b/src/Handlers/WebhookHandler.php
@@ -19,7 +19,6 @@ use DefStudio\Telegraph\Models\TelegraphChat;
 use Illuminate\Http\Request;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Log;
-use Illuminate\Support\ItemNotFoundException;
 use Illuminate\Support\Str;
 use Illuminate\Support\Stringable;
 use ReflectionMethod;
@@ -131,12 +130,28 @@ abstract class WebhookHandler
 
     protected function extractCallbackQueryData(): void
     {
-        try {
-            /** @var TelegraphChat $chat */
-            $chat = $this->bot->chats->where('chat_id', $this->request->input('callback_query.message.chat.id'))->firstOrFail();
-            $this->chat = $chat;
-        } catch (ItemNotFoundException) {
-            throw new NotFoundHttpException();
+        /** @var TelegraphChat $chat */
+        $chat = $this->bot->chats()->firstOrNew([
+            'chat_id' => $this->request->input('callback_query.message.chat.id'),
+        ]);
+
+        $this->chat = $chat;
+
+        if (!$this->chat->exists) {
+            if (!config('telegraph.security.allow_callback_queries_from_unknown_chats', false)) {
+                throw new NotFoundHttpException();
+            }
+
+            if (config('telegraph.security.store_unknown_chats_in_db', false)) {
+                $this->chat->name = Str::of("")
+                    ->append("[", $this->request->input('callback_query.message.chat.type'), ']')
+                    ->append(" ", $this->request->input(
+                        'callback_query.message.chat.username',
+                        $this->request->input('callback_query.message.chat.title')
+                    ));
+
+                $this->chat->save();
+            }
         }
 
         assert($this->callbackQuery !== null);
@@ -156,12 +171,24 @@ abstract class WebhookHandler
         assert($this->message?->chat() !== null);
 
         /** @var TelegraphChat $chat */
-        $chat = $this->bot->chats()->firstOrCreate([
+        $chat = $this->bot->chats()->firstOrNew([
             'chat_id' => $this->message->chat()->id(),
         ]);
-
         $this->chat = $chat;
 
+        if (!$this->chat->exists) {
+            if (!config('telegraph.security.allow_messages_from_unknown_chats', false)) {
+                throw new NotFoundHttpException();
+            }
+
+            if (config('telegraph.security.store_unknown_chats_in_db', false)) {
+                $this->chat->name = Str::of("")
+                    ->append("[", $this->message->chat()->type(), ']')
+                    ->append(" ", $this->message->chat()->title());
+                $this->chat->save();
+            }
+        }
+
         $this->messageId = $this->message->id();
 
         $this->data = collect([


Test patch:
diff --git a/tests/Pest.php b/tests/Pest.php
index 2d5d5d8b..d1f0c210 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -86,7 +86,7 @@ function register_webhook_handler(string $handler = TestWebhookHandler::class):
     config()->set('telegraph.webhook_handler', $handler);
 }
 
-function webhook_request($action = 'invalid', $handler = TestWebhookHandler::class): Request
+function webhook_request($action = 'invalid', $handler = TestWebhookHandler::class, int $chat_id = -123456789): Request
 {
     register_webhook_handler($handler);
 
@@ -105,8 +105,9 @@ function webhook_request($action = 'invalid', $handler = TestWebhookHandler::cla
             'message' => [
                 'message_id' => 123456,
                 'chat' => [
-                    'id' => -123456789,
+                    'id' => $chat_id,
                     'type' => 'group',
+                    'title' => 'Test chat',
                 ],
                 'reply_markup' => [
                     'inline_keyboard' => [
@@ -126,7 +127,7 @@ function webhook_request($action = 'invalid', $handler = TestWebhookHandler::cla
     ]);
 }
 
-function webhook_command($command, $handler = TestWebhookHandler::class): Request
+function webhook_command($command, $handler = TestWebhookHandler::class, int $chat_id = -123456789): Request
 {
     register_webhook_handler($handler);
 
@@ -134,8 +135,9 @@ function webhook_command($command, $handler = TestWebhookHandler::class): Reques
         'message' => [
             'message_id' => 123456,
             'chat' => [
-                'id' => -123456789,
+                'id' => $chat_id,
                 'type' => 'private',
+                'username' => 'john-smith',
             ],
             'text' => $command,
             'date' => 1646516736,
diff --git a/tests/Unit/Handlers/WebhookHandlerTest.php b/tests/Unit/Handlers/WebhookHandlerTest.php
index 4b600e00..1e9adcf3 100644
--- a/tests/Unit/Handlers/WebhookHandlerTest.php
+++ b/tests/Unit/Handlers/WebhookHandlerTest.php
@@ -6,8 +6,44 @@
 use DefStudio\Telegraph\Facades\Telegraph as Facade;
 use DefStudio\Telegraph\Telegraph;
 use DefStudio\Telegraph\Tests\Support\TestWebhookHandler;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+it('rejects unknown chat queries', function () {
+    $bot = make_bot();
+
+    app(TestWebhookHandler::class)->handle(webhook_request('test'), $bot);
+})->throws(NotFoundHttpException::class);
+
+it('can handle known chat queries', function () {
+    $chat = chat();
+
+    app(TestWebhookHandler::class)->handle(webhook_request('test'), $chat->bot);
+
+    expect(TestWebhookHandler::$calls_count)->toBe(1);
+});
+
+it('can save unknown chats sending queries', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+
+    $bot = bot();
+
+    app(TestWebhookHandler::class)->handle(webhook_request('test', chat_id: 99), $bot);
+
+    expect($bot->chats()->count())->toBe(1);
+
+    Config::set('telegraph.security.store_unknown_chats_in_db', true);
+
+    app(TestWebhookHandler::class)->handle(webhook_request('test', chat_id: 99), $bot);
+
+    expect($bot)
+        ->chats->count()->toBe(2)
+        ->chats->last()->name->toBe('[group] Test chat');
+});
 
 it('extracts call data', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
 
     app(TestWebhookHandler::class)->handle(webhook_request('test'), $bot);
@@ -17,6 +53,9 @@ it('extracts call data', function () {
 });
 
 it('can handle a registered action', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
 
     app(TestWebhookHandler::class)->handle(webhook_request('test'), $bot);
@@ -25,6 +64,9 @@ it('can handle a registered action', function () {
 });
 
 it('rejects unregistered actions', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
     Facade::fake();
 
@@ -34,6 +76,9 @@ it('rejects unregistered actions', function () {
 });
 
 it('rejects actions for non public methods', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
     Facade::fake();
 
@@ -43,6 +88,9 @@ it('rejects actions for non public methods', function () {
 });
 
 it('can reply', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
     Facade::fake();
 
@@ -52,6 +100,9 @@ it('can reply', function () {
 });
 
 it('logs webhook calls', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
     Facade::fake();
 
@@ -60,6 +111,41 @@ it('logs webhook calls', function () {
     Facade::assertRepliedWebhook('foo');
 });
 
+it('rejects unknown chats commands', function () {
+    $bot = make_bot();
+
+    app(TestWebhookHandler::class)->handle(webhook_command('/hello'), $bot);
+})->throws(NotFoundHttpException::class);
+
+it('can handle known chat commands', function () {
+    $chat = chat();
+    Facade::fake();
+
+    app(TestWebhookHandler::class)->handle(webhook_command('/hello'), $chat->bot);
+
+    Facade::assertSent("Hello!!");
+});
+
+it('can save unknown chats sending commands', function () {
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
+    $bot = bot();
+
+    Facade::fake();
+
+    app(TestWebhookHandler::class)->handle(webhook_command('/hello', chat_id: 99), $bot);
+
+    expect($bot->chats()->count())->toBe(1);
+
+    Config::set('telegraph.security.store_unknown_chats_in_db', true);
+
+    app(TestWebhookHandler::class)->handle(webhook_command('/hello', chat_id: 99), $bot);
+
+    expect($bot)
+        ->chats->count()->toBe(2)
+        ->chats->last()->name->toBe('[private] john-smith');
+});
+
 it('can handle a command', function () {
     $bot = bot();
     Facade::fake();
@@ -110,6 +196,9 @@ it('can handle a command with parameters and bot reference', function () {
 });
 
 it('can change the inline keyboard', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
     Facade::fake();
 
@@ -119,6 +208,9 @@ it('can change the inline keyboard', function () {
 });
 
 it('can delete the inline keyboard', function () {
+    Config::set('telegraph.security.allow_callback_queries_from_unknown_chats', true);
+    Config::set('telegraph.security.allow_messages_from_unknown_chats', true);
+
     $bot = make_bot();
     Facade::fake();
 
diff --git a/tests/__snapshots__/TelegraphBotTest__it_can_poll_for_updates__1.yml b/tests/__snapshots__/TelegraphBotTest__it_can_poll_for_updates__1.yml
index adc68caa..2102d710 100644
--- a/tests/__snapshots__/TelegraphBotTest__it_can_poll_for_updates__1.yml
+++ b/tests/__snapshots__/TelegraphBotTest__it_can_poll_for_updates__1.yml
@@ -1,6 +1,6 @@
 -
     id: 123456
-    message: { id: 42, date: '2022-03-05T21:45:36.000000Z', text: /start, from: { id: 444, first_name: John, last_name: Smith, username: john_smith }, chat: { id: 987654, type: private } }
+    message: { id: 42, date: '2022-03-05T21:45:36.000000Z', text: /start, from: { id: 444, first_name: John, last_name: Smith, username: john_smith }, chat: { id: 987654, type: private, title: john_smith } }
 -
     id: 123457
     message: { id: 99, date: '2022-03-05T22:35:36.000000Z', text: 'Hello world!', from: { id: 8974, is_bot: true, first_name: 'Test Bot', username: test_bot }, chat: { id: -987455499, type: group, title: 'Bot Test Chat' } }