256 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			256 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
| # -*- coding: utf-8 -*-
 | |
| # Copyright 2015 OpenMarket Ltd
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| from synapse.api.errors import SynapseError
 | |
| from synapse.types import UserID, RoomID
 | |
| 
 | |
| 
 | |
| class Filtering(object):
 | |
| 
 | |
|     def __init__(self, hs):
 | |
|         super(Filtering, self).__init__()
 | |
|         self.store = hs.get_datastore()
 | |
| 
 | |
|     def get_user_filter(self, user_localpart, filter_id):
 | |
|         result = self.store.get_user_filter(user_localpart, filter_id)
 | |
|         result.addCallback(Filter)
 | |
|         return result
 | |
| 
 | |
|     def add_user_filter(self, user_localpart, user_filter):
 | |
|         self._check_valid_filter(user_filter)
 | |
|         return self.store.add_user_filter(user_localpart, user_filter)
 | |
| 
 | |
|     # TODO(paul): surely we should probably add a delete_user_filter or
 | |
|     #   replace_user_filter at some point? There's no REST API specified for
 | |
|     #   them however
 | |
| 
 | |
|     def _check_valid_filter(self, user_filter_json):
 | |
|         """Check if the provided filter is valid.
 | |
| 
 | |
|         This inspects all definitions contained within the filter.
 | |
| 
 | |
|         Args:
 | |
|             user_filter_json(dict): The filter
 | |
|         Raises:
 | |
|             SynapseError: If the filter is not valid.
 | |
|         """
 | |
|         # NB: Filters are the complete json blobs. "Definitions" are an
 | |
|         # individual top-level key e.g. public_user_data. Filters are made of
 | |
|         # many definitions.
 | |
| 
 | |
|         top_level_definitions = [
 | |
|             "public_user_data", "private_user_data", "server_data"
 | |
|         ]
 | |
| 
 | |
|         room_level_definitions = [
 | |
|             "state", "timeline", "ephemeral"
 | |
|         ]
 | |
| 
 | |
|         for key in top_level_definitions:
 | |
|             if key in user_filter_json:
 | |
|                 self._check_definition(user_filter_json[key])
 | |
| 
 | |
|         if "room" in user_filter_json:
 | |
|             for key in room_level_definitions:
 | |
|                 if key in user_filter_json["room"]:
 | |
|                     self._check_definition(user_filter_json["room"][key])
 | |
| 
 | |
|     def _check_definition(self, definition):
 | |
|         """Check if the provided definition is valid.
 | |
| 
 | |
|         This inspects not only the types but also the values to make sure they
 | |
|         make sense.
 | |
| 
 | |
|         Args:
 | |
|             definition(dict): The filter definition
 | |
|         Raises:
 | |
|             SynapseError: If there was a problem with this definition.
 | |
|         """
 | |
|         # NB: Filters are the complete json blobs. "Definitions" are an
 | |
|         # individual top-level key e.g. public_user_data. Filters are made of
 | |
|         # many definitions.
 | |
|         if type(definition) != dict:
 | |
|             raise SynapseError(
 | |
|                 400, "Expected JSON object, not %s" % (definition,)
 | |
|             )
 | |
| 
 | |
|         # check rooms are valid room IDs
 | |
|         room_id_keys = ["rooms", "not_rooms"]
 | |
|         for key in room_id_keys:
 | |
|             if key in definition:
 | |
|                 if type(definition[key]) != list:
 | |
|                     raise SynapseError(400, "Expected %s to be a list." % key)
 | |
|                 for room_id in definition[key]:
 | |
|                     RoomID.from_string(room_id)
 | |
| 
 | |
|         # check senders are valid user IDs
 | |
|         user_id_keys = ["senders", "not_senders"]
 | |
|         for key in user_id_keys:
 | |
|             if key in definition:
 | |
|                 if type(definition[key]) != list:
 | |
|                     raise SynapseError(400, "Expected %s to be a list." % key)
 | |
|                 for user_id in definition[key]:
 | |
|                     UserID.from_string(user_id)
 | |
| 
 | |
|         # TODO: We don't limit event type values but we probably should...
 | |
|         # check types are valid event types
 | |
|         event_keys = ["types", "not_types"]
 | |
|         for key in event_keys:
 | |
|             if key in definition:
 | |
|                 if type(definition[key]) != list:
 | |
|                     raise SynapseError(400, "Expected %s to be a list." % key)
 | |
|                 for event_type in definition[key]:
 | |
|                     if not isinstance(event_type, basestring):
 | |
|                         raise SynapseError(400, "Event type should be a string")
 | |
| 
 | |
|         if "format" in definition:
 | |
|             event_format = definition["format"]
 | |
|             if event_format not in ["federation", "events"]:
 | |
|                 raise SynapseError(400, "Invalid format: %s" % (event_format,))
 | |
| 
 | |
|         if "select" in definition:
 | |
|             event_select_list = definition["select"]
 | |
|             for select_key in event_select_list:
 | |
|                 if select_key not in ["event_id", "origin_server_ts",
 | |
|                                       "thread_id", "content", "content.body"]:
 | |
|                     raise SynapseError(400, "Bad select: %s" % (select_key,))
 | |
| 
 | |
|         if ("bundle_updates" in definition and
 | |
|                 type(definition["bundle_updates"]) != bool):
 | |
|             raise SynapseError(400, "Bad bundle_updates: expected bool.")
 | |
| 
 | |
| 
 | |
| class Filter(object):
 | |
|     def __init__(self, filter_json):
 | |
|         self.filter_json = filter_json
 | |
| 
 | |
|     def timeline_limit(self):
 | |
|         return self.filter_json.get("room", {}).get("timeline", {}).get("limit", 10)
 | |
| 
 | |
|     def presence_limit(self):
 | |
|         return self.filter_json.get("presence", {}).get("limit", 10)
 | |
| 
 | |
|     def ephemeral_limit(self):
 | |
|         return self.filter_json.get("room", {}).get("ephemeral", {}).get("limit", 10)
 | |
| 
 | |
|     def filter_presence(self, events):
 | |
|         return self._filter_on_key(events, ["presence"])
 | |
| 
 | |
|     def filter_room_state(self, events):
 | |
|         return self._filter_on_key(events, ["room", "state"])
 | |
| 
 | |
|     def filter_room_timeline(self, events):
 | |
|         return self._filter_on_key(events, ["room", "timeline"])
 | |
| 
 | |
|     def filter_room_ephemeral(self, events):
 | |
|         return self._filter_on_key(events, ["room", "ephemeral"])
 | |
| 
 | |
|     def _filter_on_key(self, events, keys):
 | |
|         filter_json = self.filter_json
 | |
|         if not filter_json:
 | |
|             return events
 | |
| 
 | |
|         try:
 | |
|             # extract the right definition from the filter
 | |
|             definition = filter_json
 | |
|             for key in keys:
 | |
|                 definition = definition[key]
 | |
|             return self._filter_with_definition(events, definition)
 | |
|         except KeyError:
 | |
|             # return all events if definition isn't specified.
 | |
|             return events
 | |
| 
 | |
|     def _filter_with_definition(self, events, definition):
 | |
|         return [e for e in events if self._passes_definition(definition, e)]
 | |
| 
 | |
|     def _passes_definition(self, definition, event):
 | |
|         """Check if the event passes the filter definition
 | |
|         Args:
 | |
|             definition(dict): The filter definition to check against
 | |
|             event(dict or Event): The event to check
 | |
|         Returns:
 | |
|             True if the event passes the filter in the definition
 | |
|         """
 | |
|         if type(event) is dict:
 | |
|             room_id = event.get("room_id")
 | |
|             sender = event.get("sender")
 | |
|             event_type = event["type"]
 | |
|         else:
 | |
|             room_id = getattr(event, "room_id", None)
 | |
|             sender = getattr(event, "sender", None)
 | |
|             event_type = event.type
 | |
|         return self._event_passes_definition(
 | |
|             definition, room_id, sender, event_type
 | |
|         )
 | |
| 
 | |
|     def _event_passes_definition(self, definition, room_id, sender,
 | |
|                                  event_type):
 | |
|         """Check if the event passes through the given definition.
 | |
| 
 | |
|         Args:
 | |
|             definition(dict): The definition to check against.
 | |
|             room_id(str): The id of the room this event is in or None.
 | |
|             sender(str): The sender of the event
 | |
|             event_type(str): The type of the event.
 | |
|         Returns:
 | |
|             True if the event passes through the filter.
 | |
|         """
 | |
|         # Algorithm notes:
 | |
|         # For each key in the definition, check the event meets the criteria:
 | |
|         #   * For types: Literal match or prefix match (if ends with wildcard)
 | |
|         #   * For senders/rooms: Literal match only
 | |
|         #   * "not_" checks take presedence (e.g. if "m.*" is in both 'types'
 | |
|         #     and 'not_types' then it is treated as only being in 'not_types')
 | |
| 
 | |
|         # room checks
 | |
|         if room_id is not None:
 | |
|             allow_rooms = definition.get("rooms", None)
 | |
|             reject_rooms = definition.get("not_rooms", None)
 | |
|             if reject_rooms and room_id in reject_rooms:
 | |
|                 return False
 | |
|             if allow_rooms and room_id not in allow_rooms:
 | |
|                 return False
 | |
| 
 | |
|         # sender checks
 | |
|         if sender is not None:
 | |
|             allow_senders = definition.get("senders", None)
 | |
|             reject_senders = definition.get("not_senders", None)
 | |
|             if reject_senders and sender in reject_senders:
 | |
|                 return False
 | |
|             if allow_senders and sender not in allow_senders:
 | |
|                 return False
 | |
| 
 | |
|         # type checks
 | |
|         if "not_types" in definition:
 | |
|             for def_type in definition["not_types"]:
 | |
|                 if self._event_matches_type(event_type, def_type):
 | |
|                     return False
 | |
|         if "types" in definition:
 | |
|             included = False
 | |
|             for def_type in definition["types"]:
 | |
|                 if self._event_matches_type(event_type, def_type):
 | |
|                     included = True
 | |
|                     break
 | |
|             if not included:
 | |
|                 return False
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     def _event_matches_type(self, event_type, def_type):
 | |
|         if def_type.endswith("*"):
 | |
|             type_prefix = def_type[:-1]
 | |
|             return event_type.startswith(type_prefix)
 | |
|         else:
 | |
|             return event_type == def_type
 |