diff --git a/scenes/App.gd b/scenes/App.gd index 9c65ba4..7029249 100644 --- a/scenes/App.gd +++ b/scenes/App.gd @@ -1,6 +1,17 @@ extends Node +var _connection + +func _service_discovery(): + var iq := yield() as Xml.XmlElement + print(iq.as_string()) + func _ready(): - var connection = $Connections.establish_new_connection("poto.cafe", "veclavtalica", "-") - if connection == null: + _connection = $Connections.establish_new_connection("poto.cafe", "veclavtalica", "-") + if _connection == null: + push_error("Connection failed") + + if _connection.push_iq(_connection.domain, "get", + "", + _service_discovery()) != OK: push_error("Connection failed") diff --git a/scenes/Connections.gd b/scenes/Connections.gd index 93b3326..f635253 100644 --- a/scenes/Connections.gd +++ b/scenes/Connections.gd @@ -7,11 +7,92 @@ class Connection extends Reference: var bare_jid: String var jid: String var _id_counter: int = 0 + var _pending_iqs: Dictionary # of id to GDScriptFunctionState + var _xml_parser := Xml.Parser.new() + +# warning-ignore:unused_signal + signal presence_received(presence) +# warning-ignore:unused_signal + signal message_received(message) +# warning-ignore:unused_signal + signal iq_received(iq) func generate_id() -> int: self._id_counter += 1 return hash(self._id_counter) + func push_iq(to: String, action: String, payload: String, capture: GDScriptFunctionState) -> int: # Error + 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 ERR_CONNECTION_ERROR + + assert(not id in self._pending_iqs) + self._pending_iqs[id] = capture + + return OK + +## Registry of connections used for poking of pending iqs. +var _connections: Array # of WeakRef to Connection + +func _ready(): + # todo: Some better interval? + if get_tree().connect("physics_frame", self, "_process_connections") != OK: + assert("Bruh") + +func _process_connections() -> void: + var to_remove := PoolIntArray() + + for idx in range(_connections.size()): + var connection := _connections[idx].get_ref() as Connection + if connection == null: + to_remove.push_back(idx) + continue + + var response = _wait_blocking_for_utf8_data(connection.stream, 0) + if response == null: + continue + + if connection._xml_parser.parse_a_bit(response): + var stanza := connection._xml_parser.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") + + 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) + + elif stanza.attributes["type"] in ["set", "get"]: + connection.emit_signal("iq_received", stanza) + + elif stanza.node_name == "message": + connection.emit_signal("message_received", stanza) + + elif stanza.node_name == "presence": + connection.emit_signal("presence_received", stanza) + + connection._xml_parser = Xml.Parser.new() + + ## Collect dropped connections. + for idx in range(to_remove.size() - 1, 0, -1): + _connections.remove(to_remove[idx]) + func establish_new_connection(domain: String, identity: String, password: String) -> Connection: var stream := StreamPeerTCP.new() if stream.connect_to_host(domain, 5222) != OK: @@ -34,6 +115,8 @@ func establish_new_connection(domain: String, identity: String, password: String if _negotiate_connection(result, password) != OK: return null + _connections.push_back(weakref(result)) + return result # todo: Make it async diff --git a/scenes/Xml.gd b/scenes/Xml.gd index f53a445..945e493 100644 --- a/scenes/Xml.gd +++ b/scenes/Xml.gd @@ -19,7 +19,7 @@ class XmlElement extends XmlNode: return child return null - func as_string(level = 0) -> String: + func as_string(level: int = 0) -> String: var result = '\t'.repeat(level) + "XmlElement \"%s\"" % self.node_name if self.attributes.size() > 0: result += ", Attributes: " + String(self.attributes) @@ -32,11 +32,12 @@ class XmlText extends XmlNode: func is_text() -> bool: return true - func as_string(level = 0) -> String: + func as_string(_level: int = 0) -> String: return "XmlText \"%s\"" % self.data ## Wrapper over XMLParser for TCP stream oriented XML data. # 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 _element_stack: Array # of XmlElement @@ -86,7 +87,11 @@ class Parser extends Reference: text.data = parser.get_node_data() self._element_stack[self._element_stack.size() - 1].children.push_back(text) + elif parser.get_node_type() == XMLParser.NODE_UNKNOWN: + ## Needed for things like version specification. + pass + else: - push_warning("Node type unimplemented and ignored") + push_error("Node type unimplemented") return self._element_stack.size() == 0