Use eventables in matrix.client, implement a chunk of functionality

Yeah, I know: commits should have been smaller and all that. Yadda-yadda.
This commit is contained in:
Adrian Perez de Castro
2016-07-01 05:05:50 +03:00
parent fc99710658
commit e128366f7f
3 changed files with 269 additions and 58 deletions

View File

@@ -51,8 +51,8 @@ room:send_text("Hello!")
Low-level `matrix.api` interface:
```lua
local matrix_api = require("matrix.api")
local api = matrix_api("http://localhost:8080")
local matrix = require("matrix")
local api = matrix.api("http://localhost:8080")
local response = api:register("m.login.password",
{ user = "jdoe", password = "sup3rsecr1t" })
api.token = response.token

View File

@@ -16,7 +16,11 @@ client:login_with_password(arg[2], arg[3])
local user = client:get_user()
print("User ID: " .. user.user_id)
print("Display name: " .. user:get_display_name())
print("Avatar URL: " .. user:get_avatar_url())
user:hook("property-changed", function (user, property)
print(" - " .. property .. ": " .. tostring(user[property]))
end)
user:update_display_name()
user:update_avatar_url()
client:logout()

View File

@@ -6,33 +6,114 @@
-- Distributed under terms of the MIT license.
--
local json = require "cjson"
local API = require "matrix.api"
local eventable = require "matrix.eventable"
local function noprintf(...) end
local function eprintf(fmt, ...)
io.stderr:write("[client] ")
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_CLIENT_DEBUG_LOG")
if env_value and #env_value > 0 and env_value ~= "0" then
return eprintf
else
return noprintf
end
end
local function sanitize(text)
return (text:gsub("[^0-9a-zA-Z_]", "__"))
end
local function sorted_string_list_eq(a, b)
if #a == #b then
for i = 1, #a do
if a[i] ~= b[i] then
return false
end
end
return true
else
return false
end
end
local function set_simple_property(self, name, new_value)
local old_value = self[name]
if old_value == new_value then
self:_log(".%s: %s (unchanged)", name, old_value)
return false
else
self[name] = new_value
self:_log(".%s: %s -> %s", name, old_value, new_value)
self:fire("property-changed", name, old_value)
return true
end
end
local function set_string_list_property(self, name, new_value)
local old_value = self[name]
table.sort(old_value)
table.sort(new_value)
if sorted_string_list_eq(old_value, new_value) then
self:_log(".%s: [%s] (unachanged)", table.concat(old_value, ", "))
return false
else
self[name] = new_value
self:fire("property-changed", name, old_value, new_value)
self:_log(".%s: [%s] -> [%s]", name, table.concat(old_value, ", "),
table.concat(new_value, ", "))
return true
end
end
local User = {}
User.__name = "matrix.user"
User.__index = User
setmetatable(User, { __call = function (self, client, user_id)
return setmetatable({ user_id = user_id, _client = client }, User)
return eventable.object(setmetatable({
user_id = user_id,
_client = client,
}, User))
end })
function User:__tostring()
return self.__name .. "{" .. self.user_id .. "}"
end
function User:get_display_name()
return self._client._api:get_display_name(self.user_id)
function User:__eq(other)
return getmetatable(other) == User and self.user_id == other.user_id
end
function User:set_display_name(display_name)
return self._client._api:set_display_name(self.user_id, display_name)
function User:_log(fmt, ...)
self._client._log("{%s} " .. fmt, self.user_id, ...)
end
function User:get_avatar_url()
local mxc_url = self._client._api:get_avatar_url(self.user_id)
return self._client._api:get_download_url(mxc_url)
function User:update_display_name(value)
if value and value ~= self.display_name then
self._client._api:set_display_name(self.user_id, value)
elseif not value then
value = self._client._api:get_display_name(self.user_id)
end
return set_simple_property(self, "display_name", value)
end
function User:set_avatar_url(avatar_url)
return self._client._api:set_avatar_url(self.user_id, avatar_url)
function User:update_avatar_url(value)
if value and value ~= self.avatar_url then
self._client._api:set_avatar_url(self.user_id, value)
elseif not value then
value = self._client._api:get_avatar_url(self.user_id)
end
return set_simple_property(self, "avatar_url", value)
end
@@ -41,18 +122,27 @@ Room.__name = "matrix.room"
Room.__index = Room
setmetatable(Room, { __call = function (self, client, room_id)
return setmetatable({
return eventable.object(setmetatable({
room_id = room_id,
aliases = {},
events = {},
members = {},
invited = {},
_client = client,
}, Room)
}, Room))
end })
function Room:__tostring()
return self.__name .. "{" .. self.room_id .. "}"
end
function Room:__eq(other)
return getmetatable(other) == Room and self.room_id == other.room_id
end
function Room:_log(fmt, ...)
self._client._log("{%s} " .. fmt, self.room_id, ...)
end
function Room:send_text(text)
return self._client._api:send_message(self.room_id, text)
end
@@ -85,6 +175,7 @@ end
function Room:leave()
-- XXX: Maybe this should use pcall()?
self:fire("leave")
self._client._api:leave_room(self.room_id)
self._client.rooms[self.room_id] = nil
end
@@ -92,8 +183,7 @@ end
function Room:update_room_name()
local response = self._client._api:get_room_name(self.room_id)
if response.name and response.name ~= self.name then
self.name = response.name
return true
return set_simple_property(self, "name", response.name)
end
return false
end
@@ -101,8 +191,7 @@ end
function Room:update_room_topic()
local response = self._client._api:get_room_topic(self.room_id)
if response and response.topic ~= self.topic then
self.topic = response.topic
return true
return set_simple_property(self, "topic", response.topic)
end
return false
end
@@ -111,27 +200,109 @@ function Room:update_aliases()
local response = self._client._api:get_room_state(self.room_id)
for _, chunk in ipairs(response) do
if chunk.content and chunk.content.aliases then
table.sort(chunk.content.aliases)
if sorted_string_list_eq(self.aliases, chunk.content.aliases) then
return false
end
self.aliases = chunk.content.aliases
return true
return set_string_list_property(self, "aliases", chunk.content.aliases)
end
end
return false
end
local make_unimplemented_handler = function (self, event)
local env_value = os.getenv("MATRIX_CLIENT_LOG_UNHANDLED_EVENTS")
if env_value and #env_value > 0 and env_value ~= "0" then
local function handler(self, event)
self:_log("unhandled '%s' event: %s", event.type, json.encode(event))
end
make_unimplemented_handler = function (self, event)
return handler
end
else
local function handler(self, event) end
make_unimplemented_handler = function (self, event)
self:_log("no handler for '%s' events (this warning is shown only once)", event.type)
return handler
end
end
return make_unimplemented_handler(self, event)
end
function Room:_push_events(events)
self:_log("processing %d timeline events", #events)
for _, event in ipairs(events) do
local handler_name = "_push_event__" .. sanitize(event.type)
local handler = self[handler_name]
if not handler then
handler = make_unimplemented_handler(self, event)
self[handler_name] = handler
end
handler(self, event)
end
end
function Room:_push_event__m__room__create(event)
set_simple_property(self, "creator", event.content.creator)
end
function Room:_push_event__m__room__aliases(event)
set_string_list_property(self, "aliases", event.content.aliases)
end
function Room:_push_event__m__room__canonical_alias(event)
set_simple_property(self, "canonical_alias", event.content.alias)
end
function Room:_push_event__m__room__name(event)
set_simple_property(self, "name", event.content.name)
end
function Room:_push_event__m__room__join_rules(event)
set_simple_property(self, "join_rule", event.content.join_rule)
end
function Room:_push_event__m__room__history_visibility(event)
set_simple_property(self, "history_visibility", event.content.history_visibility)
end
function Room:_push_event__m__room__member(event)
if event.content.membership == "join" then
local user = self._client:_make_user(event.state_key,
event.content.displayname, event.content.avatar_url)
self.members[user.user_id] = user
self:fire("member-joined", user)
elseif event.content.membership == "invite" then
local user = self._client:_make_user(event.state_key,
event.content.displayname, event.content.avatar_url)
-- FIXME: Setting property from outside the User object itself.
set_simple_property(user, "invited_by", event.sender)
self.invited[user.user_id] = user
self:fire("member-invited", user)
elseif event.content.membership == "leave" then
local user = self.members[event.state_key] or self.invited[event.state_key]
if user then
if user.invited_by then
self.invited[user.user_id] = nil
else
self.members[user.user_id] = nil
end
self:fire("member-left", user)
-- TODO: Do we remove the user from self._client.presence??
end
else
error("Unhandled event: " .. json.encode(event))
end
end
local Client = {}
Client.__name = "matrix.client"
Client.__index = Client
setmetatable(Client, { __call = function (self, base_url, token, http_factory)
local c = setmetatable({
rooms = {}, -- Indexed by room_id
_api = require("matrix.api")(base_url, token, http_factory),
}, Client)
local c = eventable.object(setmetatable({
presence = {}, -- Indexed by user_id
rooms = {}, -- Indexed by room_id
_log = get_debug_log_function(),
_api = API(base_url, token, http_factory),
}, Client))
-- Do an initial sync if a token was provided on construction.
if token then
c:_sync()
@@ -154,16 +325,20 @@ function Client:login_with_password(username, password, limit)
end
function Client:_logged_in(response, limit)
self._log("logged-in: %s", response.user_id)
self.user_id = response.user_id
self.homeserver = response.home_server
self.token = response.access_token
self._api.token = response.access_token
self:_sync(limit)
self:fire("logged-in")
self:_sync()
return self.token
end
function Client:logout()
return self._api:logout()
local ret = self._api:logout()
self:fire("logged-out")
return ret
end
function Client:get_user()
@@ -181,43 +356,75 @@ end
function Client:join_room(room_id_or_alias)
local response = self._api:join_room(room_id_or_alias)
return self:_make_room(response.room_id or room_id_or_alias)
local room = self:_make_room(response.room_id or room_id_or_alias)
-- XXX: At this point we might have joined the room, but its state has not
-- been synced. Maybe firing the "joined" event should be delayed
-- until the next sync (or when the corresponding events come in).
self:fire("joined", room)
return room
end
function Client:_make_room(room_id)
assert(not self.rooms[room_id], "Room already exists")
local room = Room(self, room_id)
self.rooms[room_id] = room
return room
end
function Client:_sync(limit)
local response = self._api:initial_sync(limit)
self._end = response._end
for _, room in ipairs(response.rooms) do
local current_room = self:_make_room(room.room_id)
for _, chunk in ipairs(room.messages.chunk) do
table.insert(current_room.events, chunk)
end
for _, state_event in ipairs(room.state) do
self:_process_state_event(state_event, current_room)
function Client:_make_user(user_id, display_name, avatar_url)
local user = self.presence[user_id]
if not user then
user = User(self, user_id)
self.presence[user_id] = user
end
-- Set properties directly to avoid issues set_* API calls.
set_simple_property(user, "display_name", display_name)
set_simple_property(user, "avatar_url", avatar_url)
return user
end
function Client:_sync(options)
if not options then
options = {}
end
options.since = self._sync_next_batch
local response = self._api:sync(options)
self._sync_next_batch = response.next_batch
for _, kind in ipairs { "join", "invite", "leave" } do
local handle = self["_sync_handle_room__" .. kind]
for room_id, room_data in pairs(response.rooms[kind]) do
self._log("sync: %s %s", kind, room_id)
-- XXX: Maybe this is abusing pcall() too much to allow handler
-- code to bail and continue with the next room instead of
-- completely failing to sync. Dunno.
local ok, err = pcall(handle, self, room_id, room_data)
if not ok then
self._log("sync: Error handling '%s' event for room %s:\n%s", kind, room_id, err)
self._log("sync: Event payload: %s", json.encode(room_data))
end
end
end
end
function Client:_process_state_event(event, room)
local event_type = event.type
if not event_type then
return -- Ignore event
end
if event_type == "m.room.name" then
room.name = event.content.name
elseif event_type == "m.room.topic" then
room.topic = event.content.topic
elseif event_type == "m.room.aliases" then
room.aliases = event.content.aliases
table.sort(room.aliases)
end
function Client:_sync_handle_room__join(room_id, data)
local room = self:_make_room(room_id)
room:_push_events(data.timeline.events)
self:fire("joined", room)
end
function Client:_sync_handle_room__invite(room_id, data)
local room = Room(self, room_id)
room:_push_events(data.invite_state.events)
self:fire("invite", room)
end
function Client:_sync_handle_room__leave(room_id, data)
local room = assert(self.rooms[room_id], "No such room")
room:_push_timeline_events(data.timeline)
room:leave()
end
return { room = Room, user = User, client = Client }
return { room = Room, user = User, client = Client, api = API }