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: Low-level `matrix.api` interface:
```lua ```lua
local matrix_api = require("matrix.api") local matrix = require("matrix")
local api = matrix_api("http://localhost:8080") local api = matrix.api("http://localhost:8080")
local response = api:register("m.login.password", local response = api:register("m.login.password",
{ user = "jdoe", password = "sup3rsecr1t" }) { user = "jdoe", password = "sup3rsecr1t" })
api.token = response.token api.token = response.token

View File

@@ -16,7 +16,11 @@ client:login_with_password(arg[2], arg[3])
local user = client:get_user() local user = client:get_user()
print("User ID: " .. user.user_id) 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() client:logout()

View File

@@ -6,33 +6,114 @@
-- Distributed under terms of the MIT license. -- 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 = {} local User = {}
User.__name = "matrix.user" User.__name = "matrix.user"
User.__index = User User.__index = User
setmetatable(User, { __call = function (self, client, user_id) 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 }) end })
function User:__tostring() function User:__tostring()
return self.__name .. "{" .. self.user_id .. "}" return self.__name .. "{" .. self.user_id .. "}"
end end
function User:get_display_name() function User:__eq(other)
return self._client._api:get_display_name(self.user_id) return getmetatable(other) == User and self.user_id == other.user_id
end end
function User:set_display_name(display_name) function User:_log(fmt, ...)
return self._client._api:set_display_name(self.user_id, display_name) self._client._log("{%s} " .. fmt, self.user_id, ...)
end end
function User:get_avatar_url() function User:update_display_name(value)
local mxc_url = self._client._api:get_avatar_url(self.user_id) if value and value ~= self.display_name then
return self._client._api:get_download_url(mxc_url) 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 end
function User:set_avatar_url(avatar_url) function User:update_avatar_url(value)
return self._client._api:set_avatar_url(self.user_id, avatar_url) 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 end
@@ -41,18 +122,27 @@ Room.__name = "matrix.room"
Room.__index = Room Room.__index = Room
setmetatable(Room, { __call = function (self, client, room_id) setmetatable(Room, { __call = function (self, client, room_id)
return setmetatable({ return eventable.object(setmetatable({
room_id = room_id, room_id = room_id,
aliases = {}, aliases = {},
events = {}, members = {},
invited = {},
_client = client, _client = client,
}, Room) }, Room))
end }) end })
function Room:__tostring() function Room:__tostring()
return self.__name .. "{" .. self.room_id .. "}" return self.__name .. "{" .. self.room_id .. "}"
end 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) function Room:send_text(text)
return self._client._api:send_message(self.room_id, text) return self._client._api:send_message(self.room_id, text)
end end
@@ -85,6 +175,7 @@ end
function Room:leave() function Room:leave()
-- XXX: Maybe this should use pcall()? -- XXX: Maybe this should use pcall()?
self:fire("leave")
self._client._api:leave_room(self.room_id) self._client._api:leave_room(self.room_id)
self._client.rooms[self.room_id] = nil self._client.rooms[self.room_id] = nil
end end
@@ -92,8 +183,7 @@ end
function Room:update_room_name() function Room:update_room_name()
local response = self._client._api:get_room_name(self.room_id) local response = self._client._api:get_room_name(self.room_id)
if response.name and response.name ~= self.name then if response.name and response.name ~= self.name then
self.name = response.name return set_simple_property(self, "name", response.name)
return true
end end
return false return false
end end
@@ -101,8 +191,7 @@ end
function Room:update_room_topic() function Room:update_room_topic()
local response = self._client._api:get_room_topic(self.room_id) local response = self._client._api:get_room_topic(self.room_id)
if response and response.topic ~= self.topic then if response and response.topic ~= self.topic then
self.topic = response.topic return set_simple_property(self, "topic", response.topic)
return true
end end
return false return false
end end
@@ -111,27 +200,109 @@ function Room:update_aliases()
local response = self._client._api:get_room_state(self.room_id) local response = self._client._api:get_room_state(self.room_id)
for _, chunk in ipairs(response) do for _, chunk in ipairs(response) do
if chunk.content and chunk.content.aliases then if chunk.content and chunk.content.aliases then
table.sort(chunk.content.aliases) return set_string_list_property(self, "aliases", chunk.content.aliases)
if sorted_string_list_eq(self.aliases, chunk.content.aliases) then
return false
end
self.aliases = chunk.content.aliases
return true
end end
end end
return false return false
end 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 = {} local Client = {}
Client.__name = "matrix.client" Client.__name = "matrix.client"
Client.__index = Client Client.__index = Client
setmetatable(Client, { __call = function (self, base_url, token, http_factory) setmetatable(Client, { __call = function (self, base_url, token, http_factory)
local c = setmetatable({ local c = eventable.object(setmetatable({
rooms = {}, -- Indexed by room_id presence = {}, -- Indexed by user_id
_api = require("matrix.api")(base_url, token, http_factory), rooms = {}, -- Indexed by room_id
}, Client) _log = get_debug_log_function(),
_api = API(base_url, token, http_factory),
}, Client))
-- Do an initial sync if a token was provided on construction. -- Do an initial sync if a token was provided on construction.
if token then if token then
c:_sync() c:_sync()
@@ -154,16 +325,20 @@ function Client:login_with_password(username, password, limit)
end end
function Client:_logged_in(response, limit) function Client:_logged_in(response, limit)
self._log("logged-in: %s", response.user_id)
self.user_id = response.user_id self.user_id = response.user_id
self.homeserver = response.home_server self.homeserver = response.home_server
self.token = response.access_token self.token = response.access_token
self._api.token = response.access_token self._api.token = response.access_token
self:_sync(limit) self:fire("logged-in")
self:_sync()
return self.token return self.token
end end
function Client:logout() function Client:logout()
return self._api:logout() local ret = self._api:logout()
self:fire("logged-out")
return ret
end end
function Client:get_user() function Client:get_user()
@@ -181,43 +356,75 @@ end
function Client:join_room(room_id_or_alias) function Client:join_room(room_id_or_alias)
local response = self._api: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 end
function Client:_make_room(room_id) function Client:_make_room(room_id)
assert(not self.rooms[room_id], "Room already exists")
local room = Room(self, room_id) local room = Room(self, room_id)
self.rooms[room_id] = room self.rooms[room_id] = room
return room return room
end end
function Client:_sync(limit) function Client:_make_user(user_id, display_name, avatar_url)
local response = self._api:initial_sync(limit) local user = self.presence[user_id]
self._end = response._end if not user then
for _, room in ipairs(response.rooms) do user = User(self, user_id)
local current_room = self:_make_room(room.room_id) self.presence[user_id] = user
for _, chunk in ipairs(room.messages.chunk) do end
table.insert(current_room.events, chunk) -- Set properties directly to avoid issues set_* API calls.
end set_simple_property(user, "display_name", display_name)
for _, state_event in ipairs(room.state) do set_simple_property(user, "avatar_url", avatar_url)
self:_process_state_event(state_event, current_room) 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 end
end end
function Client:_process_state_event(event, room) function Client:_sync_handle_room__join(room_id, data)
local event_type = event.type local room = self:_make_room(room_id)
if not event_type then room:_push_events(data.timeline.events)
return -- Ignore event self:fire("joined", room)
end end
if event_type == "m.room.name" then
room.name = event.content.name function Client:_sync_handle_room__invite(room_id, data)
elseif event_type == "m.room.topic" then local room = Room(self, room_id)
room.topic = event.content.topic room:_push_events(data.invite_state.events)
elseif event_type == "m.room.aliases" then self:fire("invite", room)
room.aliases = event.content.aliases end
table.sort(room.aliases)
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 end
return { room = Room, user = User, client = Client } return { room = Room, user = User, client = Client, api = API }