xasin-telegram 0.2.3 → 0.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/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
|