230 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			230 lines
		
	
	
		
			8.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", "events", "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 filter_public_user_data(self, events):
 | |
|         return self._filter_on_key(events, ["public_user_data"])
 | |
| 
 | |
|     def filter_private_user_data(self, events):
 | |
|         return self._filter_on_key(events, ["private_user_data"])
 | |
| 
 | |
|     def filter_room_state(self, events):
 | |
|         return self._filter_on_key(events, ["room", "state"])
 | |
| 
 | |
|     def filter_room_events(self, events):
 | |
|         return self._filter_on_key(events, ["room", "events"])
 | |
| 
 | |
|     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 through the given definition.
 | |
| 
 | |
|         Args:
 | |
|             definition(dict): The definition to check against.
 | |
|             event(Event): The event to check.
 | |
|         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 hasattr(event, "room_id"):
 | |
|             room_id = event.room_id
 | |
|             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 hasattr(event, "sender"):
 | |
|             # Should we be including event.state_key for some event types?
 | |
|             sender = event.sender
 | |
|             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, def_type):
 | |
|                     return False
 | |
|         if "types" in definition:
 | |
|             included = False
 | |
|             for def_type in definition["types"]:
 | |
|                 if self._event_matches_type(event, def_type):
 | |
|                     included = True
 | |
|                     break
 | |
|             if not included:
 | |
|                 return False
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     def _event_matches_type(self, event, def_type):
 | |
|         if def_type.endswith("*"):
 | |
|             type_prefix = def_type[:-1]
 | |
|             return event.type.startswith(type_prefix)
 | |
|         else:
 | |
|             return event.type == def_type
 |