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.
- checksums.yaml +7 -0
- data/README.md +64 -0
- data/lib/xnm/telegram.rb +4 -0
- data/lib/xnm/telegram/Chat.rb +87 -0
- data/lib/xnm/telegram/GroupingAdapter.rb +273 -0
- data/lib/xnm/telegram/HTTPCore.rb +128 -0
- data/lib/xnm/telegram/Handler.rb +302 -0
- data/lib/xnm/telegram/KeyboardLayout.rb +67 -0
- data/lib/xnm/telegram/MQTT_Adapter.rb +73 -0
- data/lib/xnm/telegram/Message.rb +126 -0
- data/lib/xnm/telegram/OnCommand.rb +34 -0
- data/lib/xnm/telegram/OnMessage.rb +29 -0
- data/lib/xnm/telegram/SingleUser.rb +78 -0
- data/lib/xnm/telegram/TestHTTPCore.rb +62 -0
- data/lib/xnm/telegram/User.rb +142 -0
- metadata +72 -0
@@ -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
|