borogove
created pr with
borogove/xep0070.1
changed pr name to
XEP0070
added borogove/xep0070.2
1: d0cd98e < -: ------- Longer timeouts for slow CI
2: 69ff006 = 1: 7b3b6e4 Implement XEP-0070
commented
This sure is some code you wrote
added borogove/xep0070.3
1: 7b3b6e4 = 1: 7b3b6e4 Implement XEP-0070
-: ------- > 2: fa9eda8 review comment
cmds
checkout latest patchset:
ssh pr.pico.sh pull borogove:xep0070 | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull borogove:xep0070 [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:xep0070add comment to patch request:
ssh pr.pico.sh comment borogove:xep0070 "lgtm!"
Patchset
borogove/xep0070.3
Implement XEP-0070
Stephen Paul Weber
2026-10-20T17:59:11ZWith 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 { | |
| 133 | 133 | private var firstSync = true; | |
| 134 | 134 | @:allow(borogove.Channel) | |
| 135 | 135 | private final channelPinger: ChannelPinger; | |
| 136 | + | private final loginRequestRateLimit: Array<std.Date> = []; | |
| 136 | 137 | ||
| 137 | 138 | /** | |
| 138 | 139 | Create a new Client to connect to a particular account |
| ... | ... | @@ -590,6 +591,23 @@ class Client extends EventEmitter { | |
| 590 | 591 | this.trigger("chats/update", [channel]); | |
| 591 | 592 | }); | |
| 592 | 593 | } | |
| 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 | + | } | |
| 593 | 611 | default: | |
| 594 | 612 | // ignore | |
| 595 | 613 | trace("Ignoring non-chat message: " + stanza.toString()); |
| ... | ... | @@ -1880,6 +1898,34 @@ class Client extends EventEmitter { | |
| 1880 | 1898 | } | |
| 1881 | 1899 | #end | |
| 1882 | 1900 | ||
| 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 | + | ||
| 1883 | 1929 | /** | |
| 1884 | 1930 | Let the SDK know the UI is in the foreground | |
| 1885 | 1931 | **/ |
+12
-0
borogove/Message.hx
#
| ... | ... | @@ -41,6 +41,7 @@ enum MessageStanza { | |
| 41 | 41 | ReactionUpdateStanza(update: ReactionUpdate); | |
| 42 | 42 | MucInviteStanza(serverId: Null<String>, serverIdBy: Null<String>, reason: Null<String>, password: Null<String>); | |
| 43 | 43 | MucVoiceRequest(jid: JID); | |
| 44 | + | LoginRequest(req: LoginRequest); | |
| 44 | 45 | UnknownMessageStanza(stanza: Stanza); | |
| 45 | 46 | } | |
| 46 | 47 |
| ... | ... | @@ -249,6 +250,17 @@ class Message { | |
| 249 | 250 | if (addContext != null) msg = addContext(msg, stanza); | |
| 250 | 251 | final timeSent = msg.timeSent ?? msg.timeReceived ?? Date.format(std.Date.now()); | |
| 251 | 252 | ||
| 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 | + | ||
| 252 | 264 | if (jmi != null && jmi.name != "accept" && jmi.name != "propose") { | |
| 253 | 265 | msg.versions = [msg.build()]; | |
| 254 | 266 | } |
+1
-0
npm/index.ts
#
| ... | ... | @@ -35,6 +35,7 @@ export { | |
| 35 | 35 | borogove_Identicon as Identicon, | |
| 36 | 36 | borogove_Importer as Importer, | |
| 37 | 37 | borogove_LinkMetadata as LinkMetadata, | |
| 38 | + | borogove_LoginRequest as LoginRequest, | |
| 38 | 39 | borogove_Notification as Notification, | |
| 39 | 40 | borogove_Member as Member, | |
| 40 | 41 | borogove_Profile as Profile, |