telegram-support-bot 0.1.06 → 0.1.07
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/.idea/workspace.xml +26 -24
- data/CHANGELOG.md +21 -0
- data/README.md +72 -1
- data/lib/telegram_support_bot/adapter_factory.rb +12 -3
- data/lib/telegram_support_bot/adapters/base.rb +4 -0
- data/lib/telegram_support_bot/adapters/telegram_bot.rb +9 -0
- data/lib/telegram_support_bot/adapters/telegram_bot_ruby.rb +4 -0
- data/lib/telegram_support_bot/version.rb +1 -1
- data/lib/telegram_support_bot.rb +234 -3
- data/script/dev_poll.rb +102 -0
- metadata +8 -7
- data/telegram_support_bot.gemspec +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0dba7130ca446bc645510735687aa8501f31a6671cfb63fc29e7eb5e535c957
|
|
4
|
+
data.tar.gz: 3fa484d6c00969af671bdce6e964fd1484fce7ff96496aac9e92c3cd7e694dba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c23fe383ec0166b10ab54edf9b536a83bc8973afbd9d373f04e9d281def477682406fd7b665f31c52b622b6e52ab33e47a7478c2193bc28e0a1a9139b06da840
|
|
7
|
+
data.tar.gz: ede12ddc03a8cb4cf213507d5fa8e35faeed2bb78d5c9dc76dfa1978901164130842f055b8b66fa604b2e9d3cf3a43881752ece10a94dd2063f09a92ebce19cd
|
data/.idea/workspace.xml
CHANGED
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
<component name="ChangeListManager">
|
|
7
7
|
<list default="true" id="edf498b0-8552-42f1-846d-0c79d29ff991" name="Changes" comment="">
|
|
8
8
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
9
|
-
<change beforePath="$PROJECT_DIR$/
|
|
10
|
-
<change beforePath="$PROJECT_DIR$/lib/telegram_support_bot/adapters/telegram_bot_adapter.rb" beforeDir="false" afterPath="$PROJECT_DIR$/lib/telegram_support_bot/adapters/telegram_bot_adapter.rb" afterDir="false" />
|
|
11
|
-
<change beforePath="$PROJECT_DIR$/spec/telegram_support_bot/adapters/telegram_bot_adapter_spec.rb" beforeDir="false" afterPath="$PROJECT_DIR$/spec/telegram_support_bot/adapters/telegram_bot_adapter_spec.rb" afterDir="false" />
|
|
9
|
+
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
12
10
|
</list>
|
|
13
11
|
<option name="SHOW_DIALOG" value="false" />
|
|
14
12
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
@@ -39,30 +37,30 @@
|
|
|
39
37
|
<option name="hideEmptyMiddlePackages" value="true" />
|
|
40
38
|
<option name="showLibraryContents" value="true" />
|
|
41
39
|
</component>
|
|
42
|
-
<component name="PropertiesComponent"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
<component name="PropertiesComponent">{
|
|
41
|
+
"keyToString": {
|
|
42
|
+
"DefaultRubyCreateTestTemplate": "Minitest Spec",
|
|
43
|
+
"RSpec.Unnamed.executor": "Run",
|
|
44
|
+
"Ruby.scratch_61.executor": "Run",
|
|
45
|
+
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
|
46
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
47
|
+
"git-widget-placeholder": "main",
|
|
48
|
+
"last_opened_file_path": "/home/max/code/tg-sample",
|
|
49
|
+
"node.js.detected.package.eslint": "true",
|
|
50
|
+
"node.js.detected.package.tslint": "true",
|
|
51
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
52
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
53
|
+
"nodejs_package_manager_path": "npm",
|
|
54
|
+
"ruby.structure.view.model.defaults.configured": "true",
|
|
55
|
+
"settings.editor.selected.configurable": "org.jetbrains.plugins.ruby.settings.RubyActiveModuleSdkConfigurable",
|
|
56
|
+
"vue.rearranger.settings.migration": "true"
|
|
59
57
|
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
"keyToStringList": {
|
|
59
|
+
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
|
60
|
+
"ruby"
|
|
63
61
|
]
|
|
64
62
|
}
|
|
65
|
-
}
|
|
63
|
+
}</component>
|
|
66
64
|
<component name="RecentsManager">
|
|
67
65
|
<key name="CopyFile.RECENT_KEYS">
|
|
68
66
|
<recent name="$PROJECT_DIR$/spec/telegram_support_bot/adapters" />
|
|
@@ -139,6 +137,10 @@
|
|
|
139
137
|
<updated>1708600824931</updated>
|
|
140
138
|
<workItem from="1708600827139" duration="10157000" />
|
|
141
139
|
<workItem from="1708784534887" duration="682000" />
|
|
140
|
+
<workItem from="1708944130416" duration="99000" />
|
|
141
|
+
<workItem from="1708944249622" duration="627000" />
|
|
142
|
+
<workItem from="1708949675518" duration="80000" />
|
|
143
|
+
<workItem from="1708951183744" duration="310000" />
|
|
142
144
|
</task>
|
|
143
145
|
<servers />
|
|
144
146
|
</component>
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.07] - 2026-02-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Mirroring of `message_reaction` updates between support chat and user chats.
|
|
9
|
+
- Message mapping storage (`message_map` and `reverse_message_map`) to correlate forwarded/replied messages for reaction sync.
|
|
10
|
+
- Adapter API method `set_message_reaction` with implementations for both supported adapters.
|
|
11
|
+
- Test coverage for reaction handling and adapter reaction calls.
|
|
12
|
+
- Local polling helper script for development testing without Rails (`script/dev_poll.rb`).
|
|
13
|
+
- README documentation for local development testing workflow.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Local polling script now uses the proper polling call for `telegram-bot` gem (`get_updates`) and supports multiple client styles.
|
|
17
|
+
- Reaction mapping now correctly handles wrapped Telegram API responses (`{ "ok": true, "result": ... }`).
|
|
18
|
+
- Reaction mirroring now gracefully handles `REACTIONS_TOO_MANY` by retrying with a single reaction instead of crashing.
|
|
19
|
+
- Reaction mapping lookup now tolerates chat/message ID type mismatches (string vs integer).
|
|
20
|
+
- Added optional reaction-flow debug logs via `TSB_DEBUG=1`.
|
|
21
|
+
- Support-chat reaction mirroring now also handles `message_reaction_count` updates (anonymous reaction counts).
|
data/README.md
CHANGED
|
@@ -115,13 +115,84 @@ end
|
|
|
115
115
|
Implement custom adapters by inheriting from `TelegramSupportBot::Adapter::Base` and defining
|
|
116
116
|
message sending and forwarding methods.
|
|
117
117
|
|
|
118
|
+
## Handling User Privacy Settings for Message Forwarding
|
|
119
|
+
|
|
120
|
+
Due to Telegram's privacy settings, users may have restricted the ability for bots to forward their
|
|
121
|
+
messages with identifiable information. This restriction impacts the `forward_from` key, necessary
|
|
122
|
+
for the bot to recognize and reply to users directly. To ensure seamless communication and support,
|
|
123
|
+
we recommend including instructions in your bot's welcome message, asking users to allow message
|
|
124
|
+
forwarding from your bot. Here's an example of how you can phrase this request:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Please mind, that your privacy settings might prevent the bot from sending you the reply from the support team. Please consider adding this bot to your allow-list for forwarding. Here’s how you can do it:
|
|
128
|
+
|
|
129
|
+
1. Go to Settings in your Telegram app.
|
|
130
|
+
2. Tap on 'Privacy and Security'.
|
|
131
|
+
3. Scroll to 'Forwarded Messages'.
|
|
132
|
+
4. Add this bot to the list of exceptions by selecting 'Always Allow' for it.
|
|
133
|
+
|
|
134
|
+
This will allow the bot to send you back replies from the support team.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Including such instructions can help in reducing the friction in user support interactions and
|
|
139
|
+
ensure that your support team can effectively communicate with users through the bot.
|
|
118
140
|
|
|
119
141
|
## Development
|
|
120
142
|
|
|
121
143
|
- Run `bin/setup` to install dependencies.
|
|
122
144
|
- Use `rake spec` for tests and `bin/console` for an interactive prompt.
|
|
123
145
|
- To install locally, use `bundle exec rake install`.
|
|
124
|
-
- For releases, update `version.rb
|
|
146
|
+
- For releases, update `lib/telegram_support_bot/version.rb` and `CHANGELOG.md`, then run `bundle exec rake release`.
|
|
147
|
+
|
|
148
|
+
### Local Testing Without Rails (Polling)
|
|
149
|
+
|
|
150
|
+
You can run the bot locally without a Rails app by using the included script:
|
|
151
|
+
|
|
152
|
+
Prerequisites:
|
|
153
|
+
- Add the bot as an **administrator** in the support chat if you want support-side reactions to be delivered as updates.
|
|
154
|
+
- Bots can set only one reaction per message via Bot API, so only one mirrored reaction is applied when multiple are present.
|
|
155
|
+
|
|
156
|
+
1. Export required environment variables:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
export TELEGRAM_BOT_TOKEN=your_bot_token
|
|
160
|
+
export SUPPORT_CHAT_ID=your_support_chat_id
|
|
161
|
+
# optional; defaults to telegram_bot
|
|
162
|
+
export TSB_ADAPTER=telegram_bot
|
|
163
|
+
# optional; used by telegram_bot adapter
|
|
164
|
+
export TELEGRAM_BOT_USERNAME=your_bot_username
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
2. Disable webhook mode for that bot token (polling and webhooks cannot be used together):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/deleteWebhook" > /dev/null
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
3. Start the local poller:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bundle exec ruby script/dev_poll.rb
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
4. In Telegram, verify:
|
|
180
|
+
- user message is forwarded to support chat
|
|
181
|
+
- support reply is sent back to user
|
|
182
|
+
- reactions are mirrored in both directions
|
|
183
|
+
|
|
184
|
+
If you want to test with `telegram_bot_ruby` adapter, set `TSB_ADAPTER=telegram_bot_ruby` and add
|
|
185
|
+
the `telegram-bot-ruby` gem in your environment.
|
|
186
|
+
|
|
187
|
+
### Switch Back To Webhook Mode
|
|
188
|
+
|
|
189
|
+
After polling tests, set your webhook again:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
|
|
193
|
+
-H "Content-Type: application/json" \
|
|
194
|
+
-d '{"url":"https://YOUR_PUBLIC_HOST/telegram/webhook","allowed_updates":["message","message_reaction","message_reaction_count","my_chat_member"]}'
|
|
195
|
+
```
|
|
125
196
|
|
|
126
197
|
## Contributing
|
|
127
198
|
|
|
@@ -8,15 +8,24 @@ module TelegramSupportBot
|
|
|
8
8
|
}.freeze
|
|
9
9
|
|
|
10
10
|
def self.build(adapter_specification, adapter_options = {})
|
|
11
|
+
adapter_options ||= {}
|
|
11
12
|
case adapter_specification
|
|
12
13
|
when Symbol
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
class_name = ADAPTERS[adapter_specification]
|
|
15
|
+
raise ArgumentError, "Unsupported adapter specification: #{adapter_specification}" unless class_name
|
|
16
|
+
|
|
17
|
+
adapter_class = constantize(class_name)
|
|
18
|
+
adapter_class.new(**adapter_options)
|
|
15
19
|
when Class
|
|
16
|
-
adapter_specification.new(adapter_options)
|
|
20
|
+
adapter_specification.new(**adapter_options)
|
|
17
21
|
else
|
|
18
22
|
raise ArgumentError, "Unsupported adapter specification: #{adapter_specification}"
|
|
19
23
|
end
|
|
20
24
|
end
|
|
25
|
+
|
|
26
|
+
def self.constantize(class_name)
|
|
27
|
+
class_name.split('::').reject(&:empty?).inject(Object) { |namespace, constant| namespace.const_get(constant) }
|
|
28
|
+
end
|
|
29
|
+
private_class_method :constantize
|
|
21
30
|
end
|
|
22
31
|
end
|
|
@@ -35,6 +35,10 @@ module TelegramSupportBot
|
|
|
35
35
|
# forward messages to the support chat
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def set_message_reaction(chat_id:, message_id:, reaction:, **options)
|
|
39
|
+
# set reaction to a message
|
|
40
|
+
end
|
|
41
|
+
|
|
38
42
|
def on_message(&block)
|
|
39
43
|
# Implementation to register a block to be called on new messages
|
|
40
44
|
end
|
|
@@ -50,6 +50,15 @@ module TelegramSupportBot
|
|
|
50
50
|
message_id: message_id
|
|
51
51
|
)
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
def set_message_reaction(chat_id:, message_id:, reaction:, **options)
|
|
55
|
+
@bot.set_message_reaction(
|
|
56
|
+
chat_id: chat_id,
|
|
57
|
+
message_id: message_id,
|
|
58
|
+
reaction: reaction,
|
|
59
|
+
**options
|
|
60
|
+
)
|
|
61
|
+
end
|
|
53
62
|
end
|
|
54
63
|
end
|
|
55
64
|
end
|
|
@@ -43,6 +43,10 @@ module TelegramSupportBot
|
|
|
43
43
|
def forward_message(from_chat_id:, chat_id:, message_id:)
|
|
44
44
|
bot.api.forward_message(chat_id: chat_id, from_chat_id: from_chat_id, message_id: message_id)
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
def set_message_reaction(chat_id:, message_id:, reaction:, **options)
|
|
48
|
+
bot.api.set_message_reaction(chat_id: chat_id, message_id: message_id, reaction: reaction, **options)
|
|
49
|
+
end
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
end
|
data/lib/telegram_support_bot.rb
CHANGED
|
@@ -25,6 +25,18 @@ module TelegramSupportBot
|
|
|
25
25
|
@adapter ||= AdapterFactory.build(configuration.adapter, configuration.adapter_options)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def message_map
|
|
29
|
+
@message_map ||= {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reverse_message_map
|
|
33
|
+
@reverse_message_map ||= {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reaction_count_state
|
|
37
|
+
@reaction_count_state ||= {}
|
|
38
|
+
end
|
|
39
|
+
|
|
28
40
|
def scheduler
|
|
29
41
|
@scheduler ||= AutoAwayScheduler.new(adapter, configuration)
|
|
30
42
|
end
|
|
@@ -34,6 +46,10 @@ module TelegramSupportBot
|
|
|
34
46
|
if update['message']
|
|
35
47
|
# Process standard messages
|
|
36
48
|
process_message(update['message'])
|
|
49
|
+
elsif update['message_reaction']
|
|
50
|
+
process_message_reaction(update['message_reaction'])
|
|
51
|
+
elsif update['message_reaction_count']
|
|
52
|
+
process_message_reaction_count(update['message_reaction_count'])
|
|
37
53
|
elsif update['my_chat_member']
|
|
38
54
|
# Handle the bot being added to or removed from a chat
|
|
39
55
|
handle_my_chat_member_update(update['my_chat_member'])
|
|
@@ -121,7 +137,18 @@ module TelegramSupportBot
|
|
|
121
137
|
reply_to_message_id: message_id
|
|
122
138
|
)
|
|
123
139
|
else
|
|
124
|
-
adapter.send_media(chat_id: original_user_id, type: type, media: media, **options)
|
|
140
|
+
result = adapter.send_media(chat_id: original_user_id, type: type, media: media, **options)
|
|
141
|
+
if result
|
|
142
|
+
user_message_id = extract_message_id(result)
|
|
143
|
+
if user_message_id
|
|
144
|
+
support_message_id = message['message_id']
|
|
145
|
+
store_message_mapping(
|
|
146
|
+
support_message_id: support_message_id,
|
|
147
|
+
user_chat_id: original_user_id,
|
|
148
|
+
user_message_id: user_message_id
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
125
152
|
# scheduler.cancel_scheduled_task(message_id)
|
|
126
153
|
end
|
|
127
154
|
end
|
|
@@ -156,10 +183,21 @@ module TelegramSupportBot
|
|
|
156
183
|
|
|
157
184
|
def forward_message_to_support_chat(message)
|
|
158
185
|
message_id = message['message_id']
|
|
159
|
-
adapter.forward_message(
|
|
186
|
+
result = adapter.forward_message(
|
|
160
187
|
from_chat_id: message_chat_id,
|
|
161
188
|
message_id: message_id,
|
|
162
189
|
chat_id: configuration.support_chat_id)
|
|
190
|
+
|
|
191
|
+
if result
|
|
192
|
+
support_message_id = extract_message_id(result)
|
|
193
|
+
if support_message_id
|
|
194
|
+
store_message_mapping(
|
|
195
|
+
support_message_id: support_message_id,
|
|
196
|
+
user_chat_id: message_chat_id,
|
|
197
|
+
user_message_id: message_id
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
163
201
|
# scheduler.schedule_auto_away_message(message_id, message_chat_id)
|
|
164
202
|
end
|
|
165
203
|
|
|
@@ -171,6 +209,200 @@ module TelegramSupportBot
|
|
|
171
209
|
end
|
|
172
210
|
end
|
|
173
211
|
|
|
212
|
+
def process_message_reaction(message_reaction)
|
|
213
|
+
chat_id = message_reaction['chat']['id']
|
|
214
|
+
message_id = message_reaction['message_id']
|
|
215
|
+
new_reaction = message_reaction['new_reaction']
|
|
216
|
+
|
|
217
|
+
if same_chat_id?(chat_id, configuration.support_chat_id)
|
|
218
|
+
# Reaction in support chat, forward to user
|
|
219
|
+
if (mapping = find_message_mapping(message_id))
|
|
220
|
+
debug_log("Support->user mapping hit: support_message_id=#{message_id.inspect} => user_chat_id=#{mapping[:chat_id].inspect}, user_message_id=#{mapping[:message_id].inspect}")
|
|
221
|
+
mirror_reaction(
|
|
222
|
+
chat_id: mapping[:chat_id],
|
|
223
|
+
message_id: mapping[:message_id],
|
|
224
|
+
reaction: new_reaction
|
|
225
|
+
)
|
|
226
|
+
else
|
|
227
|
+
debug_log("Missing support->user mapping for support_message_id=#{message_id.inspect}; known_mappings=#{message_map.size}")
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
# Reaction in user chat, forward to support chat
|
|
231
|
+
if (support_message_id = reverse_message_map[reverse_mapping_key(chat_id, message_id)])
|
|
232
|
+
debug_log("User->support mapping hit: key=#{reverse_mapping_key(chat_id, message_id)} => support_message_id=#{support_message_id.inspect}")
|
|
233
|
+
mirror_reaction(
|
|
234
|
+
chat_id: configuration.support_chat_id,
|
|
235
|
+
message_id: support_message_id,
|
|
236
|
+
reaction: new_reaction
|
|
237
|
+
)
|
|
238
|
+
else
|
|
239
|
+
debug_log("Missing user->support mapping for key=#{reverse_mapping_key(chat_id, message_id)}; known_reverse_mappings=#{reverse_message_map.size}")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def process_message_reaction_count(message_reaction_count)
|
|
245
|
+
chat_id = message_reaction_count.dig('chat', 'id')
|
|
246
|
+
message_id = message_reaction_count['message_id']
|
|
247
|
+
|
|
248
|
+
unless same_chat_id?(chat_id, configuration.support_chat_id)
|
|
249
|
+
debug_log("Ignoring message_reaction_count outside support chat: chat_id=#{chat_id.inspect}")
|
|
250
|
+
return
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
mapping = find_message_mapping(message_id)
|
|
254
|
+
unless mapping
|
|
255
|
+
debug_log("Missing support->user mapping for message_reaction_count support_message_id=#{message_id.inspect}; known_mappings=#{message_map.size}")
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
state_key = reverse_mapping_key(chat_id, message_id)
|
|
260
|
+
previous_counts = reaction_count_state[state_key] || {}
|
|
261
|
+
current_counts, reaction_types = extract_reaction_counts(message_reaction_count['reactions'])
|
|
262
|
+
reaction_to_mirror = infer_reaction_from_count_diff(
|
|
263
|
+
previous_counts: previous_counts,
|
|
264
|
+
current_counts: current_counts,
|
|
265
|
+
reaction_types: reaction_types
|
|
266
|
+
)
|
|
267
|
+
reaction_count_state[state_key] = current_counts
|
|
268
|
+
|
|
269
|
+
if reaction_to_mirror.nil?
|
|
270
|
+
debug_log("No actionable reaction diff in message_reaction_count for support_message_id=#{message_id.inspect}")
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
debug_log("Support->user inferred reaction from count update: support_message_id=#{message_id.inspect} payload=#{reaction_to_mirror.inspect}")
|
|
275
|
+
mirror_reaction(
|
|
276
|
+
chat_id: mapping[:chat_id],
|
|
277
|
+
message_id: mapping[:message_id],
|
|
278
|
+
reaction: reaction_to_mirror
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def mirror_reaction(chat_id:, message_id:, reaction:)
|
|
283
|
+
normalized_reaction = normalize_reaction_payload(reaction)
|
|
284
|
+
debug_log("Mirroring reaction to chat_id=#{chat_id.inspect} message_id=#{message_id.inspect} payload=#{normalized_reaction.inspect}")
|
|
285
|
+
adapter.set_message_reaction(chat_id: chat_id, message_id: message_id, reaction: normalized_reaction)
|
|
286
|
+
debug_log("Reaction mirrored to chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}")
|
|
287
|
+
rescue StandardError => error
|
|
288
|
+
if reaction_too_many_error?(error) && normalized_reaction.size > 1
|
|
289
|
+
begin
|
|
290
|
+
adapter.set_message_reaction(chat_id: chat_id, message_id: message_id, reaction: [normalized_reaction.first])
|
|
291
|
+
debug_log("Reaction mirrored after fallback to single reaction for chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}")
|
|
292
|
+
rescue StandardError => retry_error
|
|
293
|
+
warn_reaction_forwarding_failure(chat_id: chat_id, message_id: message_id, error: retry_error)
|
|
294
|
+
end
|
|
295
|
+
else
|
|
296
|
+
warn_reaction_forwarding_failure(chat_id: chat_id, message_id: message_id, error: error)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def normalize_reaction_payload(reaction)
|
|
301
|
+
return [] if reaction.nil?
|
|
302
|
+
return reaction if reaction.is_a?(Array)
|
|
303
|
+
|
|
304
|
+
[reaction]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def reaction_too_many_error?(error)
|
|
308
|
+
error.message.to_s.upcase.include?('REACTIONS_TOO_MANY')
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def warn_reaction_forwarding_failure(chat_id:, message_id:, error:)
|
|
312
|
+
warn "Failed to mirror reaction to chat_id=#{chat_id} message_id=#{message_id}: #{error.class}: #{error.message}"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def extract_reaction_counts(reactions)
|
|
316
|
+
counts = {}
|
|
317
|
+
reaction_types = {}
|
|
318
|
+
|
|
319
|
+
Array(reactions).each do |reaction|
|
|
320
|
+
reaction_type = reaction['type'] || reaction[:type]
|
|
321
|
+
next unless reaction_type
|
|
322
|
+
|
|
323
|
+
key = reaction_type_key(reaction_type)
|
|
324
|
+
next if key.nil?
|
|
325
|
+
|
|
326
|
+
counts[key] = (reaction['total_count'] || reaction[:total_count] || 0).to_i
|
|
327
|
+
reaction_types[key] = reaction_type
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
[counts, reaction_types]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def infer_reaction_from_count_diff(previous_counts:, current_counts:, reaction_types:)
|
|
334
|
+
increments = []
|
|
335
|
+
current_counts.each do |key, current_value|
|
|
336
|
+
previous_value = previous_counts.fetch(key, 0)
|
|
337
|
+
diff = current_value - previous_value
|
|
338
|
+
increments << [key, diff, current_value] if diff.positive?
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
if increments.any?
|
|
342
|
+
selected_key = increments.max_by { |(_, diff, current_value)| [diff, current_value] }[0]
|
|
343
|
+
return [reaction_types.fetch(selected_key)]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
removed_all = previous_counts.values.any?(&:positive?) && current_counts.values.all?(&:zero?)
|
|
347
|
+
return [] if removed_all
|
|
348
|
+
|
|
349
|
+
nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def reaction_type_key(reaction_type)
|
|
353
|
+
type = reaction_type['type'] || reaction_type[:type]
|
|
354
|
+
|
|
355
|
+
case type
|
|
356
|
+
when 'emoji', :emoji
|
|
357
|
+
"emoji:#{reaction_type['emoji'] || reaction_type[:emoji]}"
|
|
358
|
+
when 'custom_emoji', :custom_emoji
|
|
359
|
+
"custom_emoji:#{reaction_type['custom_emoji_id'] || reaction_type[:custom_emoji_id]}"
|
|
360
|
+
when 'paid', :paid
|
|
361
|
+
'paid'
|
|
362
|
+
else
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def store_message_mapping(support_message_id:, user_chat_id:, user_message_id:)
|
|
368
|
+
mapping = { chat_id: user_chat_id, message_id: user_message_id }
|
|
369
|
+
message_map[support_message_id] = mapping
|
|
370
|
+
message_map[support_message_id.to_s] = mapping
|
|
371
|
+
reverse_message_map[reverse_mapping_key(user_chat_id, user_message_id)] = support_message_id
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def find_message_mapping(support_message_id)
|
|
375
|
+
message_map[support_message_id] ||
|
|
376
|
+
message_map[support_message_id.to_s] ||
|
|
377
|
+
message_map[support_message_id.to_i]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def reverse_mapping_key(chat_id, message_id)
|
|
381
|
+
"#{chat_id}:#{message_id}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def same_chat_id?(left, right)
|
|
385
|
+
left == right || left.to_s == right.to_s
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def debug_log(message)
|
|
389
|
+
return unless ENV['TSB_DEBUG'] == '1'
|
|
390
|
+
|
|
391
|
+
puts "[TelegramSupportBot DEBUG] #{message}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def extract_message_id(result)
|
|
395
|
+
if result.is_a?(Hash)
|
|
396
|
+
return result['message_id'] || result[:message_id] ||
|
|
397
|
+
result.dig('result', 'message_id') || result.dig(:result, :message_id) ||
|
|
398
|
+
result.dig('result', :message_id) || result.dig(:result, 'message_id')
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
return result.message_id if result.respond_to?(:message_id)
|
|
402
|
+
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
174
406
|
def send_welcome_message(chat_id:)
|
|
175
407
|
welcome_text = "Hello! Thank you for adding me to your chat. To configure your system, please use the following support chat ID: <code>#{chat_id}</code>."
|
|
176
408
|
adapter.send_message(chat_id: chat_id, text: welcome_text, parse_mode: 'HTML')
|
|
@@ -183,4 +415,3 @@ module TelegramSupportBot
|
|
|
183
415
|
@adapter = nil
|
|
184
416
|
end
|
|
185
417
|
end
|
|
186
|
-
|
data/script/dev_poll.rb
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'telegram_support_bot'
|
|
5
|
+
require 'telegram/bot'
|
|
6
|
+
|
|
7
|
+
token = ENV.fetch('TELEGRAM_BOT_TOKEN')
|
|
8
|
+
support_chat_id = Integer(ENV.fetch('SUPPORT_CHAT_ID'))
|
|
9
|
+
adapter = ENV.fetch('TSB_ADAPTER', 'telegram_bot')
|
|
10
|
+
username = ENV['TELEGRAM_BOT_USERNAME']
|
|
11
|
+
|
|
12
|
+
adapter_options = { token: token }
|
|
13
|
+
adapter_options[:username] = username if username && !username.empty?
|
|
14
|
+
|
|
15
|
+
TelegramSupportBot.configure do |config|
|
|
16
|
+
config.adapter = adapter.to_sym
|
|
17
|
+
config.adapter_options = adapter_options
|
|
18
|
+
config.support_chat_id = support_chat_id
|
|
19
|
+
config.welcome_message = 'Hi! How can we help you?'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
client = Telegram::Bot::Client.new(token)
|
|
23
|
+
offset = 0
|
|
24
|
+
|
|
25
|
+
puts "TelegramSupportBot dev poller started (adapter=#{adapter}, support_chat_id=#{support_chat_id})."
|
|
26
|
+
puts 'Press Ctrl+C to stop.'
|
|
27
|
+
|
|
28
|
+
def extract_result(payload)
|
|
29
|
+
return payload['result'] if payload.is_a?(Hash) && payload.key?('result')
|
|
30
|
+
return payload[:result] if payload.is_a?(Hash) && payload.key?(:result)
|
|
31
|
+
|
|
32
|
+
payload
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def verify_reaction_update_prerequisites(client:, support_chat_id:)
|
|
36
|
+
me_response = client.get_me
|
|
37
|
+
me_result = extract_result(me_response)
|
|
38
|
+
bot_user_id = me_result['id'] || me_result[:id]
|
|
39
|
+
return puts('Warning: could not verify bot identity via getMe.') unless bot_user_id
|
|
40
|
+
|
|
41
|
+
member_response = client.get_chat_member(chat_id: support_chat_id, user_id: bot_user_id)
|
|
42
|
+
member_result = extract_result(member_response)
|
|
43
|
+
status = member_result['status'] || member_result[:status]
|
|
44
|
+
|
|
45
|
+
unless %w[administrator creator].include?(status)
|
|
46
|
+
puts "Warning: bot status in support chat is #{status.inspect}. message_reaction/message_reaction_count updates require bot admin rights."
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
puts "Warning: failed to verify reaction prerequisites: #{e.class}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
verify_reaction_update_prerequisites(client: client, support_chat_id: support_chat_id)
|
|
53
|
+
|
|
54
|
+
def debug_update_summary(update)
|
|
55
|
+
return unless ENV['TSB_DEBUG'] == '1'
|
|
56
|
+
|
|
57
|
+
if update['message_reaction']
|
|
58
|
+
chat_id = update.dig('message_reaction', 'chat', 'id')
|
|
59
|
+
message_id = update.dig('message_reaction', 'message_id')
|
|
60
|
+
puts "[TSB POLL DEBUG] update=message_reaction chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}"
|
|
61
|
+
elsif update['message_reaction_count']
|
|
62
|
+
chat_id = update.dig('message_reaction_count', 'chat', 'id')
|
|
63
|
+
message_id = update.dig('message_reaction_count', 'message_id')
|
|
64
|
+
puts "[TSB POLL DEBUG] update=message_reaction_count chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}"
|
|
65
|
+
elsif update['message']
|
|
66
|
+
chat_id = update.dig('message', 'chat', 'id')
|
|
67
|
+
message_id = update.dig('message', 'message_id')
|
|
68
|
+
puts "[TSB POLL DEBUG] update=message chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}"
|
|
69
|
+
elsif update['my_chat_member']
|
|
70
|
+
chat_id = update.dig('my_chat_member', 'chat', 'id')
|
|
71
|
+
puts "[TSB POLL DEBUG] update=my_chat_member chat_id=#{chat_id.inspect}"
|
|
72
|
+
else
|
|
73
|
+
puts "[TSB POLL DEBUG] update=other keys=#{update.keys.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
loop do
|
|
78
|
+
params = {
|
|
79
|
+
offset: offset,
|
|
80
|
+
timeout: 25,
|
|
81
|
+
allowed_updates: %w[message message_reaction message_reaction_count my_chat_member]
|
|
82
|
+
}
|
|
83
|
+
response =
|
|
84
|
+
if client.respond_to?(:get_updates)
|
|
85
|
+
client.get_updates(**params)
|
|
86
|
+
elsif client.respond_to?(:api) && client.api.respond_to?(:get_updates)
|
|
87
|
+
client.api.get_updates(**params)
|
|
88
|
+
elsif client.respond_to?(:api) && client.api.respond_to?(:getUpdates)
|
|
89
|
+
client.api.getUpdates(**params)
|
|
90
|
+
else
|
|
91
|
+
raise 'Unsupported Telegram client API for polling'
|
|
92
|
+
end
|
|
93
|
+
updates = response['result'] || response[:result] || []
|
|
94
|
+
|
|
95
|
+
updates.each do |update|
|
|
96
|
+
normalized = update.respond_to?(:to_h) ? update.to_h : update
|
|
97
|
+
debug_update_summary(normalized)
|
|
98
|
+
TelegramSupportBot.process_update(normalized)
|
|
99
|
+
update_id = normalized['update_id'] || normalized[:update_id]
|
|
100
|
+
offset = update_id + 1 if update_id
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: telegram-support-bot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.07
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Max Buslaev
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |-
|
|
14
14
|
The telegram_support_bot gem provides Rails applications with an
|
|
@@ -28,6 +28,7 @@ extra_rdoc_files: []
|
|
|
28
28
|
files:
|
|
29
29
|
- ".idea/workspace.xml"
|
|
30
30
|
- ".rspec"
|
|
31
|
+
- CHANGELOG.md
|
|
31
32
|
- CODE_OF_CONDUCT.md
|
|
32
33
|
- Gemfile
|
|
33
34
|
- LICENSE.txt
|
|
@@ -41,13 +42,13 @@ files:
|
|
|
41
42
|
- lib/telegram_support_bot/auto_away_scheduler.rb
|
|
42
43
|
- lib/telegram_support_bot/configuration.rb
|
|
43
44
|
- lib/telegram_support_bot/version.rb
|
|
45
|
+
- script/dev_poll.rb
|
|
44
46
|
- sig/telegram_support_bot.rbs
|
|
45
|
-
- telegram_support_bot.gemspec
|
|
46
47
|
homepage: https://github.com/austerlitz/telegram_support_bot
|
|
47
48
|
licenses:
|
|
48
49
|
- MIT
|
|
49
50
|
metadata: {}
|
|
50
|
-
post_install_message:
|
|
51
|
+
post_install_message:
|
|
51
52
|
rdoc_options: []
|
|
52
53
|
require_paths:
|
|
53
54
|
- lib
|
|
@@ -62,8 +63,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
62
63
|
- !ruby/object:Gem::Version
|
|
63
64
|
version: '0'
|
|
64
65
|
requirements: []
|
|
65
|
-
rubygems_version: 3.
|
|
66
|
-
signing_key:
|
|
66
|
+
rubygems_version: 3.5.11
|
|
67
|
+
signing_key:
|
|
67
68
|
specification_version: 4
|
|
68
69
|
summary: A Rails gem for integrating a Telegram bot into your application for seamless
|
|
69
70
|
support desk functionality.
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'lib/telegram_support_bot/version'
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = 'telegram-support-bot'
|
|
7
|
-
spec.version = TelegramSupportBot::VERSION
|
|
8
|
-
spec.authors = ['Max Buslaev']
|
|
9
|
-
spec.email = ['max@buslaev.net']
|
|
10
|
-
|
|
11
|
-
spec.summary = 'A Rails gem for integrating a Telegram bot into your application for seamless
|
|
12
|
-
support desk functionality.'
|
|
13
|
-
spec.description = 'The telegram_support_bot gem provides Rails applications with an
|
|
14
|
-
easy-to-integrate Telegram bot, designed to enhance customer support services.
|
|
15
|
-
By leveraging this gem, developers can swiftly add a Telegram-based support desk to their
|
|
16
|
-
application, enabling direct communication between users and support agents through Telegram.
|
|
17
|
-
Features include automatic message forwarding to a designated secret chat for support agents,
|
|
18
|
-
the ability to reply directly from the secret chat to users, and customizable responses
|
|
19
|
-
for common queries. This gem simplifies the process of setting up a robust support channel on one of
|
|
20
|
-
the most popular messaging platforms, making it an ideal solution for businesses looking to improve
|
|
21
|
-
their customer service experience.'
|
|
22
|
-
spec.homepage = 'https://github.com/austerlitz/telegram_support_bot'
|
|
23
|
-
spec.license = 'MIT'
|
|
24
|
-
# spec.required_ruby_version = '>= 2.6.0'
|
|
25
|
-
|
|
26
|
-
# Specify which files should be added to the gem when it is released.
|
|
27
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
28
|
-
spec.files = Dir.chdir(__dir__) do
|
|
29
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
|
-
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
spec.bindir = 'exe'
|
|
34
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
35
|
-
spec.require_paths = ['lib']
|
|
36
|
-
|
|
37
|
-
end
|