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 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TelegramBotEngine
4
+ class AllowedUser < ActiveRecord::Base
5
+ self.table_name = "telegram_bot_engine_allowed_users"
6
+
7
+ validates :username, presence: true, uniqueness: true
8
+ end
9
+ 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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standard Rails engine tasks are provided automatically.
4
+ # Custom rake tasks for telegram_bot_engine can be added here.
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TelegramBotEngine
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace TelegramBotEngine
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+ end
11
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TelegramBotEngine
4
+ VERSION = "0.1.0"
5
+ 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: []