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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 25c7030241f3f7f6eb094897f5175c49a177e299
4
- data.tar.gz: 9d335a41200777ead31dad505e059d3a9ae726c1
3
+ metadata.gz: c2ccbe2c5e19793d17753159a59acf85dec93d22
4
+ data.tar.gz: e65517722e9e6e3899465f549e09098ae2f1afdc
5
5
  SHA512:
6
- metadata.gz: 6fe1d6ce851631e0460bba3b843eef7fbf3583bff045bd854870c9cf1cb8526c8b300e5a793d7e13e5c8ebbf22a1fc926bf0727a7f13b7d243faf35878c26685
7
- data.tar.gz: 2dc0c584e4e5e4b829d0f8c8b12ef0664a78326f6d5d7769fdfbb4da53bd2c3a47688137ea17f39bd12042bb980b65a1770629307b7b086bcfb099801b9eddbf
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
- The main element of this gem is the single user mode.
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,7 @@
1
+ Telegram:
2
+ Key: 1020712802:AAHNFf0Ofqo1pFA2f4JYgCvaHEqPWjmffBY
3
+ Permissions:
4
+ Admin:
5
+ - can_total_recall
6
+ Users:
7
+ 87816854: {}
@@ -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
@@ -1,3 +1,4 @@
1
1
 
2
2
  require_relative "telegram/HTTPCore.rb"
3
3
  require_relative "telegram/SingleUser.rb"
4
+ require_relative "telegram/MQTT_Adapter.rb"
@@ -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