atagen

macltok

public
Star0
Watch0

Commit 2006a11ce142

tarball

atagen <boss@atagen.co>2026-10-26 05:13 UTC

7 files changed+1128-30
push: manage http pushers and parse gateway notifies without a client

Logout now takes the pushers to drop, because pushers belong to the user, not the device: cleaning up by app id would also silence the same app in other browsers. macltok-push-gateway is its own crate under push-gateway/, so malice-static can parse /_matrix/push/v1/notify without pulling in the sdk. resolve_push_notification also reports the stored room's unread counts, None for a room this session hasn't synced.
Added file: push-gateway/Cargo.lock+851 additions
@@ -0,0 +1,851 @@
1+# This file is automatically @generated by Cargo.
2+# It is not intended for manual editing.
3+version = 4
4+
5+[[package]]
6+name = "aho-corasick"
7+version = "1.1.5"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
10+dependencies = [
11+ "memchr",
12+]
13+
14+[[package]]
15+name = "as_variant"
16+version = "1.3.0"
17+source = "registry+https://github.com/rust-lang/crates.io-index"
18+checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a"
19+
20+[[package]]
21+name = "base64"
22+version = "0.22.1"
23+source = "registry+https://github.com/rust-lang/crates.io-index"
24+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
25+
26+[[package]]
27+name = "bumpalo"
28+version = "3.20.3"
29+source = "registry+https://github.com/rust-lang/crates.io-index"
30+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
31+
32+[[package]]
33+name = "bytes"
34+version = "1.12.1"
35+source = "registry+https://github.com/rust-lang/crates.io-index"
36+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
37+
38+[[package]]
39+name = "cfg-if"
40+version = "1.0.5"
41+source = "registry+https://github.com/rust-lang/crates.io-index"
42+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
43+
44+[[package]]
45+name = "deranged"
46+version = "0.5.9"
47+source = "registry+https://github.com/rust-lang/crates.io-index"
48+checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
49+
50+[[package]]
51+name = "displaydoc"
52+version = "0.2.7"
53+source = "registry+https://github.com/rust-lang/crates.io-index"
54+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
55+dependencies = [
56+ "proc-macro2",
57+ "quote",
58+ "syn 3.0.9",
59+]
60+
61+[[package]]
62+name = "equivalent"
63+version = "1.0.3"
64+source = "registry+https://github.com/rust-lang/crates.io-index"
65+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
66+
67+[[package]]
68+name = "form_urlencoded"
69+version = "1.2.2"
70+source = "registry+https://github.com/rust-lang/crates.io-index"
71+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
72+dependencies = [
73+ "percent-encoding",
74+]
75+
76+[[package]]
77+name = "futures-core"
78+version = "0.3.34"
79+source = "registry+https://github.com/rust-lang/crates.io-index"
80+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
81+
82+[[package]]
83+name = "futures-task"
84+version = "0.3.34"
85+source = "registry+https://github.com/rust-lang/crates.io-index"
86+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
87+
88+[[package]]
89+name = "futures-util"
90+version = "0.3.34"
91+source = "registry+https://github.com/rust-lang/crates.io-index"
92+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
93+dependencies = [
94+ "futures-core",
95+ "futures-task",
96+ "pin-project-lite",
97+ "slab",
98+]
99+
100+[[package]]
101+name = "hashbrown"
102+version = "0.17.1"
103+source = "registry+https://github.com/rust-lang/crates.io-index"
104+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
105+
106+[[package]]
107+name = "icu_collections"
108+version = "2.4.0"
109+source = "registry+https://github.com/rust-lang/crates.io-index"
110+checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
111+dependencies = [
112+ "displaydoc",
113+ "potential_utf",
114+ "utf8_iter",
115+ "yoke",
116+ "zerofrom",
117+ "zerovec",
118+]
119+
120+[[package]]
121+name = "icu_locale_core"
122+version = "2.4.0"
123+source = "registry+https://github.com/rust-lang/crates.io-index"
124+checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
125+dependencies = [
126+ "displaydoc",
127+ "litemap",
128+ "tinystr",
129+ "writeable",
130+ "zerovec",
131+]
132+
133+[[package]]
134+name = "icu_normalizer"
135+version = "2.4.0"
136+source = "registry+https://github.com/rust-lang/crates.io-index"
137+checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
138+dependencies = [
139+ "icu_collections",
140+ "icu_normalizer_data",
141+ "icu_properties",
142+ "icu_provider",
143+ "smallvec",
144+ "zerovec",
145+]
146+
147+[[package]]
148+name = "icu_normalizer_data"
149+version = "2.4.0"
150+source = "registry+https://github.com/rust-lang/crates.io-index"
151+checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
152+
153+[[package]]
154+name = "icu_properties"
155+version = "2.4.0"
156+source = "registry+https://github.com/rust-lang/crates.io-index"
157+checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
158+dependencies = [
159+ "displaydoc",
160+ "icu_collections",
161+ "icu_locale_core",
162+ "icu_properties_data",
163+ "icu_provider",
164+ "zerotrie",
165+ "zerovec",
166+]
167+
168+[[package]]
169+name = "icu_properties_data"
170+version = "2.4.0"
171+source = "registry+https://github.com/rust-lang/crates.io-index"
172+checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
173+
174+[[package]]
175+name = "icu_provider"
176+version = "2.4.0"
177+source = "registry+https://github.com/rust-lang/crates.io-index"
178+checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
179+dependencies = [
180+ "displaydoc",
181+ "icu_locale_core",
182+ "writeable",
183+ "yoke",
184+ "zerofrom",
185+ "zerotrie",
186+ "zerovec",
187+]
188+
189+[[package]]
190+name = "idna"
191+version = "1.1.0"
192+source = "registry+https://github.com/rust-lang/crates.io-index"
193+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
194+dependencies = [
195+ "idna_adapter",
196+ "smallvec",
197+ "utf8_iter",
198+]
199+
200+[[package]]
201+name = "idna_adapter"
202+version = "1.2.2"
203+source = "registry+https://github.com/rust-lang/crates.io-index"
204+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
205+dependencies = [
206+ "icu_normalizer",
207+ "icu_properties",
208+]
209+
210+[[package]]
211+name = "indexmap"
212+version = "2.14.2"
213+source = "registry+https://github.com/rust-lang/crates.io-index"
214+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
215+dependencies = [
216+ "equivalent",
217+ "hashbrown",
218+ "serde",
219+ "serde_core",
220+]
221+
222+[[package]]
223+name = "itoa"
224+version = "1.0.18"
225+source = "registry+https://github.com/rust-lang/crates.io-index"
226+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
227+
228+[[package]]
229+name = "js-sys"
230+version = "0.3.106"
231+source = "registry+https://github.com/rust-lang/crates.io-index"
232+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
233+dependencies = [
234+ "cfg-if",
235+ "futures-util",
236+ "wasm-bindgen",
237+]
238+
239+[[package]]
240+name = "js_int"
241+version = "0.2.2"
242+source = "registry+https://github.com/rust-lang/crates.io-index"
243+checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a"
244+dependencies = [
245+ "serde",
246+]
247+
248+[[package]]
249+name = "litemap"
250+version = "0.8.3"
251+source = "registry+https://github.com/rust-lang/crates.io-index"
252+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
253+
254+[[package]]
255+name = "macltok-push-gateway"
256+version = "0.1.0"
257+dependencies = [
258+ "ruma-common",
259+ "serde",
260+ "serde_json",
261+]
262+
263+[[package]]
264+name = "memchr"
265+version = "2.8.3"
266+source = "registry+https://github.com/rust-lang/crates.io-index"
267+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
268+
269+[[package]]
270+name = "num-conv"
271+version = "0.2.2"
272+source = "registry+https://github.com/rust-lang/crates.io-index"
273+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
274+
275+[[package]]
276+name = "once_cell"
277+version = "1.21.4"
278+source = "registry+https://github.com/rust-lang/crates.io-index"
279+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
280+
281+[[package]]
282+name = "percent-encoding"
283+version = "2.3.2"
284+source = "registry+https://github.com/rust-lang/crates.io-index"
285+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
286+
287+[[package]]
288+name = "pin-project-lite"
289+version = "0.2.17"
290+source = "registry+https://github.com/rust-lang/crates.io-index"
291+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
292+
293+[[package]]
294+name = "potential_utf"
295+version = "0.1.6"
296+source = "registry+https://github.com/rust-lang/crates.io-index"
297+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
298+dependencies = [
299+ "zerovec",
300+]
301+
302+[[package]]
303+name = "powerfmt"
304+version = "0.2.1"
305+source = "registry+https://github.com/rust-lang/crates.io-index"
306+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
307+
308+[[package]]
309+name = "proc-macro-crate"
310+version = "3.5.0"
311+source = "registry+https://github.com/rust-lang/crates.io-index"
312+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
313+dependencies = [
314+ "toml_edit",
315+]
316+
317+[[package]]
318+name = "proc-macro2"
319+version = "1.0.107"
320+source = "registry+https://github.com/rust-lang/crates.io-index"
321+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
322+dependencies = [
323+ "unicode-ident",
324+]
325+
326+[[package]]
327+name = "quote"
328+version = "1.0.47"
329+source = "registry+https://github.com/rust-lang/crates.io-index"
330+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
331+dependencies = [
332+ "proc-macro2",
333+]
334+
335+[[package]]
336+name = "regex"
337+version = "1.13.1"
338+source = "registry+https://github.com/rust-lang/crates.io-index"
339+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
340+dependencies = [
341+ "aho-corasick",
342+ "memchr",
343+ "regex-automata",
344+ "regex-syntax",
345+]
346+
347+[[package]]
348+name = "regex-automata"
349+version = "0.4.18"
350+source = "registry+https://github.com/rust-lang/crates.io-index"
351+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
352+dependencies = [
353+ "aho-corasick",
354+ "memchr",
355+ "regex-syntax",
356+]
357+
358+[[package]]
359+name = "regex-syntax"
360+version = "0.8.11"
361+source = "registry+https://github.com/rust-lang/crates.io-index"
362+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
363+
364+[[package]]
365+name = "ruma-common"
366+version = "0.19.0"
367+source = "registry+https://github.com/rust-lang/crates.io-index"
368+checksum = "2c3b4f00112791b490acce57df1ce3eb3f88899b045bebcff8a29f75369640cc"
369+dependencies = [
370+ "as_variant",
371+ "base64",
372+ "bytes",
373+ "form_urlencoded",
374+ "indexmap",
375+ "js_int",
376+ "percent-encoding",
377+ "regex",
378+ "ruma-identifiers-validation",
379+ "ruma-macros",
380+ "serde",
381+ "serde_html_form",
382+ "serde_json",
383+ "thiserror",
384+ "time",
385+ "tracing",
386+ "url",
387+ "web-time",
388+ "wildmatch",
389+ "zeroize",
390+]
391+
392+[[package]]
393+name = "ruma-identifiers-validation"
394+version = "0.12.1"
395+source = "registry+https://github.com/rust-lang/crates.io-index"
396+checksum = "9d6cff00317675f487c4e7ccfb18875a14c5a14867b51d13f2a826053f03c432"
397+dependencies = [
398+ "js_int",
399+ "thiserror",
400+]
401+
402+[[package]]
403+name = "ruma-macros"
404+version = "0.19.0"
405+source = "registry+https://github.com/rust-lang/crates.io-index"
406+checksum = "c8cfb39eaa9b9fd389126ff941e060b496add5cbbfef559d80c46e571dda459c"
407+dependencies = [
408+ "as_variant",
409+ "cfg-if",
410+ "proc-macro-crate",
411+ "proc-macro2",
412+ "quote",
413+ "ruma-identifiers-validation",
414+ "serde",
415+ "syn 2.0.119",
416+ "toml",
417+]
418+
419+[[package]]
420+name = "rustversion"
421+version = "1.0.23"
422+source = "registry+https://github.com/rust-lang/crates.io-index"
423+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
424+
425+[[package]]
426+name = "serde"
427+version = "1.0.229"
428+source = "registry+https://github.com/rust-lang/crates.io-index"
429+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
430+dependencies = [
431+ "serde_core",
432+ "serde_derive",
433+]
434+
435+[[package]]
436+name = "serde_core"
437+version = "1.0.229"
438+source = "registry+https://github.com/rust-lang/crates.io-index"
439+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
440+dependencies = [
441+ "serde_derive",
442+]
443+
444+[[package]]
445+name = "serde_derive"
446+version = "1.0.229"
447+source = "registry+https://github.com/rust-lang/crates.io-index"
448+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
449+dependencies = [
450+ "proc-macro2",
451+ "quote",
452+ "syn 3.0.9",
453+]
454+
455+[[package]]
456+name = "serde_html_form"
457+version = "0.4.1"
458+source = "registry+https://github.com/rust-lang/crates.io-index"
459+checksum = "8f0346d7a342ab90f405cfc08f25d15075f944f42fcabbc5eac923829fa6d228"
460+dependencies = [
461+ "form_urlencoded",
462+ "indexmap",
463+ "itoa",
464+ "serde_core",
465+ "zmij",
466+]
467+
468+[[package]]
469+name = "serde_json"
470+version = "1.0.154"
471+source = "registry+https://github.com/rust-lang/crates.io-index"
472+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
473+dependencies = [
474+ "itoa",
475+ "memchr",
476+ "serde",
477+ "serde_core",
478+ "zmij",
479+]
480+
481+[[package]]
482+name = "serde_spanned"
483+version = "1.1.2"
484+source = "registry+https://github.com/rust-lang/crates.io-index"
485+checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
486+dependencies = [
487+ "serde_core",
488+]
489+
490+[[package]]
491+name = "slab"
492+version = "0.4.12"
493+source = "registry+https://github.com/rust-lang/crates.io-index"
494+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
495+
496+[[package]]
497+name = "smallvec"
498+version = "1.16.4"
499+source = "registry+https://github.com/rust-lang/crates.io-index"
500+checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
501+
502+[[package]]
503+name = "stable_deref_trait"
504+version = "1.2.1"
505+source = "registry+https://github.com/rust-lang/crates.io-index"
506+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
507+
508+[[package]]
509+name = "syn"
510+version = "2.0.119"
511+source = "registry+https://github.com/rust-lang/crates.io-index"
512+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
513+dependencies = [
514+ "proc-macro2",
515+ "quote",
516+ "unicode-ident",
517+]
518+
519+[[package]]
520+name = "syn"
521+version = "3.0.9"
522+source = "registry+https://github.com/rust-lang/crates.io-index"
523+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
524+dependencies = [
525+ "proc-macro2",
526+ "quote",
527+ "unicode-ident",
528+]
529+
530+[[package]]
531+name = "synstructure"
532+version = "0.14.0"
533+source = "registry+https://github.com/rust-lang/crates.io-index"
534+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
535+dependencies = [
536+ "proc-macro2",
537+ "quote",
538+ "syn 3.0.9",
539+]
540+
541+[[package]]
542+name = "thiserror"
543+version = "2.0.21"
544+source = "registry+https://github.com/rust-lang/crates.io-index"
545+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
546+dependencies = [
547+ "thiserror-impl",
548+]
549+
550+[[package]]
551+name = "thiserror-impl"
552+version = "2.0.21"
553+source = "registry+https://github.com/rust-lang/crates.io-index"
554+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
555+dependencies = [
556+ "proc-macro2",
557+ "quote",
558+ "syn 3.0.9",
559+]
560+
561+[[package]]
562+name = "time"
563+version = "0.3.55"
564+source = "registry+https://github.com/rust-lang/crates.io-index"
565+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
566+dependencies = [
567+ "deranged",
568+ "num-conv",
569+ "powerfmt",
570+ "serde_core",
571+ "time-core",
572+]
573+
574+[[package]]
575+name = "time-core"
576+version = "0.1.9"
577+source = "registry+https://github.com/rust-lang/crates.io-index"
578+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
579+
580+[[package]]
581+name = "tinystr"
582+version = "0.8.5"
583+source = "registry+https://github.com/rust-lang/crates.io-index"
584+checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
585+dependencies = [
586+ "displaydoc",
587+ "zerovec",
588+]
589+
590+[[package]]
591+name = "toml"
592+version = "1.1.8+spec-1.1.0"
593+source = "registry+https://github.com/rust-lang/crates.io-index"
594+checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
595+dependencies = [
596+ "serde_core",
597+ "serde_spanned",
598+ "toml_datetime",
599+ "toml_parser",
600+ "winnow",
601+]
602+
603+[[package]]
604+name = "toml_datetime"
605+version = "1.1.2+spec-1.1.0"
606+source = "registry+https://github.com/rust-lang/crates.io-index"
607+checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
608+dependencies = [
609+ "serde_core",
610+]
611+
612+[[package]]
613+name = "toml_edit"
614+version = "0.25.17+spec-1.1.0"
615+source = "registry+https://github.com/rust-lang/crates.io-index"
616+checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
617+dependencies = [
618+ "indexmap",
619+ "toml_datetime",
620+ "toml_parser",
621+ "winnow",
622+]
623+
624+[[package]]
625+name = "toml_parser"
626+version = "1.1.5+spec-1.1.0"
627+source = "registry+https://github.com/rust-lang/crates.io-index"
628+checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
629+dependencies = [
630+ "winnow",
631+]
632+
633+[[package]]
634+name = "tracing"
635+version = "0.1.44"
636+source = "registry+https://github.com/rust-lang/crates.io-index"
637+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
638+dependencies = [
639+ "pin-project-lite",
640+ "tracing-attributes",
641+ "tracing-core",
642+]
643+
644+[[package]]
645+name = "tracing-attributes"
646+version = "0.1.31"
647+source = "registry+https://github.com/rust-lang/crates.io-index"
648+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
649+dependencies = [
650+ "proc-macro2",
651+ "quote",
652+ "syn 2.0.119",
653+]
654+
655+[[package]]
656+name = "tracing-core"
657+version = "0.1.36"
658+source = "registry+https://github.com/rust-lang/crates.io-index"
659+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
660+dependencies = [
661+ "once_cell",
662+]
663+
664+[[package]]
665+name = "unicode-ident"
666+version = "1.0.27"
667+source = "registry+https://github.com/rust-lang/crates.io-index"
668+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
669+
670+[[package]]
671+name = "url"
672+version = "2.5.8"
673+source = "registry+https://github.com/rust-lang/crates.io-index"
674+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
675+dependencies = [
676+ "form_urlencoded",
677+ "idna",
678+ "percent-encoding",
679+ "serde",
680+]
681+
682+[[package]]
683+name = "utf8_iter"
684+version = "1.0.4"
685+source = "registry+https://github.com/rust-lang/crates.io-index"
686+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
687+
688+[[package]]
689+name = "wasm-bindgen"
690+version = "0.2.129"
691+source = "registry+https://github.com/rust-lang/crates.io-index"
692+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
693+dependencies = [
694+ "cfg-if",
695+ "once_cell",
696+ "rustversion",
697+ "wasm-bindgen-macro",
698+ "wasm-bindgen-shared",
699+]
700+
701+[[package]]
702+name = "wasm-bindgen-macro"
703+version = "0.2.129"
704+source = "registry+https://github.com/rust-lang/crates.io-index"
705+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
706+dependencies = [
707+ "quote",
708+ "wasm-bindgen-macro-support",
709+]
710+
711+[[package]]
712+name = "wasm-bindgen-macro-support"
713+version = "0.2.129"
714+source = "registry+https://github.com/rust-lang/crates.io-index"
715+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
716+dependencies = [
717+ "bumpalo",
718+ "proc-macro2",
719+ "quote",
720+ "syn 3.0.9",
721+ "wasm-bindgen-shared",
722+]
723+
724+[[package]]
725+name = "wasm-bindgen-shared"
726+version = "0.2.129"
727+source = "registry+https://github.com/rust-lang/crates.io-index"
728+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
729+dependencies = [
730+ "unicode-ident",
731+]
732+
733+[[package]]
734+name = "web-time"
735+version = "1.1.0"
736+source = "registry+https://github.com/rust-lang/crates.io-index"
737+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
738+dependencies = [
739+ "js-sys",
740+ "wasm-bindgen",
741+]
742+
743+[[package]]
744+name = "wildmatch"
745+version = "2.7.0"
746+source = "registry+https://github.com/rust-lang/crates.io-index"
747+checksum = "f18219fbd41a807dec044965d4106bb4fdb1d3a4b70aac4e5510aacc80a9f140"
748+
749+[[package]]
750+name = "winnow"
751+version = "1.0.4"
752+source = "registry+https://github.com/rust-lang/crates.io-index"
753+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
754+dependencies = [
755+ "memchr",
756+]
757+
758+[[package]]
759+name = "writeable"
760+version = "0.6.4"
761+source = "registry+https://github.com/rust-lang/crates.io-index"
762+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
763+
764+[[package]]
765+name = "yoke"
766+version = "0.8.4"
767+source = "registry+https://github.com/rust-lang/crates.io-index"
768+checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
769+dependencies = [
770+ "stable_deref_trait",
771+ "yoke-derive",
772+ "zerofrom",
773+]
774+
775+[[package]]
776+name = "yoke-derive"
777+version = "0.8.4"
778+source = "registry+https://github.com/rust-lang/crates.io-index"
779+checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
780+dependencies = [
781+ "proc-macro2",
782+ "quote",
783+ "syn 3.0.9",
784+ "synstructure",
785+]
786+
787+[[package]]
788+name = "zerofrom"
789+version = "0.1.8"
790+source = "registry+https://github.com/rust-lang/crates.io-index"
791+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
792+dependencies = [
793+ "zerofrom-derive",
794+]
795+
796+[[package]]
797+name = "zerofrom-derive"
798+version = "0.1.8"
799+source = "registry+https://github.com/rust-lang/crates.io-index"
800+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
801+dependencies = [
802+ "proc-macro2",
803+ "quote",
804+ "syn 3.0.9",
805+ "synstructure",
806+]
807+
808+[[package]]
809+name = "zeroize"
810+version = "1.9.1"
811+source = "registry+https://github.com/rust-lang/crates.io-index"
812+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
813+
814+[[package]]
815+name = "zerotrie"
816+version = "0.2.5"
817+source = "registry+https://github.com/rust-lang/crates.io-index"
818+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
819+dependencies = [
820+ "displaydoc",
821+ "yoke",
822+ "zerofrom",
823+]
824+
825+[[package]]
826+name = "zerovec"
827+version = "0.11.9"
828+source = "registry+https://github.com/rust-lang/crates.io-index"
829+checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
830+dependencies = [
831+ "yoke",
832+ "zerofrom",
833+ "zerovec-derive",
834+]
835+
836+[[package]]
837+name = "zerovec-derive"
838+version = "0.11.7"
839+source = "registry+https://github.com/rust-lang/crates.io-index"
840+checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
841+dependencies = [
842+ "proc-macro2",
843+ "quote",
844+ "syn 3.0.9",
845+]
846+
847+[[package]]
848+name = "zmij"
849+version = "1.0.23"
850+source = "registry+https://github.com/rust-lang/crates.io-index"
851+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
Added file: push-gateway/Cargo.toml+13 additions
@@ -0,0 +1,13 @@
1+[package]
2+name = "macltok-push-gateway"
3+version = "0.1.0"
4+edition = "2024"
5+description = "The Matrix push gateway wire format, without a Matrix client"
6+publish = false
7+
8+[workspace]
9+
10+[dependencies]
11+ruma-common = "0.19.0"
12+serde = { version = "1.0.219", features = ["derive"] }
13+serde_json = { version = "1.0", features = ["raw_value"] }
Added file: push-gateway/src/lib.rs+109 additions
@@ -0,0 +1,109 @@
1+//! the homeserver side of `POST /_matrix/push/v1/notify`, for a gateway that holds no session
2+
3+use ruma_common::{OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId};
4+use serde::{Deserialize, Serialize};
5+use serde_json::{Map, Value, value::RawValue};
6+
7+#[derive(Clone, Debug, Deserialize)]
8+#[non_exhaustive]
9+pub struct GatewayNotification {
10+ pub event_id: Option<OwnedEventId>,
11+ pub room_id: Option<OwnedRoomId>,
12+ #[serde(rename = "type")]
13+ pub event_type: Option<String>,
14+ pub sender: Option<OwnedUserId>,
15+ pub sender_display_name: Option<String>,
16+ pub room_name: Option<String>,
17+ pub room_alias: Option<OwnedRoomAliasId>,
18+ #[serde(default)]
19+ pub user_is_target: bool,
20+ #[serde(default)]
21+ pub prio: NotifyPrio,
22+ /// only in the full format, and still encrypted for an encrypted room
23+ pub content: Option<Box<RawValue>>,
24+ #[serde(default)]
25+ pub counts: NotifyCounts,
26+ #[serde(default)]
27+ pub devices: Vec<GatewayDevice>,
28+}
29+
30+impl GatewayNotification {
31+ /// a count-only push carries no event, and iOS punishes a web push that shows nothing
32+ pub fn is_event(&self) -> bool {
33+ self.event_id.is_some()
34+ }
35+}
36+
37+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
38+#[serde(rename_all = "lowercase")]
39+pub enum NotifyPrio {
40+ #[default]
41+ High,
42+ Low,
43+}
44+
45+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
46+#[non_exhaustive]
47+pub struct NotifyCounts {
48+ #[serde(default)]
49+ pub unread: u64,
50+ #[serde(default)]
51+ pub missed_calls: u64,
52+}
53+
54+#[derive(Clone, Debug, Deserialize)]
55+#[non_exhaustive]
56+pub struct GatewayDevice {
57+ pub app_id: String,
58+ pub pushkey: String,
59+ pub pushkey_ts: Option<u64>,
60+ /// the pusher's data as it was set, `format` included
61+ #[serde(default)]
62+ pub data: Map<String, Value>,
63+ #[serde(default)]
64+ pub tweaks: Map<String, Value>,
65+}
66+
67+#[derive(Deserialize)]
68+struct NotifyBody {
69+ notification: GatewayNotification,
70+}
71+
72+pub fn parse_notify(body: &[u8]) -> Result<GatewayNotification, serde_json::Error> {
73+ serde_json::from_slice::<NotifyBody>(body).map(|body| body.notification)
74+}
75+
76+/// the pushkeys the homeserver should drop, such as one whose web push endpoint answered 404 or 410
77+pub fn notify_response(rejected: &[String]) -> Vec<u8> {
78+ #[derive(Serialize)]
79+ struct Response<'a> {
80+ rejected: &'a [String],
81+ }
82+ serde_json::to_vec(&Response { rejected }).expect("a list of strings serialises")
83+}
84+
85+#[cfg(test)]
86+mod tests {
87+ use super::*;
88+
89+ #[test]
90+ fn a_count_only_push_is_no_event_and_devices_keep_their_data() {
91+ let body = br#"{"notification":{"counts":{"unread":2},"devices":[{"app_id":"me.malice","pushkey":"https://push.example/abc","data":{"format":"event_id_only","endpoint":"https://push.example/abc","auth":"secret"},"tweaks":{"sound":"default"}}]}}"#;
92+ let notification = parse_notify(body).unwrap();
93+ assert!(!notification.is_event());
94+ assert_eq!(notification.counts.unread, 2);
95+ assert_eq!(notification.prio, NotifyPrio::High);
96+ assert_eq!(notification.devices[0].data["auth"], "secret");
97+ assert_eq!(notification.devices[0].tweaks["sound"], "default");
98+
99+ let body = br#"{"notification":{"event_id":"$e","room_id":"!r:example.org","type":"m.room.message","sender":"@a:example.org","prio":"low","content":{"body":"hi"},"devices":[]}}"#;
100+ let notification = parse_notify(body).unwrap();
101+ assert!(notification.is_event());
102+ assert_eq!(notification.prio, NotifyPrio::Low);
103+ assert_eq!(notification.content.unwrap().get(), r#"{"body":"hi"}"#);
104+ assert_eq!(
105+ notify_response(&["gone".to_owned()]),
106+ br#"{"rejected":["gone"]}"#
107+ );
108+ }
109+}
Modified file: src/engine/mod.rs+6 additions−1 deletions
@@ -754,12 +754,17 @@ impl Engine {
754754 let client = self.client.clone();
755755 match command {
756756 Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
757- Command::Account(AccountCommand::Logout) => {
757+ Command::Account(AccountCommand::Logout { remove_pushers }) => {
758758 let events = self.port.event_sender();
759759 let stop = self.stop.clone();
760760 let logouts = self.logouts.clone();
761761 logouts.send_modify(|logouts| logouts.in_flight += 1);
762762 self.spawn_command("logout", reply, async move {
763+ for pusher in remove_pushers {
764+ if let Err(error) = crate::transport::remove_pusher(&client, pusher).await {
765+ tracing::debug!(%error, "removing a pusher before logout failed");
766+ }
767+ }
763768 let result = client.logout().await;
764769 if result.is_ok() {
765770 let _ = events.send(Event::LoggedOut).await;
Modified file: src/lib.rs+16 additions−15 deletions
@@ -60,21 +60,22 @@ pub use model::{
6060 AccountCommand, AttachmentEncryption, AttachmentUpload, Change, Command, CommandOutput,
6161 ConnectionState, CreateRoomOptions, EncryptionCommand, EncryptionState, EngineErrorKind,
6262 EngineFailure, Event, EventContext, EventContextCloseReason, EventContextCommand,
63- EventContextPage, EventContextSession, EventRef, HistoryState, ImagePack, ImagePackSource,
64- ImagePacks, KeyBackupState, LatestEvent, LifecycleCommand, LinkPreview, LinkPreviewImage,
65- Media, MediaCommand, MediaDescriptor, MediaFile, MediaId, MediaKind, MediaLocation, MediaSize,
66- MemberFilter, MemberMembership, MemberSummary, MembershipChange, Message, MessageContent,
67- MessageEditContent, MessageKind, MessageMentions, NotificationCommand, OwnDevice, PackCommand,
68- PackImage, PackImageInfo, PackUsage, Patch, PowerLevelChanges, Profile, PushNotification,
69- PushNotificationEvent, PushNotificationRoom, PushNotificationSender, PushNotificationStatus,
70- Reaction, RecentHistoryState, RecoveryKey, RecoveryState, ReplyDetails, ReplyReference, Retain,
71- RichText, RoomCategory, RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership,
72- RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary,
73- RoomVisibility, SendState, Sender, SpaceChild, Termination, ThreadListCloseReason,
74- ThreadListCommand, ThreadListEntry, ThreadSummary, ThreadUnread, TimelineCommand,
75- TimelineContent, TimelineDiff, TimelineDiffError, TimelineEvent, TimelineId, TimelineItem,
76- TimelineItemId, TypingState, UploadDetails, UploadOutcome, UploadThumbnail, UtdCause,
77- VerificationAction, VerificationFailure, VerificationState, VerificationTarget,
63+ EventContextPage, EventContextSession, EventRef, HistoryState, HttpPusher, ImagePack,
64+ ImagePackSource, ImagePacks, KeyBackupState, LatestEvent, LifecycleCommand, LinkPreview,
65+ LinkPreviewImage, Media, MediaCommand, MediaDescriptor, MediaFile, MediaId, MediaKind,
66+ MediaLocation, MediaSize, MemberFilter, MemberMembership, MemberSummary, MembershipChange,
67+ Message, MessageContent, MessageEditContent, MessageKind, MessageMentions, NotificationCommand,
68+ OwnDevice, PackCommand, PackImage, PackImageInfo, PackUsage, Patch, PowerLevelChanges, Profile,
69+ PushFormat, PushNotification, PushNotificationEvent, PushNotificationRoom,
70+ PushNotificationSender, PushNotificationStatus, PusherKey, Reaction, RecentHistoryState,
71+ RecoveryKey, RecoveryState, ReplyDetails, ReplyReference, Retain, RichText, RoomCategory,
72+ RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,
73+ RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState,
74+ Sender, SpaceChild, Termination, ThreadListCloseReason, ThreadListCommand, ThreadListEntry,
75+ ThreadSummary, ThreadUnread, TimelineCommand, TimelineContent, TimelineDiff, TimelineDiffError,
76+ TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState, UploadDetails,
77+ UploadOutcome, UploadThumbnail, UtdCause, VerificationAction, VerificationFailure,
78+ VerificationState, VerificationTarget,
7879 };
7980 pub use repair_model::{
8081 HistoryContinuity, HistoryGap, HistoryGapLifecycle, HistoryGapPause, HistoryRepairCommand,
Modified file: src/model.rs+36 additions−1 deletions
@@ -66,7 +66,11 @@ pub enum EventContextCommand {
6666 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6767 #[non_exhaustive]
6868 pub enum AccountCommand {
69- Logout,
69+ /// removes `remove_pushers` first, best effort, while the token still works. pushers belong
70+ /// to the user rather than the device, so only the caller knows which are this browser's
71+ Logout {
72+ remove_pushers: Vec<PusherKey>,
73+ },
7074 SetAccountData {
7175 event_type: GlobalAccountDataEventType,
7276 content: Raw<AnyGlobalAccountDataEventContent>,
@@ -1271,6 +1275,9 @@ pub struct PushNotificationSender {
12711275 #[non_exhaustive]
12721276 pub struct PushNotificationRoom {
12731277 pub room_id: OwnedRoomId,
1278+ /// from this session's store, so `None` for a room it hasn't synced yet
1279+ pub unread_notifications: Option<u64>,
1280+ pub unread_mentions: Option<u64>,
12741281 pub name: String,
12751282 pub avatar_url: Option<MediaId>,
12761283 pub canonical_alias: Option<OwnedRoomAliasId>,
@@ -2417,3 +2424,31 @@ pub enum EngineErrorKind {
24172424 Watcher,
24182425 Other,
24192426 }
2427+
2428+#[derive(Clone, Debug, Eq, PartialEq)]
2429+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2430+pub struct PusherKey {
2431+ pub app_id: String,
2432+ pub pushkey: String,
2433+}
2434+
2435+#[derive(Clone, Debug, PartialEq)]
2436+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2437+pub struct HttpPusher {
2438+ pub app_id: String,
2439+ pub pushkey: String,
2440+ pub app_display_name: String,
2441+ pub device_display_name: String,
2442+ pub lang: String,
2443+ pub url: String,
2444+ pub format: PushFormat,
2445+ /// sent beside `url` and `format` in the pusher's data, and back to the gateway in every push
2446+ pub data: serde_json::Map<String, serde_json::Value>,
2447+}
2448+
2449+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2450+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2451+pub enum PushFormat {
2452+ Full,
2453+ EventIdOnly,
2454+}
Modified file: src/transport.rs+97 additions−13 deletions
@@ -29,16 +29,17 @@ use tokio::sync::{OnceCell, mpsc, oneshot, watch};
2929 use crate::{
3030 AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,
3131 EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventContextCommand,
32- EventContextPage, EventContextSession, EventRef, HistoryRepairCommand, ImagePackSource,
33- ImagePacks, InteractiveAuth, InteractiveAuthOutcome, LifecycleCommand, LinkPreview, Media,
34- MediaCommand, MediaId, MediaLocation, MediaSize, MemberFilter, MemberSummary, MentionKey,
35- MentionsBoundary, MentionsCommand, MentionsPage, MessageContent, MessageEditContent,
36- NotificationCommand, OwnDevice, PackCommand, PackImageInfo, PackUsage, Patch,
37- PowerLevelChanges, Profile, PushNotification, PushNotificationEvent, PushNotificationStatus,
38- RecoveryKey, RoomCommand, RoomListFilter, RoomNotificationMode, RoomNotificationSettings,
39- RoomPowerLevels, RoomSummary, SavedMentionContext, SearchCommand, SearchCoverage, SearchPage,
40- SearchScope, SearchSession, SpaceChild, ThreadListCommand, TimelineCommand, TimelineId,
41- VerificationAction, VerificationTarget,
32+ EventContextPage, EventContextSession, EventRef, HistoryRepairCommand, HttpPusher,
33+ ImagePackSource, ImagePacks, InteractiveAuth, InteractiveAuthOutcome, LifecycleCommand,
34+ LinkPreview, Media, MediaCommand, MediaId, MediaLocation, MediaSize, MemberFilter,
35+ MemberSummary, MentionKey, MentionsBoundary, MentionsCommand, MentionsPage, MessageContent,
36+ MessageEditContent, NotificationCommand, OwnDevice, PackCommand, PackImageInfo, PackUsage,
37+ Patch, PowerLevelChanges, Profile, PushFormat, PushNotification, PushNotificationEvent,
38+ PushNotificationStatus, PusherKey, RecoveryKey, RoomCommand, RoomListFilter,
39+ RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomSummary,
40+ SavedMentionContext, SearchCommand, SearchCoverage, SearchPage, SearchScope, SearchSession,
41+ SpaceChild, ThreadListCommand, TimelineCommand, TimelineId, VerificationAction,
42+ VerificationTarget,
4243 encryption::encryption_state,
4344 engine::{MediaService, StoreLock},
4445 model::{PushNotificationRoom, PushNotificationSender},
@@ -149,8 +150,73 @@ impl ClientHandle {
149150 .await
150151 }
151152
152- pub async fn logout(&self) -> Result<(), Error> {
153- self.run(Command::Account(AccountCommand::Logout)).await
153+ pub async fn logout(&self, remove_pushers: Vec<PusherKey>) -> Result<(), Error> {
154+ self.run(Command::Account(AccountCommand::Logout { remove_pushers }))
155+ .await
156+ }
157+
158+ /// replaces any pusher with the same app id and pushkey
159+ pub async fn set_pusher(&self, pusher: HttpPusher) -> Result<(), Error> {
160+ use ruma::{
161+ api::client::push::{PusherIds, PusherInit, PusherKind},
162+ push::{HttpPusherData, PushFormat as SdkFormat},
163+ };
164+ let mut data = HttpPusherData::new(pusher.url);
165+ data.format = match pusher.format {
166+ PushFormat::Full => None,
167+ PushFormat::EventIdOnly => Some(SdkFormat::EventIdOnly),
168+ };
169+ data.data = pusher.data;
170+ let pusher = PusherInit {
171+ ids: PusherIds::new(pusher.pushkey, pusher.app_id),
172+ kind: PusherKind::Http(data),
173+ app_display_name: pusher.app_display_name,
174+ device_display_name: pusher.device_display_name,
175+ profile_tag: None,
176+ lang: pusher.lang,
177+ };
178+ self.client
179+ .pusher()
180+ .set(pusher.into(), false)
181+ .await
182+ .map_err(|source| Error::from_sdk("setting the pusher failed", source))
183+ }
184+
185+ pub async fn remove_pusher(&self, pusher: PusherKey) -> Result<(), Error> {
186+ remove_pusher(&self.client, pusher).await
187+ }
188+
189+ /// the account's http pushers, every device's alike
190+ pub async fn pushers(&self) -> Result<Vec<HttpPusher>, Error> {
191+ use ruma::{
192+ api::client::push::{PusherKind, get_pushers},
193+ push::PushFormat as SdkFormat,
194+ };
195+ let response = self
196+ .client
197+ .send(get_pushers::v3::Request::new())
198+ .await
199+ .map_err(|source| Error::from_sdk("fetching pushers failed", source.into()))?;
200+ Ok(response
201+ .pushers
202+ .into_iter()
203+ .filter_map(|pusher| match pusher.kind {
204+ PusherKind::Http(data) => Some(HttpPusher {
205+ app_id: pusher.ids.app_id,
206+ pushkey: pusher.ids.pushkey,
207+ app_display_name: pusher.app_display_name,
208+ device_display_name: pusher.device_display_name,
209+ lang: pusher.lang,
210+ url: data.url,
211+ format: match data.format {
212+ Some(SdkFormat::EventIdOnly) => PushFormat::EventIdOnly,
213+ _ => PushFormat::Full,
214+ },
215+ data: data.data,
216+ }),
217+ _ => None,
218+ })
219+ .collect())
154220 }
155221
156222 pub async fn delete_devices(
@@ -1341,7 +1407,13 @@ impl ClientHandle {
13411407 .get_notification(room_id, event_id)
13421408 .await
13431409 .map_err(notification_failed)?;
1344- Ok(normalise_push_notification(room_id, event_id, status))
1410+ let unread = self
1411+ .client
1412+ .get_room(room_id)
1413+ .map(|room| (room.num_unread_notifications(), room.num_unread_mentions()));
1414+ Ok(normalise_push_notification(
1415+ room_id, event_id, status, unread,
1416+ ))
13451417 }
13461418
13471419 pub async fn members(
@@ -1844,6 +1916,7 @@ fn normalise_push_notification(
18441916 room_id: &RoomId,
18451917 event_id: &EventId,
18461918 status: NotificationStatus,
1919+ unread: Option<(u64, u64)>,
18471920 ) -> PushNotificationStatus {
18481921 match status {
18491922 NotificationStatus::Event(item) => {
@@ -1864,6 +1937,8 @@ fn normalise_push_notification(
18641937 },
18651938 room: PushNotificationRoom {
18661939 room_id: room_id.to_owned(),
1940+ unread_notifications: unread.map(|(notifications, _)| notifications),
1941+ unread_mentions: unread.map(|(_, mentions)| mentions),
18671942 name: item.room_computed_display_name,
18681943 avatar_url: item.room_avatar_url.as_deref().map(MediaId::plain),
18691944 canonical_alias: item
@@ -1893,6 +1968,15 @@ fn normalise_push_notification(
18931968 }
18941969 }
18951970
1971+pub(crate) async fn remove_pusher(client: &Client, pusher: PusherKey) -> Result<(), Error> {
1972+ use ruma::api::client::push::PusherIds;
1973+ client
1974+ .pusher()
1975+ .delete(PusherIds::new(pusher.pushkey, pusher.app_id))
1976+ .await
1977+ .map_err(|source| Error::from_sdk("removing the pusher failed", source))
1978+}
1979+
18961980 fn unexpected(wanted: &str, output: &CommandOutput) -> Error {
18971981 Error::new(
18981982 ErrorKind::Other,