grammerb 0.1.0-x86_64-linux
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 +7 -0
- data/README.md +554 -0
- data/lib/grammerb/client.rb +802 -0
- data/lib/grammerb/conversation.rb +81 -0
- data/lib/grammerb/errors.rb +108 -0
- data/lib/grammerb/events.rb +438 -0
- data/lib/grammerb/grammerb.so +0 -0
- data/lib/grammerb/types.rb +530 -0
- data/lib/grammerb/version.rb +4 -0
- data/lib/grammerb.rb +19 -0
- data/sig/grammerb/client.rbs +145 -0
- data/sig/grammerb/conversation.rbs +20 -0
- data/sig/grammerb/errors.rbs +50 -0
- data/sig/grammerb/events.rbs +238 -0
- data/sig/grammerb/types.rbs +246 -0
- data/sig/grammerb.rbs +13 -0
- metadata +118 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# lib/grammerb/conversation.rb
|
|
2
|
+
require "set"
|
|
3
|
+
|
|
4
|
+
module Grammerb
|
|
5
|
+
class Conversation
|
|
6
|
+
class TimeoutError < Errors::Error; end
|
|
7
|
+
|
|
8
|
+
attr_reader :chat_id
|
|
9
|
+
|
|
10
|
+
def initialize(client, chat_id, timeout: 60)
|
|
11
|
+
@client = client
|
|
12
|
+
@chat_id = chat_id
|
|
13
|
+
@default_timeout = timeout
|
|
14
|
+
@queue = Queue.new
|
|
15
|
+
@last_outgoing_id = nil
|
|
16
|
+
@outgoing_ids = Set.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Send a message in this conversation.
|
|
20
|
+
def send_message(text, **opts)
|
|
21
|
+
msg = @client.send_message(@chat_id, text, **opts)
|
|
22
|
+
@last_outgoing_id = msg.id
|
|
23
|
+
@outgoing_ids.add(msg.id)
|
|
24
|
+
msg
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Wait for the next incoming message.
|
|
28
|
+
def get_response(timeout: nil)
|
|
29
|
+
wait_event(Events::NewMessage, timeout: timeout)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Wait for a message edit.
|
|
33
|
+
def get_edit(timeout: nil)
|
|
34
|
+
wait_event(Events::MessageEdited, timeout: timeout)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Wait for a callback query (button press).
|
|
38
|
+
def get_callback(timeout: nil)
|
|
39
|
+
wait_event(Events::CallbackQuery, timeout: timeout)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Wait for any event matching the given class.
|
|
43
|
+
# Automatically skips messages sent by this conversation.
|
|
44
|
+
def wait_event(event_class, timeout: nil)
|
|
45
|
+
t = timeout || @default_timeout
|
|
46
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + t
|
|
47
|
+
|
|
48
|
+
loop do
|
|
49
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
50
|
+
raise TimeoutError, "Conversation timed out after #{t}s" if remaining <= 0
|
|
51
|
+
|
|
52
|
+
event = @queue.pop(timeout: remaining)
|
|
53
|
+
raise TimeoutError, "Conversation timed out after #{t}s" if event.nil?
|
|
54
|
+
|
|
55
|
+
next unless event.is_a?(event_class)
|
|
56
|
+
|
|
57
|
+
# Skip our own outgoing messages
|
|
58
|
+
if event.respond_to?(:message_id) && @outgoing_ids.include?(event.message_id)
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return event
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Mark conversation as read up to last received message.
|
|
67
|
+
def mark_read
|
|
68
|
+
@client.mark_as_read(@chat_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Cancel the conversation early (drains the queue).
|
|
72
|
+
def cancel
|
|
73
|
+
@queue.close
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @api private — called by the event dispatch system.
|
|
77
|
+
def _push(event)
|
|
78
|
+
@queue.push(event) unless @queue.closed?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# lib/grammerb/errors.rb
|
|
2
|
+
module Grammerb
|
|
3
|
+
module Errors
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class ConnectionError < Error; end
|
|
6
|
+
class AuthError < Error; end
|
|
7
|
+
class NotConnectedError < Error; end
|
|
8
|
+
|
|
9
|
+
# Telegram-specific errors
|
|
10
|
+
class RPCError < Error
|
|
11
|
+
attr_reader :code, :message_text, :value
|
|
12
|
+
|
|
13
|
+
def initialize(code, message_text = nil, value: nil)
|
|
14
|
+
@code = code
|
|
15
|
+
@message_text = message_text
|
|
16
|
+
@value = value
|
|
17
|
+
super("RPCError #{code}: #{message_text}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class FloodWait < RPCError
|
|
22
|
+
attr_reader :seconds
|
|
23
|
+
|
|
24
|
+
def initialize(seconds)
|
|
25
|
+
@seconds = seconds
|
|
26
|
+
super(420, "FLOOD_WAIT", value: seconds)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class UserNotFound < RPCError
|
|
31
|
+
def initialize
|
|
32
|
+
super(400, "USER_NOT_FOUND")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ChatWriteForbidden < RPCError
|
|
37
|
+
def initialize
|
|
38
|
+
super(403, "CHAT_WRITE_FORBIDDEN")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class PhoneNumberInvalid < RPCError
|
|
43
|
+
def initialize
|
|
44
|
+
super(400, "PHONE_NUMBER_INVALID")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class SessionPasswordNeeded < RPCError
|
|
49
|
+
def initialize
|
|
50
|
+
super(401, "SESSION_PASSWORD_NEEDED")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Map RPC error name strings to classes
|
|
55
|
+
RPC_MAP = {
|
|
56
|
+
"FLOOD_WAIT" => FloodWait,
|
|
57
|
+
"USER_NOT_FOUND" => UserNotFound,
|
|
58
|
+
"CHAT_WRITE_FORBIDDEN" => ChatWriteForbidden,
|
|
59
|
+
"PHONE_NUMBER_INVALID" => PhoneNumberInvalid,
|
|
60
|
+
"SESSION_PASSWORD_NEEDED" => SessionPasswordNeeded,
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
# Map kind strings to exception classes
|
|
64
|
+
KIND_MAP = {
|
|
65
|
+
"flood_wait" => FloodWait,
|
|
66
|
+
"connection" => ConnectionError,
|
|
67
|
+
"auth" => AuthError,
|
|
68
|
+
"not_connected" => NotConnectedError,
|
|
69
|
+
"rpc" => RPCError,
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
# Parse a structured error string from the native extension.
|
|
73
|
+
# Format: "kind:code:name:value|Human-readable message"
|
|
74
|
+
def self.from_native(msg)
|
|
75
|
+
if msg =~ /\A(\w+):(\d+):([^:]*):([^|]*)\|(.+)\z/m
|
|
76
|
+
kind, code, name, value_str, human = $1, $2.to_i, $3, $4, $5
|
|
77
|
+
|
|
78
|
+
case kind
|
|
79
|
+
when "flood_wait"
|
|
80
|
+
FloodWait.new(value_str.to_i)
|
|
81
|
+
when "connection"
|
|
82
|
+
ConnectionError.new(human)
|
|
83
|
+
when "auth"
|
|
84
|
+
AuthError.new(human)
|
|
85
|
+
when "not_connected"
|
|
86
|
+
NotConnectedError.new(human)
|
|
87
|
+
when "rpc"
|
|
88
|
+
# Check if we have a specific class for this RPC error name
|
|
89
|
+
if (klass = RPC_MAP[name])
|
|
90
|
+
if klass == FloodWait
|
|
91
|
+
klass.new(value_str.to_i)
|
|
92
|
+
else
|
|
93
|
+
klass.new
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
RPCError.new(code, name, value: value_str.empty? ? nil : value_str.to_i)
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
Error.new(human)
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
# Unstructured message — wrap in base error
|
|
103
|
+
Error.new(msg)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# lib/grammerb/events.rb
|
|
2
|
+
module Grammerb
|
|
3
|
+
module Events
|
|
4
|
+
class Event
|
|
5
|
+
attr_reader :raw
|
|
6
|
+
attr_accessor :client
|
|
7
|
+
|
|
8
|
+
def initialize(raw)
|
|
9
|
+
@raw = raw
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Shared logic for message-like events (NewMessage, MessageEdited).
|
|
14
|
+
module MessageEvent
|
|
15
|
+
attr_reader :message_id, :text, :chat_id, :chat_name,
|
|
16
|
+
:sender_id, :sender_name, :date, :sender_is_bot, :chat_type,
|
|
17
|
+
:grouped_id, :reply_to_message_id,
|
|
18
|
+
:media_type, :media_id, :media_file_name, :media_mime_type, :media_size,
|
|
19
|
+
:buttons
|
|
20
|
+
|
|
21
|
+
def init_message_fields(raw)
|
|
22
|
+
@message_id = raw[:message_id]
|
|
23
|
+
@text = raw[:message_text]
|
|
24
|
+
@chat_id = raw[:chat_id]
|
|
25
|
+
@chat_name = raw[:chat_name]
|
|
26
|
+
@sender_id = raw[:sender_id]
|
|
27
|
+
@sender_name = raw[:sender_name]
|
|
28
|
+
@date = raw[:date] ? Time.at(raw[:date]) : nil
|
|
29
|
+
@sender_is_bot = raw[:sender_is_bot]
|
|
30
|
+
@chat_type = raw[:chat_type]
|
|
31
|
+
@grouped_id = raw[:grouped_id]
|
|
32
|
+
@reply_to_message_id = raw[:reply_to_message_id]
|
|
33
|
+
@media_type = raw[:media_type]
|
|
34
|
+
@media_id = raw[:media_id]
|
|
35
|
+
@media_file_name = raw[:media_file_name]
|
|
36
|
+
@media_mime_type = raw[:media_mime_type]
|
|
37
|
+
@media_size = raw[:media_size]
|
|
38
|
+
@buttons = raw[:buttons]&.map { |row| row.map { |b| Types::Button.new(b) } }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def album?
|
|
42
|
+
!@grouped_id.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def has_media?
|
|
46
|
+
!@media_type.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def photo?
|
|
50
|
+
@media_type == "photo"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def document?
|
|
54
|
+
@media_type == "document"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def sticker?
|
|
58
|
+
@media_type == "sticker"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def has_buttons?
|
|
62
|
+
@buttons && !@buttons.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def edit(text, parse_mode: nil, buttons: nil, reply_markup: nil)
|
|
66
|
+
@client&.edit_message(chat_id || chat_name, message_id, text, parse_mode: parse_mode, buttons: buttons, reply_markup: reply_markup)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete
|
|
70
|
+
@client&.delete_messages(chat_id || chat_name, message_id)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bot?
|
|
74
|
+
@sender_is_bot == true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def private?
|
|
78
|
+
@chat_type == "user"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def group?
|
|
82
|
+
@chat_type == "chat"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def channel?
|
|
86
|
+
@chat_type == "channel"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class NewMessage < Event
|
|
91
|
+
include MessageEvent
|
|
92
|
+
|
|
93
|
+
def initialize(raw)
|
|
94
|
+
super
|
|
95
|
+
init_message_fields(raw)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reply(text, **opts)
|
|
99
|
+
@client&.send_message(chat_id || chat_name, text, reply_to: message_id, **opts)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def respond(text, **opts)
|
|
103
|
+
@client&.send_message(chat_id || chat_name, text, **opts)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def forward(to_chat)
|
|
107
|
+
@client&.forward_messages(chat_id || chat_name, [message_id], to_chat)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class MessageEdited < Event
|
|
112
|
+
include MessageEvent
|
|
113
|
+
|
|
114
|
+
def initialize(raw)
|
|
115
|
+
super
|
|
116
|
+
init_message_fields(raw)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class MessageDeleted < Event
|
|
121
|
+
attr_reader :deleted_ids, :chat_id
|
|
122
|
+
|
|
123
|
+
def initialize(raw)
|
|
124
|
+
super
|
|
125
|
+
@deleted_ids = raw[:deleted_message_ids] || []
|
|
126
|
+
@chat_id = raw[:chat_id]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class CallbackQuery < Event
|
|
131
|
+
attr_reader :query_id, :data, :chat_id, :sender_id, :message_id
|
|
132
|
+
|
|
133
|
+
def initialize(raw)
|
|
134
|
+
super
|
|
135
|
+
@query_id = raw[:callback_query_id]
|
|
136
|
+
@data = raw[:callback_data]
|
|
137
|
+
@chat_id = raw[:callback_chat_id]
|
|
138
|
+
@sender_id = raw[:sender_id]
|
|
139
|
+
@message_id = raw[:message_id]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def data_str
|
|
143
|
+
@data_str_cache ||= @data&.pack("C*")&.force_encoding("UTF-8")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def answer(text: nil, alert: false, cache_time: nil)
|
|
147
|
+
@client&.answer_callback_query(query_id, text: text, alert: alert, cache_time: cache_time)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def edit(text, parse_mode: nil, buttons: nil, reply_markup: nil)
|
|
151
|
+
@client&.edit_message(chat_id, message_id, text, parse_mode: parse_mode, buttons: buttons, reply_markup: reply_markup)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def delete
|
|
155
|
+
@client&.delete_messages(chat_id, message_id)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def reply(text, **opts)
|
|
159
|
+
@client&.send_message(chat_id, text, reply_to: message_id, **opts)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def respond(text, **opts)
|
|
163
|
+
@client&.send_message(chat_id, text, **opts)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class InlineQuery < Event
|
|
168
|
+
attr_reader :query_id, :query, :offset, :sender_id
|
|
169
|
+
|
|
170
|
+
def initialize(raw)
|
|
171
|
+
super
|
|
172
|
+
@query_id = raw[:inline_query_id]
|
|
173
|
+
@query = raw[:inline_query_text]
|
|
174
|
+
@offset = raw[:inline_query_offset]
|
|
175
|
+
@sender_id = raw[:sender_id]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def answer(results, cache_time: nil, is_personal: false, next_offset: nil)
|
|
179
|
+
@client&.answer_inline_query(query_id, results, cache_time: cache_time, is_personal: is_personal, next_offset: next_offset)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class UpdateError < Event
|
|
184
|
+
attr_reader :message
|
|
185
|
+
|
|
186
|
+
def initialize(raw)
|
|
187
|
+
super
|
|
188
|
+
@message = raw[:message_text]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
class InlineSend < Event
|
|
193
|
+
attr_reader :sender_id, :query, :result_id
|
|
194
|
+
|
|
195
|
+
def initialize(raw)
|
|
196
|
+
super
|
|
197
|
+
@sender_id = raw[:sender_id]
|
|
198
|
+
@query = raw[:inline_send_query]
|
|
199
|
+
@result_id = raw[:inline_send_result_id]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
class PreCheckoutQuery < Event
|
|
204
|
+
attr_reader :query_id, :sender_id, :currency, :total_amount, :invoice_payload
|
|
205
|
+
|
|
206
|
+
def initialize(raw)
|
|
207
|
+
super
|
|
208
|
+
@query_id = raw[:pre_checkout_query_id]
|
|
209
|
+
@sender_id = raw[:sender_id]
|
|
210
|
+
@currency = raw[:pre_checkout_currency]
|
|
211
|
+
@total_amount = raw[:pre_checkout_amount]
|
|
212
|
+
@invoice_payload = raw[:pre_checkout_payload]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def approve
|
|
216
|
+
@client&.answer_pre_checkout_query(query_id, ok: true)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def reject(error_message = "Payment rejected")
|
|
220
|
+
@client&.answer_pre_checkout_query(query_id, ok: false, error_message: error_message)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
class SuccessfulPayment < Event
|
|
225
|
+
attr_reader :sender_id, :currency, :total_amount, :invoice_payload, :charge_id
|
|
226
|
+
|
|
227
|
+
def initialize(raw)
|
|
228
|
+
super
|
|
229
|
+
@sender_id = raw[:sender_id]
|
|
230
|
+
@currency = raw[:payment_currency]
|
|
231
|
+
@total_amount = raw[:payment_amount]
|
|
232
|
+
@invoice_payload = raw[:payment_payload]
|
|
233
|
+
@charge_id = raw[:payment_charge_id]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class Album < Event
|
|
238
|
+
attr_reader :messages, :grouped_id, :chat_id, :chat_name
|
|
239
|
+
|
|
240
|
+
def initialize(messages)
|
|
241
|
+
super(messages.first.raw)
|
|
242
|
+
@messages = messages
|
|
243
|
+
@grouped_id = messages.first.grouped_id
|
|
244
|
+
@chat_id = messages.first.chat_id
|
|
245
|
+
@chat_name = messages.first.chat_name
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def texts
|
|
249
|
+
@messages.map(&:text)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def reply(text, **opts)
|
|
253
|
+
@client&.send_message(chat_id || chat_name, text, reply_to: messages.first.message_id, **opts)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def respond(text, **opts)
|
|
257
|
+
@client&.send_message(chat_id || chat_name, text, **opts)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class ChatAction < Event
|
|
262
|
+
attr_reader :action_type, :message_id, :chat_id, :chat_name, :chat_type,
|
|
263
|
+
:sender_id, :actor_id, :user_ids, :new_title,
|
|
264
|
+
:pinned_message_id, :game_score, :date
|
|
265
|
+
|
|
266
|
+
def initialize(raw)
|
|
267
|
+
super
|
|
268
|
+
@action_type = raw[:action_type]
|
|
269
|
+
@message_id = raw[:message_id]
|
|
270
|
+
@chat_id = raw[:chat_id]
|
|
271
|
+
@chat_name = raw[:chat_name]
|
|
272
|
+
@chat_type = raw[:chat_type]
|
|
273
|
+
@sender_id = raw[:sender_id]
|
|
274
|
+
@actor_id = raw[:actor_id]
|
|
275
|
+
@user_ids = raw[:action_user_ids] || []
|
|
276
|
+
@new_title = raw[:new_title]
|
|
277
|
+
@pinned_message_id = raw[:pinned_message_id]
|
|
278
|
+
@game_score = raw[:game_score]
|
|
279
|
+
@date = raw[:date] ? Time.at(raw[:date]) : nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def user_joined?; @action_type == "user_joined" end
|
|
283
|
+
def user_added?; @action_type == "user_added" end
|
|
284
|
+
def user_kicked?; @action_type == "user_kicked" end
|
|
285
|
+
def user_left?; @action_type == "user_left" end
|
|
286
|
+
def title_changed?; @action_type == "title_changed" end
|
|
287
|
+
def photo_changed?; @action_type == "photo_changed" end
|
|
288
|
+
def photo_deleted?; @action_type == "photo_deleted" end
|
|
289
|
+
def pin_message?; @action_type == "pin_message" end
|
|
290
|
+
def unpin?; @action_type == "unpin" end
|
|
291
|
+
def chat_created?; @action_type == "chat_created" end
|
|
292
|
+
def channel_created?; @action_type == "channel_created" end
|
|
293
|
+
def user_promoted?; @action_type == "user_promoted" end
|
|
294
|
+
def user_demoted?; @action_type == "user_demoted" end
|
|
295
|
+
def new_score?; @action_type == "new_score" end
|
|
296
|
+
|
|
297
|
+
def user_id; @user_ids&.first end
|
|
298
|
+
def added_by; @actor_id if user_added? end
|
|
299
|
+
def kicked_by; @actor_id if user_kicked? end
|
|
300
|
+
|
|
301
|
+
def reply(text, **opts)
|
|
302
|
+
@client&.send_message(chat_id || chat_name, text, reply_to: message_id, **opts)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def respond(text, **opts)
|
|
306
|
+
@client&.send_message(chat_id || chat_name, text, **opts)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def delete
|
|
310
|
+
@client&.delete_messages(chat_id || chat_name, message_id) if message_id
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
class UserUpdate < Event
|
|
315
|
+
attr_reader :sender_id, :chat_id, :user_status,
|
|
316
|
+
:last_seen, :online_until, :typing_action
|
|
317
|
+
|
|
318
|
+
def initialize(raw)
|
|
319
|
+
super
|
|
320
|
+
@sender_id = raw[:sender_id]
|
|
321
|
+
@chat_id = raw[:chat_id]
|
|
322
|
+
@user_status = raw[:user_status]
|
|
323
|
+
@last_seen = raw[:last_seen] ? Time.at(raw[:last_seen]) : nil
|
|
324
|
+
@online_until = raw[:online_until] ? Time.at(raw[:online_until]) : nil
|
|
325
|
+
@typing_action = raw[:typing_action]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def online?; @user_status == "online" end
|
|
329
|
+
def offline?; @user_status == "offline" end
|
|
330
|
+
def recently?; @user_status == "recently" end
|
|
331
|
+
def within_weeks?; @user_status == "last_week" end
|
|
332
|
+
def within_months?; @user_status == "last_month" end
|
|
333
|
+
|
|
334
|
+
def typing?; @typing_action == "typing" end
|
|
335
|
+
def recording?; @typing_action&.start_with?("recording") end
|
|
336
|
+
def uploading?; @typing_action&.start_with?("uploading") end
|
|
337
|
+
def playing?; @typing_action == "playing_game" end
|
|
338
|
+
def cancel?; @typing_action == "cancel" end
|
|
339
|
+
def audio?; @typing_action&.include?("audio") end
|
|
340
|
+
def video?; @typing_action&.include?("video") && !round? end
|
|
341
|
+
def round?; @typing_action&.include?("round") end
|
|
342
|
+
def document?; @typing_action == "uploading_document" end
|
|
343
|
+
def photo?; @typing_action == "uploading_photo" end
|
|
344
|
+
def sticker?; @typing_action == "choosing_sticker" end
|
|
345
|
+
def contact?; @typing_action == "choosing_contact" end
|
|
346
|
+
def geo?; @typing_action == "geo_location" end
|
|
347
|
+
|
|
348
|
+
alias_method :user_id, :sender_id
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
class MessageRead < Event
|
|
352
|
+
attr_reader :chat_id, :max_id, :outbox, :unread_count, :message_ids
|
|
353
|
+
|
|
354
|
+
def initialize(raw)
|
|
355
|
+
super
|
|
356
|
+
@chat_id = raw[:chat_id]
|
|
357
|
+
@max_id = raw[:max_id]
|
|
358
|
+
@outbox = raw[:is_outbox] || false
|
|
359
|
+
@unread_count = raw[:unread_count]
|
|
360
|
+
@message_ids = raw[:read_message_ids] || []
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def inbox?; !@outbox end
|
|
364
|
+
def outbox?; @outbox end
|
|
365
|
+
def contents?; !@message_ids.empty? end
|
|
366
|
+
|
|
367
|
+
def is_read?(message_id)
|
|
368
|
+
if contents?
|
|
369
|
+
@message_ids.include?(message_id)
|
|
370
|
+
elsif @max_id
|
|
371
|
+
message_id <= @max_id
|
|
372
|
+
else
|
|
373
|
+
false
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def get_messages
|
|
378
|
+
return [] unless @client && @chat_id
|
|
379
|
+
ids = contents? ? @message_ids : (@max_id ? [@max_id] : [])
|
|
380
|
+
return [] if ids.empty?
|
|
381
|
+
@client.get_messages_by_id(@chat_id, *ids).compact
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
class RawUpdate < Event; end
|
|
386
|
+
|
|
387
|
+
# Maps update type strings from Rust to event classes
|
|
388
|
+
TYPE_MAP = {
|
|
389
|
+
"new_message" => NewMessage,
|
|
390
|
+
"message_edited" => MessageEdited,
|
|
391
|
+
"message_deleted" => MessageDeleted,
|
|
392
|
+
"callback_query" => CallbackQuery,
|
|
393
|
+
"inline_query" => InlineQuery,
|
|
394
|
+
"inline_send" => InlineSend,
|
|
395
|
+
"pre_checkout_query" => PreCheckoutQuery,
|
|
396
|
+
"successful_payment" => SuccessfulPayment,
|
|
397
|
+
"chat_action" => ChatAction,
|
|
398
|
+
"user_update" => UserUpdate,
|
|
399
|
+
"message_read" => MessageRead,
|
|
400
|
+
"raw" => RawUpdate,
|
|
401
|
+
"error" => UpdateError,
|
|
402
|
+
}.freeze
|
|
403
|
+
|
|
404
|
+
def self.from_raw(raw_hash)
|
|
405
|
+
type_str = raw_hash[:type]
|
|
406
|
+
klass = TYPE_MAP[type_str]
|
|
407
|
+
return nil unless klass
|
|
408
|
+
klass.new(raw_hash)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Handler registration
|
|
412
|
+
class Handler
|
|
413
|
+
attr_reader :event_class, :pattern, :chats, :block
|
|
414
|
+
|
|
415
|
+
def initialize(event_class, pattern: nil, chats: nil, &block)
|
|
416
|
+
@event_class = event_class
|
|
417
|
+
@pattern = pattern
|
|
418
|
+
@chats = chats
|
|
419
|
+
@block = block
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def matches?(event)
|
|
423
|
+
return false unless event.is_a?(@event_class)
|
|
424
|
+
if @pattern && event.respond_to?(:text)
|
|
425
|
+
text = event.text
|
|
426
|
+
# Skip pattern check for media-only messages (nil text) instead of rejecting them
|
|
427
|
+
return false if text && !text.match?(@pattern)
|
|
428
|
+
end
|
|
429
|
+
if @chats
|
|
430
|
+
chat_id = event.respond_to?(:chat_id) ? event.chat_id : nil
|
|
431
|
+
chat_name = event.respond_to?(:chat_name) ? event.chat_name : nil
|
|
432
|
+
return false unless (chat_name && @chats.include?(chat_name)) || (chat_id && @chats.include?(chat_id))
|
|
433
|
+
end
|
|
434
|
+
true
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
Binary file
|