# -*- coding: utf-8 -*- # Copyright 2014-2016 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. import attr from twisted.internet import defer from synapse.api.constants import ( KNOWN_EVENT_FORMAT_VERSIONS, KNOWN_ROOM_VERSIONS, MAX_DEPTH, ) from synapse.crypto.event_signing import add_hashes_and_signatures from synapse.types import EventID from synapse.util.stringutils import random_string from . import ( _EventInternalMetadata, event_type_from_format_version, room_version_to_event_format, ) @attr.s(slots=True, cmp=False, frozen=True) class EventBuilder(object): """A format independent event builder used to build up the event content before signing the event. (Note that while objects of this class are frozen, the content/unsigned/internal_metadata fields are still mutable) Attributes: format_version (int): Event format version room_id (str) type (str) sender (str) content (dict) unsigned (dict) internal_metadata (_EventInternalMetadata) _state (StateHandler) _auth (synapse.api.Auth) _store (DataStore) _clock (Clock) _hostname (str): The hostname of the server creating the event _signing_key: The signing key to use to sign the event as the server """ _state = attr.ib() _auth = attr.ib() _store = attr.ib() _clock = attr.ib() _hostname = attr.ib() _signing_key = attr.ib() format_version = attr.ib() room_id = attr.ib() type = attr.ib() sender = attr.ib() content = attr.ib(default=attr.Factory(dict)) unsigned = attr.ib(default=attr.Factory(dict)) # These only exist on a subset of events, so they raise AttributeError if # someone tries to get them when they don't exist. _state_key = attr.ib(default=None) _redacts = attr.ib(default=None) internal_metadata = attr.ib(default=attr.Factory(lambda: _EventInternalMetadata({}))) @property def state_key(self): if self._state_key is not None: return self._state_key raise AttributeError("state_key") def is_state(self): return self._state_key is not None @defer.inlineCallbacks def build(self, prev_event_ids): """Transform into a fully signed and hashed event Args: prev_event_ids (list[str]): The event IDs to use as the prev events Returns: Deferred[FrozenEvent] """ state_ids = yield self._state.get_current_state_ids( self.room_id, prev_event_ids, ) auth_ids = yield self._auth.compute_auth_events( self, state_ids, ) auth_events = yield self._store.add_event_hashes(auth_ids) prev_events = yield self._store.add_event_hashes(prev_event_ids) old_depth = yield self._store.get_max_depth_of( prev_event_ids, ) depth = old_depth + 1 # we cap depth of generated events, to ensure that they are not # rejected by other servers (and so that they can be persisted in # the db) depth = min(depth, MAX_DEPTH) event_dict = { "auth_events": auth_events, "prev_events": prev_events, "type": self.type, "room_id": self.room_id, "sender": self.sender, "content": self.content, "unsigned": self.unsigned, "depth": depth, "prev_state": [], } if self.is_state(): event_dict["state_key"] = self._state_key if self._redacts is not None: event_dict["redacts"] = self._redacts defer.returnValue( create_local_event_from_event_dict( clock=self._clock, hostname=self._hostname, signing_key=self._signing_key, format_version=self.format_version, event_dict=event_dict, internal_metadata_dict=self.internal_metadata.get_dict(), ) ) class EventBuilderFactory(object): def __init__(self, hs): self.clock = hs.get_clock() self.hostname = hs.hostname self.signing_key = hs.config.signing_key[0] self.store = hs.get_datastore() self.state = hs.get_state_handler() self.auth = hs.get_auth() def new(self, room_version, key_values): """Generate an event builder appropriate for the given room version Args: room_version (str): Version of the room that we're creating an event builder for key_values (dict): Fields used as the basis of the new event Returns: EventBuilder """ # There's currently only the one event version defined if room_version not in KNOWN_ROOM_VERSIONS: raise Exception( "No event format defined for version %r" % (room_version,) ) return EventBuilder( store=self.store, state=self.state, auth=self.auth, clock=self.clock, hostname=self.hostname, signing_key=self.signing_key, format_version=room_version_to_event_format(room_version), type=key_values["type"], state_key=key_values.get("state_key"), room_id=key_values["room_id"], sender=key_values["sender"], content=key_values.get("content", {}), unsigned=key_values.get("unsigned", {}), redacts=key_values.get("redacts", None), ) def create_local_event_from_event_dict(clock, hostname, signing_key, format_version, event_dict, internal_metadata_dict=None): """Takes a fully formed event dict, ensuring that fields like `origin` and `origin_server_ts` have correct values for a locally produced event, then signs and hashes it. Args: clock (Clock) hostname (str) signing_key format_version (int) event_dict (dict) internal_metadata_dict (dict|None) Returns: FrozenEvent """ # There's currently only the one event version defined if format_version not in KNOWN_EVENT_FORMAT_VERSIONS: raise Exception( "No event format defined for version %r" % (format_version,) ) if internal_metadata_dict is None: internal_metadata_dict = {} time_now = int(clock.time_msec()) event_dict["event_id"] = _create_event_id(clock, hostname) event_dict["origin"] = hostname event_dict["origin_server_ts"] = time_now event_dict.setdefault("unsigned", {}) age = event_dict["unsigned"].pop("age", 0) event_dict["unsigned"].setdefault("age_ts", time_now - age) event_dict.setdefault("signatures", {}) add_hashes_and_signatures( event_dict, hostname, signing_key, ) return event_type_from_format_version(format_version)( event_dict, internal_metadata_dict=internal_metadata_dict, ) # A counter used when generating new event IDs _event_id_counter = 0 def _create_event_id(clock, hostname): """Create a new event ID Args: clock (Clock) hostname (str): The server name for the event ID Returns: str """ global _event_id_counter i = str(_event_id_counter) _event_id_counter += 1 local_part = str(int(clock.time())) + i + random_string(5) e_id = EventID(local_part, hostname) return e_id.to_string()