Merge commit 'f4c80d70f' into release-v0.99.5
						commit
						f3ff64e000
					
				| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					Add experimental support for relations (aka reactions and edits).
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					Add experimental support for relations (aka reactions and edits).
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ from canonicaljson import encode_canonical_json, json
 | 
				
			||||||
from twisted.internet import defer
 | 
					from twisted.internet import defer
 | 
				
			||||||
from twisted.internet.defer import succeed
 | 
					from twisted.internet.defer import succeed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from synapse.api.constants import EventTypes, Membership
 | 
					from synapse.api.constants import EventTypes, Membership, RelationTypes
 | 
				
			||||||
from synapse.api.errors import (
 | 
					from synapse.api.errors import (
 | 
				
			||||||
    AuthError,
 | 
					    AuthError,
 | 
				
			||||||
    Codes,
 | 
					    Codes,
 | 
				
			||||||
| 
						 | 
					@ -601,6 +601,20 @@ class EventCreationHandler(object):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.validator.validate_new(event)
 | 
					        self.validator.validate_new(event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If this event is an annotation then we check that that the sender
 | 
				
			||||||
 | 
					        # can't annotate the same way twice (e.g. stops users from liking an
 | 
				
			||||||
 | 
					        # event multiple times).
 | 
				
			||||||
 | 
					        relation = event.content.get("m.relates_to", {})
 | 
				
			||||||
 | 
					        if relation.get("rel_type") == RelationTypes.ANNOTATION:
 | 
				
			||||||
 | 
					            relates_to = relation["event_id"]
 | 
				
			||||||
 | 
					            aggregation_key = relation["key"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            already_exists = yield self.store.has_user_annotated_event(
 | 
				
			||||||
 | 
					                relates_to, event.type, aggregation_key, event.sender,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if already_exists:
 | 
				
			||||||
 | 
					                raise SynapseError(400, "Can't send same reaction twice")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.debug(
 | 
					        logger.debug(
 | 
				
			||||||
            "Created event %s",
 | 
					            "Created event %s",
 | 
				
			||||||
            event.event_id,
 | 
					            event.event_id,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -280,7 +280,7 @@ class RelationsWorkerStore(SQLBaseStore):
 | 
				
			||||||
            having_clause = ""
 | 
					            having_clause = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sql = """
 | 
					        sql = """
 | 
				
			||||||
            SELECT type, aggregation_key, COUNT(*), MAX(stream_ordering)
 | 
					            SELECT type, aggregation_key, COUNT(DISTINCT sender), MAX(stream_ordering)
 | 
				
			||||||
            FROM event_relations
 | 
					            FROM event_relations
 | 
				
			||||||
            INNER JOIN events USING (event_id)
 | 
					            INNER JOIN events USING (event_id)
 | 
				
			||||||
            WHERE {where_clause}
 | 
					            WHERE {where_clause}
 | 
				
			||||||
| 
						 | 
					@ -350,9 +350,7 @@ class RelationsWorkerStore(SQLBaseStore):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def _get_applicable_edit_txn(txn):
 | 
					        def _get_applicable_edit_txn(txn):
 | 
				
			||||||
            txn.execute(
 | 
					            txn.execute(sql, (event_id, RelationTypes.REPLACE))
 | 
				
			||||||
                sql, (event_id, RelationTypes.REPLACE,)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            row = txn.fetchone()
 | 
					            row = txn.fetchone()
 | 
				
			||||||
            if row:
 | 
					            if row:
 | 
				
			||||||
                return row[0]
 | 
					                return row[0]
 | 
				
			||||||
| 
						 | 
					@ -367,6 +365,50 @@ class RelationsWorkerStore(SQLBaseStore):
 | 
				
			||||||
        edit_event = yield self.get_event(edit_id, allow_none=True)
 | 
					        edit_event = yield self.get_event(edit_id, allow_none=True)
 | 
				
			||||||
        defer.returnValue(edit_event)
 | 
					        defer.returnValue(edit_event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def has_user_annotated_event(self, parent_id, event_type, aggregation_key, sender):
 | 
				
			||||||
 | 
					        """Check if a user has already annotated an event with the same key
 | 
				
			||||||
 | 
					        (e.g. already liked an event).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            parent_id (str): The event being annotated
 | 
				
			||||||
 | 
					            event_type (str): The event type of the annotation
 | 
				
			||||||
 | 
					            aggregation_key (str): The aggregation key of the annotation
 | 
				
			||||||
 | 
					            sender (str): The sender of the annotation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Deferred[bool]
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sql = """
 | 
				
			||||||
 | 
					            SELECT 1 FROM event_relations
 | 
				
			||||||
 | 
					            INNER JOIN events USING (event_id)
 | 
				
			||||||
 | 
					            WHERE
 | 
				
			||||||
 | 
					                relates_to_id = ?
 | 
				
			||||||
 | 
					                AND relation_type = ?
 | 
				
			||||||
 | 
					                AND type = ?
 | 
				
			||||||
 | 
					                AND sender = ?
 | 
				
			||||||
 | 
					                AND aggregation_key = ?
 | 
				
			||||||
 | 
					            LIMIT 1;
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def _get_if_user_has_annotated_event(txn):
 | 
				
			||||||
 | 
					            txn.execute(
 | 
				
			||||||
 | 
					                sql,
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    parent_id,
 | 
				
			||||||
 | 
					                    RelationTypes.ANNOTATION,
 | 
				
			||||||
 | 
					                    event_type,
 | 
				
			||||||
 | 
					                    sender,
 | 
				
			||||||
 | 
					                    aggregation_key,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return bool(txn.fetchone())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.runInteraction(
 | 
				
			||||||
 | 
					            "get_if_user_has_annotated_event", _get_if_user_has_annotated_event
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RelationsStore(RelationsWorkerStore):
 | 
					class RelationsStore(RelationsWorkerStore):
 | 
				
			||||||
    def _handle_event_relations(self, txn, event):
 | 
					    def _handle_event_relations(self, txn, event):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,6 +90,15 @@ class RelationsTestCase(unittest.HomeserverTestCase):
 | 
				
			||||||
        channel = self._send_relation(RelationTypes.ANNOTATION, EventTypes.Member)
 | 
					        channel = self._send_relation(RelationTypes.ANNOTATION, EventTypes.Member)
 | 
				
			||||||
        self.assertEquals(400, channel.code, channel.json_body)
 | 
					        self.assertEquals(400, channel.code, channel.json_body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_deny_double_react(self):
 | 
				
			||||||
 | 
					        """Test that we deny relations on membership events
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", "a")
 | 
				
			||||||
 | 
					        self.assertEquals(200, channel.code, channel.json_body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", "a")
 | 
				
			||||||
 | 
					        self.assertEquals(400, channel.code, channel.json_body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_basic_paginate_relations(self):
 | 
					    def test_basic_paginate_relations(self):
 | 
				
			||||||
        """Tests that calling pagination API corectly the latest relations.
 | 
					        """Tests that calling pagination API corectly the latest relations.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -234,14 +243,30 @@ class RelationsTestCase(unittest.HomeserverTestCase):
 | 
				
			||||||
        """Test that we can paginate within an annotation group.
 | 
					        """Test that we can paginate within an annotation group.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We need to create ten separate users to send each reaction.
 | 
				
			||||||
 | 
					        access_tokens = [self.user_token, self.user2_token]
 | 
				
			||||||
 | 
					        idx = 0
 | 
				
			||||||
 | 
					        while len(access_tokens) < 10:
 | 
				
			||||||
 | 
					            user_id, token = self._create_user("test" + str(idx))
 | 
				
			||||||
 | 
					            idx += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.helper.join(self.room, user=user_id, tok=token)
 | 
				
			||||||
 | 
					            access_tokens.append(token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        idx = 0
 | 
				
			||||||
        expected_event_ids = []
 | 
					        expected_event_ids = []
 | 
				
			||||||
        for _ in range(10):
 | 
					        for _ in range(10):
 | 
				
			||||||
            channel = self._send_relation(
 | 
					            channel = self._send_relation(
 | 
				
			||||||
                RelationTypes.ANNOTATION, "m.reaction", key=u"👍"
 | 
					                RelationTypes.ANNOTATION,
 | 
				
			||||||
 | 
					                "m.reaction",
 | 
				
			||||||
 | 
					                key=u"👍",
 | 
				
			||||||
 | 
					                access_token=access_tokens[idx],
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            self.assertEquals(200, channel.code, channel.json_body)
 | 
					            self.assertEquals(200, channel.code, channel.json_body)
 | 
				
			||||||
            expected_event_ids.append(channel.json_body["event_id"])
 | 
					            expected_event_ids.append(channel.json_body["event_id"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            idx += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Also send a different type of reaction so that we test we don't see it
 | 
					        # Also send a different type of reaction so that we test we don't see it
 | 
				
			||||||
        channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", key="a")
 | 
					        channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", key="a")
 | 
				
			||||||
        self.assertEquals(200, channel.code, channel.json_body)
 | 
					        self.assertEquals(200, channel.code, channel.json_body)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue