293 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
# 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 logging
 | 
						|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
 | 
						|
 | 
						|
import attr
 | 
						|
from nacl.signing import SigningKey
 | 
						|
 | 
						|
from synapse.api.constants import MAX_DEPTH
 | 
						|
from synapse.api.room_versions import (
 | 
						|
    KNOWN_EVENT_FORMAT_VERSIONS,
 | 
						|
    EventFormatVersions,
 | 
						|
    RoomVersion,
 | 
						|
)
 | 
						|
from synapse.crypto.event_signing import add_hashes_and_signatures
 | 
						|
from synapse.events import EventBase, _EventInternalMetadata, make_event_from_dict
 | 
						|
from synapse.state import StateHandler
 | 
						|
from synapse.storage.databases.main import DataStore
 | 
						|
from synapse.types import EventID, JsonDict
 | 
						|
from synapse.util import Clock
 | 
						|
from synapse.util.stringutils import random_string
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from synapse.handlers.event_auth import EventAuthHandler
 | 
						|
    from synapse.server import HomeServer
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
@attr.s(slots=True, cmp=False, frozen=True, auto_attribs=True)
 | 
						|
class EventBuilder:
 | 
						|
    """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:
 | 
						|
        room_version: Version of the target room
 | 
						|
        room_id
 | 
						|
        type
 | 
						|
        sender
 | 
						|
        content
 | 
						|
        unsigned
 | 
						|
        internal_metadata
 | 
						|
 | 
						|
        _state
 | 
						|
        _auth
 | 
						|
        _store
 | 
						|
        _clock
 | 
						|
        _hostname: The hostname of the server creating the event
 | 
						|
        _signing_key: The signing key to use to sign the event as the server
 | 
						|
    """
 | 
						|
 | 
						|
    _state: StateHandler
 | 
						|
    _event_auth_handler: "EventAuthHandler"
 | 
						|
    _store: DataStore
 | 
						|
    _clock: Clock
 | 
						|
    _hostname: str
 | 
						|
    _signing_key: SigningKey
 | 
						|
 | 
						|
    room_version: RoomVersion
 | 
						|
 | 
						|
    room_id: str
 | 
						|
    type: str
 | 
						|
    sender: str
 | 
						|
 | 
						|
    content: JsonDict = attr.Factory(dict)
 | 
						|
    unsigned: JsonDict = 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: Optional[str] = None
 | 
						|
    _redacts: Optional[str] = None
 | 
						|
    _origin_server_ts: Optional[int] = None
 | 
						|
 | 
						|
    internal_metadata: _EventInternalMetadata = attr.Factory(
 | 
						|
        lambda: _EventInternalMetadata({})
 | 
						|
    )
 | 
						|
 | 
						|
    @property
 | 
						|
    def state_key(self) -> str:
 | 
						|
        if self._state_key is not None:
 | 
						|
            return self._state_key
 | 
						|
 | 
						|
        raise AttributeError("state_key")
 | 
						|
 | 
						|
    def is_state(self) -> bool:
 | 
						|
        return self._state_key is not None
 | 
						|
 | 
						|
    async def build(
 | 
						|
        self,
 | 
						|
        prev_event_ids: List[str],
 | 
						|
        auth_event_ids: Optional[List[str]],
 | 
						|
        depth: Optional[int] = None,
 | 
						|
    ) -> EventBase:
 | 
						|
        """Transform into a fully signed and hashed event
 | 
						|
 | 
						|
        Args:
 | 
						|
            prev_event_ids: The event IDs to use as the prev events
 | 
						|
            auth_event_ids: The event IDs to use as the auth events.
 | 
						|
                Should normally be set to None, which will cause them to be calculated
 | 
						|
                based on the room state at the prev_events.
 | 
						|
            depth: Override the depth used to order the event in the DAG.
 | 
						|
                Should normally be set to None, which will cause the depth to be calculated
 | 
						|
                based on the prev_events.
 | 
						|
 | 
						|
        Returns:
 | 
						|
            The signed and hashed event.
 | 
						|
        """
 | 
						|
        if auth_event_ids is None:
 | 
						|
            state_ids = await self._state.get_current_state_ids(
 | 
						|
                self.room_id, prev_event_ids
 | 
						|
            )
 | 
						|
            auth_event_ids = self._event_auth_handler.compute_auth_events(
 | 
						|
                self, state_ids
 | 
						|
            )
 | 
						|
 | 
						|
        format_version = self.room_version.event_format
 | 
						|
        # The types of auth/prev events changes between event versions.
 | 
						|
        prev_events: Union[List[str], List[Tuple[str, Dict[str, str]]]]
 | 
						|
        auth_events: Union[List[str], List[Tuple[str, Dict[str, str]]]]
 | 
						|
        if format_version == EventFormatVersions.V1:
 | 
						|
            auth_events = await self._store.add_event_hashes(auth_event_ids)
 | 
						|
            prev_events = await self._store.add_event_hashes(prev_event_ids)
 | 
						|
        else:
 | 
						|
            auth_events = auth_event_ids
 | 
						|
            prev_events = prev_event_ids
 | 
						|
 | 
						|
        # Otherwise, progress the depth as normal
 | 
						|
        if depth is None:
 | 
						|
            (
 | 
						|
                _,
 | 
						|
                most_recent_prev_event_depth,
 | 
						|
            ) = await self._store.get_max_depth_of(prev_event_ids)
 | 
						|
 | 
						|
            depth = most_recent_prev_event_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: Dict[str, Any] = {
 | 
						|
            "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
 | 
						|
 | 
						|
        if self._origin_server_ts is not None:
 | 
						|
            event_dict["origin_server_ts"] = self._origin_server_ts
 | 
						|
 | 
						|
        return create_local_event_from_event_dict(
 | 
						|
            clock=self._clock,
 | 
						|
            hostname=self._hostname,
 | 
						|
            signing_key=self._signing_key,
 | 
						|
            room_version=self.room_version,
 | 
						|
            event_dict=event_dict,
 | 
						|
            internal_metadata_dict=self.internal_metadata.get_dict(),
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class EventBuilderFactory:
 | 
						|
    def __init__(self, hs: "HomeServer"):
 | 
						|
        self.clock = hs.get_clock()
 | 
						|
        self.hostname = hs.hostname
 | 
						|
        self.signing_key = hs.signing_key
 | 
						|
 | 
						|
        self.store = hs.get_datastore()
 | 
						|
        self.state = hs.get_state_handler()
 | 
						|
        self._event_auth_handler = hs.get_event_auth_handler()
 | 
						|
 | 
						|
    def for_room_version(
 | 
						|
        self, room_version: RoomVersion, key_values: dict
 | 
						|
    ) -> EventBuilder:
 | 
						|
        """Generate an event builder appropriate for the given room version
 | 
						|
 | 
						|
        Args:
 | 
						|
            room_version:
 | 
						|
                Version of the room that we're creating an event builder for
 | 
						|
            key_values: Fields used as the basis of the new event
 | 
						|
 | 
						|
        Returns:
 | 
						|
            EventBuilder
 | 
						|
        """
 | 
						|
        return EventBuilder(
 | 
						|
            store=self.store,
 | 
						|
            state=self.state,
 | 
						|
            event_auth_handler=self._event_auth_handler,
 | 
						|
            clock=self.clock,
 | 
						|
            hostname=self.hostname,
 | 
						|
            signing_key=self.signing_key,
 | 
						|
            room_version=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),
 | 
						|
            origin_server_ts=key_values.get("origin_server_ts", None),
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
def create_local_event_from_event_dict(
 | 
						|
    clock: Clock,
 | 
						|
    hostname: str,
 | 
						|
    signing_key: SigningKey,
 | 
						|
    room_version: RoomVersion,
 | 
						|
    event_dict: JsonDict,
 | 
						|
    internal_metadata_dict: Optional[JsonDict] = None,
 | 
						|
) -> EventBase:
 | 
						|
    """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.
 | 
						|
    """
 | 
						|
 | 
						|
    format_version = room_version.event_format
 | 
						|
    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())
 | 
						|
 | 
						|
    if format_version == EventFormatVersions.V1:
 | 
						|
        event_dict["event_id"] = _create_event_id(clock, hostname)
 | 
						|
 | 
						|
    event_dict["origin"] = hostname
 | 
						|
    event_dict.setdefault("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(room_version, event_dict, hostname, signing_key)
 | 
						|
    return make_event_from_dict(
 | 
						|
        event_dict, room_version, internal_metadata_dict=internal_metadata_dict
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
# A counter used when generating new event IDs
 | 
						|
_event_id_counter = 0
 | 
						|
 | 
						|
 | 
						|
def _create_event_id(clock: Clock, hostname: str) -> str:
 | 
						|
    """Create a new event ID
 | 
						|
 | 
						|
    Args:
 | 
						|
        clock
 | 
						|
        hostname: The server name for the event ID
 | 
						|
 | 
						|
    Returns:
 | 
						|
        The new event ID
 | 
						|
    """
 | 
						|
 | 
						|
    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()
 |