diff --git a/flake.lock b/flake.lock
index a2d8e37..f0c3b8e 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
]
},
"locked": {
- "lastModified": 1742213273,
- "narHash": "sha256-0l0vDb4anfsBu1rOs94bC73Hub+xEivgBAo6QXl2MmU=",
+ "lastModified": 1752936381,
+ "narHash": "sha256-b191B12GRfvOT3odGpx5IFyGRPZbBrvCLADZfFHoJFg=",
"owner": "hyprwm",
"repo": "aquamarine",
- "rev": "484b732195cc53f4536ce4bd59a5c6402b1e7ccf",
+ "rev": "141a991678b34e768f09b3a670c61a4c1d5d7110",
"type": "github"
},
"original": {
@@ -590,16 +590,16 @@
]
},
"locked": {
- "lastModified": 1743128724,
- "narHash": "sha256-CUlxc2u1Y8gpeAl7NKrZxxpeZjyU2DBxOYb8b0haM2M=",
+ "lastModified": 1752830283,
+ "narHash": "sha256-1BTJSqkj+lkIry27HuqA5UB7uRqAUvGT7LAUDQhKjU0=",
"owner": "outfoxxed",
"repo": "hy3",
- "rev": "4014433d1c3d1bf36c6684cff14c23d538337070",
+ "rev": "d61a2eb9b9f22c6e46edad3e8f5fbd3578961b11",
"type": "github"
},
"original": {
"owner": "outfoxxed",
- "ref": "hl0.48.0",
+ "ref": "hl0.50.0",
"repo": "hy3",
"type": "github"
}
@@ -620,11 +620,11 @@
]
},
"locked": {
- "lastModified": 1742215578,
- "narHash": "sha256-zfs71PXVVPEe56WEyNi2TJQPs0wabU4WAlq0XV7GcdE=",
+ "lastModified": 1749155331,
+ "narHash": "sha256-XR9fsI0zwLiFWfqi/pdS/VD+YNorKb3XIykgTg4l1nA=",
"owner": "hyprwm",
"repo": "hyprcursor",
- "rev": "2fd36421c21aa87e2fe3bee11067540ae612f719",
+ "rev": "45fcc10b4c282746d93ec406a740c43b48b4ef80",
"type": "github"
},
"original": {
@@ -649,11 +649,11 @@
]
},
"locked": {
- "lastModified": 1739049071,
- "narHash": "sha256-3+7TpXMrbsUXSwgr5VAKAnmkzMb6JO+Rvc9XRb5NMg4=",
+ "lastModified": 1752149140,
+ "narHash": "sha256-gbh1HL98Fdqu0jJIWN4OJQN7Kkth7+rbkFpSZLm/62A=",
"owner": "hyprwm",
"repo": "hyprgraphics",
- "rev": "175c6b29b6ff82100539e7c4363a35a02c74dd73",
+ "rev": "340494a38b5ec453dfc542c6226481f736cc8a9a",
"type": "github"
},
"original": {
@@ -678,17 +678,17 @@
"xdph": "xdph"
},
"locked": {
- "lastModified": 1743178567,
- "narHash": "sha256-skuJFly6LSFfyAVy2ByNolkEwIijsTu2TxzQ9ugWarI=",
- "ref": "refs/tags/v0.48.1",
- "rev": "29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc",
- "revCount": 5937,
+ "lastModified": 1752961026,
+ "narHash": "sha256-ALp/WkfOfXMScwytTmjxpjRNmbezrgFQdEX6n3py7L8=",
+ "ref": "refs/tags/v0.50.1",
+ "rev": "4e242d086e20b32951fdc0ebcbfb4d41b5be8dcc",
+ "revCount": 6291,
"submodules": true,
"type": "git",
"url": "https://github.com/hyprwm/Hyprland"
},
"original": {
- "ref": "refs/tags/v0.48.1",
+ "ref": "refs/tags/v0.50.1",
"submodules": true,
"type": "git",
"url": "https://github.com/hyprwm/Hyprland"
@@ -706,11 +706,11 @@
]
},
"locked": {
- "lastModified": 1738422629,
- "narHash": "sha256-5v+bv75wJWvahyM2xcMTSNNxmV8a7hb01Eey5zYnBJw=",
+ "lastModified": 1749046714,
+ "narHash": "sha256-kymV5FMnddYGI+UjwIw8ceDjdeg7ToDVjbHCvUlhn14=",
"owner": "hyprwm",
"repo": "hyprland-protocols",
- "rev": "755aef8dab49d0fc4663c715fa4ad221b2aedaed",
+ "rev": "613878cb6f459c5e323aaafe1e6f388ac8a36330",
"type": "github"
},
"original": {
@@ -738,11 +738,11 @@
]
},
"locked": {
- "lastModified": 1737634706,
- "narHash": "sha256-nGCibkfsXz7ARx5R+SnisRtMq21IQIhazp6viBU8I/A=",
+ "lastModified": 1749154592,
+ "narHash": "sha256-DO7z5CeT/ddSGDEnK9mAXm1qlGL47L3VAHLlLXoCjhE=",
"owner": "hyprwm",
"repo": "hyprland-qt-support",
- "rev": "8810df502cdee755993cb803eba7b23f189db795",
+ "rev": "4c8053c3c888138a30c3a6c45c2e45f5484f2074",
"type": "github"
},
"original": {
@@ -774,11 +774,11 @@
]
},
"locked": {
- "lastModified": 1739048983,
- "narHash": "sha256-REhTcXq4qs3B3cCDtLlYDz0GZvmsBSh947Ub6pQWGTQ=",
+ "lastModified": 1750371812,
+ "narHash": "sha256-D868K1dVEACw17elVxRgXC6hOxY+54wIEjURztDWLk8=",
"owner": "hyprwm",
"repo": "hyprland-qtutils",
- "rev": "3504a293c8f8db4127cb0f7cfc1a318ffb4316f8",
+ "rev": "b13c7481e37856f322177010bdf75fccacd1adc8",
"type": "github"
},
"original": {
@@ -803,11 +803,11 @@
]
},
"locked": {
- "lastModified": 1741191527,
- "narHash": "sha256-kM+11Nch47Xwfgtw2EpRitJuORy4miwoMuRi5tyMBDY=",
+ "lastModified": 1750371198,
+ "narHash": "sha256-/iuJ1paQOBoSLqHflRNNGyroqfF/yvPNurxzcCT0cAE=",
"owner": "hyprwm",
"repo": "hyprlang",
- "rev": "72df3861f1197e41b078faa3e38eedd60e00018d",
+ "rev": "cee01452bca58d6cadb3224e21e370de8bc20f0b",
"type": "github"
},
"original": {
@@ -828,11 +828,11 @@
]
},
"locked": {
- "lastModified": 1741534688,
- "narHash": "sha256-EV3945SnjOCuRVbGRghsWx/9D89FyshnSO1Q6/TuQ14=",
+ "lastModified": 1752252310,
+ "narHash": "sha256-06i1pIh6wb+sDeDmWlzuPwIdaFMxLlj1J9I5B9XqSeo=",
"owner": "hyprwm",
"repo": "hyprutils",
- "rev": "dd1f720cbc2dbb3c71167c9598045dd3261d27b3",
+ "rev": "bcabcbada90ed2aacb435dc09b91001819a6dc82",
"type": "github"
},
"original": {
@@ -853,11 +853,11 @@
]
},
"locked": {
- "lastModified": 1739870480,
- "narHash": "sha256-SiDN5BGxa/1hAsqhgJsS03C3t2QrLgBT8u+ENJ0Qzwc=",
+ "lastModified": 1751897909,
+ "narHash": "sha256-FnhBENxihITZldThvbO7883PdXC/2dzW4eiNvtoV5Ao=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
- "rev": "206367a08dc5ac4ba7ad31bdca391d098082e64b",
+ "rev": "fcca0c61f988a9d092cbb33e906775014c61579d",
"type": "github"
},
"original": {
@@ -1207,11 +1207,11 @@
},
"nixpkgs_7": {
"locked": {
- "lastModified": 1742069588,
- "narHash": "sha256-C7jVfohcGzdZRF6DO+ybyG/sqpo1h6bZi9T56sxLy+k=",
+ "lastModified": 1752687322,
+ "narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5",
+ "rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251",
"type": "github"
},
"original": {
@@ -1281,11 +1281,11 @@
]
},
"locked": {
- "lastModified": 1742058297,
- "narHash": "sha256-b4SZc6TkKw8WQQssbN5O2DaCEzmFfvSTPYHlx/SFW9Y=",
+ "lastModified": 1750779888,
+ "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"owner": "cachix",
"repo": "git-hooks.nix",
- "rev": "59f17850021620cd348ad2e9c0c64f4e6325ce2a",
+ "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"type": "github"
},
"original": {
@@ -1545,11 +1545,11 @@
]
},
"locked": {
- "lastModified": 1741934139,
- "narHash": "sha256-ZhTcTH9FoeAtbPfWGrhkH7RjLJZ7GeF18nygLAMR+WE=",
+ "lastModified": 1751300244,
+ "narHash": "sha256-PFuv1TZVYvQhha0ac53E3YgdtmLShrN0t4T6xqHl0jE=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
- "rev": "150b0b6f52bb422a1b232a53698606fe0320dde0",
+ "rev": "6115f3fdcb2c1a57b4a80a69f3c797e47607b90a",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 675a614..d38e272 100755
--- a/flake.nix
+++ b/flake.nix
@@ -102,9 +102,9 @@
flake = false;
};
- hyprland.url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.48.1";
+ hyprland.url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.50.1";
hy3 = {
- url = "github:outfoxxed/hy3?ref=hl0.48.0";
+ url = "github:outfoxxed/hy3?ref=hl0.50.0";
inputs.hyprland.follows = "hyprland";
};
diff --git a/host/Rory-nginx/services/matrix/synapse/workers/event-creator.nix b/host/Rory-nginx/services/matrix/synapse/workers/event-creator.nix
index 0966573..9534b62 100644
--- a/host/Rory-nginx/services/matrix/synapse/workers/event-creator.nix
+++ b/host/Rory-nginx/services/matrix/synapse/workers/event-creator.nix
@@ -14,6 +14,7 @@ let
"~ ^/_matrix/client/(api/v1|r0|v3|unstable)/knock/"
"~ ^/_matrix/client/(api/v1|r0|v3|unstable)/profile/"
"~ ^/_synapse/admin/v1/rooms" # We have a lot of them, so let's do a bunch of jobs at once!
+ "~ ^/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/.*/bulk_send_events"
];
federation = [ ];
media = [ ];
diff --git a/packages/overlays/matrix-synapse/patches/0001-Fast-auth-links.patch b/packages/overlays/matrix-synapse/patches/0001-Fast-auth-links.patch
index 0d92e4e..7b91953 100644
--- a/packages/overlays/matrix-synapse/patches/0001-Fast-auth-links.patch
+++ b/packages/overlays/matrix-synapse/patches/0001-Fast-auth-links.patch
@@ -1,7 +1,7 @@
From 1b82f35b613e96c56bf18015e33f34328ad73188 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Tue, 22 Jul 2025 05:07:01 +0200
-Subject: [PATCH 01/11] Fast auth links
+Subject: [PATCH 01/14] Fast auth links
---
synapse/storage/database.py | 43 +++++++++++++++++++
diff --git a/packages/overlays/matrix-synapse/patches/0002-Hotfix-ignore-rejected-events-in-delayed_events.patch b/packages/overlays/matrix-synapse/patches/0002-Hotfix-ignore-rejected-events-in-delayed_events.patch
index 3d5ea60..adc1b50 100644
--- a/packages/overlays/matrix-synapse/patches/0002-Hotfix-ignore-rejected-events-in-delayed_events.patch
+++ b/packages/overlays/matrix-synapse/patches/0002-Hotfix-ignore-rejected-events-in-delayed_events.patch
@@ -1,7 +1,7 @@
From 346fb5899fa42d4604b7bf0261c5e1774e6d2c04 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Sun, 20 Apr 2025 00:30:29 +0200
-Subject: [PATCH 02/11] Hotfix: ignore rejected events in delayed_events
+Subject: [PATCH 02/14] Hotfix: ignore rejected events in delayed_events
---
synapse/handlers/delayed_events.py | 7 ++++++-
diff --git a/packages/overlays/matrix-synapse/patches/0003-Add-too-much-logging-to-room-summary-over-federation.patch b/packages/overlays/matrix-synapse/patches/0003-Add-too-much-logging-to-room-summary-over-federation.patch
index 5f4e596..c5a71ec 100644
--- a/packages/overlays/matrix-synapse/patches/0003-Add-too-much-logging-to-room-summary-over-federation.patch
+++ b/packages/overlays/matrix-synapse/patches/0003-Add-too-much-logging-to-room-summary-over-federation.patch
@@ -1,7 +1,7 @@
From 929d1e329ec26d2e351591206a82c6e235660437 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Wed, 23 Apr 2025 17:53:52 +0200
-Subject: [PATCH 03/11] Add too much logging to room summary over federation
+Subject: [PATCH 03/14] Add too much logging to room summary over federation
Signed-off-by: Rory& <root@rory.gay>
---
diff --git a/packages/overlays/matrix-synapse/patches/0004-Log-entire-room-if-accessibility-check-fails.patch b/packages/overlays/matrix-synapse/patches/0004-Log-entire-room-if-accessibility-check-fails.patch
index 290f0da..04c00c1 100644
--- a/packages/overlays/matrix-synapse/patches/0004-Log-entire-room-if-accessibility-check-fails.patch
+++ b/packages/overlays/matrix-synapse/patches/0004-Log-entire-room-if-accessibility-check-fails.patch
@@ -1,7 +1,7 @@
From 0ce933278f77e272e2cc894229a1178e1b4fb552 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Wed, 23 Apr 2025 18:24:57 +0200
-Subject: [PATCH 04/11] Log entire room if accessibility check fails
+Subject: [PATCH 04/14] Log entire room if accessibility check fails
Signed-off-by: Rory& <root@rory.gay>
---
diff --git a/packages/overlays/matrix-synapse/patches/0005-Log-policy-server-rejected-events.patch b/packages/overlays/matrix-synapse/patches/0005-Log-policy-server-rejected-events.patch
index ae59e63..7c6b002 100644
--- a/packages/overlays/matrix-synapse/patches/0005-Log-policy-server-rejected-events.patch
+++ b/packages/overlays/matrix-synapse/patches/0005-Log-policy-server-rejected-events.patch
@@ -1,7 +1,7 @@
From 0b5d4c8104bf25f7bbb4e4e7db229742f04199b6 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Tue, 27 May 2025 05:21:46 +0200
-Subject: [PATCH 05/11] Log policy server rejected events
+Subject: [PATCH 05/14] Log policy server rejected events
---
synapse/handlers/room_policy.py | 7 +++++++
diff --git a/packages/overlays/matrix-synapse/patches/0006-Use-parse_boolean-for-unredacted-content.patch b/packages/overlays/matrix-synapse/patches/0006-Use-parse_boolean-for-unredacted-content.patch
index 1c2841c..45fbd2c 100644
--- a/packages/overlays/matrix-synapse/patches/0006-Use-parse_boolean-for-unredacted-content.patch
+++ b/packages/overlays/matrix-synapse/patches/0006-Use-parse_boolean-for-unredacted-content.patch
@@ -1,7 +1,7 @@
From 07d72fd39ea3044577322647d5ed1dd8cb6f77d9 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Tue, 27 May 2025 06:14:26 +0200
-Subject: [PATCH 06/11] Use parse_boolean for unredacted content
+Subject: [PATCH 06/14] Use parse_boolean for unredacted content
---
synapse/rest/client/room.py | 5 ++---
diff --git a/packages/overlays/matrix-synapse/patches/0007-Expose-tombstone-in-room-admin-api.patch b/packages/overlays/matrix-synapse/patches/0007-Expose-tombstone-in-room-admin-api.patch
index 719705e..f331512 100644
--- a/packages/overlays/matrix-synapse/patches/0007-Expose-tombstone-in-room-admin-api.patch
+++ b/packages/overlays/matrix-synapse/patches/0007-Expose-tombstone-in-room-admin-api.patch
@@ -1,7 +1,7 @@
From d3edb4aa9a225f521fdbc406c187fd40343b3963 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Tue, 27 May 2025 06:37:52 +0200
-Subject: [PATCH 07/11] Expose tombstone in room admin api
+Subject: [PATCH 07/14] Expose tombstone in room admin api
---
synapse/rest/admin/rooms.py | 5 ++++
diff --git a/packages/overlays/matrix-synapse/patches/0008-fix-Always-recheck-messages-pagination-data-if-a-bac.patch b/packages/overlays/matrix-synapse/patches/0008-fix-Always-recheck-messages-pagination-data-if-a-bac.patch
index 363204e..724c134 100644
--- a/packages/overlays/matrix-synapse/patches/0008-fix-Always-recheck-messages-pagination-data-if-a-bac.patch
+++ b/packages/overlays/matrix-synapse/patches/0008-fix-Always-recheck-messages-pagination-data-if-a-bac.patch
@@ -1,7 +1,7 @@
From afecddceaa6ece4cf797ce27e226a99acb8e8a6d Mon Sep 17 00:00:00 2001
From: Jason Little <j.little@famedly.com>
Date: Wed, 30 Apr 2025 09:29:42 -0500
-Subject: [PATCH 08/11] fix: Always recheck `/messages` pagination data if a
+Subject: [PATCH 08/14] fix: Always recheck `/messages` pagination data if a
backfill might have been needed (#28)
---
diff --git a/packages/overlays/matrix-synapse/patches/0009-Fix-pagination-with-large-gaps-of-rejected-events.patch b/packages/overlays/matrix-synapse/patches/0009-Fix-pagination-with-large-gaps-of-rejected-events.patch
index ebed62e..e249252 100644
--- a/packages/overlays/matrix-synapse/patches/0009-Fix-pagination-with-large-gaps-of-rejected-events.patch
+++ b/packages/overlays/matrix-synapse/patches/0009-Fix-pagination-with-large-gaps-of-rejected-events.patch
@@ -1,7 +1,7 @@
From 2f2dd65326b8a8dc6b7ac99dbe7476abb2163469 Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Sun, 8 Jun 2025 23:14:31 +0200
-Subject: [PATCH 09/11] Fix pagination with large gaps of rejected events
+Subject: [PATCH 09/14] Fix pagination with large gaps of rejected events
---
synapse/handlers/pagination.py | 13 +++++++++++--
diff --git a/packages/overlays/matrix-synapse/patches/0010-Fix-nix-flake.patch b/packages/overlays/matrix-synapse/patches/0010-Fix-nix-flake.patch
index 4df6090..a2bb1ed 100644
--- a/packages/overlays/matrix-synapse/patches/0010-Fix-nix-flake.patch
+++ b/packages/overlays/matrix-synapse/patches/0010-Fix-nix-flake.patch
@@ -1,7 +1,7 @@
From 448de6ea7bfe1c6073726f517988e5deeb510861 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Mon, 9 Jun 2025 17:38:34 +0200
-Subject: [PATCH 10/11] Fix nix flake
+Subject: [PATCH 10/14] Fix nix flake
---
flake.lock | 58 +++++++++++++++++++-----------------------------------
diff --git a/packages/overlays/matrix-synapse/patches/0011-Fix-gitignore-to-ignore-.venv.patch b/packages/overlays/matrix-synapse/patches/0011-Fix-gitignore-to-ignore-.venv.patch
index 82335db..f627abc 100644
--- a/packages/overlays/matrix-synapse/patches/0011-Fix-gitignore-to-ignore-.venv.patch
+++ b/packages/overlays/matrix-synapse/patches/0011-Fix-gitignore-to-ignore-.venv.patch
@@ -1,7 +1,7 @@
From e1b50954048039a23c538cd260644ccc63d82941 Mon Sep 17 00:00:00 2001
From: Rory& <root@rory.gay>
Date: Mon, 9 Jun 2025 17:46:10 +0200
-Subject: [PATCH 11/11] Fix gitignore to ignore .venv
+Subject: [PATCH 11/14] Fix gitignore to ignore .venv
---
.gitignore | 1 +
diff --git a/packages/overlays/matrix-synapse/patches/0012-Devenv-use-postgres-17.patch b/packages/overlays/matrix-synapse/patches/0012-Devenv-use-postgres-17.patch
new file mode 100644
index 0000000..0e78105
--- /dev/null
+++ b/packages/overlays/matrix-synapse/patches/0012-Devenv-use-postgres-17.patch
@@ -0,0 +1,24 @@
+From 8fefc1ece0f73ab4e4867cbb4cc1511dc7faa56f Mon Sep 17 00:00:00 2001
+From: Rory& <root@rory.gay>
+Date: Fri, 25 Jul 2025 08:25:28 +0200
+Subject: [PATCH 12/14] Devenv: use postgres 17
+
+---
+ flake.nix | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/flake.nix b/flake.nix
+index 76b3c1a4b0..cc41490a41 100644
+--- a/flake.nix
++++ b/flake.nix
+@@ -157,6 +157,7 @@
+ # Postgres is needed to run Synapse with postgres support and
+ # to run certain unit tests that require postgres.
+ services.postgres.enable = true;
++ services.postgres.package = pkgs.postgresql_17;
+
+ # On the first invocation of `devenv up`, create a database for
+ # Synapse to store data in.
+--
+2.49.0
+
diff --git a/packages/overlays/matrix-synapse/patches/0013-RequestRatelimiter-expose-can_do_action.patch b/packages/overlays/matrix-synapse/patches/0013-RequestRatelimiter-expose-can_do_action.patch
new file mode 100644
index 0000000..2ad8e55
--- /dev/null
+++ b/packages/overlays/matrix-synapse/patches/0013-RequestRatelimiter-expose-can_do_action.patch
@@ -0,0 +1,95 @@
+From 4b62d4e914d8ff7e21bcfbbc6572f1f2a363e066 Mon Sep 17 00:00:00 2001
+From: Rory& <root@rory.gay>
+Date: Fri, 25 Jul 2025 08:26:15 +0200
+Subject: [PATCH 13/14] RequestRatelimiter: expose can_do_action
+
+---
+ synapse/api/ratelimiting.py | 75 +++++++++++++++++++++++++++++++++++++
+ 1 file changed, 75 insertions(+)
+
+diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py
+index 509ef6b2c1..5f22089a6b 100644
+--- a/synapse/api/ratelimiting.py
++++ b/synapse/api/ratelimiting.py
+@@ -435,3 +435,78 @@ class RequestRatelimiter:
+ update=update,
+ n_actions=n_actions,
+ )
++
++ async def can_do_action(
++ self,
++ requester: Optional[Requester],
++ burst_count: Optional[int] = None,
++ update: bool = True,
++ is_admin_redaction: bool = False,
++ n_actions: int = 1,
++ ) -> Tuple[bool, float]:
++ """Can the entity (e.g. user or IP address) perform the action?
++
++ Checks if the user has ratelimiting disabled in the database by looking
++ for null/zero values in the `ratelimit_override` table. (Non-zero
++ values aren't honoured, as they're specific to the event sending
++ ratelimiter, rather than all ratelimiters)
++
++ Args:
++ requester: The requester that is doing the action, if any. Used to check
++ if the user has ratelimits disabled in the database.
++ key: An arbitrary key used to classify an action. Defaults to the
++ requester's user ID.
++ rate_hz: The long term number of actions that can be performed in a second.
++ Overrides the value set during instantiation if set.
++ burst_count: How many actions that can be performed before being limited.
++ Overrides the value set during instantiation if set.
++ update: Whether to count this check as performing the action. If the action
++ cannot be performed, the user's action count is not incremented at all.
++ n_actions: The number of times the user wants to do this action. If the user
++ cannot do all of the actions, the user's action count is not incremented
++ at all.
++ _time_now_s: The current time. Optional, defaults to the current time according
++ to self.clock. Only used by tests.
++
++ Returns:
++ A tuple containing:
++ * A bool indicating if they can perform the action now
++ * The reactor timestamp for when the action can be performed next.
++ -1 if rate_hz is less than or equal to zero
++ """
++ user_id = requester.user.to_string()
++
++ # The AS user itself is never rate limited.
++ app_service = self.store.get_app_service_by_user_id(user_id)
++ if app_service is not None:
++ return True, 0 # do not ratelimit app service senders
++
++ messages_per_second = self._rc_message.per_second
++ burst_count = self._rc_message.burst_count
++
++ # Check if there is a per user override in the DB.
++ override = await self.store.get_ratelimit_for_user(user_id)
++ if override:
++ # If overridden with a null Hz then ratelimiting has been entirely
++ # disabled for the user
++ if not override.messages_per_second:
++ return True, 0
++
++ messages_per_second = override.messages_per_second
++ burst_count = override.burst_count
++
++ if is_admin_redaction and self.admin_redaction_ratelimiter:
++ # If we have separate config for admin redactions, use a separate
++ # ratelimiter as to not have user_ids clash
++ return await self.admin_redaction_ratelimiter.can_do_action(
++ requester, update=update, n_actions=n_actions
++ )
++ else:
++ # Override rate and burst count per-user
++ return await self.request_ratelimiter.can_do_action(
++ requester,
++ rate_hz=messages_per_second,
++ burst_count=burst_count,
++ update=update,
++ n_actions=n_actions,
++ )
+--
+2.49.0
+
diff --git a/packages/overlays/matrix-synapse/patches/0014-Add-bulk-send-events-endpoint.patch b/packages/overlays/matrix-synapse/patches/0014-Add-bulk-send-events-endpoint.patch
new file mode 100644
index 0000000..fdd6030
--- /dev/null
+++ b/packages/overlays/matrix-synapse/patches/0014-Add-bulk-send-events-endpoint.patch
@@ -0,0 +1,187 @@
+From 452f38800dd00b8686543099d6a085f9b4210687 Mon Sep 17 00:00:00 2001
+From: Rory& <root@rory.gay>
+Date: Sat, 26 Jul 2025 09:50:56 +0200
+Subject: [PATCH 14/14] Add bulk send events endpoint
+
+---
+ synapse/rest/client/capabilities.py | 3 +
+ synapse/rest/client/room.py | 117 +++++++++++++++++++++++++++-
+ 2 files changed, 119 insertions(+), 1 deletion(-)
+
+diff --git a/synapse/rest/client/capabilities.py b/synapse/rest/client/capabilities.py
+index 8f3193fb47..7220b75006 100644
+--- a/synapse/rest/client/capabilities.py
++++ b/synapse/rest/client/capabilities.py
+@@ -74,6 +74,9 @@ class CapabilitiesRestServlet(RestServlet):
+ "m.get_login_token": {
+ "enabled": self.config.auth.login_via_existing_enabled,
+ },
++ "gay.rory.bulk_send_events": {
++ "enabled": True
++ }
+ }
+ }
+
+diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py
+index f61152c35b..19ba13dd64 100644
+--- a/synapse/rest/client/room.py
++++ b/synapse/rest/client/room.py
+@@ -23,10 +23,12 @@
+
+ import logging
+ import re
++import ijson
+ from enum import Enum
+ from http import HTTPStatus
+ from typing import TYPE_CHECKING, Awaitable, Dict, List, Optional, Tuple
+ from urllib import parse as urlparse
++from twisted.internet import defer
+
+ from prometheus_client.core import Histogram
+
+@@ -44,6 +46,7 @@ from synapse.api.errors import (
+ UnredactedContentDeletedError,
+ )
+ from synapse.api.filtering import Filter
++from synapse.events import EventBase
+ from synapse.events.utils import SerializeEventConfig, format_event_for_client_v2
+ from synapse.http.server import HttpServer
+ from synapse.http.servlet import (
+@@ -469,7 +472,6 @@ class RoomSendEventRestServlet(TransactionRestServlet):
+ txn_id,
+ )
+
+-
+ def _parse_request_delay(
+ request: SynapseRequest,
+ max_delay: Optional[int],
+@@ -1610,6 +1612,118 @@ class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet):
+ remote_room_hosts,
+ )
+
++class RoomBulkSendEventRestServlet(ResolveRoomIdMixin, RestServlet):
++ """
++ Bulk send events to a room.
++
++ This endpoint allows sending multiple events to a room in a single request,
++ avoiding event linearisation issues.
++ """
++
++ PATTERNS = (
++ re.compile(
++ "^/_matrix/client/unstable/gay.rory.bulk_send_events"
++ "/rooms/(?P<room_identifier>[^/]*)/bulk_send_events$"
++ ),
++ )
++ CATEGORY = "Event sending requests"
++
++ def __init__(self, hs: "HomeServer"):
++ super().__init__(hs)
++ self._auth = hs.get_auth()
++ self._event_creation_handler = hs.get_event_creation_handler()
++ self._message_handler = hs.get_message_handler()
++
++ async def on_POST(
++ self, request: SynapseRequest, room_identifier: str
++ ) -> Tuple[int, JsonDict]:
++ logger.warning("bulk_send_events: Got bulk send events request")
++ requester = await self._auth.get_user_by_req(request, allow_guest=False)
++ room_id, remote_room_hosts = await self.resolve_room_id(room_identifier)
++
++ force_sync_interval = parse_integer(request, "force_sync_interval", default=250)
++
++ current_state_events = await self._message_handler.get_state_events(
++ room_id=room_id,
++ requester=requester,
++ )
++
++ state_map = {(event["type"], event.get("state_key", "")): event.get("event_id") for event in current_state_events}
++
++ events = ijson.items(
++ request.content,
++ "item"
++ )
++
++ i = 0
++ unpersisted_events = []
++
++ for event_data in events:
++ current_index = i
++ i += 1
++ logger.info("bulk_send_events: Processing event %d: %s", current_index, event_data)
++
++ event_dict: JsonDict = {
++ "type": event_data.get("type"),
++ "content": event_data.get("content", {}),
++ "room_id": room_id,
++ "sender": requester.user.to_string(),
++ }
++
++ if "state_key" in event_data:
++ event_dict["state_key"] = event_data["state_key"]
++
++ # Explicitly handle rate limits in order to avoid compounding effects
++ awaiting_ratelimit = False
++ ratelimit_hit = False
++ while awaiting_ratelimit:
++ can_do_action, ratelimit_expiry = await self._event_creation_handler.request_ratelimiter.can_do_action(requester, update=False)
++ if not can_do_action:
++ # can_do_action returns an absolute timestamp, convert it to a relative time
++ time_to_sleep = ratelimit_expiry - self._event_creation_handler.request_ratelimiter.clock.time()
++ logger.warning("bulk_send_events: Got rate limited in bulk sending events, waiting %ds", time_to_sleep)
++ await self._event_creation_handler.request_ratelimiter.clock.sleep(time_to_sleep)
++ ratelimit_hit = True
++ else:
++ awaiting_ratelimit = False
++ await self._event_creation_handler.request_ratelimiter.can_do_action(requester, update=True)
++
++ event, unpersisted_context = await self._event_creation_handler.create_event(
++ requester,
++ event_dict,
++ for_batch=True,
++ state_map=state_map,
++ )
++ context = await unpersisted_context.persist(event)
++
++ if event.is_state():
++ prev_event = await self._event_creation_handler.deduplicate_state_event(event, context)
++ if prev_event is not None:
++ logger.info(
++ "Not bothering to persist state event %s duplicated by %s",
++ event.event_id,
++ prev_event.event_id,
++ )
++ continue
++ else:
++ state_map[(event_dict["type"], event_dict["state_key"])] = event.event_id
++ logger.warning("bulk_send_events: Updated state_map!")
++
++ unpersisted_events.append((event, context))
++ logger.warning("bulk_send_events: Persisted event %d: %s", current_index, event)
++
++ if ratelimit_hit or len(unpersisted_events) >= force_sync_interval:
++ logger.warning("bulk_send_events: Hit rate limit or max batch size, sending %d events", len(unpersisted_events))
++ await self._event_creation_handler.handle_new_client_event(requester, unpersisted_events, ratelimit=False)
++ unpersisted_events = []
++
++ # Finalize any remaining unpersisted events
++ if(len(unpersisted_events) > 0):
++ await self._event_creation_handler.handle_new_client_event(requester, unpersisted_events, ratelimit=False)
++ unpersisted_events = []
++
++ return 200, {}
++
+
+ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ RoomStateEventRestServlet(hs).register(http_server)
+@@ -1619,6 +1733,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ JoinRoomAliasServlet(hs).register(http_server)
+ RoomMembershipRestServlet(hs).register(http_server)
+ RoomSendEventRestServlet(hs).register(http_server)
++ RoomBulkSendEventRestServlet(hs).register(http_server)
+ PublicRoomListRestServlet(hs).register(http_server)
+ RoomStateRestServlet(hs).register(http_server)
+ RoomRedactEventRestServlet(hs).register(http_server)
+--
+2.49.0
+
|