-
Notifications
You must be signed in to change notification settings - Fork 52
/
matrix.lua
3455 lines (3193 loc) · 117 KB
/
matrix.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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- WeeChat Matrix.org Client
-- vim: expandtab:ts=4:sw=4:sts=4
-- luacheck: globals weechat command_help command_connect matrix_command_cb matrix_away_command_run_cb configuration_changed_cb real_http_cb matrix_unload http_cb upload_cb send buffer_input_cb poll polltimer_cb cleartyping otktimer_cb join_command_cb part_command_cb leave_command_cb me_command_cb topic_command_cb upload_command_cb query_command_cb create_command_cb createalias_command_cb invite_command_cb list_command_cb op_command_cb voice_command_cb devoice_command_cbtow.config_get_plugin('timeout')) kick_command_cb deop_command_cb nick_command_cb whois_command_cb notice_command_cb msg_command_cb encrypt_command_cb public_command_cb names_command_cb more_command_cb roominfo_command_cb name_command_cb closed_matrix_buffer_cb closed_matrix_room_cb typing_notification_cb buffer_switch_cb typing_bar_item_cb devoice_command_cb
-- lots of shadowing here, just ignore it
-- luacheck: ignore current_buffer
--[[
Author: xt <xt@xt.gg>
Thanks to Ryan Huber of wee_slack.py for some ideas and inspiration.
This script is considered alpha quality as only the bare minimal of
functionality is in place and it is not very well tested.
It is known to be able to crash WeeChat in certain scenarioes so all
usage of this script is at your own risk.
If at any point there seems to be problem, make sure you update to
the latest version of this script. You can also try reloading the
script using /lua reload matrix to refresh all the state.
Power Levels
------------
A default Matrix room has power level between 0 to 100.
This script maps this as follows:
~ Room creator
& Power level 100
@ Power level 50
+ Power level > 0
TODO
----
/ban
Giving people arbitrary power levels
Lazyload messages instead of HUGE initialSync
Dynamically fetch more messages in backlog when user reaches the
oldest message using pgup
Need a way to change room join rule
Fix broken state after failed initial connect
Fix parsing of multiple join messages
Friendlier error message on bad user/password
Parse some HTML and turn into color/bold/etc
Support weechat.look.prefix_same_nick
]]
local json = require 'cjson' -- apt-get install lua-cjson
local olmstatus, olm = pcall(require, 'olm') -- LuaJIT olm FFI binding ln -s ~/olm/olm.lua /usr/local/share/lua/5.1
local w = weechat
local SCRIPT_NAME = "matrix"
local SCRIPT_AUTHOR = "xt <xt@xt.gg>"
local SCRIPT_VERSION = "3"
local SCRIPT_LICENSE = "MIT"
local SCRIPT_DESC = "Matrix.org chat plugin"
local SCRIPT_COMMAND = SCRIPT_NAME
local WEECHAT_VERSION
local SERVER
local STDOUT = {}
local OUT = {}
local BUFFER
local Room
local MatrixServer
local Olm
local DEBUG = false
-- How many seconds to timeout if nothing happened on the server. If something
-- happens before it will return sooner.
-- default Nginx proxy timeout is 60s, so we go slightly lower
local POLL_INTERVAL = 55
-- Time in seconds until a connection is assumed to be timed out.
-- Floating values like 0.4 should work too.
local timeout = 5*1000 -- overriden by w.config_get_plugin later
local current_buffer
local default_color = w.color('default')
-- Cache error variables so we don't have to look them up for every error
-- message, a normal user will not change these ever anyway.
local errprefix
local errprefix_c
local HOMEDIR
local OLM_ALGORITHM = 'm.olm.v1.curve25519-aes-sha2'
local OLM_KEY = 'secr3t' -- TODO configurable using weechat sec data
local v2_api_ns = '_matrix/client/v2_alpha'
local function tprint(tbl, indent, out)
if not indent then indent = 0 end
if not out then out = BUFFER end
for k, v in pairs(tbl) do
local formatting = string.rep(" ", indent) .. k .. ": "
if type(v) == "table" then
w.print(out, formatting)
tprint(v, indent+1, out)
elseif type(v) == 'boolean' then
w.print(out, formatting .. tostring(v))
elseif type(v) == 'userdata' then
w.print(out, formatting .. tostring(v))
else
w.print(out, formatting .. v)
end
end
end
local function mprint(message)
-- Print message to matrix buffer
if type(message) == 'table' then
tprint(message)
else
message = tostring(message)
w.print(BUFFER, message)
end
end
local function perr(message)
if message == nil then return end
-- Print error message to the matrix "server" buffer using WeeChat styled
-- error message
mprint(
errprefix_c ..
errprefix ..
'\t' ..
default_color ..
tostring(message)
)
end
local function dbg(message)
perr('- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -')
if type(message) == 'table' then
tprint(message)
else
message = ("DEBUG\t%s"):format(tostring(message))
mprint(BUFFER, message)
end
end
local dtraceback = debug.traceback
-- luacheck: ignore debug
debug.traceback = function (...)
if select('#', ...) >= 1 then
local err, lvl = ...
local trace = dtraceback(err, (lvl or 2)+1)
perr(trace)
end
-- direct call to debug.traceback: return the original.
-- debug.traceback(nil, level) doesn't work in Lua 5.1
-- (http://lua-users.org/lists/lua-l/2011-06/msg00574.html), so
-- simply remove first frame from the stack trace
return (dtraceback(...):gsub("(stack traceback:\n)[^\n]*\n", "%1"))
end
local function weechat_eval(text)
if WEECHAT_VERSION >= 0x00040200 then
return w.string_eval_expression(text,{},{},{})
end
return text
end
local urllib = {}
urllib.quote = function(str)
if not str then return '' end
if type(str) == 'number' then return str end
return str:gsub(
'([^%w ])',
function (c)
return string.format ("%%%02X", string.byte(c))
end
):gsub(' ', '+')
end
urllib.urlencode = function(tbl)
local out = {}
for k, v in pairs(tbl) do
table.insert(out, urllib.quote(k)..'='..urllib.quote(v))
end
return table.concat(out, '&')
end
local function accesstoken_redact(str)
return (str:gsub('access.*token=[0-9a-zA-Z%%]*', 'access_token=[redacted]'))
end
local transaction_id_counter = 0
local function get_next_transaction_id()
transaction_id_counter = transaction_id_counter + 1
return transaction_id_counter
end
--[[
-- Function for signing json, unused for now, we hand craft the required
-- signed json in the encryption function. But I think this function will be
-- needed in the future so leaving it here in a commented version
local function sign_json(json_object, signing_key, signing_name)
-- See: https://github.com/matrix-org/matrix-doc/blob/master/specification/31_event_signing.rst
-- Maybe use:http://regex.info/code/JSON.lua which sorts keys
local signatures = json_object.signatures or {}
json_object.signatures = nil
local unsigned = json_object.unsigned or nil
json_object.unsigned = nil
-- TODO ensure canonical json
local signed = signing_key:sign(json.encode(json_object))
local signature_base64 = encode_base64(signed.signature)
local key_id = ("%s:%s"):format(signing_key.agl, signing_key.version)
signatures[signing_name] = {[key_id] = signature_base64}
json_object.signatures = signatures
if unsigned then
json_object.unsigned = unsigned
end
return json_object
end
--]]
local function split_args(args)
local function_name, arg = args:match('^(.-) (.*)$')
return function_name, arg
end
local function byte_to_tag(s, byte, open_tag, close_tag)
if s:match(byte) then
local inside = false
local open_tags = 0
local htmlbody = s:gsub(byte, function(c)
if inside then
inside = false
return close_tag
end
inside = true
open_tags = open_tags + 1
return open_tag
end)
local _, count = htmlbody:gsub(close_tag, '')
-- Ensure we close tags
if count < open_tags then
htmlbody = htmlbody .. close_tag
end
return htmlbody
end
return s
end
local function irc_formatting_to_html(s)
-- TODO, support foreground and background?
local ct = {'white','black','blue','green','red','maroon','purple',
'orange','yellow','lightgreen','teal','cyan', 'lightblue',
'fuchsia', 'gray', 'lightgray'}
s = byte_to_tag(s, '\02', '<strong>', '</strong>')
s = byte_to_tag(s, '\029', '<em>', '</em>')
s = byte_to_tag(s, '\031', '<u>', '</u>')
-- First do full color strings with reset.
-- Iterate backwards to catch long colors before short
for i=#ct,1,-1 do
s = s:gsub(
'\003'..tostring(i-1)..'(.-)\003',
'<font color="'..ct[i]..'">%1</font>')
end
-- Then replace unmatch colors
-- Iterate backwards to catch long colors before short
for i=#ct,1,-1 do
local c = ct[i]
s = byte_to_tag(s, '\003'..tostring(i-1),
'<font color="'..c..'">', '</font>')
end
return s
end
local function strip_irc_formatting(s)
if not s then return '' end
return (s
:gsub("\02", "")
:gsub("\03%d%d?,%d%d?", "")
:gsub("\03%d%d?", "")
:gsub("\03", "")
:gsub("\15", "")
:gsub("\17", "")
:gsub("\18", "")
:gsub("\22", "")
:gsub("\29", "")
:gsub("\31", ""))
end
local function irc_formatting_to_weechat_color(s)
-- TODO, support foreground and background?
-- - is atribute to remove formatting
-- | is to keep formatting during color changes
s = byte_to_tag(s, '\02', w.color'bold', w.color'-bold')
s = byte_to_tag(s, '\029', w.color'italic', w.color'-italic')
s = byte_to_tag(s, '\031', w.color'underline', w.color'-underline')
-- backwards to catch long numbers before short
for i=16,1,-1 do
i = tostring(i)
s = byte_to_tag(s, '\003'..i,
w.color("|"..i), w.color("-"..i))
end
return s
end
function matrix_unload()
w.print('', 'matrix: Unloading')
-- Clear/free olm memory if loaded
if olmstatus then
w.print('', 'matrix: Saving olm state')
SERVER.olm:save()
w.print('', 'matrix: Clearing olm state from memory')
SERVER.olm.account:clear()
--SERVER.olm = nil
end
w.print('', 'matrix: done cleaning up!')
return w.WEECHAT_RC_OK
end
local function wconf(optionname)
return w.config_string(w.config_get(optionname))
end
local function wcolor(optionname)
return w.color(wconf(optionname))
end
function command_help(current_buffer, args)
if args then
local help_cmds = {args=args}
if not help_cmds then
w.print("", "Command not found: " .. args)
return
end
for cmd, helptext in pairs(help_cmds) do
w.print('', w.color("bold") .. cmd)
w.print('', (helptext or 'No help text').strip())
w.print('', '')
end
end
end
function command_connect(current_buffer, args)
if not SERVER.connected then
SERVER:connect()
end
return w.WEECHAT_RC_OK
end
function matrix_command_cb(data, current_buffer, args)
if args == 'connect' then
return command_connect(current_buffer)
elseif args == 'debug' then
if DEBUG then
DEBUG = false
w.print('', SCRIPT_NAME..': debugging messages disabled')
else
DEBUG = true
w.print('', SCRIPT_NAME..': debugging messages enabled')
end
elseif args:match('^msg ') then
local _
_, args = split_args(args) -- remove cmd
local roomarg, msg = split_args(args)
local room
for id, r in pairs(SERVER.rooms) do
-- Send /msg to a ID
if id == roomarg then
room = r
break
elseif roomarg == r.name then
room = r
break
elseif roomarg == r.roomname then
room = r
break
end
end
if room then
room:Msg(msg)
return w.WEECHAT_RC_OK_EAT
end
else
perr("Command not found: " .. args)
end
return w.WEECHAT_RC_OK
end
function matrix_away_command_run_cb(data, buffer, args)
-- Callback for command /away -all
local _
_, args = split_args(args) -- remove cmd
local msg
_, msg = split_args(args)
w.buffer_set(BUFFER, "localvar_set_away", msg)
for id, room in pairs(SERVER.rooms) do
if msg and msg ~= '' then
w.buffer_set(room.buffer, "localvar_set_away", msg)
else
-- Delete takes empty string, and not nil
w.buffer_set(room.buffer, "localvar_del_away", '')
end
end
if msg and msg ~= '' then
SERVER:SendPresence('unavailable', msg)
mprint 'You have been marked as unavailable'
else
SERVER:SendPresence('online', nil)
mprint 'You have been marked as online'
end
return w.WEECHAT_RC_OK
end
function configuration_changed_cb(data, option, value)
if option == 'plugins.var.lua.matrix.timeout' then
timeout = tonumber(value)*1000
elseif option == 'plugins.var.lua.matrix.debug' then
if value == 'on' then
DEBUG = true
w.print('', SCRIPT_NAME..': debugging messages enabled')
else
DEBUG = false
w.print('', SCRIPT_NAME..': debugging messages disabled')
end
end
end
local function http(url, post, cb, h_timeout, extra, api_ns)
if not post then
post = {}
end
if not cb then
cb = 'http_cb'
end
if not h_timeout then
h_timeout = 60*1000
end
if not extra then
extra = nil
end
if not api_ns then
api_ns = "_matrix/client/r0"
end
-- Add accept encoding by default if it's not already there
if not post.accept_encoding then
post.accept_encoding = 'application/json'
end
if not post.header then
post.header = 1 -- Request http headers in the response
end
if not url:match'https?://' then
local homeserver_url = w.config_get_plugin('homeserver_url')
homeserver_url = homeserver_url .. api_ns
url = homeserver_url .. url
end
if DEBUG then
dbg{request={
url=accesstoken_redact(url),
post=post,extra=extra}
}
end
w.hook_process_hashtable('url:' .. url, post, h_timeout, cb, extra)
end
local function parse_http_statusline(line)
-- Attempt to match HTTP/1.0 or HTTP/1.1
local httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.-)\r?\n")
if not httpversion then
-- Attempt to match HTTP/1 or HTTP/2 if previous match fell through
httpversion, status_code, reason_phrase = line:match("^HTTP/([12]) (%d%d%d) (.-)\r?\n")
if not httpversion then
return
end
end
return httpversion, tonumber(status_code), reason_phrase
end
function real_http_cb(extra, command, rc, stdout, stderr)
if DEBUG then
dbg{reply={
command=accesstoken_redact(command),
extra=extra,rc=rc,stdout=stdout,stderr=stderr}
}
end
if stderr and stderr ~= '' then
mprint(('error: %s'):format(accesstoken_redact(stderr)))
return w.WEECHAT_RC_OK
end
-- Because of a bug in WeeChat sometimes the stdout gets prepended by
-- any number of BEL chars (hex 07). Let's have a nasty workaround and
-- just replace them away.
if WEECHAT_VERSION < 0x01030000 then -- fixed in 1.3
stdout = (stdout:gsub('^\007*', ''))
end
if stdout ~= '' then
if not STDOUT[command] then
STDOUT[command] = {}
end
table.insert(STDOUT[command], stdout)
end
if tonumber(rc) >= 0 then
stdout = table.concat(STDOUT[command] or {})
STDOUT[command] = nil
local httpversion, status_code, reason_phrase = parse_http_statusline(stdout)
if not httpversion then
perr(('Invalid http request: %s'):format(stdout))
return w.WEECHAT_RC_OK
end
if status_code == 504 and command:find'/sync' then -- keep hammering to try to get in as the server will keep slowly generating the response
SERVER:initial_sync()
return w.WEECHAT_RC_OK
end
if status_code >= 500 then
perr(('HTTP API returned error. Code: %s, reason: %s'):format(status_code, reason_phrase))
return w.WEECHAT_RC_OK
end
-- Skip to data
stdout = (stdout:match('.-\r?\n\r?\n(.*)'))
-- Protected call in case of JSON errors. 'json.new()' ensures that locale is detected correctly (fixes bug #49)
local success, js = pcall(json.new().decode, stdout)
if not success then
mprint(('error\t%s during json load: %s'):format(js, stdout))
return w.WEECHAT_RC_OK
end
if js['errcode'] or js['error'] then
if command:find'login' then
w.print('', ('matrix: Error code during login: %s, code: %s'):format(
js['error'], js['errcode']))
w.print('', 'matrix: Please verify your username and password')
else
perr('API call returned error: '..js['error'] .. '('..tostring(js.errcode)..')')
end
return w.WEECHAT_RC_OK
end
-- Get correct handler
if command:find('login') then
for k, v in pairs(js) do
SERVER[k] = v
end
SERVER.connected = true
SERVER:initial_sync()
elseif command:find'/rooms/.*/initialSync' then
local myroom = SERVER:addRoom(js)
for _, chunk in ipairs(js['presence']) do
myroom:ParseChunk(chunk, true, 'presence')
end
for _, chunk in ipairs(js['messages']['chunk']) do
myroom:ParseChunk(chunk, true, 'messages')
end
elseif command:find'/sync' then
SERVER.end_token = js.next_batch
-- We have a new end token, which means we safely can release the
-- poll lock
SERVER.poll_lock = false
local backlog = false
local initial = false
if extra == 'initial' then
initial = true
backlog = true
end
-- Start with setting the global presence variable on the server
-- so when the nicks get added to the room they can get added to
-- the correct nicklist group according to if they have presence
-- or not
for _, e in ipairs(js.presence.events) do
SERVER:UpdatePresence(e)
end
for membership, rooms in pairs(js['rooms']) do
-- If we left the room, simply ignore it
if membership ~= 'leave' or (membership == 'leave' and (not backlog)) then
for identifier, room in pairs(rooms) do
-- Monkey patch it to look like v1 object
room.room_id = identifier
local myroom
if initial then
myroom = SERVER:addRoom(room)
else
myroom = SERVER.rooms[identifier]
-- Chunk for non-existing room
if not myroom then
myroom = SERVER:addRoom(room)
if not membership == 'invite' then
perr('Event for unknown room')
end
end
end
-- First of all parse invite states.
local inv_states = room.invite_state
if inv_states then
local chunks = room.invite_state.events or {}
for _, chunk in ipairs(chunks) do
myroom:ParseChunk(chunk, backlog, 'states')
end
end
-- Parse states before messages so we can add nicks and stuff
-- before messages start appearing
local states = room.state
if states then
local chunks = room.state.events or {}
for _, chunk in ipairs(chunks) do
myroom:ParseChunk(chunk, backlog, 'states')
end
end
local timeline = room.timeline
if timeline then
-- Save the prev_batch on the initial message so we
-- know for later when we picked up the sync
if initial then
myroom.prev_batch = timeline.prev_batch
end
local chunks = timeline.events or {}
for _, chunk in ipairs(chunks) do
myroom:ParseChunk(chunk, backlog, 'messages')
end
end
local ephemeral = room.ephemeral
-- Ignore Ephemeral Events during initial sync
if (extra and extra ~= 'initial') and ephemeral then
local chunks = ephemeral.events or {}
for _, chunk in ipairs(chunks) do
myroom:ParseChunk(chunk, backlog, 'states')
end
end
local account_data = room.account_data
if account_data then
-- looks for m.fully_read event
local chunks = account_data.events or {}
for _, chunk in ipairs(chunks) do
myroom:ParseChunk(chunk, backlog, 'account_data')
end
end
if backlog then
-- All the state should be done. Try to get a good name for the room now.
myroom:SetName(myroom.identifier)
end
end
end
end
-- Now we have created rooms and can go over the rooms and update
-- the presence for each nick
for _, e in pairs(js.presence.events) do
SERVER:UpdatePresence(e)
end
if initial then
SERVER:post_initial_sync()
end
SERVER:poll()
elseif command:find'messages' then
local identifier = extra
local myroom = SERVER.rooms[identifier]
myroom.prev_batch = js['end']
-- Freeze buffer
myroom:Freeze()
-- Clear buffer
myroom:Clear()
-- We request backwards direction, so iterate backwards
for i=#js.chunk,1,-1 do
local chunk = js.chunk[i]
myroom:ParseChunk(chunk, true, 'messages')
end
-- Thaw!
myroom:Thaw()
elseif command:find'/join/' then
-- We came from a join command, fecth some messages
local found = false
for id, _ in pairs(SERVER.rooms) do
if id == js.room_id then
found = true
-- this is a false positive for example when getting
-- invited. need to investigate more
--mprint('error\tJoined room, but already in it.')
break
end
end
if not found then
local data = urllib.urlencode({
access_token= SERVER.access_token,
--limit= w.config_get_plugin('backlog_lines'),
limit = 10,
})
http(('/rooms/%s/initialSync?%s'):format(
urllib.quote(js.room_id), data))
end
elseif command:find'leave' then
-- We store room_id in extra
local room_id = extra
SERVER:delRoom(room_id)
elseif command:find'/keys/claim' then
local count = 0
for user_id, v in pairs(js.one_time_keys or {}) do
for device_id, keys in pairs(v or {}) do
for key_id, key in pairs(keys or {}) do
SERVER.olm.otks[user_id..':'..device_id] = {[device_id]=key}
perr(('olm: Recieved OTK for user %s for device id %s'):format(user_id, device_id))
count = count + 1
SERVER.olm:create_session(user_id, device_id)
end
end
end
elseif command:find'/keys/query' then
for k, v in pairs(js.device_keys or {}) do
SERVER.olm.device_keys[k] = v
-- Claim keys for all only if missing session
for device_id, device_data in pairs(v) do
-- First try to create session from saved data
-- if that doesn't success we will download otk
local device_key = device_data.keys['curve25519:'..device_id]
local sessions = SERVER.olm:get_sessions(device_key)
if #sessions == 0 then
perr('olm: Downloading otk for user '..k..', and device_id: '..device_id)
SERVER.olm:claim(k, device_id)
else
perr('olm: Reusing existing session for user '..k)
end
end
end
elseif command:find'/keys/upload' then
local key_count = 0
local sensible_number_of_keys = 20
for algo, count in pairs(js.one_time_key_counts) do
key_count = count
SERVER.olm.key_count = key_count
end
if DEBUG then
perr('olm: Number of own OTKs uploaded to server: '..key_count)
end
-- TODO make endless loop prevention in case of server error
if key_count < sensible_number_of_keys then
SERVER.olm:upload_keys()
end
elseif command:find'upload' then
-- We store room_id in extra
local room_id = extra
if js.content_uri then
SERVER:Msg(room_id, js.content_uri)
end
-- luacheck: ignore 542
elseif command:find'/typing/' then
-- either it errs or it is empty
elseif command:find'/state/' then
-- TODO errorcode: M_FORBIDDEN
-- either it errs or it is empty
--dbg({state= js})
elseif command:find'/send/' then
-- XXX Errorhandling
-- TODO save event id to use for localecho
local event_id = js.event_id
local room_id = extra
-- When using relay client, WeeChat doesn't get any buffer_switch
-- signals, and thus cannot know when the relay client reads any
-- messages. https://github.com/weechat/weechat/issues/180
-- As a better than nothing approach we send read receipt when
-- user sends a message, since most likely the user has read
-- messages in that room if sending messages to it.
SERVER:SendReadMarker(room_id, event_id)
elseif command:find'createRoom' then
-- We get join events, so we don't have to do anything
elseif command:find'/publicRooms' then
mprint 'Public rooms:'
mprint '\tUsers\tName\tTopic\tAliases'
table.sort(js.chunk, function(a, b)
return a.num_joined_members > b.num_joined_members
end)
for _, r in ipairs(js.chunk) do
local name = ''
if r.name and r.name ~= json.null then
name = r.name:gsub('\n', '')
end
local topic = ''
if r.topic and r.topic ~= json.null then
topic = r.topic:gsub('\n', '')
end
mprint(('%s %s %s %s')
:format(
r.num_joined_members or '',
name or '',
topic or '',
table.concat(r.aliases or {}, ', ')))
end
-- luacheck: ignore 542
elseif command:find'/invite' then
elseif command:find'receipt' then
-- we don't care about receipts for now
elseif command:find'read_markers' then
-- we don't care about read markers for now
elseif command:find'directory/room' then
--- XXX: parse result
mprint 'Created new alias for room'
elseif command:match'presence/.*/status' then
-- Return of SendPresence which we don't have to handle because
-- it will be sent back to us as an event
else
dbg{['error'] = {msg='Unknown command in http cb', command=accesstoken_redact(command),
js=js}}
end
end
if tonumber(rc) == -2 then -- -2 == WEECHAT_HOOK_PROCESS_ERROR
perr(('Call to API errored in command %s, maybe timeout?'):format(
accesstoken_redact(command)))
-- Empty cache in case of errors
if STDOUT[command] then
STDOUT[command] = nil
end
-- Release poll lock in case of errors
SERVER.poll_lock = false
end
return w.WEECHAT_RC_OK
end
function http_cb(data, command, rc, stdout, stderr)
local status, result = pcall(real_http_cb, data, command, rc, stdout, stderr)
if not status then
perr('Error in http_cb: ' .. tostring(result))
perr(debug.traceback())
end
return result
end
function upload_cb(data, command, rc, stdout, stderr)
local success, js = pcall(json.decode, stdout)
if not success then
mprint(('error\t%s when getting uploaded URI: %s'):format(js, stdout))
return w.WEECHAT_RC_OK
end
local uri = js.content_uri
if not uri then
mprint(('error\tNo content_uri after upload. Stdout: %s, stderr: %s'):format(stdout, stderr))
return w.WEECHAT_RC_OK
end
local room_id = data
local body = 'Image'
local msgtype = 'm.image'
SERVER:Msg(room_id, body, msgtype, uri)
return w.WEECHAT_RC_OK
end
Olm = {}
Olm.__index = Olm
Olm.create = function()
local olmdata = {}
setmetatable(olmdata, Olm)
if not olmstatus then
w.print('', SCRIPT_NAME .. ': Unable to load olm encryption library. Not enabling encryption. Please see documentation (README.md) for information on how to enable.')
return
end
local account = olm.Account.new()
olmdata.account = account
olmdata.sessions = {}
olmdata.device_keys = {}
olmdata.otks = {}
-- Try to read account from filesystem, if not generate a new account
local fd = io.open(HOMEDIR..'account.olm', 'rb')
local pickled = ''
if fd then
pickled = fd:read'*all'
fd:close()
end
if pickled == '' then
account:create()
local _, err = account:generate_one_time_keys(5)
perr(err)
else
local _, err = account:unpickle(OLM_KEY, pickled)
perr(err)
end
local identity = json.decode(account:identity_keys())
-- TODO figure out what device id is supposed to be
olmdata.device_id = identity.ed25519:match'%w*' -- problems with nonalfanum
olmdata.device_key = identity.curve25519
w.print('', 'matrix: Encryption loaded. To send encrypted messages in a room, use command /encrypt on with a room as active current buffer')
if DEBUG then
dbg{olm={
'Loaded identity:',
json.decode(account:identity_keys())
}}
end
return olmdata
end
function Olm:save()
-- Save account and every pickled session
local pickle, err = self.account:pickle(OLM_KEY)
perr(err)
local fd = io.open(HOMEDIR..'account.olm', 'wb')
fd:write(pickle)
fd:close()
--for key, pickled in pairs(self.sessions) do
-- local user_id, device_id = key:match('(.*):(.+)')
-- self.write_session_to_file(pickled, user_id, device_id)
--end
end
function Olm:query(user_ids) -- Query keys from other user_id
if DEBUG then
perr('olm: querying user_ids')
tprint(user_ids)
end
local auth = urllib.urlencode{access_token=SERVER.access_token}
local data = {
device_keys = {}
}
for _, uid in pairs(user_ids) do
data.device_keys[uid] = {false}
end
http('/keys/query/?'..auth,
{postfields=json.encode(data)},
'http_cb',
timeout, nil,
v2_api_ns
)
end
function Olm:check_server_keycount()
local data = urllib.urlencode{access_token=SERVER.access_token}
http('/keys/upload/'..self.device_id..'?'..data,
{},
'http_cb', timeout, nil, v2_api_ns
)
end
function Olm:upload_keys()
if DEBUG then
perr('olm: Uploading keys')
end
local id_keys = json.decode(self.account:identity_keys())
local user_id = SERVER.user_id
local one_time_keys = {}
local otks = json.decode(self.account:one_time_keys())
local keyCount = 0
for id, k in pairs(otks.curve25519) do
keyCount = keyCount + 1
end
perr('olm: keycount: '..tostring(keyCount))
if keyCount < 5 then -- try to always have 5 keys
perr('olm: newly generated keys: '..tostring(tonumber(
self.account:generate_one_time_keys(5 - keyCount))))
otks = json.decode(self.account:one_time_keys())
end
for id, key in pairs(otks.curve25519) do
one_time_keys['curve25519:'..id] = key
keyCount = keyCount + 1
end
-- Construct JSON manually so it's ready for signing
local keys_json = '{"algorithms":["' .. OLM_ALGORITHM .. '"]'
.. ',"device_id":"' .. self.device_id .. '"'
.. ',"keys":'
.. '{"ed25519:' .. self.device_id .. '":"'
.. id_keys.ed25519
.. '","curve25519:' .. self.device_id .. '":"'
.. id_keys.curve25519
.. '"}'
.. ',"user_id":"' .. user_id
.. '"}'
local success, key_data = pcall(json.decode, keys_json)
-- TODO save key data to device_keys so we don't have to download
-- our own keys from the servers?
if not success then
perr(('olm: upload_keys: %s when converting to json: %s')
:format(key_data, keys_json))
end
local msg = {
device_keys = key_data,
one_time_keys = one_time_keys
}
msg.device_keys.signatures = {
[user_id] = {
["ed25519:"..self.device_id] = self.account:sign(keys_json)
}
}
local data = urllib.urlencode{
access_token = SERVER.access_token
}
http('/keys/upload/'..self.device_id..'?'..data, {
postfields = json.encode(msg)