xnm-telegram 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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