xnm-telegram 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +64 -0
- data/lib/xnm/telegram.rb +4 -0
- data/lib/xnm/telegram/Chat.rb +87 -0
- data/lib/xnm/telegram/GroupingAdapter.rb +273 -0
- data/lib/xnm/telegram/HTTPCore.rb +128 -0
- data/lib/xnm/telegram/Handler.rb +302 -0
- data/lib/xnm/telegram/KeyboardLayout.rb +67 -0
- data/lib/xnm/telegram/MQTT_Adapter.rb +73 -0
- data/lib/xnm/telegram/Message.rb +126 -0
- data/lib/xnm/telegram/OnCommand.rb +34 -0
- data/lib/xnm/telegram/OnMessage.rb +29 -0
- data/lib/xnm/telegram/SingleUser.rb +78 -0
- data/lib/xnm/telegram/TestHTTPCore.rb +62 -0
- data/lib/xnm/telegram/User.rb +142 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b56c7a24cc65be03dda2eb188715b26d24bad941c90f2f6d98b1f95bd4f672b9
|
4
|
+
data.tar.gz: 7f2d2bf294f9152379c726f9a92868b562f30343019f21a5e597cfa26aa21d68
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 561e100a3a128011b6df5e4e44e8d3e56aac6fb297f1b9ec987fd4fdf0c31b2c04ec1ba1cbdf471935dce9228f4df2e7f04b4ed4f3d327f17ca09bd31365b193
|
7
|
+
data.tar.gz: 87c30f5062838b931693acb00714dde8c6da9dc63a8745f9a6eeac24f24fb90b7641432839267cd26cb29106ccb98f8bf24b0481369b035f659faf218782e7ed
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
# Xasin's simplifying Telegram gem
|
3
|
+
This gem is mainly meant for personal use.
|
4
|
+
Many of the existing Ruby gems for Telegram have all the functionality you want,
|
5
|
+
but also require a fairly complex setup, and good knowledge of the Telegram API.
|
6
|
+
|
7
|
+
My gem doesn't include all functionality, but aims to provide a somewhat simpler interface of
|
8
|
+
interaction with a small number (or just a single) of users.
|
9
|
+
I mainly use it for my own smart home system, to easily send and receive messages, but not much more.
|
10
|
+
|
11
|
+
## MQTT-Mode (Preferred)
|
12
|
+
The main mode of using this gem would be via the MQTT Adapter system.
|
13
|
+
It translates a lot of commonly used functionality into an asynchronous MQTT API, allowing other parts of your code, but also embedded devices like ESPs to very easily interface with the Telegram bot.
|
14
|
+
|
15
|
+
*Note:* A MQTT server is not needed! The "virtual" MQTT Server `MQTT::Testing::SubHandler` can be used too!
|
16
|
+
|
17
|
+
Setting up the system is fairly easy:
|
18
|
+
|
19
|
+
```Ruby
|
20
|
+
require 'xasin/telegram.rb'
|
21
|
+
require 'xasin/telegram/MQTT_Adapter.rb'
|
22
|
+
|
23
|
+
# Create the HTTP core, which will handle all the communication to the REST api
|
24
|
+
httpCore = Xasin::Telegram::HTTPCore.new(APIKEY);
|
25
|
+
|
26
|
+
# Now create the MQTT interface.
|
27
|
+
mqttAdapter = Xasin::Telegram::MQTT::Server.new(httpCore, mqtt);
|
28
|
+
```
|
29
|
+
|
30
|
+
This exposes the Telegram Bot's interface functions to the "Telegram/#" tree.
|
31
|
+
|
32
|
+
Incoming messages will be published to
|
33
|
+
|
34
|
+
## Single-User mode
|
35
|
+
Another way to use the HTTP-Core is by only looking at a single user.
|
36
|
+
It discards messages of all users except one, and provides a simple "send" and "on_message" interface:
|
37
|
+
|
38
|
+
```Ruby
|
39
|
+
require 'xasin/telegram.rb'
|
40
|
+
|
41
|
+
# The HTTP "core" handles sending raw commands and getting updates, but not much more.
|
42
|
+
httpCore = Xasin::Telegram::HTTPCore.new(APIKEY);
|
43
|
+
|
44
|
+
# The SingleUser class handles the aforementioned sending/receiving of messages.
|
45
|
+
# Its first argument is the Chat-ID it should use, which is NOT the user ID
|
46
|
+
# To find it out you have to perform an update request to the Telegram API, send a message to your bot,
|
47
|
+
# and get the Chat_ID from there.
|
48
|
+
# I'm working on a better way >.>
|
49
|
+
singleUser = Xasin::Telegram::SingleUser.new(CHAT_ID, httpCore);
|
50
|
+
|
51
|
+
|
52
|
+
# From then, using the user is simple. Only text is required!
|
53
|
+
returned_id = singleUser.send_message(TEXT, **args);
|
54
|
+
|
55
|
+
# The ID that is returned can then be used to edit or delete the message:
|
56
|
+
singleUser.edit_message(returned_id, NEW_TEXT);
|
57
|
+
singleUser.delete_message(returned_id);
|
58
|
+
|
59
|
+
# And to receive messages:
|
60
|
+
singleUser.on_message do |message|
|
61
|
+
# Fetch the text from the message first!
|
62
|
+
text = message[:text];
|
63
|
+
end
|
64
|
+
```
|
data/lib/xnm/telegram.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
module XNM
|
3
|
+
module Telegram
|
4
|
+
class Chat
|
5
|
+
attr_reader :chat_id
|
6
|
+
attr_reader :str_id
|
7
|
+
|
8
|
+
attr_reader :casual_name
|
9
|
+
|
10
|
+
attr_reader :chat_obj
|
11
|
+
|
12
|
+
attr_reader :on_telegram_event
|
13
|
+
|
14
|
+
# Initialize a chat object.
|
15
|
+
# Call this to generate a new chat object. It will
|
16
|
+
# always have to be called with a Chat object, as returned by
|
17
|
+
# a message's "chat" field or the getChat function
|
18
|
+
def initialize(handler, chat_info)
|
19
|
+
@handler = handler;
|
20
|
+
|
21
|
+
@chat_id = chat_info[:id];
|
22
|
+
@str_id = chat_info[:username] ||
|
23
|
+
chat_info[:title]
|
24
|
+
|
25
|
+
@casual_name = @str_id;
|
26
|
+
|
27
|
+
@chat_obj = chat_info;
|
28
|
+
|
29
|
+
@on_telegram_event = []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Send a message to this chat.
|
33
|
+
# @see Handler#send_message
|
34
|
+
def send_message(text, **options)
|
35
|
+
@handler.send_message(self, text, **options);
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add a new message callback.
|
39
|
+
# Similar to the Handler's function, but only applies
|
40
|
+
# to this chat's messages. Especially nice for one-on-one bots.
|
41
|
+
# @see Handler#on_message
|
42
|
+
def on_message(regexp = nil, &block)
|
43
|
+
raise ArgumentError, 'Block must be given!' unless block_given?
|
44
|
+
|
45
|
+
out_evt = OnMessage.new({ block: block, regexp: regexp });
|
46
|
+
@on_telegram_event << out_evt
|
47
|
+
|
48
|
+
out_evt
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add a command callback.
|
52
|
+
# Equivalent to {Handler#on_command}, but will only be called
|
53
|
+
# on commands issued in this chat.
|
54
|
+
def on_command(command, **options, &block)
|
55
|
+
raise ArgumentError, 'Block must be given!' unless block_given?
|
56
|
+
|
57
|
+
options[:block] = block
|
58
|
+
options[:command] = command
|
59
|
+
|
60
|
+
out_evt = OnCommand.new(options);
|
61
|
+
@on_telegram_event << out_evt
|
62
|
+
|
63
|
+
out_evt
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return a Telegram mention link.
|
67
|
+
# Can be inserted into a Telegram HTML formatted message, and
|
68
|
+
# allows people to click on the name.
|
69
|
+
def tg_mention
|
70
|
+
"<a href=\"tg://user?id=#{@chat_id}\">@#{@str_id}</a>"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return a more human-friendly name for the chat.
|
74
|
+
def casual_name
|
75
|
+
@str_id
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_i
|
79
|
+
@chat_id
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_s
|
83
|
+
@casual_name
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
|
2
|
+
require_relative 'HTTPCore.rb'
|
3
|
+
|
4
|
+
module XNM
|
5
|
+
module Telegram
|
6
|
+
# This class handles translating the sometimes a bit interesting
|
7
|
+
# Telegram API data to more usable types.
|
8
|
+
# It also handles the translation of User-IDs to the Usernames,
|
9
|
+
# and provides "Grouping IDs" to make it easier to edit, reply to, and
|
10
|
+
# delete messages
|
11
|
+
# It also exposes a much neater way of constructing inline keyboards.
|
12
|
+
class GroupingAdapter
|
13
|
+
attr_accessor :usernameList
|
14
|
+
attr_reader :groupIDList
|
15
|
+
|
16
|
+
attr_reader :testLastUID
|
17
|
+
attr_reader :testLastData
|
18
|
+
|
19
|
+
def initialize(httpCore)
|
20
|
+
# Check if we already have a HTTPCore, else create one
|
21
|
+
@httpCore = if(httpCore.is_a? Telegram::HTTPCore)
|
22
|
+
httpCore;
|
23
|
+
else
|
24
|
+
Telegram::HTTPCore.new(httpCore);
|
25
|
+
end
|
26
|
+
@httpCore.attach_receptor(self);
|
27
|
+
|
28
|
+
_reset();
|
29
|
+
end
|
30
|
+
|
31
|
+
def _reset()
|
32
|
+
# Hash {username => ChatID}
|
33
|
+
@usernameList = Hash.new();
|
34
|
+
# Hash {ChatID => {GroupID => MessageID}}
|
35
|
+
@groupIDList = Hash.new do |hash, key|
|
36
|
+
hash[key] = Hash.new;
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Tested in ts_group_adapter/test_keyboard_build
|
41
|
+
def _process_inline_keyboard(keyboardLayout, gID = nil)
|
42
|
+
# Return unless we have a structure we can form into a keyboard
|
43
|
+
return nil unless (keyboardLayout.is_a? Array or keyboardLayout.is_a? Hash)
|
44
|
+
|
45
|
+
# Make sure the structure of keyboardLayout is [{}] or [[]]
|
46
|
+
if(keyboardLayout.is_a? Hash)
|
47
|
+
keyboardLayout = [keyboardLayout]
|
48
|
+
elsif(not (keyboardLayout[0].is_a? Array or keyboardLayout[0].is_a? Hash))
|
49
|
+
keyboardLayout = [keyboardLayout]
|
50
|
+
end
|
51
|
+
|
52
|
+
outData = Array.new();
|
53
|
+
|
54
|
+
# Iterate through the rows of keyboards
|
55
|
+
keyboardLayout.each do |row|
|
56
|
+
newRow = Array.new();
|
57
|
+
|
58
|
+
# Create the INLINE KEY button elements
|
59
|
+
row.each do |key, val|
|
60
|
+
cbd = {i: gID, k: (val or key)};
|
61
|
+
newRow << {text: key, callback_data: cbd.to_json}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add the new row to the array of rows
|
65
|
+
outData << newRow;
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return the ready reply_markup element
|
69
|
+
return {inline_keyboard: outData};
|
70
|
+
end
|
71
|
+
|
72
|
+
# Processes messages received through MQTT/Custom input
|
73
|
+
# It takes care of setting a few good defaults (like parse_mode),
|
74
|
+
# deletes any old messages of the same GroupID (if requested),
|
75
|
+
# and stores the new Message ID for later processing
|
76
|
+
# @param data [Hash] The message packet to be sent to Telegram
|
77
|
+
# The "text" field is always required. Optional fields are:
|
78
|
+
# - gid: Grouping-ID for later editing/deleting/replying to
|
79
|
+
# - replace: true/false whether or not the old GID-Tagged message should be deleted
|
80
|
+
# - overwrite: Similar to replace, but instead of re-sending, it only edits the old message
|
81
|
+
# - silent: Sets the "disable_notification" flag
|
82
|
+
# - inline_keyboard: Hash of inline keyboard buttons (Button-text as key, button reply as value)
|
83
|
+
# @param uID [Integer,String] The user-id as received from the MQTT Wildcard.
|
84
|
+
# Can be a username defined in @usernameList, or the raw Chat ID
|
85
|
+
# Tested in ts_mqtt/test_send
|
86
|
+
def _handle_send(data, uID)
|
87
|
+
# Resolve a saved Username to a User-ID
|
88
|
+
uID = @usernameList[uID] if(@usernameList.key? uID)
|
89
|
+
return if (uID = uID.to_i) == 0; # Return if a unknown Username was used
|
90
|
+
|
91
|
+
gID = data[:gid];
|
92
|
+
|
93
|
+
# Check if a GroupID is present and a former message is known
|
94
|
+
if(gID and @groupIDList[uID][gID])
|
95
|
+
if(data[:replace])
|
96
|
+
_handle_delete(gID, uID)
|
97
|
+
elsif(data[:overwrite])
|
98
|
+
_handle_edit(data, uID);
|
99
|
+
return; # After editing, no new message should be sent!
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Lay out all mandatory parameters for sendMessage request
|
104
|
+
outData = {
|
105
|
+
chat_id: uID,
|
106
|
+
parse_mode: (data[:parse_mode] or "Markdown"), # Markdown parse mode is just nice
|
107
|
+
text: data[:text]
|
108
|
+
}
|
109
|
+
|
110
|
+
# Check if the message is meant to be sent without notification
|
111
|
+
if(data[:silent])
|
112
|
+
outData[:disable_notification] = true;
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if an inline keyboard layout is given, and parse that.
|
116
|
+
if((ilk = data[:inline_keyboard]))
|
117
|
+
outData[:reply_markup] = _process_inline_keyboard(ilk, gID);
|
118
|
+
end
|
119
|
+
|
120
|
+
reply = @httpCore.perform_post("sendMessage", outData);
|
121
|
+
return unless reply[:ok] # Something was wrong about our message layout
|
122
|
+
# TODO Add a proper error handler here.
|
123
|
+
|
124
|
+
# If a GroupID was given, save the sent message's ID
|
125
|
+
@groupIDList[uID][gID] = reply[:result][:message_id] if(gID);
|
126
|
+
end
|
127
|
+
|
128
|
+
# Edits an already known message. Takes arguments similar to _handle_send
|
129
|
+
# @param data [Hash] The message to be edited. GID must be set, and optionally
|
130
|
+
# a inline keyboard markup or a new message text must be provided.
|
131
|
+
# @param uID [Integer,String] The user-id as received from the MQTT Wildcard.
|
132
|
+
# Can be a username defined in @usernameList, or the raw Chat ID
|
133
|
+
def _handle_edit(data, uID)
|
134
|
+
# Resolve a saved Username to a User-ID
|
135
|
+
uID = @usernameList[uID] if(@usernameList.key? uID)
|
136
|
+
return if (uID = uID.to_i) == 0; # Return if a unknown Username was used
|
137
|
+
|
138
|
+
# Fetch the target MessageID - Return if none is present
|
139
|
+
return unless mID = @groupIDList[uID][data[:gid]]
|
140
|
+
|
141
|
+
# Lay out all mandatory arguments for the edit POST
|
142
|
+
outData = {
|
143
|
+
chat_id: uID,
|
144
|
+
message_id: mID,
|
145
|
+
};
|
146
|
+
|
147
|
+
# If a inline keyboard was given, parse that.
|
148
|
+
if(ilk = data[:inline_keyboard])
|
149
|
+
outData[:reply_markup] = _process_inline_keyboard(ilk, data[:gid]);
|
150
|
+
end
|
151
|
+
|
152
|
+
if(data[:text]) # Check if text was given
|
153
|
+
outData[:text] = data[:text];
|
154
|
+
# Send the POST request editing the message text
|
155
|
+
@httpCore.perform_post("editMessageText", outData);
|
156
|
+
else
|
157
|
+
# Otherwise, only edit the reply markup (keyboard etc.)
|
158
|
+
@httpCore.perform_post("editMessageReplyMarkup", outData);
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Deletes a message marked by a given GID
|
163
|
+
# @param gid [String] The grouping-ID of the message to delete.
|
164
|
+
# @param uID [Integer, String] The User-ID (as defined in @usernameList, or
|
165
|
+
# the raw ID), for which to delete.
|
166
|
+
def _handle_delete(data, uID)
|
167
|
+
# Resolve a saved Username to a User-ID
|
168
|
+
uID = @usernameList[uID] if(@usernameList.key? uID)
|
169
|
+
return if (uID = uID.to_i) == 0; # Return unless the username was known
|
170
|
+
|
171
|
+
# Fetch the real message ID held by a grouping ID
|
172
|
+
return unless mID = @groupIDList[uID][data]
|
173
|
+
@groupIDList[uID].delete(data); # Clear that ID from the list
|
174
|
+
|
175
|
+
# Perform the actual delete
|
176
|
+
@httpCore.perform_post("deleteMessage", {chat_id: uID, message_id: mID});
|
177
|
+
end
|
178
|
+
|
179
|
+
def on_message(data, uID)
|
180
|
+
@testLastUID = uID;
|
181
|
+
@testLastData = data;
|
182
|
+
end
|
183
|
+
|
184
|
+
def on_command(data, uID)
|
185
|
+
end
|
186
|
+
|
187
|
+
def on_reply(data, uID)
|
188
|
+
end
|
189
|
+
|
190
|
+
def on_callback_pressed(data, uID)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Handle an incoming message packet from the HTTP core
|
194
|
+
# @private
|
195
|
+
def handle_message(msg)
|
196
|
+
uID = msg[:chat][:id];
|
197
|
+
# Resolve the User-ID, if it's known.
|
198
|
+
if(newUID = @usernameList.key(uID))
|
199
|
+
uID = newUID
|
200
|
+
end
|
201
|
+
|
202
|
+
data = Hash.new();
|
203
|
+
# Only accept messages that contain text (things like keyboard replies
|
204
|
+
# are handled elsewhere).
|
205
|
+
return unless(data[:text] = msg[:text])
|
206
|
+
|
207
|
+
# See if this message was a reply, and if we know said reply under a group-id
|
208
|
+
if(replyMSG = msg[:reply_to_message])
|
209
|
+
data[:reply_gid] = @groupIDList[uID].key(replyMSG[:message_id]);
|
210
|
+
end
|
211
|
+
|
212
|
+
# Distinguish the type of message. If it starts with a command-slash,
|
213
|
+
# it will be excempt from normal processing.
|
214
|
+
# If it has a reply message ID that we know, handle it as a reply.
|
215
|
+
# Otherwise, simply send it off as a normal message.
|
216
|
+
if(data[:text] =~ /^\//)
|
217
|
+
on_command(data, uID)
|
218
|
+
elsif(data[:reply_gid])
|
219
|
+
on_reply(data, uID)
|
220
|
+
else
|
221
|
+
on_message(data, uID)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Handle an incoming callback query (inline keyboard button press)
|
226
|
+
# as received from the HTTP Core
|
227
|
+
# @private
|
228
|
+
def handle_callback_query(cbq)
|
229
|
+
# Send out a callback query reply (i.e. Telegram now knows we saw it)
|
230
|
+
@httpCore.perform_post("answerCallbackQuery", {callback_query_id: cbq[:id]});
|
231
|
+
|
232
|
+
# Resolve the username, if we know it.
|
233
|
+
uID = msg[:message][:chat][:id];
|
234
|
+
if(newUID = @usernameList.key(uID))
|
235
|
+
uID = newUID
|
236
|
+
end
|
237
|
+
|
238
|
+
# Try to parse the data. This gem sets inline keyboard reply data to
|
239
|
+
# a small JSON, which identifies the GID and the key that was pressed.
|
240
|
+
begin
|
241
|
+
data = JSON.parse(cbq[:data], symbolize_names: true);
|
242
|
+
rescue
|
243
|
+
return;
|
244
|
+
end
|
245
|
+
|
246
|
+
data = {
|
247
|
+
gid: data[:i],
|
248
|
+
key: data[:k],
|
249
|
+
}
|
250
|
+
|
251
|
+
# If the key ID starts with a command slash, treat it like a normal
|
252
|
+
# command. Has the benefit of making it super easy to execute already
|
253
|
+
# implemented actions as a inline keyboard
|
254
|
+
if(data[:key] =~ /^\//)
|
255
|
+
on_command({text: data[:key], gid: data[:gid]}, uID);
|
256
|
+
end
|
257
|
+
on_callback_pressed(data, uID);
|
258
|
+
end
|
259
|
+
|
260
|
+
# Handle incoming HTTP Core packets. Just send them to the appropriate
|
261
|
+
# handler function
|
262
|
+
def handle_packet(packet)
|
263
|
+
if(msg = packet[:message])
|
264
|
+
handle_message(msg);
|
265
|
+
end
|
266
|
+
|
267
|
+
if(cbq = packet[:callback_query])
|
268
|
+
handle_callback_query(cbq);
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module XNM
|
7
|
+
module Telegram
|
8
|
+
# This class handles the direct connection to the Telegram API
|
9
|
+
# All it does is handle a receive loop with the fitting update IDs, as well
|
10
|
+
# as a rescue'd "perform post" message.
|
11
|
+
class HTTPCore
|
12
|
+
def initialize(apikey)
|
13
|
+
@apikey = apikey;
|
14
|
+
|
15
|
+
@lastUpdateID = 0;
|
16
|
+
# Start the receive loop. Shouldn't crash, but if it does we
|
17
|
+
# want to know.
|
18
|
+
@receiveThread = Thread.new do
|
19
|
+
receive_loop();
|
20
|
+
end
|
21
|
+
@receiveThread.abort_on_exception = true
|
22
|
+
|
23
|
+
# Receptors are class instances that will receive updates
|
24
|
+
# from the HTTP update connection
|
25
|
+
@receptors = Array.new();
|
26
|
+
end
|
27
|
+
|
28
|
+
private def _double_json_data(data)
|
29
|
+
return {} unless data
|
30
|
+
|
31
|
+
out_data = {}
|
32
|
+
# JSON-Ify nested Hashes and Arrays to a String before the main
|
33
|
+
# POST request is performed. Needed by Telegram, it seems.
|
34
|
+
data.each do |key, val|
|
35
|
+
if(val.is_a? Hash or val.is_a? Array)
|
36
|
+
out_data[key] = val.to_json
|
37
|
+
else
|
38
|
+
out_data[key] = val;
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
out_data
|
43
|
+
end
|
44
|
+
|
45
|
+
private def _raw_post(addr, data)
|
46
|
+
d_str = Shellwords.escape(data.to_json)
|
47
|
+
|
48
|
+
response = `curl -s -X POST -H "Content-Type: application/json" -d #{d_str} "#{addr}"`
|
49
|
+
|
50
|
+
JSON.parse(response, symbolize_names: true)
|
51
|
+
end
|
52
|
+
# Perform a POST request.
|
53
|
+
# @param method [String] The Telegram bot API method that should be called
|
54
|
+
# @param data [nil, Hash] The data to be sent with the command.
|
55
|
+
# Caution, any nested Hashes and Arrays will be converted to JSON
|
56
|
+
# BEFORE the Hash itself is also JSON-ified. Telegram apparently
|
57
|
+
# needs this to work.
|
58
|
+
def perform_post(method, data = nil)
|
59
|
+
call_address = "https://api.telegram.org/bot#{@apikey}/#{method}"
|
60
|
+
|
61
|
+
# Rescue-construct to prevent a HTTP error from
|
62
|
+
# crashing our system.
|
63
|
+
timeoutLen = data[:timeout] if data.is_a? Hash
|
64
|
+
timeoutLen ||= 4;
|
65
|
+
retryCount = 0;
|
66
|
+
begin
|
67
|
+
Timeout.timeout(timeoutLen) do
|
68
|
+
return _raw_post(call_address, _double_json_data(data));
|
69
|
+
end
|
70
|
+
rescue
|
71
|
+
retryCount += 1;
|
72
|
+
return {} if retryCount >= 3;
|
73
|
+
|
74
|
+
sleep 0.5;
|
75
|
+
retry
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def feed_receptors(data)
|
80
|
+
# Hand it out to the receptors
|
81
|
+
@receptors.each do |r|
|
82
|
+
begin
|
83
|
+
r.handle_packet(data);
|
84
|
+
rescue => e
|
85
|
+
warn "Error in repector: #{e}"
|
86
|
+
warn e.backtrace.join("\n");
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Handle receiving of the data from Telegram API
|
92
|
+
# This is done via the "getUpdates" HTTP Request, with a timeout
|
93
|
+
# of 20s
|
94
|
+
# Update-ID offset is handled automagically by the system, so you needn't
|
95
|
+
# worry about it.
|
96
|
+
def receive_loop()
|
97
|
+
loop do
|
98
|
+
begin
|
99
|
+
# Perform the update request
|
100
|
+
packet = perform_post("getUpdates", {timeout: 20, offset: @lastUpdateID + 1})
|
101
|
+
|
102
|
+
next unless packet[:ok];
|
103
|
+
# Check if there even was a message sent by Telegram
|
104
|
+
# Due to the 20s timeout, a zero-length reply can happen
|
105
|
+
next if packet[:result].length == 0;
|
106
|
+
|
107
|
+
# Handle each result individually.
|
108
|
+
packet[:result].each do |data|
|
109
|
+
hUpdateID = data[:update_id].to_i
|
110
|
+
# Calculate the maximum Update ID (for the offset "getUpdates" parameter)
|
111
|
+
@lastUpdateID = [hUpdateID, @lastUpdateID].max
|
112
|
+
|
113
|
+
feed_receptors data
|
114
|
+
end
|
115
|
+
rescue
|
116
|
+
sleep 1
|
117
|
+
retry
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# TODO check if the class supports handle_packet
|
123
|
+
def attach_receptor(receptorClass)
|
124
|
+
@receptors << receptorClass;
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|