telegram_bot_engine 0.3.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/app/controllers/telegram_bot_engine/admin/allowlist_controller.rb +5 -2
- data/app/controllers/telegram_bot_engine/admin/bots_controller.rb +73 -0
- data/app/controllers/telegram_bot_engine/admin/dashboard_controller.rb +6 -0
- data/app/controllers/telegram_bot_engine/admin/events_controller.rb +3 -0
- data/app/controllers/telegram_bot_engine/admin/subscriptions_controller.rb +3 -1
- data/app/jobs/telegram_bot_engine/application_job.rb +10 -0
- data/app/jobs/telegram_bot_engine/delivery_job.rb +15 -8
- data/app/models/telegram_bot_engine/allowed_user.rb +7 -1
- data/app/models/telegram_bot_engine/bot.rb +177 -0
- data/app/models/telegram_bot_engine/event.rb +8 -3
- data/app/models/telegram_bot_engine/subscription.rb +14 -1
- data/app/views/telegram_bot_engine/admin/allowlist/index.html.erb +17 -1
- data/app/views/telegram_bot_engine/admin/bots/edit.html.erb +69 -0
- data/app/views/telegram_bot_engine/admin/bots/index.html.erb +70 -0
- data/app/views/telegram_bot_engine/admin/bots/new.html.erb +53 -0
- data/app/views/telegram_bot_engine/admin/dashboard/show.html.erb +40 -1
- data/app/views/telegram_bot_engine/admin/events/index.html.erb +21 -4
- data/app/views/telegram_bot_engine/admin/layouts/application.html.erb +2 -0
- data/app/views/telegram_bot_engine/admin/subscriptions/index.html.erb +22 -1
- data/config/routes.rb +3 -0
- data/db/migrate/004_create_telegram_bot_engine_bots.rb +21 -0
- data/db/migrate/005_add_bot_to_telegram_bot_engine_subscriptions.rb +38 -0
- data/db/migrate/006_add_bot_to_telegram_bot_engine_allowed_users.rb +32 -0
- data/db/migrate/007_add_bot_to_telegram_bot_engine_events.rb +11 -0
- data/db/migrate/008_add_webhook_id_to_telegram_bot_engine_bots.rb +29 -0
- data/lib/telegram_bot_engine/authorizer.rb +10 -6
- data/lib/telegram_bot_engine/configuration.rb +8 -1
- data/lib/telegram_bot_engine/dispatch.rb +73 -0
- data/lib/telegram_bot_engine/registry.rb +39 -0
- data/lib/telegram_bot_engine/subscriber_commands.rb +17 -7
- data/lib/telegram_bot_engine/version.rb +1 -1
- data/lib/telegram_bot_engine/webhook_registrar.rb +32 -0
- data/lib/telegram_bot_engine.rb +24 -9
- metadata +31 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Edit Bot</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500 font-mono"><%= @bot.slug %></p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200 p-6 max-w-2xl mb-6">
|
|
7
|
+
<%= form_with url: telegram_bot_engine.admin_bot_path(@bot), method: :patch, class: "space-y-4" do %>
|
|
8
|
+
<div>
|
|
9
|
+
<label for="bot_name" class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
10
|
+
<input type="text" name="bot[name]" id="bot_name" value="<%= @bot.name %>" required
|
|
11
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border">
|
|
12
|
+
</div>
|
|
13
|
+
<div>
|
|
14
|
+
<label for="bot_slug" class="block text-sm font-medium text-gray-700 mb-1">Slug</label>
|
|
15
|
+
<input type="text" name="bot[slug]" id="bot_slug" value="<%= @bot.slug %>" required
|
|
16
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border font-mono">
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<label for="bot_purpose" class="block text-sm font-medium text-gray-700 mb-1">Purpose (optional)</label>
|
|
20
|
+
<input type="text" name="bot[purpose]" id="bot_purpose" value="<%= @bot.purpose %>"
|
|
21
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border">
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex items-center space-x-2">
|
|
24
|
+
<input type="hidden" name="bot[active]" value="0">
|
|
25
|
+
<input type="checkbox" name="bot[active]" id="bot_active" value="1" <%= "checked" if @bot.active %>
|
|
26
|
+
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
27
|
+
<label for="bot_active" class="text-sm text-gray-700">Active</label>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="flex items-center space-x-2">
|
|
30
|
+
<input type="hidden" name="bot[default]" value="0">
|
|
31
|
+
<input type="checkbox" name="bot[default]" id="bot_default" value="1" <%= "checked" if @bot.default %>
|
|
32
|
+
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
33
|
+
<label for="bot_default" class="text-sm text-gray-700">Default bot</label>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="flex items-center space-x-3 pt-2">
|
|
36
|
+
<button type="submit"
|
|
37
|
+
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
|
|
38
|
+
Save
|
|
39
|
+
</button>
|
|
40
|
+
<%= link_to "Cancel", telegram_bot_engine.admin_bots_path, class: "text-sm text-gray-600 hover:text-gray-900" %>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200 p-6 max-w-2xl mb-6">
|
|
46
|
+
<h2 class="text-sm font-medium text-gray-700 mb-1">Rotate token</h2>
|
|
47
|
+
<p class="text-xs text-gray-500 mb-4">Replaces the stored token and invalidates the cached client immediately (token-rotation safe). Current token: <span class="font-mono"><%= @bot.token.to_s.length > 4 ? "••••#{@bot.token.to_s.last(4)}" : "set" %></span></p>
|
|
48
|
+
<%= form_with url: telegram_bot_engine.rotate_token_admin_bot_path(@bot), method: :patch, class: "flex items-end space-x-3" do %>
|
|
49
|
+
<div class="flex-1">
|
|
50
|
+
<label for="token" class="block text-sm font-medium text-gray-700 mb-1">New token</label>
|
|
51
|
+
<input type="password" name="token" id="token" required autocomplete="off"
|
|
52
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border font-mono"
|
|
53
|
+
placeholder="123456:ABC-DEF...">
|
|
54
|
+
</div>
|
|
55
|
+
<button type="submit"
|
|
56
|
+
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
|
57
|
+
Rotate
|
|
58
|
+
</button>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<% unless @bot.default? %>
|
|
63
|
+
<div class="max-w-2xl">
|
|
64
|
+
<%= button_to "Delete this bot", telegram_bot_engine.admin_bot_path(@bot),
|
|
65
|
+
method: :delete,
|
|
66
|
+
data: { turbo_confirm: "Delete bot \"#{@bot.name}\" and its subscribers/allowlist?" },
|
|
67
|
+
class: "inline-flex items-center px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50" %>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<div class="mb-6 flex items-center justify-between">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900">Bots</h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500"><%= @bots.count %> total</p>
|
|
5
|
+
</div>
|
|
6
|
+
<%= link_to "Add Bot", telegram_bot_engine.new_admin_bot_path,
|
|
7
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
|
|
11
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
12
|
+
<thead class="bg-gray-50">
|
|
13
|
+
<tr>
|
|
14
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
15
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
|
|
16
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Purpose</th>
|
|
17
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Token</th>
|
|
18
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
19
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Webhook</th>
|
|
20
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
24
|
+
<% @bots.each do |bot| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"><%= bot.name %></td>
|
|
27
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono"><%= bot.slug %></td>
|
|
28
|
+
<td class="px-6 py-4 text-sm text-gray-600"><%= bot.purpose.presence || "-" %></td>
|
|
29
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
|
|
30
|
+
<%= bot.token.to_s.length > 4 ? "••••#{bot.token.to_s.last(4)}" : "set" %>
|
|
31
|
+
</td>
|
|
32
|
+
<td class="px-6 py-4 whitespace-nowrap space-x-1">
|
|
33
|
+
<% if bot.default? %>
|
|
34
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">Default</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% if bot.active? %>
|
|
37
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
|
|
38
|
+
<% else %>
|
|
39
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">Inactive</span>
|
|
40
|
+
<% end %>
|
|
41
|
+
</td>
|
|
42
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
43
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
|
|
44
|
+
Secret set
|
|
45
|
+
</span>
|
|
46
|
+
<span class="block text-xs text-gray-400 mt-1">registers in Phase 3</span>
|
|
47
|
+
</td>
|
|
48
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm space-x-2">
|
|
49
|
+
<%= link_to "Edit", telegram_bot_engine.edit_admin_bot_path(bot),
|
|
50
|
+
class: "inline-flex items-center px-3 py-1.5 border border-gray-300 rounded text-xs font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
51
|
+
<% unless bot.default? %>
|
|
52
|
+
<%= button_to "Delete", telegram_bot_engine.admin_bot_path(bot),
|
|
53
|
+
method: :delete,
|
|
54
|
+
data: { turbo_confirm: "Delete bot \"#{bot.name}\" and its subscribers/allowlist?" },
|
|
55
|
+
class: "inline-flex items-center px-3 py-1.5 border border-red-300 rounded text-xs font-medium text-red-700 bg-white hover:bg-red-50" %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<% end %>
|
|
60
|
+
|
|
61
|
+
<% if @bots.empty? %>
|
|
62
|
+
<tr>
|
|
63
|
+
<td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500">
|
|
64
|
+
No bots yet. <%= link_to "Add one", telegram_bot_engine.new_admin_bot_path, class: "text-blue-600 hover:text-blue-800" %>.
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% end %>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Add Bot</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200 p-6 max-w-2xl">
|
|
6
|
+
<%= form_with url: telegram_bot_engine.admin_bots_path, method: :post, class: "space-y-4" do %>
|
|
7
|
+
<div>
|
|
8
|
+
<label for="bot_name" class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
9
|
+
<input type="text" name="bot[name]" id="bot_name" value="<%= @bot.name %>" required
|
|
10
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border"
|
|
11
|
+
placeholder="Assistant">
|
|
12
|
+
</div>
|
|
13
|
+
<div>
|
|
14
|
+
<label for="bot_slug" class="block text-sm font-medium text-gray-700 mb-1">Slug</label>
|
|
15
|
+
<input type="text" name="bot[slug]" id="bot_slug" value="<%= @bot.slug %>" required
|
|
16
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border font-mono"
|
|
17
|
+
placeholder="assistant">
|
|
18
|
+
<p class="mt-1 text-xs text-gray-500">Stable handle used by the app (e.g. <code>Bot.resolve("assistant")</code>). Cannot collide.</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<label for="bot_purpose" class="block text-sm font-medium text-gray-700 mb-1">Purpose (optional)</label>
|
|
22
|
+
<input type="text" name="bot[purpose]" id="bot_purpose" value="<%= @bot.purpose %>"
|
|
23
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border"
|
|
24
|
+
placeholder="Ops alerts from the assistant">
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<label for="bot_token" class="block text-sm font-medium text-gray-700 mb-1">Token</label>
|
|
28
|
+
<input type="password" name="bot[token]" id="bot_token" required autocomplete="off"
|
|
29
|
+
class="block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border font-mono"
|
|
30
|
+
placeholder="123456:ABC-DEF...">
|
|
31
|
+
<p class="mt-1 text-xs text-gray-500">Stored as-is until ActiveRecord Encryption keys are provisioned (docs/0001 §6). Rotate later from the bot's edit page.</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="flex items-center space-x-2">
|
|
34
|
+
<input type="hidden" name="bot[active]" value="0">
|
|
35
|
+
<input type="checkbox" name="bot[active]" id="bot_active" value="1" <%= "checked" if @bot.active %>
|
|
36
|
+
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
37
|
+
<label for="bot_active" class="text-sm text-gray-700">Active</label>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="flex items-center space-x-2">
|
|
40
|
+
<input type="hidden" name="bot[default]" value="0">
|
|
41
|
+
<input type="checkbox" name="bot[default]" id="bot_default" value="1" <%= "checked" if @bot.default %>
|
|
42
|
+
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
43
|
+
<label for="bot_default" class="text-sm text-gray-700">Default bot <span class="text-gray-400">(promoting this demotes the current default)</span></label>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="flex items-center space-x-3 pt-2">
|
|
46
|
+
<button type="submit"
|
|
47
|
+
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
|
|
48
|
+
Create Bot
|
|
49
|
+
</button>
|
|
50
|
+
<%= link_to "Cancel", telegram_bot_engine.admin_bots_path, class: "text-sm text-gray-600 hover:text-gray-900" %>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
@@ -2,7 +2,43 @@
|
|
|
2
2
|
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
|
-
<% if @
|
|
5
|
+
<% if @bots.any? %>
|
|
6
|
+
<div class="mb-6 bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
7
|
+
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
8
|
+
<h2 class="text-sm font-medium text-gray-500 uppercase tracking-wider">Bots</h2>
|
|
9
|
+
<%= link_to "Manage", telegram_bot_engine.admin_bots_path, class: "text-sm text-blue-600 hover:text-blue-800" %>
|
|
10
|
+
</div>
|
|
11
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
12
|
+
<thead class="bg-gray-50">
|
|
13
|
+
<tr>
|
|
14
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
15
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
|
|
16
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
17
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Active Subscribers</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
21
|
+
<% @bots.each do |bot| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"><%= bot.name %></td>
|
|
24
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono"><%= bot.slug %></td>
|
|
25
|
+
<td class="px-6 py-4 whitespace-nowrap space-x-1">
|
|
26
|
+
<% if bot.default? %>
|
|
27
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">Default</span>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% if bot.active? %>
|
|
30
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
|
|
31
|
+
<% else %>
|
|
32
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">Inactive</span>
|
|
33
|
+
<% end %>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-semibold text-gray-900"><%= @bot_active_counts[bot] %></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
<% elsif @bot_username %>
|
|
6
42
|
<div class="mb-6 bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
7
43
|
<h2 class="text-sm font-medium text-gray-500 uppercase tracking-wider mb-2">Bot</h2>
|
|
8
44
|
<p class="text-lg font-semibold text-gray-900">@<%= @bot_username %></p>
|
|
@@ -31,6 +67,9 @@
|
|
|
31
67
|
</div>
|
|
32
68
|
|
|
33
69
|
<div class="flex space-x-4">
|
|
70
|
+
<%= link_to "Manage Bots", telegram_bot_engine.admin_bots_path,
|
|
71
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
72
|
+
|
|
34
73
|
<%= link_to "Manage Subscriptions", telegram_bot_engine.admin_subscriptions_path,
|
|
35
74
|
class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
36
75
|
|
|
@@ -29,6 +29,17 @@
|
|
|
29
29
|
class="block rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border"
|
|
30
30
|
placeholder="e.g. 12345">
|
|
31
31
|
</div>
|
|
32
|
+
<% if @bots.any? %>
|
|
33
|
+
<div>
|
|
34
|
+
<label for="bot_id" class="block text-sm font-medium text-gray-700 mb-1">Bot</label>
|
|
35
|
+
<select name="bot_id" id="bot_id" class="block rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border">
|
|
36
|
+
<option value="">All</option>
|
|
37
|
+
<% @bots.each do |bot| %>
|
|
38
|
+
<option value="<%= bot.id %>" <%= "selected" if params[:bot_id].to_s == bot.id.to_s %>><%= bot.name %></option>
|
|
39
|
+
<% end %>
|
|
40
|
+
</select>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
32
43
|
<div>
|
|
33
44
|
<button type="submit"
|
|
34
45
|
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
@@ -45,12 +56,14 @@
|
|
|
45
56
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timestamp</th>
|
|
46
57
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
|
47
58
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
|
|
59
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bot</th>
|
|
48
60
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
|
|
49
61
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chat ID</th>
|
|
50
62
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Details</th>
|
|
51
63
|
</tr>
|
|
52
64
|
</thead>
|
|
53
65
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
66
|
+
<% bot_names = @bots.each_with_object({}) { |bot, h| h[bot.id] = bot.name } %>
|
|
54
67
|
<% @events.each do |event| %>
|
|
55
68
|
<tr>
|
|
56
69
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
@@ -73,6 +86,9 @@
|
|
|
73
86
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
74
87
|
<%= event.action %>
|
|
75
88
|
</td>
|
|
89
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
|
90
|
+
<%= bot_names[event.bot_id] || "-" %>
|
|
91
|
+
</td>
|
|
76
92
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
|
77
93
|
<%= event.username || "-" %>
|
|
78
94
|
</td>
|
|
@@ -80,14 +96,15 @@
|
|
|
80
96
|
<%= event.chat_id || "-" %>
|
|
81
97
|
</td>
|
|
82
98
|
<td class="px-6 py-4 text-gray-500 max-w-md break-words" style="font-size: 0.7rem; line-height: 1.15rem;">
|
|
83
|
-
|
|
99
|
+
<% detail_data = event.details.is_a?(Hash) ? event.details.except("bot", :bot) : event.details %>
|
|
100
|
+
<%= detail_data.present? ? detail_data.to_json : "-" %>
|
|
84
101
|
</td>
|
|
85
102
|
</tr>
|
|
86
103
|
<% end %>
|
|
87
104
|
|
|
88
105
|
<% if @events.empty? %>
|
|
89
106
|
<tr>
|
|
90
|
-
<td colspan="
|
|
107
|
+
<td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500">
|
|
91
108
|
No events found.
|
|
92
109
|
</td>
|
|
93
110
|
</tr>
|
|
@@ -103,11 +120,11 @@
|
|
|
103
120
|
</div>
|
|
104
121
|
<div class="flex space-x-2">
|
|
105
122
|
<% if @page > 1 %>
|
|
106
|
-
<%= link_to "Previous", telegram_bot_engine.admin_events_path(type: params[:type], action_name: params[:action_name], chat_id: params[:chat_id], page: @page - 1),
|
|
123
|
+
<%= link_to "Previous", telegram_bot_engine.admin_events_path(type: params[:type], action_name: params[:action_name], chat_id: params[:chat_id], bot_id: params[:bot_id], page: @page - 1),
|
|
107
124
|
class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
108
125
|
<% end %>
|
|
109
126
|
<% if @page < @total_pages %>
|
|
110
|
-
<%= link_to "Next", telegram_bot_engine.admin_events_path(type: params[:type], action_name: params[:action_name], chat_id: params[:chat_id], page: @page + 1),
|
|
127
|
+
<%= link_to "Next", telegram_bot_engine.admin_events_path(type: params[:type], action_name: params[:action_name], chat_id: params[:chat_id], bot_id: params[:bot_id], page: @page + 1),
|
|
111
128
|
class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
112
129
|
<% end %>
|
|
113
130
|
</div>
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
<div class="flex space-x-4">
|
|
17
17
|
<%= link_to "Dashboard", telegram_bot_engine.admin_dashboard_path,
|
|
18
18
|
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == telegram_bot_engine.admin_dashboard_path || request.path == telegram_bot_engine.admin_root_path ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" %>
|
|
19
|
+
<%= link_to "Bots", telegram_bot_engine.admin_bots_path,
|
|
20
|
+
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path.include?('bots') ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" %>
|
|
19
21
|
<%= link_to "Subscriptions", telegram_bot_engine.admin_subscriptions_path,
|
|
20
22
|
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path.include?('subscriptions') ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" %>
|
|
21
23
|
<% if TelegramBotEngine.config.allowed_usernames == :database %>
|
|
@@ -3,12 +3,30 @@
|
|
|
3
3
|
<p class="mt-1 text-sm text-gray-500"><%= @subscriptions.count %> total</p>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
+
<% if @bots.any? %>
|
|
7
|
+
<div class="mb-6 bg-white shadow-sm rounded-lg border border-gray-200 p-4">
|
|
8
|
+
<%= form_with url: telegram_bot_engine.admin_subscriptions_path, method: :get, local: true, class: "flex items-end space-x-3" do %>
|
|
9
|
+
<div>
|
|
10
|
+
<label for="bot_id" class="block text-sm font-medium text-gray-700 mb-1">Bot</label>
|
|
11
|
+
<select name="bot_id" id="bot_id" class="block rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border">
|
|
12
|
+
<option value="">All bots</option>
|
|
13
|
+
<% @bots.each do |bot| %>
|
|
14
|
+
<option value="<%= bot.id %>" <%= "selected" if params[:bot_id].to_s == bot.id.to_s %>><%= bot.name %></option>
|
|
15
|
+
<% end %>
|
|
16
|
+
</select>
|
|
17
|
+
</div>
|
|
18
|
+
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">Filter</button>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
6
23
|
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
|
|
7
24
|
<table class="min-w-full divide-y divide-gray-200">
|
|
8
25
|
<thead class="bg-gray-50">
|
|
9
26
|
<tr>
|
|
10
27
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
|
|
11
28
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">First Name</th>
|
|
29
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bot</th>
|
|
12
30
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chat ID</th>
|
|
13
31
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
14
32
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Subscribed</th>
|
|
@@ -24,6 +42,9 @@
|
|
|
24
42
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
|
25
43
|
<%= subscription.first_name || "-" %>
|
|
26
44
|
</td>
|
|
45
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
|
|
46
|
+
<%= subscription.bot&.slug || "default" %>
|
|
47
|
+
</td>
|
|
27
48
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
|
|
28
49
|
<%= subscription.chat_id %>
|
|
29
50
|
</td>
|
|
@@ -53,7 +74,7 @@
|
|
|
53
74
|
|
|
54
75
|
<% if @subscriptions.empty? %>
|
|
55
76
|
<tr>
|
|
56
|
-
<td colspan="
|
|
77
|
+
<td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500">
|
|
57
78
|
No subscriptions yet.
|
|
58
79
|
</td>
|
|
59
80
|
</tr>
|
data/config/routes.rb
CHANGED
|
@@ -4,6 +4,9 @@ TelegramBotEngine::Engine.routes.draw do
|
|
|
4
4
|
scope module: :admin, as: :admin do
|
|
5
5
|
root to: "dashboard#show"
|
|
6
6
|
get "dashboard", to: "dashboard#show", as: :dashboard
|
|
7
|
+
resources :bots, except: %i[show] do
|
|
8
|
+
member { patch :rotate_token }
|
|
9
|
+
end
|
|
7
10
|
resources :subscriptions, only: %i[index update destroy]
|
|
8
11
|
resources :allowlist, only: %i[index create destroy]
|
|
9
12
|
resources :events, only: %i[index]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTelegramBotEngineBots < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :telegram_bot_engine_bots do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :slug, null: false
|
|
8
|
+
t.string :purpose
|
|
9
|
+
t.string :token, null: false # encrypted at rest once keys are provisioned (docs/0001 §6)
|
|
10
|
+
t.string :webhook_secret, null: false # per-bot inbound secret path segment
|
|
11
|
+
t.boolean :active, null: false, default: true
|
|
12
|
+
t.boolean :default, null: false, default: false # exactly one row true (enforced in the model)
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :telegram_bot_engine_bots, :slug, unique: true
|
|
18
|
+
add_index :telegram_bot_engine_bots, :webhook_secret, unique: true
|
|
19
|
+
add_index :telegram_bot_engine_bots, :active
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-bot subscribers (docs/0001 §3.4): a chat can now subscribe to more than one bot,
|
|
4
|
+
# so global chat_id uniqueness becomes (bot_id, chat_id) uniqueness.
|
|
5
|
+
#
|
|
6
|
+
# Additive + rollback-safe: bot_id is nullable and there is NO data backfill. Back-compat
|
|
7
|
+
# is handled at the read layer (Subscription.for_bot(default_bot) folds nil-bot_id rows in),
|
|
8
|
+
# so a rolled-back gem that knows nothing about bot_id still reads every row (docs/0001 §9).
|
|
9
|
+
#
|
|
10
|
+
# A partial unique index on (chat_id WHERE bot_id IS NULL) preserves the duplicate guard the
|
|
11
|
+
# old global unique index gave the pre-multi-bot (default-bot) population — composite unique
|
|
12
|
+
# indexes treat NULL bot_id as distinct, so that set would otherwise lose DB-level protection.
|
|
13
|
+
#
|
|
14
|
+
# Explicit up/down so a rollback faithfully restores the original UNIQUE(chat_id) index
|
|
15
|
+
# instead of silently leaving it non-unique (the default `change` inverse would).
|
|
16
|
+
class AddBotToTelegramBotEngineSubscriptions < ActiveRecord::Migration[7.0]
|
|
17
|
+
DEFAULT_BOT_UNIQUE = "index_tbe_subscriptions_on_chat_id_default_bot"
|
|
18
|
+
|
|
19
|
+
def up
|
|
20
|
+
add_column :telegram_bot_engine_subscriptions, :bot_id, :bigint
|
|
21
|
+
add_index :telegram_bot_engine_subscriptions, :bot_id
|
|
22
|
+
|
|
23
|
+
remove_index :telegram_bot_engine_subscriptions, :chat_id # drop the global UNIQUE(chat_id)
|
|
24
|
+
add_index :telegram_bot_engine_subscriptions, :chat_id # keep a non-unique lookup index
|
|
25
|
+
add_index :telegram_bot_engine_subscriptions, %i[bot_id chat_id], unique: true
|
|
26
|
+
add_index :telegram_bot_engine_subscriptions, :chat_id, unique: true,
|
|
27
|
+
where: "bot_id IS NULL", name: DEFAULT_BOT_UNIQUE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def down
|
|
31
|
+
remove_index :telegram_bot_engine_subscriptions, name: DEFAULT_BOT_UNIQUE
|
|
32
|
+
remove_index :telegram_bot_engine_subscriptions, column: %i[bot_id chat_id]
|
|
33
|
+
remove_index :telegram_bot_engine_subscriptions, :chat_id
|
|
34
|
+
add_index :telegram_bot_engine_subscriptions, :chat_id, unique: true # restore original guard
|
|
35
|
+
remove_index :telegram_bot_engine_subscriptions, :bot_id
|
|
36
|
+
remove_column :telegram_bot_engine_subscriptions, :bot_id
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-bot allowlist (docs/0001 §3.5): an AllowedUser may be scoped to a bot. A nil bot_id
|
|
4
|
+
# is a GLOBAL allow entry that applies to every bot — exactly today's behavior, so existing
|
|
5
|
+
# rows (all nil) keep authorizing as before. Additive + nullable.
|
|
6
|
+
#
|
|
7
|
+
# Partial unique index on (username WHERE bot_id IS NULL) preserves the duplicate guard the
|
|
8
|
+
# old global unique(username) index gave global entries (NULLs are distinct in the composite
|
|
9
|
+
# index). Explicit up/down so rollback restores the original UNIQUE(username) faithfully.
|
|
10
|
+
class AddBotToTelegramBotEngineAllowedUsers < ActiveRecord::Migration[7.0]
|
|
11
|
+
GLOBAL_UNIQUE = "index_tbe_allowed_users_on_username_global"
|
|
12
|
+
|
|
13
|
+
def up
|
|
14
|
+
add_column :telegram_bot_engine_allowed_users, :bot_id, :bigint
|
|
15
|
+
add_index :telegram_bot_engine_allowed_users, :bot_id
|
|
16
|
+
|
|
17
|
+
remove_index :telegram_bot_engine_allowed_users, :username # drop the global UNIQUE(username)
|
|
18
|
+
add_index :telegram_bot_engine_allowed_users, :username # keep a non-unique lookup index
|
|
19
|
+
add_index :telegram_bot_engine_allowed_users, %i[bot_id username], unique: true
|
|
20
|
+
add_index :telegram_bot_engine_allowed_users, :username, unique: true,
|
|
21
|
+
where: "bot_id IS NULL", name: GLOBAL_UNIQUE
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def down
|
|
25
|
+
remove_index :telegram_bot_engine_allowed_users, name: GLOBAL_UNIQUE
|
|
26
|
+
remove_index :telegram_bot_engine_allowed_users, column: %i[bot_id username]
|
|
27
|
+
remove_index :telegram_bot_engine_allowed_users, :username
|
|
28
|
+
add_index :telegram_bot_engine_allowed_users, :username, unique: true # restore original guard
|
|
29
|
+
remove_index :telegram_bot_engine_allowed_users, :bot_id
|
|
30
|
+
remove_column :telegram_bot_engine_allowed_users, :bot_id
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-bot event log (docs/0001 §3.8): tag delivery events with the bot that produced
|
|
4
|
+
# them so the events admin screen can be scoped per bot. Nullable + additive; command
|
|
5
|
+
# events (start/stop/help) stay nil until the inbound dispatcher knows the bot (Phase 3).
|
|
6
|
+
class AddBotToTelegramBotEngineEvents < ActiveRecord::Migration[7.0]
|
|
7
|
+
def change
|
|
8
|
+
add_column :telegram_bot_engine_events, :bot_id, :bigint
|
|
9
|
+
add_index :telegram_bot_engine_events, :bot_id
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
# Decouple the inbound ROUTING key from the bearer SECRET (security hardening, docs/0001
|
|
6
|
+
# §3.6/§3.7). Previously the webhook URL embedded `webhook_secret`, which Rails logs verbatim
|
|
7
|
+
# in request lines and SQL binds — leaking the secret_token to any log reader. `webhook_id` is
|
|
8
|
+
# a stable, NON-secret public id that goes in the URL path; `webhook_secret` now lives ONLY in
|
|
9
|
+
# the X-Telegram-Bot-Api-Secret-Token header. Additive + backfilled; uses update_columns via an
|
|
10
|
+
# anonymous model so no Bot callbacks/validations fire during the migration.
|
|
11
|
+
class AddWebhookIdToTelegramBotEngineBots < ActiveRecord::Migration[7.0]
|
|
12
|
+
def up
|
|
13
|
+
add_column :telegram_bot_engine_bots, :webhook_id, :string
|
|
14
|
+
|
|
15
|
+
seam = Class.new(ActiveRecord::Base) { self.table_name = "telegram_bot_engine_bots" }
|
|
16
|
+
seam.reset_column_information
|
|
17
|
+
seam.where(webhook_id: nil).find_each do |row|
|
|
18
|
+
row.update_columns(webhook_id: SecureRandom.hex(16))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
change_column_null :telegram_bot_engine_bots, :webhook_id, false
|
|
22
|
+
add_index :telegram_bot_engine_bots, :webhook_id, unique: true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def down
|
|
26
|
+
remove_index :telegram_bot_engine_bots, :webhook_id
|
|
27
|
+
remove_column :telegram_bot_engine_bots, :webhook_id
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module TelegramBotEngine
|
|
4
4
|
class Authorizer
|
|
5
|
-
|
|
5
|
+
# Authorizes an inbound command username. `bot: nil` ⇒ global behavior, unchanged
|
|
6
|
+
# (docs/0001 §3.5). When a bot is given in :database mode, both global (nil bot_id)
|
|
7
|
+
# and that bot's own allow entries apply.
|
|
8
|
+
def self.authorized?(username, bot: nil)
|
|
6
9
|
return true if TelegramBotEngine.config.allowed_usernames.nil?
|
|
7
10
|
|
|
8
|
-
allowed = resolve_allowed_usernames
|
|
11
|
+
allowed = resolve_allowed_usernames(bot)
|
|
9
12
|
allowed.map(&:downcase).include?(username&.downcase)
|
|
10
13
|
end
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def self.resolve_allowed_usernames
|
|
15
|
+
def self.resolve_allowed_usernames(bot = nil)
|
|
15
16
|
config = TelegramBotEngine.config.allowed_usernames
|
|
16
17
|
|
|
17
18
|
case config
|
|
@@ -20,10 +21,13 @@ module TelegramBotEngine
|
|
|
20
21
|
when Proc
|
|
21
22
|
config.call
|
|
22
23
|
when :database
|
|
23
|
-
TelegramBotEngine::AllowedUser.
|
|
24
|
+
scope = bot ? TelegramBotEngine::AllowedUser.for_bot(bot) : TelegramBotEngine::AllowedUser.all
|
|
25
|
+
scope.pluck(:username)
|
|
24
26
|
else
|
|
25
27
|
[]
|
|
26
28
|
end
|
|
27
29
|
end
|
|
30
|
+
|
|
31
|
+
private_class_method :resolve_allowed_usernames
|
|
28
32
|
end
|
|
29
33
|
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module TelegramBotEngine
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :allowed_usernames, :admin_enabled, :unauthorized_message, :welcome_message,
|
|
6
|
-
:event_logging, :event_retention_days
|
|
6
|
+
:event_logging, :event_retention_days,
|
|
7
|
+
# Phase 3 — webhook registrar (§3.6) + inbound dispatcher (§3.7)
|
|
8
|
+
:webhook_base_url, :webhook_mount_path, :dispatch_controller, :auto_register_webhooks
|
|
7
9
|
|
|
8
10
|
def initialize
|
|
9
11
|
@allowed_usernames = nil
|
|
@@ -12,6 +14,11 @@ module TelegramBotEngine
|
|
|
12
14
|
@welcome_message = "Welcome %{username}! Available commands:\n%{commands}"
|
|
13
15
|
@event_logging = true
|
|
14
16
|
@event_retention_days = 30
|
|
17
|
+
|
|
18
|
+
@webhook_base_url = nil # host public https base, e.g. "https://app.example.com"
|
|
19
|
+
@webhook_mount_path = "/telegram/bot" # MUST match `mount TelegramBotEngine::Dispatch, at:`
|
|
20
|
+
@dispatch_controller = nil # host UpdatesController (String/class) incl. SubscriberCommands
|
|
21
|
+
@auto_register_webhooks = true # (un)register webhooks on Bot save/destroy when base_url present
|
|
15
22
|
end
|
|
16
23
|
end
|
|
17
24
|
end
|