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.
@@ -0,0 +1,302 @@
1
+
2
+ require_relative 'HTTPCore.rb'
3
+ require_relative 'Message.rb'
4
+ require_relative 'User.rb'
5
+
6
+ module XNM
7
+ module Telegram
8
+ # Dummy class to provide sensible defaults for most things.
9
+ # Only really matters for priority, so that we can sort
10
+ # after that.
11
+ #
12
+ # @todo Test if the sorting is the right way around
13
+ class OnTelegramEvent
14
+ attr_accessor :priority
15
+
16
+ def initialize()
17
+ @priority = 0
18
+ end
19
+
20
+ def nomp_message(message) end
21
+
22
+ def <=>(other)
23
+ self.priority <=> other.priority
24
+ end
25
+ end
26
+
27
+ # Main handler class, provides interfaces.
28
+ # It gives the user a way to send messages, set callbacks
29
+ # and retrieve chats with detailed data.
30
+ # More in-depth functions for messages are provided in the
31
+ # message class itself.
32
+ class Handler
33
+ # Returns the {HTTPCore} used to communicate with
34
+ # Telegram.
35
+ attr_reader :core
36
+
37
+ # List of permissions.
38
+ # Set this to a Hash containing arrays to
39
+ # included permissions, i.e. the following:
40
+ # {
41
+ # "admin" => ["default", "advanced"]
42
+ # }
43
+ #
44
+ # A user with "admin" would now also have "default" and "advanced"
45
+ # permissions for command execution.
46
+ #
47
+ # @note The :sudo permission will always overwrite everything, use
48
+ # only for developer access!
49
+ attr_accessor :permissions_list
50
+
51
+ def self.from_options(options)
52
+ out_handler = Handler.new(options['Key']);
53
+
54
+ if perms = options['Permissions']
55
+ raise ArgumentError, 'Permission list must be a hash!' unless perms.is_a? Hash
56
+ out_handler.permissions_list = perms
57
+ end
58
+
59
+ if u_list = options['Users']
60
+ raise ArgumentError, 'Userlist must be a hash!' unless u_list.is_a? Hash
61
+
62
+ u_list.each do |key, extra_opts|
63
+ u = out_handler[key];
64
+
65
+ unless u
66
+ warn "User #{key} could not be found."
67
+ next
68
+ end
69
+
70
+ if u_perms = extra_opts['Permissions']
71
+ u.add_permissions u_perms
72
+ end
73
+ end
74
+ end
75
+
76
+ out_handler
77
+ end
78
+
79
+ # initialize a new Handler.
80
+ #
81
+ # @param [String, HTTPCore] http_core The core to use, either
82
+ # a String representing the API Key, or an initialized Telegram Core
83
+ def initialize(http_core)
84
+ @core = if(http_core.is_a? Telegram::HTTPCore)
85
+ http_core;
86
+ elsif http_core.is_a? String
87
+ Telegram::HTTPCore.new(http_core);
88
+ else
89
+ raise ArgumentError, "Could not make a valid HTTPCore from string!"
90
+ end
91
+
92
+ @core.attach_receptor(self);
93
+
94
+ @chats = {}
95
+
96
+ @on_telegram_event = []
97
+
98
+ @permissions_list = {}
99
+ end
100
+
101
+ private def handle_message(message)
102
+ message = Message.new(self, message);
103
+
104
+ return unless message.valid
105
+
106
+ (@on_telegram_event + message.chat&.on_telegram_event).sort.each do |evt|
107
+ evt.nomp_message message
108
+
109
+ break if message.handled
110
+ end
111
+ end
112
+
113
+ # Handle an incoming callback query (inline keyboard button press)
114
+ # as received from the HTTP Core
115
+ private def handle_callback_query(cbq)
116
+ # Generate a fake message to feed into the message
117
+ # handling system ;)
118
+
119
+ fake_msg = {
120
+ from: cbq[:from],
121
+ text: cbq[:data],
122
+ chat: cbq[:message][:chat],
123
+ message_id: cbq[:message][:message_id],
124
+ reply_to_message: { message_id: cbq[:message][:message_id] }
125
+ }
126
+
127
+ handle_message fake_msg
128
+
129
+ # Send out a callback query reply (i.e. Telegram now knows we saw it)
130
+ @core.perform_post("answerCallbackQuery", {callback_query_id: cbq[:id]})
131
+ end
132
+
133
+ # Internal function, called from the Telegram Core.
134
+ # This is meant to be fed a Hash representing one update object
135
+ # from Telegram's /getUpdate function
136
+ def handle_packet(packet)
137
+ if m = packet[:message]
138
+ handle_message m
139
+ end
140
+
141
+ if cbq = packet[:callback_query]
142
+ handle_callback_query cbq
143
+ end
144
+ end
145
+
146
+ # Return a {Chat} object directly constructed from the
147
+ # passed Hash.
148
+ #
149
+ # Pass a Hash here to either fetch a chat with matching ID, or
150
+ # else constuct a new chat from the provided data. Can be used
151
+ # for getting a chat from a Message object, or adding a chat from
152
+ # stored chat configs.
153
+ def chat_from_object(object)
154
+ chat_id = object[:id];
155
+
156
+ if c = @chats[chat_id]
157
+ return c
158
+ end
159
+
160
+ c = nil;
161
+ if object[:username]
162
+ c = User.new(self, object);
163
+ else
164
+ c = Chat.new(self, object);
165
+ end
166
+
167
+ @chats[chat_id] = c;
168
+
169
+ c
170
+ end
171
+
172
+ # Try to find a chat with matching string ID.
173
+ # Useful when trying to find a chat by username, or a channel.
174
+ def chat_from_string(str)
175
+ @chats.each do |_, chat|
176
+ if chat.str_id == str
177
+ return chat
178
+ end
179
+ end
180
+
181
+ nil
182
+ end
183
+
184
+ # Return the {Chat} with given ID.
185
+ # This will either return a known chat with the wanted ID, or else
186
+ # will call getChat to fetch details on the wanted chat and
187
+ # construct a new {Chat} object. May also return nil if the wanted
188
+ # chat does not exist!
189
+ def chat_from_id(num)
190
+ if c = @chats[num]
191
+ return c
192
+ end
193
+
194
+ chat_obj = @core.perform_post('getChat', { chat_id: num });
195
+
196
+ return nil unless chat_obj[:ok]
197
+
198
+ chat_from_object chat_obj[:result]
199
+ end
200
+
201
+ # Convenience function to get a chat by any means.
202
+ # Pass a Number (interpreted as Chat ID), String (username)
203
+ # or Hash into here to try and fetch a Chat based on the parameter.
204
+ # Chat ID is preferred as it will let the system fetch the Chat from
205
+ # Telegram's API.
206
+ def get_chat(object)
207
+ if object.is_a? Chat
208
+ object
209
+ elsif object.is_a? Hash
210
+ chat_from_object object;
211
+ elsif object.is_a? Numeric
212
+ chat_from_id object
213
+ elsif object.is_a? String
214
+ if object =~ /^@/
215
+ chat_from_id object
216
+ else
217
+ chat_from_string object
218
+ end
219
+ end
220
+ end
221
+ alias [] get_chat
222
+
223
+ # Send a message to a given chat.
224
+ # The following options are supported when sending:
225
+ #
226
+ # - silent: true/false, whether to enable or disable notification
227
+ # - reply_to: {Message}/nil, try to reply to the given message ID.
228
+ # - inline_keyboard: Hash or Array of Hashes, to set the inline keyboard
229
+ # with fitting commands.
230
+ #
231
+ # @note When a Inline Keyboard is used, the button presses are
232
+ # internally interpreted as messages. This way, they can feed into
233
+ # the /command syntax.
234
+ def send_message(chat, text, **options)
235
+ raise ArgumentError, "Text needs to be a string" unless text.is_a? String
236
+
237
+ if text.length > 900
238
+ text = text[0..900] + "..."
239
+ end
240
+
241
+ out_data = {
242
+ chat_id: get_chat(chat).chat_id,
243
+ parse_mode: 'HTML',
244
+ text: text
245
+ }
246
+
247
+ if r = options[:reply_to]
248
+ out_data[:reply_to_message_id] = r.to_i
249
+ end
250
+
251
+ if options[:silent]
252
+ out_data[:disable_notification] = true;
253
+ end
254
+
255
+ if layout = options[:inline_keyboard]
256
+ out_data[:reply_markup] = KeyboardLayout.new(layout).ilk_reply_markup
257
+ end
258
+
259
+ reply = @core.perform_post('sendMessage', out_data);
260
+
261
+ Message.new(self, reply[:result]);
262
+ end
263
+
264
+ # Add a new callback on any generic message.
265
+ # The provided block will be called with the Message as parameter
266
+ # when something arrives. May additionally specify a RegExp to match
267
+ # against, in which case the match is a second parameter to the
268
+ # block.
269
+ def on_message(regexp = nil, &block)
270
+ raise ArgumentError, 'Block must be given!' unless block_given?
271
+
272
+ out_evt = OnMessage.new({ block: block, regexp: regexp });
273
+ @on_telegram_event << out_evt
274
+
275
+ out_evt
276
+ end
277
+
278
+ # Add a new callback on any /command message.
279
+ # The provided block will be called whenever the fitting /command
280
+ # is called. Additionally, by setting a list of priorities
281
+ # (options[:priorities] = []), only certain users may be allowed
282
+ # to execute a command.
283
+ #
284
+ # The block will be passed the message as argument.
285
+ def on_command(command, **options, &block)
286
+ raise ArgumentError, 'Block must be given!' unless block_given?
287
+
288
+ options[:block] = block
289
+ options[:command] = command
290
+
291
+ out_evt = OnCommand.new(options);
292
+ @on_telegram_event << out_evt
293
+
294
+ out_evt
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ require_relative 'OnMessage.rb'
301
+ require_relative 'OnCommand.rb'
302
+ require_relative 'KeyboardLayout.rb'
@@ -0,0 +1,67 @@
1
+
2
+ module XNM
3
+ module Telegram
4
+ class KeyboardLayout
5
+ def initialize(start_config = nil)
6
+ @rows = Array.new()
7
+
8
+ if start_config.is_a? Hash
9
+ start_config = [start_config]
10
+ end
11
+
12
+ if start_config.is_a? Array
13
+ start_config.each_index do |i|
14
+ j = 0
15
+ start_config[i].each do |k, v|
16
+ set_button(k, v, r: i, c: j)
17
+ j += 1
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def [](i)
24
+ @rows[i]
25
+ end
26
+
27
+ def set_button(name, command = nil, c: 0, r: 0)
28
+ command ||= name
29
+
30
+ @rows[r] ||= [];
31
+ out_button = {text: name}
32
+ if command =~ /^(http|tg)/
33
+ out_button[:url] = command
34
+ else
35
+ out_button[:callback_data] = command
36
+ end
37
+
38
+ @rows[r][c] = out_button
39
+ end
40
+
41
+ def ilk_reply_markup()
42
+ return { inline_keyboard: to_ilk_layout }
43
+ end
44
+
45
+ def to_ilk_layout()
46
+ out_data = [];
47
+
48
+ @rows.each do |row|
49
+ next if row.nil?
50
+ next if row.empty?
51
+
52
+ formatted_row = []
53
+
54
+ row.each do |button|
55
+ next if button.nil?
56
+
57
+ formatted_row << button
58
+ end
59
+
60
+ out_data << formatted_row unless formatted_row.empty?
61
+ end
62
+
63
+ out_data
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,73 @@
1
+
2
+ require_relative 'GroupingAdapter.rb'
3
+ require 'mqtt/sub_handler'
4
+
5
+ module XNM
6
+ module Telegram
7
+ module MQTT
8
+ class Server < GroupingAdapter
9
+ attr_accessor :usernameList
10
+
11
+ def initialize(httpCore, mqtt)
12
+ super(httpCore);
13
+
14
+ @mqtt = mqtt;
15
+ setup_mqtt();
16
+ end
17
+
18
+ def setup_mqtt()
19
+ @mqtt.subscribe_to "Telegram/+/Send" do |data, tSplit|
20
+ begin
21
+ data = JSON.parse(data, symbolize_names: true);
22
+ rescue
23
+ data = { text: data }
24
+ end
25
+
26
+ _handle_send(data, tSplit[0]);
27
+ end
28
+
29
+ @mqtt.subscribe_to "Telegram/+/Edit" do |data, tSplit|
30
+ begin
31
+ data = JSON.parse(data, symbolize_names: true);
32
+ rescue
33
+ next;
34
+ end
35
+
36
+ _handle_edit(data, tSplit[0])
37
+ end
38
+
39
+ @mqtt.subscribe_to "Telegram/+/Delete" do |data, tSplit|
40
+ _handle_delete(data, tSplit[0])
41
+ end
42
+
43
+ @mqtt.subscribe_to "Telegram/+/Release" do |data, tSplit|
44
+ # Resolve a saved Username to a User-ID
45
+ uID = tSplit[0];
46
+ uID = @usernameList[uID] if(@usernameList.key? uID)
47
+ uID = uID.to_i;
48
+
49
+ # Delete the stored GID key
50
+ @groupIDList[uID].delete(data);
51
+ end
52
+ end
53
+
54
+ def on_message(data, uID)
55
+ super(data, uID);
56
+ @mqtt.publish_to "Telegram/#{uID}/Received", data.to_json;
57
+ end
58
+ def on_command(data, uID)
59
+ super(data, uID);
60
+ @mqtt.publish_to "Telegram/#{uID}/Command", data.to_json;
61
+ end
62
+ def on_reply(data, uID)
63
+ super(data, uID);
64
+ @mqtt.publish_to "Telegram/#{uID}/Reply", data.to_json;
65
+ end
66
+ def on_callback_pressed(data, uID)
67
+ super(data, uID);
68
+ @mqtt.publish_to "Telegram/#{uID}/KeyboardPress", data.to_json
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,126 @@
1
+
2
+ module XNM
3
+ module Telegram
4
+ class Message
5
+ # Return whether or not parsing of the message was successful.
6
+ # TODO Actually perfom validity checks.
7
+ attr_reader :valid
8
+
9
+ # Returns the message ID of this message.
10
+ attr_reader :message_id
11
+ # Returns the {Chat} object this message was sent in.
12
+ attr_reader :chat
13
+ # Returns the {User} Object that sent this message.
14
+ attr_reader :user
15
+
16
+ # Optional argument, ID of the message that was replied to.
17
+ attr_reader :reply_to_id
18
+
19
+ # String, text of the message.
20
+ # Even if the message is not a String (i.e. a Sticker etc.),
21
+ # this will be at least an empty string.
22
+ attr_reader :text
23
+
24
+ # Timestamp, from Telegram, that the message was sent on.
25
+ attr_reader :timestamp
26
+
27
+ # Optional, command included in this message.
28
+ # Can be nil.
29
+ attr_reader :command
30
+
31
+ # Whether a command already handled this message.
32
+ # Usually means that it should not be processed any further,
33
+ # in order to prevent multiple commands from acting on the same
34
+ # message and causing weird behaviors.
35
+ attr_accessor :handled
36
+
37
+ # Initialize a message object.
38
+ # This will create a new message object with the given
39
+ # Telegram "Message" Hash. It can be taken directly from
40
+ # the Telegram API.
41
+ #
42
+ # The Message will automatically try to fetch the matching
43
+ # {Chat} and {User} that sent the message, and will also
44
+ # parse any additional metadata such as commands, stickers,
45
+ # etc.
46
+ def initialize(handler, message_object)
47
+ @handler = handler
48
+
49
+ return if message_object.nil?
50
+
51
+ @valid = true;
52
+
53
+ @message_id = message_object[:message_id]
54
+
55
+ @chat = handler[message_object[:chat]]
56
+ @user = handler[message_object[:from] || message_object[:sender_chat]]
57
+
58
+ @reply_to_id = message_object.dig(:reply_to_message, :message_id)
59
+
60
+ @text = message_object[:text] || "";
61
+
62
+ @timestamp = Time.at(message_object[:date] || 0)
63
+
64
+ m = /\/([\S]*)/.match(@text)
65
+ @command = m[1] if m
66
+
67
+ @handled = false
68
+ end
69
+
70
+ # Edit the text of the message.
71
+ # Simple wrapper for the 'editMessageText' Telegram
72
+ # API function, and will directly set the messge's text.
73
+ #
74
+ # parse_mode is set to HTML.
75
+ def edit_text(text)
76
+ out_data = {
77
+ chat_id: @chat.chat_id,
78
+ message_id: @message_id,
79
+ parse_mode: 'HTML',
80
+ text: text
81
+ }
82
+
83
+ @handler.core.perform_post('editMessageText', out_data);
84
+ end
85
+ alias text= edit_text
86
+
87
+ # Try to delete this message.
88
+ # Wrapper for Telegram's deleteMessage function
89
+ def delete!
90
+ @handler.core.perform_post('deleteMessage',
91
+ {
92
+ chat_id: @chat.chat_id,
93
+ message_id: @message_id
94
+ }
95
+ );
96
+ end
97
+
98
+ # Send a text message with it's reply set to this.
99
+ #
100
+ # This will send a new message with given text, whose reply
101
+ # message ID is set to this message. Makes it easy to respond to
102
+ # certain events quite cleanly.
103
+ def reply(text)
104
+ @chat.send_message(text, reply_to: self)
105
+ end
106
+
107
+ # Send a message to the chat this message originated from.
108
+ #
109
+ # This is a wrapper for message.chat.send_message, as it allows
110
+ # the Bot to easily respond to a {User}'s action in the same chat
111
+ # the user wrote it in. It will simply forward all arguments to
112
+ # {Chat#send_message}
113
+ def send_message(text, **opts)
114
+ @chat.send_message(text, **opts)
115
+ end
116
+
117
+ def to_s
118
+ @text
119
+ end
120
+
121
+ def to_i
122
+ @message_id
123
+ end
124
+ end
125
+ end
126
+ end