diff --git a/scenes/App.gd b/scenes/App.gd
index 7029249..836a6ad 100644
--- a/scenes/App.gd
+++ b/scenes/App.gd
@@ -4,14 +4,32 @@ var _connection
func _service_discovery():
var iq := yield() as Xml.XmlElement
- print(iq.as_string())
+
+ var feature_promises := Array()
+ for item in iq.children[0].children:
+ feature_promises.push_back(_connection.promise_iq(item.attributes["jid"], "get",
+ "",
+ _connection.iq_as_is()))
+
+ while not _connection.are_promises_done(feature_promises):
+ yield()
+
+ for feature_promise in feature_promises:
+ if not feature_promise.is_ok:
+ push_error("Connection failed")
+ return
+
+ var feature := feature_promise.value as Xml.XmlElement
+ print(feature.as_string())
func _ready():
_connection = $Connections.establish_new_connection("poto.cafe", "veclavtalica", "-")
if _connection == null:
push_error("Connection failed")
+ return
if _connection.push_iq(_connection.domain, "get",
"",
_service_discovery()) != OK:
push_error("Connection failed")
+ return
diff --git a/scenes/Connections.gd b/scenes/Connections.gd
index f635253..200ba14 100644
--- a/scenes/Connections.gd
+++ b/scenes/Connections.gd
@@ -8,6 +8,7 @@ class Connection extends Reference:
var jid: String
var _id_counter: int = 0
var _pending_iqs: Dictionary # of id to GDScriptFunctionState
+ var _pending_tail_iqs: Array # of GDScriptFunctionState
var _xml_parser := Xml.Parser.new()
# warning-ignore:unused_signal
@@ -21,6 +22,48 @@ class Connection extends Reference:
self._id_counter += 1
return hash(self._id_counter)
+ class Promise extends Reference:
+ var is_done: bool = false
+ var is_ok: bool
+ var value = null
+
+ static func from(capture: GDScriptFunctionState) -> Promise:
+ var result := Promise.new()
+ if capture.connect("completed", result, "result_arrived") != OK:
+ assert("Bruh")
+ return result
+
+ static func make_error(error: int) -> Promise:
+ var result := Promise.new()
+ result.is_done = true
+ result.is_ok = false
+ result.value = error
+ return result
+
+ func result_arrived(p_result) -> void:
+ self.is_done = true
+ self.is_ok = true
+ self.value = p_result
+
+ func promise_iq(to: String, action: String, payload: String, capture: GDScriptFunctionState) -> Promise:
+ assert(action in ["get", "set"])
+
+ var id := self.generate_id()
+ if self.stream.put_data(
+ """{payload}""".format({
+ "from": self.jid.xml_escape(),
+ "id": id,
+ "to": to.xml_escape(),
+ "action": action,
+ "payload": payload
+ }).to_utf8()) != OK:
+ return Promise.make_error(ERR_CONNECTION_ERROR)
+
+ assert(not id in self._pending_iqs)
+ self._pending_iqs[id] = capture
+
+ return Promise.from(capture)
+
func push_iq(to: String, action: String, payload: String, capture: GDScriptFunctionState) -> int: # Error
assert(action in ["get", "set"])
@@ -40,6 +83,16 @@ class Connection extends Reference:
return OK
+ static func are_promises_done(promises: Array) -> bool:
+ for promise in promises:
+ assert(promise is Promise)
+ if not promise.is_done:
+ return false
+ return true
+
+ static func iq_as_is():
+ return yield()
+
## Registry of connections used for poking of pending iqs.
var _connections: Array # of WeakRef to Connection
@@ -61,8 +114,10 @@ func _process_connections() -> void:
if response == null:
continue
- if connection._xml_parser.parse_a_bit(response):
- var stanza := connection._xml_parser.root
+ while connection._xml_parser.parse_a_bit(response):
+ response = null
+
+ var stanza := connection._xml_parser.take_root()
if stanza.node_name == "iq":
if "to" in stanza.attributes and stanza.attributes["to"] != connection.jid:
push_error("Stanza is not addressed to assigned jid")
@@ -70,13 +125,12 @@ func _process_connections() -> void:
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(stanza)
- if result is GDScriptFunctionState:
- connection._pending_iqs[id] = result
- elif result != null:
- assert("Ignored result of iq subroutine: " + result)
- else:
- var was_present := connection._pending_iqs.erase(id)
- assert(was_present)
+ 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)
+ var was_present := connection._pending_iqs.erase(id)
+ assert(was_present)
elif stanza.attributes["type"] in ["set", "get"]:
connection.emit_signal("iq_received", stanza)
@@ -87,7 +141,17 @@ func _process_connections() -> void:
elif stanza.node_name == "presence":
connection.emit_signal("presence_received", stanza)
- connection._xml_parser = Xml.Parser.new()
+ 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 tail_idx in range(to_remove_tails.size() - 1, 0, -1):
+ connection._pending_tail_iqs.remove(to_remove_tails[tail_idx])
## Collect dropped connections.
for idx in range(to_remove.size() - 1, 0, -1):
@@ -161,21 +225,21 @@ func _negotiate_tls(connection: Connection) -> int:
## Check that server response corresponds to what we ask for.
# todo: For conformity client must send closing stream tag on error.
- if parsed_response.root.node_name != "stream:stream": return ERR_CANT_CONNECT
- if parsed_response.root.attributes["from"] != connection.domain: return ERR_CANT_CONNECT
- if "to" in parsed_response.root.attributes:
- if parsed_response.root.attributes["to"] != connection.bare_jid: return ERR_CANT_CONNECT
- if parsed_response.root.attributes["version"] != "1.0": return ERR_CANT_CONNECT
+ if parsed_response._root.node_name != "stream:stream": return ERR_CANT_CONNECT
+ if parsed_response._root.attributes["from"] != connection.domain: return ERR_CANT_CONNECT
+ if "to" in parsed_response._root.attributes:
+ if parsed_response._root.attributes["to"] != connection.bare_jid: return ERR_CANT_CONNECT
+ if parsed_response._root.attributes["version"] != "1.0": return ERR_CANT_CONNECT
# todo: Assert for namespaces.
## Step 3: Server sends stream features to client (only the STARTTLS
## extension at this point, which is mandatory-to-negotiate):
- if parsed_response.root.children.size() == 0:
+ if parsed_response._root.children.size() == 0:
response = _wait_blocking_for_utf8_data(connection.stream)
if response == null: return ERR_CONNECTION_ERROR
if parsed_response.parse_a_bit(response): return ERR_CONNECTION_ERROR
- var features = parsed_response.root.take_named_child_element("stream:features")
+ var features = parsed_response._root.take_named_child_element("stream:features")
if features == null: return ERR_CANT_CONNECT
var starttls = features.take_named_child_element("starttls")
if starttls == null or starttls.attributes["xmlns"] != "urn:ietf:params:xml:ns:xmpp-tls":
@@ -193,7 +257,7 @@ func _negotiate_tls(connection: Connection) -> int:
# todo: Handle failure case.
- var proceed = parsed_response.root.take_named_child_element("proceed")
+ var proceed = parsed_response._root.take_named_child_element("proceed")
if proceed == null: return ERR_CANT_CONNECT
if proceed.attributes["xmlns"] != "urn:ietf:params:xml:ns:xmpp-tls": return ERR_CANT_CONNECT
@@ -232,19 +296,19 @@ func _negotiate_sasl(connection: Connection, password: String) -> int:
## Check that server response corresponds to what we ask for.
# todo: For conformity client must send closing stream tag on error.
- if parsed_response.root.node_name != "stream:stream": return ERR_CANT_CONNECT
- if parsed_response.root.attributes["from"] != connection.domain: return ERR_CANT_CONNECT
- if "to" in parsed_response.root.attributes:
- if parsed_response.root.attributes["to"] != connection.bare_jid: return ERR_CANT_CONNECT
- if parsed_response.root.attributes["version"] != "1.0": return ERR_CANT_CONNECT
+ if parsed_response._root.node_name != "stream:stream": return ERR_CANT_CONNECT
+ if parsed_response._root.attributes["from"] != connection.domain: return ERR_CANT_CONNECT
+ if "to" in parsed_response._root.attributes:
+ if parsed_response._root.attributes["to"] != connection.bare_jid: return ERR_CANT_CONNECT
+ if parsed_response._root.attributes["version"] != "1.0": return ERR_CANT_CONNECT
# todo: Assert for namespaces.
- if parsed_response.root.children.size() == 0:
+ if parsed_response._root.children.size() == 0:
response = _wait_blocking_for_utf8_data(connection.stream)
if response == null: return ERR_CONNECTION_ERROR
if parsed_response.parse_a_bit(response): return ERR_CONNECTION_ERROR
- var features = parsed_response.root.take_named_child_element("stream:features")
+ var features = parsed_response._root.take_named_child_element("stream:features")
if features == null: return ERR_CANT_CONNECT
var mechanisms = features.take_named_child_element("mechanisms")
if mechanisms == null or mechanisms.attributes["xmlns"] != "urn:ietf:params:xml:ns:xmpp-sasl":
@@ -278,7 +342,7 @@ func _negotiate_sasl(connection: Connection, password: String) -> int:
if response == null: return ERR_CONNECTION_ERROR
if parsed_response.parse_a_bit(response): return ERR_CONNECTION_ERROR
- var success = parsed_response.root.take_named_child_element("success")
+ var success = parsed_response._root.take_named_child_element("success")
if success == null or success.attributes["xmlns"] != "urn:ietf:params:xml:ns:xmpp-sasl":
return ERR_CANT_CONNECT
@@ -300,19 +364,19 @@ func _bind_resource(connection: Connection, resource: String = "tochie-facade")
## Check that server response corresponds to what we ask for.
# todo: For conformity client must send closing stream tag on error.
- if parsed_response.root.node_name != "stream:stream": return ERR_CANT_CONNECT
- if parsed_response.root.attributes["from"] != connection.domain: return ERR_CANT_CONNECT
- if "to" in parsed_response.root.attributes:
- if parsed_response.root.attributes["to"] != connection.bare_jid: return ERR_CANT_CONNECT
- if parsed_response.root.attributes["version"] != "1.0": return ERR_CANT_CONNECT
+ if parsed_response._root.node_name != "stream:stream": return ERR_CANT_CONNECT
+ if parsed_response._root.attributes["from"] != connection.domain: return ERR_CANT_CONNECT
+ if "to" in parsed_response._root.attributes:
+ if parsed_response._root.attributes["to"] != connection.bare_jid: return ERR_CANT_CONNECT
+ if parsed_response._root.attributes["version"] != "1.0": return ERR_CANT_CONNECT
# todo: Assert for namespaces.
- if parsed_response.root.children.size() == 0:
+ if parsed_response._root.children.size() == 0:
response = _wait_blocking_for_utf8_data(connection.stream)
if response == null: return ERR_CONNECTION_ERROR
if parsed_response.parse_a_bit(response): return ERR_CONNECTION_ERROR
- var features = parsed_response.root.take_named_child_element("stream:features")
+ var features = parsed_response._root.take_named_child_element("stream:features")
if features == null: return ERR_CANT_CONNECT
var bind = features.take_named_child_element("bind")
if bind == null or bind.attributes["xmlns"] != "urn:ietf:params:xml:ns:xmpp-bind":
@@ -336,7 +400,7 @@ func _bind_resource(connection: Connection, resource: String = "tochie-facade")
## Step 16: Server accepts submitted resourcepart and informs client of
## successful resource binding:
- var iq = parsed_response.root.take_named_child_element("iq")
+ var iq = parsed_response._root.take_named_child_element("iq")
if iq.attributes["id"] != String(iq_id):
push_error("iq id mismatch, expected: " + String(iq_id) + " but got: " + iq.attributes["id"])
return ERR_CANT_CONNECT
diff --git a/scenes/Xml.gd b/scenes/Xml.gd
index 945e493..712caea 100644
--- a/scenes/Xml.gd
+++ b/scenes/Xml.gd
@@ -39,35 +39,49 @@ class XmlText extends XmlNode:
# todo: Save namespaces.
# todo: Ability to parse from partially arrived data, with saving for future resume.
class Parser extends Reference:
- var root: XmlElement = null
+ var _root: XmlElement = null
var _element_stack: Array # of XmlElement
+ var _pending: PoolByteArray
+
+ func take_root() -> XmlElement:
+ var result := self._root
+ assert(result != null)
+ self._root = null
+ return result
## Returns true if root element is closed, otherwise more tags are expected to come later.
func parse_a_bit(data): # -> bool or Error:
var error: int
var parser := XMLParser.new()
if data is String: data = data.to_utf8()
- error = parser.open_buffer(data)
+ if data == null: data = PoolByteArray()
+ var total = self._pending + data
+ if total.size() == 0:
+ return false
+
+ error = parser.open_buffer(total)
if error != OK:
push_error("Error opening a buffer for XML parsing")
return error
+ self._pending = PoolByteArray()
+
while parser.read() == OK:
if parser.get_node_type() == XMLParser.NODE_ELEMENT:
var element := XmlElement.new()
-
- if self.root != null:
- self._element_stack[self._element_stack.size() - 1].children.push_back(element)
- else: self.root = element
-
- if not parser.is_empty():
- self._element_stack.push_back(element)
-
element.node_name = parser.get_node_name()
var attribute_count := parser.get_attribute_count()
for idx in range(attribute_count):
element.attributes[parser.get_attribute_name(idx)] = parser.get_attribute_value(idx)
+ if self._root != null:
+ self._element_stack[self._element_stack.size() - 1].children.push_back(element)
+ else: self._root = element
+
+ # todo: Handle _root empty element.
+ if not parser.is_empty():
+ self._element_stack.push_back(element)
+
elif parser.get_node_type() == XMLParser.NODE_ELEMENT_END:
if self._element_stack.size() == 0:
push_error("Closing element %s> closes nothing" % [parser.get_node_name()])
@@ -78,6 +92,12 @@ class Parser extends Reference:
push_error("Element <%s> closes sooner than <%s>" % [parser.get_node_name(), popped.node_name])
return ERR_PARSE_ERROR
+ if self._element_stack.size() == 0:
+ # todo: Handle partial data.
+ if parser.read() == OK:
+ self._pending = total.subarray(parser.get_node_offset(), -1)
+ return true
+
elif parser.get_node_type() == XMLParser.NODE_TEXT:
if self._element_stack.size() == 0:
push_error("Text node should be a child of an element")