xasin-telegram 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +24 -1
- data/lib/xasin/bad_config.yml +7 -0
- data/lib/xasin/bad_test.rb +38 -0
- data/lib/xasin/telegram.rb +1 -0
- data/lib/xasin/telegram/Chat.rb +87 -0
- data/lib/xasin/telegram/GroupingAdapter.rb +273 -0
- data/lib/xasin/telegram/HTTPCore.rb +75 -22
- data/lib/xasin/telegram/Handler.rb +301 -0
- data/lib/xasin/telegram/KeyboardLayout.rb +67 -0
- data/lib/xasin/telegram/MQTT_Adapter.rb +21 -188
- data/lib/xasin/telegram/Message.rb +126 -0
- data/lib/xasin/telegram/OnCommand.rb +34 -0
- data/lib/xasin/telegram/OnMessage.rb +29 -0
- data/lib/xasin/telegram/SingleUser.rb +12 -7
- data/lib/xasin/telegram/TestHTTPCore.rb +30 -8
- data/lib/xasin/telegram/User.rb +142 -0
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c2ccbe2c5e19793d17753159a59acf85dec93d22
|
4
|
+
data.tar.gz: e65517722e9e6e3899465f549e09098ae2f1afdc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c47bcc7ca20f4601a05124f54e5cbd6401830549ff498dd56bd288c93144460ac066e2a6034a1b82ccec54e948865c04b0b0d5316264c2f46459b0b84de541a
|
7
|
+
data.tar.gz: 9b5b1f2572223209dc066acf0bac202c6ba1f758eec3308d9486b1ec063ab24e36ee35b7e642a1a33f2f8f0e359c887506c6993fc8afa935d6693aba720e3bfd
|
data/README.md
CHANGED
@@ -8,8 +8,31 @@ My gem doesn't include all functionality, but aims to provide a somewhat simpler
|
|
8
8
|
interaction with a small number (or just a single) of users.
|
9
9
|
I mainly use it for my own smart home system, to easily send and receive messages, but not much more.
|
10
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
|
+
|
11
34
|
## Single-User mode
|
12
|
-
|
35
|
+
Another way to use the HTTP-Core is by only looking at a single user.
|
13
36
|
It discards messages of all users except one, and provides a simple "send" and "on_message" interface:
|
14
37
|
|
15
38
|
```Ruby
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
require_relative 'telegram/Handler.rb'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
opts = YAML.load(File.read('bad_config.yml'));
|
7
|
+
|
8
|
+
puts "My opts are #{opts}"
|
9
|
+
|
10
|
+
handler = Xasin::Telegram::Handler.from_options(opts['Telegram'])
|
11
|
+
|
12
|
+
$xasin = handler[87816854]
|
13
|
+
msg = $xasin.send_message "Smol test :>"
|
14
|
+
|
15
|
+
handler.on_command 'give_me_perm' do |msg|
|
16
|
+
permission = /\/give_me_perm (.*)/.match(msg.text)[1]
|
17
|
+
|
18
|
+
msg.user.add_permission permission
|
19
|
+
|
20
|
+
msg.send_message "You now have the following permissions: #{msg.user.permissions}",
|
21
|
+
inline_keyboard: { "Good!" => "/give_me_perm sudo", "Recall!" => "/total_recall"}
|
22
|
+
end
|
23
|
+
|
24
|
+
handler.on_command 'total_recall', permissions: 'can_total_recall' do |msg|
|
25
|
+
msg.send_message "Yes, recalling!"
|
26
|
+
end
|
27
|
+
|
28
|
+
sleep 4
|
29
|
+
|
30
|
+
msg.edit_text "NOT SO SMALL ANY MORE, AI?"
|
31
|
+
|
32
|
+
sleep 2
|
33
|
+
|
34
|
+
msg.delete!
|
35
|
+
|
36
|
+
loop do
|
37
|
+
sleep 3
|
38
|
+
end
|
data/lib/xasin/telegram.rb
CHANGED
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
module Xasin
|
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 Xasin
|
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
|