diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74ae39b --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Compiled Lua sources +luac.out +matrix-send + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex diff --git a/COMPILE b/COMPILE new file mode 100644 index 0000000..5caca11 --- /dev/null +++ b/COMPILE @@ -0,0 +1,35 @@ + ___________________ + < Manually building > + ------------------- + +Since matrix-send uses Lua, an interpreted programming language, +building is not necessary; you could just move matrix-send.lua to +/usr/local/bin and be done with it. However, for additional speed, +the Makefile precompiles it to Lua bytecode using +luac which is included +in the Lua distribution. To do this yourself, follow these steps: + + => Create the shebang + +In order to be run by a shell, the compiled bytecode needs a +shebang at the start. To do this, run this command: + + printf '#!/usr/bin/env lua\n' > matrix-send + + + => Precompile + +Now, let's append the bytecode to the file: + + luac -o - matrix-send.lua >> matrix-send + + + => Mark as an executable + +Finally, mark the file as executable: + + chmod +x matrix-send + +The file `matrix-send` is now ready to be run by a shell. + +# vi: ft=txt diff --git a/LICENSE b/LICENCE similarity index 100% rename from LICENSE rename to LICENCE diff --git a/Makefile b/Makefile index db545b9..a230233 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,21 @@ -VERSION = 1.0 +VERSION = 2.1 PREFIX = /usr/local MANPREFIX = ${PREFIX}/man config: - cp matrix-send.conf ~/.config/matrix-send.conf + mkdir -p ~/.config/matrix-send + cp config.lua ~/.config/matrix-send/config.lua install: + @# How I wish luarocks was better than this + luarocks install luaposix + luarocks install luasocket + luarocks install lua-cjson + printf '#!/usr/bin/env lua\n' > matrix-send + luac -o - matrix-send.lua >> matrix-send + chmod +x matrix-send cp -f matrix-send ${DESTDIR}${PREFIX}/bin - chmod +x ${DESTDIR}${PREFIX}/bin/matrix-send mkdir -p ${DESTDIR}${MANPREFIX}/man1 cp -f matrix-send.1 ${DESTDIR}${MANPREFIX}/man1 mkdir -p ${DESTDIR}${MANPREFIX}/man5 - cp -f matrix-send.conf.5 ${DESTDIR}${MANPREFIX}/man5 + cp -f matrix-send-config.5 ${DESTDIR}${MANPREFIX}/man5 diff --git a/README.md b/README.md index 3b5d4ec..27fdd89 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,51 @@ # matrix-send -A script that sends a message to a Matrix room. +A script that sends messages to Matrix rooms. [Read the manual page](https://jtbx.codeberg.page/man/matrix-send.1) --- -**Syntax:** `matrix-send message room` +matrix-send is a simple script that sends messages to Matrix rooms. -**Example:** `matrix-send "Hello world\!" \!aBcDeFgHiJkLmNoP:example.org` +It does this by directly making requests to the API of your Matrix server. +The supported event types are `m.text` and `m.notice`. ---- - -matrix-send is a simple script that sends a message to a Matrix room. - -It does this by sending a JSON message to your Matrix server. The message types that are supported are *m.text*, and *m.notice*. - -It is mainly designed for automation. I use it on a private Matrix room of mine. +It is mainly designed for automation. There aren't any plans for matrix-send to support encryption. ## Get started -In order to start using matrix-send, you need a config file. To copy the default configuration to .config, clone this repository and type `make config` as the desired user (not root). +In order to start using matrix-send, you need a config file. To copy the default configuration to .config, clone this repository and type `make config` as your user (not root). -Now edit the file ~/.config/matrix-send.conf. You will see three (uncommented) lines. +Now edit the file ~/.config/matrix-send/config.lua. You will first see the `login` table. -``` -Server ... -Username ... -Password ... +```lua +-- This is a comment +login = { + server = "matrix.org", + username = "user", + password = "password" +} ``` Delete the example values and enter in your credentials. -If you don't want to put your password in plain text, as matrix-send.conf is a shell script, you could do something like this: - -```shell -Password $(gpg -d /path/to/passwd.gpg) -``` - -/path/to/passwd.gpg is a GPG-encrypted file containing only your password, once decrypted. - -For more configuration options, see [matrix-send.conf(5)](https://jtbx.codeberg.page/man/matrix-send.conf.5). +For more configuration options, see [matrix-send-config(5)](https://jtbx.codeberg.page/man/matrix-send-config.5). --- +## Installation + +Before you install, you will need a few dependencies: + + * lua: >= 5.3 + * [luarocks](https://luarocks.org) + * plus some luarocks packages... + To install matrix-send, run `make install` as root, in the cloned repository's directory. +This will install all required packages, and copy matrix-send and its documentation to your system. In order to send messages, you will need the Room ID of your choice. Find the room you want to send to, and find its Room ID in the settings or somewhere else. It will likely be under Advanced or something similar. @@ -56,6 +55,33 @@ To send a message, type `matrix-send` followed by your message and then your Roo matrix-send 'Hi!' \!asdfasdfasdfasdf:matrix.org ``` -That will send the message "Hi!" to the Matrix room !asdfasdfasdfasdf:matrix.org. +That will send the message "Hi!" to the Matrix room `!asdfasdfasdfasdf:matrix.org.` -For more information, see [matrix-send(1)](https://jtbx.codeberg.page/man/matrix-send.1). +If you don't want to type the long room ID every time, you can add an alias. + +Open your configuration file, and create a `rooms` table like so: + +```lua +rooms = { +} +``` + +Now, you can add aliases. Say you wanted to add an alias called `lounge` for a room with room ID `!AbCdEfGhIjKlMn:matrix.org`. +You would make it this way: + +```lua +rooms = { + lounge = "!AbCdEfGhIjKlMn:matrix.org" +} +``` + +Multiple aliases are separated by commas (as with any Lua table): + +```lua +rooms = { + lounge = "!AbCdEfGhIjKlMn:matrix.org", -- <-- notice the comma + bathroom = "!OpQrStUvWxYz:matrix.org" +} +``` + +For more information, see [matrix-send(1)](https://jtbx.codeberg.page/man/matrix-send.1) and [matrix-send-config(5)](https://jtbx.codeberg.page/man/matrix-send-config.5). diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..87ce0c4 --- /dev/null +++ b/config.lua @@ -0,0 +1,31 @@ +login = { + -- The Matrix server to use. + server = "matrix.org", + + -- The user to log in to. + username = "user", + + -- The password for the user. + password = "password", + + -- 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. +} + +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. +} diff --git a/default_config.lua b/default_config.lua new file mode 100644 index 0000000..76cd492 --- /dev/null +++ b/default_config.lua @@ -0,0 +1,49 @@ +-- 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. + +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" +} diff --git a/matrix-send b/matrix-send deleted file mode 100755 index 0c15e98..0000000 --- a/matrix-send +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env sh -# matrix-send: send a message to a Matrix room - -version="1.0" - -########################### -#### Generic Functions #### -########################### - -error () { - printf "\033[31;1merror:\033[0m $1\n" - exit 1 -} - -conf_error () { - printf "\033[31;1mconfiguration error:\033[0m $1\n" - exit 2 -} - -vargrep () { - printf "$2\n" | grep "$1" $3 -} - -usage () { - printf "usage: matrix-send [-t type] [-c] [-h] [-V] message room\n" - exit 1 -} - -help () { - cat < "$cacheloc/matrix-send/access-token"; - fi -} - -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 -} - -Send () { - curl -s -XPOST -d "{"'"'"msgtype"'"'":"'"'"$mtype"'"'", "'"'"body"'"'":"'"'"$message"'"'"}" "https://$server/_matrix/client/r0/rooms/%21$roomid/send/m.room.message?access_token=$token" - #cat <. +]]-- + +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