telegrama 0.2.0 → 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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +23 -0
- data/lib/telegrama/client.rb +55 -10
- data/lib/telegrama/configuration.rb +14 -0
- data/lib/telegrama/formatter.rb +40 -4
- data/lib/telegrama/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ae6c6b2bb6c3da545107d0f91b63e490fbd6e7e5a6e6be633291346c9f6ebcb
|
|
4
|
+
data.tar.gz: 42b7590b77f6497a8b972a97a1bb8cd371f89bf5db322bcd779b7d0a7ae9a331
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 596ad2ca19bb16c3c50d83f0ee7891e4a0ffe44218f34de187497ddb23a58862695564183402abe3b13e290801dc3bcaf033e00330e26b5faf5da2d97eb6e3bd
|
|
7
|
+
data.tar.gz: 810fe478ac180741ca83d2210eb179776ad815e647297cb037ad1d33a49cf0a81630bc27ac11a4393309ef4be23b23f330f0b75754c60cacaf37b78668adb658
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.3.0] - 2026-05-22
|
|
4
|
+
|
|
5
|
+
- Added support for sending messages to Telegram forum topics with `message_thread_id`
|
|
6
|
+
- Added `config.message_thread_id` as an optional default forum topic for all messages
|
|
7
|
+
- Added per-message `message_thread_id:` overrides, including `message_thread_id: nil`
|
|
8
|
+
to bypass a configured default topic for one send
|
|
9
|
+
- Preserved `message_thread_id` through async ActiveJob delivery, string-key serialized
|
|
10
|
+
options, and Markdown-to-HTML-to-plain-text fallback retries
|
|
11
|
+
- Added validation so `message_thread_id` must be a positive integer when present
|
|
12
|
+
- Fixed `parse_mode: nil` to omit the `parse_mode` parameter from Telegram API
|
|
13
|
+
requests instead of sending `null`
|
|
14
|
+
- Fixed plain-text sends so `parse_mode: nil` does not inherit MarkdownV2 or HTML
|
|
15
|
+
escaping from configured formatting defaults
|
|
16
|
+
- Fixed HTML sends so they do not inherit MarkdownV2 escaping from configured
|
|
17
|
+
formatting defaults
|
|
18
|
+
- Fixed MarkdownV2 escaping for underscores inside identifiers such as `is_bot`,
|
|
19
|
+
`message_thread_id`, and `send_message`
|
|
20
|
+
|
|
1
21
|
## [0.2.0] - 2026-01-17
|
|
2
22
|
|
|
3
23
|
- Added Minitest test suite
|
data/README.md
CHANGED
|
@@ -53,6 +53,12 @@ Telegrama.send_message(a_general_message, chat_id: general_chat_id)
|
|
|
53
53
|
Telegrama.send_message(marketing_message, chat_id: marketing_chat_id)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
You can also send messages to a specific topic inside a forum-enabled Telegram group:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Telegrama.send_message(deploy_message, chat_id: engineering_group_id, message_thread_id: deployments_topic_id)
|
|
60
|
+
```
|
|
61
|
+
|
|
56
62
|
The goal with this gem is to provide a straightforward, no-frills, minimal API to send Telegram messages reliably for admin purposes, without you having to write your own wrapper over the Telegram API.
|
|
57
63
|
|
|
58
64
|
## Quick start
|
|
@@ -75,6 +81,7 @@ Then, create an initializer file under `config/initializers/telegrama.rb` and se
|
|
|
75
81
|
Telegrama.configure do |config|
|
|
76
82
|
config.bot_token = Rails.application.credentials.dig(Rails.env.to_sym, :telegram, :bot_token)
|
|
77
83
|
config.chat_id = Rails.application.credentials.dig(Rails.env.to_sym, :telegram, :chat_id)
|
|
84
|
+
config.message_thread_id = nil # Optional default Telegram forum topic ID
|
|
78
85
|
config.default_parse_mode = 'MarkdownV2'
|
|
79
86
|
|
|
80
87
|
# Optional prefix/suffix for all messages (useful to identify messages from different apps or environments)
|
|
@@ -163,6 +170,22 @@ Both `message_prefix` and `message_suffix` are optional and can be used independ
|
|
|
163
170
|
Telegrama.send_message("Hello, alternate group!", chat_id: alternate_chat_id)
|
|
164
171
|
```
|
|
165
172
|
|
|
173
|
+
- **`message_thread_id`**
|
|
174
|
+
*Send to a specific Telegram forum topic inside a group. Telegram's Bot API calls topic IDs `message_thread_id` values.*
|
|
175
|
+
**Usage Example:**
|
|
176
|
+
```ruby
|
|
177
|
+
Telegrama.send_message("Deploy finished!", chat_id: engineering_group_id, message_thread_id: deployments_topic_id)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
You can get this ID from an incoming Telegram update sent inside the topic, or from the `ForumTopic` returned by Telegram's `createForumTopic` API.
|
|
181
|
+
|
|
182
|
+
You can also configure a default topic:
|
|
183
|
+
```ruby
|
|
184
|
+
config.message_thread_id = deployments_topic_id
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To bypass a configured default topic for one message, pass `message_thread_id: nil`.
|
|
188
|
+
|
|
166
189
|
- **`parse_mode`**
|
|
167
190
|
*Override the default parse mode (default is `"MarkdownV2"`).*
|
|
168
191
|
**Usage Example:**
|
data/lib/telegrama/client.rb
CHANGED
|
@@ -13,9 +13,20 @@ module Telegrama
|
|
|
13
13
|
|
|
14
14
|
# Send a message with built-in error handling and fallbacks
|
|
15
15
|
def send_message(message, options = {})
|
|
16
|
+
options = normalize_option_keys(options)
|
|
17
|
+
|
|
16
18
|
# Allow chat ID override; fallback to config default
|
|
17
19
|
chat_id = options.delete(:chat_id) || Telegrama.configuration.chat_id
|
|
18
20
|
|
|
21
|
+
# Allow topic/thread override; fallback to config default.
|
|
22
|
+
# Explicit nil means "send to the chat normally", even when a default topic is configured.
|
|
23
|
+
message_thread_id = if options.key?(:message_thread_id)
|
|
24
|
+
options.delete(:message_thread_id)
|
|
25
|
+
else
|
|
26
|
+
Telegrama.configuration.message_thread_id
|
|
27
|
+
end
|
|
28
|
+
validate_message_thread_id!(message_thread_id)
|
|
29
|
+
|
|
19
30
|
# Get client options from config
|
|
20
31
|
client_opts = Telegrama.configuration.client_options || {}
|
|
21
32
|
client_opts = client_opts.merge(@config)
|
|
@@ -24,15 +35,11 @@ module Telegrama
|
|
|
24
35
|
# Use key? to allow explicit nil override (for plain text without formatting)
|
|
25
36
|
parse_mode = options.key?(:parse_mode) ? options[:parse_mode] : Telegrama.configuration.default_parse_mode
|
|
26
37
|
|
|
27
|
-
# Allow runtime formatting options, merging with configured defaults
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
formatting_opts[:escape_markdown] = true unless formatting_opts.key?(:escape_markdown)
|
|
33
|
-
elsif parse_mode == 'HTML'
|
|
34
|
-
formatting_opts[:escape_html] = true unless formatting_opts.key?(:escape_html)
|
|
35
|
-
end
|
|
38
|
+
# Allow runtime formatting options, merging with configured defaults.
|
|
39
|
+
# Escape behavior must match the outgoing Telegram parse mode; otherwise
|
|
40
|
+
# plain-text sends can display MarkdownV2 escape characters literally.
|
|
41
|
+
formatting_opts = normalize_option_keys(options.delete(:formatting) || {})
|
|
42
|
+
formatting_opts = formatting_options_for_parse_mode(parse_mode, formatting_opts)
|
|
36
43
|
|
|
37
44
|
# Format the message text with our formatter
|
|
38
45
|
formatted_message = Formatter.format(message, formatting_opts)
|
|
@@ -46,10 +53,11 @@ module Telegrama
|
|
|
46
53
|
payload = {
|
|
47
54
|
chat_id: chat_id,
|
|
48
55
|
text: formatted_message,
|
|
49
|
-
parse_mode: parse_mode,
|
|
50
56
|
disable_web_page_preview: options.fetch(:disable_web_page_preview,
|
|
51
57
|
Telegrama.configuration.disable_web_page_preview)
|
|
52
58
|
}
|
|
59
|
+
payload[:parse_mode] = parse_mode unless parse_mode.nil?
|
|
60
|
+
payload[:message_thread_id] = message_thread_id unless message_thread_id.nil?
|
|
53
61
|
|
|
54
62
|
# Additional options such as reply_markup can be added here
|
|
55
63
|
payload.merge!(options.select { |k, _| [:reply_markup, :reply_to_message_id].include?(k) })
|
|
@@ -115,6 +123,43 @@ module Telegrama
|
|
|
115
123
|
|
|
116
124
|
private
|
|
117
125
|
|
|
126
|
+
def normalize_option_keys(options)
|
|
127
|
+
return {} if options.nil?
|
|
128
|
+
|
|
129
|
+
options.to_h.each_with_object({}) do |(key, value), normalized|
|
|
130
|
+
normalized[key.respond_to?(:to_sym) ? key.to_sym : key] = value
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_message_thread_id!(message_thread_id)
|
|
135
|
+
return if message_thread_id.nil?
|
|
136
|
+
|
|
137
|
+
unless message_thread_id.is_a?(Integer) && message_thread_id.positive?
|
|
138
|
+
raise ArgumentError, "Telegrama send_message option error: message_thread_id must be a positive integer."
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def formatting_options_for_parse_mode(parse_mode, formatting_opts)
|
|
143
|
+
case parse_mode
|
|
144
|
+
when 'MarkdownV2'
|
|
145
|
+
{
|
|
146
|
+
escape_html: false
|
|
147
|
+
}.merge(formatting_opts).tap do |opts|
|
|
148
|
+
opts[:escape_markdown] = true unless opts.key?(:escape_markdown)
|
|
149
|
+
end
|
|
150
|
+
when 'HTML'
|
|
151
|
+
{
|
|
152
|
+
escape_markdown: false
|
|
153
|
+
}.merge(formatting_opts).tap do |opts|
|
|
154
|
+
opts[:escape_html] = true unless opts.key?(:escape_html)
|
|
155
|
+
end
|
|
156
|
+
when nil
|
|
157
|
+
formatting_opts.merge(escape_markdown: false, escape_html: false)
|
|
158
|
+
else
|
|
159
|
+
formatting_opts
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
118
163
|
def perform_request(payload, options = {})
|
|
119
164
|
uri = URI("https://api.telegram.org/bot#{Telegrama.configuration.bot_token}/sendMessage")
|
|
120
165
|
request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
|
|
@@ -10,6 +10,10 @@ module Telegrama
|
|
|
10
10
|
# You can override this on the fly when sending messages.
|
|
11
11
|
attr_accessor :chat_id
|
|
12
12
|
|
|
13
|
+
# Default message thread ID for sending messages to a forum topic.
|
|
14
|
+
# You can override this on the fly when sending messages.
|
|
15
|
+
attr_accessor :message_thread_id
|
|
16
|
+
|
|
13
17
|
# Default parse mode for messages (e.g. "MarkdownV2" or "HTML").
|
|
14
18
|
attr_accessor :default_parse_mode
|
|
15
19
|
|
|
@@ -57,6 +61,7 @@ module Telegrama
|
|
|
57
61
|
# Credentials (must be set via initializer)
|
|
58
62
|
@bot_token = nil
|
|
59
63
|
@chat_id = nil
|
|
64
|
+
@message_thread_id = nil
|
|
60
65
|
|
|
61
66
|
# Defaults for message formatting
|
|
62
67
|
@default_parse_mode = 'MarkdownV2'
|
|
@@ -90,6 +95,7 @@ module Telegrama
|
|
|
90
95
|
def validate!
|
|
91
96
|
validate_bot_token!
|
|
92
97
|
validate_default_parse_mode!
|
|
98
|
+
validate_message_thread_id!
|
|
93
99
|
validate_formatting_options!
|
|
94
100
|
validate_client_options!
|
|
95
101
|
true
|
|
@@ -110,6 +116,14 @@ module Telegrama
|
|
|
110
116
|
end
|
|
111
117
|
end
|
|
112
118
|
|
|
119
|
+
def validate_message_thread_id!
|
|
120
|
+
return if message_thread_id.nil?
|
|
121
|
+
|
|
122
|
+
unless message_thread_id.is_a?(Integer) && message_thread_id.positive?
|
|
123
|
+
raise ArgumentError, "Telegrama configuration error: message_thread_id must be a positive integer."
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
113
127
|
def validate_formatting_options!
|
|
114
128
|
unless formatting_options.is_a?(Hash)
|
|
115
129
|
raise ArgumentError, "Telegrama configuration error: formatting_options must be a hash."
|
data/lib/telegrama/formatter.rb
CHANGED
|
@@ -179,8 +179,12 @@ module Telegrama
|
|
|
179
179
|
@result += '*'
|
|
180
180
|
advance
|
|
181
181
|
elsif char == '_' && !escaped?
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
if italic_opening_delimiter?
|
|
183
|
+
enter_state(:italic)
|
|
184
|
+
@result += '_'
|
|
185
|
+
else
|
|
186
|
+
@result += '\\_'
|
|
187
|
+
end
|
|
184
188
|
advance
|
|
185
189
|
elsif char == '[' && !escaped?
|
|
186
190
|
if looking_at_markdown_link?
|
|
@@ -256,8 +260,12 @@ module Telegrama
|
|
|
256
260
|
char = current_char
|
|
257
261
|
|
|
258
262
|
if char == '_' && !escaped?
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
if italic_closing_delimiter?
|
|
264
|
+
exit_state
|
|
265
|
+
@result += '_'
|
|
266
|
+
else
|
|
267
|
+
@result += '\\_'
|
|
268
|
+
end
|
|
261
269
|
advance
|
|
262
270
|
elsif char == '\\' && !escaped?
|
|
263
271
|
handle_escape_sequence
|
|
@@ -389,6 +397,34 @@ module Telegrama
|
|
|
389
397
|
@chars[@position + 1]
|
|
390
398
|
end
|
|
391
399
|
|
|
400
|
+
# Get the previous character
|
|
401
|
+
def previous_char
|
|
402
|
+
@position.positive? ? @chars[@position - 1] : nil
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Underscores inside identifiers should render literally, not start italics.
|
|
406
|
+
def italic_opening_delimiter?
|
|
407
|
+
following = next_char
|
|
408
|
+
return false if following.nil? || whitespace?(following)
|
|
409
|
+
|
|
410
|
+
!word_char?(previous_char)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def italic_closing_delimiter?
|
|
414
|
+
previous = previous_char
|
|
415
|
+
return false if previous.nil? || whitespace?(previous)
|
|
416
|
+
|
|
417
|
+
!word_char?(next_char)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def word_char?(char)
|
|
421
|
+
!!(char =~ /[[:alnum:]]/)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def whitespace?(char)
|
|
425
|
+
!!(char =~ /\s/)
|
|
426
|
+
end
|
|
427
|
+
|
|
392
428
|
# Check if next character is one of the given characters
|
|
393
429
|
def next_char_is?(*chars)
|
|
394
430
|
has_chars_ahead?(1) && chars.include?(next_char)
|
data/lib/telegrama/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: telegrama
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Javi R
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|