-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathswiftob.lua
352 lines (306 loc) · 8.81 KB
/
swiftob.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
--[[
Copyright (c) 2014 Remko Tronçon
Licensed under the GNU General Public License v3.
See LICENSE for more information.
--]]
local table = require('table')
local debug = require('debug')
local io = require('io')
local os = require('os')
local sluift = require('sluift')
local environment = require('environment')
local serialize = require('serialize')
local storage = require('storage')
local program_options = require('program_options')
local util = require('util')
local configuration = require('configuration')
-- The (maximum) interval between iterators of the event loop.
-- This shouldn't really impact anything, so better leave it as it is.
local SLEEP_INTERVAL = 10
local SCRIPTS_DIR = "scripts"
-- Forward declarations
local reload
local client = nil
local config = nil
local tasks = {}
local commands = {}
local listeners = {}
local periodicals = {}
local quit_requested = false
local restart_requested = false
local function call(f)
return xpcall(f, function(e) print(debug.traceback(e)) end)
end
local function quit()
quit_requested = true
end
local function restart()
restart_requested = true
end
local function is_role_allowed(role, allowed_role)
if role == "owner" then return true end
for _, allowed_role in pairs(allowed_role) do
if allowed_role == 'anyone' or role == allowed_role then return true end
end
return false
end
local function get_role(jid)
-- TODO
end
local function command_descriptions_for_jid(jid, is_muc)
local role = get_role(jid)
local bang = ''
if is_muc then bang = '!' end
local result = {'Available commands:'}
for _, command in pairs(commands) do
if is_role_allowed(role, command.allowed_role) then
table.insert(result, bang .. command.name .. ": " .. command.description)
end
end
return table.concat(result, '\n')
end
local function register_task(task)
table.insert(tasks, {
next_activation_time = os.time(),
coroutine = coroutine.create(task)
})
end
local function register_command(command)
if not command.name then error("Missing command name") end
if not command.callback then error("Missing command callback") end
command.description = command.description or ""
command.allowed_role = command.allowed_role or "anyone"
table.insert(commands, command)
end
local function register_listener(listener, options)
options = options or {}
if options.include_own_messages == nil then
options.include_own_messages = true
end
table.insert(listeners, {
callback = listener,
include_own_messages = options.include_own_messages
})
end
local function register_periodical(periodical)
if not periodical.callback then error("Missing callback") end
if not periodical.interval then error("Missing interval") end
table.insert(periodicals, {
interval = periodical.interval,
callback = periodical.callback,
next_activation_time = os.time()
})
end
local function reply_to(message, body, options)
local options = options or {}
local to = message['from']
local message_type = message.type
if options.out_of_muc then
to = sluift.jid.to_bare(to)
message_type = 'chat'
end
if message_type == 'groupchat' then
body = sluift.jid.resource(message['from']) .. ': ' .. body
end
if client:is_connected() then
client:send_message{to = to, type = message_type, body = body}
else
print("Dropping message")
end
end
local function load_script(script_file, privileged)
local _, _, basename = string.find(script_file, "([^\\/:]*)%.lua$")
local script_storage = storage.load(script_file .. '.storage')
-- Create swiftob interface
local swiftob = {
register_task = register_task,
register_command = register_command,
register_listener = register_listener,
register_periodical = register_periodical,
get_setting = function(...) script_storage:get_setting(...) end,
set_setting = function(...) script_storage:set_setting(...) end,
reply_to = reply_to,
-- Sluift modules
jid = sluift.jid,
base64 = sluift.base64,
idn = sluift.idn,
crypto = sluift.crypto,
fs = sluift.fs,
itunes = sluift.itunes,
disco = sluift.disco,
-- Sluift functions
copy = sluift.copy,
with = sluift.with,
read_file = sluift.read_file,
tprint = sluift.tprint,
-- Configuration
config = config.scripts[basename] or {}
}
-- Add privileged commands
if privileged then
swiftob.quit = quit
swiftob.restart = restart
swiftob.reload = reload
swiftob.command_descriptions_for_jid = command_descriptions_for_jid
end
-- Add fallback to client
setmetatable(swiftob, {
__index = function(table, key)
if client then
local f = client[key]
if f then
return function(...)
return f(client, ...)
end
end
end
end
})
-- Create the script sandbox environment
local env = environment.new_environment()
env.swiftob = swiftob
env.sleep = coroutine.yield
-- Load the script
local script, message = util.load_file(script_file, env)
if not script then
print("Unable to read " .. script_file .. ": " .. message)
return
end
local result, message = call(function() script(swiftob) end)
if not result then
print(message)
end
end
local function load_privileged_script(script_file)
load_script(script_file, true)
end
local function parse_command(message)
local body = message['body']
if message['type'] ~= 'groupchat' and not string.find(body, '^!') then
body = '!' .. body
end
local _, _, command_name, arguments = string.find(body, "^!(%w+)%s+(.*)")
if not command_name then
_, _, command_name = string.find(body, "^!(%w+)$")
end
return command_name, arguments
end
local function get_earliest_activation_time()
-- Initialize value
local result
if #tasks > 0 then
result = tasks[1].next_activation_time
elseif #periodicals > 0 then
result = periodicals[1].next_activation_time
else
return SLEEP_INTERVAL
end
-- Find lowest task
for _, task in pairs(tasks) do
if task.next_activation_time < result then
result = task.next_activation_time
end
end
-- Find lowest periodical
for _, periodical in pairs(periodicals) do
if periodical.next_activation_time < result then
result = periodical.next_activation_time
end
end
return result
end
-- reloads all the scripts
function reload()
tasks = {}
commands = {}
listeners = {}
periodicals = {}
print("Loading scripts ...")
load_privileged_script('builtins.lua')
local scripts = sluift.fs.list(SCRIPTS_DIR)
if scripts then
for _, script in pairs(scripts) do
if sluift.fs.is_file(script) and string.find(script, "%.lua$") then
load_script(script)
end
end
end
end
-- Parse options
local options = program_options.parse(arg)
local config_file = options.config or (os.getenv("HOME") .. "/.swiftob.lua")
if options.help then
print(string.format([[Usage: %s [OPTIONS]
Options:
--config=FILE Use FILE as config file
]], arg[0]))
do return end
end
if type(config_file) ~= "string" then error("Expected config file") end
-- Load configuration file
config = configuration.load(config_file)
-- Load all the scripts
reload()
-- Start the loop
local jid = config.jid
local password = config.password
--sluift.debug = 1
client = sluift.new_client(jid, password)
while not quit_requested do
restart_requested = false
client:connect(function()
print("Connected")
client:send_presence{type = 'available'}
while not quit_requested and not restart_requested do
local current_time = os.time()
-- Process the tasks
local new_tasks = {}
for _, task in pairs(tasks) do
if task.next_activation_time <= current_time then
local success, result = coroutine.resume(task.coroutine)
if success then
if coroutine.status(task.coroutine) ~= "dead" then
task.next_activation_time = current_time + result
table.insert(new_tasks, task)
end
else
print(result)
end
else
table.insert(new_tasks, task)
end
end
tasks = new_tasks
-- Process the periodicals
for _, periodical in pairs(periodicals) do
if periodical.next_activation_time <= current_time then
call(periodical.callback)
periodical.next_activation_time = current_time + periodical.interval
end
end
-- Get the next event time
local next_activation_time = get_earliest_activation_time()
local event = client:get_next_event{timeout = next_activation_time - current_time}
-- Dispatch the event
if event and (event['type'] == 'message' or event['type'] == 'groupchat') then
local message = sluift.copy(event)
message.type = message.message_type
message.message_type = nil
-- Notify listeners
for _, listener in pairs(listeners) do
-- TODO: Check own messages
call(function() listener.callback(message) end)
end
-- Handle commands
local command_name, arguments = parse_command(message)
if command_name then
for _, command in pairs(commands) do
if command.name == command_name then
call(function() command.callback(command_name, arguments, message) end)
end
end
end
end
end
end)
end