2016-02-12 14:23:44 +01:00
-- WeeChat Matrix.org Client
-- vim: expandtab:ts=4:sw=4:sts=4
2017-08-10 17:53:30 +02:00
-- 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_cb 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
2016-02-12 14:23:44 +01:00
--[[
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> "
2017-08-10 17:53:30 +02:00
local SCRIPT_VERSION = " 3 "
2016-02-12 14:23:44 +01:00
local SCRIPT_LICENSE = " MIT "
local SCRIPT_DESC = " Matrix.org chat plugin "
local SCRIPT_COMMAND = SCRIPT_NAME
2017-08-10 17:53:30 +02:00
local WEECHAT_VERSION
2016-02-12 14:23:44 +01:00
local SERVER
local STDOUT = { }
local OUT = { }
local BUFFER
local Room
local MatrixServer
2017-08-10 17:53:30 +02:00
local Olm
2016-02-12 14:23:44 +01:00
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
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
2017-08-10 17:53:30 +02:00
-- luacheck: ignore debug
2016-02-12 14:23:44 +01:00
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 )
2017-08-10 17:53:30 +02:00
if WEECHAT_VERSION >= 0x00040200 then
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
local function accesstoken_redact ( str )
return ( str : gsub ( ' access.*token=[0-9a-zA-Z%%]* ' , ' access_token=[redacted] ' ) )
end
2016-02-12 14:23:44 +01:00
local transaction_id_counter = 0
local function get_next_transaction_id ( )
transaction_id_counter = transaction_id_counter + 1
return transaction_id_counter
end
2017-08-10 17:53:30 +02:00
--[[
-- 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
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
--]]
2016-02-12 14:23:44 +01:00
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 )
2017-08-10 17:53:30 +02:00
-- TODO, support foreground and background?
local ct = { ' white ' , ' black ' , ' blue ' , ' green ' , ' red ' , ' maroon ' , ' purple ' ,
2016-02-12 14:23:44 +01:00
' orange ' , ' yellow ' , ' lightgreen ' , ' teal ' , ' cyan ' , ' lightblue ' ,
' fuchsia ' , ' gray ' , ' lightgray ' }
s = byte_to_tag ( s , ' \02 ' , ' <em> ' , ' </em> ' )
s = byte_to_tag ( s , ' \029 ' , ' <i> ' , ' </i> ' )
s = byte_to_tag ( s , ' \031 ' , ' <u> ' , ' </u> ' )
2017-08-10 17:53:30 +02:00
-- 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 ]
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
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
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
local help_cmds = { args = args }
2016-02-12 14:23:44 +01:00
if not help_cmds then
w.print ( " " , " Command not found: " .. args )
return
end
2017-08-10 17:53:30 +02:00
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
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
return command_connect ( current_buffer )
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
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
2016-02-12 14:23:44 +01:00
else
perr ( " Command not found: " .. args )
end
return w.WEECHAT_RC_OK
end
2017-08-10 17:53:30 +02:00
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
2016-02-12 14:23:44 +01:00
function configuration_changed_cb ( data , option , value )
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
local function http ( url , post , cb , timeout , extra , api_ns )
if not post then
post = { }
end
if not cb then
cb = ' http_cb '
end
if not timeout then
timeout = 60 * 1000
end
if not extra then
extra = nil
end
if not api_ns then
2017-08-10 17:53:30 +02:00
api_ns = " _matrix/client/r0 "
2016-02-12 14:23:44 +01:00
end
-- Add accept encoding by default if it's not already there
if not post.accept_encoding then
post.accept_encoding = ' application/json '
end
2017-08-10 17:53:30 +02:00
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
2016-02-12 14:23:44 +01:00
if DEBUG then
dbg { request = {
2017-08-10 17:53:30 +02:00
url = accesstoken_redact ( url ) ,
2016-02-12 14:23:44 +01:00
post = post , extra = extra }
}
end
w.hook_process_hashtable ( ' url: ' .. url , post , timeout , cb , extra )
end
2017-08-10 17:53:30 +02:00
local function parse_http_statusline ( line )
local httpversion , status_code , reason_phrase = line : match ( " ^HTTP/(1%.[01]) (%d%d%d) (.-) \r ? \n " )
if not httpversion then
return
end
return httpversion , tonumber ( status_code ) , reason_phrase
end
2016-02-12 14:23:44 +01:00
function real_http_cb ( extra , command , rc , stdout , stderr )
if DEBUG then
dbg { reply = {
2017-08-10 17:53:30 +02:00
command = accesstoken_redact ( command ) ,
2016-02-12 14:23:44 +01:00
extra = extra , rc = rc , stdout = stdout , stderr = stderr }
}
end
if stderr and stderr ~= ' ' then
mprint ( ( ' error: %s ' ) : format ( 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.
2017-08-10 17:53:30 +02:00
if WEECHAT_VERSION < 0x01030000 then -- fixed in 1.3
stdout = ( stdout : gsub ( ' ^ \007 * ' , ' ' ) )
end
2016-02-12 14:23:44 +01:00
if stdout ~= ' ' then
if not STDOUT [ command ] then
STDOUT [ command ] = { }
end
table.insert ( STDOUT [ command ] , stdout )
end
if tonumber ( rc ) >= 0 then
2017-08-10 17:53:30 +02:00
stdout = table.concat ( STDOUT [ command ] or { } )
2016-02-12 14:23:44 +01:00
STDOUT [ command ] = nil
2017-08-10 17:53:30 +02:00
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 >= 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 (.*) ' ) )
2016-02-12 14:23:44 +01:00
-- Protected call in case of JSON errors
local success , js = pcall ( json.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
2017-08-10 17:53:30 +02:00
myroom : ParseChunk ( chunk , true , ' presence ' )
2016-02-12 14:23:44 +01:00
end
for _ , chunk in ipairs ( js [ ' messages ' ] [ ' chunk ' ] ) do
2017-08-10 17:53:30 +02:00
myroom : ParseChunk ( chunk , true , ' messages ' )
2016-02-12 14:23:44 +01:00
end
2017-08-10 17:53:30 +02:00
elseif command : find ' /sync ' then
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
if membership ~= ' leave ' or ( membership == ' leave ' and ( not backlog ) ) then
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
-- 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
2016-02-12 14:23:44 +01:00
-- 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
2017-08-10 17:53:30 +02:00
myroom : ParseChunk ( chunk , backlog , ' states ' )
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
myroom : ParseChunk ( chunk , backlog , ' messages ' )
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
myroom : ParseChunk ( chunk , backlog , ' states ' )
2016-02-12 14:23:44 +01:00
end
end
2017-08-10 17:53:30 +02:00
if backlog then
-- All the state should be done. Try to get a good name for the room now.
myroom : SetName ( myroom.identifier )
end
2016-02-12 14:23:44 +01:00
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 ]
2017-08-10 17:53:30 +02:00
myroom : ParseChunk ( chunk , true , ' messages ' )
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
-- luacheck: ignore 542
2016-02-12 14:23:44 +01:00
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
2017-08-10 17:53:30 +02:00
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 : SendReadReceipt ( room_id , event_id )
2016-02-12 14:23:44 +01:00
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: '
2017-08-10 17:53:30 +02:00
mprint ' \t Users \t Name \t Topic \t Aliases '
table.sort ( js.chunk , function ( a , b )
return a.num_joined_members > b.num_joined_members
end )
2016-02-12 14:23:44 +01:00
for _ , r in ipairs ( js.chunk ) do
local name = ' '
2017-08-10 17:53:30 +02:00
if r.name and r.name ~= json.null then
name = r.name : gsub ( ' \n ' , ' ' )
2016-02-12 14:23:44 +01:00
end
local topic = ' '
2017-08-10 17:53:30 +02:00
if r.topic and r.topic ~= json.null then
topic = r.topic : gsub ( ' \n ' , ' ' )
2016-02-12 14:23:44 +01:00
end
mprint ( ( ' %s %s %s %s ' )
: format (
2017-08-10 17:53:30 +02:00
r.num_joined_members or ' ' ,
name or ' ' ,
topic or ' ' ,
table.concat ( r.aliases or { } , ' , ' ) ) )
2016-02-12 14:23:44 +01:00
end
2017-08-10 17:53:30 +02:00
-- luacheck: ignore 542
2016-02-12 14:23:44 +01:00
elseif command : find ' /invite ' then
elseif command : find ' receipt ' then
-- we don't care about receipts for now
elseif command : find ' directory/room ' then
--- XXX: parse result
mprint ' Created new alias for room '
2017-08-10 17:53:30 +02:00
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
2016-02-12 14:23:44 +01:00
else
dbg { [ ' error ' ] = { msg = ' Unknown command in http cb ' , command = 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 (
2017-08-10 17:53:30 +02:00
accesstoken_redact ( command ) ) )
2016-02-12 14:23:44 +01:00
-- 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
2017-08-10 17:53:30 +02:00
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 \t No 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
2016-02-12 14:23:44 +01:00
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 ( )
2017-08-10 17:53:30 +02:00
local _ , err = account : generate_one_time_keys ( 5 )
2016-02-12 14:23:44 +01:00
perr ( err )
else
2017-08-10 17:53:30 +02:00
local _ , err = account : unpickle ( OLM_KEY , pickled )
2016-02-12 14:23:44 +01:00
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 ' ,
5 * 1000 , 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 ' , 5 * 1000 , 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 )
} , ' http_cb ' , 5 * 1000 , nil , v2_api_ns )
self.account : mark_keys_as_published ( )
end
function Olm : claim ( user_id , device_id ) -- Fetch one time keys
if DEBUG then
perr ( ( ' olm: Claiming OTK for user: %s and device: %s ' ) : format ( user_id , device_id ) )
end
-- TODO take a list of ids for batch downloading
local auth = urllib.urlencode { access_token = SERVER.access_token }
local data = {
one_time_keys = {
[ user_id ] = {
[ device_id ] = ' curve25519 '
}
}
}
http ( ' /keys/claim? ' .. auth ,
{ postfields = json.encode ( data ) } ,
' http_cb ' , 30 * 1000 , nil , v2_api_ns
)
end
function Olm : create_session ( user_id , device_id )
perr ( ( ' olm: creating session for user: %s, and device: %s ' ) : format ( user_id , device_id ) )
local device_data = self.device_keys [ user_id ] [ device_id ]
if not device_data then
perr ( ( ' olm: missing device data for user: %s, and device: %s ' ) : format ( user_id , device_id ) )
return
end
local device_key = device_data.keys [ ' curve25519: ' .. device_id ]
if not device_key then
perr ( " olm: Missing key for user: " .. user_id .. " and device: " .. device_id .. " " )
return
end
local sessions = self : get_sessions ( device_key )
2017-08-10 17:53:30 +02:00
if not sessions [ device_key ] then
2016-02-12 14:23:44 +01:00
perr ( ( ' olm: creating NEW session for: %s, and device: %s ' ) : format ( user_id , device_id ) )
local session = olm.Session . new ( )
local otk = self.otks [ user_id .. ' : ' .. device_id ]
if not otk then
perr ( " olm: Missing OTK for user: " .. user_id .. " and device: " .. device_id .. " " )
else
otk = otk [ device_id ]
end
if otk then
session : create_outbound ( self.account , device_key , otk )
local session_id = session : session_id ( )
2017-08-10 17:53:30 +02:00
perr ( ' olm: Session ID: ' .. tostring ( session_id ) )
2016-02-12 14:23:44 +01:00
self : store_session ( device_key , session )
end
session : clear ( )
end
end
function Olm : get_sessions ( device_key )
if DEBUG then
perr ( " olm: get_sessions: device: " .. device_key .. " " )
end
local sessions = self.sessions [ device_key ]
if not sessions then
sessions = self : read_session ( device_key )
end
return sessions
end
function Olm : read_session ( device_key )
local session_filename = HOMEDIR .. device_key .. ' .session.olm '
local fd , err = io.open ( session_filename , ' rb ' )
if fd then
perr ( ( ' olm: reading saved session device: %s ' ) : format ( device_key ) )
local sessions = fd : read ' *all '
2017-08-10 17:53:30 +02:00
sessions = json.decode ( sessions )
2016-02-12 14:23:44 +01:00
self.sessions [ device_key ] = sessions
fd : close ( )
return sessions
2017-08-10 17:53:30 +02:00
else
perr ( ( ' olm: Error: %s, reading saved session device: %s ' ) : format ( err , device_key ) )
2016-02-12 14:23:44 +01:00
end
return { }
end
function Olm : store_session ( device_key , session )
local session_id = session : session_id ( )
if DEBUG then
perr ( " olm: store_session: device: " .. device_key .. " , Session ID: " .. session_id )
end
local sessions = self.sessions [ device_key ] or { }
local pickled = session : pickle ( OLM_KEY )
sessions [ session_id ] = pickled
self.sessions [ device_key ] = sessions
self : write_session_to_file ( sessions , device_key )
end
function Olm : write_session_to_file ( sessions , device_key )
local session_filename = HOMEDIR .. device_key .. ' .session.olm '
local fd , err = io.open ( session_filename , ' wb ' )
if fd then
fd : write ( json.encode ( sessions ) )
fd : close ( )
else
perr ( ' olm: error saving session: ' .. tostring ( err ) )
end
end
MatrixServer = { }
MatrixServer.__index = MatrixServer
MatrixServer.create = function ( )
local server = { }
setmetatable ( server , MatrixServer )
server.nick = nil
server.connecting = false
server.connected = false
server.rooms = { }
-- Store user presences here since they are not local to the rooms
server.presence = { }
server.end_token = ' END '
server.typing_time = os.time ( )
2017-08-10 17:53:30 +02:00
if w.config_get_plugin ( ' presence_filter ' ) ~= ' on ' then
server.typingtimer = w.hook_timer ( 10 * 1000 , 0 , 0 , " cleartyping " , " " )
end
2016-02-12 14:23:44 +01:00
-- Use a lock to prevent multiple simul poll with same end token, which
-- could lead to duplicate messages
server.poll_lock = false
server.olm = Olm.create ( )
2017-08-10 17:53:30 +02:00
if server.olm then -- might not be available
-- Run save so we do not lose state. Create might create new account,
-- new keys, etc.
server.olm : save ( )
end
2016-02-12 14:23:44 +01:00
return server
end
function MatrixServer : UpdatePresence ( c )
2017-08-10 17:53:30 +02:00
local user_id = c.sender or c.content . user_id
self.presence [ user_id ] = c.content . presence
2016-02-12 14:23:44 +01:00
for id , room in pairs ( self.rooms ) do
room : UpdatePresence ( c.sender , c.content . presence )
end
end
function MatrixServer : findRoom ( buffer_ptr )
for id , room in pairs ( self.rooms ) do
if room.buffer == buffer_ptr then
return room
end
end
end
function MatrixServer : connect ( )
if not self.connecting then
local user = weechat_eval ( w.config_get_plugin ( ' user ' ) )
local password = weechat_eval ( w.config_get_plugin ( ' password ' ) )
if user == ' ' or password == ' ' then
w.print ( ' ' , ' Please set your username and password using the settings system and then type /matrix connect ' )
return
end
self.connecting = true
w.print ( ' ' , ' matrix: Connecting to homeserver URL: ' ..
w.config_get_plugin ( ' homeserver_url ' ) )
local post = {
[ " type " ] = " m.login.password " ,
[ " user " ] = user ,
[ " password " ] = password
}
http ( ' /login ' , {
postfields = json.encode ( post )
} , ' http_cb ' , 5 * 1000 ) -- Set a short timeout so user can get more immidiate feedback
end
end
function MatrixServer : initial_sync ( )
2017-08-10 17:53:30 +02:00
BUFFER = w.buffer_new ( SCRIPT_NAME , " " , " " , " closed_matrix_buffer_cb " , " " )
w.buffer_set ( BUFFER , " short_name " , SCRIPT_NAME )
w.buffer_set ( BUFFER , " name " , SCRIPT_NAME )
2016-02-12 14:23:44 +01:00
w.buffer_set ( BUFFER , " localvar_set_type " , " server " )
2017-08-10 17:53:30 +02:00
w.buffer_set ( BUFFER , " localvar_set_server " , SCRIPT_NAME )
2016-02-12 14:23:44 +01:00
w.buffer_set ( BUFFER , " title " , ( " Matrix: %s " ) : format (
w.config_get_plugin ' homeserver_url ' ) )
2017-08-10 17:53:30 +02:00
if w.config_string ( w.config_get ( ' irc.look.server_buffer ' ) ) == ' merge_with_core ' then
w.buffer_merge ( BUFFER , w.buffer_search_main ( ) )
end
2016-02-12 14:23:44 +01:00
w.buffer_set ( BUFFER , " display " , " auto " )
local data = urllib.urlencode ( {
access_token = self.access_token ,
timeout = 1000 * POLL_INTERVAL ,
2017-08-10 17:53:30 +02:00
full_state = ' true ' ,
filter = json.encode ( { -- timeline filter
room = {
timeline = {
limit = tonumber ( w.config_get_plugin ( ' backlog_lines ' ) )
}
} ,
presence = {
not_types = { ' * ' } , -- dont want presence
}
} )
2016-02-12 14:23:44 +01:00
} )
local extra = ' initial '
-- New v2 sync API is slow. Until we can easily ignore archived rooms
-- let's increase the timer for the initial login
local login_timer = 60 * 5 * 1000
2017-08-10 17:53:30 +02:00
http ( ' /sync? ' .. data , nil , ' http_cb ' , login_timer , extra )
2016-02-12 14:23:44 +01:00
end
function MatrixServer : post_initial_sync ( )
-- Timer used in cased of errors to restart the polling cycle
-- During normal operation the polling should re-invoke itself
SERVER.polltimer = w.hook_timer ( POLL_INTERVAL * 1000 , 0 , 0 , " polltimer_cb " , " " )
if olmstatus then
-- timer that checks number of otks available on the server
SERVER.otktimer = w.hook_timer ( 5 * 60 * 1000 , 0 , 0 , " otktimer_cb " , " " )
SERVER.olm : query { SERVER.user_id }
--SERVER.olm.upload_keys()
SERVER.olm : check_server_keycount ( )
end
end
function MatrixServer : getMessages ( room_id , dir , from , limit )
if not dir then dir = ' b ' end
if not from then from = ' END ' end
if not limit then limit = w.config_get_plugin ( ' backlog_lines ' ) end
local data = urllib.urlencode ( {
access_token = self.access_token ,
dir = dir ,
from = from ,
limit = limit ,
} )
http ( ( ' /rooms/%s/messages?%s ' )
: format ( urllib.quote ( room_id ) , data ) , nil , nil , nil , room_id )
end
2017-08-10 17:53:30 +02:00
function MatrixServer : Join ( room )
2016-02-12 14:23:44 +01:00
if not self.connected then
--XXX'''
return
end
mprint ( ' \t Joining room ' .. room )
room = urllib.quote ( room )
http ( ' /join/ ' .. room ,
{ postfields = " access_token= " .. self.access_token } )
end
function MatrixServer : part ( room )
if not self.connected then
--XXX'''
return
end
local id = urllib.quote ( room.identifier )
local data = urllib.urlencode ( {
access_token = self.access_token ,
} )
http ( ( ' /rooms/%s/leave?%s ' ) : format ( id , data ) , { postfields = " {} " } ,
' http_cb ' , 10000 , room.identifier )
end
function MatrixServer : poll ( )
if self.connected == false then
return
end
if self.poll_lock then
return
end
self.poll_lock = true
self.polltime = os.time ( )
2017-08-10 17:53:30 +02:00
local filter = { }
if w.config_get_plugin ( ' presence_filter ' ) == ' on ' then
filter = { -- timeline filter
presence = {
not_types = { ' * ' } , -- dont want presence
} ,
room = {
ephemeral = {
not_types = { ' * ' } , -- dont want read receipt and typing notices
}
}
}
end
2016-02-12 14:23:44 +01:00
local data = urllib.urlencode ( {
access_token = self.access_token ,
timeout = 1000 * POLL_INTERVAL ,
full_state = ' false ' ,
2017-08-10 17:53:30 +02:00
filter = json.encode ( filter ) ,
2016-02-12 14:23:44 +01:00
since = self.end_token
} )
2017-08-10 17:53:30 +02:00
http ( ' /sync? ' .. data , nil , ' http_cb ' , ( POLL_INTERVAL + 10 ) * 1000 )
2016-02-12 14:23:44 +01:00
end
function MatrixServer : addRoom ( room )
-- Just in case, we check for duplicates here
if self.rooms [ room [ ' room_id ' ] ] then
return self.rooms [ room [ ' room_id ' ] ]
end
local myroom = Room.create ( room )
myroom : create_buffer ( )
self.rooms [ room [ ' room_id ' ] ] = myroom
return myroom
end
function MatrixServer : delRoom ( room_id )
for id , room in pairs ( self.rooms ) do
if id == room_id then
mprint ( ' \t Left room ' .. room.name )
room : destroy ( )
self.rooms [ id ] = nil
break
end
end
end
function MatrixServer : SendReadReceipt ( room_id , event_id )
-- TODO: prevent sending multiple identical read receipts
local r_type = ' m.read '
local auth = urllib.urlencode { access_token = self.access_token }
room_id = urllib.quote ( room_id )
event_id = urllib.quote ( event_id )
local url = ' /rooms/ ' .. room_id .. ' /receipt/ ' .. r_type .. ' / ' .. event_id .. ' ? ' .. auth
http ( url ,
{ customrequest = ' POST ' } ,
' http_cb ' ,
2017-08-10 17:53:30 +02:00
5 * 1000
)
2016-02-12 14:23:44 +01:00
end
2017-08-10 17:53:30 +02:00
function MatrixServer : Msg ( room_id , body , msgtype , url )
2016-02-12 14:23:44 +01:00
-- check if there's an outgoing message timer already
self : ClearSendTimer ( )
if not msgtype then
msgtype = ' m.text '
end
if not OUT [ room_id ] then
OUT [ room_id ] = { }
end
-- Add message to outgoing queue of messages for this room
2017-08-10 17:53:30 +02:00
table.insert ( OUT [ room_id ] , { msgtype , body , url } )
2016-02-12 14:23:44 +01:00
self : StartSendTimer ( )
end
function MatrixServer : StartSendTimer ( )
local send_delay = 50 -- Wait this long for paste detection
self.sendtimer = w.hook_timer ( send_delay , 0 , 1 , " send " , " " )
end
function MatrixServer : ClearSendTimer ( )
-- Clear timer if it exists
if self.sendtimer then
w.unhook ( self.sendtimer )
end
self.sendtimer = nil
end
2017-08-10 17:53:30 +02:00
function send ( cbdata , calls )
2016-02-12 14:23:44 +01:00
SERVER : ClearSendTimer ( )
2017-08-10 17:53:30 +02:00
-- Find the room
local room
2016-02-12 14:23:44 +01:00
for id , msgs in pairs ( OUT ) do
-- Clear message
OUT [ id ] = nil
local body = { }
local htmlbody = { }
local msgtype
2017-08-10 17:53:30 +02:00
local url
2016-02-12 14:23:44 +01:00
local ishtml = false
2017-08-10 17:53:30 +02:00
for _ , r in pairs ( SERVER.rooms ) do
if r.identifier == id then
room = r
break
end
end
2016-02-12 14:23:44 +01:00
for _ , msg in pairs ( msgs ) do
-- last msgtype will override any other for simplicity's sake
msgtype = msg [ 1 ]
local html = irc_formatting_to_html ( msg [ 2 ] )
if html ~= msg [ 2 ] then
ishtml = true
end
table.insert ( htmlbody , html )
table.insert ( body , msg [ 2 ] )
2017-08-10 17:53:30 +02:00
if msg [ 3 ] then -- Primarily image upload
url = msg [ 3 ]
end
2016-02-12 14:23:44 +01:00
end
body = table.concat ( body , ' \n ' )
-- Run IRC modifiers (XXX: maybe run out1 also?
body = w.hook_modifier_exec ( ' irc_out1_PRIVMSG ' , ' ' , body )
if w.config_get_plugin ( ' local_echo ' ) == ' on ' or
room.encrypted then
-- Generate local echo
local color = default_color
if msgtype == ' m.text ' then
--- XXX: no localecho for encrypted messages?
local tags = ' notify_none,localecho,no_highlight '
if room.encrypted then
tags = tags .. ' ,no_log '
color = w.color ( w.config_get_plugin (
' encrypted_message_color ' ) )
end
w.print_date_tags ( room.buffer , nil ,
tags , ( " %s \t %s%s " ) : format (
room : formatNick ( SERVER.user_id ) ,
color ,
2017-08-10 17:53:30 +02:00
irc_formatting_to_weechat_color ( body )
2016-02-12 14:23:44 +01:00
)
)
elseif msgtype == ' m.emote ' then
local prefix_c = wcolor ' weechat.color.chat_prefix_action '
local prefix = wconf ' weechat.look.prefix_action '
local tags = ' notify_none,localecho,irc_action,no_highlight '
if room.encrypted then
tags = tags .. ' ,no_log '
color = w.color ( w.config_get_plugin (
' encrypted_message_color ' ) )
end
w.print_date_tags ( room.buffer , nil ,
tags , ( " %s%s \t %s%s%s %s " ) : format (
prefix_c ,
prefix ,
w.color ( ' chat_nick_self ' ) ,
room.users [ SERVER.user_id ] ,
color ,
2017-08-10 17:53:30 +02:00
irc_formatting_to_weechat_color ( body )
2016-02-12 14:23:44 +01:00
)
)
end
end
local data = {
postfields = {
msgtype = msgtype ,
body = body ,
2017-08-10 17:53:30 +02:00
url = url ,
2016-02-12 14:23:44 +01:00
} }
if ishtml then
htmlbody = table.concat ( htmlbody , ' \n ' )
data.postfields . body = strip_irc_formatting ( body )
data.postfields . format = ' org.matrix.custom.html '
data.postfields . formatted_body = htmlbody
end
local api_event_function = ' m.room.message '
if olmstatus and room.encrypted then
api_event_function = ' m.room.encrypted '
local olmd = SERVER.olm
data.postfields . algorithm = OLM_ALGORITHM
data.postfields . sender_key = olmd.device_key
data.postfields . ciphertext = { }
-- Count number of devices we are sending to
local recipient_count = 0
for user_id , _ in pairs ( room.users ) do
for device_id , device_data in pairs ( olmd.device_keys [ user_id ] or { } ) do -- FIXME check for missing keys?
local device_key
-- TODO save this better somehow?
for key_id , key_data in pairs ( device_data.keys ) do
if key_id : match ( ' ^curve25519 ' ) then
device_key = key_data
end
end
local sessions = olmd : get_sessions ( device_key )
-- Use the session with the lowest ID
2017-08-10 17:53:30 +02:00
-- TODO: figure out how to pick session better?
2016-02-12 14:23:44 +01:00
table.sort ( sessions )
2017-08-10 17:53:30 +02:00
local pickled = next ( sessions )
2016-02-12 14:23:44 +01:00
if pickled then
local session = olm.Session . new ( )
session : unpickle ( OLM_KEY , pickled )
local session_id = session : session_id ( )
perr ( ( ' Session ID: %s, user_id: %s, device_id: %s ' ) :
format ( session_id , user_id , device_id ) )
local payload = {
room_id = room.identifier ,
[ ' type ' ] = " m.room.message " ,
fingerprint = " " , -- TODO: Olm:sha256 participants
sender_device = olmd.device_id ,
content = {
msgtype = msgtype ,
2017-08-10 17:53:30 +02:00
body = data.postfields . body or ' ' ,
url = url
2016-02-12 14:23:44 +01:00
}
}
-- encrypt body
local mtype , e_body = session : encrypt ( json.encode ( payload ) )
local ciphertext = {
[ " type " ] = mtype ,
body = e_body
}
data.postfields . ciphertext [ device_key ] = ciphertext
recipient_count = recipient_count + 1
-- Save session
olmd : store_session ( device_key , session )
session : clear ( )
end
end
end
-- remove cleartext from original msg
data.postfields . body = nil
data.postfields . formatted_body = nil
if recipient_count == 0 then
perr ( ' Aborted sending of encrypted message: could not find any valid recipients ' )
return
end
end
data.postfields = json.encode ( data.postfields )
data.customrequest = ' PUT '
http ( ( ' /rooms/%s/send/%s/%s?access_token=%s ' )
: format (
urllib.quote ( id ) ,
api_event_function ,
get_next_transaction_id ( ) ,
urllib.quote ( SERVER.access_token )
) ,
2017-08-10 17:53:30 +02:00
data ,
nil ,
nil ,
id -- send room id to extra
2016-02-12 14:23:44 +01:00
)
end
end
function MatrixServer : emote ( room_id , body )
self : Msg ( room_id , body , ' m.emote ' )
end
function MatrixServer : notice ( room_id , body )
self : Msg ( room_id , body , ' m.notice ' )
end
function MatrixServer : state ( room_id , key , data )
http ( ( ' /rooms/%s/state/%s?access_token=%s ' )
: format ( urllib.quote ( room_id ) ,
urllib.quote ( key ) ,
urllib.quote ( self.access_token ) ) ,
{ customrequest = ' PUT ' ,
postfields = json.encode ( data ) ,
} )
end
function MatrixServer : set_membership ( room_id , userid , data )
http ( ( ' /rooms/%s/state/m.room.member/%s?access_token=%s ' )
: format ( urllib.quote ( room_id ) ,
urllib.quote ( userid ) ,
urllib.quote ( self.access_token ) ) ,
{ customrequest = ' PUT ' ,
postfields = json.encode ( data ) ,
} )
end
2017-08-10 17:53:30 +02:00
function MatrixServer : SendPresence ( p , status_msg )
-- One of: ["online", "offline", "unavailable", "free_for_chat"]
local data = {
presence = p ,
status_msg = status_msg
}
http ( ( ' /presence/%s/status?access_token=%s ' )
: format (
urllib.quote ( self.user_id ) ,
urllib.quote ( self.access_token ) ) ,
{ customrequest = ' PUT ' ,
postfields = json.encode ( data ) ,
} )
end
2016-02-12 14:23:44 +01:00
function MatrixServer : SendTypingNotice ( room_id )
local data = {
typing = true ,
timeout = 4 * 1000
}
http ( ( ' /rooms/%s/typing/%s?access_token=%s ' )
: format ( urllib.quote ( room_id ) ,
urllib.quote ( self.user_id ) ,
urllib.quote ( self.access_token ) ) ,
{ customrequest = ' PUT ' ,
postfields = json.encode ( data ) ,
} )
end
2017-08-10 17:53:30 +02:00
function MatrixServer : Upload ( room_id , filename )
2016-02-12 14:23:44 +01:00
local content_type = ' image/jpeg '
2017-08-10 17:53:30 +02:00
if filename : match ' %.[Pp][nN][gG]$ ' then
2016-02-12 14:23:44 +01:00
content_type = ' image/png '
end
2017-08-10 17:53:30 +02:00
local url = w.config_get_plugin ( ' homeserver_url ' ) ..
( ' _matrix/media/r0/upload?access_token=%s ' )
: format ( urllib.quote ( SERVER.access_token ) )
w.hook_process_hashtable ( ' /usr/bin/curl ' , {
arg1 = ' --data-binary ' , -- no encoding of data
arg2 = ' @ ' .. filename , -- @means curl will load the filename
arg3 = ' -XPOST ' , -- HTTP POST method
arg4 = ' -H ' , -- header
arg5 = ' Content-Type: ' .. content_type ,
arg6 = ' -s ' , -- silent
arg7 = url ,
} , 30 * 1000 , ' upload_cb ' , room_id )
2016-02-12 14:23:44 +01:00
end
function MatrixServer : CreateRoom ( public , alias , invites )
local data = { }
if alias then
data.room_alias_name = alias
end
if public then
data.visibility = ' public '
else
data.visibility = ' private '
end
if invites then
data.invite = invites
end
http ( ( ' /createRoom?access_token=%s ' )
: format ( urllib.quote ( self.access_token ) ) ,
{ customrequest = ' POST ' ,
postfields = json.encode ( data ) ,
} )
end
function MatrixServer : CreateRoomAlias ( room_id , alias )
local data = { room_id = room_id }
alias = urllib.quote ( alias )
http ( ( ' /directory/room/%s?access_token=%s ' )
: format ( alias , urllib.quote ( self.access_token ) ) ,
{ customrequest = ' PUT ' ,
postfields = json.encode ( data ) ,
} )
end
2017-08-10 17:53:30 +02:00
function MatrixServer : ListRooms ( arg )
local apipart = ( ' /publicRooms?access_token=%s ' ) : format ( urllib.quote ( self.access_token ) )
if arg then
local url = ' https:// ' .. arg .. " /_matrix/client/r0 "
http ( url .. apipart )
else
http ( apipart )
end
2016-02-12 14:23:44 +01:00
end
function MatrixServer : Invite ( room_id , user_id )
local data = {
user_id = user_id
}
http ( ( ' /rooms/%s/invite?access_token=%s ' )
: format ( urllib.quote ( room_id ) ,
urllib.quote ( self.access_token ) ) ,
{ customrequest = ' POST ' ,
postfields = json.encode ( data ) ,
} )
end
function MatrixServer : Nick ( displayname )
local data = {
displayname = displayname ,
}
http ( ( ' /profile/%s/displayname?access_token=%s ' )
: format (
urllib.quote ( self.user_id ) ,
urllib.quote ( self.access_token ) ) ,
{ customrequest = ' PUT ' ,
postfields = json.encode ( data ) ,
} )
end
function buffer_input_cb ( b , buffer , data )
for r_id , room in pairs ( SERVER.rooms ) do
if buffer == room.buffer then
2017-08-10 17:53:30 +02:00
data = data : gsub ( ' ^// ' , ' / ' )
2016-02-12 14:23:44 +01:00
SERVER : Msg ( r_id , data )
2017-08-10 17:53:30 +02:00
break
2016-02-12 14:23:44 +01:00
end
end
return w.WEECHAT_RC_OK
end
Room = { }
Room.__index = Room
Room.create = function ( obj )
local room = { }
setmetatable ( room , Room )
room.buffer = nil
room.identifier = obj [ ' room_id ' ]
2017-08-10 17:53:30 +02:00
local _ , server = room.identifier : match ( ' ^(.*):(.+)$ ' )
room.server = server
2016-02-12 14:23:44 +01:00
room.member_count = 0
-- Cache users for presence/nicklist
room.users = { }
-- Table of ids currently typing
room.typing_ids = { }
-- Cache the rooms power levels state
room.power_levels = { users = { } , users_default = 0 }
-- Encryption status of room
room.encrypted = false
room.visibility = ' public '
room.join_rule = nil
room.roomname = nil -- m.room.name
room.aliases = nil -- aliases
room.canonical_alias = nil
-- We might not be a member yet
local state_events = obj.state or { }
for _ , state in ipairs ( state_events ) do
if state [ ' type ' ] == ' m.room.aliases ' then
2017-08-10 17:53:30 +02:00
local name = state.content . aliases [ 1 ]
if name then
room.name , _ = name : match ( ' (.+):(.+) ' )
2016-02-12 14:23:44 +01:00
end
end
end
if not room.name then
room.name = room.identifier
end
if not room.server then
2017-08-10 17:53:30 +02:00
room.server = ' matrix '
2016-02-12 14:23:44 +01:00
end
room.visibility = obj.visibility
if not obj [ ' visibility ' ] then
room.visibility = ' public '
end
return room
end
2017-08-10 17:53:30 +02:00
function Room : SetName ( name )
2016-02-12 14:23:44 +01:00
if not name or name == ' ' or name == json.null then
return
end
-- override hierarchy
2017-08-10 17:53:30 +02:00
if self.roomname and self.roomname ~= ' ' then
name = self.roomname
elseif self.canonical_alias then
name = self.canonical_alias
local short_name , _ = self.canonical_alias : match ( ' ^(.-):(.+)$ ' )
if short_name then
name = short_name
end
elseif self.aliases then
local alias = self.aliases [ 1 ]
if name then
local _
name , _ = alias : match ( ' (.+):(.+) ' )
end
else
-- NO names. Set dynamic name based on members
local new = { }
for id , nick in pairs ( self.users ) do
-- Set the name to the other party
if id ~= SERVER.user_id then
new [ # new + 1 ] = nick
end
end
name = table.concat ( new , ' , ' )
end
if not name or name == ' ' or name == json.null then
return
end
2016-02-12 14:23:44 +01:00
-- Check for dupe
local buffer_name = w.buffer_get_string ( self.buffer , ' name ' )
if buffer_name == name then
return
end
2017-08-10 17:53:30 +02:00
2016-02-12 14:23:44 +01:00
w.buffer_set ( self.buffer , " short_name " , name )
w.buffer_set ( self.buffer , " name " , name )
-- Doesn't work
w.buffer_set ( self.buffer , " plugin " , " matrix " )
w.buffer_set ( self.buffer , " full_name " ,
self.server .. " . " .. name )
w.buffer_set ( self.buffer , " localvar_set_channel " , name )
end
2017-08-10 17:53:30 +02:00
function Room : Topic ( topic )
2016-02-12 14:23:44 +01:00
SERVER : state ( self.identifier , ' m.room.topic ' , { topic = topic } )
end
2017-08-10 17:53:30 +02:00
function Room : Name ( name )
SERVER : state ( self.identifier , ' m.room.name ' , { name = name } )
end
2016-02-12 14:23:44 +01:00
function Room : public ( )
SERVER : state ( self.identifier , ' m.room.join_rules ' , { join_rule = ' public ' } )
end
2017-08-10 17:53:30 +02:00
function Room : Upload ( filename )
SERVER : Upload ( self.identifier , filename )
2016-02-12 14:23:44 +01:00
end
function Room : Msg ( msg )
SERVER : Msg ( self.identifier , msg )
end
function Room : emote ( msg )
SERVER : emote ( self.identifier , msg )
end
function Room : Notice ( msg )
SERVER : notice ( self.identifier , msg )
end
function Room : SendTypingNotice ( )
SERVER : SendTypingNotice ( self.identifier )
end
function Room : create_buffer ( )
--local buffer = w.buffer_search("", ("%s.%s"):format(self.server, self.name))
self.buffer = w.buffer_new ( ( " %s.%s " )
: format ( self.server , self.name ) , " buffer_input_cb " ,
self.name , " closed_matrix_room_cb " , " " )
-- Needs to correspond with return values from Room:GetNickGroup()
-- We will use 5 nick groups:
-- 1: Ops
-- 2: Half-ops
-- 3: Voice
-- 4: People with presence
-- 5: People without presence
self.nicklist_groups = {
-- Emulate OPs
w.nicklist_add_group ( self.buffer ,
' ' , " 000|o " , " weechat.color.nicklist_group " , 1 ) ,
w.nicklist_add_group ( self.buffer ,
' ' , " 001|h " , " weechat.color.nicklist_group " , 1 ) ,
-- Emulate half-op
w.nicklist_add_group ( self.buffer ,
' ' , " 002|v " , " weechat.color.nicklist_group " , 1 ) ,
-- Defined in weechat's irc-nick.h
w.nicklist_add_group ( self.buffer ,
' ' , " 998|... " , " weechat.color.nicklist_group " , 1 ) ,
w.nicklist_add_group ( self.buffer ,
' ' , " 999|... " , " weechat.color.nicklist_group " , 1 ) ,
}
w.buffer_set ( self.buffer , " nicklist " , " 1 " )
-- Set to 1 for easier debugging of nick groups
w.buffer_set ( self.buffer , " nicklist_display_groups " , " 0 " )
w.buffer_set ( self.buffer , " localvar_set_server " , self.server )
w.buffer_set ( self.buffer , " localvar_set_roomid " , self.identifier )
2017-08-10 17:53:30 +02:00
self : SetName ( self.name )
2016-02-12 14:23:44 +01:00
if self.membership == ' invite ' then
self : addNick ( self.inviter )
if w.config_get_plugin ( ' autojoin_on_invite ' ) ~= ' on ' then
w.print_date_tags (
self.buffer ,
nil ,
' notify_message ' ,
( ' You have been invited to join room %s by %s. Type /join in this buffer to join. ' )
: format (
self.name ,
self.inviter ,
self.identifier )
)
end
end
end
function Room : Freeze ( )
-- Function that saves all the lines in a buffer in a cache to be thawed
-- later. Used to redraw buffer when user requests more lines. Since
-- WeeChat can only render lines in order this is the workaround
local freezer = { }
local lines = w.hdata_pointer ( w.hdata_get ( ' buffer ' ) , self.buffer , ' own_lines ' )
if lines == ' ' then return end
-- Start at top
local line = w.hdata_pointer ( w.hdata_get ( ' lines ' ) , lines , ' first_line ' )
if line == ' ' then return end
local hdata_line = w.hdata_get ( ' line ' )
local hdata_line_data = w.hdata_get ( ' line_data ' )
while # line > 0 do
local data = w.hdata_pointer ( hdata_line , line , ' data ' )
local tags = { }
local tag_count = w.hdata_integer ( hdata_line_data , data , " tags_count " )
if tag_count > 0 then
for i = 0 , tag_count - 1 do
local tag = w.hdata_string ( hdata_line_data , data , i .. " |tags_array " )
-- Skip notify tags since this is backlog
if not tag : match ' ^notify ' then
tags [ # tags + 1 ] = tag
end
end
end
tags [ # tags + 1 ] = ' no_log '
freezer [ # freezer + 1 ] = {
time = w.hdata_integer ( hdata_line_data , data , ' time ' ) ,
tags = tags ,
prefix = w.hdata_string ( hdata_line_data , data , ' prefix ' ) ,
message = w.hdata_string ( hdata_line_data , data , ' message ' ) ,
}
-- Move forward since we start at top
line = w.hdata_move ( hdata_line , line , 1 )
end
self.freezer = freezer
end
function Room : Thaw ( )
for _ , l in ipairs ( self.freezer ) do
w.print_date_tags (
self.buffer ,
l.time ,
table.concat ( l.tags , ' , ' ) ,
l.prefix .. ' \t ' .. l.message
)
end
-- Clear old data
self.freezer = nil
end
function Room : Clear ( )
w.buffer_clear ( self.buffer )
end
function Room : destroy ( )
w.buffer_close ( self.buffer )
end
function Room : _nickListChanged ( )
-- Check the user count, if it's 2 or less then we decide this buffer
-- is a "private" one like IRC's query type
2017-08-10 17:53:30 +02:00
if self.member_count == 3 then
2016-02-12 14:23:44 +01:00
w.buffer_set ( self.buffer , " localvar_set_type " , ' channel ' )
self.buffer_type = ' channel '
elseif self.member_count == 2 then
-- At the point where we reach two nicks, set the buffer name to be
-- the display name of the other guy that is not our self since it's
-- in effect a query, but the matrix protocol doesn't have such
-- a concept
w.buffer_set ( self.buffer , " localvar_set_type " , ' private ' )
self.buffer_type = ' query '
elseif self.member_count == 1 then
if not self.roomname and not self.aliases then
-- Set the name to ourselves
2017-08-10 17:53:30 +02:00
self : SetName ( self.users [ SERVER.user_id ] )
2016-02-12 14:23:44 +01:00
end
end
end
function Room : addNick ( user_id , displayname )
local newnick = false
2017-08-10 17:53:30 +02:00
-- Sanitize displaynames a bit
if not displayname
or displayname == json.null
or displayname == ' '
or displayname : match ' ^%s+$ ' then
2016-02-12 14:23:44 +01:00
displayname = user_id : match ( ' @(.*):.+ ' )
end
if not self.users [ user_id ] then
self.member_count = self.member_count + 1
newnick = true
end
if self.users [ user_id ] ~= displayname then
self.users [ user_id ] = displayname
end
local nick_c = self : GetPresenceNickColor ( user_id , SERVER.presence [ user_id ] )
-- Check if this is ourselves
if user_id == SERVER.user_id then
w.buffer_set ( self.buffer , " highlight_words " , displayname )
w.buffer_set ( self.buffer , " localvar_set_nick " , displayname )
end
local ngroup , nprefix , nprefix_color = self : GetNickGroup ( user_id )
-- Check if nick already exists
--local nick_ptr = w.nicklist_search_nick(self.buffer, '', displayname)
--if nick_ptr == '' then
local nick_ptr = w.nicklist_add_nick ( self.buffer ,
self.nicklist_groups [ ngroup ] ,
displayname ,
nick_c , nprefix , nprefix_color , 1 )
--else
-- -- TODO CHANGE nickname here
--end
if nick_ptr == ' ' then
-- Duplicate nick names :(
-- We just add the full id to the nicklist so atleast it will show
-- but we should probably assign something new and track the state
-- so we can print msgs with non-conflicting nicks too
w.nicklist_add_nick ( self.buffer ,
self.nicklist_groups [ ngroup ] ,
user_id ,
nick_c , nprefix , nprefix_color , 1 )
-- Since we can't allow duplicate displaynames, we just use the
-- user_id straight up. Maybe we could invent some clever
-- scheme here, like user(homeserver), user (2) or something
self.users [ user_id ] = user_id
end
if newnick then -- run this after nick been added so it can be used
self : _nickListChanged ( )
end
return displayname
end
function Room : GetNickGroup ( user_id )
-- TODO, cache
local ngroup = 5
local nprefix = ' '
local nprefix_color = ' '
if self : GetPowerLevel ( user_id ) >= 100 then
ngroup = 1
nprefix = ' & '
nprefix_color = ' lightgreen '
if user_id == self.creator then
nprefix = ' ~ '
nprefix_color = ' lightred '
end
elseif self : GetPowerLevel ( user_id ) >= 50 then
ngroup = 2
nprefix = ' @ '
nprefix_color = ' lightgreen '
elseif self : GetPowerLevel ( user_id ) > 0 then
ngroup = 3
nprefix = ' + '
nprefix_color = ' yellow '
elseif SERVER.presence [ user_id ] then
-- User has a presence, put him in group3
ngroup = 4
end
return ngroup , nprefix , nprefix_color
end
function Room : GetPowerLevel ( user_id )
2017-08-10 17:53:30 +02:00
return tonumber ( self.power_levels . users [ user_id ] or self.power_levels . users_default or 0 )
2016-02-12 14:23:44 +01:00
end
function Room : ClearTyping ( )
for user_id , nick in pairs ( self.users ) do
local _ , nprefix , nprefix_color = self : GetNickGroup ( user_id )
self : UpdateNick ( user_id , ' prefix ' , nprefix )
self : UpdateNick ( user_id , ' prefix_color ' , nprefix_color )
end
end
function Room : GetPresenceNickColor ( user_id , presence )
local nick = self.users [ user_id ]
local nick_c
if user_id == SERVER.user_id then
-- Always use correct color for self
nick_c = ' weechat.color.chat_nick_self '
elseif presence == ' online ' then
nick_c = w.info_get ( ' irc_nick_color_name ' , nick )
elseif presence == ' unavailable ' then
nick_c = ' weechat.color.nicklist_away '
elseif presence == ' offline ' then
nick_c = ' red '
elseif presence == nil then
nick_c = ' bar_fg '
else
dbg { err = ' unknown presence type ' , presence = presence }
end
return nick_c
end
function Room : UpdatePresence ( user_id , presence )
if presence == ' typing ' then
self : UpdateNick ( user_id , ' prefix ' , ' ! ' )
self : UpdateNick ( user_id , ' prefix_color ' , ' magenta ' )
return
end
2017-08-10 17:53:30 +02:00
local nick_c = self : GetPresenceNickColor ( user_id , presence )
2016-02-12 14:23:44 +01:00
self : UpdateNick ( user_id , ' color ' , nick_c )
end
function Room : UpdateNick ( user_id , key , val )
local nick = self.users [ user_id ]
if not nick then return end
local nick_ptr = w.nicklist_search_nick ( self.buffer , ' ' , nick )
if nick_ptr ~= ' ' and key and val then
-- Check if we need to move the nick into another group
local group_ptr = w.nicklist_nick_get_pointer ( self.buffer , nick_ptr ,
' group ' )
local ngroup , nprefix , nprefix_color = self : GetNickGroup ( user_id )
if group_ptr ~= self.nicklist_groups [ ngroup ] then
local nick_c = w.nicklist_nick_get_string ( self.buffer , nick_ptr ,
' color ' )
-- No WeeChat API for changing a nick's group so we will have to
-- delete the nick from the old nicklist and add it to the correct
-- nicklist group
2017-08-10 17:53:30 +02:00
w.nicklist_remove_nick ( self.buffer , nick_ptr )
2016-02-12 14:23:44 +01:00
-- TODO please check if this call fails, if it does it means the
-- WeeChat version is old and has a bug so it can't remove nicks
-- and so it needs some workaround
nick_ptr = w.nicklist_add_nick ( self.buffer ,
self.nicklist_groups [ ngroup ] ,
nick ,
nick_c , nprefix , nprefix_color , 1 )
end
-- Check if we are clearing a typing notice, and don't issue updates
-- if we are, because it spams the API so much, including potential
-- relay clients
if key == ' prefix ' and val == ' ' then
-- TODO check existing values like + and @ too
local prefix = w.nicklist_nick_get_string ( self.buffer , nick_ptr ,
key )
if prefix == ' ! ' then
w.nicklist_nick_set ( self.buffer , nick_ptr , key , val )
end
elseif key == ' prefix_color ' then
local prefix_color = w.nicklist_nick_get_string ( self.buffer ,
nick_ptr , key )
if prefix_color ~= val then
w.nicklist_nick_set ( self.buffer , nick_ptr , key , val )
end
else
-- Check if we are actually updating something, so there's less
-- updates issued (I think WeeChat sends all changes as nicklist
-- diffs to both UI code and to relay clients
local existing = w.nicklist_nick_get_string ( self.buffer , nick_ptr , key )
if val ~= existing then
w.nicklist_nick_set ( self.buffer , nick_ptr , key , val )
end
end
end
end
function Room : delNick ( id )
if self.users [ id ] then
local nick = self.users [ id ]
local nick_ptr = w.nicklist_search_nick ( self.buffer , ' ' , nick )
if nick_ptr ~= ' ' then
w.nicklist_remove_nick ( self.buffer , nick_ptr )
end
self.users [ id ] = nil
self.member_count = self.member_count - 1
self : _nickListChanged ( )
return true
end
end
function Room : UpdateLine ( id , message )
local lines = w.hdata_pointer ( w.hdata_get ( ' buffer ' ) , self.buffer , ' own_lines ' )
if lines == ' ' then return end
local line = w.hdata_pointer ( w.hdata_get ( ' lines ' ) , lines , ' last_line ' )
if line == ' ' then return end
local hdata_line = w.hdata_get ( ' line ' )
local hdata_line_data = w.hdata_get ( ' line_data ' )
while # line > 0 do
local needsupdate = false
local data = w.hdata_pointer ( hdata_line , line , ' data ' )
local tags = { }
local tag_count = w.hdata_integer ( hdata_line_data , data , " tags_count " )
if tag_count > 0 then
for i = 0 , tag_count - 1 do
local tag = w.hdata_string ( hdata_line_data , data , i .. " |tags_array " )
tags [ # tags + 1 ] = tag
if tag : match ( id ) then
needsupdate = true
end
end
if needsupdate then
w.hdata_update ( hdata_line_data , data , {
2017-08-10 17:53:30 +02:00
prefix = nil ,
2016-02-12 14:23:44 +01:00
message = message ,
tags_array = table.concat ( tags , ' , ' ) ,
} )
return true
end
end
line = w.hdata_move ( hdata_line , line , - 1 )
end
return false
end
function Room : formatNick ( user_id )
-- Turns a nick name into a weechat-styled nickname. This means giving
-- it colors, and proper prefix and suffix
local nick = self.users [ user_id ]
if not nick then
return user_id
end
2017-08-10 17:53:30 +02:00
-- Remove nasty white space
nick = nick : gsub ( ' [ \n \t ] ' , ' ' )
2016-02-12 14:23:44 +01:00
local color
if user_id == SERVER.user_id then
color = w.color ( ' chat_nick_self ' )
else
color = w.info_get ( ' irc_nick_color ' , nick )
end
local _ , nprefix , nprefix_c = self : GetNickGroup ( user_id )
local prefix = wconf ( ' weechat.look.nick_prefix ' )
local prefix_c = wcolor ( ' weechat.color.chat_nick_prefix ' )
local suffix = wconf ( ' weechat.look.nick_suffix ' )
local suffix_c = wcolor ( ' weechat.color.chat_nick_suffix ' )
local nick_f = prefix_c
.. prefix
.. wcolor ( nprefix_c )
.. nprefix
.. color
.. nick
.. suffix_c
.. suffix
return nick_f
end
function Room : decryptChunk ( chunk )
-- vector client doesn't provide this
chunk.content . msgtype = ' m.text '
if not olmstatus then
chunk.content . body = ' encrypted message, unable to decrypt '
return chunk
end
chunk.content . body = ' encrypted message, unable to decrypt '
local device_key = chunk.content . sender_key
-- Find our id
local ciphertexts = chunk.content . ciphertext
local ciphertext
if not ciphertexts then
chunk.content . body = ' Recieved an encrypted message, but could not find ciphertext array '
else
ciphertext = ciphertexts [ SERVER.olm . device_key ]
end
if not ciphertext then
chunk.content . body = ' Recieved an encrypted message, but could not find cipher for ourselves from the sender. '
return chunk
end
local session
local decrypted
local err
local found_session = false
local sessions = SERVER.olm : get_sessions ( device_key )
for id , pickle in pairs ( sessions ) do
-- Check if we already successfully decrypted with a sesssion, if that
-- is the case we break the loop
if decrypted then
break
end
session = olm.Session . new ( )
session : unpickle ( OLM_KEY , pickle )
local matches_inbound = session : matches_inbound ( ciphertext.body )
---if ciphertext.type == 0 and matches_inbound then
if matches_inbound then
found_session = true
end
2017-08-10 17:53:30 +02:00
local cleartext
cleartext , err = session : decrypt ( ciphertext.type , ciphertext.body )
2016-02-12 14:23:44 +01:00
if not err then
if DEBUG then
perr ( ( ' olm: Able to decrypt with an existing session %s ' ) : format ( session : session_id ( ) ) )
end
decrypted = cleartext
SERVER.olm : store_session ( device_key , session )
else
chunk.content . body = " Decryption error: " .. err
if DEBUG then
perr ( ( ' olm: Unable to decrypt with an existing session: %s. Session-ID: %s ' ) : format ( err , session : session_id ( ) ) )
end
end
session : clear ( )
end
if ciphertext.type == 0 and not found_session and not decrypted then
session = olm.Session . new ( )
2017-08-10 17:53:30 +02:00
local _
_ , err = session : create_inbound_from (
2016-02-12 14:23:44 +01:00
SERVER.olm . account , device_key , ciphertext.body )
if err then
session : clear ( )
chunk.content . body = " Decryption error: create inbound " .. err
return chunk
end
decrypted , err = session : decrypt ( ciphertext.type , ciphertext.body )
if err then
session : clear ( )
chunk.content . body = " Decryption error: " .. err
return chunk
end
-- TODO SERVER.olm.account:remove_one_time_keys(session)
local session_id = session : session_id ( )
perr ( ( ' Session ID: %s, user_id: %s, device_id: %s ' ) :
format ( session_id , SERVER.user_id , SERVER.olm . device_id ) )
SERVER.olm : store_session ( device_key , session )
session : clear ( )
if err then
chunk.content . body = " Decryption error: " .. err
return chunk
end
end
if decrypted then
local success , payload = pcall ( json.decode , decrypted )
if not success then
chunk.content . body = " Payload error: " .. payload
return chunk
end
-- TODO use the room id from payload for security
chunk.content . msgtype = payload.content . msgtype
-- Style the message so user can tell if it's
-- an encrypted message or not
local color = w.color ( w.config_get_plugin (
' encrypted_message_color ' ) )
chunk.content . body = color .. payload.content . body
end
return chunk
end
-- Parses a chunk of json meant for a room
2017-08-10 17:53:30 +02:00
function Room : ParseChunk ( chunk , backlog , chunktype )
2016-02-12 14:23:44 +01:00
local taglist = { }
local tag = function ( tag )
-- Helper function to add tags
if type ( tag ) == ' table ' then
for _ , t in ipairs ( tag ) do
taglist [ t ] = true
end
else
taglist [ tag ] = true
end
end
local tags = function ( )
-- Helper for returning taglist for this message
local out = { }
for k , v in pairs ( taglist ) do
table.insert ( out , k )
end
return table.concat ( out , ' , ' )
end
if not backlog then
backlog = false
end
if backlog then
tag { ' no_highlight ' , ' notify_none ' , ' no_log ' }
end
local is_self = false
local was_decrypted = false
-- Sender of chunk, used to be chunk.user_id, v2 uses chunk.sender
local sender = chunk.sender or chunk.user_id
-- Check if own message
if sender == SERVER.user_id then
is_self = true
tag { ' no_highlight ' , ' notify_none ' }
end
-- Add Event ID to each line so can use it later to match on for things
-- like redactions and localecho, etc
tag { chunk.event_id }
-- Some messages are missing ts
local origin_server_ts = chunk [ ' origin_server_ts ' ] or 0
local time_int = origin_server_ts / 1000
if chunk [ ' type ' ] == ' m.room.message ' or chunk [ ' type ' ] == ' m.room.encrypted ' then
if chunk [ ' type ' ] == ' m.room.encrypted ' then
tag { ' no_log ' } -- Don't log encrypted message
chunk = self : decryptChunk ( chunk )
was_decrypted = true
end
if not backlog and not is_self then
tag ' notify_message '
if self.buffer_type == ' query ' then
tag ' notify_private '
end
end
local color = default_color
local content = chunk [ ' content ' ]
2017-08-10 17:53:30 +02:00
local body = content [ ' body ' ]
2016-02-12 14:23:44 +01:00
if not content [ ' msgtype ' ] then
-- We don't support redactions
return
end
-- If it has transaction id, it is from this client.
local is_from_this_client = false
if chunk.unsigned and chunk.unsigned . transaction_id then
is_from_this_client = true
end
2017-08-10 17:53:30 +02:00
-- luacheck: ignore 542
2016-02-12 14:23:44 +01:00
if content [ ' msgtype ' ] == ' m.text ' then
-- TODO
-- Parse HTML here:
-- content.format = 'org.matrix.custom.html'
-- fontent.formatted_body...
elseif content [ ' msgtype ' ] == ' m.image ' then
2017-08-10 17:53:30 +02:00
local url = content [ ' url ' ]
if type ( url ) ~= ' string ' then
url = ' '
end
url = url : gsub ( ' mxc:// ' ,
w.config_get_plugin ( ' homeserver_url ' )
.. ' _matrix/media/v1/download/ ' )
-- Synapse homeserver supports arbitrary file endings, so we put
-- filename at the end to make it nicer for URL "sniffers" to
-- realise it's a image URL
body = url .. ' / ' .. content.body
elseif content.msgtype == ' m.file ' or content.msgtype == ' m.video ' or
content.msgtype == ' m.audio ' then
local url = content [ ' url ' ] or ' '
url = url : gsub ( ' mxc:// ' ,
2016-02-12 14:23:44 +01:00
w.config_get_plugin ( ' homeserver_url ' )
.. ' _matrix/media/v1/download/ ' )
2017-08-10 17:53:30 +02:00
body = ' File upload: ' ..
tostring ( content [ ' body ' ] )
.. ' ' .. url
2016-02-12 14:23:44 +01:00
elseif content [ ' msgtype ' ] == ' m.notice ' then
color = wcolor ( ' irc.color.notice ' )
body = content [ ' body ' ]
elseif content [ ' msgtype ' ] == ' m.emote ' then
local nick_c
local nick = self.users [ sender ] or sender
if is_self then
nick_c = w.color ( ' chat_nick_self ' )
else
nick_c = w.info_get ( ' irc_nick_color ' , nick )
end
tag " irc_action "
local prefix_c = wcolor ' weechat.color.chat_prefix_action '
local prefix = wconf ' weechat.look.prefix_action '
body = ( " %s%s %s%s " ) : format (
nick_c , nick , color , content [ ' body ' ]
)
2017-08-10 17:53:30 +02:00
prefix = prefix_c .. prefix
2016-02-12 14:23:44 +01:00
local data = ( " %s \t %s " ) : format ( prefix , body )
if not backlog and is_self and is_from_this_client and
( w.config_get_plugin ( ' local_echo ' ) == ' on '
or was_decrypted -- local echo for encryption
)
then
-- We have already locally echoed this line
return
else
return w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
end
else
-- Unknown content type, but if it contains an URL we will print
-- URL and body
local url = content [ ' url ' ]
if url ~= nil then
url = url : gsub ( ' mxc:// ' ,
w.config_get_plugin ( ' homeserver_url ' )
.. ' _matrix/media/v1/download/ ' )
body = content [ ' body ' ] .. ' ' .. url
end
dbg {
warning = ' Warning: unknown/unhandled content type ' ,
event = content
}
end
if not backlog and is_self and is_from_this_client
-- TODO better check, to work for multiple weechat clients
and (
w.config_get_plugin ( ' local_echo ' ) == ' on '
or was_decrypted -- local echo for encrypted messages
2017-08-10 17:53:30 +02:00
)
and ( -- we don't generate local echo for files and images
content.msgtype == ' m.text '
)
then
2016-02-12 14:23:44 +01:00
-- We have already locally echoed this line
return
end
local data = ( " %s \t %s%s " ) : format (
self : formatNick ( sender ) ,
color ,
body )
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
elseif chunk [ ' type ' ] == ' m.room.topic ' then
local title = chunk [ ' content ' ] [ ' topic ' ]
if not title then
title = ' '
end
w.buffer_set ( self.buffer , " title " , title )
local color = wcolor ( " irc.color.topic_new " )
local nick = self.users [ sender ] or sender
local data = ( ' -- \t %s%s has changed the topic to "%s%s%s" ' ) : format (
nick ,
default_color ,
color ,
title ,
default_color
)
w.print_date_tags ( self.buffer , chunk.origin_server_ts , tags ( ) ,
data )
elseif chunk [ ' type ' ] == ' m.room.name ' then
local name = chunk [ ' content ' ] [ ' name ' ]
if name ~= ' ' or name ~= json.null then
self.roomname = name
2017-08-10 17:53:30 +02:00
self : SetName ( name )
2016-02-12 14:23:44 +01:00
end
elseif chunk [ ' type ' ] == ' m.room.member ' then
if chunk [ ' content ' ] [ ' membership ' ] == ' join ' then
tag " irc_join "
--- FIXME shouldn't be neccessary adding all the time
2017-08-10 17:53:30 +02:00
local name = self.users [ sender ] or self : addNick ( sender , chunk.content . displayname )
2016-02-12 14:23:44 +01:00
if not name or name == json.null or name == ' ' then
name = sender
end
-- Check if the chunk has prev_content or not
-- if there is prev_content there wasn't a join but a nick change
-- or duplicate join
2017-08-10 17:53:30 +02:00
local prev_content = chunk.unsigned and chunk.unsigned . prev_content
2016-02-12 14:23:44 +01:00
if prev_content
and prev_content.membership == ' join '
and chunktype == ' messages ' then
local oldnick = prev_content.displayname
if not oldnick or oldnick == json.null then
oldnick = sender
else
if oldnick == name then
-- Maybe they changed their avatar or something else
-- that we don't care about (or multiple joins)
return
end
2017-08-10 17:53:30 +02:00
if not backlog then
self : delNick ( sender )
self : addNick ( sender , chunk.content . displayname )
end
2016-02-12 14:23:44 +01:00
end
local pcolor = wcolor ' weechat.color.chat_prefix_network '
tag ' irc_nick '
local data = ( ' %s-- \t %s%s%s is now known as %s%s ' ) : format (
pcolor ,
w.info_get ( ' irc_nick_color ' , oldnick ) ,
oldnick ,
default_color ,
w.info_get ( ' irc_nick_color ' , name ) ,
name )
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
elseif chunktype == ' messages ' then
tag " irc_smart_filter "
local data = ( ' %s%s \t %s%s%s (%s%s%s) joined the room. ' ) : format (
wcolor ( ' weechat.color.chat_prefix_join ' ) ,
wconf ( ' weechat.look.prefix_join ' ) ,
w.info_get ( ' irc_nick_color ' , name ) ,
name ,
wcolor ( ' irc.color.message_join ' ) ,
wcolor ' weechat.color.chat_host ' ,
sender ,
wcolor ( ' irc.color.message_join ' )
)
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
-- if this is an encrypted room, also download key
if olmstatus and self.encrypted then
SERVER.olm : query { sender }
end
end
elseif chunk [ ' content ' ] [ ' membership ' ] == ' leave ' then
if chunktype == ' messages ' then
2017-08-10 17:53:30 +02:00
local nick = self.users [ chunk.state_key ] or sender
2016-02-12 14:23:44 +01:00
local prev = chunk.unsigned . prev_content
if ( prev and
prev.displayname and
prev.displayname ~= json.null ) then
nick = prev.displayname
end
2017-08-10 17:53:30 +02:00
if sender ~= chunk.state_key then -- Kick
tag { " irc_quit " , " irc_kick " , " irc_smart_filter " }
local reason = chunk.content . reason or ' '
local sender_nick = self.users [ chunk.sender ]
local data = ( ' %s%s \t %s%s%s has kicked %s%s%s (%s). ' ) : format (
wcolor ( ' weechat.color.chat_prefix_quit ' ) ,
wconf ( ' weechat.look.prefix_quit ' ) ,
w.info_get ( ' irc_nick_color ' , sender_nick ) ,
sender_nick ,
wcolor ( ' irc.color.message_quit ' ) ,
w.info_get ( ' irc_nick_color ' , nick ) ,
nick ,
default_color ,
reason
)
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
else
tag { " irc_quit " , " irc_smart_filter " }
local data = ( ' %s%s \t %s%s%s left the room. ' ) : format (
wcolor ( ' weechat.color.chat_prefix_quit ' ) ,
wconf ( ' weechat.look.prefix_quit ' ) ,
w.info_get ( ' irc_nick_color ' , nick ) ,
nick ,
wcolor ( ' irc.color.message_quit ' )
)
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
end
2016-02-12 14:23:44 +01:00
end
2017-08-10 17:53:30 +02:00
self : delNick ( chunk.state_key )
2016-02-12 14:23:44 +01:00
elseif chunk [ ' content ' ] [ ' membership ' ] == ' invite ' then
-- Check if we were the one being invited
2017-08-10 17:53:30 +02:00
if chunk.state_key == SERVER.user_id and (
( not backlog and chunktype == ' messages ' ) or
chunktype == ' states ' ) then
--self:addNick(sender)
2016-02-12 14:23:44 +01:00
if w.config_get_plugin ( ' autojoin_on_invite ' ) == ' on ' then
2017-08-10 17:53:30 +02:00
SERVER : Join ( self.identifier )
mprint ( ( ' %s invited you ' ) : format ( sender ) )
2016-02-12 14:23:44 +01:00
else
mprint ( ( ' You have been invited to join room %s by %s. Type /join %s to join. ' )
: format (
self.name ,
sender ,
self.identifier ) )
end
end
if chunktype == ' messages ' then
tag " irc_invite "
local prefix_c = wcolor ' weechat.color.chat_prefix_action '
local prefix = wconf ' weechat.look.prefix_action '
local data = ( " %s%s \t %s invited %s to join " ) : format (
prefix_c ,
prefix ,
self.users [ sender ] or sender ,
self.users [ chunk.state_key ] or chunk.state_key
)
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
end
elseif chunk [ ' content ' ] [ ' membership ' ] == ' ban ' then
if chunktype == ' messages ' then
tag " irc_ban "
local prefix_c = wcolor ' weechat.color.chat_prefix_action '
local prefix = wconf ' weechat.look.prefix_action '
local data = ( " %s%s \t %s banned %s " ) : format (
prefix_c ,
prefix ,
self.users [ sender ] or sender ,
self.users [ chunk.state_key ] or chunk.state_key
)
w.print_date_tags ( self.buffer , time_int , tags ( ) , data )
end
else
2017-08-10 17:53:30 +02:00
dbg { err = ' unknown membership type in ParseChunk ' , chunk = chunk }
end
-- if it's backlog this is done at the end from the caller place
if not backlog then
-- Run SetName on each member change in case we need to update room name
self : SetName ( self.identifier )
2016-02-12 14:23:44 +01:00
end
elseif chunk [ ' type ' ] == ' m.room.create ' then
self.creator = chunk.content . creator
elseif chunk [ ' type ' ] == ' m.room.power_levels ' then
2017-08-10 17:53:30 +02:00
if chunk.content . users then
self.power_levels = chunk.content
for user_id , lvl in pairs ( self.power_levels . users ) do
-- TODO
-- calculate changes here and generate message lines
-- describing the change
end
for user_id , lvl in pairs ( self.power_levels . users ) do
local _ , nprefix , nprefix_color = self : GetNickGroup ( user_id )
self : UpdateNick ( user_id , ' prefix ' , nprefix )
self : UpdateNick ( user_id , ' prefix_color ' , nprefix_color )
end
2016-02-12 14:23:44 +01:00
end
elseif chunk [ ' type ' ] == ' m.room.join_rules ' then
-- TODO: parse join_rules events --
self.join_rules = chunk.content
elseif chunk [ ' type ' ] == ' m.typing ' then
-- Store the typing ids in a table that the bar item can use
local typing_ids = { }
for _ , id in ipairs ( chunk.content . user_ids ) do
self : UpdatePresence ( id , ' typing ' )
typing_ids [ # typing_ids + 1 ] = self.users [ id ]
end
self.typing_ids = typing_ids
w.bar_item_update ( ' matrix_typing_notice ' )
elseif chunk [ ' type ' ] == ' m.presence ' then
SERVER : UpdatePresence ( chunk )
elseif chunk [ ' type ' ] == ' m.room.aliases ' then
-- Use first alias, weechat doesn't really support multiple aliases
self.aliases = chunk.content . aliases
2017-08-10 17:53:30 +02:00
self : SetName ( chunk.content . aliases [ 1 ] )
2016-02-12 14:23:44 +01:00
elseif chunk [ ' type ' ] == ' m.room.canonical_alias ' then
self.canonical_alias = chunk.content . alias
2017-08-10 17:53:30 +02:00
self : SetName ( self.canonical_alias )
2016-02-12 14:23:44 +01:00
elseif chunk [ ' type ' ] == ' m.room.redaction ' then
local redact_id = chunk.redacts
2017-08-10 17:53:30 +02:00
--perr('Redacting message ' .. redact_id)
2016-02-12 14:23:44 +01:00
local result = self : UpdateLine ( redact_id , w.color ' darkgray ' .. ' (redacted) ' )
2017-08-10 17:53:30 +02:00
if not result and not backlog then
-- backlog doesn't send original message
2016-02-12 14:23:44 +01:00
perr ' Could not find message to redact :( '
end
elseif chunk [ ' type ' ] == ' m.room.history_visibility ' then
self.history_visibility = chunk.content . history_visibility
2017-08-10 17:53:30 +02:00
-- luacheck: ignore 542
2016-02-12 14:23:44 +01:00
elseif chunk [ ' type ' ] == ' m.receipt ' then
-- TODO: figure out if we can do something sensible with read receipts
else
if DEBUG then
perr ( ( ' Unknown event type %s%s%s in room %s%s%s ' ) : format (
w.color ' bold ' ,
chunk.type ,
default_color ,
w.color ' bold ' ,
self.name ,
default_color ) )
dbg { chunk = chunk }
end
end
end
function Room : Op ( nick )
for id , name in pairs ( self.users ) do
if name == nick then
-- patch the locally cached power levels
self.power_levels . users [ id ] = 99
SERVER : state ( self.identifier , ' m.room.power_levels ' ,
self.power_levels )
break
end
end
end
function Room : Voice ( nick )
for id , name in pairs ( self.users ) do
if name == nick then
-- patch the locally cached power levels
self.power_levels . users [ id ] = 25
SERVER : state ( self.identifier , ' m.room.power_levels ' ,
self.power_levels )
break
end
end
end
function Room : Devoice ( nick )
for id , name in pairs ( self.users ) do
if name == nick then
-- patch the locally cached power levels
self.power_levels . users [ id ] = 0
SERVER : state ( self.identifier , ' m.room.power_levels ' ,
self.power_levels )
break
end
end
end
function Room : Deop ( nick )
for id , name in pairs ( self.users ) do
if name == nick then
-- patch the locally cached power levels
self.power_levels . users [ id ] = 0
SERVER : state ( self.identifier , ' m.room.power_levels ' ,
self.power_levels )
break
end
end
end
function Room : Kick ( nick , reason )
for id , name in pairs ( self.users ) do
if name == nick then
local data = {
membership = ' leave ' ,
reason = ' Kicked by ' .. SERVER.user_id
}
SERVER : set_membership ( self.identifier , id , data )
break
end
end
end
function Room : Whois ( nick )
for id , name in pairs ( self.users ) do
if name == nick then
local pcolor = wcolor ' weechat.color.chat_prefix_network '
local data = ( ' %s-- \t %s%s%s has user id %s%s ' ) : format (
pcolor ,
w.info_get ( ' irc_nick_color ' , nick ) ,
nick ,
default_color ,
w.info_get ( ' irc_nick_color ' , id ) ,
id )
w.print_date_tags ( self.buffer , nil , ' notify_message ' , data )
2017-08-10 17:53:30 +02:00
local pdata = ( ' %s-- \t %s%s%s has presence %s%s ' ) : format (
pcolor ,
w.info_get ( ' irc_nick_color ' , nick ) ,
nick ,
default_color ,
pcolor ,
SERVER.presence [ id ] or ' offline ' )
w.print_date_tags ( self.buffer , nil , ' notify_message ' , pdata )
-- TODO support printing status_msg field in presence data here
2016-02-12 14:23:44 +01:00
break
end
end
end
function Room : Invite ( id )
SERVER : Invite ( self.identifier , id )
end
function Room : Encrypt ( )
self.encrypted = true
-- Download keys for all members
self : Download_keys ( )
-- Create sessions
-- Pickle.
-- Save
end
function Room : Download_keys ( )
for id , name in pairs ( self.users ) do
-- TODO enable batch downloading of keys here when synapse can handle it
SERVER.olm : query ( { id } )
end
end
function Room : MarkAsRead ( )
-- Get event id from tag of last line in buffer
local lines = w.hdata_pointer ( w.hdata_get ( ' buffer ' ) , self.buffer , ' own_lines ' )
if lines == ' ' then return end
local line = w.hdata_pointer ( w.hdata_get ( ' lines ' ) , lines , ' last_line ' )
if line == ' ' then return end
local hdata_line = w.hdata_get ( ' line ' )
local hdata_line_data = w.hdata_get ( ' line_data ' )
local data = w.hdata_pointer ( hdata_line , line , ' data ' )
local tag_count = w.hdata_integer ( hdata_line_data , data , " tags_count " )
if tag_count > 0 then
for i = 0 , tag_count - 1 do
local tag = w.hdata_string ( hdata_line_data , data , i .. " |tags_array " )
-- Event ids are like $142533663810152bfUKc:matrix.org
if tag : match ' ^%$.*: ' then
SERVER : SendReadReceipt ( self.identifier , tag )
break
end
end
end
end
function poll ( a , b )
SERVER : poll ( )
return w.WEECHAT_RC_OK
end
function polltimer_cb ( a , b )
local now = os.time ( )
if ( now - SERVER.polltime ) > POLL_INTERVAL + 10 then
-- Release the poll lock
SERVER.poll_lock = false
SERVER : poll ( )
end
return w.WEECHAT_RC_OK
end
function otktimer_cb ( a , b )
SERVER.olm : check_server_keycount ( )
return w.WEECHAT_RC_OK
end
function cleartyping ( a , b )
for id , room in pairs ( SERVER.rooms ) do
room : ClearTyping ( )
end
return w.WEECHAT_RC_OK
end
function join_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if current_buffer == BUFFER or room then
2017-08-10 17:53:30 +02:00
local _ , alias = split_args ( args )
if not alias then
2016-02-12 14:23:44 +01:00
-- To support running /join on a invited room without args
2017-08-10 17:53:30 +02:00
SERVER : Join ( room.identifier )
2016-02-12 14:23:44 +01:00
else
2017-08-10 17:53:30 +02:00
SERVER : Join ( alias )
2016-02-12 14:23:44 +01:00
end
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function part_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
SERVER : part ( room )
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function leave_command_cb ( data , current_buffer , args )
return part_command_cb ( data , current_buffer , args )
end
function me_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , message = split_args ( args )
room : emote ( message or ' ' )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function topic_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , topic = split_args ( args )
room : Topic ( topic )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function upload_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , upload = split_args ( args )
room : Upload ( upload )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function query_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , query = split_args ( args )
2016-02-12 14:23:44 +01:00
for id , displayname in pairs ( room.users ) do
2017-08-10 17:53:30 +02:00
if displayname == query then
2016-02-12 14:23:44 +01:00
-- Create a new room and invite the guy
SERVER : CreateRoom ( false , nil , { id } )
return w.WEECHAT_RC_OK_EAT
end
end
else
return w.WEECHAT_RC_OK
end
end
function create_command_cb ( data , current_buffer , args )
2017-08-10 17:53:30 +02:00
local command , arg = split_args ( args )
2016-02-12 14:23:44 +01:00
local room = SERVER : findRoom ( current_buffer )
if ( room or current_buffer == BUFFER ) and command == ' /create ' then
2017-08-10 17:53:30 +02:00
if arg then
2016-02-12 14:23:44 +01:00
-- Room names are supposed to be without # and homeserver, so
-- we try to help the user out here
2017-08-10 17:53:30 +02:00
local alias = arg : match ' #?(.*):? '
2016-02-12 14:23:44 +01:00
-- Create a non-public room with argument as alias
SERVER : CreateRoom ( false , alias , nil )
else
mprint ' Use /create room-name '
end
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function createalias_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
local _ , alias = split_args ( args )
SERVER : CreateRoomAlias ( room.identifier , alias )
return w.WEECHAT_RC_OK_EAT
elseif current_buffer == BUFFER then
mprint ' Use /createalias #alias:homeserver.domain from a room '
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function invite_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , invitee = split_args ( args )
room : Invite ( invitee )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function list_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room or current_buffer == BUFFER then
2017-08-10 17:53:30 +02:00
local _ , target = split_args ( args )
SERVER : ListRooms ( target )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function op_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , target = split_args ( args )
room : Op ( target )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function voice_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , target = split_args ( args )
room : Voice ( target )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function devoice_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , target = split_args ( args )
room : Devoice ( target )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function deop_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , target = split_args ( args )
room : Deop ( target )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function kick_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , target = split_args ( args )
room : Kick ( target )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function nick_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room or current_buffer == BUFFER then
local _ , nick = split_args ( args )
SERVER : Nick ( nick )
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function whois_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
local _ , nick = split_args ( args )
room : Whois ( nick )
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function notice_command_cb ( data , current_buffer , args )
-- TODO sending from matrix buffer given a room name
local room = SERVER : findRoom ( current_buffer )
if room then
local _ , msg = split_args ( args )
room : Notice ( msg )
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function msg_command_cb ( data , current_buffer , args )
2017-08-10 17:53:30 +02:00
local _ , msgmask = split_args ( args )
local mask , msg = split_args ( msgmask )
2016-02-12 14:23:44 +01:00
local room
-- WeeChat uses * as a mask for current buffer
if mask == ' * ' then
room = SERVER : findRoom ( current_buffer )
else
for id , r in pairs ( SERVER.rooms ) do
-- Send /msg to a ID
if id == mask then
room = r
break
elseif mask == r.name then
room = r
break
end
end
end
if room then
room : Msg ( msg )
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function encrypt_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
2017-08-10 17:53:30 +02:00
local _ , arg = split_args ( args )
if arg == ' on ' then
2016-02-12 14:23:44 +01:00
mprint ( ' Enabling encryption for outgoing messages in room ' .. tostring ( room.name ) )
room : Encrypt ( )
2017-08-10 17:53:30 +02:00
elseif arg == ' off ' then
2016-02-12 14:23:44 +01:00
mprint ( ' Disabling encryption for outgoing messages in room ' .. tostring ( room.name ) )
room.encrypted = false
else
w.print ( current_buffer , ' Use /encrypt on or /encrypt off to turn encryption on or off ' )
end
return w.WEECHAT_RC_OK_EAT
else
return w.WEECHAT_RC_OK
end
end
function public_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
mprint ( ' Marking room as public: ' .. tostring ( room.name ) )
room : public ( )
return w.WEECHAT_RC_OK_EAT
else
mprint ( ' Run command from a room ' )
return w.WEECHAT_RC_OK
end
end
2017-08-10 17:53:30 +02:00
function names_command_cb ( cbdata , current_buffer , args )
2016-02-12 14:23:44 +01:00
local room = SERVER : findRoom ( current_buffer )
if room then
local nrcolor = function ( nr )
return wcolor ' weechat.color.chat_channel '
.. tostring ( nr )
.. default_color
end
local buffer_name = nrcolor ( w.buffer_get_string ( room.buffer , ' name ' ) )
local delim_c = wcolor ' weechat.color.chat_delimiters '
local tags = ' no_highlight,no_log,irc_names '
local pcolor = wcolor ' weechat.color.chat_prefix_network '
local ngroups = { }
local nicks = { }
for id , name in pairs ( room.users ) do
local ncolor
2017-08-10 17:53:30 +02:00
if id == SERVER.user_id then
2016-02-12 14:23:44 +01:00
ncolor = w.color ( ' chat_nick_self ' )
else
ncolor = w.info_get ( ' irc_nick_color ' , name )
end
local ngroup , nprefix , nprefix_color = room : GetNickGroup ( id )
if nprefix == ' ' then nprefix = ' ' end
nicks [ # nicks + 1 ] = ( ' %s%s%s%s ' ) : format (
w.color ( nprefix_color ) ,
nprefix ,
ncolor ,
name
)
if not ngroups [ ngroup ] then
ngroups [ ngroup ] = 0
end
ngroups [ ngroup ] = ngroups [ ngroup ] + 1
end
2017-08-10 17:53:30 +02:00
local line1 = ( ' %s-- \t Nicks %s: %s[%s%s] ' ) : format (
2016-02-12 14:23:44 +01:00
pcolor ,
buffer_name ,
delim_c ,
table.concat ( nicks , ' ' ) ,
delim_c
)
2017-08-10 17:53:30 +02:00
w.print_date_tags ( room.buffer , 0 , tags , line1 )
local line2 = (
2016-02-12 14:23:44 +01:00
' %s-- \t Channel %s: %s nicks %s(%s%s ops, %s voice, %s normals%s) '
) : format (
pcolor ,
buffer_name ,
nrcolor ( room.member_count ) ,
delim_c ,
default_color ,
nrcolor ( ( ngroups [ 1 ] or 0 ) + ( ngroups [ 2 ] or 0 ) ) ,
nrcolor ( ngroups [ 3 ] or 0 ) ,
nrcolor ( ( ngroups [ 4 ] or 0 ) + ( ngroups [ 5 ] or 0 ) ) ,
delim_c
)
2017-08-10 17:53:30 +02:00
w.print_date_tags ( room.buffer , 0 , tags , line2 )
2016-02-12 14:23:44 +01:00
return w.WEECHAT_RC_OK_EAT
else
perr ( ' Could not find room ' )
return w.WEECHAT_RC_OK
end
end
function more_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
SERVER : getMessages ( room.identifier , ' b ' , room.prev_batch , 120 )
return w.WEECHAT_RC_OK_EAT
else
perr ( ' /more Could not find room ' )
end
return w.WEECHAT_RC_OK
end
2017-08-10 17:53:30 +02:00
function roominfo_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
dbg { room = room }
return w.WEECHAT_RC_OK_EAT
else
perr ( ' /roominfo Could not find room ' )
end
return w.WEECHAT_RC_OK
end
function name_command_cb ( data , current_buffer , args )
local room = SERVER : findRoom ( current_buffer )
if room then
local _ , name = split_args ( args )
room : Name ( name )
return w.WEECHAT_RC_OK_EAT
else
perr ( ' /name Could not find room ' )
end
return w.WEECHAT_RC_OK
end
2016-02-12 14:23:44 +01:00
function closed_matrix_buffer_cb ( data , buffer )
BUFFER = nil
return w.WEECHAT_RC_OK
end
function closed_matrix_room_cb ( data , buffer )
-- WeeChat closed our room
local room = SERVER : findRoom ( buffer )
if room then
room.buffer = nil
perr ( ' Room got closed: ' .. room.name )
SERVER.rooms [ room.identifier ] = nil
return w.WEECHAT_RC_OK
end
return w.WEECHAT_RC_ERR
end
function typing_notification_cb ( signal , sig_type , data )
-- Ignore commands
if data : match ' ^/ ' then
return w.WEECHAT_RC_OK
end
-- Is this signal coming from a matrix buffer?
local room = SERVER : findRoom ( data )
if room then
local input = w.buffer_get_string ( data , " input " )
-- Start sending when it reaches > 4 and doesn't start with command
if # input > 4 and not input : match ' ^/ ' then
local now = os.time ( )
-- Generate typing events every 4th second
if SERVER.typing_time + 4 < now then
SERVER.typing_time = now
room : SendTypingNotice ( )
end
end
end
return w.WEECHAT_RC_OK
end
function buffer_switch_cb ( signal , sig_type , data )
-- Update bar item
w.bar_item_update ( ' matrix_typing_notice ' )
local current_buffer = w.current_buffer ( )
local room = SERVER : findRoom ( current_buffer )
if room then
room : MarkAsRead ( )
end
return w.WEECHAT_RC_OK
end
function typing_bar_item_cb ( data , buffer , args )
local current_buffer = w.current_buffer ( )
local room = SERVER : findRoom ( current_buffer )
if not room then return ' ' end
local typing_ids = table.concat ( room.typing_ids , ' ' )
if # typing_ids > 0 then
return " Typing: " .. typing_ids
end
return ' '
end
if w.register ( SCRIPT_NAME , SCRIPT_AUTHOR , SCRIPT_VERSION , SCRIPT_LICENSE , SCRIPT_DESC , " matrix_unload " , " UTF-8 " ) then
2017-08-10 17:53:30 +02:00
-- Save WeeChat version to a global so other functionality can see it
local version = w.info_get ( ' version_number ' , ' ' ) or 0
WEECHAT_VERSION = tonumber ( version )
2016-02-12 14:23:44 +01:00
local settings = {
homeserver_url = { ' https://matrix.org/ ' , ' Full URL including port to your homeserver (including trailing slash) or use default matrix.org ' } ,
user = { ' ' , ' Your homeserver username ' } ,
password = { ' ' , ' Your homeserver password ' } ,
backlog_lines = { ' 120 ' , ' Number of lines to fetch from backlog upon connecting ' } ,
2017-08-10 17:53:30 +02:00
presence_filter = { ' off ' , ' Filter presence messages and ephemeral events (for performance) ' } ,
2016-02-12 14:23:44 +01:00
autojoin_on_invite = { ' on ' , ' Automatically join rooms you are invited to ' } ,
typing_notices = { ' on ' , ' Send typing notices when you type ' } ,
local_echo = { ' on ' , ' Print lines locally instead of waiting for return from server ' } ,
debug = { ' off ' , ' Print a lot of extra information to help with finding bugs and other problems. ' } ,
encrypted_message_color = { ' lightgreen ' , ' Print encrypted mesages with this color ' } ,
--olm_secret = {'', 'Password used to secure olm stores'},
}
-- set default settings
for option , value in pairs ( settings ) do
if w.config_is_set_plugin ( option ) ~= 1 then
w.config_set_plugin ( option , value [ 1 ] )
end
2017-08-10 17:53:30 +02:00
if WEECHAT_VERSION >= 0x00030500 then
2016-02-12 14:23:44 +01:00
w.config_set_desc_plugin ( option , ( ' %s (default: "%s") ' ) : format (
value [ 2 ] , value [ 1 ] ) )
end
end
errprefix = wconf ' weechat.look.prefix_error '
errprefix_c = wcolor ' weechat.color.chat_prefix_error '
HOMEDIR = w.info_get ( ' weechat_dir ' , ' ' ) .. ' / '
local commands = {
' join ' , ' part ' , ' leave ' , ' me ' , ' topic ' , ' upload ' , ' query ' , ' list ' ,
' op ' , ' voice ' , ' deop ' , ' devoice ' , ' kick ' , ' create ' , ' createalias ' , ' invite ' , ' nick ' ,
2017-08-10 17:53:30 +02:00
' whois ' , ' notice ' , ' msg ' , ' encrypt ' , ' public ' , ' names ' , ' more ' ,
' roominfo ' , ' name '
2016-02-12 14:23:44 +01:00
}
for _ , c in pairs ( commands ) do
w.hook_command_run ( ' / ' .. c , c .. ' _command_cb ' , ' ' )
end
if w.config_get_plugin ( ' typing_notices ' ) == ' on ' then
w.hook_signal ( ' input_text_changed ' , " typing_notification_cb " , ' ' )
end
if w.config_get_plugin ( ' debug ' ) == ' on ' then
DEBUG = true
end
2017-08-10 17:53:30 +02:00
w.hook_config ( ' plugins.var.lua.matrix.debug ' , ' configuration_changed_cb ' , ' ' )
2016-02-12 14:23:44 +01:00
2017-08-10 17:53:30 +02:00
local cmds = { ' help ' , ' connect ' , ' debug ' , ' msg ' }
2016-02-12 14:23:44 +01:00
w.hook_command ( SCRIPT_COMMAND , ' Plugin for matrix.org chat protocol ' ,
' [command] [command options] ' ,
' Commands: \n ' .. table.concat ( cmds , ' \n ' ) ..
2017-08-10 17:53:30 +02:00
' \n Use /matrix help [command] to find out more \n ' ..
' \n Supported slash commands (i.e. /commands): \n ' ..
table.concat ( commands , ' , ' ) ,
2016-02-12 14:23:44 +01:00
-- Completions
table.concat ( cmds , ' | ' ) ,
' matrix_command_cb ' , ' ' )
2017-08-10 17:53:30 +02:00
w.hook_command_run ( ' /away -all* ' , ' matrix_away_command_run_cb ' , ' ' )
2016-02-12 14:23:44 +01:00
SERVER = MatrixServer.create ( )
2017-08-10 17:53:30 +02:00
if WEECHAT_VERSION < 0x01040000 then
perr ( SCRIPT_NAME .. ' : Please upgrade your WeeChat before using this script. Using this script on older WeeChat versions may lead to crashes. Many bugs have been fixed in newer versions of WeeChat. ' )
perr ( SCRIPT_NAME .. ' : Refusing to automatically connect you. If you insist, type / ' .. SCRIPT_COMMAND .. ' connect, and do not act surprised if it crashes :-) ' )
else
SERVER : connect ( )
end
2016-02-12 14:23:44 +01:00
w.hook_signal ( ' buffer_switch ' , " buffer_switch_cb " , " " )
w.bar_item_new ( ' matrix_typing_notice ' , ' typing_bar_item_cb ' , ' ' )
end