342 lines
8.3 KiB
Lua
Executable file
342 lines
8.3 KiB
Lua
Executable file
#!/usr/bin/env lua
|
|
--[[
|
|
The GPLv2 License (GPLv2)
|
|
Copyright (c) 2022 Jeremy Baxter
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
]]--
|
|
|
|
require "luarocks.loader"
|
|
getopt = require "posix.unistd".getopt
|
|
mkdir = require "posix.sys.stat".mkdir
|
|
bname = require "posix.libgen".basename
|
|
http = require "socket.http"
|
|
cjson = require "cjson"
|
|
ltn12 = require "ltn12"
|
|
|
|
version = "2.1"
|
|
confdir = string.gsub("~/.config/matrix-send", '~', os.getenv("HOME"), 1)
|
|
confpath = confdir .. "/config.lua"
|
|
confvarv = os.getenv("MATRIXSEND_CONFIG")
|
|
hostname = io.input("/etc/hostname"):read("l")
|
|
|
|
matrix = {}
|
|
json = {}
|
|
uri = {}
|
|
sh = os.execute
|
|
|
|
-----------------
|
|
--- Functions ---
|
|
-----------------
|
|
|
|
-- Returns true if the given file name file exists, otherwise returns false.
|
|
function fileexists(file)
|
|
local f, err = io.open(file, 'r')
|
|
if f then
|
|
f:close()
|
|
return true
|
|
else
|
|
return false, err
|
|
end
|
|
end
|
|
|
|
-- Prints a message to stdout in the following format:
|
|
-- matrix-send: s
|
|
function msg(s)
|
|
io.stdout:write(string.format("%s: %s\n", bname(arg[0]), s))
|
|
end
|
|
|
|
-- Prints an error message to stderr in the following format:
|
|
-- matrix-send: s
|
|
-- After that, exits and returns 1 to the OS.
|
|
function panic(s)
|
|
io.stderr:write(string.format("%s: %s\n", bname(arg[0]), s))
|
|
os.exit(1)
|
|
end
|
|
|
|
-- Prints an error message to stderr in the following format:
|
|
-- matrix-send: confpath: s
|
|
-- After that, exits and returns 1 to the OS.
|
|
function confpanic(s)
|
|
panic(string.format("%s: %s", bname(confpath), s))
|
|
os.exit(1)
|
|
end
|
|
|
|
------------------------
|
|
--- Matrix functions ---
|
|
------------------------
|
|
|
|
function matrix.login(uri, json)
|
|
local respbody = {}
|
|
local result, respcode, respheaders, respstatus = http.request {
|
|
method = "POST",
|
|
url = uri,
|
|
source = ltn12.source.string(json),
|
|
headers = {
|
|
["content-type"] = "application/json",
|
|
["content-length"] = tostring(#json)
|
|
},
|
|
sink = ltn12.sink.table(respbody)
|
|
}
|
|
local body = table.concat(respbody)
|
|
local t = cjson.decode(body)
|
|
if t.error then
|
|
panic("server: " .. t.error)
|
|
else
|
|
return t.access_token
|
|
end
|
|
end
|
|
|
|
function matrix.send(uri, json, login_uri, login_json)
|
|
local respbody = {}
|
|
local result, respcode, respheaders, respstatus = http.request {
|
|
method = "PUT",
|
|
url = uri,
|
|
source = ltn12.source.string(json),
|
|
headers = {
|
|
["content-type"] = "application/json",
|
|
["content-length"] = tostring(#json)
|
|
},
|
|
sink = ltn12.sink.table(respbody)
|
|
}
|
|
local body = table.concat(respbody)
|
|
local t = cjson.decode(body)
|
|
if t.error then
|
|
if t.errcode == "M_UNKNOWN_TOKEN" then
|
|
msg("token expired/invalidated; re-authenticating")
|
|
-- I mean, it works
|
|
t = setmetatable({}, {
|
|
__concat = function(left, right)
|
|
if type(left) == "string" then
|
|
return left .. tostring(right)
|
|
end
|
|
return tostring(left) .. tostring(right)
|
|
end
|
|
})
|
|
t.token = matrix.login(login_uri, login_json)
|
|
if not cache.disable then
|
|
mkdir(cache.location)
|
|
cf = io.open(cache.location .. "/token", 'w+')
|
|
cf:write(t.token)
|
|
cf:close()
|
|
end
|
|
matrix.send(string.gsub(uri, "access_token=.+", "access_token=" .. t.token), json, login_uri, login_json)
|
|
else
|
|
panic("server: " .. t.error)
|
|
end
|
|
else
|
|
return t
|
|
end
|
|
end
|
|
|
|
-- Default configuration file
|
|
default_config = [[
|
|
-- Default configuration file for matrix-send
|
|
-- In comments, here is a simple example configuration.
|
|
|
|
--login = {
|
|
-- server = "envs.net",
|
|
-- username = "john",
|
|
-- password = "examplepassword
|
|
--}
|
|
|
|
-- You could also do it this way:
|
|
|
|
--login.server = "envs.net"
|
|
--login.username = "john"
|
|
--login.password = "examplepassword"
|
|
|
|
-- Both configurations do the same thing.
|
|
|
|
-- See matrix-send.conf(5) for more information
|
|
-- on configuration files.
|
|
]]
|
|
|
|
------------------------------------
|
|
--- Default configuration values ---
|
|
------------------------------------
|
|
|
|
-- Initialise tables
|
|
login = {}
|
|
cache = {}
|
|
rooms = {}
|
|
advanced = {}
|
|
|
|
-- Cache location
|
|
cache.location = "~/.cache/matrix-send"
|
|
|
|
-- Disable caching access tokens?
|
|
cache.disable = false
|
|
|
|
-- Message type
|
|
advanced.event = "m.text"
|
|
|
|
----------------------
|
|
--- Initial checks ---
|
|
----------------------
|
|
|
|
if arg[1] == nil then
|
|
io.stderr:write("usage: " .. bname(arg[0]) .. " [-c config] [-t type] [-CV] message room\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
if confvarv then
|
|
if fileexists(confvarv) then
|
|
confpath = os.getenv("MATRIXSEND_CONFIG")
|
|
else
|
|
local _, err = fileexists(os.getenv("MATRIXSEND_CONFIG"))
|
|
panic("error opening " .. err)
|
|
end
|
|
end
|
|
|
|
if not fileexists(confpath) then
|
|
mkdir(confdir)
|
|
f = io.open(confpath, 'w')
|
|
f:write(default_config)
|
|
f:close()
|
|
end
|
|
dofile(confpath)
|
|
|
|
cache.location = string.gsub(cache.location, '~', os.getenv("HOME"), 1)
|
|
|
|
-- Make sure all required values are set
|
|
if not login.server then
|
|
confpanic("required value 'login.server' left unset")
|
|
end
|
|
|
|
-- login.token always supersedes login.username and password
|
|
if not login.token then
|
|
if not login.username then
|
|
confpanic("required value 'login.username' left unset")
|
|
elseif not login.password then
|
|
confpanic("required value 'login.password' left unset")
|
|
end
|
|
end
|
|
|
|
-- Parse options
|
|
for opt, optarg, ind in getopt(arg, ':Cc:t:V') do
|
|
optind = ind
|
|
if opt == 'C' then
|
|
msg("clearing cache")
|
|
os.execute("rm -rf " .. cache.location) -- TODO: replace this os.execute line
|
|
os.exit(0)
|
|
elseif opt == 'c' then
|
|
confpath = optarg
|
|
elseif opt == 't' then
|
|
if optarg:match("m%.text")
|
|
or optarg:match("m%.notice") then
|
|
advanced.event = optarg
|
|
else panic("unknown message type '" .. optarg .. "'")
|
|
end
|
|
elseif opt == 'V' then
|
|
io.stdout:write("matrix-send version " .. version .. '\n')
|
|
io.stdout:write(_VERSION .. '\n')
|
|
os.exit(0)
|
|
elseif opt == '?' then
|
|
panic("unknown option " .. arg[optind-1])
|
|
os.exit(1)
|
|
elseif opt == ':' then
|
|
panic("missing argument for option " .. arg[optind-1])
|
|
os.exit(1)
|
|
end
|
|
end
|
|
|
|
if optind == nil then
|
|
message = arg[1]
|
|
room = arg[2]
|
|
else
|
|
message = arg[optind+1]
|
|
room = arg[optind+2]
|
|
end
|
|
|
|
if room:match("^!") then
|
|
room_id = room
|
|
else
|
|
if rooms[room] ~= nil then
|
|
room_id = rooms[room]
|
|
else
|
|
panic("alias '" .. room .. "' invalid")
|
|
end
|
|
end
|
|
|
|
if fileexists(cache.location .. "/txnid") then
|
|
txnidf = io.open(cache.location .. "/txnid", 'r')
|
|
txnid = txnidf:read('l')
|
|
txnidf:close()
|
|
else
|
|
txnid = 0
|
|
end
|
|
|
|
-- Get .well-known
|
|
uri.well_known = string.format("https://%s/.well-known/matrix/client", login.server)
|
|
wellknown_body, errno = http.request(uri.well_known)
|
|
if errno ~= 200 then
|
|
panic("server: Error " .. errno .. " while getting /.well-known/matrix/client")
|
|
end
|
|
wellknown_t = cjson.decode(wellknown_body)
|
|
login.server = wellknown_t["m.homeserver"].base_url
|
|
|
|
-- Assign URIs and JSON bodies
|
|
uri.login = string.format("%s/_matrix/client/v3/login", login.server)
|
|
json.login = string.format([[
|
|
{
|
|
"identifier": {
|
|
"type": "m.id.user",
|
|
"user": "%s"
|
|
},
|
|
"initial_device_display_name": "matrix-send@%s",
|
|
"password": "%s",
|
|
"type": "m.login.password"
|
|
}
|
|
]], login.username, hostname, login.password)
|
|
|
|
json.message = string.format([[
|
|
{
|
|
"body": "%s",
|
|
"msgtype": "%s"
|
|
}
|
|
]], message, advanced.event)
|
|
|
|
-- Get the access token if credentials are used
|
|
if not login.token then
|
|
if fileexists(cache.location .. "/token") then
|
|
cf = io.open(cache.location .. "/token", 'r')
|
|
login.token = cf:read('l')
|
|
cf:close()
|
|
else
|
|
-- Login
|
|
login.token = matrix.login(uri.login, json.login)
|
|
|
|
-- Cache the access token
|
|
if not cache.disable then
|
|
mkdir(cache.location)
|
|
cf = io.open(cache.location .. "/token", 'w+')
|
|
cf:write(login.token)
|
|
cf:close()
|
|
end
|
|
end
|
|
end
|
|
|
|
uri.message = string.format("%s/_matrix/client/v3/rooms/%s/send/m.room.message/%d?access_token=%s",
|
|
login.server, room_id, txnid, login.token)
|
|
|
|
-- Send the message!
|
|
matrix.send(uri.message, json.message, uri.login, json.login)
|
|
|
|
-- Increment txnid and cache
|
|
if not cache.disable then
|
|
txnidf = io.open(cache.location .. "/txnid", 'w+')
|
|
txnidf:write(txnid + 1)
|
|
txnidf:close()
|
|
end
|