test_push_rule_evaluator.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  1. # Copyright 2020 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Any, Dict, List, Optional, Union, cast
  15. from twisted.test.proto_helpers import MemoryReactor
  16. import synapse.rest.admin
  17. from synapse.api.constants import EventTypes, HistoryVisibility, Membership
  18. from synapse.api.room_versions import RoomVersions
  19. from synapse.appservice import ApplicationService
  20. from synapse.events import FrozenEvent, make_event_from_dict
  21. from synapse.push.bulk_push_rule_evaluator import _flatten_dict
  22. from synapse.push.httppusher import tweaks_for_actions
  23. from synapse.rest import admin
  24. from synapse.rest.client import login, register, room
  25. from synapse.server import HomeServer
  26. from synapse.storage.databases.main.appservice import _make_exclusive_regex
  27. from synapse.synapse_rust.push import PushRuleEvaluator
  28. from synapse.types import JsonDict, JsonMapping, UserID
  29. from synapse.util import Clock
  30. from synapse.util.frozenutils import freeze
  31. from tests import unittest
  32. from tests.test_utils.event_injection import create_event, inject_member_event
  33. class FlattenDictTestCase(unittest.TestCase):
  34. def test_simple(self) -> None:
  35. """Test a dictionary that isn't modified."""
  36. input = {"foo": "abc"}
  37. self.assertEqual(input, _flatten_dict(input))
  38. def test_nested(self) -> None:
  39. """Nested dictionaries become dotted paths."""
  40. input = {"foo": {"bar": "abc"}}
  41. self.assertEqual({"foo.bar": "abc"}, _flatten_dict(input))
  42. # If a field has a dot in it, escape it.
  43. input = {"m.foo": {"b\\ar": "abc"}}
  44. self.assertEqual({"m\\.foo.b\\\\ar": "abc"}, _flatten_dict(input))
  45. def test_non_string(self) -> None:
  46. """String, booleans, ints, nulls and list of those should be kept while other items are dropped."""
  47. input: Dict[str, Any] = {
  48. "woo": "woo",
  49. "foo": True,
  50. "bar": 1,
  51. "baz": None,
  52. "fuzz": ["woo", True, 1, None, [], {}],
  53. "boo": {},
  54. }
  55. self.assertEqual(
  56. {
  57. "woo": "woo",
  58. "foo": True,
  59. "bar": 1,
  60. "baz": None,
  61. "fuzz": ["woo", True, 1, None],
  62. },
  63. _flatten_dict(input),
  64. )
  65. def test_event(self) -> None:
  66. """Events can also be flattened."""
  67. event = make_event_from_dict(
  68. {
  69. "room_id": "!test:test",
  70. "type": "m.room.message",
  71. "sender": "@alice:test",
  72. "content": {
  73. "msgtype": "m.text",
  74. "body": "Hello world!",
  75. "format": "org.matrix.custom.html",
  76. "formatted_body": "<h1>Hello world!</h1>",
  77. },
  78. },
  79. room_version=RoomVersions.V8,
  80. )
  81. expected = {
  82. "content.msgtype": "m.text",
  83. "content.body": "Hello world!",
  84. "content.format": "org.matrix.custom.html",
  85. "content.formatted_body": "<h1>Hello world!</h1>",
  86. "room_id": "!test:test",
  87. "sender": "@alice:test",
  88. "type": "m.room.message",
  89. }
  90. self.assertEqual(expected, _flatten_dict(event))
  91. def test_extensible_events(self) -> None:
  92. """Extensible events has compatibility behaviour."""
  93. event_dict = {
  94. "room_id": "!test:test",
  95. "type": "m.room.message",
  96. "sender": "@alice:test",
  97. "content": {
  98. "org.matrix.msc1767.markup": [
  99. {"mimetype": "text/plain", "body": "Hello world!"},
  100. {"mimetype": "text/html", "body": "<h1>Hello world!</h1>"},
  101. ]
  102. },
  103. }
  104. # For a current room version, there's no special behavior.
  105. event = make_event_from_dict(event_dict, room_version=RoomVersions.V8)
  106. expected = {
  107. "room_id": "!test:test",
  108. "sender": "@alice:test",
  109. "type": "m.room.message",
  110. "content.org\\.matrix\\.msc1767\\.markup": [],
  111. }
  112. self.assertEqual(expected, _flatten_dict(event))
  113. # For a room version with extensible events, they parse out the text/plain
  114. # to a content.body property.
  115. event = make_event_from_dict(event_dict, room_version=RoomVersions.MSC1767v10)
  116. expected = {
  117. "content.body": "hello world!",
  118. "room_id": "!test:test",
  119. "sender": "@alice:test",
  120. "type": "m.room.message",
  121. "content.org\\.matrix\\.msc1767\\.markup": [],
  122. }
  123. self.assertEqual(expected, _flatten_dict(event))
  124. class PushRuleEvaluatorTestCase(unittest.TestCase):
  125. def _get_evaluator(
  126. self,
  127. content: JsonMapping,
  128. *,
  129. related_events: Optional[JsonDict] = None,
  130. ) -> PushRuleEvaluator:
  131. event = FrozenEvent(
  132. {
  133. "event_id": "$event_id",
  134. "type": "m.room.history_visibility",
  135. "sender": "@user:test",
  136. "state_key": "",
  137. "room_id": "#room:test",
  138. "content": content,
  139. },
  140. RoomVersions.V1,
  141. )
  142. room_member_count = 0
  143. sender_power_level = 0
  144. power_levels: Dict[str, Union[int, Dict[str, int]]] = {}
  145. return PushRuleEvaluator(
  146. _flatten_dict(event),
  147. False,
  148. room_member_count,
  149. sender_power_level,
  150. cast(Dict[str, int], power_levels.get("notifications", {})),
  151. {} if related_events is None else related_events,
  152. related_event_match_enabled=True,
  153. room_version_feature_flags=event.room_version.msc3931_push_features,
  154. msc3931_enabled=True,
  155. )
  156. def test_display_name(self) -> None:
  157. """Check for a matching display name in the body of the event."""
  158. evaluator = self._get_evaluator({"body": "foo bar baz"})
  159. condition = {"kind": "contains_display_name"}
  160. # Blank names are skipped.
  161. self.assertFalse(evaluator.matches(condition, "@user:test", ""))
  162. # Check a display name that doesn't match.
  163. self.assertFalse(evaluator.matches(condition, "@user:test", "not found"))
  164. # Check a display name which matches.
  165. self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))
  166. # A display name that matches, but not a full word does not result in a match.
  167. self.assertFalse(evaluator.matches(condition, "@user:test", "ba"))
  168. # A display name should not be interpreted as a regular expression.
  169. self.assertFalse(evaluator.matches(condition, "@user:test", "ba[rz]"))
  170. # A display name with spaces should work fine.
  171. self.assertTrue(evaluator.matches(condition, "@user:test", "foo bar"))
  172. def _assert_matches(
  173. self, condition: JsonDict, content: JsonMapping, msg: Optional[str] = None
  174. ) -> None:
  175. evaluator = self._get_evaluator(content)
  176. self.assertTrue(evaluator.matches(condition, "@user:test", "display_name"), msg)
  177. def _assert_not_matches(
  178. self, condition: JsonDict, content: JsonDict, msg: Optional[str] = None
  179. ) -> None:
  180. evaluator = self._get_evaluator(content)
  181. self.assertFalse(
  182. evaluator.matches(condition, "@user:test", "display_name"), msg
  183. )
  184. def test_event_match_body(self) -> None:
  185. """Check that event_match conditions on content.body work as expected"""
  186. # if the key is `content.body`, the pattern matches substrings.
  187. # non-wildcards should match
  188. condition = {
  189. "kind": "event_match",
  190. "key": "content.body",
  191. "pattern": "foobaz",
  192. }
  193. self._assert_matches(
  194. condition,
  195. {"body": "aaa FoobaZ zzz"},
  196. "patterns should match and be case-insensitive",
  197. )
  198. self._assert_not_matches(
  199. condition,
  200. {"body": "aa xFoobaZ yy"},
  201. "pattern should only match at word boundaries",
  202. )
  203. self._assert_not_matches(
  204. condition,
  205. {"body": "aa foobazx yy"},
  206. "pattern should only match at word boundaries",
  207. )
  208. # wildcards should match
  209. condition = {
  210. "kind": "event_match",
  211. "key": "content.body",
  212. "pattern": "f?o*baz",
  213. }
  214. self._assert_matches(
  215. condition,
  216. {"body": "aaa FoobarbaZ zzz"},
  217. "* should match string and pattern should be case-insensitive",
  218. )
  219. self._assert_matches(
  220. condition, {"body": "aa foobaz yy"}, "* should match 0 characters"
  221. )
  222. self._assert_not_matches(
  223. condition, {"body": "aa fobbaz yy"}, "? should not match 0 characters"
  224. )
  225. self._assert_not_matches(
  226. condition, {"body": "aa fiiobaz yy"}, "? should not match 2 characters"
  227. )
  228. self._assert_not_matches(
  229. condition,
  230. {"body": "aa xfooxbaz yy"},
  231. "pattern should only match at word boundaries",
  232. )
  233. self._assert_not_matches(
  234. condition,
  235. {"body": "aa fooxbazx yy"},
  236. "pattern should only match at word boundaries",
  237. )
  238. # test backslashes
  239. condition = {
  240. "kind": "event_match",
  241. "key": "content.body",
  242. "pattern": r"f\oobaz",
  243. }
  244. self._assert_matches(
  245. condition,
  246. {"body": r"F\oobaz"},
  247. "backslash should match itself",
  248. )
  249. condition = {
  250. "kind": "event_match",
  251. "key": "content.body",
  252. "pattern": r"f\?obaz",
  253. }
  254. self._assert_matches(
  255. condition,
  256. {"body": r"F\oobaz"},
  257. r"? after \ should match any character",
  258. )
  259. def test_event_match_non_body(self) -> None:
  260. """Check that event_match conditions on other keys work as expected"""
  261. # if the key is anything other than 'content.body', the pattern must match the
  262. # whole value.
  263. # non-wildcards should match
  264. condition = {
  265. "kind": "event_match",
  266. "key": "content.value",
  267. "pattern": "foobaz",
  268. }
  269. self._assert_matches(
  270. condition,
  271. {"value": "FoobaZ"},
  272. "patterns should match and be case-insensitive",
  273. )
  274. self._assert_not_matches(
  275. condition,
  276. {"value": "xFoobaZ"},
  277. "pattern should only match at the start/end of the value",
  278. )
  279. self._assert_not_matches(
  280. condition,
  281. {"value": "FoobaZz"},
  282. "pattern should only match at the start/end of the value",
  283. )
  284. # it should work on frozen dictionaries too
  285. self._assert_matches(
  286. condition,
  287. freeze({"value": "FoobaZ"}),
  288. "patterns should match on frozen dictionaries",
  289. )
  290. # wildcards should match
  291. condition = {
  292. "kind": "event_match",
  293. "key": "content.value",
  294. "pattern": "f?o*baz",
  295. }
  296. self._assert_matches(
  297. condition,
  298. {"value": "FoobarbaZ"},
  299. "* should match string and pattern should be case-insensitive",
  300. )
  301. self._assert_matches(
  302. condition, {"value": "foobaz"}, "* should match 0 characters"
  303. )
  304. self._assert_not_matches(
  305. condition, {"value": "fobbaz"}, "? should not match 0 characters"
  306. )
  307. self._assert_not_matches(
  308. condition, {"value": "fiiobaz"}, "? should not match 2 characters"
  309. )
  310. self._assert_not_matches(
  311. condition,
  312. {"value": "xfooxbaz"},
  313. "pattern should only match at the start/end of the value",
  314. )
  315. self._assert_not_matches(
  316. condition,
  317. {"value": "fooxbazx"},
  318. "pattern should only match at the start/end of the value",
  319. )
  320. self._assert_not_matches(
  321. condition,
  322. {"value": "x\nfooxbaz"},
  323. "pattern should not match after a newline",
  324. )
  325. self._assert_not_matches(
  326. condition,
  327. {"value": "fooxbaz\nx"},
  328. "pattern should not match before a newline",
  329. )
  330. def test_event_match_pattern(self) -> None:
  331. """Check that event_match conditions do not use a "pattern_type" from user data."""
  332. # The pattern_type should not be deserialized into anything valid.
  333. condition = {
  334. "kind": "event_match",
  335. "key": "content.value",
  336. "pattern_type": "user_id",
  337. }
  338. self._assert_not_matches(
  339. condition,
  340. {"value": "@user:test"},
  341. "should not be possible to pass a pattern_type in",
  342. )
  343. # This is an internal-only condition which shouldn't get deserialized.
  344. condition = {
  345. "kind": "event_match_type",
  346. "key": "content.value",
  347. "pattern_type": "user_id",
  348. }
  349. self._assert_not_matches(
  350. condition,
  351. {"value": "@user:test"},
  352. "should not be possible to pass a pattern_type in",
  353. )
  354. def test_exact_event_match_string(self) -> None:
  355. """Check that exact_event_match conditions work as expected for strings."""
  356. # Test against a string value.
  357. condition = {
  358. "kind": "event_property_is",
  359. "key": "content.value",
  360. "value": "foobaz",
  361. }
  362. self._assert_matches(
  363. condition,
  364. {"value": "foobaz"},
  365. "exact value should match",
  366. )
  367. self._assert_not_matches(
  368. condition,
  369. {"value": "FoobaZ"},
  370. "values should match and be case-sensitive",
  371. )
  372. self._assert_not_matches(
  373. condition,
  374. {"value": "test foobaz test"},
  375. "values must exactly match",
  376. )
  377. value: Any
  378. for value in (True, False, 1, 1.1, None, [], {}):
  379. self._assert_not_matches(
  380. condition,
  381. {"value": value},
  382. "incorrect types should not match",
  383. )
  384. # it should work on frozen dictionaries too
  385. self._assert_matches(
  386. condition,
  387. freeze({"value": "foobaz"}),
  388. "values should match on frozen dictionaries",
  389. )
  390. def test_exact_event_match_boolean(self) -> None:
  391. """Check that exact_event_match conditions work as expected for booleans."""
  392. # Test against a True boolean value.
  393. condition = {"kind": "event_property_is", "key": "content.value", "value": True}
  394. self._assert_matches(
  395. condition,
  396. {"value": True},
  397. "exact value should match",
  398. )
  399. self._assert_not_matches(
  400. condition,
  401. {"value": False},
  402. "incorrect values should not match",
  403. )
  404. for value in ("foobaz", 1, 1.1, None, [], {}):
  405. self._assert_not_matches(
  406. condition,
  407. {"value": value},
  408. "incorrect types should not match",
  409. )
  410. # Test against a False boolean value.
  411. condition = {
  412. "kind": "event_property_is",
  413. "key": "content.value",
  414. "value": False,
  415. }
  416. self._assert_matches(
  417. condition,
  418. {"value": False},
  419. "exact value should match",
  420. )
  421. self._assert_not_matches(
  422. condition,
  423. {"value": True},
  424. "incorrect values should not match",
  425. )
  426. # Choose false-y values to ensure there's no type coercion.
  427. for value in ("", 0, 1.1, None, [], {}):
  428. self._assert_not_matches(
  429. condition,
  430. {"value": value},
  431. "incorrect types should not match",
  432. )
  433. def test_exact_event_match_null(self) -> None:
  434. """Check that exact_event_match conditions work as expected for null."""
  435. condition = {"kind": "event_property_is", "key": "content.value", "value": None}
  436. self._assert_matches(
  437. condition,
  438. {"value": None},
  439. "exact value should match",
  440. )
  441. for value in ("foobaz", True, False, 1, 1.1, [], {}):
  442. self._assert_not_matches(
  443. condition,
  444. {"value": value},
  445. "incorrect types should not match",
  446. )
  447. def test_exact_event_match_integer(self) -> None:
  448. """Check that exact_event_match conditions work as expected for integers."""
  449. condition = {"kind": "event_property_is", "key": "content.value", "value": 1}
  450. self._assert_matches(
  451. condition,
  452. {"value": 1},
  453. "exact value should match",
  454. )
  455. value: Any
  456. for value in (1.1, -1, 0):
  457. self._assert_not_matches(
  458. condition,
  459. {"value": value},
  460. "incorrect values should not match",
  461. )
  462. for value in ("1", True, False, None, [], {}):
  463. self._assert_not_matches(
  464. condition,
  465. {"value": value},
  466. "incorrect types should not match",
  467. )
  468. def test_exact_event_property_contains(self) -> None:
  469. """Check that exact_event_property_contains conditions work as expected."""
  470. condition = {
  471. "kind": "event_property_contains",
  472. "key": "content.value",
  473. "value": "foobaz",
  474. }
  475. self._assert_matches(
  476. condition,
  477. {"value": ["foobaz"]},
  478. "exact value should match",
  479. )
  480. self._assert_matches(
  481. condition,
  482. {"value": ["foobaz", "bugz"]},
  483. "extra values should match",
  484. )
  485. self._assert_not_matches(
  486. condition,
  487. {"value": ["FoobaZ"]},
  488. "values should match and be case-sensitive",
  489. )
  490. self._assert_not_matches(
  491. condition,
  492. {"value": "foobaz"},
  493. "does not search in a string",
  494. )
  495. # it should work on frozen dictionaries too
  496. self._assert_matches(
  497. condition,
  498. freeze({"value": ["foobaz"]}),
  499. "values should match on frozen dictionaries",
  500. )
  501. def test_no_body(self) -> None:
  502. """Not having a body shouldn't break the evaluator."""
  503. evaluator = self._get_evaluator({})
  504. condition = {
  505. "kind": "contains_display_name",
  506. }
  507. self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))
  508. def test_invalid_body(self) -> None:
  509. """A non-string body should not break the evaluator."""
  510. condition = {
  511. "kind": "contains_display_name",
  512. }
  513. for body in (1, True, {"foo": "bar"}):
  514. evaluator = self._get_evaluator({"body": body})
  515. self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))
  516. def test_tweaks_for_actions(self) -> None:
  517. """
  518. This tests the behaviour of tweaks_for_actions.
  519. """
  520. actions: List[Union[Dict[str, str], str]] = [
  521. {"set_tweak": "sound", "value": "default"},
  522. {"set_tweak": "highlight"},
  523. "notify",
  524. ]
  525. self.assertEqual(
  526. tweaks_for_actions(actions),
  527. {"sound": "default", "highlight": True},
  528. )
  529. def test_related_event_match(self) -> None:
  530. evaluator = self._get_evaluator(
  531. {
  532. "m.relates_to": {
  533. "event_id": "$parent_event_id",
  534. "key": "😀",
  535. "rel_type": "m.annotation",
  536. "m.in_reply_to": {
  537. "event_id": "$parent_event_id",
  538. },
  539. }
  540. },
  541. related_events={
  542. "m.in_reply_to": {
  543. "event_id": "$parent_event_id",
  544. "type": "m.room.message",
  545. "sender": "@other_user:test",
  546. "room_id": "!room:test",
  547. "content.msgtype": "m.text",
  548. "content.body": "Original message",
  549. },
  550. "m.annotation": {
  551. "event_id": "$parent_event_id",
  552. "type": "m.room.message",
  553. "sender": "@other_user:test",
  554. "room_id": "!room:test",
  555. "content.msgtype": "m.text",
  556. "content.body": "Original message",
  557. },
  558. },
  559. )
  560. self.assertTrue(
  561. evaluator.matches(
  562. {
  563. "kind": "im.nheko.msc3664.related_event_match",
  564. "key": "sender",
  565. "rel_type": "m.in_reply_to",
  566. "pattern": "@other_user:test",
  567. },
  568. "@user:test",
  569. "display_name",
  570. )
  571. )
  572. self.assertFalse(
  573. evaluator.matches(
  574. {
  575. "kind": "im.nheko.msc3664.related_event_match",
  576. "key": "sender",
  577. "rel_type": "m.in_reply_to",
  578. "pattern": "@user:test",
  579. },
  580. "@other_user:test",
  581. "display_name",
  582. )
  583. )
  584. self.assertTrue(
  585. evaluator.matches(
  586. {
  587. "kind": "im.nheko.msc3664.related_event_match",
  588. "key": "sender",
  589. "rel_type": "m.annotation",
  590. "pattern": "@other_user:test",
  591. },
  592. "@other_user:test",
  593. "display_name",
  594. )
  595. )
  596. self.assertFalse(
  597. evaluator.matches(
  598. {
  599. "kind": "im.nheko.msc3664.related_event_match",
  600. "key": "sender",
  601. "rel_type": "m.in_reply_to",
  602. },
  603. "@user:test",
  604. "display_name",
  605. )
  606. )
  607. self.assertTrue(
  608. evaluator.matches(
  609. {
  610. "kind": "im.nheko.msc3664.related_event_match",
  611. "rel_type": "m.in_reply_to",
  612. },
  613. "@user:test",
  614. "display_name",
  615. )
  616. )
  617. self.assertFalse(
  618. evaluator.matches(
  619. {
  620. "kind": "im.nheko.msc3664.related_event_match",
  621. "rel_type": "m.replace",
  622. },
  623. "@other_user:test",
  624. "display_name",
  625. )
  626. )
  627. def test_related_event_match_with_fallback(self) -> None:
  628. evaluator = self._get_evaluator(
  629. {
  630. "m.relates_to": {
  631. "event_id": "$parent_event_id",
  632. "key": "😀",
  633. "rel_type": "m.thread",
  634. "is_falling_back": True,
  635. "m.in_reply_to": {
  636. "event_id": "$parent_event_id",
  637. },
  638. }
  639. },
  640. related_events={
  641. "m.in_reply_to": {
  642. "event_id": "$parent_event_id",
  643. "type": "m.room.message",
  644. "sender": "@other_user:test",
  645. "room_id": "!room:test",
  646. "content.msgtype": "m.text",
  647. "content.body": "Original message",
  648. "im.vector.is_falling_back": "",
  649. },
  650. "m.thread": {
  651. "event_id": "$parent_event_id",
  652. "type": "m.room.message",
  653. "sender": "@other_user:test",
  654. "room_id": "!room:test",
  655. "content.msgtype": "m.text",
  656. "content.body": "Original message",
  657. },
  658. },
  659. )
  660. self.assertTrue(
  661. evaluator.matches(
  662. {
  663. "kind": "im.nheko.msc3664.related_event_match",
  664. "key": "sender",
  665. "rel_type": "m.in_reply_to",
  666. "pattern": "@other_user:test",
  667. "include_fallbacks": True,
  668. },
  669. "@user:test",
  670. "display_name",
  671. )
  672. )
  673. self.assertFalse(
  674. evaluator.matches(
  675. {
  676. "kind": "im.nheko.msc3664.related_event_match",
  677. "key": "sender",
  678. "rel_type": "m.in_reply_to",
  679. "pattern": "@other_user:test",
  680. "include_fallbacks": False,
  681. },
  682. "@user:test",
  683. "display_name",
  684. )
  685. )
  686. self.assertFalse(
  687. evaluator.matches(
  688. {
  689. "kind": "im.nheko.msc3664.related_event_match",
  690. "key": "sender",
  691. "rel_type": "m.in_reply_to",
  692. "pattern": "@other_user:test",
  693. },
  694. "@user:test",
  695. "display_name",
  696. )
  697. )
  698. def test_related_event_match_no_related_event(self) -> None:
  699. evaluator = self._get_evaluator(
  700. {"msgtype": "m.text", "body": "Message without related event"}
  701. )
  702. self.assertFalse(
  703. evaluator.matches(
  704. {
  705. "kind": "im.nheko.msc3664.related_event_match",
  706. "key": "sender",
  707. "rel_type": "m.in_reply_to",
  708. "pattern": "@other_user:test",
  709. },
  710. "@user:test",
  711. "display_name",
  712. )
  713. )
  714. self.assertFalse(
  715. evaluator.matches(
  716. {
  717. "kind": "im.nheko.msc3664.related_event_match",
  718. "key": "sender",
  719. "rel_type": "m.in_reply_to",
  720. },
  721. "@user:test",
  722. "display_name",
  723. )
  724. )
  725. self.assertFalse(
  726. evaluator.matches(
  727. {
  728. "kind": "im.nheko.msc3664.related_event_match",
  729. "rel_type": "m.in_reply_to",
  730. },
  731. "@user:test",
  732. "display_name",
  733. )
  734. )
  735. class TestBulkPushRuleEvaluator(unittest.HomeserverTestCase):
  736. """Tests for the bulk push rule evaluator"""
  737. servlets = [
  738. synapse.rest.admin.register_servlets_for_client_rest_resource,
  739. login.register_servlets,
  740. register.register_servlets,
  741. room.register_servlets,
  742. ]
  743. def prepare(
  744. self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
  745. ) -> None:
  746. # Define an application service so that we can register appservice users
  747. self._service_token = "some_token"
  748. self._service = ApplicationService(
  749. self._service_token,
  750. "as1",
  751. "@as.sender:test",
  752. namespaces={
  753. "users": [
  754. {"regex": "@_as_.*:test", "exclusive": True},
  755. {"regex": "@as.sender:test", "exclusive": True},
  756. ]
  757. },
  758. msc3202_transaction_extensions=True,
  759. )
  760. self.hs.get_datastores().main.services_cache = [self._service]
  761. self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
  762. [self._service]
  763. )
  764. self._as_user, _ = self.register_appservice_user(
  765. "_as_user", self._service_token
  766. )
  767. self.evaluator = self.hs.get_bulk_push_rule_evaluator()
  768. def test_ignore_appservice_users(self) -> None:
  769. "Test that we don't generate push for appservice users"
  770. user_id = self.register_user("user", "pass")
  771. token = self.login("user", "pass")
  772. room_id = self.helper.create_room_as(user_id, tok=token)
  773. self.get_success(
  774. inject_member_event(self.hs, room_id, self._as_user, Membership.JOIN)
  775. )
  776. event, context = self.get_success(
  777. create_event(
  778. self.hs,
  779. type=EventTypes.Message,
  780. room_id=room_id,
  781. sender=user_id,
  782. content={"body": "test", "msgtype": "m.text"},
  783. )
  784. )
  785. # Assert the returned push rules do not contain the app service user
  786. rules = self.get_success(self.evaluator._get_rules_for_event(event))
  787. self.assertTrue(self._as_user not in rules)
  788. # Assert that no push actions have been added to the staging table (the
  789. # sender should not be pushed for the event)
  790. users_with_push_actions = self.get_success(
  791. self.hs.get_datastores().main.db_pool.simple_select_onecol(
  792. table="event_push_actions_staging",
  793. keyvalues={"event_id": event.event_id},
  794. retcol="user_id",
  795. desc="test_ignore_appservice_users",
  796. )
  797. )
  798. self.assertEqual(len(users_with_push_actions), 0)
  799. class BulkPushRuleEvaluatorTestCase(unittest.HomeserverTestCase):
  800. servlets = [
  801. admin.register_servlets,
  802. login.register_servlets,
  803. room.register_servlets,
  804. ]
  805. def prepare(
  806. self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
  807. ) -> None:
  808. self.main_store = homeserver.get_datastores().main
  809. self.user_id1 = self.register_user("user1", "password")
  810. self.tok1 = self.login(self.user_id1, "password")
  811. self.user_id2 = self.register_user("user2", "password")
  812. self.tok2 = self.login(self.user_id2, "password")
  813. self.room_id = self.helper.create_room_as(tok=self.tok1)
  814. # We want to test history visibility works correctly.
  815. self.helper.send_state(
  816. self.room_id,
  817. EventTypes.RoomHistoryVisibility,
  818. {"history_visibility": HistoryVisibility.JOINED},
  819. tok=self.tok1,
  820. )
  821. def get_notif_count(self, user_id: str) -> int:
  822. return self.get_success(
  823. self.main_store.db_pool.simple_select_one_onecol(
  824. table="event_push_actions",
  825. keyvalues={"user_id": user_id},
  826. retcol="COALESCE(SUM(notif), 0)",
  827. desc="get_staging_notif_count",
  828. )
  829. )
  830. def test_plain_message(self) -> None:
  831. """Test that sending a normal message in a room will trigger a
  832. notification
  833. """
  834. # Have user2 join the room and cle
  835. self.helper.join(self.room_id, self.user_id2, tok=self.tok2)
  836. # They start off with no notifications, but get them when messages are
  837. # sent.
  838. self.assertEqual(self.get_notif_count(self.user_id2), 0)
  839. user1 = UserID.from_string(self.user_id1)
  840. self.create_and_send_event(self.room_id, user1)
  841. self.assertEqual(self.get_notif_count(self.user_id2), 1)
  842. def test_delayed_message(self) -> None:
  843. """Test that a delayed message that was from before a user joined
  844. doesn't cause a notification for the joined user.
  845. """
  846. user1 = UserID.from_string(self.user_id1)
  847. # Send a message before user2 joins
  848. event_id1 = self.create_and_send_event(self.room_id, user1)
  849. # Have user2 join the room
  850. self.helper.join(self.room_id, self.user_id2, tok=self.tok2)
  851. # They start off with no notifications
  852. self.assertEqual(self.get_notif_count(self.user_id2), 0)
  853. # Send another message that references the event before the join to
  854. # simulate a "delayed" event
  855. self.create_and_send_event(self.room_id, user1, prev_event_ids=[event_id1])
  856. # user2 should not be notified about it, because they can't see it.
  857. self.assertEqual(self.get_notif_count(self.user_id2), 0)