Rewrote the whole entire thing in Lua
Shortened list of changes: - Added room aliases - Switched the -C and -c flags - Added the MATRIXSEND_CONFIG environment variable - Changed config format to Lua over custom /bin/sh - Upgraded to Matrix spec 1.5 over whatever version it was previously using - Removed JSON printing (and in turn, pretty printing) - Added actual Matrix error reporting rather than just feeding the JSON to the user - Switched to luaposix getopt() over /bin/sh getopts - Some more changes...
This commit is contained in:
parent
aaf2350de3
commit
914e8af956
1 changed files with 347 additions and 204 deletions
545
matrix-send
545
matrix-send
|
@ -1,220 +1,363 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env lua
|
||||||
# matrix-send: send a message to a Matrix room
|
--[[
|
||||||
|
The GPLv2 License (GPLv2)
|
||||||
|
Copyright (c) 2022 Jeremy Baxter
|
||||||
|
|
||||||
version="1.1"
|
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,
|
||||||
#### Generic Functions ####
|
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.
|
||||||
|
|
||||||
error () {
|
You should have received a copy of the GNU General Public License
|
||||||
printf "\033[31;1merror:\033[0m $1\n"
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
exit 1
|
]]--
|
||||||
}
|
|
||||||
|
|
||||||
conf_error () {
|
require "luarocks.loader"
|
||||||
printf "\033[31;1mconfiguration error:\033[0m $1\n"
|
getopt = require "posix.unistd".getopt
|
||||||
exit 2
|
mkdir = require "posix.sys.stat".mkdir
|
||||||
}
|
bname = require "posix.libgen".basename
|
||||||
|
http = require "socket.http"
|
||||||
|
cjson = require "cjson"
|
||||||
|
ltn12 = require "ltn12"
|
||||||
|
|
||||||
warning () {
|
version = "2.0"
|
||||||
printf "\033[93;1mwarning:\033[0m $1\n"
|
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")
|
||||||
|
|
||||||
vargrep () {
|
json = {}
|
||||||
printf "$2\n" | grep "$1" $3
|
uri = {}
|
||||||
}
|
sh = os.execute
|
||||||
|
|
||||||
usage () {
|
-----------------
|
||||||
printf "usage: matrix-send [-t type] [-C config] [-chV] message room\n"
|
--- Functions ---
|
||||||
exit 1
|
-----------------
|
||||||
}
|
|
||||||
|
|
||||||
help () {
|
-- Returns true if the given file name file exists, otherwise returns false.
|
||||||
cat <<EOF
|
function fileexists(file)
|
||||||
matrix-send: send a message to a Matrix room
|
local f, err = io.open(file, 'r')
|
||||||
Options:
|
if f then
|
||||||
-t type: change default event type
|
f:close()
|
||||||
-C config: use a custom configuration file
|
return true
|
||||||
-c: clear cached access tokens
|
|
||||||
-h: show this help menu
|
|
||||||
-V: show version and program information
|
|
||||||
|
|
||||||
For more information, type 'man matrix-send'.
|
|
||||||
https://codeberg.org/jtbx/matrix-send
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
version () {
|
|
||||||
cat <<EOF
|
|
||||||
matrix-send: send a message to a Matrix room
|
|
||||||
Version $version
|
|
||||||
|
|
||||||
matrix-send is licensed under the GNU General Public License v2.
|
|
||||||
Made in New Zealand
|
|
||||||
https://codeberg.org/jtbx/matrix-send
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
##################################
|
|
||||||
#### Configuration Directives ####
|
|
||||||
##################################
|
|
||||||
|
|
||||||
Server () {
|
|
||||||
[ -z $1 ] && conf_error "No argument for directive Server"
|
|
||||||
server="$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
Username () {
|
|
||||||
[ -z $1 ] && conf_error "No argument for directive Username"
|
|
||||||
username="$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
Password () {
|
|
||||||
[ -z $1 ] && conf_error "No argument(s) for directive Password"
|
|
||||||
password="$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessToken () {
|
|
||||||
[ -z $1 ] && conf_error "No argument for directive AccessToken"
|
|
||||||
manualAuth="true"
|
|
||||||
token="$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
CacheLocation () {
|
|
||||||
[ -z $1 ] && conf_error "No argument for directive CacheLocation"
|
|
||||||
if vargrep "^/.+$" "$1" -Eq || \
|
|
||||||
vargrep "^~.+$" "$1" -Eq;
|
|
||||||
then cacheloc="$1";
|
|
||||||
else conf_error "Cache location is not valid (does not begin with / or ~)"; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultEvent () {
|
|
||||||
[ -z $1 ] && conf_error "No argument for directive DefaultEvent"
|
|
||||||
if vargrep "m\.(text|notice)" "$1" -Eq
|
|
||||||
then defaultevent="$1"
|
|
||||||
else conf_error "Invalid default event type"; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
##################################
|
|
||||||
#### Configuration Statements ####
|
|
||||||
##################################
|
|
||||||
|
|
||||||
NoCache () {
|
|
||||||
nocache="true"
|
|
||||||
}
|
|
||||||
|
|
||||||
PrettyPrint () {
|
|
||||||
prettyprint="true"
|
|
||||||
}
|
|
||||||
|
|
||||||
############################
|
|
||||||
#### Specific Functions ####
|
|
||||||
############################
|
|
||||||
|
|
||||||
GetAccessToken () {
|
|
||||||
[ -z "$manualAuth" ] && printf "Getting access token...\n"
|
|
||||||
if [ "$manualAuth" = "true" ];
|
|
||||||
then printf "";
|
|
||||||
else token=$(curl -s -XPOST -d "{"'"'"type"'"'":"'"'"m.login.password"'"'", "'"'"user"'"'":"'"'"$username"'"'", "'"'"password"'"'":"'"'"$password"'"'"}" "https://$server/_matrix/client/r0/login" | grep -oE 'syt_.+_...................._......');
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
CacheAccessToken () {
|
|
||||||
if [ "$nocache" = "true" ];
|
|
||||||
then printf "";
|
|
||||||
else
|
else
|
||||||
mkdir -p "$cacheloc/matrix-send"
|
return false, err
|
||||||
printf "$token\n" > "$cacheloc/matrix-send/access-token";
|
end
|
||||||
fi
|
end
|
||||||
}
|
|
||||||
|
|
||||||
ClearCache () {
|
-- Makes an HTTP POST request to uri and sends body. Returns the output of the HTTP server.
|
||||||
[ -e "$cacheloc/matrix-send/access-token" ] || printf "There is no cache to be cleared.\n"
|
function post(uri, body)
|
||||||
[ -e "$cacheloc/matrix-send/access-token" ] && rm -rf "$cacheloc/matrix-send/" && printf "Cleared cache\n"
|
local respbody = {} -- for the response body
|
||||||
exit 0
|
local result, respcode, respheaders, respstatus = http.request {
|
||||||
}
|
method = "POST",
|
||||||
|
url = uri,
|
||||||
|
source = ltn12.source.string(body),
|
||||||
|
headers = {
|
||||||
|
["content-type"] = "application/json",
|
||||||
|
["content-length"] = tostring(#body)
|
||||||
|
},
|
||||||
|
sink = ltn12.sink.table(respbody)
|
||||||
|
}
|
||||||
|
return table.concat(respbody)
|
||||||
|
end
|
||||||
|
|
||||||
Send () {
|
-- Makes an HTTP PUT request to uri and sends body. Returns the output of the HTTP server.
|
||||||
if [ -z "$prettyprint" ]
|
function put(uri, body)
|
||||||
then curl -s -XPOST -d "{"'"'"msgtype"'"'":"'"'"$mtype"'"'", "'"'"body"'"'":"'"'"$message"'"'"}" "https://$server/_matrix/client/r0/rooms/%21$roomid/send/m.room.message?access_token=$token"
|
local respbody = {} -- for the response body
|
||||||
else curl -s -XPOST -d "{"'"'"msgtype"'"'":"'"'"$mtype"'"'", "'"'"body"'"'":"'"'"$message"'"'"}" "https://$server/_matrix/client/r0/rooms/%21$roomid/send/m.room.message?access_token=$token" | jq
|
local result, respcode, respheaders, respstatus = http.request {
|
||||||
fi
|
method = "PUT",
|
||||||
}
|
url = uri,
|
||||||
|
source = ltn12.source.string(body),
|
||||||
|
headers = {
|
||||||
|
["content-type"] = "application/json",
|
||||||
|
["content-length"] = tostring(#body)
|
||||||
|
},
|
||||||
|
sink = ltn12.sink.table(respbody)
|
||||||
|
}
|
||||||
|
return table.concat(respbody)
|
||||||
|
end
|
||||||
|
|
||||||
########################
|
-- Prints a message to stdout in the following format:
|
||||||
#### Initial checks ####
|
-- matrix-send: s
|
||||||
########################
|
function msg(s)
|
||||||
|
io.stdout:write(string.format("%s: %s\n", bname(arg[0]), s))
|
||||||
|
end
|
||||||
|
|
||||||
[ -e /usr/local/bin/curl ] || [ -e /usr/bin/curl ] || error "curl not found"
|
-- 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
|
||||||
|
|
||||||
[ -z "$1" ] && usage
|
-- Prints an error message to stderr in the following format:
|
||||||
CacheLocation "$HOME/.cache"
|
-- matrix-send: confpath: s
|
||||||
while getopts :t:C:chV opt
|
-- After that, exits and returns 1 to the OS.
|
||||||
do
|
function confpanic(s)
|
||||||
case $opt in
|
panic(string.format("%s: %s", bname(confpath), s))
|
||||||
t)
|
os.exit(1)
|
||||||
if vargrep "m\.(text|notice)" "$OPTARG" -Eq
|
end
|
||||||
then
|
|
||||||
mtype="$OPTARG"
|
|
||||||
optind="$OPTIND"
|
|
||||||
else error "Type not valid (-t)"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
C)
|
|
||||||
if vargrep "^~" "$OPTARG" -Eq
|
|
||||||
then config="$OPTARG"
|
|
||||||
elif vargrep "^/" "$OPTARG" -Eq
|
|
||||||
then config="$OPTARG"
|
|
||||||
else error "Configuration file location not valid (-C)"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
c) ClearCache ;;
|
|
||||||
h) help ;;
|
|
||||||
V) version ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
unset OPTARG
|
|
||||||
unset OPTIND
|
|
||||||
|
|
||||||
###############################
|
--[[
|
||||||
#### Configuration loading ####
|
The configuration file for matrix-send is written in Lua format.
|
||||||
###############################
|
Here is a list of all possible options in the configuration file.
|
||||||
|
Default values appear after the '=' sign.
|
||||||
|
|
||||||
# Load configuration
|
login = {
|
||||||
if [ -e "$config" ];
|
-- The Matrix server to use.
|
||||||
then . "$config"
|
server = nil,
|
||||||
|
|
||||||
|
-- The user to log in to.
|
||||||
|
username = nil,
|
||||||
|
|
||||||
|
-- The password for the user.
|
||||||
|
password = nil,
|
||||||
|
|
||||||
|
-- The access token to use (instead of credentials).
|
||||||
|
-- If token equals nil, credentials are used.
|
||||||
|
-- If token is not nil, credentials are ignored.
|
||||||
|
token = nil
|
||||||
|
|
||||||
|
-- The server value needs to be provided.
|
||||||
|
-- You can choose to login with user credentials or a
|
||||||
|
-- token. One of them needs to be provided.
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = {
|
||||||
|
-- The path to cache access tokens at.
|
||||||
|
location = "~/.cache/matrix-send",
|
||||||
|
|
||||||
|
-- Disable caching access tokens?
|
||||||
|
disable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms = {
|
||||||
|
-- Room aliases.
|
||||||
|
-- Here you can add aliases for rooms,
|
||||||
|
-- instead of having to type the confusing
|
||||||
|
-- Room ID every single time you send a message.
|
||||||
|
-- Examples:
|
||||||
|
--my_alias = "!AbCdEfGhIjKl:burger.land",
|
||||||
|
--lounge = "!MnOpQrSTuVWxYz:gaming.bruvs"
|
||||||
|
-- When you want to send to a Matrix room, you
|
||||||
|
-- can just type the alias instead of the long Room ID.
|
||||||
|
}
|
||||||
|
|
||||||
|
advanced = {
|
||||||
|
-- The default event type.
|
||||||
|
-- Can be either m.text or m.notice.
|
||||||
|
event = "m.text"
|
||||||
|
}
|
||||||
|
--]]
|
||||||
|
|
||||||
|
-- Default configuration file
|
||||||
|
default_config = [[
|
||||||
|
-- Default configuration file for matrix-send
|
||||||
|
-- In comments, here is a simple example configuration.
|
||||||
|
|
||||||
|
--login = {
|
||||||
|
-- server = "matrix.envs.net",
|
||||||
|
-- username = "john",
|
||||||
|
-- password = "examplepassword
|
||||||
|
--}
|
||||||
|
|
||||||
|
-- You could also do it this way:
|
||||||
|
|
||||||
|
--login.server = "matrix.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]) .. " [-t type] [-c config] [-ChV] 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
|
else
|
||||||
warning "~/.config/matrix-send.conf doesn't exist; creating it"
|
message = arg[optind+1]
|
||||||
mkdir -p $HOME/.config
|
room = arg[optind+2]
|
||||||
touch $HOME/.config/matrix-send.conf
|
end
|
||||||
fi
|
|
||||||
|
|
||||||
# Run checks for essential directives
|
if room:match("^!") then
|
||||||
[ -z $server ] && conf_error "Server directive is not present"
|
room_id = room
|
||||||
[ -z $username ] && conf_error "Username directive is not present"
|
|
||||||
[ -z "$password" ] && conf_error "Password directive is not present"
|
|
||||||
|
|
||||||
##############
|
|
||||||
#### Main ####
|
|
||||||
##############
|
|
||||||
|
|
||||||
# Get token and cache it (unless NoCache is set)
|
|
||||||
[ -e "$cacheloc/matrix-send/access-token" ] && token=$(cat $cacheloc/matrix-send/access-token)
|
|
||||||
[ -e "$cacheloc/matrix-send/access-token" ] || GetAccessToken
|
|
||||||
CacheAccessToken
|
|
||||||
|
|
||||||
if [ -z "$2" ];
|
|
||||||
then error "Room ID not specified.";
|
|
||||||
else
|
else
|
||||||
shift $((OPTIND-1))
|
if rooms[room] ~= nil then
|
||||||
message="$1"
|
room_id = rooms[room]
|
||||||
roomid_input="$2"
|
else
|
||||||
if [ -z "$mtype" ]; then mtype="$defaultevent"; fi;
|
panic("alias '" .. room .. "' invalid")
|
||||||
if vargrep '!' "$roomid_input" -qo
|
end
|
||||||
then roomid="$(printf "$roomid_input" | sed 's/!//g')"
|
end
|
||||||
else roomid="$roomid_input"; fi
|
|
||||||
Send
|
if fileexists(cache.location .. "/txnid") then
|
||||||
fi
|
txnidf = io.open(cache.location .. "/txnid", 'r')
|
||||||
|
txnid = txnidf:read('l')
|
||||||
|
txnidf:close()
|
||||||
|
else
|
||||||
|
txnid = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
uri.login = string.format("https://%s/_matrix/client/v3/login", login.server)
|
||||||
|
|
||||||
|
-- Assemble various JSON messages
|
||||||
|
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
|
||||||
|
-- json.login should only be assigned if it needs to be assigned
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- Send the request!
|
||||||
|
local body = post(uri.login, json.login)
|
||||||
|
|
||||||
|
-- What else do I call it?
|
||||||
|
local t = cjson.decode(body)
|
||||||
|
|
||||||
|
-- If there was a server side error, print the error and exit.
|
||||||
|
if t.error then
|
||||||
|
panic(login.server .. ": " .. t.error)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Log the access token
|
||||||
|
login.token = t.access_token
|
||||||
|
|
||||||
|
-- Cache the access token
|
||||||
|
mkdir(cache.location)
|
||||||
|
cf = io.open(cache.location .. "/token", 'w+')
|
||||||
|
cf:write(login.token)
|
||||||
|
cf:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
uri.message = string.format("https://%s/_matrix/client/v3/rooms/%s/send/m.room.message/%d?access_token=%s",
|
||||||
|
login.server, room_id, txnid, login.token)
|
||||||
|
|
||||||
|
-- Send the message!
|
||||||
|
body = put(uri.message, json.message)
|
||||||
|
t = cjson.decode(body)
|
||||||
|
if t.error then
|
||||||
|
panic(login.server .. ": " .. t.error)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Increment txnid and cache
|
||||||
|
txnidf = io.open(cache.location .. "/txnid", 'w+')
|
||||||
|
txnidf:write(txnid + 1)
|
||||||
|
txnidf:close()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue