borogove

created pr with borogove/xep0070.1 on 2026-10-20T20:37:12Z · by 9e834e0d
changed pr name to XEP0070 on 2026-10-20T20:38:13Z · by 9e834e0d
added borogove/xep0070.2 on 2026-10-20T20:40:24Z · by 9e834e0d
1: d0cd98e < -: ------- Longer timeouts for slow CI
2: 69ff006 = 1: 7b3b6e4 Implement XEP-0070
commented on 2026-10-20T20:42:31Z · by 9e834e0d
This sure is some code you wrote
added borogove/xep0070.3 on 2026-10-20T20:47:22Z · by 9e834e0d
1: 7b3b6e4 = 1: 7b3b6e4 Implement XEP-0070
-: ------- > 2: fa9eda8 review comment
cmds
checkout latest patchset:
ssh pr.pico.sh pull borogove:xep0070 | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull borogove:xep0070 [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:xep0070
add comment to patch request:
ssh pr.pico.sh comment borogove:xep0070 "lgtm!"

Patchset borogove/xep0070.2 on 2026-10-20T20:40:24Z · commit 7b3b6e4

Implement XEP-0070
Stephen Paul Weber 2026-10-20T17:59:11Z
With rate limits at 5, 15, and 60 minute sliding windows
Semantic diff summary
0 added, 1 modified, 0 signature changed, 0 removed across 1 analyzed file (2 files skipped: unsupported file type)
+46 -0 borogove/Client.hx #
......@@ -133,6 +133,7 @@ class Client extends EventEmitter {
133133 private var firstSync = true;
134134 @:allow(borogove.Channel)
135135 private final channelPinger: ChannelPinger;
136+ private final loginRequestRateLimit: Array<std.Date> = [];
136137
137138 /**
138139 Create a new Client to connect to a particular account
......@@ -590,6 +591,23 @@ class Client extends EventEmitter {
590591 this.trigger("chats/update", [channel]);
591592 });
592593 }
594+ case LoginRequest(req):
595+ final tooOld = Date.format(
596+ DateTools.delta(std.Date.now(), DateTools.minutes(-5))
597+ );
598+ while (loginRequestRateLimit.length > 0 && loginRequestRateLimit[0].getTime() < DateTools.delta(std.Date.now(), DateTools.minutes(-60)).getTime()) {
599+ loginRequestRateLimit.shift();
600+ }
601+ final lastMinute = loginRequestRateLimit.filter(d -> d.getTime() > DateTools.delta(std.Date.now(), DateTools.minutes(-1)).getTime()).length;
602+ final last15Mins = loginRequestRateLimit.filter(d -> d.getTime() > DateTools.delta(std.Date.now(), DateTools.minutes(-15)).getTime()).length;
603+ if (req.timeSent > tooOld) {
604+ if (lastMinute < 5 && last15Mins < 10 && loginRequestRateLimit.length < 20) {
605+ loginRequestRateLimit.push(std.Date.now());
606+ trigger("message/login", req);
607+ }
608+ } else {
609+ rejectLoginRequest(req, "Expired");
610+ }
593611 default:
594612 // ignore
595613 trace("Ignoring non-chat message: " + stanza.toString());
......@@ -1880,6 +1898,34 @@ class Client extends EventEmitter {
18801898 }
18811899 #end
18821900
1901+ /**
1902+ Event fired when the user is trying to login to a website
1903+
1904+ @param handler takes one argument, the LoginRequest
1905+ @returns token for use with removeEventListener
1906+ **/
1907+ @:HaxeSwiftBridge.contextLifetime(handler, EventEmitter)
1908+ public function addLoginRequestListener(handler: (LoginRequest)->Void) {
1909+ return this.on("message/login", (req) -> {
1910+ handler(req);
1911+ return EventHandled;
1912+ });
1913+ }
1914+
1915+ /**
1916+ Approve a LoginRequest to a website
1917+ **/
1918+ public function approveLoginRequest(req: LoginRequest) {
1919+ sendStanza(req.approveStanza());
1920+ }
1921+
1922+ /**
1923+ Reject a LoginRequest to a website
1924+ **/
1925+ public function rejectLoginRequest(req: LoginRequest, reason: String) {
1926+ sendStanza(req.rejectStanza(reason));
1927+ }
1928+
18831929 /**
18841930 Let the SDK know the UI is in the foreground
18851931 **/
+12 -0 borogove/Message.hx #
......@@ -41,6 +41,7 @@ enum MessageStanza {
4141 ReactionUpdateStanza(update: ReactionUpdate);
4242 MucInviteStanza(serverId: Null<String>, serverIdBy: Null<String>, reason: Null<String>, password: Null<String>);
4343 MucVoiceRequest(jid: JID);
44+ LoginRequest(req: LoginRequest);
4445 UnknownMessageStanza(stanza: Stanza);
4546 }
4647
......@@ -249,6 +250,17 @@ class Message {
249250 if (addContext != null) msg = addContext(msg, stanza);
250251 final timeSent = msg.timeSent ?? msg.timeReceived ?? Date.format(std.Date.now());
251252
253+ final xep0070 = stanza.getChild("confirm", "http://jabber.org/protocol/http-auth");
254+ if (xep0070 != null) {
255+ final threadId = msg.threadId;
256+ final id = xep0070.attr.get("id");
257+ final method = xep0070.attr.get("method");
258+ final url = xep0070.attr.get("url");
259+ if (from != null && threadId != null && id != null && method != null && url != null) {
260+ return new Message(msg.chatId(), msg.senderId, threadId, LoginRequest(new LoginRequest(from, threadId, id, method, url, timeSent)), encryptionInfo);
261+ }
262+ }
263+
252264 if (jmi != null && jmi.name != "accept" && jmi.name != "propose") {
253265 msg.versions = [msg.build()];
254266 }
+1 -0 npm/index.ts #
......@@ -35,6 +35,7 @@ export {
3535 borogove_Identicon as Identicon,
3636 borogove_Importer as Importer,
3737 borogove_LinkMetadata as LinkMetadata,
38+ borogove_LoginRequest as LoginRequest,
3839 borogove_Notification as Notification,
3940 borogove_Member as Member,
4041 borogove_Profile as Profile,
Back to top