telegram_bot_engine 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3d331110ead0fd92e4fee373b4fa5429479116cd9e518a367aea45cdda067df
4
- data.tar.gz: cb35bb73052d715549a8146cde0ab25d5d990f7b20840ae28859633ab6265609
3
+ metadata.gz: 5b3bf5d33786a7847de84d3b85308001301ea88d642d8b8e881341be9bd311d1
4
+ data.tar.gz: 71a00b7a640dfd91c79b6080a3295ab6621fc938a05504f94c28f9757a655c59
5
5
  SHA512:
6
- metadata.gz: 7b02b7c338ee4dbd4db036e32e2f77fb909aabe9a64e7ed19e9bb2a82b87ee4c8f3dd321e79772aa52d99dc637f986c02d3d7273d2279be1f6be69e22c638e30
7
- data.tar.gz: d0d8c3bc21441fe79c1e7dd14cfd2ca9a452e2733f6fc0d08b478fadb9322a74968b3d0c0f974189bd20803339020409489f8ae960b70bf93b0381923cde285c
6
+ metadata.gz: 4d892f84a28953e2bc78c157ae9169ba3907e0482b15c79ae3068b2d7c43514d3014ee46ca415e33aaaa94c72d1c9470a9e139066ea816c17844711373d7d18f
7
+ data.tar.gz: f9154fdc913b4fe766d89a9e5a6fcca88333a192e1e71c233972c0d1335d7641ef0f30fe2554a1ad16e37920c36e5a91b53a3600f7c754f3eb8e74890cd3c2cf
data/README.md CHANGED
@@ -53,6 +53,10 @@ TelegramBotEngine.configure do |config|
53
53
  # Optional: custom messages
54
54
  # config.unauthorized_message = "Sorry, you're not authorized to use this bot."
55
55
  # config.welcome_message = "Welcome %{username}! Available commands:\n%{commands}"
56
+
57
+ # Event logging — logs commands, deliveries, auth failures to the database
58
+ # config.event_logging = true # default: true
59
+ # config.event_retention_days = 30 # default: 30, auto-purges older events
56
60
  end
57
61
  ```
58
62
 
@@ -149,6 +153,35 @@ When mounted, the engine provides a web interface for:
149
153
  - **Dashboard** — bot info, subscription counts
150
154
  - **Subscriptions** — list, activate/deactivate, delete
151
155
  - **Allowlist** — add/remove usernames (when `config.allowed_usernames = :database`)
156
+ - **Events** — browsable log of commands, deliveries, and auth failures with filtering by type, action, and chat ID
157
+
158
+ ### Event log
159
+
160
+ The engine automatically logs operational events to the `telegram_bot_engine_events` table:
161
+
162
+ | Event type | Actions | When |
163
+ |---|---|---|
164
+ | `command` | `start`, `stop`, `help` | User sends a bot command |
165
+ | `delivery` | `broadcast`, `notify`, `delivered`, `blocked` | Messages are queued or delivered |
166
+ | `auth_failure` | `unauthorized` | Unauthorized user attempts a command |
167
+
168
+ Events are viewable in the admin UI and can be queried directly:
169
+
170
+ ```ruby
171
+ # Recent command events
172
+ TelegramBotEngine::Event.by_type("command").recent.limit(20)
173
+
174
+ # Deliveries to a specific chat
175
+ TelegramBotEngine::Event.by_type("delivery").by_chat_id(123456789)
176
+
177
+ # Events in the last 24 hours
178
+ TelegramBotEngine::Event.since(24.hours.ago)
179
+
180
+ # Manual purge (automatic purge runs probabilistically)
181
+ TelegramBotEngine::Event.purge_old!
182
+ ```
183
+
184
+ Disable event logging entirely with `config.event_logging = false`.
152
185
 
153
186
  ## Requirements
154
187
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TelegramBotEngine
4
+ module Admin
5
+ class EventsController < BaseController
6
+ PER_PAGE = 50
7
+
8
+ def index
9
+ @events = Event.recent
10
+ @events = @events.by_type(params[:type]) if params[:type].present?
11
+ @events = @events.by_action(params[:action_name]) if params[:action_name].present?
12
+ @events = @events.by_chat_id(params[:chat_id]) if params[:chat_id].present?
13
+
14
+ @total_count = @events.count
15
+ @page = [params[:page].to_i, 1].max
16
+ @events = @events.offset((@page - 1) * PER_PAGE).limit(PER_PAGE)
17
+ @total_pages = (@total_count.to_f / PER_PAGE).ceil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -11,10 +11,22 @@ module TelegramBotEngine
11
11
  text: text,
12
12
  **options.symbolize_keys
13
13
  )
14
+
15
+ TelegramBotEngine::Event.log(
16
+ event_type: "delivery", action: "delivered",
17
+ chat_id: chat_id,
18
+ details: { text_preview: text.to_s[0, 100] }
19
+ )
14
20
  rescue Telegram::Bot::Forbidden
15
21
  # User blocked the bot - deactivate subscription
16
22
  TelegramBotEngine::Subscription.where(chat_id: chat_id).update_all(active: false)
17
23
  Rails.logger.info("[TelegramBotEngine] Deactivated subscription for blocked chat: #{chat_id}")
24
+
25
+ TelegramBotEngine::Event.log(
26
+ event_type: "delivery", action: "blocked",
27
+ chat_id: chat_id,
28
+ details: { text_preview: text.to_s[0, 100] }
29
+ )
18
30
  end
19
31
  end
20
32
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TelegramBotEngine
4
+ class Event < ActiveRecord::Base
5
+ self.table_name = "telegram_bot_engine_events"
6
+
7
+ scope :recent, -> { order(created_at: :desc) }
8
+ scope :by_type, ->(type) { where(event_type: type) if type.present? }
9
+ scope :by_action, ->(action) { where(action: action) if action.present? }
10
+ scope :by_chat_id, ->(chat_id) { where(chat_id: chat_id) if chat_id.present? }
11
+ scope :since, ->(time) { where("created_at >= ?", time) }
12
+
13
+ validates :event_type, presence: true
14
+ validates :action, presence: true
15
+
16
+ def self.log(event_type:, action:, chat_id: nil, username: nil, details: {})
17
+ return unless TelegramBotEngine.config.event_logging
18
+
19
+ create!(
20
+ event_type: event_type,
21
+ action: action,
22
+ chat_id: chat_id,
23
+ username: username,
24
+ details: details
25
+ )
26
+
27
+ purge_old_randomly!
28
+ end
29
+
30
+ def self.purge_old!
31
+ where("created_at < ?", TelegramBotEngine.config.event_retention_days.days.ago).delete_all
32
+ end
33
+
34
+ def self.purge_old_randomly!
35
+ purge_old! if rand(100).zero?
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,115 @@
1
+ <div class="mb-6">
2
+ <h1 class="text-2xl font-bold text-gray-900">Events</h1>
3
+ <p class="mt-1 text-sm text-gray-500"><%= @total_count %> total</p>
4
+ </div>
5
+
6
+ <div class="mb-6 bg-white shadow-sm rounded-lg border border-gray-200 p-6">
7
+ <%= form_with url: telegram_bot_engine.admin_events_path, method: :get, local: true, class: "flex items-end space-x-4" do %>
8
+ <div>
9
+ <label for="type" class="block text-sm font-medium text-gray-700 mb-1">Event Type</label>
10
+ <select name="type" id="type" class="block rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border">
11
+ <option value="">All</option>
12
+ <% %w[command delivery auth_failure subscription_change].each do |t| %>
13
+ <option value="<%= t %>" <%= "selected" if params[:type] == t %>><%= t %></option>
14
+ <% end %>
15
+ </select>
16
+ </div>
17
+ <div>
18
+ <label for="action_name" class="block text-sm font-medium text-gray-700 mb-1">Action</label>
19
+ <select name="action_name" id="action_name" class="block rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm px-3 py-2 border">
20
+ <option value="">All</option>
21
+ <% %w[start stop help broadcast notify delivered blocked failed unauthorized].each do |a| %>
22
+ <option value="<%= a %>" <%= "selected" if params[:action_name] == a %>><%= a %></option>
23
+ <% end %>
24
+ </select>
25
+ </div>
26
+ <div>
27
+ <label for="chat_id" class="block text-sm font-medium text-gray-700 mb-1">Chat ID</label>
28
+ <input type="text" name="chat_id" id="chat_id" value="<%= params[:chat_id] %>"
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
+ placeholder="e.g. 12345">
31
+ </div>
32
+ <div>
33
+ <button type="submit"
34
+ 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">
35
+ Filter
36
+ </button>
37
+ </div>
38
+ <% end %>
39
+ </div>
40
+
41
+ <div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
42
+ <table class="min-w-full divide-y divide-gray-200">
43
+ <thead class="bg-gray-50">
44
+ <tr>
45
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timestamp</th>
46
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
47
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
48
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
49
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chat ID</th>
50
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Details</th>
51
+ </tr>
52
+ </thead>
53
+ <tbody class="bg-white divide-y divide-gray-200">
54
+ <% @events.each do |event| %>
55
+ <tr>
56
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
57
+ <%= event.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
58
+ </td>
59
+ <td class="px-6 py-4 whitespace-nowrap">
60
+ <%
61
+ badge_colors = {
62
+ "command" => "bg-blue-100 text-blue-800",
63
+ "delivery" => "bg-green-100 text-green-800",
64
+ "auth_failure" => "bg-red-100 text-red-800",
65
+ "subscription_change" => "bg-yellow-100 text-yellow-800"
66
+ }
67
+ color = badge_colors[event.event_type] || "bg-gray-100 text-gray-800"
68
+ %>
69
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= color %>">
70
+ <%= event.event_type %>
71
+ </span>
72
+ </td>
73
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
74
+ <%= event.action %>
75
+ </td>
76
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
77
+ <%= event.username || "-" %>
78
+ </td>
79
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">
80
+ <%= event.chat_id || "-" %>
81
+ </td>
82
+ <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate" title="<%= event.details.to_json %>">
83
+ <%= event.details.present? ? event.details.to_json.truncate(80) : "-" %>
84
+ </td>
85
+ </tr>
86
+ <% end %>
87
+
88
+ <% if @events.empty? %>
89
+ <tr>
90
+ <td colspan="6" class="px-6 py-12 text-center text-sm text-gray-500">
91
+ No events found.
92
+ </td>
93
+ </tr>
94
+ <% end %>
95
+ </tbody>
96
+ </table>
97
+ </div>
98
+
99
+ <% if @total_pages > 1 %>
100
+ <div class="mt-4 flex justify-between items-center">
101
+ <div class="text-sm text-gray-500">
102
+ Page <%= @page %> of <%= @total_pages %>
103
+ </div>
104
+ <div class="flex space-x-2">
105
+ <% 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),
107
+ 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
+ <% end %>
109
+ <% 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),
111
+ 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
+ <% end %>
113
+ </div>
114
+ </div>
115
+ <% end %>
@@ -22,6 +22,8 @@
22
22
  <%= link_to "Allowlist", telegram_bot_engine.admin_allowlist_index_path,
23
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
24
  <% end %>
25
+ <%= link_to "Events", telegram_bot_engine.admin_events_path,
26
+ class: "px-3 py-2 rounded-md text-sm font-medium #{request.path.include?('events') ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" %>
25
27
  </div>
26
28
  </div>
27
29
  </div>
data/config/routes.rb CHANGED
@@ -6,5 +6,6 @@ TelegramBotEngine::Engine.routes.draw do
6
6
  get "dashboard", to: "dashboard#show", as: :dashboard
7
7
  resources :subscriptions, only: %i[index update destroy]
8
8
  resources :allowlist, only: %i[index create destroy]
9
+ resources :events, only: %i[index]
9
10
  end
10
11
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateTelegramBotEngineEvents < ActiveRecord::Migration[7.0]
4
+ def adapter_type
5
+ ActiveRecord::Base.connection.adapter_name.downcase.include?("postgresql") ? :jsonb : :json
6
+ end
7
+
8
+ def change
9
+ create_table :telegram_bot_engine_events do |t|
10
+ t.string :event_type, null: false
11
+ t.string :action, null: false
12
+ t.bigint :chat_id
13
+ t.string :username
14
+ t.column :details, adapter_type, default: {}
15
+ t.datetime :created_at, null: false
16
+ end
17
+
18
+ add_index :telegram_bot_engine_events, :event_type
19
+ add_index :telegram_bot_engine_events, :created_at
20
+ add_index :telegram_bot_engine_events, :chat_id
21
+ end
22
+ end
@@ -2,13 +2,16 @@
2
2
 
3
3
  module TelegramBotEngine
4
4
  class Configuration
5
- attr_accessor :allowed_usernames, :admin_enabled, :unauthorized_message, :welcome_message
5
+ attr_accessor :allowed_usernames, :admin_enabled, :unauthorized_message, :welcome_message,
6
+ :event_logging, :event_retention_days
6
7
 
7
8
  def initialize
8
9
  @allowed_usernames = nil
9
10
  @admin_enabled = true
10
11
  @unauthorized_message = "Sorry, you're not authorized to use this bot."
11
12
  @welcome_message = "Welcome %{username}! Available commands:\n%{commands}"
13
+ @event_logging = true
14
+ @event_retention_days = 30
12
15
  end
13
16
  end
14
17
  end
@@ -22,6 +22,11 @@ module TelegramBotEngine
22
22
  )
23
23
  subscription.save!
24
24
 
25
+ TelegramBotEngine::Event.log(
26
+ event_type: "command", action: "start",
27
+ chat_id: chat["id"], username: from["username"]
28
+ )
29
+
25
30
  welcome = TelegramBotEngine.config.welcome_message % {
26
31
  username: from["first_name"] || from["username"],
27
32
  commands: available_commands_text
@@ -33,11 +38,22 @@ module TelegramBotEngine
33
38
  def stop!(*)
34
39
  subscription = TelegramBotEngine::Subscription.find_by(chat_id: chat["id"])
35
40
  subscription&.update(active: false)
41
+
42
+ TelegramBotEngine::Event.log(
43
+ event_type: "command", action: "stop",
44
+ chat_id: chat["id"], username: from["username"]
45
+ )
46
+
36
47
  respond_with :message, text: "You've been unsubscribed. Send /start to resubscribe."
37
48
  end
38
49
 
39
50
  # /help - list all available commands
40
51
  def help!(*)
52
+ TelegramBotEngine::Event.log(
53
+ event_type: "command", action: "help",
54
+ chat_id: chat["id"], username: from["username"]
55
+ )
56
+
41
57
  respond_with :message, text: "📋 *Available Commands*\n\n#{available_commands_text}", parse_mode: "Markdown"
42
58
  end
43
59
 
@@ -46,6 +62,11 @@ module TelegramBotEngine
46
62
  def authorize_user!
47
63
  return if TelegramBotEngine::Authorizer.authorized?(from["username"])
48
64
 
65
+ TelegramBotEngine::Event.log(
66
+ event_type: "auth_failure", action: "unauthorized",
67
+ chat_id: chat["id"], username: from["username"]
68
+ )
69
+
49
70
  respond_with :message, text: TelegramBotEngine.config.unauthorized_message
50
71
  throw :abort
51
72
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TelegramBotEngine
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -23,18 +23,31 @@ module TelegramBotEngine
23
23
 
24
24
  # Broadcast to all active subscribers via background jobs
25
25
  def broadcast(text, **options)
26
+ subscriber_count = 0
26
27
  TelegramBotEngine::Subscription.active.find_each do |subscription|
27
28
  TelegramBotEngine::DeliveryJob.perform_later(
28
29
  subscription.chat_id,
29
30
  text,
30
31
  options
31
32
  )
33
+ subscriber_count += 1
32
34
  end
35
+
36
+ TelegramBotEngine::Event.log(
37
+ event_type: "delivery", action: "broadcast",
38
+ details: { subscriber_count: subscriber_count, text_preview: text.to_s[0, 100] }
39
+ )
33
40
  end
34
41
 
35
42
  # Send to a specific chat via background job
36
43
  def notify(chat_id:, text:, **options)
37
44
  TelegramBotEngine::DeliveryJob.perform_later(chat_id, text, options)
45
+
46
+ TelegramBotEngine::Event.log(
47
+ event_type: "delivery", action: "notify",
48
+ chat_id: chat_id,
49
+ details: { text_preview: text.to_s[0, 100] }
50
+ )
38
51
  end
39
52
  end
40
53
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegram_bot_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TelegramBotEngine Contributors
@@ -47,17 +47,21 @@ files:
47
47
  - app/controllers/telegram_bot_engine/admin/allowlist_controller.rb
48
48
  - app/controllers/telegram_bot_engine/admin/base_controller.rb
49
49
  - app/controllers/telegram_bot_engine/admin/dashboard_controller.rb
50
+ - app/controllers/telegram_bot_engine/admin/events_controller.rb
50
51
  - app/controllers/telegram_bot_engine/admin/subscriptions_controller.rb
51
52
  - app/jobs/telegram_bot_engine/delivery_job.rb
52
53
  - app/models/telegram_bot_engine/allowed_user.rb
54
+ - app/models/telegram_bot_engine/event.rb
53
55
  - app/models/telegram_bot_engine/subscription.rb
54
56
  - app/views/telegram_bot_engine/admin/allowlist/index.html.erb
55
57
  - app/views/telegram_bot_engine/admin/dashboard/show.html.erb
58
+ - app/views/telegram_bot_engine/admin/events/index.html.erb
56
59
  - app/views/telegram_bot_engine/admin/layouts/application.html.erb
57
60
  - app/views/telegram_bot_engine/admin/subscriptions/index.html.erb
58
61
  - config/routes.rb
59
62
  - db/migrate/001_create_telegram_bot_engine_subscriptions.rb
60
63
  - db/migrate/002_create_telegram_bot_engine_allowed_users.rb
64
+ - db/migrate/003_create_telegram_bot_engine_events.rb
61
65
  - lib/tasks/telegram_bot_engine.rake
62
66
  - lib/telegram_bot_engine.rb
63
67
  - lib/telegram_bot_engine/authorizer.rb