diff --git a/scenes/App.gd b/scenes/App.gd index 2ae4d4b..bad6c2c 100644 --- a/scenes/App.gd +++ b/scenes/App.gd @@ -5,7 +5,7 @@ var _connection onready var n_Connection := get_node("Connections") func _ready(): - _connection = n_Connection.establish_new_connection("poto.cafe", "veclavtalica", "-") + _connection = n_Connection.establish_new_connection("poto.cafe", "veclavtalica", "dynamite-onlooker3-snowman") if _connection == null: push_error("Connection failed") return @@ -18,7 +18,7 @@ func _ready(): push_error("Service builder errored out: %s" % [service_request.value]) return - for service in service_request.value.value: + for service in service_request.value: if service.muc == null: continue var rooms_request = service.muc.request_rooms() @@ -29,5 +29,5 @@ func _ready(): push_error("Room request errored out: %s" % [rooms_request.value]) return - for room in rooms_request.value.value: - print(room.as_string()) + for room in rooms_request.value: + var me = service.muc.join_room(room, "tochie-facade") diff --git a/scenes/Connections.gd b/scenes/Connections.gd index f5b80d3..e12a4d1 100644 --- a/scenes/Connections.gd +++ b/scenes/Connections.gd @@ -1,6 +1,8 @@ extends Node class_name Connections +# todo: Settle on whether connection should send Sums.Result or receiver should check stanza errors by itself. + class Connection extends Reference: var stream: StreamPeer var identity: String @@ -10,7 +12,7 @@ class Connection extends Reference: var _id_counter: int = 0 var _pending_iqs: Dictionary # of id to GDScriptFunctionState - var _pending_tail_iqs: Array # of GDScriptFunctionState + var _presence_sinks: Dictionary # of Jid to [[WeakRef, String]] var _xml_parser := Xml.Parser.new() # todo: Route signals to particular receivers, based on 'from' or 'to' @@ -26,16 +28,16 @@ class Connection extends Reference: self._id_counter += 1 return hash(self._id_counter) - func promise_iq(to: String, action: String, payload: String, capture: GDScriptFunctionState): - assert(action in ["get", "set"]) + func promise_iq(to: String, type: String, payload: String, capture: GDScriptFunctionState) -> Sums.Promise: + assert(type in ["get", "set"]) var id := self.generate_id() if self.stream.put_data( - """{payload}""".format({ + """{payload}""".format({ "from": self.jid.xml_escape(), - "id": id, + "id": String(id).xml_escape(), "to": to.xml_escape(), - "action": action, + "type": type, "payload": payload }).to_utf8()) != OK: return Sums.Promise.make_error(ERR_CONNECTION_ERROR) @@ -45,6 +47,22 @@ class Connection extends Reference: return Sums.Promise.from(capture) + func push_presence(to: String, type, payload: String) -> Sums.Result: + assert(type == null or type is String) + + var message = Stanza.form_presence( + String(self.generate_id()), + self.jid, to, type, payload) + + if self.stream.put_data(message.to_utf8()) != OK: + return Sums.Result.make_error(ERR_CONNECTION_ERROR) + + return Sums.Result.make_value(null) + + func presence_sink(p_base_jid: String, p_sink: Object, p_signal: String) -> void: + self._presence_sinks[p_base_jid] = \ + self._presence_sinks.get(p_base_jid, []) + [[weakref(p_sink), p_signal]] + ## Registry of connections used for poking of pending iqs. var _connections: Array # of WeakRef to Connection @@ -73,21 +91,21 @@ func _process_connections() -> void: response = null var stanza := connection._xml_parser.take_root() + print(stanza.as_string()) + if stanza.name == "iq": if "to" in stanza.attributes and stanza.attributes["to"] != connection.jid: + # todo: Server errors should not be raised in client. push_error("Stanza is not addressed to assigned jid") if stanza.attributes["type"] in ["result", "error"]: var id := int(stanza.attributes["id"]) # todo: Use strings directly instead? - var result = connection._pending_iqs[id].resume(Sums.Result.make_value(stanza)) - if result is GDScriptFunctionState and result.is_valid(): - connection._pending_tail_iqs.push_back(result) - # elif result != null: - # assert(false, "Ignored result of iq subroutine: " + "%s" % result) + connection._pending_iqs[id].resume(Sums.Result.make_value(stanza)) var was_present := connection._pending_iqs.erase(id) assert(was_present) elif stanza.attributes["type"] in ["set", "get"]: + # todo: Emit in any way? connection.emit_signal("iq_received", stanza) elif stanza.name == "message": @@ -96,17 +114,12 @@ func _process_connections() -> void: elif stanza.name == "presence": connection.emit_signal("presence_received", stanza) - var to_remove_tails := PoolIntArray() - for tail_idx in range(connection._pending_tail_iqs.size()): - var result = connection._pending_tail_iqs[tail_idx].resume() - if not result is GDScriptFunctionState or not result.is_valid(): - # assert(result == null, "Ignored result of iq subroutine: " + "%s" % result) - to_remove_tails.push_back(tail_idx) - else: - connection._pending_tail_iqs[tail_idx] = result + for base_jid in connection._presence_sinks.keys(): + if not stanza.attributes["from"].begins_with(base_jid): + continue - for tail_idx in range(to_remove_tails.size() - 1, 0, -1): - connection._pending_tail_iqs.remove(to_remove_tails[tail_idx]) + for to_emit in connection._presence_sinks[base_jid]: + to_emit[0].emit_signal(to_emit[1], stanza) ## Collect dropped connections. for idx in range(to_remove.size() - 1, 0, -1): diff --git a/scenes/MucService.gd b/scenes/MucService.gd index b19bb6b..ab6bf66 100644 --- a/scenes/MucService.gd +++ b/scenes/MucService.gd @@ -6,10 +6,40 @@ class_name MucService class MucRoom extends Reference: var jid: String var name: String + var members: Dictionary # nick to MucMember + + signal presence_received(presence) + + func _init() -> void: + if self.connect("presence_received", self, "_presence_received") != OK: + assert(false) + + func _presence_received(presence) -> void: + if presence.children.size() == 0: + return + var x = presence.get_named_child_element("x") + if x.name != "x" and x.attributes["xmlns"] != "http://jabber.org/protocol/muc#user": + ## Member information came. + var item = x.get_named_child_element("item") + var nick = presence.attributes["from"].rsplit("/").pop_front() + var member = MucMember.new() + member.jid = presence.attributes["from"] + member.nick = nick + member.role = item.attributes["role"] + member.affiliation = item.attributes["affiliation"] + # member.room = self + members[nick] = member func as_string() -> String: return "MucRoom \"%s\", Jid: \"%s\"" % [name, jid] +class MucMember extends Reference: + var jid: String + var nick: String + var role: String + # var room: MucRoom + var affiliation: String + var jid: String var _connection: Connections.Connection @@ -41,7 +71,7 @@ func request_rooms() -> Sums.Promise: return self._rooms_promise -func _iq_rooms(): +func _iq_rooms() -> Sums.Result: var response = yield() if not response.is_ok: return response @@ -54,8 +84,42 @@ func _iq_rooms(): for item in query.value.children: if item.is_element() and item.name == "item": var muc_room := MucRoom.new() + _connection.presence_sink(muc_room.jid, muc_room, "presence_received") muc_room.jid = item.attributes["jid"] muc_room.name = item.attributes["name"] rooms.push_back(muc_room) return Sums.Result.make_value(rooms) + +## Returns MucMemeber that is registered on behalf on user. +func join_room(room: MucRoom, nick: String) -> Sums.Promise: + # todo: https://xmpp.org/extensions/xep-0045.html#reservednick + return Sums.Promise.from(_join_room(room, nick)) + +func _join_room(room: MucRoom, nick: String) -> Sums.Result: + var member_jid = room.jid + '/' + nick + var result = _connection.push_presence(member_jid, null, + "") + if not result.is_ok: + return result + + var response = Stanza.presence_result(yield(room, "presence_received")) + if not response.is_ok: + return response + + while true: + response = Stanza.presence_result(yield(room, "presence_received")) + if not response.is_ok: + return response + + if response.value.attributes["from"] == member_jid: + if response.value.children.size() == 0: + continue + var x = response.value.children[0] + if x.name != "x" and x.attributes["xmlns"] != "http://jabber.org/protocol/muc#user": + continue + for child in x.children: + if child.is_element() and child.name == "status" and child.attrbiutes["code"] == "110": + return Sums.Result.make_value(room.members[nick]) + + return Sums.Result.make_error(null) diff --git a/scenes/PolyServiceBuilder.gd b/scenes/PolyServiceBuilder.gd index 5ecf669..54bc7d8 100644 --- a/scenes/PolyServiceBuilder.gd +++ b/scenes/PolyServiceBuilder.gd @@ -17,8 +17,7 @@ func _service_discovery(connection: Connections.Connection) -> Sums.Result: if not query.is_ok: return query.value - var feature_promises := Array() - var poly_services := Dictionary() # of Jid to PolyService + var poly_services := Array() for item in query.value.children: if not item.is_element() or item.name != "item": continue @@ -27,30 +26,28 @@ func _service_discovery(connection: Connections.Connection) -> Sums.Result: poly_service.jid = item.attributes["jid"] if "name" in item.attributes: poly_service.name = item.attributes["name"] - poly_services[item.attributes["jid"]] = poly_service - feature_promises.push_back(connection.promise_iq( + poly_services.push_back(poly_service) + + var feature_promise = connection.promise_iq( item.attributes["jid"], "get", Stanza.disco_info_queury, - Stanza.yield_as_is())) + Stanza.yield_as_is()) - while not Sums.are_promises_done(feature_promises): - yield() + if not feature_promise.is_done: + yield(feature_promise, "done") - # todo: Allow partial success. - if not Sums.are_promises_ok(feature_promises): - return Sums.Result.make_error(Sums.collect_promise_errors(feature_promises)) + # todo: Propagate error if it's unrelated to service discovery itself. + if feature_promise.is_ok: + # If features arrived, - populate services. + var jid = feature_promise.value.attributes["from"] - for feature_response in Sums.collect_promise_values(feature_promises): - var jid = feature_response.attributes["from"] + var features := Stanza.unwrap_query_result(feature_promise.value) + if not features.is_ok: + # todo: Propagate the error. + continue - var features := Stanza.unwrap_query_result(feature_response) - if not features.is_ok: - # todo: Signal the error. - continue + poly_service.muc = load("res://scenes/MucService.gd").new().try_init( + connection, jid, features.value) - var poly_service = poly_services[jid] - poly_service.muc = load("res://scenes/MucService.gd").new().try_init( - connection, jid, features.value) - - return Sums.Result.make_value(poly_services.values()) + return Sums.Result.make_value(poly_services) diff --git a/scenes/Stanza.gd b/scenes/Stanza.gd index ee2c3df..7be1857 100644 --- a/scenes/Stanza.gd +++ b/scenes/Stanza.gd @@ -20,3 +20,26 @@ static func unwrap_query_result(iq: Xml.XmlElement) -> Sums.Result: return Sums.Result.make_value(child) return Sums.Result.make_error(ERR_INVALID_DATA) + +static func form_presence(id: String, from: String, to: String, type, payload: String) -> String: + if type != null: + return """{payload}""".format({ + "from": from.xml_escape(), + "id": id.xml_escape(), + "to": to.xml_escape(), + "type": type, + "payload": payload + }) + else: + return """{payload}""".format({ + "from": from.xml_escape(), + "id": id.xml_escape(), + "to": to.xml_escape(), + "payload": payload + }) + +static func presence_result(stanza: Xml.XmlElement) -> Sums.Result: + if "type" in stanza.attributes and stanza.attributes["type"] == "error": + return Sums.Result.make_error(stanza) + else: + return Sums.Result.make_value(stanza) diff --git a/scenes/Sums.gd b/scenes/Sums.gd index bb2f70e..111fa82 100644 --- a/scenes/Sums.gd +++ b/scenes/Sums.gd @@ -41,14 +41,15 @@ class Promise extends Reference: var result := Promise.new() result.is_done = true result.is_ok = false + # todo: Proper error object / convention. result.value = [error, get_stack().slice(1, -1)] return result - # todo: Use Error class value for erroneous state instead. - func _result_arrived(p_result) -> void: + func _result_arrived(p_result: Result) -> void: + assert(p_result != null) self.is_done = true - self.is_ok = true - self.value = p_result + self.is_ok = p_result.is_ok + self.value = p_result.value emit_signal("done") static func are_promises_done(promises: Array) -> bool: @@ -71,11 +72,7 @@ static func collect_promise_values(promises: Array) -> Array: for promise in promises: assert(promise is Promise) assert(promise.is_done) - # todo: Don't do this. - if promise.value is Result: - assert(promise.value.is_ok) - result.push_back(promise.value.value) - else: result.push_back(promise.value) + result.push_back(promise.value) return result static func collect_promise_errors(promises: Array) -> Array: @@ -85,7 +82,4 @@ static func collect_promise_errors(promises: Array) -> Array: assert(promise.is_done) if not promise.is_ok: result.push_back(promise.value) - # todo: Don't do this. - elif promise.value is Result and not promise.value.is_ok: - result.push_back(promise.value.value) return result diff --git a/scenes/Xml.gd b/scenes/Xml.gd index 75f85c2..6974b33 100644 --- a/scenes/Xml.gd +++ b/scenes/Xml.gd @@ -12,6 +12,13 @@ class XmlElement extends XmlNode: func is_element() -> bool: return true + func get_named_child_element(p_name: String) -> XmlElement: + for idx in range(self.children.size()): + var child := self.children[idx] as XmlElement + if child != null and child.name == p_name: + return child + return null + func take_named_child_element(p_name: String) -> XmlElement: for idx in range(self.children.size()): var child := self.children[idx] as XmlElement