diff --git a/matrix-send b/matrix-send
index 3d5f606..4d6e5f6 100755
--- a/matrix-send
+++ b/matrix-send
@@ -1,220 +1,363 @@
-#!/usr/bin/env sh
-# matrix-send: send a message to a Matrix room
+#!/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 .
+]]--
-version="1.1"
+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"
-###########################
-#### Generic Functions ####
-###########################
+ version = "2.0"
+ 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")
-error () {
- printf "\033[31;1merror:\033[0m $1\n"
- exit 1
-}
+json = {}
+ uri = {}
+ sh = os.execute
-conf_error () {
- printf "\033[31;1mconfiguration error:\033[0m $1\n"
- exit 2
-}
+-----------------
+--- Functions ---
+-----------------
-warning () {
- printf "\033[93;1mwarning:\033[0m $1\n"
-}
-
-vargrep () {
- printf "$2\n" | grep "$1" $3
-}
-
-usage () {
- printf "usage: matrix-send [-t type] [-C config] [-chV] message room\n"
- exit 1
-}
-
-help () {
- cat < "$cacheloc/matrix-send/access-token";
- fi
-}
+ return false, err
+ end
+end
-ClearCache () {
- [ -e "$cacheloc/matrix-send/access-token" ] || printf "There is no cache to be cleared.\n"
- [ -e "$cacheloc/matrix-send/access-token" ] && rm -rf "$cacheloc/matrix-send/" && printf "Cleared cache\n"
- exit 0
-}
+-- Makes an HTTP POST request to uri and sends body. Returns the output of the HTTP server.
+function post(uri, body)
+ local respbody = {} -- for the response body
+ 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 () {
- if [ -z "$prettyprint" ]
- then curl -s -XPOST -d "{"'"'"msgtype"'"'":"'"'"$mtype"'"'", "'"'"body"'"'":"'"'"$message"'"'"}" "https://$server/_matrix/client/r0/rooms/%21$roomid/send/m.room.message?access_token=$token"
- 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
- fi
-}
+-- Makes an HTTP PUT request to uri and sends body. Returns the output of the HTTP server.
+function put(uri, body)
+ local respbody = {} -- for the response body
+ local result, respcode, respheaders, respstatus = http.request {
+ 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
-########################
-#### Initial checks ####
-########################
+-- 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
-[ -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
-CacheLocation "$HOME/.cache"
-while getopts :t:C:chV opt
-do
- case $opt in
- t)
- if vargrep "m\.(text|notice)" "$OPTARG" -Eq
- 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
+-- 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
-###############################
-#### 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
-if [ -e "$config" ];
-then . "$config"
+ login = {
+ -- The Matrix server to use.
+ 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
- warning "~/.config/matrix-send.conf doesn't exist; creating it"
- mkdir -p $HOME/.config
- touch $HOME/.config/matrix-send.conf
-fi
+ message = arg[optind+1]
+ room = arg[optind+2]
+end
-# Run checks for essential directives
-[ -z $server ] && conf_error "Server directive is not present"
-[ -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.";
+if room:match("^!") then
+ room_id = room
else
- shift $((OPTIND-1))
- message="$1"
- roomid_input="$2"
- if [ -z "$mtype" ]; then mtype="$defaultevent"; fi;
- if vargrep '!' "$roomid_input" -qo
- then roomid="$(printf "$roomid_input" | sed 's/!//g')"
- else roomid="$roomid_input"; fi
- Send
-fi
+ 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
+
+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()