telegram_bot_engine 0.1.0
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 +7 -0
- data/README.md +135 -0
- data/app/controllers/telegram_bot_engine/admin/allowlist_controller.rb +38 -0
- data/app/controllers/telegram_bot_engine/admin/base_controller.rb +17 -0
- data/app/controllers/telegram_bot_engine/admin/dashboard_controller.rb +22 -0
- data/app/controllers/telegram_bot_engine/admin/subscriptions_controller.rb +23 -0
- data/app/jobs/telegram_bot_engine/delivery_job.rb +20 -0
- data/app/models/telegram_bot_engine/allowed_user.rb +9 -0
- data/app/models/telegram_bot_engine/subscription.rb +11 -0
- data/app/views/telegram_bot_engine/admin/allowlist/index.html.erb +71 -0
- data/app/views/telegram_bot_engine/admin/dashboard/show.html.erb +41 -0
- data/app/views/telegram_bot_engine/admin/layouts/application.html.erb +47 -0
- data/app/views/telegram_bot_engine/admin/subscriptions/index.html.erb +63 -0
- data/config/routes.rb +10 -0
- data/db/migrate/001_create_telegram_bot_engine_subscriptions.rb +20 -0
- data/db/migrate/002_create_telegram_bot_engine_allowed_users.rb +14 -0
- data/lib/tasks/telegram_bot_engine.rake +4 -0
- data/lib/telegram_bot_engine/authorizer.rb +29 -0
- data/lib/telegram_bot_engine/configuration.rb +14 -0
- data/lib/telegram_bot_engine/engine.rb +11 -0
- data/lib/telegram_bot_engine/subscriber_commands.rb +62 -0
- data/lib/telegram_bot_engine/version.rb +5 -0
- data/lib/telegram_bot_engine.rb +40 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6328e735752f40d861612353af4506231c527650bb1af06eb41e555266a57d6e
|
|
4
|
+
data.tar.gz: bbf440a730524d681420d2c65f9835a7c13e5250042c5d5f32d7ca1797a9803c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e3addc87463489304620537db0c299cce2825e5a82239eafbc1a0fffad91bd806f8453893fb79ed79cb133501e8b07be035de481380cb0d94fcfa18f28d8a405
|
|
7
|
+
data.tar.gz: 8aa8eb624f5d29d562186d9888c3027886f65f11417e6f34a480bd5dca607a446ff4ea224ecb56c18238b179946ad9dcba2f863d262c690104db0c02e50ba2cd
|
data/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# TelegramBotEngine
|
|
2
|
+
|
|
3
|
+
A mountable Rails engine that adds subscriber management, authorization, broadcasting, and an admin UI on top of the [`telegram-bot`](https://github.com/telegram-bot-rb/telegram-bot) gem (v0.16.x).
|
|
4
|
+
|
|
5
|
+
The `telegram-bot` gem handles all Telegram protocol concerns (API client, webhook ingestion, controller/command routing, callback queries, session, async delivery, and testing). This engine adds the persistence and management layer that `telegram-bot` deliberately doesn't provide.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "telegram_bot_engine"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install migrations:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bin/rails telegram_bot_engine:install:migrations
|
|
19
|
+
bin/rails db:migrate
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
### Bot token
|
|
25
|
+
|
|
26
|
+
Following `telegram-bot`'s convention, configure via Rails credentials:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
# config/credentials.yml.enc
|
|
30
|
+
telegram:
|
|
31
|
+
bot:
|
|
32
|
+
token: "YOUR_BOT_TOKEN"
|
|
33
|
+
username: "your_bot"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Engine configuration
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# config/initializers/telegram_bot_engine.rb
|
|
40
|
+
TelegramBotEngine.configure do |config|
|
|
41
|
+
# Authorization: only these Telegram usernames can /start and subscribe.
|
|
42
|
+
config.allowed_usernames = %w[alice bob charlie]
|
|
43
|
+
# OR dynamic:
|
|
44
|
+
# config.allowed_usernames = -> { MyModel.pluck(:telegram_username) }
|
|
45
|
+
# OR managed via admin UI:
|
|
46
|
+
# config.allowed_usernames = :database
|
|
47
|
+
# OR open access (no allowlist):
|
|
48
|
+
# config.allowed_usernames = nil
|
|
49
|
+
|
|
50
|
+
# Optional: disable admin UI
|
|
51
|
+
# config.admin_enabled = false
|
|
52
|
+
|
|
53
|
+
# Optional: custom messages
|
|
54
|
+
# config.unauthorized_message = "Sorry, you're not authorized to use this bot."
|
|
55
|
+
# config.welcome_message = "Welcome %{username}! Available commands:\n%{commands}"
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Webhook controller
|
|
60
|
+
|
|
61
|
+
Create a controller inheriting from `telegram-bot`'s `UpdatesController` and include the engine's concern:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# app/controllers/telegram_webhook_controller.rb
|
|
65
|
+
class TelegramWebhookController < Telegram::Bot::UpdatesController
|
|
66
|
+
include TelegramBotEngine::SubscriberCommands
|
|
67
|
+
# Provides: start!, stop!, help! with authorization and subscription management.
|
|
68
|
+
|
|
69
|
+
# Add your own commands:
|
|
70
|
+
def status!(*)
|
|
71
|
+
respond_with :message, text: "All systems operational"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Routes
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# config/routes.rb
|
|
80
|
+
Rails.application.routes.draw do
|
|
81
|
+
telegram_webhook TelegramWebhookController
|
|
82
|
+
|
|
83
|
+
# Mount admin UI (protect with your own authentication)
|
|
84
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
85
|
+
mount TelegramBotEngine::Engine, at: "/telegram/admin"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Set webhook
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bin/rails telegram:bot:set_webhook
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
### Broadcasting
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Broadcast to all active subscribers
|
|
102
|
+
TelegramBotEngine.broadcast("Deployment complete!")
|
|
103
|
+
|
|
104
|
+
# With Markdown formatting
|
|
105
|
+
TelegramBotEngine.broadcast(
|
|
106
|
+
"*Deploy complete*\nVersion: `v2.3.4`",
|
|
107
|
+
parse_mode: "Markdown"
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Direct messaging
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
TelegramBotEngine.notify(
|
|
115
|
+
chat_id: 123456789,
|
|
116
|
+
text: "Your report is ready."
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Admin UI
|
|
121
|
+
|
|
122
|
+
When mounted, the engine provides a web interface for:
|
|
123
|
+
- **Dashboard** — bot info, subscription counts
|
|
124
|
+
- **Subscriptions** — list, activate/deactivate, delete
|
|
125
|
+
- **Allowlist** — add/remove usernames (when `config.allowed_usernames = :database`)
|
|
126
|
+
|
|
127
|
+
## Requirements
|
|
128
|
+
|
|
129
|
+
- Ruby >= 3.3.0
|
|
130
|
+
- Rails >= 7.0
|
|
131
|
+
- `telegram-bot` ~> 0.16
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
module Admin
|
|
5
|
+
class AllowlistController < BaseController
|
|
6
|
+
before_action :require_database_mode!
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@allowed_users = AllowedUser.order(:username)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
AllowedUser.create!(allowed_user_params)
|
|
14
|
+
redirect_to admin_allowlist_index_path, notice: "Username added to allowlist."
|
|
15
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
16
|
+
redirect_to admin_allowlist_index_path, alert: e.message
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destroy
|
|
20
|
+
allowed_user = AllowedUser.find(params[:id])
|
|
21
|
+
allowed_user.destroy!
|
|
22
|
+
redirect_to admin_allowlist_index_path, notice: "Username removed from allowlist."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def allowed_user_params
|
|
28
|
+
params.require(:allowed_user).permit(:username, :note)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def require_database_mode!
|
|
32
|
+
unless TelegramBotEngine.config.allowed_usernames == :database
|
|
33
|
+
redirect_to admin_dashboard_path, alert: "Allowlist management is only available in database mode."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
module Admin
|
|
5
|
+
class BaseController < ActionController::Base
|
|
6
|
+
layout "telegram_bot_engine/admin/layouts/application"
|
|
7
|
+
|
|
8
|
+
before_action :check_admin_enabled!
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def check_admin_enabled!
|
|
13
|
+
raise ActionController::RoutingError, "Not Found" unless TelegramBotEngine.config.admin_enabled
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
module Admin
|
|
5
|
+
class DashboardController < BaseController
|
|
6
|
+
def show
|
|
7
|
+
@total_subscriptions = Subscription.count
|
|
8
|
+
@active_subscriptions = Subscription.active.count
|
|
9
|
+
@inactive_subscriptions = @total_subscriptions - @active_subscriptions
|
|
10
|
+
@bot_username = bot_username
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def bot_username
|
|
16
|
+
Telegram.bot.username
|
|
17
|
+
rescue StandardError
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
module Admin
|
|
5
|
+
class SubscriptionsController < BaseController
|
|
6
|
+
def index
|
|
7
|
+
@subscriptions = Subscription.order(created_at: :desc)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def update
|
|
11
|
+
subscription = Subscription.find(params[:id])
|
|
12
|
+
subscription.update!(active: !subscription.active)
|
|
13
|
+
redirect_to admin_subscriptions_path, notice: "Subscription #{subscription.active ? 'activated' : 'deactivated'}."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def destroy
|
|
17
|
+
subscription = Subscription.find(params[:id])
|
|
18
|
+
subscription.destroy!
|
|
19
|
+
redirect_to admin_subscriptions_path, notice: "Subscription deleted."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
class DeliveryJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 3
|
|
7
|
+
|
|
8
|
+
def perform(chat_id, text, options = {})
|
|
9
|
+
Telegram.bot.send_message(
|
|
10
|
+
chat_id: chat_id,
|
|
11
|
+
text: text,
|
|
12
|
+
**options.symbolize_keys
|
|
13
|
+
)
|
|
14
|
+
rescue Telegram::Bot::Forbidden
|
|
15
|
+
# User blocked the bot - deactivate subscription
|
|
16
|
+
TelegramBotEngine::Subscription.where(chat_id: chat_id).update_all(active: false)
|
|
17
|
+
Rails.logger.info("[TelegramBotEngine] Deactivated subscription for blocked chat: #{chat_id}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
class Subscription < ActiveRecord::Base
|
|
5
|
+
self.table_name = "telegram_bot_engine_subscriptions"
|
|
6
|
+
|
|
7
|
+
scope :active, -> { where(active: true) }
|
|
8
|
+
|
|
9
|
+
validates :chat_id, presence: true, uniqueness: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Allowlist</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500">Manage authorized Telegram usernames</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="mb-6 bg-white shadow-sm rounded-lg border border-gray-200 p-6">
|
|
7
|
+
<h2 class="text-sm font-medium text-gray-700 mb-4">Add Username</h2>
|
|
8
|
+
<%= form_with url: telegram_bot_engine.admin_allowlist_index_path, method: :post, class: "flex items-end space-x-4" do |f| %>
|
|
9
|
+
<div class="flex-1">
|
|
10
|
+
<label for="allowed_user_username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
|
11
|
+
<input type="text" name="allowed_user[username]" id="allowed_user_username" required
|
|
12
|
+
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"
|
|
13
|
+
placeholder="telegram_username">
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex-1">
|
|
16
|
+
<label for="allowed_user_note" class="block text-sm font-medium text-gray-700 mb-1">Note (optional)</label>
|
|
17
|
+
<input type="text" name="allowed_user[note]" id="allowed_user_note"
|
|
18
|
+
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"
|
|
19
|
+
placeholder="e.g. Tom - developer">
|
|
20
|
+
</div>
|
|
21
|
+
<div>
|
|
22
|
+
<button type="submit"
|
|
23
|
+
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">
|
|
24
|
+
Add
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
|
|
31
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
32
|
+
<thead class="bg-gray-50">
|
|
33
|
+
<tr>
|
|
34
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
|
|
35
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Note</th>
|
|
36
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Added</th>
|
|
37
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
41
|
+
<% @allowed_users.each do |user| %>
|
|
42
|
+
<tr>
|
|
43
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
44
|
+
@<%= user.username %>
|
|
45
|
+
</td>
|
|
46
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
47
|
+
<%= user.note || "-" %>
|
|
48
|
+
</td>
|
|
49
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
50
|
+
<%= user.created_at.strftime("%Y-%m-%d %H:%M") %>
|
|
51
|
+
</td>
|
|
52
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
|
|
53
|
+
<%= button_to "Remove",
|
|
54
|
+
telegram_bot_engine.admin_allowlist_path(user),
|
|
55
|
+
method: :delete,
|
|
56
|
+
data: { turbo_confirm: "Remove @#{user.username} from the allowlist?" },
|
|
57
|
+
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" %>
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<% end %>
|
|
61
|
+
|
|
62
|
+
<% if @allowed_users.empty? %>
|
|
63
|
+
<tr>
|
|
64
|
+
<td colspan="4" class="px-6 py-12 text-center text-sm text-gray-500">
|
|
65
|
+
No usernames in the allowlist yet.
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
<% end %>
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="mb-8">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<% if @bot_username %>
|
|
6
|
+
<div class="mb-6 bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
7
|
+
<h2 class="text-sm font-medium text-gray-500 uppercase tracking-wider mb-2">Bot</h2>
|
|
8
|
+
<p class="text-lg font-semibold text-gray-900">@<%= @bot_username %></p>
|
|
9
|
+
<a href="https://t.me/<%= @bot_username %>" target="_blank" rel="noopener"
|
|
10
|
+
class="text-sm text-blue-600 hover:text-blue-800">
|
|
11
|
+
t.me/<%= @bot_username %>
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
17
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
18
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wider">Total Subscriptions</h3>
|
|
19
|
+
<p class="mt-2 text-3xl font-bold text-gray-900"><%= @total_subscriptions %></p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
23
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wider">Active</h3>
|
|
24
|
+
<p class="mt-2 text-3xl font-bold text-green-600"><%= @active_subscriptions %></p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
28
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wider">Inactive</h3>
|
|
29
|
+
<p class="mt-2 text-3xl font-bold text-gray-400"><%= @inactive_subscriptions %></p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="flex space-x-4">
|
|
34
|
+
<%= link_to "Manage Subscriptions", telegram_bot_engine.admin_subscriptions_path,
|
|
35
|
+
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
|
+
|
|
37
|
+
<% if TelegramBotEngine.config.allowed_usernames == :database %>
|
|
38
|
+
<%= link_to "Manage Allowlist", telegram_bot_engine.admin_allowlist_index_path,
|
|
39
|
+
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" %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Telegram Bot Engine Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
</head>
|
|
10
|
+
<body class="bg-gray-50 min-h-screen">
|
|
11
|
+
<nav class="bg-white shadow-sm border-b border-gray-200">
|
|
12
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
13
|
+
<div class="flex justify-between h-16">
|
|
14
|
+
<div class="flex items-center space-x-8">
|
|
15
|
+
<span class="text-xl font-bold text-gray-900">Telegram Bot Engine</span>
|
|
16
|
+
<div class="flex space-x-4">
|
|
17
|
+
<%= link_to "Dashboard", telegram_bot_engine.admin_dashboard_path,
|
|
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 "Subscriptions", telegram_bot_engine.admin_subscriptions_path,
|
|
20
|
+
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
|
+
<% if TelegramBotEngine.config.allowed_usernames == :database %>
|
|
22
|
+
<%= link_to "Allowlist", telegram_bot_engine.admin_allowlist_index_path,
|
|
23
|
+
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path.include?('allowlist') ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</nav>
|
|
30
|
+
|
|
31
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
32
|
+
<% if notice %>
|
|
33
|
+
<div class="mb-4 p-4 rounded-md bg-green-50 border border-green-200">
|
|
34
|
+
<p class="text-sm text-green-800"><%= notice %></p>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<% if alert %>
|
|
39
|
+
<div class="mb-4 p-4 rounded-md bg-red-50 border border-red-200">
|
|
40
|
+
<p class="text-sm text-red-800"><%= alert %></p>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
44
|
+
<%= yield %>
|
|
45
|
+
</main>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Subscriptions</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500"><%= @subscriptions.count %> total</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
|
|
7
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
8
|
+
<thead class="bg-gray-50">
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
|
|
11
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">First Name</th>
|
|
12
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chat ID</th>
|
|
13
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
14
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Subscribed</th>
|
|
15
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
19
|
+
<% @subscriptions.each do |subscription| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
22
|
+
<%= subscription.username || "-" %>
|
|
23
|
+
</td>
|
|
24
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
|
25
|
+
<%= subscription.first_name || "-" %>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
|
|
28
|
+
<%= subscription.chat_id %>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
31
|
+
<% if subscription.active? %>
|
|
32
|
+
<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>
|
|
33
|
+
<% else %>
|
|
34
|
+
<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>
|
|
35
|
+
<% end %>
|
|
36
|
+
</td>
|
|
37
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
38
|
+
<%= subscription.created_at.strftime("%Y-%m-%d %H:%M") %>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm space-x-2">
|
|
41
|
+
<%= button_to subscription.active? ? "Deactivate" : "Activate",
|
|
42
|
+
telegram_bot_engine.admin_subscription_path(subscription),
|
|
43
|
+
method: :patch,
|
|
44
|
+
class: "inline-flex items-center px-3 py-1.5 border border-gray-300 rounded text-xs font-medium #{subscription.active? ? 'text-yellow-700 hover:bg-yellow-50' : 'text-green-700 hover:bg-green-50'} bg-white" %>
|
|
45
|
+
<%= button_to "Delete",
|
|
46
|
+
telegram_bot_engine.admin_subscription_path(subscription),
|
|
47
|
+
method: :delete,
|
|
48
|
+
data: { turbo_confirm: "Are you sure you want to delete this subscription?" },
|
|
49
|
+
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" %>
|
|
50
|
+
</td>
|
|
51
|
+
</tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
54
|
+
<% if @subscriptions.empty? %>
|
|
55
|
+
<tr>
|
|
56
|
+
<td colspan="6" class="px-6 py-12 text-center text-sm text-gray-500">
|
|
57
|
+
No subscriptions yet.
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<% end %>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
TelegramBotEngine::Engine.routes.draw do
|
|
4
|
+
scope module: :admin, as: :admin do
|
|
5
|
+
root to: "dashboard#show"
|
|
6
|
+
get "dashboard", to: "dashboard#show", as: :dashboard
|
|
7
|
+
resources :subscriptions, only: %i[index update destroy]
|
|
8
|
+
resources :allowlist, only: %i[index create destroy]
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTelegramBotEngineSubscriptions < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :telegram_bot_engine_subscriptions do |t|
|
|
6
|
+
t.bigint :chat_id, null: false
|
|
7
|
+
t.bigint :user_id
|
|
8
|
+
t.string :username
|
|
9
|
+
t.string :first_name
|
|
10
|
+
t.boolean :active, default: true
|
|
11
|
+
t.jsonb :metadata, default: {}
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :telegram_bot_engine_subscriptions, :chat_id, unique: true
|
|
17
|
+
add_index :telegram_bot_engine_subscriptions, :active
|
|
18
|
+
add_index :telegram_bot_engine_subscriptions, :username
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTelegramBotEngineAllowedUsers < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :telegram_bot_engine_allowed_users do |t|
|
|
6
|
+
t.string :username, null: false
|
|
7
|
+
t.string :note
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :telegram_bot_engine_allowed_users, :username, unique: true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
class Authorizer
|
|
5
|
+
def self.authorized?(username)
|
|
6
|
+
return true if TelegramBotEngine.config.allowed_usernames.nil?
|
|
7
|
+
|
|
8
|
+
allowed = resolve_allowed_usernames
|
|
9
|
+
allowed.map(&:downcase).include?(username&.downcase)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def self.resolve_allowed_usernames
|
|
15
|
+
config = TelegramBotEngine.config.allowed_usernames
|
|
16
|
+
|
|
17
|
+
case config
|
|
18
|
+
when Array
|
|
19
|
+
config
|
|
20
|
+
when Proc
|
|
21
|
+
config.call
|
|
22
|
+
when :database
|
|
23
|
+
TelegramBotEngine::AllowedUser.pluck(:username)
|
|
24
|
+
else
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :allowed_usernames, :admin_enabled, :unauthorized_message, :welcome_message
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@allowed_usernames = nil
|
|
9
|
+
@admin_enabled = true
|
|
10
|
+
@unauthorized_message = "Sorry, you're not authorized to use this bot."
|
|
11
|
+
@welcome_message = "Welcome %{username}! Available commands:\n%{commands}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TelegramBotEngine
|
|
4
|
+
module SubscriberCommands
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :authorize_user!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# /start - create subscription if authorized
|
|
12
|
+
def start!(*)
|
|
13
|
+
subscription = TelegramBotEngine::Subscription.find_or_initialize_by(
|
|
14
|
+
chat_id: chat["id"]
|
|
15
|
+
)
|
|
16
|
+
subscription.assign_attributes(
|
|
17
|
+
user_id: from["id"],
|
|
18
|
+
username: from["username"],
|
|
19
|
+
first_name: from["first_name"],
|
|
20
|
+
active: true
|
|
21
|
+
)
|
|
22
|
+
subscription.save!
|
|
23
|
+
|
|
24
|
+
welcome = TelegramBotEngine.config.welcome_message % {
|
|
25
|
+
username: from["first_name"] || from["username"],
|
|
26
|
+
commands: available_commands_text
|
|
27
|
+
}
|
|
28
|
+
respond_with :message, text: welcome
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# /stop - deactivate subscription
|
|
32
|
+
def stop!(*)
|
|
33
|
+
subscription = TelegramBotEngine::Subscription.find_by(chat_id: chat["id"])
|
|
34
|
+
subscription&.update(active: false)
|
|
35
|
+
respond_with :message, text: "You've been unsubscribed. Send /start to resubscribe."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# /help - list all available commands
|
|
39
|
+
def help!(*)
|
|
40
|
+
respond_with :message, text: "📋 *Available Commands*\n\n#{available_commands_text}", parse_mode: "Markdown"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def authorize_user!
|
|
46
|
+
return if TelegramBotEngine::Authorizer.authorized?(from["username"])
|
|
47
|
+
|
|
48
|
+
respond_with :message, text: TelegramBotEngine.config.unauthorized_message
|
|
49
|
+
throw :abort
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Auto-generates command list from public methods ending with !
|
|
53
|
+
def available_commands_text
|
|
54
|
+
commands = self.class.public_instance_methods(false)
|
|
55
|
+
.select { |m| m.to_s.end_with?("!") }
|
|
56
|
+
.map { |m| "/#{m.to_s.delete_suffix('!')}" }
|
|
57
|
+
|
|
58
|
+
all_commands = ["/start", "/stop", "/help"] | commands
|
|
59
|
+
all_commands.join("\n")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "telegram/bot"
|
|
4
|
+
require "telegram_bot_engine/version"
|
|
5
|
+
require "telegram_bot_engine/configuration"
|
|
6
|
+
require "telegram_bot_engine/authorizer"
|
|
7
|
+
require "telegram_bot_engine/subscriber_commands"
|
|
8
|
+
require "telegram_bot_engine/engine"
|
|
9
|
+
|
|
10
|
+
module TelegramBotEngine
|
|
11
|
+
class << self
|
|
12
|
+
def configure
|
|
13
|
+
yield(config)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def config
|
|
17
|
+
@config ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset_config!
|
|
21
|
+
@config = Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Broadcast to all active subscribers via background jobs
|
|
25
|
+
def broadcast(text, **options)
|
|
26
|
+
TelegramBotEngine::Subscription.active.find_each do |subscription|
|
|
27
|
+
TelegramBotEngine::DeliveryJob.perform_later(
|
|
28
|
+
subscription.chat_id,
|
|
29
|
+
text,
|
|
30
|
+
options
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Send to a specific chat via background job
|
|
36
|
+
def notify(chat_id:, text:, **options)
|
|
37
|
+
TelegramBotEngine::DeliveryJob.perform_later(chat_id, text, options)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: telegram_bot_engine
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TelegramBotEngine Contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: telegram-bot
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.16'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.16'
|
|
40
|
+
description: A mountable Rails engine that adds subscriber persistence, authorization,
|
|
41
|
+
broadcasting, and an admin UI on top of the telegram-bot gem.
|
|
42
|
+
executables: []
|
|
43
|
+
extensions: []
|
|
44
|
+
extra_rdoc_files: []
|
|
45
|
+
files:
|
|
46
|
+
- README.md
|
|
47
|
+
- app/controllers/telegram_bot_engine/admin/allowlist_controller.rb
|
|
48
|
+
- app/controllers/telegram_bot_engine/admin/base_controller.rb
|
|
49
|
+
- app/controllers/telegram_bot_engine/admin/dashboard_controller.rb
|
|
50
|
+
- app/controllers/telegram_bot_engine/admin/subscriptions_controller.rb
|
|
51
|
+
- app/jobs/telegram_bot_engine/delivery_job.rb
|
|
52
|
+
- app/models/telegram_bot_engine/allowed_user.rb
|
|
53
|
+
- app/models/telegram_bot_engine/subscription.rb
|
|
54
|
+
- app/views/telegram_bot_engine/admin/allowlist/index.html.erb
|
|
55
|
+
- app/views/telegram_bot_engine/admin/dashboard/show.html.erb
|
|
56
|
+
- app/views/telegram_bot_engine/admin/layouts/application.html.erb
|
|
57
|
+
- app/views/telegram_bot_engine/admin/subscriptions/index.html.erb
|
|
58
|
+
- config/routes.rb
|
|
59
|
+
- db/migrate/001_create_telegram_bot_engine_subscriptions.rb
|
|
60
|
+
- db/migrate/002_create_telegram_bot_engine_allowed_users.rb
|
|
61
|
+
- lib/tasks/telegram_bot_engine.rake
|
|
62
|
+
- lib/telegram_bot_engine.rb
|
|
63
|
+
- lib/telegram_bot_engine/authorizer.rb
|
|
64
|
+
- lib/telegram_bot_engine/configuration.rb
|
|
65
|
+
- lib/telegram_bot_engine/engine.rb
|
|
66
|
+
- lib/telegram_bot_engine/subscriber_commands.rb
|
|
67
|
+
- lib/telegram_bot_engine/version.rb
|
|
68
|
+
homepage: https://github.com/landovsky/telegram-bot-channels-gem
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata: {}
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.3.0
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 3.6.9
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Rails engine for Telegram bot subscriber management, authorization, broadcasting,
|
|
89
|
+
and admin UI
|
|
90
|
+
test_files: []
|