git » sdk » main » tree

[main] / borogove / Import.hx

package borogove;

import ltx.Sax;
using StringTools;

@:expose
@:access(Xml)
class Import {
	private final p: Sax;
	private final targetAccount: Null<String>;
	private var sortA: String;
	private final sortAinit: String;
	private final sortB: String;
	private var counterSameTime = 0;
	private var previousMessageTime = "";
	private var ignoredSources = new Map();

	public var onAccount: String->Void;
	public var onChannel: String->Void;
	public var onMessage: (String, Message)->Void;

	public function new(targetAccount: Null<String>, sortA: String = "Wt@|q", sortB: String = "a ") {
		this.targetAccount = targetAccount;
		this.sortAinit = this.sortA = sortA;
		this.sortB = sortB;
		p = new Sax();

		var ns = new NsContext(null, new Map());
		var host = null;
		var item = null;
		var mucComponent = false;
		var inArchive = false;
		var resultStanza: Null<Stanza> = null;
		p.onStartElement = (tag: Xml) -> {
			ns = NsContext.from(ns, tag);

			if (resultStanza != null) {
				resultStanza.tag(tag.nodeName, tag.attributeMap);
				return;
			}

			switch (ns.expandName(tag.nodeName)) {
			case "{urn:xmpp:pie:0}host":
				host = tag.get("jid");
			case "{urn:xmpp:pie:0}user":
				sortA = sortAinit;
				item = tag.get("name");
				if (onAccount != null) onAccount(item + "@" + host);
			case "{urn:xmpp:pie:0#component}component":
				host = tag.get("jid");
				mucComponent = tag.get("type") == "muc";
			case "{urn:xmpp:pie:0#component}item":
				sortA = sortAinit;
				item = tag.get("name");
				if (mucComponent && onChannel != null) onChannel(item + "@" + host);
			case "{urn:xmpp:pie:0#mam}archive":
				if (item != null && host != null) inArchive = true;
			case "{urn:xmpp:mam:2}result":
				if (inArchive && onMessage != null && !ignoredSources[item + "@" + host]) {
					resultStanza = new Stanza("result", tag.attributeMap);
				}
			}
		};
		p.onEndElement = (tagName: String) -> {
			if (resultStanza != null) {
				resultStanza.up();
				if (resultStanza.atTop()) {
					if (onMessage != null) processResultStanza(resultStanza, item, host);
					resultStanza = null;
				}
				return;
			}

			switch (ns.expandName(tagName)) {
			case "{urn:xmpp:pie:0}host":
				host = null;
			case "{urn:xmpp:pie:0#component}component":
				host = null;
				mucComponent = false;
			case "{urn:xmpp:pie:0#component}item", "{urn:xmpp:pie:0}user":
				item = null;
			case "{urn:xmpp:pie:0#mam}archive":
				inArchive = false;
			case "{urn:xmpp:mam:2}result":
				resultStanza = null;
			}

			ns = ns.parent;
		};

		p.onText = (txt: String) -> {
			if (resultStanza != null) resultStanza.text(txt);
		};
	}

	public function ignoreSource(source: String) {
		ignoredSources.set(source, true);
	}

	public function write(chunk: String) {
		p.write(chunk);
	}

	inline private function processResultStanza(resultStanza: Stanza, item: String, host: String) {
		final originalMessage = resultStanza.findChild("{urn:xmpp:forward:0}forwarded/{jabber:client}message");
		if (originalMessage == null) return;

		final source = item + "@" + host;
		if (ignoredSources.get(source)) return;

		if (targetAccount != null) {
			// TODO: setup senderId and direction on MUC messages based on targetAccount?
			// What if we don't even know target occupant id at this time? Just direction?
			// Worst case if we don't is sent messages show as received from a different account...
			// which is honestly kinda right so may don't do this?

			final from = originalMessage.attr.get("from");
			if (from != null && from.startsWith(source)) {
				originalMessage.attr.set("from", targetAccount);
			}

			final to = originalMessage.attr.get("to");
			if (to != null && to.startsWith(source)) {
				originalMessage.attr.set("to", targetAccount);
			}
		}

		sortA = FractionalIndexing.between(sortA, sortB, FractionalIndexing.BASE_95_DIGITS);
		var timestamp = resultStanza.findText("{urn:xmpp:forward:0}forwarded/{urn:xmpp:delay}delay@stamp");
		if (timestamp != null) {
			// If no subseconds, fix them to at least sort right
			timestamp = ~/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])(\.[0-9][0-9][0-9])?/.map(timestamp, (ereg) -> {
				if (ereg.matched(2) == null || ereg.matched(2) == ".000") {
					if (ereg.matched(1) == previousMessageTime) {
						counterSameTime++;
					} else {
						previousMessageTime = ereg.matched(1);
						counterSameTime = 1;
					}

					return ereg.matched(1) + "." + Std.string(counterSameTime).lpad("0", 3);
				}

				return ereg.matched(0);
			});
		}

		if (resultStanza.attr.get("id") == null && originalMessage.attr.get("id") == null) {
			// Skip messages in the export with no id of any kind
			return;
		}

		final msg = Message.fromStanza(originalMessage, targetAccount == null ? new JID(item, host) : JID.parse(targetAccount), (builder, stanza) -> {
			builder.sortId = sortA;
			builder.serverId = resultStanza.attr.get("id");
			builder.serverIdBy = source;
			if (timestamp != null && builder.timestamp == null) builder.timestamp = timestamp;
			return builder;
		});

		onMessage(source, msg);
	}
}

class NsContext {
	public final parent: Null<NsContext>;
	private final ns: Map<String, String>;

	public function new(parent: Null<NsContext>, ns: Map<String, String>) {
		this.parent = parent;
		this.ns = ns;
	}

	static public function from(parent: Null<NsContext>, tag: Xml) {
		final ns = new Map();
		for (att in tag.attributes()) {
			if (att == "xmlns") {
				ns.set("", tag.get(att));
			} else if(att.startsWith("xmlns:")) {
				ns.set(att.substr(6), tag.get(att));
			}
		}

		return new NsContext(parent, ns);
	}

	public function get(prefix: String) {
		final uri = ns.get(prefix);
		if (uri != null) return uri;

		return parent?.get(prefix);
	}

	public function expandName(name: String) {
		var parts = name.split(":");
		if (parts[0] == "xml") return name;

		if (parts.length < 2) parts = ["", name];

		final ns = get(parts[0]);
		if (ns == null) throw "Undefined prefix";

		return '{$ns}${parts[1]}';
	}
}