From 6528727cff2e7f32d69103748275d199c03986f6 Mon Sep 17 00:00:00 2001 From: Adrian Perez de Castro Date: Thu, 23 Jun 2016 02:45:22 +0300 Subject: [PATCH] Initial import: low-level API wrapper --- matrix/api.lua | 269 +++++++++++++++++++++++++++++++++++++++ matrix/factory/chttp.lua | 70 ++++++++++ 2 files changed, 339 insertions(+) create mode 100644 matrix/api.lua create mode 100644 matrix/factory/chttp.lua diff --git a/matrix/api.lua b/matrix/api.lua new file mode 100644 index 0000000..e275a25 --- /dev/null +++ b/matrix/api.lua @@ -0,0 +1,269 @@ +#! /usr/bin/env lua +-- +-- api.lua +-- Copyright (C) 2016 Adrian Perez +-- +-- Distributed under terms of the MIT license. +-- + +local json = require "cjson" + +local function noprintf(...) end +local function eprintf(fmt, ...) + io.stderr:write("[http] ") + io.stderr:write(fmt:format(...)) + io.stderr:write("\n") + io.stderr:flush() +end + +local function get_debug_log_function() + local env_value = os.getenv("MATRIX_DEBUG_LOG") + if env_value and #env_value > 0 and env_value ~= "0" then + return eprintf + else + return noprintf + end +end + + +local API = {} +API.__index = API + +setmetatable(API, { __call = function (self, base_url, token, http_factory) + return setmetatable({ + base_url = base_url, + token = token, + txn_id = 0, + api_path = "/_matrix/client/r0", -- TODO: De-hardcode + _log = get_debug_log_function(), + _http = require("matrix.factory." .. (http_factory or "chttp"))(), + }, API) +end }) + +function API:_quote(string) + return self._http:quote(string) +end + +function API:initial_sync(limit) + return self:_send("GET", "/initialSync", { limit = limit or 1 }) +end + +function API:register(login_type, params) + return self:_send_with_params("POST", "/register", nil, + { type = login_type }, params) +end + +function API:login(login_type, params) + return self:_send_with_params("POST", "/login", nil, + { type = login_type }, params) +end + +function API:logout() + return self:_send("POST", "/logout") +end + +function API:refresh_token(refresh_token) + return self:_send("POST", "/tokenrefresh", nil, { refresh_token = refresh_token }) +end + +function API:set_password(new_password, params) + return self:_send_with_params("POST", "/account/password", + { new_password = new_password }, params) +end + +function API:get_3pids() + local data = self:_send("GET", "/account/3pid") + return data.threepids +end + +function API:set_3pids(threepids, bind) + return self:_send("POST", "/account/3pid", nil, + { three_pid_creds = threepids, bind = (bind and true or false) }) +end + +---- +-- | Option | Type | Default Value | +-- |:==========|:=========|===============| +-- | alias | string | nil | +-- | public | boolean | false | +-- | invite | {string} | {} | +---- +function API:create_room(options) + local params = { + visibility = options.public and "public" or "private", + room_alias_name = options.alias, + invite = options.invite, + } + return self:_send("POST", "/createRoom", nil, params) +end + +function API:join_room(room_id_or_alias) + return self:_send("POST", "/join/" .. self:_quote(room_id_or_alias)) +end + +function API:event_stream(from_token, timeout) + return self:_send("GET", "/events", { from = from_token, timeout = timeout or 30000 }) +end + +function API:send_state_event(room_id, event_type, content, state_key) + local path = "/rooms/" .. self:_quote(room_id) .. "/state/" .. self:_quote(event_type) + if state_key then + path = path .. "/" .. self:_quote(state_key) + end + return self:_send("PUT", path, nil, content) +end + +function API:send_message_event(room_id, event_type, content, txn_id) + if not txn_id then + txn_id = self.txn_id + self.txn_id = self.txn_id + 1 + end + local path = "/rooms/" .. self:_quote(room_id) .. "/send/" .. + self:_quote(event_type) .. "/" .. self:_quote(tostring(txn_id)) + return self:_send("PUT", path, nil, content) +end + +function API:send_content(room_id, item_url, item_name, msg_type, extra_info) + return self:send_message_event(room_id, "m.room.message", + { url = item_url, msgtype = msg_type, body = item_name, info = extra_info }) +end + +function API:send_message(room_id, text_content, msg_type) + return self:send_message_event(room_id, "m.room.message", + self:get_text_body(text_content, msg_type or "m.text")) +end + +function API:send_emote(room_id, text_content) + return self:send_message_event(room_id, "m.room.message", + self:get_emote_body(text_content)) +end + +function API:send_notice(room_id, text_content) + return self:send_message_event(room_id, "m.room.message", + { msgtype = "m.notice", body = text_content }) +end + +function API:get_room_name(room_id) + return self:_send("GET", "/rooms/" .. self:_quote(room_id) .. "/state/m.room.name") +end + +function API:get_room_topic(room_id) + return self:_send("GET", "/rooms/" .. self:_quote(room_id) .. "/state/m.room.topic") +end + +function API:leave_room(room_id) + return self:_send("POST", "/rooms/" .. self:_quote(room_id) .. "/leave") +end + +function API:invite_user(room_id, user_id) + return self:_send("POST", "/rooms/" .. self:_quote(room_id) .. "/invite", nil, + { user_id = user_id }) +end + +function API:kick_user(room_id, user_id, reason) + return self:set_membership(room_id, user_id, "leave", reason) +end + +function API:set_membership(room_id, user_id, membership, reason) + local path = "/rooms/" .. self:_quote(room_id) .. "/state/m.room.member/" .. self:_quote(user_id) + return self:_send("PUT", path, nil, { membership = membership, reason = reason or "" }) +end + +function API:ban_user(room_id, user_id, reason) + return self:_send("POST", "/rooms/" .. self:_quote(room_id) .. "/ban", nil, + { user_id = user_id, reason = reason or "" }) +end + +function API:get_room_state(room_id) + return self:_send("GET", "/rooms/" .. self:_quote(room_id) .. "/state") +end + +function API:get_text_body(text, msg_type) + return { msgtype = msg_type or "m.text", body = text } +end + +function API:get_emote_body(text) + return { msgtype = "m.emote", body = text } +end + +function API:media_upload(content, content_type) + -- TODO: De-harcode media API path + return self:_send("POST", "", nil, content, + { ["content-type"] = content_type }, + "/_matrix/media/r0/upload") +end + +function API:get_display_name(user_id) + local data = self:_send("GET", "/profile/" .. self:_quote(user_id) .. "/displayname") + return data.displayname +end + +function API:set_display_name(user_id, display_name) + return self:_send("PUT", "/profile/" .. self:_quote(user_id) .. "/displayname", + nil, { displayname = display_name }) +end + +function API:get_avatar_url(user_id) + local data = self:_send("GET", "/profile/" .. self:_quote(user_id) .. "/avatar_url") + return data.avatar_url +end + +function API:set_avatar_url(user_id, avatar_url) + return self:_send("PUT", "/profile/" .. self:_quote(user_id) .. "/avatar_url", + nil, { avatar_url = avatar_url }) +end + +function API:get_download_url(mxc_url) + if mxc_url:sub(1, #"mxc://") == "mxc://" then + -- TODO: De-hardcode API version + return self.base_url .. "/_matrix/media/r0/download/" .. mxc_url:sub(7) + end + error("no mxc: scheme in URL: " .. mxc_url) +end + +function API:_send_with_params(method, path, query_args, params, extra_params) + for name, value in pairs(extra_params) do + params[name] = value + end + return self:_send(method, path, query_args, params) +end + +function API:_send(method, path, query_args, body, headers, api_path) + -- Ensure that there is a Content-Type header. + if not headers then + headers = {} + end + if not headers["content-type"] then + headers["content-type"] = "application/json" + end + + -- Encode the request body, if necessary. + if headers["content-type"] == "application/json" then + body = body and json.encode(body) or "{}" + elseif not body then + body = "" + end + + -- Copy the parameters, adding the access token. + local params = { access_token = self.token } + if query_args then + for name, value in pairs(query_args) do + params[name] = tostring(value) + end + end + + -- Call the HTTP library. + local code, headers, body = self._http:request(self._log, method:upper(), + self.base_url .. (api_path or self.api_path) .. path, params, body, headers) + if code == 200 then + if headers["content-type"] == "application/json" then + body = json.decode(body) + end + return body + else + return error("HTTP " .. code .. " - " .. body) + end +end + + +return API diff --git a/matrix/factory/chttp.lua b/matrix/factory/chttp.lua new file mode 100644 index 0000000..fd1c462 --- /dev/null +++ b/matrix/factory/chttp.lua @@ -0,0 +1,70 @@ +#! /usr/bin/env lua +-- +-- chttp.lua +-- Copyright (C) 2016 Adrian Perez +-- +-- Distributed under terms of the MIT license. +-- + +local request = require "http.request" +local headers = require "http.headers" +local util = require "http.util" + +local encodeURI, decodeURI = util.encodeURI, util.decodeURI +local dict_to_query = util.dict_to_query + + +local CqHttpClient = { + quote = function (self, text) return encodeURI(text) end, + unquote = function (self, text) return decodeURI(text) end, +} +CqHttpClient.__index = CqHttpClient + + +local function headers_to_dict(h) + local headers = {} + for name, value in pairs(h) do + if name:sub(1, 1) ~= ":" then + headers[name] = value + end + end + return headers +end + + +function CqHttpClient:request(log, method, url, query_args, body, headers) + do + local qs = dict_to_query(query_args) + if #qs > 0 then + url = url .. "?" .. qs + end + end + + log(">~> %s %s", method, url) + log(">>> %s", body) + + local req = request.new_from_uri(url) + for name, value in pairs(headers) do + req.headers:append(name, value) + end + req.headers:upsert(":method", method) + if body then + req:set_body(body) + end + local h, s = req:go() + if not h then + log("