chat_sdk-telegram 1.1.0 → 1.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/lib/chat_sdk/telegram/adapter.rb +12 -9
- data/lib/chat_sdk/telegram/event_parser.rb +18 -4
- data/lib/chat_sdk/telegram/format_converter.rb +148 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3684daba2940e20bf967eb893864d9ada4e88c1829564c8d6707cacb3d2d70af
|
|
4
|
+
data.tar.gz: f570ad027ae3a715268b64cf817743cd74278da097ddfa52ece098baeb18c5bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42b0ab459cba54c49a4b7402fdb3481e55369b280f61c0e30857bb75bcc75774b68fed1bbea5d5877deb01f40f6e5ecdec15e30d5c7f6d27bda59792d6d5c304
|
|
7
|
+
data.tar.gz: 303379f0c56a05e2a11b82b3225a64f51791bd3d439ad55750015568afc4b3df955b66f376fd294b8d2d4c6187b89986529ff42c51d3c7fc6c13998f55681cdd
|
|
@@ -10,10 +10,12 @@ module ChatSDK
|
|
|
10
10
|
|
|
11
11
|
attr_reader :client
|
|
12
12
|
|
|
13
|
-
def initialize(bot_token: nil, secret_token: nil, bot_username: nil, allowed_user_ids: nil, allow_unverified_webhooks: nil
|
|
13
|
+
def initialize(bot_token: nil, secret_token: nil, bot_username: nil, allowed_user_ids: nil, allow_unverified_webhooks: nil,
|
|
14
|
+
mention_on_reply: false)
|
|
14
15
|
@bot_token = bot_token || ENV["TELEGRAM_BOT_TOKEN"]
|
|
15
16
|
@secret_token = secret_token || ENV["TELEGRAM_WEBHOOK_SECRET_TOKEN"]
|
|
16
17
|
@bot_username = bot_username || ENV["TELEGRAM_BOT_USERNAME"]
|
|
18
|
+
@mention_on_reply = mention_on_reply
|
|
17
19
|
configured_user_ids = allowed_user_ids || ENV["TELEGRAM_ALLOWED_USER_IDS"]&.split(",")
|
|
18
20
|
@allowed_user_ids = configured_user_ids&.map(&:to_s)&.to_set
|
|
19
21
|
@allow_unverified_webhooks = if allow_unverified_webhooks.nil?
|
|
@@ -26,6 +28,7 @@ module ChatSDK
|
|
|
26
28
|
|
|
27
29
|
@client = ApiClient.new(@bot_token)
|
|
28
30
|
@renderer = KeyboardRenderer.new
|
|
31
|
+
@format_converter = FormatConverter.new
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def name
|
|
@@ -62,7 +65,7 @@ module ChatSDK
|
|
|
62
65
|
payload = read_json_body(rack_request)
|
|
63
66
|
return [] unless allowed_user?(payload)
|
|
64
67
|
|
|
65
|
-
EventParser.parse(payload, bot_username: @bot_username)
|
|
68
|
+
EventParser.parse(payload, bot_username: @bot_username, mention_on_reply: @mention_on_reply)
|
|
66
69
|
rescue JSON::ParserError
|
|
67
70
|
[]
|
|
68
71
|
end
|
|
@@ -93,7 +96,7 @@ module ChatSDK
|
|
|
93
96
|
parse_telegram_message(result, channel_id)
|
|
94
97
|
end
|
|
95
98
|
|
|
96
|
-
def edit_message(channel_id:, message_id:, message
|
|
99
|
+
def edit_message(channel_id:, message_id:, message:, **_options)
|
|
97
100
|
text, reply_markup = prepare_message_payload(message)
|
|
98
101
|
|
|
99
102
|
@client.edit_message_text(
|
|
@@ -104,7 +107,7 @@ module ChatSDK
|
|
|
104
107
|
)
|
|
105
108
|
end
|
|
106
109
|
|
|
107
|
-
def delete_message(channel_id:, message_id
|
|
110
|
+
def delete_message(channel_id:, message_id:, **_options)
|
|
108
111
|
@client.delete_message(chat_id: channel_id, message_id: message_id)
|
|
109
112
|
end
|
|
110
113
|
|
|
@@ -118,7 +121,7 @@ module ChatSDK
|
|
|
118
121
|
)
|
|
119
122
|
end
|
|
120
123
|
|
|
121
|
-
def add_reaction(channel_id:, message_id:, emoji
|
|
124
|
+
def add_reaction(channel_id:, message_id:, emoji:, **_options)
|
|
122
125
|
@client.set_message_reaction(
|
|
123
126
|
chat_id: channel_id,
|
|
124
127
|
message_id: message_id,
|
|
@@ -126,7 +129,7 @@ module ChatSDK
|
|
|
126
129
|
)
|
|
127
130
|
end
|
|
128
131
|
|
|
129
|
-
def remove_reaction(channel_id:, message_id:, emoji
|
|
132
|
+
def remove_reaction(channel_id:, message_id:, emoji:, **_options) # rubocop:disable Lint/UnusedMethodArgument
|
|
130
133
|
@client.set_message_reaction(
|
|
131
134
|
chat_id: channel_id,
|
|
132
135
|
message_id: message_id,
|
|
@@ -151,7 +154,7 @@ module ChatSDK
|
|
|
151
154
|
user_id
|
|
152
155
|
end
|
|
153
156
|
|
|
154
|
-
def start_typing(channel_id:, thread_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
157
|
+
def start_typing(channel_id:, thread_id: nil, message_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
155
158
|
@client.send_chat_action(chat_id: channel_id, action: "typing")
|
|
156
159
|
end
|
|
157
160
|
|
|
@@ -178,7 +181,7 @@ module ChatSDK
|
|
|
178
181
|
offset = update["update_id"] + 1
|
|
179
182
|
next unless allowed_user?(update)
|
|
180
183
|
|
|
181
|
-
events = EventParser.parse(update, bot_username: @bot_username)
|
|
184
|
+
events = EventParser.parse(update, bot_username: @bot_username, mention_on_reply: @mention_on_reply)
|
|
182
185
|
events.each { |event| block.call(event) }
|
|
183
186
|
end
|
|
184
187
|
end
|
|
@@ -207,7 +210,7 @@ module ChatSDK
|
|
|
207
210
|
text = rendered[:text] if rendered[:text] && !rendered[:text].empty?
|
|
208
211
|
reply_markup = rendered[:reply_markup]
|
|
209
212
|
end
|
|
210
|
-
[text, reply_markup]
|
|
213
|
+
[@format_converter.truncate_markdown_v2(text, 4096), reply_markup]
|
|
211
214
|
end
|
|
212
215
|
|
|
213
216
|
def parse_telegram_message(data, channel_id)
|
|
@@ -4,7 +4,7 @@ module ChatSDK
|
|
|
4
4
|
module Telegram
|
|
5
5
|
class EventParser
|
|
6
6
|
class << self
|
|
7
|
-
def parse(payload, bot_username: nil)
|
|
7
|
+
def parse(payload, bot_username: nil, mention_on_reply: false)
|
|
8
8
|
return [] unless payload.is_a?(Hash)
|
|
9
9
|
|
|
10
10
|
if payload["callback_query"]
|
|
@@ -12,7 +12,7 @@ module ChatSDK
|
|
|
12
12
|
elsif payload["message_reaction"]
|
|
13
13
|
parse_message_reaction(payload["message_reaction"])
|
|
14
14
|
elsif payload["message"]
|
|
15
|
-
parse_message(payload["message"], bot_username: bot_username)
|
|
15
|
+
parse_message(payload["message"], bot_username: bot_username, mention_on_reply: mention_on_reply)
|
|
16
16
|
else
|
|
17
17
|
[]
|
|
18
18
|
end
|
|
@@ -20,7 +20,7 @@ module ChatSDK
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def parse_message(message, bot_username: nil)
|
|
23
|
+
def parse_message(message, bot_username: nil, mention_on_reply: false)
|
|
24
24
|
return parse_bot_command(message, extract_user(message), message.dig("chat", "id")&.to_s, resolve_thread_id(message)) if bot_command?(message)
|
|
25
25
|
|
|
26
26
|
user = extract_user(message)
|
|
@@ -28,6 +28,7 @@ module ChatSDK
|
|
|
28
28
|
chat_type = message.dig("chat", "type")
|
|
29
29
|
text = message["text"] || ""
|
|
30
30
|
thread_id = resolve_thread_id(message)
|
|
31
|
+
reply_mention = reply_to_bot?(message, bot_username, mention_on_reply)
|
|
31
32
|
|
|
32
33
|
msg = ChatSDK::Message.new(
|
|
33
34
|
id: message["message_id"]&.to_s,
|
|
@@ -36,11 +37,12 @@ module ChatSDK
|
|
|
36
37
|
thread_id: thread_id,
|
|
37
38
|
channel_id: chat_id,
|
|
38
39
|
platform: :telegram,
|
|
40
|
+
is_mention: reply_mention ? true : nil,
|
|
39
41
|
reply_to: build_reply_to(message["reply_to_message"], chat_id),
|
|
40
42
|
raw: message
|
|
41
43
|
)
|
|
42
44
|
|
|
43
|
-
event_class = if mention?(text, bot_username)
|
|
45
|
+
event_class = if mention?(text, bot_username) || reply_mention
|
|
44
46
|
ChatSDK::Events::Mention
|
|
45
47
|
elsif chat_type == "private"
|
|
46
48
|
ChatSDK::Events::DirectMessage
|
|
@@ -149,6 +151,18 @@ module ChatSDK
|
|
|
149
151
|
/(?<!\w)@#{Regexp.escape(bot_username)}(?![\w-])/i.match?(text)
|
|
150
152
|
end
|
|
151
153
|
|
|
154
|
+
def reply_to_bot?(message, bot_username, mention_on_reply)
|
|
155
|
+
return false unless mention_on_reply && bot_username
|
|
156
|
+
|
|
157
|
+
reply = message["reply_to_message"] || {}
|
|
158
|
+
bot_user = reply["from"] || {}
|
|
159
|
+
return false unless bot_user["is_bot"] && bot_user["username"].to_s.casecmp?(bot_username)
|
|
160
|
+
|
|
161
|
+
topic_root_reply = message["is_topic_message"] &&
|
|
162
|
+
reply["message_id"].to_s == message["message_thread_id"].to_s
|
|
163
|
+
!topic_root_reply
|
|
164
|
+
end
|
|
165
|
+
|
|
152
166
|
def build_reply_to(reply, chat_id)
|
|
153
167
|
return unless reply
|
|
154
168
|
|
|
@@ -33,8 +33,156 @@ module ChatSDK
|
|
|
33
33
|
parts.map.with_index { |part, i| i.odd? ? part : escape_special_chars(part) }.join
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def truncate_markdown_v2(text, limit)
|
|
37
|
+
text = text.to_s
|
|
38
|
+
return text if text.length <= limit
|
|
39
|
+
|
|
40
|
+
ellipsis = "\\.\\.\\."
|
|
41
|
+
slice = text[0, limit - ellipsis.length]
|
|
42
|
+
"#{trim_to_safe_boundary(slice)}#{ellipsis}"
|
|
43
|
+
end
|
|
44
|
+
|
|
36
45
|
private
|
|
37
46
|
|
|
47
|
+
def trim_to_safe_boundary(text)
|
|
48
|
+
text = text[0...-1] while trailing_backslashes(text).odd?
|
|
49
|
+
link_detection_text, = mask_code_spans(text)
|
|
50
|
+
link_start = incomplete_link_start(link_detection_text)
|
|
51
|
+
text = text[0...link_start] if link_start
|
|
52
|
+
|
|
53
|
+
masked_links = mask_links(text)
|
|
54
|
+
masked, incomplete_code_start = mask_code_spans(masked_links)
|
|
55
|
+
text = text[0...incomplete_code_start] if incomplete_code_start
|
|
56
|
+
return text unless incomplete_code_start.nil?
|
|
57
|
+
|
|
58
|
+
marker_positions = Hash.new { |hash, key| hash[key] = [] }
|
|
59
|
+
chars = masked.chars
|
|
60
|
+
index = 0
|
|
61
|
+
while index < chars.length
|
|
62
|
+
if chars[index] == "\\"
|
|
63
|
+
index += 2
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
if %w[* _ ~ |].include?(chars[index])
|
|
67
|
+
run_length = 1
|
|
68
|
+
run_length += 1 while chars[index + run_length] == chars[index]
|
|
69
|
+
marker_positions[[chars[index], run_length]] << index
|
|
70
|
+
index += run_length
|
|
71
|
+
else
|
|
72
|
+
index += 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
cut = marker_positions.values
|
|
77
|
+
.select { |positions| positions.length.odd? }
|
|
78
|
+
.map(&:last)
|
|
79
|
+
.min
|
|
80
|
+
cut ? text[0...cut] : text
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def trailing_backslashes(text)
|
|
84
|
+
text[/\\+\z/].to_s.length
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def incomplete_link_start(text)
|
|
88
|
+
chars = text.chars
|
|
89
|
+
openings = []
|
|
90
|
+
index = 0
|
|
91
|
+
while index < chars.length
|
|
92
|
+
if chars[index] == "\\"
|
|
93
|
+
index += 2
|
|
94
|
+
next
|
|
95
|
+
elsif chars[index] == "["
|
|
96
|
+
openings << index
|
|
97
|
+
elsif chars[index] == "]" && !openings.empty?
|
|
98
|
+
opening = openings.pop
|
|
99
|
+
if chars[index + 1] == "("
|
|
100
|
+
closing = index + 2
|
|
101
|
+
closing += 1 while closing < chars.length &&
|
|
102
|
+
(chars[closing] != ")" || escaped_at?(chars, closing))
|
|
103
|
+
return opening if closing == chars.length
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
index += 1
|
|
107
|
+
end
|
|
108
|
+
openings.last
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mask_code_spans(text)
|
|
112
|
+
chars = text.chars
|
|
113
|
+
masked = chars.dup
|
|
114
|
+
index = 0
|
|
115
|
+
while index < chars.length
|
|
116
|
+
if chars[index] == "\\"
|
|
117
|
+
index += 2
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
unless chars[index] == "`"
|
|
121
|
+
index += 1
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
run_length = 1
|
|
126
|
+
run_length += 1 while chars[index + run_length] == "`"
|
|
127
|
+
closing = index + run_length
|
|
128
|
+
while closing < chars.length
|
|
129
|
+
if chars[closing] == "`"
|
|
130
|
+
candidate_run_length = backtick_run_length(chars, closing)
|
|
131
|
+
break if candidate_run_length == run_length
|
|
132
|
+
|
|
133
|
+
closing += candidate_run_length
|
|
134
|
+
else
|
|
135
|
+
closing += 1
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
return [masked.join, index] if closing >= chars.length
|
|
139
|
+
|
|
140
|
+
closing_end = closing + run_length
|
|
141
|
+
(index...closing_end).each { |position| masked[position] = " " }
|
|
142
|
+
index = closing_end
|
|
143
|
+
end
|
|
144
|
+
[masked.join, nil]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def mask_links(text)
|
|
148
|
+
chars = text.chars
|
|
149
|
+
masked = chars.dup
|
|
150
|
+
openings = []
|
|
151
|
+
index = 0
|
|
152
|
+
while index < chars.length
|
|
153
|
+
if chars[index] == "\\"
|
|
154
|
+
index += 2
|
|
155
|
+
next
|
|
156
|
+
elsif chars[index] == "["
|
|
157
|
+
openings << index
|
|
158
|
+
elsif chars[index] == "]" && !openings.empty? && chars[index + 1] == "("
|
|
159
|
+
opening = openings.pop
|
|
160
|
+
closing = index + 2
|
|
161
|
+
closing += 1 while closing < chars.length &&
|
|
162
|
+
(chars[closing] != ")" || escaped_at?(chars, closing))
|
|
163
|
+
if closing < chars.length
|
|
164
|
+
(opening..closing).each { |position| masked[position] = " " }
|
|
165
|
+
index = closing
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
index += 1
|
|
169
|
+
end
|
|
170
|
+
masked.join
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def backtick_run_length(chars, index)
|
|
174
|
+
length = 1
|
|
175
|
+
length += 1 while chars[index + length] == "`"
|
|
176
|
+
length
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def escaped_at?(chars, index)
|
|
180
|
+
backslashes = 0
|
|
181
|
+
backslashes += 1 while index - backslashes - 1 >= 0 &&
|
|
182
|
+
chars[index - backslashes - 1] == "\\"
|
|
183
|
+
backslashes.odd?
|
|
184
|
+
end
|
|
185
|
+
|
|
38
186
|
# Escape Telegram MarkdownV2 special chars outside of markdown syntax.
|
|
39
187
|
# Preserves bold (**), italic (*), strikethrough (~~), links, etc.
|
|
40
188
|
def escape_special_chars(text)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chat_sdk-telegram
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quentin Rousseau
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '1.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: faraday
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|