We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Events for newly created rooms are not always sent to appservices
Reproduction flow:
- Have an appservice that autojoins rooms when you invite it.
- Invite the appservice bot to a DM.
- Send a message into the DM
- Notice that the appservice doesn't receive a transaction for the event.

Expected behaviour:
- Appservices should get any and all events for rooms of which they are members of.

An example of this not working can be found by looking for the event `$gyPH0FEqlReAQwWGdpPLz-UdpQ5T2YjwRy4Cpa8ZaHQ` in `!BEPhTttjYKNQfBgWvS:matrix.org`.

Additionally, sending new messages into the room after some time will allow those to be bridged so this smells strongly of stale cache.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `synapse/appservice/__init__.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # Copyright 2015, 2016 OpenMarket Ltd
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 import logging
16 import re
17 from typing import TYPE_CHECKING, Iterable, List, Match, Optional
18 
19 from synapse.api.constants import EventTypes
20 from synapse.events import EventBase
21 from synapse.types import GroupID, JsonDict, UserID, get_domain_from_id
22 from synapse.util.caches.descriptors import cached
23 
24 if TYPE_CHECKING:
25     from synapse.appservice.api import ApplicationServiceApi
26     from synapse.storage.databases.main import DataStore
27 
28 logger = logging.getLogger(__name__)
29 
30 
31 class ApplicationServiceState:
32     DOWN = "down"
33     UP = "up"
34 
35 
36 class ApplicationService:
37     """Defines an application service. This definition is mostly what is
38     provided to the /register AS API.
39 
40     Provides methods to check if this service is "interested" in events.
41     """
42 
43     NS_USERS = "users"
44     NS_ALIASES = "aliases"
45     NS_ROOMS = "rooms"
46     # The ordering here is important as it is used to map database values (which
47     # are stored as ints representing the position in this list) to namespace
48     # values.
49     NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS]
50 
51     def __init__(
52         self,
53         token,
54         hostname,
55         url=None,
56         namespaces=None,
57         hs_token=None,
58         sender=None,
59         id=None,
60         protocols=None,
61         rate_limited=True,
62         ip_range_whitelist=None,
63         supports_ephemeral=False,
64     ):
65         self.token = token
66         self.url = (
67             url.rstrip("/") if isinstance(url, str) else None
68         )  # url must not end with a slash
69         self.hs_token = hs_token
70         self.sender = sender
71         self.server_name = hostname
72         self.namespaces = self._check_namespaces(namespaces)
73         self.id = id
74         self.ip_range_whitelist = ip_range_whitelist
75         self.supports_ephemeral = supports_ephemeral
76 
77         if "|" in self.id:
78             raise Exception("application service ID cannot contain '|' character")
79 
80         # .protocols is a publicly visible field
81         if protocols:
82             self.protocols = set(protocols)
83         else:
84             self.protocols = set()
85 
86         self.rate_limited = rate_limited
87 
88     def _check_namespaces(self, namespaces):
89         # Sanity check that it is of the form:
90         # {
91         #   users: [ {regex: "[A-z]+.*", exclusive: true}, ...],
92         #   aliases: [ {regex: "[A-z]+.*", exclusive: true}, ...],
93         #   rooms: [ {regex: "[A-z]+.*", exclusive: true}, ...],
94         # }
95         if not namespaces:
96             namespaces = {}
97 
98         for ns in ApplicationService.NS_LIST:
99             if ns not in namespaces:
100                 namespaces[ns] = []
101                 continue
102 
103             if type(namespaces[ns]) != list:
104                 raise ValueError("Bad namespace value for '%s'" % ns)
105             for regex_obj in namespaces[ns]:
106                 if not isinstance(regex_obj, dict):
107                     raise ValueError("Expected dict regex for ns '%s'" % ns)
108                 if not isinstance(regex_obj.get("exclusive"), bool):
109                     raise ValueError("Expected bool for 'exclusive' in ns '%s'" % ns)
110                 group_id = regex_obj.get("group_id")
111                 if group_id:
112                     if not isinstance(group_id, str):
113                         raise ValueError(
114                             "Expected string for 'group_id' in ns '%s'" % ns
115                         )
116                     try:
117                         GroupID.from_string(group_id)
118                     except Exception:
119                         raise ValueError(
120                             "Expected valid group ID for 'group_id' in ns '%s'" % ns
121                         )
122 
123                     if get_domain_from_id(group_id) != self.server_name:
124                         raise ValueError(
125                             "Expected 'group_id' to be this host in ns '%s'" % ns
126                         )
127 
128                 regex = regex_obj.get("regex")
129                 if isinstance(regex, str):
130                     regex_obj["regex"] = re.compile(regex)  # Pre-compile regex
131                 else:
132                     raise ValueError("Expected string for 'regex' in ns '%s'" % ns)
133         return namespaces
134 
135     def _matches_regex(self, test_string: str, namespace_key: str) -> Optional[Match]:
136         for regex_obj in self.namespaces[namespace_key]:
137             if regex_obj["regex"].match(test_string):
138                 return regex_obj
139         return None
140 
141     def _is_exclusive(self, ns_key: str, test_string: str) -> bool:
142         regex_obj = self._matches_regex(test_string, ns_key)
143         if regex_obj:
144             return regex_obj["exclusive"]
145         return False
146 
147     async def _matches_user(
148         self, event: Optional[EventBase], store: Optional["DataStore"] = None
149     ) -> bool:
150         if not event:
151             return False
152 
153         if self.is_interested_in_user(event.sender):
154             return True
155         # also check m.room.member state key
156         if event.type == EventTypes.Member and self.is_interested_in_user(
157             event.state_key
158         ):
159             return True
160 
161         if not store:
162             return False
163 
164         does_match = await self.matches_user_in_member_list(event.room_id, store)
165         return does_match
166 
167     @cached(num_args=1)
168     async def matches_user_in_member_list(
169         self, room_id: str, store: "DataStore"
170     ) -> bool:
171         """Check if this service is interested a room based upon it's membership
172 
173         Args:
174             room_id: The room to check.
175             store: The datastore to query.
176 
177         Returns:
178             True if this service would like to know about this room.
179         """
180         member_list = await store.get_users_in_room(room_id)
181 
182         # check joined member events
183         for user_id in member_list:
184             if self.is_interested_in_user(user_id):
185                 return True
186         return False
187 
188     def _matches_room_id(self, event: EventBase) -> bool:
189         if hasattr(event, "room_id"):
190             return self.is_interested_in_room(event.room_id)
191         return False
192 
193     async def _matches_aliases(
194         self, event: EventBase, store: Optional["DataStore"] = None
195     ) -> bool:
196         if not store or not event:
197             return False
198 
199         alias_list = await store.get_aliases_for_room(event.room_id)
200         for alias in alias_list:
201             if self.is_interested_in_alias(alias):
202                 return True
203         return False
204 
205     async def is_interested(
206         self, event: EventBase, store: Optional["DataStore"] = None
207     ) -> bool:
208         """Check if this service is interested in this event.
209 
210         Args:
211             event: The event to check.
212             store: The datastore to query.
213 
214         Returns:
215             True if this service would like to know about this event.
216         """
217         # Do cheap checks first
218         if self._matches_room_id(event):
219             return True
220 
221         # This will check the namespaces first before
222         # checking the store, so should be run before _matches_aliases
223         if await self._matches_user(event, store):
224             return True
225 
226         # This will check the store, so should be run last
227         if await self._matches_aliases(event, store):
228             return True
229 
230         return False
231 
232     @cached(num_args=1)
233     async def is_interested_in_presence(
234         self, user_id: UserID, store: "DataStore"
235     ) -> bool:
236         """Check if this service is interested a user's presence
237 
238         Args:
239             user_id: The user to check.
240             store: The datastore to query.
241 
242         Returns:
243             True if this service would like to know about presence for this user.
244         """
245         # Find all the rooms the sender is in
246         if self.is_interested_in_user(user_id.to_string()):
247             return True
248         room_ids = await store.get_rooms_for_user(user_id.to_string())
249 
250         # Then find out if the appservice is interested in any of those rooms
251         for room_id in room_ids:
252             if await self.matches_user_in_member_list(room_id, store):
253                 return True
254         return False
255 
256     def is_interested_in_user(self, user_id: str) -> bool:
257         return (
258             bool(self._matches_regex(user_id, ApplicationService.NS_USERS))
259             or user_id == self.sender
260         )
261 
262     def is_interested_in_alias(self, alias: str) -> bool:
263         return bool(self._matches_regex(alias, ApplicationService.NS_ALIASES))
264 
265     def is_interested_in_room(self, room_id: str) -> bool:
266         return bool(self._matches_regex(room_id, ApplicationService.NS_ROOMS))
267 
268     def is_exclusive_user(self, user_id: str) -> bool:
269         return (
270             self._is_exclusive(ApplicationService.NS_USERS, user_id)
271             or user_id == self.sender
272         )
273 
274     def is_interested_in_protocol(self, protocol: str) -> bool:
275         return protocol in self.protocols
276 
277     def is_exclusive_alias(self, alias: str) -> bool:
278         return self._is_exclusive(ApplicationService.NS_ALIASES, alias)
279 
280     def is_exclusive_room(self, room_id: str) -> bool:
281         return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
282 
283     def get_exclusive_user_regexes(self):
284         """Get the list of regexes used to determine if a user is exclusively
285         registered by the AS
286         """
287         return [
288             regex_obj["regex"]
289             for regex_obj in self.namespaces[ApplicationService.NS_USERS]
290             if regex_obj["exclusive"]
291         ]
292 
293     def get_groups_for_user(self, user_id: str) -> Iterable[str]:
294         """Get the groups that this user is associated with by this AS
295 
296         Args:
297             user_id: The ID of the user.
298 
299         Returns:
300             An iterable that yields group_id strings.
301         """
302         return (
303             regex_obj["group_id"]
304             for regex_obj in self.namespaces[ApplicationService.NS_USERS]
305             if "group_id" in regex_obj and regex_obj["regex"].match(user_id)
306         )
307 
308     def is_rate_limited(self) -> bool:
309         return self.rate_limited
310 
311     def __str__(self):
312         # copy dictionary and redact token fields so they don't get logged
313         dict_copy = self.__dict__.copy()
314         dict_copy["token"] = "<redacted>"
315         dict_copy["hs_token"] = "<redacted>"
316         return "ApplicationService: %s" % (dict_copy,)
317 
318 
319 class AppServiceTransaction:
320     """Represents an application service transaction."""
321 
322     def __init__(
323         self,
324         service: ApplicationService,
325         id: int,
326         events: List[EventBase],
327         ephemeral: List[JsonDict],
328     ):
329         self.service = service
330         self.id = id
331         self.events = events
332         self.ephemeral = ephemeral
333 
334     async def send(self, as_api: "ApplicationServiceApi") -> bool:
335         """Sends this transaction using the provided AS API interface.
336 
337         Args:
338             as_api: The API to use to send.
339         Returns:
340             True if the transaction was sent.
341         """
342         return await as_api.push_bulk(
343             service=self.service,
344             events=self.events,
345             ephemeral=self.ephemeral,
346             txn_id=self.id,
347         )
348 
349     async def complete(self, store: "DataStore") -> None:
350         """Completes this transaction as successful.
351 
352         Marks this transaction ID on the application service and removes the
353         transaction contents from the database.
354 
355         Args:
356             store: The database store to operate on.
357         """
358         await store.complete_appservice_txn(service=self.service, txn_id=self.id)
359
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/synapse/appservice/__init__.py b/synapse/appservice/__init__.py
--- a/synapse/appservice/__init__.py
+++ b/synapse/appservice/__init__.py
@@ -19,7 +19,7 @@
 from synapse.api.constants import EventTypes
 from synapse.events import EventBase
 from synapse.types import GroupID, JsonDict, UserID, get_domain_from_id
-from synapse.util.caches.descriptors import cached
+from synapse.util.caches.descriptors import _CacheContext, cached
 
 if TYPE_CHECKING:
     from synapse.appservice.api import ApplicationServiceApi
@@ -164,9 +164,9 @@
         does_match = await self.matches_user_in_member_list(event.room_id, store)
         return does_match
 
-    @cached(num_args=1)
+    @cached(num_args=1, cache_context=True)
     async def matches_user_in_member_list(
-        self, room_id: str, store: "DataStore"
+        self, room_id: str, store: "DataStore", cache_context: _CacheContext,
     ) -> bool:
         """Check if this service is interested a room based upon it's membership
 
@@ -177,7 +177,9 @@
         Returns:
             True if this service would like to know about this room.
         """
-        member_list = await store.get_users_in_room(room_id)
+        member_list = await store.get_users_in_room(
+            room_id, on_invalidate=cache_context.invalidate
+        )
 
         # check joined member events
         for user_id in member_list:

{"golden_diff": "diff --git a/synapse/appservice/__init__.py b/synapse/appservice/__init__.py\n--- a/synapse/appservice/__init__.py\n+++ b/synapse/appservice/__init__.py\n@@ -19,7 +19,7 @@\n from synapse.api.constants import EventTypes\n from synapse.events import EventBase\n from synapse.types import GroupID, JsonDict, UserID, get_domain_from_id\n-from synapse.util.caches.descriptors import cached\n+from synapse.util.caches.descriptors import _CacheContext, cached\n \n if TYPE_CHECKING:\n     from synapse.appservice.api import ApplicationServiceApi\n@@ -164,9 +164,9 @@\n         does_match = await self.matches_user_in_member_list(event.room_id, store)\n         return does_match\n \n-    @cached(num_args=1)\n+    @cached(num_args=1, cache_context=True)\n     async def matches_user_in_member_list(\n-        self, room_id: str, store: \"DataStore\"\n+        self, room_id: str, store: \"DataStore\", cache_context: _CacheContext,\n     ) -> bool:\n         \"\"\"Check if this service is interested a room based upon it's membership\n \n@@ -177,7 +177,9 @@\n         Returns:\n             True if this service would like to know about this room.\n         \"\"\"\n-        member_list = await store.get_users_in_room(room_id)\n+        member_list = await store.get_users_in_room(\n+            room_id, on_invalidate=cache_context.invalidate\n+        )\n \n         # check joined member events\n         for user_id in member_list:\n", "issue": "Events for newly created rooms are not always sent to appservices\nReproduction flow:\r\n- Have an appservice that autojoins rooms when you invite it.\r\n- Invite the appservice bot to a DM.\r\n- Send a message into the DM\r\n- Notice that the appservice doesn't receive a transaction for the event.\r\n\r\nExpected behaviour:\r\n- Appservices should get any and all events for rooms of which they are members of.\r\n\r\nAn example of this not working can be found by looking for the event `$gyPH0FEqlReAQwWGdpPLz-UdpQ5T2YjwRy4Cpa8ZaHQ` in `!BEPhTttjYKNQfBgWvS:matrix.org`.\r\n\r\nAdditionally, sending new messages into the room after some time will allow those to be bridged so this smells strongly of stale cache.\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright 2015, 2016 OpenMarket Ltd\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport logging\nimport re\nfrom typing import TYPE_CHECKING, Iterable, List, Match, Optional\n\nfrom synapse.api.constants import EventTypes\nfrom synapse.events import EventBase\nfrom synapse.types import GroupID, JsonDict, UserID, get_domain_from_id\nfrom synapse.util.caches.descriptors import cached\n\nif TYPE_CHECKING:\n    from synapse.appservice.api import ApplicationServiceApi\n    from synapse.storage.databases.main import DataStore\n\nlogger = logging.getLogger(__name__)\n\n\nclass ApplicationServiceState:\n    DOWN = \"down\"\n    UP = \"up\"\n\n\nclass ApplicationService:\n    \"\"\"Defines an application service. This definition is mostly what is\n    provided to the /register AS API.\n\n    Provides methods to check if this service is \"interested\" in events.\n    \"\"\"\n\n    NS_USERS = \"users\"\n    NS_ALIASES = \"aliases\"\n    NS_ROOMS = \"rooms\"\n    # The ordering here is important as it is used to map database values (which\n    # are stored as ints representing the position in this list) to namespace\n    # values.\n    NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS]\n\n    def __init__(\n        self,\n        token,\n        hostname,\n        url=None,\n        namespaces=None,\n        hs_token=None,\n        sender=None,\n        id=None,\n        protocols=None,\n        rate_limited=True,\n        ip_range_whitelist=None,\n        supports_ephemeral=False,\n    ):\n        self.token = token\n        self.url = (\n            url.rstrip(\"/\") if isinstance(url, str) else None\n        )  # url must not end with a slash\n        self.hs_token = hs_token\n        self.sender = sender\n        self.server_name = hostname\n        self.namespaces = self._check_namespaces(namespaces)\n        self.id = id\n        self.ip_range_whitelist = ip_range_whitelist\n        self.supports_ephemeral = supports_ephemeral\n\n        if \"|\" in self.id:\n            raise Exception(\"application service ID cannot contain '|' character\")\n\n        # .protocols is a publicly visible field\n        if protocols:\n            self.protocols = set(protocols)\n        else:\n            self.protocols = set()\n\n        self.rate_limited = rate_limited\n\n    def _check_namespaces(self, namespaces):\n        # Sanity check that it is of the form:\n        # {\n        #   users: [ {regex: \"[A-z]+.*\", exclusive: true}, ...],\n        #   aliases: [ {regex: \"[A-z]+.*\", exclusive: true}, ...],\n        #   rooms: [ {regex: \"[A-z]+.*\", exclusive: true}, ...],\n        # }\n        if not namespaces:\n            namespaces = {}\n\n        for ns in ApplicationService.NS_LIST:\n            if ns not in namespaces:\n                namespaces[ns] = []\n                continue\n\n            if type(namespaces[ns]) != list:\n                raise ValueError(\"Bad namespace value for '%s'\" % ns)\n            for regex_obj in namespaces[ns]:\n                if not isinstance(regex_obj, dict):\n                    raise ValueError(\"Expected dict regex for ns '%s'\" % ns)\n                if not isinstance(regex_obj.get(\"exclusive\"), bool):\n                    raise ValueError(\"Expected bool for 'exclusive' in ns '%s'\" % ns)\n                group_id = regex_obj.get(\"group_id\")\n                if group_id:\n                    if not isinstance(group_id, str):\n                        raise ValueError(\n                            \"Expected string for 'group_id' in ns '%s'\" % ns\n                        )\n                    try:\n                        GroupID.from_string(group_id)\n                    except Exception:\n                        raise ValueError(\n                            \"Expected valid group ID for 'group_id' in ns '%s'\" % ns\n                        )\n\n                    if get_domain_from_id(group_id) != self.server_name:\n                        raise ValueError(\n                            \"Expected 'group_id' to be this host in ns '%s'\" % ns\n                        )\n\n                regex = regex_obj.get(\"regex\")\n                if isinstance(regex, str):\n                    regex_obj[\"regex\"] = re.compile(regex)  # Pre-compile regex\n                else:\n                    raise ValueError(\"Expected string for 'regex' in ns '%s'\" % ns)\n        return namespaces\n\n    def _matches_regex(self, test_string: str, namespace_key: str) -> Optional[Match]:\n        for regex_obj in self.namespaces[namespace_key]:\n            if regex_obj[\"regex\"].match(test_string):\n                return regex_obj\n        return None\n\n    def _is_exclusive(self, ns_key: str, test_string: str) -> bool:\n        regex_obj = self._matches_regex(test_string, ns_key)\n        if regex_obj:\n            return regex_obj[\"exclusive\"]\n        return False\n\n    async def _matches_user(\n        self, event: Optional[EventBase], store: Optional[\"DataStore\"] = None\n    ) -> bool:\n        if not event:\n            return False\n\n        if self.is_interested_in_user(event.sender):\n            return True\n        # also check m.room.member state key\n        if event.type == EventTypes.Member and self.is_interested_in_user(\n            event.state_key\n        ):\n            return True\n\n        if not store:\n            return False\n\n        does_match = await self.matches_user_in_member_list(event.room_id, store)\n        return does_match\n\n    @cached(num_args=1)\n    async def matches_user_in_member_list(\n        self, room_id: str, store: \"DataStore\"\n    ) -> bool:\n        \"\"\"Check if this service is interested a room based upon it's membership\n\n        Args:\n            room_id: The room to check.\n            store: The datastore to query.\n\n        Returns:\n            True if this service would like to know about this room.\n        \"\"\"\n        member_list = await store.get_users_in_room(room_id)\n\n        # check joined member events\n        for user_id in member_list:\n            if self.is_interested_in_user(user_id):\n                return True\n        return False\n\n    def _matches_room_id(self, event: EventBase) -> bool:\n        if hasattr(event, \"room_id\"):\n            return self.is_interested_in_room(event.room_id)\n        return False\n\n    async def _matches_aliases(\n        self, event: EventBase, store: Optional[\"DataStore\"] = None\n    ) -> bool:\n        if not store or not event:\n            return False\n\n        alias_list = await store.get_aliases_for_room(event.room_id)\n        for alias in alias_list:\n            if self.is_interested_in_alias(alias):\n                return True\n        return False\n\n    async def is_interested(\n        self, event: EventBase, store: Optional[\"DataStore\"] = None\n    ) -> bool:\n        \"\"\"Check if this service is interested in this event.\n\n        Args:\n            event: The event to check.\n            store: The datastore to query.\n\n        Returns:\n            True if this service would like to know about this event.\n        \"\"\"\n        # Do cheap checks first\n        if self._matches_room_id(event):\n            return True\n\n        # This will check the namespaces first before\n        # checking the store, so should be run before _matches_aliases\n        if await self._matches_user(event, store):\n            return True\n\n        # This will check the store, so should be run last\n        if await self._matches_aliases(event, store):\n            return True\n\n        return False\n\n    @cached(num_args=1)\n    async def is_interested_in_presence(\n        self, user_id: UserID, store: \"DataStore\"\n    ) -> bool:\n        \"\"\"Check if this service is interested a user's presence\n\n        Args:\n            user_id: The user to check.\n            store: The datastore to query.\n\n        Returns:\n            True if this service would like to know about presence for this user.\n        \"\"\"\n        # Find all the rooms the sender is in\n        if self.is_interested_in_user(user_id.to_string()):\n            return True\n        room_ids = await store.get_rooms_for_user(user_id.to_string())\n\n        # Then find out if the appservice is interested in any of those rooms\n        for room_id in room_ids:\n            if await self.matches_user_in_member_list(room_id, store):\n                return True\n        return False\n\n    def is_interested_in_user(self, user_id: str) -> bool:\n        return (\n            bool(self._matches_regex(user_id, ApplicationService.NS_USERS))\n            or user_id == self.sender\n        )\n\n    def is_interested_in_alias(self, alias: str) -> bool:\n        return bool(self._matches_regex(alias, ApplicationService.NS_ALIASES))\n\n    def is_interested_in_room(self, room_id: str) -> bool:\n        return bool(self._matches_regex(room_id, ApplicationService.NS_ROOMS))\n\n    def is_exclusive_user(self, user_id: str) -> bool:\n        return (\n            self._is_exclusive(ApplicationService.NS_USERS, user_id)\n            or user_id == self.sender\n        )\n\n    def is_interested_in_protocol(self, protocol: str) -> bool:\n        return protocol in self.protocols\n\n    def is_exclusive_alias(self, alias: str) -> bool:\n        return self._is_exclusive(ApplicationService.NS_ALIASES, alias)\n\n    def is_exclusive_room(self, room_id: str) -> bool:\n        return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)\n\n    def get_exclusive_user_regexes(self):\n        \"\"\"Get the list of regexes used to determine if a user is exclusively\n        registered by the AS\n        \"\"\"\n        return [\n            regex_obj[\"regex\"]\n            for regex_obj in self.namespaces[ApplicationService.NS_USERS]\n            if regex_obj[\"exclusive\"]\n        ]\n\n    def get_groups_for_user(self, user_id: str) -> Iterable[str]:\n        \"\"\"Get the groups that this user is associated with by this AS\n\n        Args:\n            user_id: The ID of the user.\n\n        Returns:\n            An iterable that yields group_id strings.\n        \"\"\"\n        return (\n            regex_obj[\"group_id\"]\n            for regex_obj in self.namespaces[ApplicationService.NS_USERS]\n            if \"group_id\" in regex_obj and regex_obj[\"regex\"].match(user_id)\n        )\n\n    def is_rate_limited(self) -> bool:\n        return self.rate_limited\n\n    def __str__(self):\n        # copy dictionary and redact token fields so they don't get logged\n        dict_copy = self.__dict__.copy()\n        dict_copy[\"token\"] = \"<redacted>\"\n        dict_copy[\"hs_token\"] = \"<redacted>\"\n        return \"ApplicationService: %s\" % (dict_copy,)\n\n\nclass AppServiceTransaction:\n    \"\"\"Represents an application service transaction.\"\"\"\n\n    def __init__(\n        self,\n        service: ApplicationService,\n        id: int,\n        events: List[EventBase],\n        ephemeral: List[JsonDict],\n    ):\n        self.service = service\n        self.id = id\n        self.events = events\n        self.ephemeral = ephemeral\n\n    async def send(self, as_api: \"ApplicationServiceApi\") -> bool:\n        \"\"\"Sends this transaction using the provided AS API interface.\n\n        Args:\n            as_api: The API to use to send.\n        Returns:\n            True if the transaction was sent.\n        \"\"\"\n        return await as_api.push_bulk(\n            service=self.service,\n            events=self.events,\n            ephemeral=self.ephemeral,\n            txn_id=self.id,\n        )\n\n    async def complete(self, store: \"DataStore\") -> None:\n        \"\"\"Completes this transaction as successful.\n\n        Marks this transaction ID on the application service and removes the\n        transaction contents from the database.\n\n        Args:\n            store: The database store to operate on.\n        \"\"\"\n        await store.complete_appservice_txn(service=self.service, txn_id=self.id)\n", "path": "synapse/appservice/__init__.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright 2015, 2016 OpenMarket Ltd\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport logging\nimport re\nfrom typing import TYPE_CHECKING, Iterable, List, Match, Optional\n\nfrom synapse.api.constants import EventTypes\nfrom synapse.events import EventBase\nfrom synapse.types import GroupID, JsonDict, UserID, get_domain_from_id\nfrom synapse.util.caches.descriptors import _CacheContext, cached\n\nif TYPE_CHECKING:\n    from synapse.appservice.api import ApplicationServiceApi\n    from synapse.storage.databases.main import DataStore\n\nlogger = logging.getLogger(__name__)\n\n\nclass ApplicationServiceState:\n    DOWN = \"down\"\n    UP = \"up\"\n\n\nclass ApplicationService:\n    \"\"\"Defines an application service. This definition is mostly what is\n    provided to the /register AS API.\n\n    Provides methods to check if this service is \"interested\" in events.\n    \"\"\"\n\n    NS_USERS = \"users\"\n    NS_ALIASES = \"aliases\"\n    NS_ROOMS = \"rooms\"\n    # The ordering here is important as it is used to map database values (which\n    # are stored as ints representing the position in this list) to namespace\n    # values.\n    NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS]\n\n    def __init__(\n        self,\n        token,\n        hostname,\n        url=None,\n        namespaces=None,\n        hs_token=None,\n        sender=None,\n        id=None,\n        protocols=None,\n        rate_limited=True,\n        ip_range_whitelist=None,\n        supports_ephemeral=False,\n    ):\n        self.token = token\n        self.url = (\n            url.rstrip(\"/\") if isinstance(url, str) else None\n        )  # url must not end with a slash\n        self.hs_token = hs_token\n        self.sender = sender\n        self.server_name = hostname\n        self.namespaces = self._check_namespaces(namespaces)\n        self.id = id\n        self.ip_range_whitelist = ip_range_whitelist\n        self.supports_ephemeral = supports_ephemeral\n\n        if \"|\" in self.id:\n            raise Exception(\"application service ID cannot contain '|' character\")\n\n        # .protocols is a publicly visible field\n        if protocols:\n            self.protocols = set(protocols)\n        else:\n            self.protocols = set()\n\n        self.rate_limited = rate_limited\n\n    def _check_namespaces(self, namespaces):\n        # Sanity check that it is of the form:\n        # {\n        #   users: [ {regex: \"[A-z]+.*\", exclusive: true}, ...],\n        #   aliases: [ {regex: \"[A-z]+.*\", exclusive: true}, ...],\n        #   rooms: [ {regex: \"[A-z]+.*\", exclusive: true}, ...],\n        # }\n        if not namespaces:\n            namespaces = {}\n\n        for ns in ApplicationService.NS_LIST:\n            if ns not in namespaces:\n                namespaces[ns] = []\n                continue\n\n            if type(namespaces[ns]) != list:\n                raise ValueError(\"Bad namespace value for '%s'\" % ns)\n            for regex_obj in namespaces[ns]:\n                if not isinstance(regex_obj, dict):\n                    raise ValueError(\"Expected dict regex for ns '%s'\" % ns)\n                if not isinstance(regex_obj.get(\"exclusive\"), bool):\n                    raise ValueError(\"Expected bool for 'exclusive' in ns '%s'\" % ns)\n                group_id = regex_obj.get(\"group_id\")\n                if group_id:\n                    if not isinstance(group_id, str):\n                        raise ValueError(\n                            \"Expected string for 'group_id' in ns '%s'\" % ns\n                        )\n                    try:\n                        GroupID.from_string(group_id)\n                    except Exception:\n                        raise ValueError(\n                            \"Expected valid group ID for 'group_id' in ns '%s'\" % ns\n                        )\n\n                    if get_domain_from_id(group_id) != self.server_name:\n                        raise ValueError(\n                            \"Expected 'group_id' to be this host in ns '%s'\" % ns\n                        )\n\n                regex = regex_obj.get(\"regex\")\n                if isinstance(regex, str):\n                    regex_obj[\"regex\"] = re.compile(regex)  # Pre-compile regex\n                else:\n                    raise ValueError(\"Expected string for 'regex' in ns '%s'\" % ns)\n        return namespaces\n\n    def _matches_regex(self, test_string: str, namespace_key: str) -> Optional[Match]:\n        for regex_obj in self.namespaces[namespace_key]:\n            if regex_obj[\"regex\"].match(test_string):\n                return regex_obj\n        return None\n\n    def _is_exclusive(self, ns_key: str, test_string: str) -> bool:\n        regex_obj = self._matches_regex(test_string, ns_key)\n        if regex_obj:\n            return regex_obj[\"exclusive\"]\n        return False\n\n    async def _matches_user(\n        self, event: Optional[EventBase], store: Optional[\"DataStore\"] = None\n    ) -> bool:\n        if not event:\n            return False\n\n        if self.is_interested_in_user(event.sender):\n            return True\n        # also check m.room.member state key\n        if event.type == EventTypes.Member and self.is_interested_in_user(\n            event.state_key\n        ):\n            return True\n\n        if not store:\n            return False\n\n        does_match = await self.matches_user_in_member_list(event.room_id, store)\n        return does_match\n\n    @cached(num_args=1, cache_context=True)\n    async def matches_user_in_member_list(\n        self, room_id: str, store: \"DataStore\", cache_context: _CacheContext,\n    ) -> bool:\n        \"\"\"Check if this service is interested a room based upon it's membership\n\n        Args:\n            room_id: The room to check.\n            store: The datastore to query.\n\n        Returns:\n            True if this service would like to know about this room.\n        \"\"\"\n        member_list = await store.get_users_in_room(\n            room_id, on_invalidate=cache_context.invalidate\n        )\n\n        # check joined member events\n        for user_id in member_list:\n            if self.is_interested_in_user(user_id):\n                return True\n        return False\n\n    def _matches_room_id(self, event: EventBase) -> bool:\n        if hasattr(event, \"room_id\"):\n            return self.is_interested_in_room(event.room_id)\n        return False\n\n    async def _matches_aliases(\n        self, event: EventBase, store: Optional[\"DataStore\"] = None\n    ) -> bool:\n        if not store or not event:\n            return False\n\n        alias_list = await store.get_aliases_for_room(event.room_id)\n        for alias in alias_list:\n            if self.is_interested_in_alias(alias):\n                return True\n        return False\n\n    async def is_interested(\n        self, event: EventBase, store: Optional[\"DataStore\"] = None\n    ) -> bool:\n        \"\"\"Check if this service is interested in this event.\n\n        Args:\n            event: The event to check.\n            store: The datastore to query.\n\n        Returns:\n            True if this service would like to know about this event.\n        \"\"\"\n        # Do cheap checks first\n        if self._matches_room_id(event):\n            return True\n\n        # This will check the namespaces first before\n        # checking the store, so should be run before _matches_aliases\n        if await self._matches_user(event, store):\n            return True\n\n        # This will check the store, so should be run last\n        if await self._matches_aliases(event, store):\n            return True\n\n        return False\n\n    @cached(num_args=1)\n    async def is_interested_in_presence(\n        self, user_id: UserID, store: \"DataStore\"\n    ) -> bool:\n        \"\"\"Check if this service is interested a user's presence\n\n        Args:\n            user_id: The user to check.\n            store: The datastore to query.\n\n        Returns:\n            True if this service would like to know about presence for this user.\n        \"\"\"\n        # Find all the rooms the sender is in\n        if self.is_interested_in_user(user_id.to_string()):\n            return True\n        room_ids = await store.get_rooms_for_user(user_id.to_string())\n\n        # Then find out if the appservice is interested in any of those rooms\n        for room_id in room_ids:\n            if await self.matches_user_in_member_list(room_id, store):\n                return True\n        return False\n\n    def is_interested_in_user(self, user_id: str) -> bool:\n        return (\n            bool(self._matches_regex(user_id, ApplicationService.NS_USERS))\n            or user_id == self.sender\n        )\n\n    def is_interested_in_alias(self, alias: str) -> bool:\n        return bool(self._matches_regex(alias, ApplicationService.NS_ALIASES))\n\n    def is_interested_in_room(self, room_id: str) -> bool:\n        return bool(self._matches_regex(room_id, ApplicationService.NS_ROOMS))\n\n    def is_exclusive_user(self, user_id: str) -> bool:\n        return (\n            self._is_exclusive(ApplicationService.NS_USERS, user_id)\n            or user_id == self.sender\n        )\n\n    def is_interested_in_protocol(self, protocol: str) -> bool:\n        return protocol in self.protocols\n\n    def is_exclusive_alias(self, alias: str) -> bool:\n        return self._is_exclusive(ApplicationService.NS_ALIASES, alias)\n\n    def is_exclusive_room(self, room_id: str) -> bool:\n        return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)\n\n    def get_exclusive_user_regexes(self):\n        \"\"\"Get the list of regexes used to determine if a user is exclusively\n        registered by the AS\n        \"\"\"\n        return [\n            regex_obj[\"regex\"]\n            for regex_obj in self.namespaces[ApplicationService.NS_USERS]\n            if regex_obj[\"exclusive\"]\n        ]\n\n    def get_groups_for_user(self, user_id: str) -> Iterable[str]:\n        \"\"\"Get the groups that this user is associated with by this AS\n\n        Args:\n            user_id: The ID of the user.\n\n        Returns:\n            An iterable that yields group_id strings.\n        \"\"\"\n        return (\n            regex_obj[\"group_id\"]\n            for regex_obj in self.namespaces[ApplicationService.NS_USERS]\n            if \"group_id\" in regex_obj and regex_obj[\"regex\"].match(user_id)\n        )\n\n    def is_rate_limited(self) -> bool:\n        return self.rate_limited\n\n    def __str__(self):\n        # copy dictionary and redact token fields so they don't get logged\n        dict_copy = self.__dict__.copy()\n        dict_copy[\"token\"] = \"<redacted>\"\n        dict_copy[\"hs_token\"] = \"<redacted>\"\n        return \"ApplicationService: %s\" % (dict_copy,)\n\n\nclass AppServiceTransaction:\n    \"\"\"Represents an application service transaction.\"\"\"\n\n    def __init__(\n        self,\n        service: ApplicationService,\n        id: int,\n        events: List[EventBase],\n        ephemeral: List[JsonDict],\n    ):\n        self.service = service\n        self.id = id\n        self.events = events\n        self.ephemeral = ephemeral\n\n    async def send(self, as_api: \"ApplicationServiceApi\") -> bool:\n        \"\"\"Sends this transaction using the provided AS API interface.\n\n        Args:\n            as_api: The API to use to send.\n        Returns:\n            True if the transaction was sent.\n        \"\"\"\n        return await as_api.push_bulk(\n            service=self.service,\n            events=self.events,\n            ephemeral=self.ephemeral,\n            txn_id=self.id,\n        )\n\n    async def complete(self, store: \"DataStore\") -> None:\n        \"\"\"Completes this transaction as successful.\n\n        Marks this transaction ID on the application service and removes the\n        transaction contents from the database.\n\n        Args:\n            store: The database store to operate on.\n        \"\"\"\n        await store.complete_appservice_txn(service=self.service, txn_id=self.id)\n", "path": "synapse/appservice/__init__.py"}]}