test_push_rule_evaluator.py 32 KB

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