xnm-telegram 0.4.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 +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
|