diff options
168 files changed, 1898 insertions, 789 deletions
diff --git a/UPGRADE.rst b/UPGRADE.rst index 99d58ab64f..da2a7a0a21 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -1,11 +1,6 @@ Upgrading to v0.2.0 =================== -To upgrade the database schema, run:: - - ./database-prepare-for-0.2.0.sh "<database>.db" - - The home server now requires setting up of SSL config before it can run. To automatically generate default config use:: diff --git a/cmdclient/console.py b/cmdclient/console.py index f6a7731eab..2e6b026762 100755 --- a/cmdclient/console.py +++ b/cmdclient/console.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -88,6 +88,8 @@ class SynapseCmd(cmd.Cmd): return False def _domain(self): + if "user" not in self.config or not self.config["user"]: + return None return self.config["user"].split(":")[1] def do_config(self, line): @@ -191,8 +193,10 @@ class SynapseCmd(cmd.Cmd): p = getpass.getpass("Enter your password: ") user = args["user_id"] if self._is_on("complete_usernames") and not user.startswith("@"): - user = "@" + user + ":" + self._domain() - + domain = self._domain() + if domain: + user = "@" + user + ":" + domain + reactor.callFromThread(self._do_login, user, p) #print " got %s " % p except Exception as e: @@ -312,7 +316,7 @@ class SynapseCmd(cmd.Cmd): try: args = self._parse(line, ["roomname"], force_keys=True) path = "/join/%s" % urllib.quote(args["roomname"]) - reactor.callFromThread(self._run_and_pprint, "PUT", path, {}) + reactor.callFromThread(self._run_and_pprint, "POST", path, {}) except Exception as e: print e @@ -700,7 +704,7 @@ def main(server_url, identity_server_url, username, token, config_path): if __name__ == '__main__': parser = argparse.ArgumentParser("Starts a synapse client.") parser.add_argument( - "-s", "--server", dest="server", default="http://localhost:8080", + "-s", "--server", dest="server", default="http://localhost:8008", help="The URL of the home server to talk to.") parser.add_argument( "-i", "--identity-server", dest="identityserver", default="http://localhost:8090", diff --git a/cmdclient/http.py b/cmdclient/http.py index 9de6be9b72..869f782ec1 100644 --- a/cmdclient/http.py +++ b/cmdclient/http.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/database-prepare-for-0.2.0.sh b/database-prepare-for-0.2.0.sh deleted file mode 100755 index e90171010b..0000000000 --- a/database-prepare-for-0.2.0.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# This is will prepare a synapse database for running with v0.2.0 of synapse. - -set -e - -cp "$1" "$1.bak" - -sqlite3 "$1" < "synapse/storage/schema/im.sql" -sqlite3 "$1" <<< "PRAGMA user_version = 2;" diff --git a/docs/client-server/specification.rst b/docs/client-server/OLD_specification.rst index 2f6645ceb9..47fba5eeac 100644 --- a/docs/client-server/specification.rst +++ b/docs/client-server/OLD_specification.rst @@ -2,6 +2,20 @@ Matrix Client-Server API ======================== + +.. WARNING:: + This specification is old. Please see /docs/specification.rst instead. + + + + + + + + + + + The following specification outlines how a client can send and receive data from a home server. diff --git a/docs/client-server/swagger_matrix/api-docs-directory b/docs/client-server/swagger_matrix/api-docs-directory index 98109a0fbc..ce12be8c96 100644 --- a/docs/client-server/swagger_matrix/api-docs-directory +++ b/docs/client-server/swagger_matrix/api-docs-directory @@ -1,7 +1,7 @@ { "apiVersion": "1.0.0", "swaggerVersion": "1.2", - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "resourcePath": "/directory", "produces": [ "application/json" @@ -13,6 +13,7 @@ { "method": "GET", "summary": "Get the room ID corresponding to this room alias.", + "notes": "Volatile: This API is likely to change.", "type": "DirectoryResponse", "nickname": "get_room_id_for_alias", "parameters": [ @@ -28,6 +29,7 @@ { "method": "PUT", "summary": "Create a new mapping from room alias to room ID.", + "notes": "Volatile: This API is likely to change.", "type": "void", "nickname": "add_room_alias", "parameters": [ diff --git a/docs/client-server/swagger_matrix/api-docs-events b/docs/client-server/swagger_matrix/api-docs-events index e5dd3a6113..1bdb9b034a 100644 --- a/docs/client-server/swagger_matrix/api-docs-events +++ b/docs/client-server/swagger_matrix/api-docs-events @@ -1,7 +1,7 @@ { "apiVersion": "1.0.0", "swaggerVersion": "1.2", - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "resourcePath": "/events", "produces": [ "application/json" diff --git a/docs/client-server/swagger_matrix/api-docs-login b/docs/client-server/swagger_matrix/api-docs-login index 8cc598b3c1..d6f8d84f29 100644 --- a/docs/client-server/swagger_matrix/api-docs-login +++ b/docs/client-server/swagger_matrix/api-docs-login @@ -8,7 +8,7 @@ "nickname": "get_login_info", "notes": "All login stages MUST be mentioned if there is >1 login type.", "summary": "Get the login mechanism to use when logging in.", - "type": "LoginInfo" + "type": "LoginFlows" }, { "method": "POST", @@ -40,17 +40,31 @@ "path": "/login" } ], - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "consumes": [ "application/json" ], "models": { + "LoginFlows": { + "id": "LoginFlows", + "properties": { + "flows": { + "description": "A list of valid login flows.", + "type": "array", + "items": { + "$ref": "LoginInfo" + } + } + } + }, "LoginInfo": { "id": "LoginInfo", "properties": { "stages": { "description": "Multi-stage login only: An array of all the login types required to login.", - "format": "string", + "items": { + "$ref": "string" + }, "type": "array" }, "type": { @@ -65,6 +79,10 @@ "access_token": { "description": "The access token for this user's login if this is the final stage of the login process.", "type": "string" + }, + "user_id": { + "description": "The user's fully-qualified user ID.", + "type": "string" }, "next": { "description": "Multi-stage login only: The next login type to submit.", diff --git a/docs/client-server/swagger_matrix/api-docs-presence b/docs/client-server/swagger_matrix/api-docs-presence index d52ce2164a..6b22446024 100644 --- a/docs/client-server/swagger_matrix/api-docs-presence +++ b/docs/client-server/swagger_matrix/api-docs-presence @@ -1,7 +1,7 @@ { "apiVersion": "1.0.0", "swaggerVersion": "1.2", - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "resourcePath": "/presence", "produces": [ "application/json" @@ -106,7 +106,7 @@ "PresenceUpdate": { "id": "PresenceUpdate", "properties": { - "state": { + "presence": { "type": "string", "description": "Enum: The presence state.", "enum": [ @@ -128,10 +128,10 @@ "Presence": { "id": "Presence", "properties": { - "mtime_age": { + "last_active_ago": { "type": "integer", "format": "int64", - "description": "The last time this user's presence state changed, in milliseconds." + "description": "The last time this user performed an action on their home server." }, "user_id": { "type": "string", diff --git a/docs/client-server/swagger_matrix/api-docs-profile b/docs/client-server/swagger_matrix/api-docs-profile index 188259fa3d..d2fccaa67d 100644 --- a/docs/client-server/swagger_matrix/api-docs-profile +++ b/docs/client-server/swagger_matrix/api-docs-profile @@ -1,7 +1,7 @@ { "apiVersion": "1.0.0", "swaggerVersion": "1.2", - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "resourcePath": "/profile", "produces": [ "application/json" diff --git a/docs/client-server/swagger_matrix/api-docs-registration b/docs/client-server/swagger_matrix/api-docs-registration index 2048aec1d2..f4669ea2f0 100644 --- a/docs/client-server/swagger_matrix/api-docs-registration +++ b/docs/client-server/swagger_matrix/api-docs-registration @@ -37,7 +37,7 @@ "path": "/register" } ], - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "consumes": [ "application/json" ], @@ -52,6 +52,10 @@ "user_id": { "description": "The fully-qualified user ID.", "type": "string" + }, + "home_server": { + "description": "The name of the home server.", + "type": "string" } } }, diff --git a/docs/client-server/swagger_matrix/api-docs-rooms b/docs/client-server/swagger_matrix/api-docs-rooms index 0a8bb3c2a5..0e1fa452a2 100644 --- a/docs/client-server/swagger_matrix/api-docs-rooms +++ b/docs/client-server/swagger_matrix/api-docs-rooms @@ -1,7 +1,7 @@ { "apiVersion": "1.0.0", "swaggerVersion": "1.2", - "basePath": "http://localhost:8080/_matrix/client/api/v1", + "basePath": "http://localhost:8008/_matrix/client/api/v1", "resourcePath": "/rooms", "produces": [ "application/json" @@ -181,6 +181,59 @@ ] }, { + "path": "/rooms/{roomId}/state/m.room.name", + "operations": [ + { + "method": "PUT", + "summary": "Set the name of this room.", + "notes": "Set the name of this room.", + "type": "void", + "nickname": "set_room_name", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "name": "body", + "description": "The name contents", + "required": true, + "type": "RoomName", + "paramType": "body" + }, + { + "name": "roomId", + "description": "The room to set the name of.", + "required": true, + "type": "string", + "paramType": "path" + } + ] + }, + { + "method": "GET", + "summary": "Get the room's name.", + "notes": "", + "type": "RoomName", + "nickname": "get_room_name", + "parameters": [ + { + "name": "roomId", + "description": "The room to get the name of.", + "required": true, + "type": "string", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 404, + "message": "Name not found." + } + ] + } + ] + }, + { "path": "/rooms/{roomId}/send/m.room.message.feedback", "operations": [ { @@ -267,6 +320,12 @@ "required": true, "type": "string", "paramType": "path" + }, + { + "name": "body", + "required": true, + "type": "JoinRequest", + "paramType": "body" } ] } @@ -291,6 +350,12 @@ "required": true, "type": "string", "paramType": "path" + }, + { + "name": "body", + "required": true, + "type": "LeaveRequest", + "paramType": "body" } ] } @@ -424,10 +489,10 @@ "path": "/join/{roomAliasOrId}", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Join a room via a room alias or room ID.", "notes": "Join a room via a room alias or room ID.", - "type": "RoomInfo", + "type": "JoinRoomInfo", "nickname": "join", "consumes": [ "application/json" @@ -574,7 +639,7 @@ { "method": "GET", "summary": "Get a list of all the current state events for this room.", - "notes": "Get a list of all the current state events for this room.", + "notes": "NOT YET IMPLEMENTED.", "type": "array", "items": { "$ref": "Event" @@ -598,7 +663,7 @@ { "method": "GET", "summary": "Get all the current information for this room, including messages and state events.", - "notes": "Get all the current information for this room, including messages and state events.", + "notes": "NOT YET IMPLEMENTED.", "type": "InitialSyncRoomData", "nickname": "get_room_sync_data", "parameters": [ @@ -624,6 +689,15 @@ } } }, + "RoomName": { + "id": "RoomName", + "properties": { + "name": { + "type": "string", + "description": "The human-readable name for the room. Can contain spaces." + } + } + }, "Message": { "id": "Message", "properties": { @@ -640,6 +714,16 @@ "Feedback": { "id": "Feedback", "properties": { + "target_event_id": { + "type": "string", + "description": "The event ID being acknowledged.", + "required": true + }, + "type": { + "type": "string", + "description": "The type of feedback. Either 'delivered' or 'read'.", + "required": true + } } }, "Member": { @@ -652,7 +736,7 @@ "invite", "join", "leave", - "knock" + "ban" ] } } @@ -672,6 +756,16 @@ } } }, + "JoinRoomInfo": { + "id": "JoinRoomInfo", + "properties": { + "room_id": { + "type": "string", + "description": "The room ID joined, if joined via a room alias only.", + "required": true + } + } + }, "RoomConfig": { "id": "RoomConfig", "properties": { @@ -830,6 +924,14 @@ } } }, + "JoinRequest": { + "id": "JoinRequest", + "properties": {} + }, + "LeaveRequest": { + "id": "LeaveRequest", + "properties": {} + }, "BanRequest": { "id": "BanRequest", "properties": { diff --git a/docs/server-server/security-threat-model.rst b/docs/server-server/security-threat-model.rst deleted file mode 100644 index cf0430e43d..0000000000 --- a/docs/server-server/security-threat-model.rst +++ /dev/null @@ -1,141 +0,0 @@ -Overview -======== - -Scope ------ - -This document considers threats specific to the server to server federation -synapse protocol. - - -Attacker --------- - -It is assumed that the attacker can see and manipulate all network traffic -between any of the servers and may be in control of one or more homeservers -participating in the federation protocol. - -Threat Model -============ - -Denial of Service ------------------ - -The attacker could attempt to prevent delivery of messages to or from the -victim in order to: - - * Disrupt service or marketing campaign of a commercial competitor. - * Censor a discussion or censor a participant in a discussion. - * Perform general vandalism. - -Threat: Resource Exhaustion -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could cause the victims server to exhaust a particular resource -(e.g. open TCP connections, CPU, memory, disk storage) - -Threat: Unrecoverable Consistency Violations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could send messages which created an unrecoverable "split-brain" -state in the cluster such that the victim's servers could no longer dervive a -consistent view of the chatroom state. - -Threat: Bad History -~~~~~~~~~~~~~~~~~~~ - -An attacker could convince the victim to accept invalid messages which the -victim would then include in their view of the chatroom history. Other servers -in the chatroom would reject the invalid messages and potentially reject the -victims messages as well since they depended on the invalid messages. - -Threat: Block Network Traffic -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could try to firewall traffic between the victim's server and some -or all of the other servers in the chatroom. - -Threat: High Volume of Messages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could send large volumes of messages to a chatroom with the victim -making the chatroom unusable. - -Threat: Banning users without necessary authorisation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could attempt to ban a user from a chatroom with the necessary -authorisation. - -Spoofing --------- - -An attacker could try to send a message claiming to be from the victim without -the victim having sent the message in order to: - - * Impersonate the victim while performing illict activity. - * Obtain privileges of the victim. - -Threat: Altering Message Contents -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could try to alter the contents of an existing message from the -victim. - -Threat: Fake Message "origin" Field -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could try to send a new message purporting to be from the victim -with a phony "origin" field. - -Spamming --------- - -The attacker could try to send a high volume of solicicted or unsolicted -messages to the victim in order to: - - * Find victims for scams. - * Market unwanted products. - -Threat: Unsoliticted Messages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could try to send messages to victims who do not wish to receive -them. - -Threat: Abusive Messages -~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could send abusive or threatening messages to the victim - -Spying ------- - -The attacker could try to access message contents or metadata for messages sent -by the victim or to the victim that were not intended to reach the attacker in -order to: - - * Gain sensitive personal or commercial information. - * Impersonate the victim using credentials contained in the messages. - (e.g. password reset messages) - * Discover who the victim was talking to and when. - -Threat: Disclosure during Transmission -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could try to expose the message contents or metadata during -transmission between the servers. - -Threat: Disclosure to Servers Outside Chatroom -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could try to convince servers within a chatroom to send messages to -a server it controls that was not authorised to be within the chatroom. - -Threat: Disclosure to Servers Within Chatroom -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An attacker could take control of a server within a chatroom to expose message -contents or metadata for messages in that room. - - diff --git a/docs/specification.rst b/docs/specification.rst index fdd5f07917..239e51b4f3 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -30,48 +30,52 @@ ecosystem to communicate with one another. The principles that Matrix attempts to follow are: - - Pragmatic Web-friendly APIs (i.e. JSON over REST) - - Keep It Simple & Stupid +- Pragmatic Web-friendly APIs (i.e. JSON over REST) +- Keep It Simple & Stupid - + provide a simple architecture with minimal third-party dependencies. + + provide a simple architecture with minimal third-party dependencies. - - Fully open: +- Fully open: - + Fully open federation - anyone should be able to participate in the global Matrix network - + Fully open standard - publicly documented standard with no IP or patent licensing encumbrances - + Fully open source reference implementation - liberally-licensed example implementations with no - IP or patent licensing encumbrances + + Fully open federation - anyone should be able to participate in the global + Matrix network + + Fully open standard - publicly documented standard with no IP or patent + licensing encumbrances + + Fully open source reference implementation - liberally-licensed example + implementations with no IP or patent licensing encumbrances - - Empowering the end-user +- Empowering the end-user - + The user should be able to choose the server and clients they use - + The user should be control how private their communication is - + The user should know precisely where their data is stored + + The user should be able to choose the server and clients they use + + The user should be control how private their communication is + + The user should know precisely where their data is stored - - Fully decentralised - no single points of control over conversations or the network as a whole - - Learning from history to avoid repeating it +- Fully decentralised - no single points of control over conversations or the + network as a whole +- Learning from history to avoid repeating it - + Trying to take the best aspects of XMPP, SIP, IRC, SMTP, IMAP and NNTP whilst trying to avoid their failings + + Trying to take the best aspects of XMPP, SIP, IRC, SMTP, IMAP and NNTP + whilst trying to avoid their failings The functionality that Matrix provides includes: - - Creation and management of fully distributed chat rooms with no - single points of control or failure - - Eventually-consistent cryptographically secure synchronisation of room - state across a global open network of federated servers and services - - Sending and receiving extensible messages in a room with (optional) - end-to-end encryption - - Extensible user management (inviting, joining, leaving, kicking, banning) - mediated by a power-level based user privilege system. - - Extensible room state management (room naming, aliasing, topics, bans) - - Extensible user profile management (avatars, displaynames, etc) - - Managing user accounts (registration, login, logout) - - Use of 3rd Party IDs (3PIDs) such as email addresses, phone numbers, - Facebook accounts to authenticate, identify and discover users on Matrix. - - Trusted federation of Identity servers for: - - + Publishing user public keys for PKI - + Mapping of 3PIDs to Matrix IDs +- Creation and management of fully distributed chat rooms with no + single points of control or failure +- Eventually-consistent cryptographically secure synchronisation of room + state across a global open network of federated servers and services +- Sending and receiving extensible messages in a room with (optional) + end-to-end encryption +- Extensible user management (inviting, joining, leaving, kicking, banning) + mediated by a power-level based user privilege system. +- Extensible room state management (room naming, aliasing, topics, bans) +- Extensible user profile management (avatars, displaynames, etc) +- Managing user accounts (registration, login, logout) +- Use of 3rd Party IDs (3PIDs) such as email addresses, phone numbers, + Facebook accounts to authenticate, identify and discover users on Matrix. +- Trusted federation of Identity servers for: + + + Publishing user public keys for PKI + + Mapping of 3PIDs to Matrix IDs The end goal of Matrix is to be a ubiquitous messaging layer for synchronising arbitrary data between sets of people, devices and services - be that for instant @@ -428,9 +432,10 @@ event also has a ``creator`` key which contains the user ID of the room creator. It will also generate several other events in order to manage permissions in this room. This includes: - - ``m.room.power_levels`` : Sets the authority of the room creator. + - ``m.room.power_levels`` : Sets the power levels of users. - ``m.room.join_rules`` : Whether the room is "invite-only" or not. - - ``m.room.add_state_level`` + - ``m.room.add_state_level``: The power level required in order to + add new state to the room (as opposed to updating exisiting state) - ``m.room.send_event_level`` : The power level required in order to send a message in this room. - ``m.room.ops_level`` : The power level required in order to kick or @@ -463,6 +468,27 @@ Permissions Link through to respective sections where necessary. How does this tie in with permissions, e.g. give example of creating a read-only room. +Permissions for rooms are done via the concept of power levels - to do any +action in a room a user must have a suitable power level. + +Power levels for users are defined in ``m.room.power_levels``, where both +a default and specific users' power levels can be set. By default all users +have a power level of 0. + +State events may contain a ``required_power_level`` key, which indicates the +minimum power a user must have before they can update that state key. The only +exception to this is when a user leaves a room. + +To perform certain actions there are additional power level requirements +defined in the following state events: + +- ``m.room.send_event_level`` defines the minimum level for sending non-state + events. Defaults to 5. +- ``m.room.add_state_level`` defines the minimum level for adding new state, + rather than updating existing state. Defaults to 5. +- ``m.room.ops_level`` defines the minimum levels to ban and kick other users. + This defaults to a kick and ban levels of 5 each. + Joining rooms ------------- @@ -797,89 +823,88 @@ prefixed with ``m.`` to force this state change directly will fail. See the `Rooms`_ section for how to use the membership APIs. -``m.room.config`` +``m.room.create`` Summary: - The room config. + The first event in the room. Type: State event JSON format: - TODO + ``{ "creator": "string"}`` Example: - TODO + ``{ "creator": "@user:example.com" }`` Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + This is the first event in a room and cannot be changed. It acts as the + root of all other events. -``m.room.invite_join`` - Summary: - TODO. - Type: - State event - JSON format: - TODO - Example: - TODO - Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what - ``m.room.join_rules`` Summary: - TODO. + Descripes how/if people are allowed to join. Type: State event JSON format: - TODO + ``{ "join_rule": "enum [ public|knock|invite|private ]" }`` Example: - TODO + ``{ "join_rule": "public" }`` Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what - + TODO : Use docs/models/rooms.rst + ``m.room.power_levels`` Summary: - TODO. + Defines the power levels of users in the room. Type: State event JSON format: - TODO + ``{ "<user_id>": <int>, ..., "default": <int>}`` Example: - TODO + ``{ "@user:example.com": 5, "@user2:example.com": 10, "default": 0 }`` Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what - + If a user is in the list, then they have the associated power level. + Otherwise they have the default level. If not ``default`` key is supplied, + it is assumed to be 0. + ``m.room.add_state_level`` Summary: - TODO. + Defines the minimum power level a user needs to add state. Type: State event JSON format: - TODO + ``{ "level": <int> }`` Example: - TODO + ``{ "level": 5 }`` Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + To add a new piece of state to the room a user must have the given power + level. This does not apply to updating current state, which is goverened + by the ``required_power_level`` event key. ``m.room.send_event_level`` Summary: - TODO. + Defines the minimum power level a user needs to send an event. Type: State event JSON format: - TODO + ``{ "level": <int> }`` Example: - TODO + ``{ "level": 0 }`` Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what - + To send a new event into the room a user must have at least this power + level. This allows ops to make the room read only by increasing this level, + or muting individual users by lowering their power level below this + threshold. + ``m.room.ops_levels`` Summary: - TODO. + Defines the minimum power levels that a user must have before they can + kick and/or ban other users. Type: State event JSON format: - TODO + ``{ "ban_level": <int>, "kick_level": <int> }`` Example: - TODO + ``{ "ban_level": 5, "kick_level": 5 }`` Description: - TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + This defines who can ban and/or kick people in the room. Most of the time + ``ban_level`` will be greater than or equal to ``kick_level`` since + banning is more severe than kicking. ``m.room.message`` Summary: @@ -1485,6 +1510,31 @@ Each transaction has: - An origin and destination server name. - A list of "previous IDs". - A list of PDUs and EDUs - the actual message payload that the Transaction carries. + +``origin`` + Type: + String + Description: + DNS name of homeserver making this transaction. + +``ts`` + Type: + Integer + Description: + Timestamp in milliseconds on originating homeserver when this transaction + started. + +``previous_ids`` + Type: + List of strings + Description: + List of transactions that were sent immediately prior to this transaction. + +``pdus`` + Type: + List of Objects. + Description: + List of updates contained in this transaction. :: @@ -1526,8 +1576,98 @@ All PDUs have: - A list of other PDU IDs that have been seen recently on that context (regardless of which origin sent them) -[[TODO(paul): Update this structure so that 'pdu_id' is a two-element -[origin,ref] pair like the prev_pdus are]] +``context`` + Type: + String + Description: + Event context identifier + +``origin`` + Type: + String + Description: + DNS name of homeserver that created this PDU. + +``pdu_id`` + Type: + String + Description: + Unique identifier for PDU within the context for the originating homeserver + +``ts`` + Type: + Integer + Description: + Timestamp in milliseconds on originating homeserver when this PDU was created. + +``pdu_type`` + Type: + String + Description: + PDU event type. + +``prev_pdus`` + Type: + List of pairs of strings + Description: + The originating homeserver and PDU ids of the most recent PDUs the + homeserver was aware of for this context when it made this PDU. + +``depth`` + Type: + Integer + Description: + The maximum depth of the previous PDUs plus one. + + +.. TODO paul + [[TODO(paul): Update this structure so that 'pdu_id' is a two-element + [origin,ref] pair like the prev_pdus are]] + + +For state updates: + +``is_state`` + Type: + Boolean + Description: + True if this PDU is updating state. + +``state_key`` + Type: + String + Description: + Optional key identifying the updated state within the context. + +``power_level`` + Type: + Integer + Description: + The asserted power level of the user performing the update. + +``min_update`` + Type: + Integer + Description: + The required power level needed to replace this update. + +``prev_state_id`` + Type: + String + Description: + PDU event type. + +``prev_state_origin`` + Type: + String + Description: + The PDU id of the update this replaces. + +``user`` + Type: + String + Description: + The user updating the state. :: @@ -1568,12 +1708,13 @@ keys exist to support this: "prev_state_id":TODO "prev_state_origin":TODO} -[[TODO(paul): At this point we should probably have a long description of how -State management works, with descriptions of clobbering rules, power levels, etc -etc... But some of that detail is rather up-in-the-air, on the whiteboard, and -so on. This part needs refining. And writing in its own document as the details -relate to the server/system as a whole, not specifically to server-server -federation.]] +.. TODO paul + [[TODO(paul): At this point we should probably have a long description of how + State management works, with descriptions of clobbering rules, power levels, etc + etc... But some of that detail is rather up-in-the-air, on the whiteboard, and + so on. This part needs refining. And writing in its own document as the details + relate to the server/system as a whole, not specifically to server-server + federation.]] EDUs, by comparison to PDUs, do not have an ID, a context, or a list of "previous" IDs. The only mandatory fields for these are the type, origin and @@ -1585,6 +1726,79 @@ destination home server names, and the actual nested content. "origin":"blue", "destination":"orange", "content":...} + + +Protocol URLs +============= +.. WARNING:: + This section may be misleading or inaccurate. + +All these URLs are namespaced within a prefix of:: + + /_matrix/federation/v1/... + +For active pushing of messages representing live activity "as it happens":: + + PUT .../send/:transaction_id/ + Body: JSON encoding of a single Transaction + Response: TODO + +The transaction_id path argument will override any ID given in the JSON body. +The destination name will be set to that of the receiving server itself. Each +embedded PDU in the transaction body will be processed. + + +To fetch a particular PDU:: + + GET .../pdu/:origin/:pdu_id/ + Response: JSON encoding of a single Transaction containing one PDU + +Retrieves a given PDU from the server. The response will contain a single new +Transaction, inside which will be the requested PDU. + + +To fetch all the state of a given context:: + + GET .../state/:context/ + Response: JSON encoding of a single Transaction containing multiple PDUs + +Retrieves a snapshot of the entire current state of the given context. The +response will contain a single Transaction, inside which will be a list of +PDUs that encode the state. + +To backfill events on a given context:: + + GET .../backfill/:context/ + Query args: v, limit + Response: JSON encoding of a single Transaction containing multiple PDUs + +Retrieves a sliding-window history of previous PDUs that occurred on the +given context. Starting from the PDU ID(s) given in the "v" argument, the +PDUs that preceeded it are retrieved, up to a total number given by the +"limit" argument. These are then returned in a new Transaction containing all +off the PDUs. + + +To stream events all the events:: + + GET .../pull/ + Query args: origin, v + Response: JSON encoding of a single Transaction consisting of multiple PDUs + +Retrieves all of the transactions later than any version given by the "v" +arguments. + + +To make a query:: + + GET .../query/:query_type + Query args: as specified by the individual query types + Response: JSON encoding of a response object + +Performs a single query request on the receiving home server. The Query Type +part of the path specifies the kind of query being made, and its query +arguments have a meaning specific to that kind of query. The response is a +JSON-encoded object whose meaning also depends on the kind of query. Backfilling ----------- @@ -1604,9 +1818,133 @@ SRV Records Security ======== + .. NOTE:: This section is a work in progress. +Threat Model +------------ + +Denial of Service +~~~~~~~~~~~~~~~~~ + +The attacker could attempt to prevent delivery of messages to or from the +victim in order to: + +* Disrupt service or marketing campaign of a commercial competitor. +* Censor a discussion or censor a participant in a discussion. +* Perform general vandalism. + +Threat: Resource Exhaustion ++++++++++++++++++++++++++++ + +An attacker could cause the victims server to exhaust a particular resource +(e.g. open TCP connections, CPU, memory, disk storage) + +Threat: Unrecoverable Consistency Violations +++++++++++++++++++++++++++++++++++++++++++++ + +An attacker could send messages which created an unrecoverable "split-brain" +state in the cluster such that the victim's servers could no longer dervive a +consistent view of the chatroom state. + +Threat: Bad History ++++++++++++++++++++ + +An attacker could convince the victim to accept invalid messages which the +victim would then include in their view of the chatroom history. Other servers +in the chatroom would reject the invalid messages and potentially reject the +victims messages as well since they depended on the invalid messages. + +Threat: Block Network Traffic ++++++++++++++++++++++++++++++ + +An attacker could try to firewall traffic between the victim's server and some +or all of the other servers in the chatroom. + +Threat: High Volume of Messages ++++++++++++++++++++++++++++++++ + +An attacker could send large volumes of messages to a chatroom with the victim +making the chatroom unusable. + +Threat: Banning users without necessary authorisation ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +An attacker could attempt to ban a user from a chatroom with the necessary +authorisation. + +Spoofing +~~~~~~~~ + +An attacker could try to send a message claiming to be from the victim without +the victim having sent the message in order to: + +* Impersonate the victim while performing illict activity. +* Obtain privileges of the victim. + +Threat: Altering Message Contents ++++++++++++++++++++++++++++++++++ + +An attacker could try to alter the contents of an existing message from the +victim. + +Threat: Fake Message "origin" Field ++++++++++++++++++++++++++++++++++++ + +An attacker could try to send a new message purporting to be from the victim +with a phony "origin" field. + +Spamming +~~~~~~~~ + +The attacker could try to send a high volume of solicicted or unsolicted +messages to the victim in order to: + +* Find victims for scams. +* Market unwanted products. + +Threat: Unsoliticted Messages ++++++++++++++++++++++++++++++ + +An attacker could try to send messages to victims who do not wish to receive +them. + +Threat: Abusive Messages +++++++++++++++++++++++++ + +An attacker could send abusive or threatening messages to the victim + +Spying +~~~~~~ + +The attacker could try to access message contents or metadata for messages sent +by the victim or to the victim that were not intended to reach the attacker in +order to: + +* Gain sensitive personal or commercial information. +* Impersonate the victim using credentials contained in the messages. + (e.g. password reset messages) +* Discover who the victim was talking to and when. + +Threat: Disclosure during Transmission +++++++++++++++++++++++++++++++++++++++ + +An attacker could try to expose the message contents or metadata during +transmission between the servers. + +Threat: Disclosure to Servers Outside Chatroom +++++++++++++++++++++++++++++++++++++++++++++++ + +An attacker could try to convince servers within a chatroom to send messages to +a server it controls that was not authorised to be within the chatroom. + +Threat: Disclosure to Servers Within Chatroom +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An attacker could take control of a server within a chatroom to expose message +contents or metadata for messages in that room. + Rate limiting ------------- Home servers SHOULD implement rate limiting to reduce the risk of being overloaded. If a @@ -1660,57 +1998,121 @@ Glossary .. NOTE:: This section is a work in progress. -.. TODO - - domain specific words/acronyms with definitions +Backfilling: + The process of synchronising historic state from one home server to another, + to backfill the event storage so that scrollback can be presented to the + client(s). Not to be confused with pagination. + +Context: + A single human-level entity of interest (currently, a chat room) + +EDU (Ephemeral Data Unit): + A message that relates directly to a given pair of home servers that are + exchanging it. EDUs are short-lived messages that related only to one single + pair of servers; they are not persisted for a long time and are not forwarded + on to other servers. Because of this, they have no internal ID nor previous + EDUs reference chain. + +Event: + A record of activity that records a single thing that happened on to a context + (currently, a chat room). These are the "chat messages" that Synapse makes + available. + +PDU (Persistent Data Unit): + A message that relates to a single context, irrespective of the server that + is communicating it. PDUs either encode a single Event, or a single State + change. A PDU is referred to by its PDU ID; the pair of its origin server + and local reference from that server. + +PDU ID: + The pair of PDU Origin and PDU Reference, that together globally uniquely + refers to a specific PDU. + +PDU Origin: + The name of the origin server that generated a given PDU. This may not be the + server from which it has been received, due to the way they are copied around + from server to server. The origin always records the original server that + created it. + +PDU Reference: + A local ID used to refer to a specific PDU from a given origin server. These + references are opaque at the protocol level, but may optionally have some + structured meaning within a given origin server or implementation. + +Presence: + The concept of whether a user is currently online, how available they declare + they are, and so on. See also: doc/model/presence + +Profile: + A set of metadata about a user, such as a display name, provided for the + benefit of other users. See also: doc/model/profiles + +Room ID: + An opaque string (of as-yet undecided format) that identifies a particular + room and used in PDUs referring to it. + +Room Alias: + A human-readable string of the form #name:some.domain that users can use as a + pointer to identify a room; a Directory Server will map this to its Room ID + +State: + A set of metadata maintained about a Context, which is replicated among the + servers in addition to the history of Events. User ID: - An opaque ID which identifies an end-user, which consists of some opaque - localpart combined with the domain name of their home server. + A string of the form @localpart:domain.name that identifies a user for + wire-protocol purposes. The localpart is meaningless outside of a particular + home server. This takes a human-readable form that end-users can use directly + if they so wish, avoiding the 3PIDs. + +Transaction: + A message which relates to the communication between a given pair of servers. + A transaction contains possibly-empty lists of PDUs and EDUs. .. Links through the external API docs are below .. ============================================= .. |createRoom| replace:: ``/createRoom`` -.. _createRoom: /-rooms/create_room +.. _createRoom: /docs/api/client-server/#!/-rooms/create_room .. |initialSync| replace:: ``/initialSync`` -.. _initialSync: /-events/initial_sync +.. _initialSync: /docs/api/client-server/#!/-events/initial_sync .. |/rooms/<room_id>/initialSync| replace:: ``/rooms/<room_id>/initialSync`` -.. _/rooms/<room_id>/initialSync: /-rooms/get_room_sync_data +.. _/rooms/<room_id>/initialSync: /docs/api/client-server/#!/-rooms/get_room_sync_data .. |login| replace:: ``/login`` -.. _login: /-login +.. _login: /docs/api/client-server/#!/-login .. |/rooms/<room_id>/messages| replace:: ``/rooms/<room_id>/messages`` -.. _/rooms/<room_id>/messages: /-rooms/get_messages +.. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages .. |/rooms/<room_id>/members| replace:: ``/rooms/<room_id>/members`` -.. _/rooms/<room_id>/members: /-rooms/get_members +.. _/rooms/<room_id>/members: /docs/api/client-server/#!/-rooms/get_members .. |/rooms/<room_id>/state| replace:: ``/rooms/<room_id>/state`` -.. _/rooms/<room_id>/state: /-rooms/get_state_events +.. _/rooms/<room_id>/state: /docs/api/client-server/#!/-rooms/get_state_events .. |/rooms/<room_id>/send/<event_type>| replace:: ``/rooms/<room_id>/send/<event_type>`` -.. _/rooms/<room_id>/send/<event_type>: /-rooms/send_non_state_event +.. _/rooms/<room_id>/send/<event_type>: /docs/api/client-server/#!/-rooms/send_non_state_event .. |/rooms/<room_id>/state/<event_type>/<state_key>| replace:: ``/rooms/<room_id>/state/<event_type>/<state_key>`` -.. _/rooms/<room_id>/state/<event_type>/<state_key>: /-rooms/send_state_event +.. _/rooms/<room_id>/state/<event_type>/<state_key>: /docs/api/client-server/#!/-rooms/send_state_event .. |/rooms/<room_id>/invite| replace:: ``/rooms/<room_id>/invite`` -.. _/rooms/<room_id>/invite: /-rooms/invite +.. _/rooms/<room_id>/invite: /docs/api/client-server/#!/-rooms/invite .. |/rooms/<room_id>/join| replace:: ``/rooms/<room_id>/join`` -.. _/rooms/<room_id>/join: /-rooms/join_room +.. _/rooms/<room_id>/join: /docs/api/client-server/#!/-rooms/join_room .. |/rooms/<room_id>/leave| replace:: ``/rooms/<room_id>/leave`` -.. _/rooms/<room_id>/leave: /-rooms/leave +.. _/rooms/<room_id>/leave: /docs/api/client-server/#!/-rooms/leave .. |/rooms/<room_id>/ban| replace:: ``/rooms/<room_id>/ban`` -.. _/rooms/<room_id>/ban: /-rooms/ban +.. _/rooms/<room_id>/ban: /docs/api/client-server/#!/-rooms/ban .. |/join/<room_alias_or_id>| replace:: ``/join/<room_alias_or_id>`` -.. _/join/<room_alias_or_id>: /-rooms/join +.. _/join/<room_alias_or_id>: /docs/api/client-server/#!/-rooms/join -.. _`Event Stream`: /-events/get_event_stream +.. _`Event Stream`: /docs/api/client-server/#!/-events/get_event_stream diff --git a/experiments/cursesio.py b/experiments/cursesio.py index 31fbda5504..95d87a1fda 100644 --- a/experiments/cursesio.py +++ b/experiments/cursesio.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/experiments/test_messaging.py b/experiments/test_messaging.py index 3ff7ab820f..fedf786cec 100644 --- a/experiments/test_messaging.py +++ b/experiments/test_messaging.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/graph/graph.py b/graph/graph.py index ac06d979e1..b2acadcf5e 100644 --- a/graph/graph.py +++ b/graph/graph.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/copyrighter.pl b/scripts/copyrighter.pl index e476c9cc85..7c03ef21fc 100755 --- a/scripts/copyrighter.pl +++ b/scripts/copyrighter.pl @@ -1,5 +1,5 @@ #!/usr/bin/perl -pi -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # limitations under the License. $copyright = <<EOT; -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/gendoc.sh b/scripts/gendoc.sh index 30ba1db629..3c849e52e1 100755 --- a/scripts/gendoc.sh +++ b/scripts/gendoc.sh @@ -9,4 +9,6 @@ perl -pi -e 's#<head>#<head><link rel="stylesheet" href="/site.css">#' $MATRIXDO perl -pi -e 's#<body>#<body><div id="header"><div id="headerContent"> </div></div><div id="page"><div id="wrapper"><div style="text-align: center; padding: 40px;"><img src="/matrix.png" width="305" height="130" alt="[matrix]"/></div>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html -perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">© 2014 Matrix.org</div></div></body>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html \ No newline at end of file +perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">© 2014 Matrix.org</div></div></body>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html + +scp -r $MATRIXDOTORG/docs matrix@ldc-prd-matrix-001:/sites/matrix-beta \ No newline at end of file diff --git a/setup.py b/setup.py index 0eec5c9354..a9470b4c9e 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/__init__.py b/synapse/__init__.py index b45cf47b56..564d789645 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/__init__.py b/synapse/api/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/synapse/api/__init__.py +++ b/synapse/api/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 54ecbe5b3a..b4eda3df01 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 668ffa07ca..fcef062fc9 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index ad79bc7ff9..84afe4fa37 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index 9502f5df8f..f95468fc65 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index 159728b2d2..a3b293e024 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py index f6d3c59a9a..33f0f0cb99 100644 --- a/synapse/api/events/room.py +++ b/synapse/api/events/room.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -103,8 +103,7 @@ class FeedbackEvent(SynapseEvent): def get_content_template(self): return { "type": u"string", - "target_event_id": u"string", - "msg_sender_id": u"string" + "target_event_id": u"string" } diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py index b9b6a9de1a..b25358090f 100644 --- a/synapse/api/ratelimiting.py +++ b/synapse/api/ratelimiting.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/urls.py b/synapse/api/urls.py index 3d0b5de965..6314f31f7a 100644 --- a/synapse/api/urls.py +++ b/synapse/api/urls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/app/__init__.py b/synapse/app/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/synapse/app/__init__.py +++ b/synapse/app/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8a7cd07fec..49cf928cc1 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,8 @@ from twisted.enterprise import adbapi from twisted.web.resource import Resource from twisted.web.static import File from twisted.web.server import Site -from synapse.http.server import JsonResource, RootRedirect, ContentRepoResource +from synapse.http.server import JsonResource, RootRedirect +from synapse.http.content_repository import ContentRepoResource from synapse.http.client import TwistedHttpClient from synapse.api.urls import ( CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX @@ -74,7 +75,9 @@ class SynapseHomeServer(HomeServer): return File("webclient") # TODO configurable? def build_resource_for_content_repo(self): - return ContentRepoResource(self, self.upload_dir, self.auth) + return ContentRepoResource( + self, self.upload_dir, self.auth, self.content_addr + ) def build_db_pool(self): """ Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we @@ -90,20 +93,28 @@ class SynapseHomeServer(HomeServer): if row and row[0]: user_version = row[0] - if user_version < SCHEMA_VERSION: - # TODO(paul): add some kind of intelligent fixup here - raise ValueError("Cannot use this database as the " + - "schema version (%d) does not match (%d)" % - (user_version, SCHEMA_VERSION) + if user_version > SCHEMA_VERSION: + raise ValueError("Cannot use this database as it is too " + + "new for the server to understand" + ) + elif user_version < SCHEMA_VERSION: + logging.info("Upgrading database from version %d", + user_version ) + # Run every version since after the current version. + for v in range(user_version + 1, SCHEMA_VERSION + 1): + sql_script = read_schema("delta/v%d" % (v)) + c.executescript(sql_script) + + db_conn.commit() + else: for sql_loc in SCHEMAS: sql_script = read_schema(sql_loc) c.executescript(sql_script) - db_conn.commit() - + db_conn.commit() c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION) c.close() @@ -248,6 +259,7 @@ def setup(): db_name=config.database_path, tls_context_factory=tls_context_factory, config=config, + content_addr=config.content_addr, ) hs.register_servlets() diff --git a/synapse/config/__init__.py b/synapse/config/__init__.py index fe8a073cd3..f9811bfa04 100644 --- a/synapse/config/__init__.py +++ b/synapse/config/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 4b445806da..7dc68230bd 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/config/database.py b/synapse/config/database.py index edf2361914..460445f15d 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index a9aa4c735c..76e2cdeddd 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ from .server import ServerConfig from .logger import LoggingConfig from .database import DatabaseConfig from .ratelimiting import RatelimitConfig +from .repository import ContentRepositoryConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, - RatelimitConfig): + RatelimitConfig, ContentRepositoryConfig): pass if __name__=='__main__': diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 8db6621ae8..56cd095433 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index ee572e1fbf..f126782b8d 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/config/repository.py b/synapse/config/repository.py new file mode 100644 index 0000000000..407c8d6c24 --- /dev/null +++ b/synapse/config/repository.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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 ._base import Config +import os + +class ContentRepositoryConfig(Config): + def __init__(self, args): + super(ContentRepositoryConfig, self).__init__(args) + self.max_upload_size = self.parse_size(args.max_upload_size) + + def parse_size(self, string): + sizes = {"K": 1024, "M": 1024 * 1024} + size = 1 + suffix = string[-1] + if suffix in sizes: + string = string[:-1] + size = sizes[suffix] + return int(string) * size + + @classmethod + def add_arguments(cls, parser): + super(ContentRepositoryConfig, cls).add_arguments(parser) + db_group = parser.add_argument_group("content_repository") + db_group.add_argument( + "--max-upload-size", default="1M" + ) diff --git a/synapse/config/server.py b/synapse/config/server.py index 36143e3c9c..516e4cf882 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,6 +32,14 @@ class ServerConfig(Config): self.webclient = True self.manhole = args.manhole + if not args.content_addr: + host = args.server_name + if ':' not in host: + host = "%s:%d" % (host, args.bind_port) + args.content_addr = "https://%s" % (host,) + + self.content_addr = args.content_addr + @classmethod def add_arguments(cls, parser): super(ServerConfig, cls).add_arguments(parser) @@ -50,13 +58,16 @@ class ServerConfig(Config): help="Local interface to listen on") server_group.add_argument("-D", "--daemonize", action='store_true', help="Daemonize the home server") - server_group.add_argument('--pid-file', default="hs.pid", + server_group.add_argument('--pid-file', default="homeserver.pid", help="When running as a daemon, the file to" " store the pid in") server_group.add_argument("--manhole", metavar="PORT", dest="manhole", type=int, help="Turn on the twisted telnet manhole" " service on the given port.") + server_group.add_argument("--content-addr", default=None, + help="The host and scheme to use for the " + "content repository") def read_signing_key(self, signing_key_path): signing_key_base64 = self.read_file(signing_key_path, "signing_key") @@ -77,3 +88,4 @@ class ServerConfig(Config): with open(args.signing_key_path, "w") as signing_key_file: key = nacl.signing.SigningKey.generate() signing_key_file.write(encode_base64(key.encode())) + diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 16f6f3aba6..72d5518a89 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/crypto/__init__.py b/synapse/crypto/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/synapse/crypto/__init__.py +++ b/synapse/crypto/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py index 344f2dd218..f86bd19255 100644 --- a/synapse/crypto/context_factory.py +++ b/synapse/crypto/context_factory.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/crypto/keyclient.py b/synapse/crypto/keyclient.py index e615866b68..c11df5c529 100644 --- a/synapse/crypto/keyclient.py +++ b/synapse/crypto/keyclient.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/crypto/keyserver.py b/synapse/crypto/keyserver.py index 3d80a0e660..a23484dbae 100644 --- a/synapse/crypto/keyserver.py +++ b/synapse/crypto/keyserver.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/crypto/resource/__init__.py b/synapse/crypto/resource/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/synapse/crypto/resource/__init__.py +++ b/synapse/crypto/resource/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/crypto/resource/key.py b/synapse/crypto/resource/key.py index 6aecd2b95f..48d14b9f4a 100644 --- a/synapse/crypto/resource/key.py +++ b/synapse/crypto/resource/key.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/federation/__init__.py b/synapse/federation/__init__.py index b15e7cf941..1351b68fd6 100644 --- a/synapse/federation/__init__.py +++ b/synapse/federation/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/federation/pdu_codec.py b/synapse/federation/pdu_codec.py index adc166c564..cef61108dd 100644 --- a/synapse/federation/pdu_codec.py +++ b/synapse/federation/pdu_codec.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/federation/persistence.py b/synapse/federation/persistence.py index 4cf72b2e42..de36a80e41 100644 --- a/synapse/federation/persistence.py +++ b/synapse/federation/persistence.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py index cadf574b3b..e12510017f 100644 --- a/synapse/federation/replication.py +++ b/synapse/federation/replication.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -628,7 +628,6 @@ class _TransactionQueue(object): for deferred in deferreds: deferred.errback(e) - yield deferred finally: # We want to be *very* sure we delete this after we stop processing diff --git a/synapse/federation/transport.py b/synapse/federation/transport.py index 50c3df4a5d..6e62ae7c74 100644 --- a/synapse/federation/transport.py +++ b/synapse/federation/transport.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/federation/units.py b/synapse/federation/units.py index b468f70546..9740431279 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index b2208b26c3..5308e2c8e1 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 57429ad2ef..9989fe8670 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 7c89150d99..1b9e831fc0 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ from twisted.internet import defer from ._base import BaseHandler from synapse.api.errors import SynapseError +from synapse.http.client import HttpClient import logging @@ -36,7 +37,7 @@ class DirectoryHandler(BaseHandler): ) @defer.inlineCallbacks - def create_association(self, room_alias, room_id, servers): + def create_association(self, room_alias, room_id, servers=None): # TODO(erikj): Do auth. if not room_alias.is_mine: @@ -47,6 +48,12 @@ class DirectoryHandler(BaseHandler): # TODO(erikj): Check if there is a current association. + if not servers: + servers = yield self.store.get_joined_hosts_for_room(room_id) + + if not servers: + raise SynapseError(400, "Failed to get server list") + yield self.store.create_room_alias_association( room_alias, room_id, @@ -68,7 +75,10 @@ class DirectoryHandler(BaseHandler): result = yield self.federation.make_query( destination=room_alias.domain, query_type="directory", - args={"room_alias": room_alias.to_string()}, + args={ + "room_alias": room_alias.to_string(), + HttpClient.RETRY_DNS_LOOKUP_FAILURES: False + } ) if result and "room_id" in result and "servers" in result: @@ -79,6 +89,9 @@ class DirectoryHandler(BaseHandler): defer.returnValue({}) return + extra_servers = yield self.store.get_joined_hosts_for_room(room_id) + servers = list(set(extra_servers) | set(servers)) + defer.returnValue({ "room_id": room_id, "servers": servers, diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index 980a169b25..fd24a11fb8 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -126,5 +126,7 @@ class EventHandler(BaseHandler): defer.returnValue(None) return - yield self.auth.check(event, raises=True) + if hasattr(event, "room_id"): + yield self.auth.check_joined_room(event.room_id, user.to_string()) + defer.returnValue(event) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index eac110419c..84059d8033 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 0220fa0604..6ee7ce5a2d 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index c9e3c4e451..dad2bbd1a4 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -142,7 +142,12 @@ class MessageHandler(BaseRoomHandler): SynapseError if something went wrong. """ - snapshot = yield self.store.snapshot_room(event.room_id, event.user_id) + snapshot = yield self.store.snapshot_room( + event.room_id, + event.user_id, + state_type=event.type, + state_key=event.state_key, + ) yield self.auth.check(event, snapshot, raises=True) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 9bfceda88a..c79bb6ff76 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -155,19 +155,18 @@ class PresenceHandler(BaseHandler): if observer_user == observed_user: defer.returnValue(True) - allowed_by_subscription = yield self.store.is_presence_visible( + if (yield self.store.user_rooms_intersect( + [u.to_string() for u in observer_user, observed_user] + )): + defer.returnValue(True) + + if (yield self.store.is_presence_visible( observed_localpart=observed_user.localpart, observer_userid=observer_user.to_string(), - ) - - if allowed_by_subscription: + )): defer.returnValue(True) - share_room = yield self.store.do_users_share_a_room( - [observer_user, observed_user] - ) - - defer.returnValue(share_room) + defer.returnValue(False) @defer.inlineCallbacks def get_state(self, target_user, auth_user): @@ -181,7 +180,7 @@ class PresenceHandler(BaseHandler): state = yield self.store.get_presence_state(target_user.localpart) if "mtime" in state: del state["mtime"] - state["presence"] = state["state"] + state["presence"] = state.pop("state") if target_user in self._user_cachemap: state["last_active"] = ( @@ -208,21 +207,17 @@ class PresenceHandler(BaseHandler): raise SynapseError(400, "User is not hosted on this Home Server") if target_user != auth_user: - raise AuthError(400, "Cannot set another user's displayname") + raise AuthError(400, "Cannot set another user's presence") if "status_msg" not in state: state["status_msg"] = None for k in state.keys(): - if k not in ("presence", "state", "status_msg"): + if k not in ("presence", "status_msg"): raise SynapseError( 400, "Unexpected presence state key '%s'" % (k,) ) - # Handle legacy "state" key for now - if "state" in state: - state["presence"] = state.pop("state") - if state["presence"] not in self.STATE_LEVELS: raise SynapseError(400, "'%s' is not a valid presence state" % state["presence"] @@ -601,7 +596,7 @@ class PresenceHandler(BaseHandler): if state is None: state = yield self.store.get_presence_state(user.localpart) del state["mtime"] - state["presence"] = state["state"] + state["presence"] = state.pop("state") if user in self._user_cachemap: state["last_active"] = ( @@ -622,8 +617,6 @@ class PresenceHandler(BaseHandler): "user_id": user.to_string(), } user_state.update(**state) - if "state" in user_state and "presence" not in user_state: - user_state["presence"] = user_state["state"] yield self.federation.send_edu( destination=destination, @@ -655,21 +648,12 @@ class PresenceHandler(BaseHandler): state = dict(push) del state["user_id"] - if "presence" in state: - # all is OK - pass - elif "state" in state: - # Legacy handling - state["presence"] = state["state"] - else: + if "presence" not in state: logger.warning("Received a presence 'push' EDU from %s without" - + " either a 'presence' or 'state' key", origin + + " a 'presence' key", origin ) continue - if "state" in state: - del state["state"] - if "last_active_ago" in state: state["last_active"] = int( self.clock.time_msec() - state.pop("last_active_ago") @@ -773,15 +757,52 @@ class PresenceEventSource(object): self.hs = hs self.clock = hs.get_clock() + @defer.inlineCallbacks + def is_visible(self, observer_user, observed_user): + if observer_user == observed_user: + defer.returnValue(True) + + presence = self.hs.get_handlers().presence_handler + + if (yield presence.store.user_rooms_intersect( + [u.to_string() for u in observer_user, observed_user] + )): + defer.returnValue(True) + + if observed_user.is_mine: + pushmap = presence._local_pushmap + + defer.returnValue( + observed_user.localpart in pushmap and + observer_user in pushmap[observed_user.localpart] + ) + else: + recvmap = presence._remote_recvmap + + defer.returnValue( + observed_user in recvmap and + observer_user in recvmap[observed_user] + ) + + @defer.inlineCallbacks def get_new_events_for_user(self, user, from_key, limit): from_key = int(from_key) + observer_user = user + presence = self.hs.get_handlers().presence_handler cachemap = presence._user_cachemap - # TODO(paul): limit, and filter by visibility - updates = [(k, cachemap[k]) for k in cachemap - if from_key < cachemap[k].serial] + updates = [] + # TODO(paul): use a DeferredList ? How to limit concurrency. + for observed_user in cachemap.keys(): + if not (from_key < cachemap[observed_user].serial): + continue + + if (yield self.is_visible(observer_user, observed_user)): + updates.append((observed_user, cachemap[observed_user])) + + # TODO(paul): limit if updates: clock = self.clock @@ -789,20 +810,23 @@ class PresenceEventSource(object): latest_serial = max([x[1].serial for x in updates]) data = [x[1].make_event(user=x[0], clock=clock) for x in updates] - return ((data, latest_serial)) + defer.returnValue((data, latest_serial)) else: - return (([], presence._user_cachemap_latest_serial)) + defer.returnValue(([], presence._user_cachemap_latest_serial)) def get_current_key(self): presence = self.hs.get_handlers().presence_handler return presence._user_cachemap_latest_serial + @defer.inlineCallbacks def get_pagination_rows(self, user, pagination_config, key): # TODO (erikj): Does this make sense? Ordering? from_token = pagination_config.from_token to_token = pagination_config.to_token + observer_user = user + from_key = int(from_token.presence_key) if to_token: @@ -813,7 +837,17 @@ class PresenceEventSource(object): presence = self.hs.get_handlers().presence_handler cachemap = presence._user_cachemap - # TODO(paul): limit, and filter by visibility + updates = [] + # TODO(paul): use a DeferredList ? How to limit concurrency. + for observed_user in cachemap.keys(): + if not (to_key < cachemap[observed_user].serial < from_key): + continue + + if (yield self.is_visible(observer_user, observed_user)): + updates.append((observed_user, cachemap[observed_user])) + + # TODO(paul): limit + updates = [(k, cachemap[k]) for k in cachemap if to_key < cachemap[k].serial < from_key] @@ -831,13 +865,13 @@ class PresenceEventSource(object): next_token = next_token.copy_and_replace( "presence_key", earliest_serial ) - return ((data, next_token)) + defer.returnValue((data, next_token)) else: if not to_token: to_token = from_token.copy_and_replace( "presence_key", 0 ) - return (([], to_token)) + defer.returnValue(([], to_token)) class UserPresenceCache(object): @@ -851,7 +885,6 @@ class UserPresenceCache(object): def update(self, state, serial): assert("mtime_age" not in state) - assert("state" not in state) self.state.update(state) # Delete keys that are now 'None' @@ -869,11 +902,6 @@ class UserPresenceCache(object): def get_state(self): # clone it so caller can't break our cache state = dict(self.state) - - # Legacy handling - if "presence" in state: - state["state"] = state["presence"] - return state def make_event(self, user, clock): diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 6799132054..023d8c0cf2 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index d56d2663d7..bee052274f 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 58afced8f5..64956c320c 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 3268427ecd..0ca4e5c31e 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/http/__init__.py b/synapse/http/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/synapse/http/__init__.py +++ b/synapse/http/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/http/client.py b/synapse/http/client.py index 5a5e40a26e..ebf1aa47c4 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ from twisted.internet import defer, reactor +from twisted.internet.error import DNSLookupError from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer from twisted.web.http_headers import Headers @@ -23,7 +24,7 @@ from synapse.util.async import sleep from syutil.jsonutil import encode_canonical_json -from synapse.api.errors import CodeMessageException +from synapse.api.errors import CodeMessageException, SynapseError from StringIO import StringIO @@ -45,6 +46,7 @@ _destination_mappings = { class HttpClient(object): """ Interface for talking json over http """ + RETRY_DNS_LOOKUP_FAILURES = "__retry_dns" def put_json(self, destination, path, data): """ Sends the specifed json data using PUT @@ -144,13 +146,23 @@ class TwistedHttpClient(HttpClient): destination = _destination_mappings[destination] logger.debug("get_json args: %s", args) + + retry_on_dns_fail = True + if HttpClient.RETRY_DNS_LOOKUP_FAILURES in args: + # FIXME: This isn't ideal, but the interface exposed in get_json + # isn't comprehensive enough to give caller's any control over + # their connection mechanics. + retry_on_dns_fail = args.pop(HttpClient.RETRY_DNS_LOOKUP_FAILURES) + query_bytes = urllib.urlencode(args, True) + logger.debug("Query bytes: %s Retry DNS: %s", args, retry_on_dns_fail) response = yield self._create_request( destination.encode("ascii"), "GET", path.encode("ascii"), - query_bytes=query_bytes + query_bytes=query_bytes, + retry_on_dns_fail=retry_on_dns_fail ) body = yield readBody(response) @@ -179,7 +191,8 @@ class TwistedHttpClient(HttpClient): @defer.inlineCallbacks def _create_request(self, destination, method, path_bytes, param_bytes=b"", - query_bytes=b"", producer=None, headers_dict={}): + query_bytes=b"", producer=None, headers_dict={}, + retry_on_dns_fail=True): """ Creates and sends a request to the given url """ headers_dict[b"User-Agent"] = [b"Synapse"] @@ -218,6 +231,11 @@ class TwistedHttpClient(HttpClient): logger.debug("Got response to %s", method) break except Exception as e: + if not retry_on_dns_fail and isinstance(e, DNSLookupError): + logger.warn("DNS Lookup failed to %s with %s", destination, + e) + raise SynapseError(400, "Domain specified not found.") + logger.exception("Got error in _create_request") _print_ex(e) diff --git a/synapse/http/content_repository.py b/synapse/http/content_repository.py new file mode 100644 index 0000000000..7dd4a859f8 --- /dev/null +++ b/synapse/http/content_repository.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 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 .server import respond_with_json_bytes + +from synapse.util.stringutils import random_string +from synapse.api.errors import ( + cs_exception, SynapseError, CodeMessageException, Codes, cs_error +) + +from twisted.protocols.basic import FileSender +from twisted.web import server, resource +from twisted.internet import defer + +import base64 +import json +import logging +import os +import re + +logger = logging.getLogger(__name__) + + +class ContentRepoResource(resource.Resource): + """Provides file uploading and downloading. + + Uploads are POSTed to wherever this Resource is linked to. This resource + returns a "content token" which can be used to GET this content again. The + token is typically a path, but it may not be. Tokens can expire, be one-time + uses, etc. + + In this case, the token is a path to the file and contains 3 interesting + sections: + - User ID base64d (for namespacing content to each user) + - random 24 char string + - Content type base64d (so we can return it when clients GET it) + + """ + isLeaf = True + + def __init__(self, hs, directory, auth, external_addr): + resource.Resource.__init__(self) + self.hs = hs + self.directory = directory + self.auth = auth + self.external_addr = external_addr.rstrip('/') + self.max_upload_size = hs.config.max_upload_size + + if not os.path.isdir(self.directory): + os.mkdir(self.directory) + logger.info("ContentRepoResource : Created %s directory.", + self.directory) + + @defer.inlineCallbacks + def map_request_to_name(self, request): + # auth the user + auth_user = yield self.auth.get_user_by_req(request) + + # namespace all file uploads on the user + prefix = base64.urlsafe_b64encode( + auth_user.to_string() + ).replace('=', '') + + # use a random string for the main portion + main_part = random_string(24) + + # suffix with a file extension if we can make one. This is nice to + # provide a hint to clients on the file information. We will also reuse + # this info to spit back the content type to the client. + suffix = "" + if request.requestHeaders.hasHeader("Content-Type"): + content_type = request.requestHeaders.getRawHeaders( + "Content-Type")[0] + suffix = "." + base64.urlsafe_b64encode(content_type) + if (content_type.split("/")[0].lower() in + ["image", "video", "audio"]): + file_ext = content_type.split("/")[-1] + # be a little paranoid and only allow a-z + file_ext = re.sub("[^a-z]", "", file_ext) + suffix += "." + file_ext + + file_name = prefix + main_part + suffix + file_path = os.path.join(self.directory, file_name) + logger.info("User %s is uploading a file to path %s", + auth_user.to_string(), + file_path) + + # keep trying to make a non-clashing file, with a sensible max attempts + attempts = 0 + while os.path.exists(file_path): + main_part = random_string(24) + file_name = prefix + main_part + suffix + file_path = os.path.join(self.directory, file_name) + attempts += 1 + if attempts > 25: # really? Really? + raise SynapseError(500, "Unable to create file.") + + defer.returnValue(file_path) + + def render_GET(self, request): + # no auth here on purpose, to allow anyone to view, even across home + # servers. + + # TODO: A little crude here, we could do this better. + filename = request.path.split('/')[-1] + # be paranoid + filename = re.sub("[^0-9A-z.-_]", "", filename) + + file_path = self.directory + "/" + filename + + logger.debug("Searching for %s", file_path) + + if os.path.isfile(file_path): + # filename has the content type + base64_contentype = filename.split(".")[1] + content_type = base64.urlsafe_b64decode(base64_contentype) + logger.info("Sending file %s", file_path) + f = open(file_path, 'rb') + request.setHeader('Content-Type', content_type) + d = FileSender().beginFileTransfer(f, request) + + # after the file has been sent, clean up and finish the request + def cbFinished(ignored): + f.close() + request.finish() + d.addCallback(cbFinished) + else: + respond_with_json_bytes( + request, + 404, + json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)), + send_cors=True) + + return server.NOT_DONE_YET + + def render_POST(self, request): + self._async_render(request) + return server.NOT_DONE_YET + + def render_OPTIONS(self, request): + respond_with_json_bytes(request, 200, {}, send_cors=True) + return server.NOT_DONE_YET + + @defer.inlineCallbacks + def _async_render(self, request): + try: + # TODO: The checks here are a bit late. The content will have + # already been uploaded to a tmp file at this point + content_length = request.getHeader("Content-Length") + if content_length is None: + raise SynapseError( + msg="Request must specify a Content-Length", code=400 + ) + if int(content_length) > self.max_upload_size: + raise SynapseError( + msg="Upload request body is too large", + code=413, + ) + + fname = yield self.map_request_to_name(request) + + # TODO I have a suspcious feeling this is just going to block + with open(fname, "wb") as f: + f.write(request.content.read()) + + + # FIXME (erikj): These should use constants. + file_name = os.path.basename(fname) + # FIXME: we can't assume what the public mounted path of the repo is + # ...plus self-signed SSL won't work to remote clients anyway + # ...and we can't assume that it's SSL anyway, as we might want to + # server it via the non-SSL listener... + url = "%s/_matrix/content/%s" % ( + self.external_addr, file_name + ) + + respond_with_json_bytes(request, 200, + json.dumps({"content_token": url}), + send_cors=True) + + except CodeMessageException as e: + logger.exception(e) + respond_with_json_bytes(request, e.code, + json.dumps(cs_exception(e))) + except Exception as e: + logger.error("Failed to store file: %s" % e) + respond_with_json_bytes( + request, + 500, + json.dumps({"error": "Internal server error"}), + send_cors=True) + + + diff --git a/synapse/http/endpoint.py b/synapse/http/endpoint.py index 6c1fdcb853..7018ee3458 100644 --- a/synapse/http/endpoint.py +++ b/synapse/http/endpoint.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/http/server.py b/synapse/http/server.py index 74c220e869..8d419c02dd 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,22 +18,16 @@ from syutil.jsonutil import ( encode_canonical_json, encode_pretty_printed_json ) from synapse.api.errors import ( - cs_exception, SynapseError, CodeMessageException, Codes, cs_error + cs_exception, SynapseError, CodeMessageException ) -from synapse.util.stringutils import random_string from twisted.internet import defer, reactor -from twisted.protocols.basic import FileSender from twisted.web import server, resource from twisted.web.server import NOT_DONE_YET from twisted.web.util import redirectTo -import base64 import collections -import json import logging -import os -import re logger = logging.getLogger(__name__) @@ -198,161 +192,6 @@ class RootRedirect(resource.Resource): return resource.Resource.getChild(self, name, request) -class ContentRepoResource(resource.Resource): - """Provides file uploading and downloading. - - Uploads are POSTed to wherever this Resource is linked to. This resource - returns a "content token" which can be used to GET this content again. The - token is typically a path, but it may not be. Tokens can expire, be one-time - uses, etc. - - In this case, the token is a path to the file and contains 3 interesting - sections: - - User ID base64d (for namespacing content to each user) - - random 24 char string - - Content type base64d (so we can return it when clients GET it) - - """ - isLeaf = True - - def __init__(self, hs, directory, auth): - resource.Resource.__init__(self) - self.hs = hs - self.directory = directory - self.auth = auth - - if not os.path.isdir(self.directory): - os.mkdir(self.directory) - logger.info("ContentRepoResource : Created %s directory.", - self.directory) - - @defer.inlineCallbacks - def map_request_to_name(self, request): - # auth the user - auth_user = yield self.auth.get_user_by_req(request) - - # namespace all file uploads on the user - prefix = base64.urlsafe_b64encode( - auth_user.to_string() - ).replace('=', '') - - # use a random string for the main portion - main_part = random_string(24) - - # suffix with a file extension if we can make one. This is nice to - # provide a hint to clients on the file information. We will also reuse - # this info to spit back the content type to the client. - suffix = "" - if request.requestHeaders.hasHeader("Content-Type"): - content_type = request.requestHeaders.getRawHeaders( - "Content-Type")[0] - suffix = "." + base64.urlsafe_b64encode(content_type) - if (content_type.split("/")[0].lower() in - ["image", "video", "audio"]): - file_ext = content_type.split("/")[-1] - # be a little paranoid and only allow a-z - file_ext = re.sub("[^a-z]", "", file_ext) - suffix += "." + file_ext - - file_name = prefix + main_part + suffix - file_path = os.path.join(self.directory, file_name) - logger.info("User %s is uploading a file to path %s", - auth_user.to_string(), - file_path) - - # keep trying to make a non-clashing file, with a sensible max attempts - attempts = 0 - while os.path.exists(file_path): - main_part = random_string(24) - file_name = prefix + main_part + suffix - file_path = os.path.join(self.directory, file_name) - attempts += 1 - if attempts > 25: # really? Really? - raise SynapseError(500, "Unable to create file.") - - defer.returnValue(file_path) - - def render_GET(self, request): - # no auth here on purpose, to allow anyone to view, even across home - # servers. - - # TODO: A little crude here, we could do this better. - filename = request.path.split('/')[-1] - # be paranoid - filename = re.sub("[^0-9A-z.-_]", "", filename) - - file_path = self.directory + "/" + filename - - logger.debug("Searching for %s", file_path) - - if os.path.isfile(file_path): - # filename has the content type - base64_contentype = filename.split(".")[1] - content_type = base64.urlsafe_b64decode(base64_contentype) - logger.info("Sending file %s", file_path) - f = open(file_path, 'rb') - request.setHeader('Content-Type', content_type) - d = FileSender().beginFileTransfer(f, request) - - # after the file has been sent, clean up and finish the request - def cbFinished(ignored): - f.close() - request.finish() - d.addCallback(cbFinished) - else: - respond_with_json_bytes( - request, - 404, - json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)), - send_cors=True) - - return server.NOT_DONE_YET - - def render_POST(self, request): - self._async_render(request) - return server.NOT_DONE_YET - - def render_OPTIONS(self, request): - respond_with_json_bytes(request, 200, {}, send_cors=True) - return server.NOT_DONE_YET - - @defer.inlineCallbacks - def _async_render(self, request): - try: - fname = yield self.map_request_to_name(request) - - # TODO I have a suspcious feeling this is just going to block - with open(fname, "wb") as f: - f.write(request.content.read()) - - - # FIXME (erikj): These should use constants. - file_name = os.path.basename(fname) - # FIXME: we can't assume what the public mounted path of the repo is - # ...plus self-signed SSL won't work to remote clients anyway - # ...and we can't assume that it's SSL anyway, as we might want to - # server it via the non-SSL listener... - url = "https://%s/_matrix/content/%s" % ( - self.hs.domain_with_port, file_name - ) - - respond_with_json_bytes(request, 200, - json.dumps({"content_token": url}), - send_cors=True) - - except CodeMessageException as e: - logger.exception(e) - respond_with_json_bytes(request, e.code, - json.dumps(cs_exception(e))) - except Exception as e: - logger.error("Failed to store file: %s" % e) - respond_with_json_bytes( - request, - 500, - json.dumps({"error": "Internal server error"}), - send_cors=True) - - def respond_with_json_bytes(request, code, json_bytes, send_cors=False, response_code_message=None): """Sends encoded JSON in response to the given request. diff --git a/synapse/notifier.py b/synapse/notifier.py index 3260aa744f..5b02c71d1e 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -167,7 +167,12 @@ class Notifier(object): ) def eb(failure): - logger.exception("Failed to notify listener", failure) + logger.error("Failed to notify listener", + exc_info=( + failure.type, + failure.value, + failure.getTracebackObject()) + ) yield defer.DeferredList( [notify(l).addErrback(eb) for l in listeners] diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index f33024e72a..ed785cfbd5 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/base.py b/synapse/rest/base.py index e855d293e5..2e8e3fa7d4 100644 --- a/synapse/rest/base.py +++ b/synapse/rest/base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/directory.py b/synapse/rest/directory.py index dc347652a0..18df7c8d8b 100644 --- a/synapse/rest/directory.py +++ b/synapse/rest/directory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ from twisted.internet import defer +from synapse.api.errors import SynapseError, Codes from base import RestServlet, client_path_pattern import json @@ -44,8 +45,10 @@ class ClientDirectoryServer(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, room_alias): - # TODO(erikj): Exceptions - content = json.loads(request.content.read()) + content = _parse_json(request) + if not "room_id" in content: + raise SynapseError(400, "Missing room_id key", + errcode=Codes.BAD_JSON) logger.debug("Got content: %s", content) @@ -54,7 +57,7 @@ class ClientDirectoryServer(RestServlet): logger.debug("Got room name: %s", room_alias.to_string()) room_id = content["room_id"] - servers = content["servers"] + servers = content["servers"] if "servers" in content else None logger.debug("Got room_id: %s", room_id) logger.debug("Got servers: %s", servers) @@ -68,7 +71,20 @@ class ClientDirectoryServer(RestServlet): yield dir_handler.create_association( room_alias, room_id, servers ) + except SynapseError as e: + raise e except: logger.exception("Failed to create association") defer.returnValue((200, {})) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.", + errcode=Codes.NOT_JSON) + return content + except ValueError: + raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) diff --git a/synapse/rest/events.py b/synapse/rest/events.py index 2e7563d14b..7fde143200 100644 --- a/synapse/rest/events.py +++ b/synapse/rest/events.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/initial_sync.py b/synapse/rest/initial_sync.py index d18c4c0f60..a1cb442256 100644 --- a/synapse/rest/initial_sync.py +++ b/synapse/rest/initial_sync.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/login.py b/synapse/rest/login.py index 99e4f10aac..c7bf901c8e 100644 --- a/synapse/rest/login.py +++ b/synapse/rest/login.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py index bce3943542..7fc8ce4404 100644 --- a/synapse/rest/presence.py +++ b/synapse/rest/presence.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,11 +17,12 @@ """ from twisted.internet import defer +from synapse.api.errors import SynapseError from base import RestServlet, client_path_pattern import json import logging - +import urllib logger = logging.getLogger(__name__) @@ -32,6 +33,7 @@ class PresenceStatusRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) state = yield self.handlers.presence_handler.get_state( @@ -42,25 +44,26 @@ class PresenceStatusRestServlet(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) state = {} try: content = json.loads(request.content.read()) - # Legacy handling - if "state" in content: - state["presence"] = content.pop("state") - else: - state["presence"] = content.pop("presence") + state["presence"] = content.pop("presence") if "status_msg" in content: state["status_msg"] = content.pop("status_msg") + if not isinstance(state["status_msg"], basestring): + raise SynapseError(400, "status_msg must be a string.") if content: raise KeyError() + except SynapseError as e: + raise e except: - defer.returnValue((400, "Unable to parse state")) + raise SynapseError(400, "Unable to parse state") yield self.handlers.presence_handler.set_state( target_user=user, auth_user=auth_user, state=state) @@ -77,13 +80,14 @@ class PresenceListRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) if not user.is_mine: - defer.returnValue((400, "User not hosted on this Home Server")) + raise SynapseError(400, "User not hosted on this Home Server") if auth_user != user: - defer.returnValue((400, "Cannot get another user's presence list")) + raise SynapseError(400, "Cannot get another user's presence list") presence = yield self.handlers.presence_handler.get_presence_list( observer_user=user, accepted=True) @@ -97,31 +101,40 @@ class PresenceListRestServlet(RestServlet): @defer.inlineCallbacks def on_POST(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) if not user.is_mine: - defer.returnValue((400, "User not hosted on this Home Server")) + raise SynapseError(400, "User not hosted on this Home Server") if auth_user != user: - defer.returnValue(( - 400, "Cannot modify another user's presence list")) + raise SynapseError( + 400, "Cannot modify another user's presence list") try: content = json.loads(request.content.read()) except: logger.exception("JSON parse error") - defer.returnValue((400, "Unable to parse content")) + raise SynapseError(400, "Unable to parse content") deferreds = [] if "invite" in content: for u in content["invite"]: + if not isinstance(u, basestring): + raise SynapseError(400, "Bad invite value.") + if len(u) == 0: + continue invited_user = self.hs.parse_userid(u) deferreds.append(self.handlers.presence_handler.send_invite( observer_user=user, observed_user=invited_user)) if "drop" in content: for u in content["drop"]: + if not isinstance(u, basestring): + raise SynapseError(400, "Bad drop value.") + if len(u) == 0: + continue dropped_user = self.hs.parse_userid(u) deferreds.append(self.handlers.presence_handler.drop( observer_user=user, observed_user=dropped_user)) diff --git a/synapse/rest/profile.py b/synapse/rest/profile.py index 06076667c7..2e17f87fa1 100644 --- a/synapse/rest/profile.py +++ b/synapse/rest/profile.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ from twisted.internet import defer from base import RestServlet, client_path_pattern import json +import urllib class ProfileDisplaynameRestServlet(RestServlet): @@ -26,6 +27,7 @@ class ProfileDisplaynameRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) displayname = yield self.handlers.profile_handler.get_displayname( @@ -37,6 +39,7 @@ class ProfileDisplaynameRestServlet(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) try: @@ -59,6 +62,7 @@ class ProfileAvatarURLRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) avatar_url = yield self.handlers.profile_handler.get_avatar_url( @@ -70,6 +74,7 @@ class ProfileAvatarURLRestServlet(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) try: @@ -92,6 +97,7 @@ class ProfileRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): + user_id = urllib.unquote(user_id) user = self.hs.parse_userid(user_id) displayname = yield self.handlers.profile_handler.get_displayname( diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 27ab7f182b..b8de3b250d 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/room.py b/synapse/rest/room.py index d76a2f5cd4..308b447090 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -388,7 +388,7 @@ class RoomMembershipRestServlet(RestServlet): def register(self, http_server): # /rooms/$roomid/[invite|join|leave] PATTERN = ("/rooms/(?P<room_id>[^/]*)/" + - "(?P<membership_action>join|invite|leave|ban)") + "(?P<membership_action>join|invite|leave|ban|kick)") register_txn_path(self, PATTERN, http_server) @defer.inlineCallbacks @@ -399,11 +399,14 @@ class RoomMembershipRestServlet(RestServlet): # target user is you unless it is an invite state_key = user.to_string() - if membership_action in ["invite", "ban"]: + if membership_action in ["invite", "ban", "kick"]: if "user_id" not in content: raise SynapseError(400, "Missing user_id key.") state_key = content["user_id"] + if membership_action == "kick": + membership_action = "leave" + event = self.event_factory.create_event( etype=RoomMemberEvent.TYPE, content={"membership": unicode(membership_action)}, diff --git a/synapse/rest/transactions.py b/synapse/rest/transactions.py index b8aa1ef11c..e06dcc8c57 100644 --- a/synapse/rest/transactions.py +++ b/synapse/rest/transactions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/server.py b/synapse/server.py index 35e311a47d..83368ea5a7 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/state.py b/synapse/state.py index e1a1a159bb..36d8210eb5 100644 --- a/synapse/state.py +++ b/synapse/state.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -179,6 +179,18 @@ class StateHandler(object): key=lambda x: x.depth ) + if not hasattr(missing_prev, "prev_state_id"): + # FIXME Hmm + # temporary fallback + for algo in conflict_res: + new_res, curr_res = algo(new_branch, current_branch) + + if new_res < curr_res: + defer.returnValue(False) + elif new_res > curr_res: + defer.returnValue(True) + return + pdu_id = missing_prev.prev_state_id origin = missing_prev.prev_state_origin diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index aadaab06e7..d97014f4da 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 33d56f47ce..bae50e7d1f 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -79,19 +79,21 @@ class SQLBaseStore(object): # "Simple" SQL API methods that operate on a single table with no JOINs, # no complex WHERE clauses, just a dict of values for columns. - def _simple_insert(self, table, values): + def _simple_insert(self, table, values, or_replace=False): """Executes an INSERT query on the named table. Args: table : string giving the table name values : dict of new column names and values for them + or_replace : bool; if True performs an INSERT OR REPLACE """ return self._db_pool.runInteraction( - self._simple_insert_txn, table, values, + self._simple_insert_txn, table, values, or_replace=or_replace ) - def _simple_insert_txn(self, txn, table, values): - sql = "INSERT INTO %s (%s) VALUES(%s)" % ( + def _simple_insert_txn(self, txn, table, values, or_replace=False): + sql = "%s INTO %s (%s) VALUES(%s)" % ( + ("INSERT OR REPLACE" if or_replace else "INSERT"), table, ", ".join(k for k in values), ", ".join("?" for k in values) diff --git a/synapse/storage/directory.py b/synapse/storage/directory.py index b22ce02f3f..bf55449253 100644 --- a/synapse/storage/directory.py +++ b/synapse/storage/directory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/feedback.py b/synapse/storage/feedback.py index bac3dea955..8a18617188 100644 --- a/synapse/storage/feedback.py +++ b/synapse/storage/feedback.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py index 4d19b9f641..5a38c3e8f2 100644 --- a/synapse/storage/keys.py +++ b/synapse/storage/keys.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/pdu.py b/synapse/storage/pdu.py index 9fd44f2454..0bf97e37ee 100644 --- a/synapse/storage/pdu.py +++ b/synapse/storage/pdu.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/presence.py b/synapse/storage/presence.py index a529104f4d..71b2bb084d 100644 --- a/synapse/storage/presence.py +++ b/synapse/storage/presence.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/profile.py b/synapse/storage/profile.py index 91dd565033..7e1fdd9d88 100644 --- a/synapse/storage/profile.py +++ b/synapse/storage/profile.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index b1e4196435..fd762bc643 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/room.py b/synapse/storage/room.py index 01ae190316..017169ce00 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 2746126e85..75c9a60101 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -165,7 +165,7 @@ class RoomMemberStore(SQLBaseStore): defer.returnValue(results) @defer.inlineCallbacks - def do_users_share_a_room(self, user_list): + def user_rooms_intersect(self, user_list): """ Checks whether a list of users share a room. """ user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list)) diff --git a/synapse/storage/schema/delta/v2.sql b/synapse/storage/schema/delta/v2.sql new file mode 100644 index 0000000000..73b140465e --- /dev/null +++ b/synapse/storage/schema/delta/v2.sql @@ -0,0 +1,168 @@ +/* Copyright 2014 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. + */ + +CREATE TABLE IF NOT EXISTS events( + stream_ordering INTEGER PRIMARY KEY AUTOINCREMENT, + topological_ordering INTEGER NOT NULL, + event_id TEXT NOT NULL, + type TEXT NOT NULL, + room_id TEXT NOT NULL, + content TEXT NOT NULL, + unrecognized_keys TEXT, + processed BOOL NOT NULL, + outlier BOOL NOT NULL, + CONSTRAINT ev_uniq UNIQUE (event_id) +); + +CREATE INDEX IF NOT EXISTS events_event_id ON events (event_id); +CREATE INDEX IF NOT EXISTS events_stream_ordering ON events (stream_ordering); +CREATE INDEX IF NOT EXISTS events_topological_ordering ON events (topological_ordering); +CREATE INDEX IF NOT EXISTS events_room_id ON events (room_id); + +CREATE TABLE IF NOT EXISTS state_events( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + type TEXT NOT NULL, + state_key TEXT NOT NULL, + prev_state TEXT +); + +CREATE UNIQUE INDEX IF NOT EXISTS state_events_event_id ON state_events (event_id); +CREATE INDEX IF NOT EXISTS state_events_room_id ON state_events (room_id); +CREATE INDEX IF NOT EXISTS state_events_type ON state_events (type); +CREATE INDEX IF NOT EXISTS state_events_state_key ON state_events (state_key); + + +CREATE TABLE IF NOT EXISTS current_state_events( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + type TEXT NOT NULL, + state_key TEXT NOT NULL, + CONSTRAINT curr_uniq UNIQUE (room_id, type, state_key) ON CONFLICT REPLACE +); + +CREATE INDEX IF NOT EXISTS curr_events_event_id ON current_state_events (event_id); +CREATE INDEX IF NOT EXISTS current_state_events_room_id ON current_state_events (room_id); +CREATE INDEX IF NOT EXISTS current_state_events_type ON current_state_events (type); +CREATE INDEX IF NOT EXISTS current_state_events_state_key ON current_state_events (state_key); + +CREATE TABLE IF NOT EXISTS room_memberships( + event_id TEXT NOT NULL, + user_id TEXT NOT NULL, + sender TEXT NOT NULL, + room_id TEXT NOT NULL, + membership TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_memberships_event_id ON room_memberships (event_id); +CREATE INDEX IF NOT EXISTS room_memberships_room_id ON room_memberships (room_id); +CREATE INDEX IF NOT EXISTS room_memberships_user_id ON room_memberships (user_id); + +CREATE TABLE IF NOT EXISTS feedback( + event_id TEXT NOT NULL, + feedback_type TEXT, + target_event_id TEXT, + sender TEXT, + room_id TEXT +); + +CREATE TABLE IF NOT EXISTS topics( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + topic TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS room_names( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS rooms( + room_id TEXT PRIMARY KEY NOT NULL, + is_public INTEGER, + creator TEXT +); + +CREATE TABLE IF NOT EXISTS room_join_rules( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + join_rule TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id); +CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id); + + +CREATE TABLE IF NOT EXISTS room_power_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + level INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id); +CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id); +CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id); + + +CREATE TABLE IF NOT EXISTS room_default_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id); +CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_add_state_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id); +CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_send_event_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id); +CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_ops_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + ban_level INTEGER, + kick_level INTEGER +); + +CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id); +CREATE INDEX IF NOT EXISTS room_ops_levels_room_id ON room_ops_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_hosts( + room_id TEXT NOT NULL, + host TEXT NOT NULL, + CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE +); + +CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id); + +PRAGMA user_version = 2; diff --git a/synapse/storage/schema/edge_pdus.sql b/synapse/storage/schema/edge_pdus.sql index 17b3c52f0d..8a00868065 100644 --- a/synapse/storage/schema/edge_pdus.sql +++ b/synapse/storage/schema/edge_pdus.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index dbefbbda31..6ffea51310 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/keys.sql b/synapse/storage/schema/keys.sql index 45cdbcecae..706a1a03ff 100644 --- a/synapse/storage/schema/keys.sql +++ b/synapse/storage/schema/keys.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/pdu.sql b/synapse/storage/schema/pdu.sql index ca3de005e9..16e111a56c 100644 --- a/synapse/storage/schema/pdu.sql +++ b/synapse/storage/schema/pdu.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/presence.sql b/synapse/storage/schema/presence.sql index b1081d3aab..595b3b5a69 100644 --- a/synapse/storage/schema/presence.sql +++ b/synapse/storage/schema/presence.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/profiles.sql b/synapse/storage/schema/profiles.sql index 1092d7672c..58209f1af0 100644 --- a/synapse/storage/schema/profiles.sql +++ b/synapse/storage/schema/profiles.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/room_aliases.sql b/synapse/storage/schema/room_aliases.sql index d72b79e6ed..9191016814 100644 --- a/synapse/storage/schema/room_aliases.sql +++ b/synapse/storage/schema/room_aliases.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/transactions.sql b/synapse/storage/schema/transactions.sql index 4b1a2368f6..88e3e4e04d 100644 --- a/synapse/storage/schema/transactions.sql +++ b/synapse/storage/schema/transactions.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/schema/users.sql b/synapse/storage/schema/users.sql index 46b60297cb..2519702971 100644 --- a/synapse/storage/schema/users.sql +++ b/synapse/storage/schema/users.sql @@ -1,4 +1,4 @@ -/* Copyright 2014 matrix.org +/* Copyright 2014 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index 0b78222827..2cb0067a67 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py index a277e4971a..7467e1035b 100644 --- a/synapse/storage/transactions.py +++ b/synapse/storage/transactions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/streams/__init__.py b/synapse/streams/__init__.py index fe8a073cd3..f9811bfa04 100644 --- a/synapse/streams/__init__.py +++ b/synapse/streams/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/streams/config.py b/synapse/streams/config.py index 01bab568ff..6483ce2e25 100644 --- a/synapse/streams/config.py +++ b/synapse/streams/config.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 08d6e6f733..41715436b0 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/types.py b/synapse/types.py index 1a9dceabf5..c51bc8e4f2 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py index 3ea431a7f9..c9a73b0413 100644 --- a/synapse/util/__init__.py +++ b/synapse/util/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/util/async.py b/synapse/util/async.py index ebbdc00ae1..647ea6142c 100644 --- a/synapse/util/async.py +++ b/synapse/util/async.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/util/distributor.py b/synapse/util/distributor.py index 9605d7d1b9..1de50e049f 100644 --- a/synapse/util/distributor.py +++ b/synapse/util/distributor.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,7 +32,9 @@ class Distributor(object): model will do for today. """ - def __init__(self): + def __init__(self, suppress_failures=True): + self.suppress_failures = suppress_failures + self.signals = {} self.pre_registration = {} @@ -40,7 +42,9 @@ class Distributor(object): if name in self.signals: raise KeyError("%r already has a signal named %s" % (self, name)) - self.signals[name] = Signal(name) + self.signals[name] = Signal(name, + suppress_failures=self.suppress_failures, + ) if name in self.pre_registration: signal = self.signals[name] @@ -74,8 +78,9 @@ class Signal(object): method into all of the observers. """ - def __init__(self, name): + def __init__(self, name, suppress_failures): self.name = name + self.suppress_failures = suppress_failures self.observers = [] def observe(self, observer): @@ -104,6 +109,10 @@ class Signal(object): failure.type, failure.value, failure.getTracebackObject())) + if not self.suppress_failures: + raise failure deferreds.append(d.addErrback(eb)) - return defer.DeferredList(deferreds) + return defer.DeferredList( + deferreds, fireOnOneErrback=not self.suppress_failures + ) diff --git a/synapse/util/jsonobject.py b/synapse/util/jsonobject.py index e2840b59f9..6c99705747 100644 --- a/synapse/util/jsonobject.py +++ b/synapse/util/jsonobject.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/util/lockutils.py b/synapse/util/lockutils.py index d0bb50d035..3a84c09db4 100644 --- a/synapse/util/lockutils.py +++ b/synapse/util/lockutils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py index 3d7c46ae44..fadf0bd510 100644 --- a/synapse/util/logutils.py +++ b/synapse/util/logutils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index e1b0796e56..8767e437dd 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/__init__.py b/tests/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/events/__init__.py b/tests/events/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/tests/events/__init__.py +++ b/tests/events/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/events/test_events.py b/tests/events/test_events.py index 35e9c68f5c..93d5c15c6f 100644 --- a/tests/events/test_events.py +++ b/tests/events/test_events.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/federation/test_federation.py b/tests/federation/test_federation.py index 51308ca358..0b105fe723 100644 --- a/tests/federation/test_federation.py +++ b/tests/federation/test_federation.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/federation/test_pdu_codec.py b/tests/federation/test_pdu_codec.py index 2c546040b8..9f74ba119f 100644 --- a/tests/federation/test_pdu_codec.py +++ b/tests/federation/test_pdu_codec.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 88ac8933f8..72a2b1443a 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ from mock import Mock import logging from synapse.server import HomeServer +from synapse.http.client import HttpClient from synapse.handlers.directory import DirectoryHandler from synapse.storage.directory import RoomAliasMapping @@ -49,6 +50,7 @@ class DirectoryTestCase(unittest.TestCase): hs = HomeServer("test", datastore=Mock(spec=[ "get_association_from_room_alias", + "get_joined_hosts_for_room", ]), http_client=None, resource_for_federation=Mock(), @@ -60,6 +62,10 @@ class DirectoryTestCase(unittest.TestCase): self.datastore = hs.get_datastore() + def hosts(room_id): + return defer.succeed([]) + self.datastore.get_joined_hosts_for_room.side_effect = hosts + self.my_room = hs.parse_roomalias("#my-room:test") self.remote_room = hs.parse_roomalias("#another:remote") @@ -92,7 +98,10 @@ class DirectoryTestCase(unittest.TestCase): self.mock_federation.make_query.assert_called_with( destination="remote", query_type="directory", - args={"room_alias": "#another:remote"} + args={ + "room_alias": "#another:remote", + HttpClient.RETRY_DNS_LOOKUP_FAILURES: False + } ) @defer.inlineCallbacks diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index fd19442645..7c4921e226 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -1,4 +1,4 @@ -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index b8309bc063..9eb8b6909f 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -117,10 +117,12 @@ class PresenceStateTestCase(unittest.TestCase): return defer.succeed([]) room_member_handler.get_room_members = get_room_members - def do_users_share_a_room(userlist): - shared = all(map(lambda u: u in self.room_members, userlist)) + def user_rooms_intersect(userlist): + room_member_ids = map(lambda u: u.to_string(), self.room_members) + + shared = all(map(lambda i: i in room_member_ids, userlist)) return defer.succeed(shared) - self.datastore.do_users_share_a_room = do_users_share_a_room + self.datastore.user_rooms_intersect = user_rooms_intersect self.mock_start = Mock() self.mock_stop = Mock() @@ -140,7 +142,7 @@ class PresenceStateTestCase(unittest.TestCase): ) self.assertEquals( - {"state": ONLINE, "presence": ONLINE, "status_msg": "Online"}, + {"presence": ONLINE, "status_msg": "Online"}, state ) mocked_get.assert_called_with("apple") @@ -157,7 +159,7 @@ class PresenceStateTestCase(unittest.TestCase): ) self.assertEquals( - {"state": ONLINE, "presence": ONLINE, "status_msg": "Online"}, + {"presence": ONLINE, "status_msg": "Online"}, state ) mocked_get.assert_called_with("apple") @@ -176,7 +178,7 @@ class PresenceStateTestCase(unittest.TestCase): ) self.assertEquals( - {"state": ONLINE, "presence": ONLINE, "status_msg": "Online"}, + {"presence": ONLINE, "status_msg": "Online"}, state ) @@ -206,7 +208,8 @@ class PresenceStateTestCase(unittest.TestCase): state={"presence": UNAVAILABLE, "status_msg": "Away"}) mocked_set.assert_called_with("apple", - {"state": UNAVAILABLE, "status_msg": "Away"}) + {"state": UNAVAILABLE, "status_msg": "Away"} + ) self.mock_start.assert_called_with(self.u_apple, state={ "presence": UNAVAILABLE, @@ -458,8 +461,7 @@ class PresenceInvitesTestCase(unittest.TestCase): self.assertEquals([ {"observed_user": self.u_banana, - "presence": OFFLINE, - "state": OFFLINE}, + "presence": OFFLINE}, ], presence) self.datastore.get_presence_list.assert_called_with("apple", @@ -476,8 +478,7 @@ class PresenceInvitesTestCase(unittest.TestCase): self.assertEquals([ {"observed_user": self.u_banana, - "presence": OFFLINE, - "state": OFFLINE}, + "presence": OFFLINE}, ], presence) self.datastore.get_presence_list.assert_called_with("apple", @@ -562,6 +563,13 @@ class PresencePushTestCase(unittest.TestCase): return defer.succeed([]) self.datastore.get_joined_hosts_for_room = get_room_hosts + def user_rooms_intersect(userlist): + room_member_ids = map(lambda u: u.to_string(), self.room_members) + + shared = all(map(lambda i: i in room_member_ids, userlist)) + return defer.succeed(shared) + self.datastore.user_rooms_intersect = user_rooms_intersect + @defer.inlineCallbacks def fetch_room_distributions_into(room_id, localusers=None, remotedomains=None, ignore_user=None): @@ -604,6 +612,7 @@ class PresencePushTestCase(unittest.TestCase): self.u_apple = hs.parse_userid("@apple:test") self.u_banana = hs.parse_userid("@banana:test") self.u_clementine = hs.parse_userid("@clementine:test") + self.u_durian = hs.parse_userid("@durian:test") self.u_elderberry = hs.parse_userid("@elderberry:test") # Remote user @@ -615,7 +624,8 @@ class PresencePushTestCase(unittest.TestCase): self.room_members = [self.u_apple, self.u_elderberry] self.datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE}) + {"state": ONLINE} + ) # TODO(paul): Gut-wrenching self.handler._user_cachemap[self.u_apple] = UserPresenceCache() @@ -632,6 +642,7 @@ class PresencePushTestCase(unittest.TestCase): {"presence": ONLINE} ) + # Apple sees self-reflection (events, _) = yield self.event_source.get_new_events_for_user( self.u_apple, 0, None ) @@ -643,10 +654,56 @@ class PresencePushTestCase(unittest.TestCase): "content": { "user_id": "@apple:test", "presence": ONLINE, - "state": ONLINE, "last_active_ago": 0, }}, ], + msg="Presence event should be visible to self-reflection" + ) + + # Banana sees it because of presence subscription + (events, _) = yield self.event_source.get_new_events_for_user( + self.u_banana, 0, None + ) + + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals(events, + [ + {"type": "m.presence", + "content": { + "user_id": "@apple:test", + "presence": ONLINE, + "last_active_ago": 0, + }}, + ], + msg="Presence event should be visible to explicit subscribers" + ) + + # Elderberry sees it because of same room + (events, _) = yield self.event_source.get_new_events_for_user( + self.u_elderberry, 0, None + ) + + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals(events, + [ + {"type": "m.presence", + "content": { + "user_id": "@apple:test", + "presence": ONLINE, + "last_active_ago": 0, + }}, + ], + msg="Presence event should be visible to other room members" + ) + + # Durian is not in the room, should not see this event + (events, _) = yield self.event_source.get_new_events_for_user( + self.u_durian, 0, None + ) + + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals(events, [], + msg="Presence event should not be visible to others" ) presence = yield self.handler.get_presence_list( @@ -655,15 +712,17 @@ class PresencePushTestCase(unittest.TestCase): self.assertEquals( [ {"observed_user": self.u_banana, - "presence": OFFLINE, - "state": OFFLINE}, + "presence": OFFLINE}, {"observed_user": self.u_clementine, - "presence": OFFLINE, - "state": OFFLINE}, + "presence": OFFLINE}, ], presence ) + # TODO(paul): Gut-wrenching + banana_set = self.handler._local_pushmap.setdefault("banana", set()) + banana_set.add(self.u_apple) + yield self.handler.set_state(self.u_banana, self.u_banana, {"presence": ONLINE} ) @@ -676,11 +735,9 @@ class PresencePushTestCase(unittest.TestCase): self.assertEquals([ {"observed_user": self.u_banana, "presence": ONLINE, - "state": ONLINE, "last_active_ago": 2000}, {"observed_user": self.u_clementine, - "presence": OFFLINE, - "state": OFFLINE}, + "presence": OFFLINE}, ], presence) (events, _) = yield self.event_source.get_new_events_for_user( @@ -694,7 +751,6 @@ class PresencePushTestCase(unittest.TestCase): "content": { "user_id": "@banana:test", "presence": ONLINE, - "state": ONLINE, "last_active_ago": 2000 }}, ] @@ -711,7 +767,21 @@ class PresencePushTestCase(unittest.TestCase): "push": [ {"user_id": "@apple:test", "presence": u"online", - "state": u"online", + "last_active_ago": 0}, + ], + } + ) + ), + defer.succeed((200, "OK")) + ) + put_json.expect_call_and_return( + call("remote", + path=ANY, # Can't guarantee which txn ID will be which + data=_expect_edu("remote", "m.presence", + content={ + "push": [ + {"user_id": "@apple:test", + "presence": u"online", "last_active_ago": 0}, ], } @@ -757,7 +827,7 @@ class PresencePushTestCase(unittest.TestCase): content={ "push": [ {"user_id": "@potato:remote", - "state": "online", + "presence": "online", "last_active_ago": 1000}, ], } @@ -775,7 +845,6 @@ class PresencePushTestCase(unittest.TestCase): "content": { "user_id": "@potato:remote", "presence": ONLINE, - "state": ONLINE, "last_active_ago": 1000, }} ] @@ -786,7 +855,7 @@ class PresencePushTestCase(unittest.TestCase): state = yield self.handler.get_state(self.u_potato, self.u_apple) self.assertEquals( - {"state": ONLINE, "presence": ONLINE, "last_active_ago": 3000}, + {"presence": ONLINE, "last_active_ago": 3000}, state ) @@ -809,6 +878,8 @@ class PresencePushTestCase(unittest.TestCase): "a-room" ) + self.room_members.append(self.u_clementine) + (events, _) = yield self.event_source.get_new_events_for_user( self.u_apple, 0, None ) @@ -820,7 +891,6 @@ class PresencePushTestCase(unittest.TestCase): "content": { "user_id": "@clementine:test", "presence": ONLINE, - "state": ONLINE, "last_active_ago": 0, }} ] @@ -837,8 +907,7 @@ class PresencePushTestCase(unittest.TestCase): content={ "push": [ {"user_id": "@apple:test", - "presence": "online", - "state": "online"}, + "presence": "online"}, ], } ), @@ -852,8 +921,7 @@ class PresencePushTestCase(unittest.TestCase): content={ "push": [ {"user_id": "@banana:test", - "presence": "offline", - "state": "offline"}, + "presence": "offline"}, ], } ), @@ -882,8 +950,7 @@ class PresencePushTestCase(unittest.TestCase): content={ "push": [ {"user_id": "@clementine:test", - "presence": "online", - "state": "online"}, + "presence": "online"}, ], } ), @@ -1182,7 +1249,6 @@ class PresencePollingTestCase(unittest.TestCase): "push": [ {"user_id": "@banana:test", "presence": "offline", - "state": "offline", "status_msg": None}, ], }, diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py index 38cc34350b..b35980d948 100644 --- a/tests/handlers/test_presencelike.py +++ b/tests/handlers/test_presencelike.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -107,9 +107,9 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): return defer.succeed(self.presence_list) self.datastore.get_presence_list = get_presence_list - def do_users_share_a_room(userlist): + def user_rooms_intersect(userlist): return defer.succeed(False) - self.datastore.do_users_share_a_room = do_users_share_a_room + self.datastore.user_rooms_intersect = user_rooms_intersect self.handlers = hs.get_handlers() @@ -148,10 +148,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): yield self.handlers.presence_handler.set_state( target_user=self.u_apple, auth_user=self.u_apple, - state={"state": UNAVAILABLE, "status_msg": "Away"}) + state={"presence": UNAVAILABLE, "status_msg": "Away"}) mocked_set.assert_called_with("apple", - {"state": UNAVAILABLE, "status_msg": "Away"}) + {"state": UNAVAILABLE, "status_msg": "Away"} + ) @defer.inlineCallbacks def test_push_local(self): @@ -161,7 +162,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): ] self.datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE}) + {"state": ONLINE} + ) # TODO(paul): Gut-wrenching from synapse.handlers.presence import UserPresenceCache @@ -177,9 +179,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): apple_set.add(self.u_clementine) yield self.handlers.presence_handler.set_state(self.u_apple, - self.u_apple, {"state": ONLINE}) + self.u_apple, {"presence": ONLINE} + ) yield self.handlers.presence_handler.set_state(self.u_banana, - self.u_banana, {"state": ONLINE}) + self.u_banana, {"presence": ONLINE} + ) presence = yield self.handlers.presence_handler.get_presence_list( observer_user=self.u_apple, accepted=True) @@ -187,14 +191,12 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): self.assertEquals([ {"observed_user": self.u_banana, "presence": ONLINE, - "state": ONLINE, "last_active_ago": 0, "displayname": "Frank", "avatar_url": "http://foo"}, {"observed_user": self.u_clementine, - "presence": OFFLINE, - "state": OFFLINE}], - presence) + "presence": OFFLINE} + ], presence) self.mock_update_client.assert_has_calls([ call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]), @@ -242,7 +244,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): ] self.datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE}) + {"state": ONLINE} + ) # TODO(paul): Gut-wrenching from synapse.handlers.presence import UserPresenceCache @@ -257,7 +260,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): apple_set.add(self.u_potato.domain) yield self.handlers.presence_handler.set_state(self.u_apple, - self.u_apple, {"state": ONLINE}) + self.u_apple, {"presence": ONLINE} + ) self.replication.send_edu.assert_called_with( destination="remote", @@ -266,7 +270,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): "push": [ {"user_id": "@apple:test", "presence": "online", - "state": "online", "last_active_ago": 0, "displayname": "Frank", "avatar_url": "http://foo"}, @@ -283,18 +286,19 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): # TODO(paul): Gut-wrenching potato_set = self.handlers.presence_handler._remote_recvmap.setdefault( - self.u_potato, set()) + self.u_potato, set() + ) potato_set.add(self.u_apple) yield self.replication.received_edu( - "remote", "m.presence", { - "push": [ - {"user_id": "@potato:remote", - "state": "online", - "displayname": "Frank", - "avatar_url": "http://foo"}, - ], - } + "remote", "m.presence", { + "push": [ + {"user_id": "@potato:remote", + "presence": "online", + "displayname": "Frank", + "avatar_url": "http://foo"}, + ], + } ) self.mock_update_client.assert_called_with( @@ -313,7 +317,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): self.assertEquals( {"presence": ONLINE, - "state": ONLINE, "displayname": "Frank", "avatar_url": "http://foo"}, state) diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 87a8139920..8e7a89b479 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index 4591a5ea58..5687bbea0b 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index c3c98074cc..6532ac94a3 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/tests/rest/__init__.py +++ b/tests/rest/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py index c8527f3517..1dccf4c503 100644 --- a/tests/rest/test_events.py +++ b/tests/rest/test_events.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py index e2cdd80e07..a1db0fbcf3 100644 --- a/tests/rest/test_presence.py +++ b/tests/rest/test_presence.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -99,7 +99,7 @@ class PresenceStateTestCase(unittest.TestCase): self.assertEquals(200, code) self.assertEquals( - {"presence": ONLINE, "state": ONLINE, "status_msg": "Available"}, + {"presence": ONLINE, "status_msg": "Available"}, response ) mocked_get.assert_called_with("apple") @@ -115,7 +115,8 @@ class PresenceStateTestCase(unittest.TestCase): self.assertEquals(200, code) mocked_set.assert_called_with("apple", - {"state": UNAVAILABLE, "status_msg": "Away"}) + {"state": UNAVAILABLE, "status_msg": "Away"} + ) class PresenceListTestCase(unittest.TestCase): @@ -176,7 +177,7 @@ class PresenceListTestCase(unittest.TestCase): self.assertEquals(200, code) self.assertEquals([ - {"user_id": "@banana:test", "presence": OFFLINE, "state": OFFLINE}, + {"user_id": "@banana:test", "presence": OFFLINE}, ], response) self.datastore.get_presence_list.assert_called_with( @@ -269,11 +270,16 @@ class PresenceEventStreamTestCase(unittest.TestCase): hs.register_servlets() - hs.handlers.room_member_handler = Mock(spec=[ - "get_rooms_for_user", - ]) - hs.handlers.room_member_handler.get_rooms_for_user = ( - lambda u: defer.succeed([])) + hs.handlers.room_member_handler = Mock(spec=[]) + + self.room_members = [] + + def get_rooms_for_user(user): + if user in self.room_members: + return ["a-room"] + else: + return [] + hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user self.mock_datastore = hs.get_datastore() @@ -285,6 +291,17 @@ class PresenceEventStreamTestCase(unittest.TestCase): return defer.succeed(None) self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url + def user_rooms_intersect(user_list): + room_member_ids = map(lambda u: u.to_string(), self.room_members) + + shared = all(map(lambda i: i in room_member_ids, user_list)) + return defer.succeed(shared) + self.mock_datastore.user_rooms_intersect = user_rooms_intersect + + def get_joined_hosts_for_room(room_id): + return [] + self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room + self.presence = hs.get_handlers().presence_handler self.u_apple = hs.parse_userid("@apple:test") @@ -292,10 +309,14 @@ class PresenceEventStreamTestCase(unittest.TestCase): @defer.inlineCallbacks def test_shortpoll(self): + self.room_members = [self.u_apple, self.u_banana] + self.mock_datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE}) + {"state": ONLINE} + ) self.mock_datastore.get_presence_list.return_value = defer.succeed( - []) + [] + ) (code, response) = yield self.mock_resource.trigger("GET", "/events?timeout=0", None) @@ -311,9 +332,11 @@ class PresenceEventStreamTestCase(unittest.TestCase): ) self.mock_datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE}) + {"state": ONLINE} + ) self.mock_datastore.get_presence_list.return_value = defer.succeed( - []) + [] + ) yield self.presence.set_state(self.u_banana, self.u_banana, state={"presence": ONLINE} @@ -328,7 +351,6 @@ class PresenceEventStreamTestCase(unittest.TestCase): "content": { "user_id": "@banana:test", "presence": ONLINE, - "state": ONLINE, "displayname": "Frank", "last_active_ago": 0, }}, diff --git a/tests/rest/test_profile.py b/tests/rest/test_profile.py index 24456769c7..f41810df1f 100644 --- a/tests/rest/test_profile.py +++ b/tests/rest/test_profile.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/test_rooms.py b/tests/rest/test_rooms.py index 23c50824c7..4ea5828d4f 100644 --- a/tests/rest/test_rooms.py +++ b/tests/rest/test_rooms.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/utils.py b/tests/rest/utils.py index ef9a6071e2..77f5ecf0df 100644 --- a/tests/rest/utils.py +++ b/tests/rest/utils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index 5567480921..330311448d 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_distributor.py b/tests/test_distributor.py index 21c91f335b..04933f0ecf 100644 --- a/tests/test_distributor.py +++ b/tests/test_distributor.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,9 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from twisted.internet import defer +from twisted.trial import unittest from mock import Mock, patch @@ -75,6 +74,24 @@ class DistributorTestCase(unittest.TestCase): self.assertIsInstance(mock_logger.warning.call_args[0][0], str) + @defer.inlineCallbacks + def test_signal_catch_no_suppress(self): + # Gut-wrenching + self.dist.suppress_failures = False + + self.dist.declare("whail") + + observer = Mock() + observer.return_value = defer.fail( + Exception("Oopsie") + ) + + self.dist.observe("whail", observer) + + d = self.dist.fire("whail") + + yield self.assertFailure(d, Exception) + def test_signal_prereg(self): observer = Mock() self.dist.observe("flare", observer) @@ -85,5 +102,6 @@ class DistributorTestCase(unittest.TestCase): observer.assert_called_with(4, 5) def test_signal_undeclared(self): - with self.assertRaises(KeyError): + def code(): self.dist.fire("notification") + self.assertRaises(KeyError, code) diff --git a/tests/test_state.py b/tests/test_state.py index 58fd0bf3be..a1f5ee869b 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_types.py b/tests/test_types.py index d2ccbcfa55..571938356c 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/util/__init__.py b/tests/util/__init__.py index 2216c0f1ca..9bff9ec169 100644 --- a/tests/util/__init__.py +++ b/tests/util/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/util/test_lock.py b/tests/util/test_lock.py index dd83d204d9..5623d78423 100644 --- a/tests/util/test_lock.py +++ b/tests/util/test_lock.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/utils.py b/tests/utils.py index aa7e499e15..d90214e418 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014 matrix.org +# Copyright 2014 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/webclient/app-controller.js b/webclient/app-controller.js index a77d32a5ac..ea48cbb011 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/app-directive.js b/webclient/app-directive.js index eee0d3842f..75283598ab 100644 --- a/webclient/app-directive.js +++ b/webclient/app-directive.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/app-filter.js b/webclient/app-filter.js index e0e8130e45..27f435674f 100644 --- a/webclient/app-filter.js +++ b/webclient/app-filter.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -97,23 +97,28 @@ angular.module('matrixWebClient') // Else, build the name from its users var room = $rootScope.events.rooms[room_id]; if (room) { - if (room.members) { + var room_name_event = room["m.room.name"]; + + if (room_name_event) { + roomName = room_name_event.content.name; + } + else if (room.members) { // Limit the room renaming to 1:1 room if (2 === Object.keys(room.members).length) { for (var i in room.members) { var member = room.members[i]; - if (member.user_id !== matrixService.config().user_id) { + if (member.state_key !== matrixService.config().user_id) { - if (member.user_id in $rootScope.presence) { + if (member.state_key in $rootScope.presence) { // If the user is available in presence, use the displayname there // as it is the most uptodate - roomName = $rootScope.presence[member.user_id].content.displayname; + roomName = $rootScope.presence[member.state_key].content.displayname; } else if (member.content.displayname) { roomName = member.content.displayname; } else { - roomName = member.user_id; + roomName = member.state_key; } } } @@ -140,7 +145,7 @@ angular.module('matrixWebClient') roomName = $rootScope.presence[userID].content.displayname; } else { - roomName = member.user_id; + roomName = userID; } } } diff --git a/webclient/app.js b/webclient/app.js index dac4f048cd..d25e2a6234 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js index c5e4ae07a8..14e2f772f7 100644 --- a/webclient/components/fileInput/file-input-directive.js +++ b/webclient/components/fileInput/file-input-directive.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js index 699a3cbffc..e0f67b2c6c 100644 --- a/webclient/components/fileUpload/file-upload-service.js +++ b/webclient/components/fileUpload/file-upload-service.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index d6a0600132..ee478d2eb0 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,7 +32,9 @@ angular.module('eventHandlerService', []) var MSG_EVENT = "MSG_EVENT"; var MEMBER_EVENT = "MEMBER_EVENT"; var PRESENCE_EVENT = "PRESENCE_EVENT"; + var POWERLEVEL_EVENT = "POWERLEVEL_EVENT"; var CALL_EVENT = "CALL_EVENT"; + var NAME_EVENT = "NAME_EVENT"; var InitialSyncDeferred = $q.defer(); @@ -95,7 +97,7 @@ angular.module('eventHandlerService', []) } } - $rootScope.events.rooms[event.room_id].members[event.user_id] = event; + $rootScope.events.rooms[event.room_id].members[event.state_key] = event; $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent); }; @@ -107,10 +109,20 @@ angular.module('eventHandlerService', []) var handlePowerLevels = function(event, isLiveEvent) { initRoom(event.room_id); - $rootScope.events.rooms[event.room_id][event.type] = event; + // Keep the latest data. Do not care of events that come when paginating back + if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) { + $rootScope.events.rooms[event.room_id][event.type] = event; + $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent); + } + }; - //TODO - //$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); + var handleRoomName = function(event, isLiveEvent) { + console.log("handleRoomName " + isLiveEvent); + + initRoom(event.room_id); + + $rootScope.events.rooms[event.room_id][event.type] = event; + $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent); }; var handleCallEvent = function(event, isLiveEvent) { @@ -122,7 +134,9 @@ angular.module('eventHandlerService', []) MSG_EVENT: MSG_EVENT, MEMBER_EVENT: MEMBER_EVENT, PRESENCE_EVENT: PRESENCE_EVENT, + POWERLEVEL_EVENT: POWERLEVEL_EVENT, CALL_EVENT: CALL_EVENT, + NAME_EVENT: NAME_EVENT, handleEvent: function(event, isLiveEvent) { @@ -146,7 +160,9 @@ angular.module('eventHandlerService', []) case 'm.room.power_levels': handlePowerLevels(event, isLiveEvent); break; - + case 'm.room.name': + handleRoomName(event, isLiveEvent); + break; default: console.log("Unable to handle event type " + event.type); console.log(JSON.stringify(event, undefined, 4)); diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js index 441148670e..1c0f7712b4 100644 --- a/webclient/components/matrix/event-stream-service.js +++ b/webclient/components/matrix/event-stream-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 47b63d7f2f..3e13e4e81f 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js index d9e2e8baa3..ca86b473e7 100644 --- a/webclient/components/matrix/matrix-phone-service.js +++ b/webclient/components/matrix/matrix-phone-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index d399aa3cb9..7c6d4ae50f 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -167,6 +167,29 @@ angular.module('matrixService', []) return doRequest("POST", path, undefined, data); }, + // Change the membership of an another user + setMembership: function(room_id, user_id, membershipValue) { + // The REST path spec + var path = "/rooms/$room_id/state/m.room.member/$user_id"; + path = path.replace("$room_id", encodeURIComponent(room_id)); + path = path.replace("$user_id", user_id); + + return doRequest("PUT", path, undefined, { + membership: membershipValue + }); + }, + + // Bans a user from from a room + ban: function(room_id, user_id, reason) { + var path = "/rooms/$room_id/ban"; + path = path.replace("$room_id", encodeURIComponent(room_id)); + + return doRequest("POST", path, undefined, { + user_id: user_id, + reason: reason + }); + }, + // Retrieves the room ID corresponding to a room alias resolveRoomAlias:function(room_alias) { var path = "/_matrix/client/api/v1/directory/room/$room_alias"; @@ -253,7 +276,7 @@ angular.module('matrixService', []) // get a list of public rooms on your home server publicRooms: function() { - var path = "/publicRooms" + var path = "/publicRooms"; return doRequest("GET", path); }, @@ -309,7 +332,7 @@ angular.module('matrixService', []) // hit the Identity Server for a 3PID request. linkEmail: function(email, clientSecret, sendAttempt) { - var path = "/_matrix/identity/api/v1/validate/email/requestToken" + var path = "/_matrix/identity/api/v1/validate/email/requestToken"; var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt; var headers = {}; headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -414,7 +437,8 @@ angular.module('matrixService', []) state: presence }); }, - + + /****** Permanent storage of user information ******/ // Returns the current config @@ -514,6 +538,35 @@ angular.module('matrixService', []) } } return powerLevel; + }, + + /** + * Change or reset the power level of a user + * @param {String} room_id the room id + * @param {String} user_id the user id + * @param {Number} powerLevel a value between 0 and 10 + * If undefined, the user power level will be reset, ie he will use the default room user power level + * @returns {promise} an $http promise + */ + setUserPowerLevel: function(room_id, user_id, powerLevel) { + + // Hack: currently, there is no home server API so do it by hand by updating + // the current m.room.power_levels of the room and send it to the server + var room = $rootScope.events.rooms[room_id]; + if (room && room["m.room.power_levels"]) { + var content = angular.copy(room["m.room.power_levels"].content); + content[user_id] = powerLevel; + + var path = "/rooms/$room_id/state/m.room.power_levels"; + path = path.replace("$room_id", encodeURIComponent(room_id)); + + return doRequest("PUT", path, undefined, content); + } + + // The room does not exist or does not contain power_levels data + var deferred = $q.defer(); + deferred.reject({data:{error: "Invalid room: " + room_id}}); + return deferred.promise; } }; diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js index 555118133b..952c8ec8a9 100644 --- a/webclient/components/matrix/presence-service.js +++ b/webclient/components/matrix/presence-service.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js index 3df2f04458..b417cc5b39 100644 --- a/webclient/components/utilities/utilities-service.js +++ b/webclient/components/utilities/utilities-service.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index f4ce3053ea..11b3682d34 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js index e367b2f0c5..5ef39a7122 100644 --- a/webclient/login/login-controller.js +++ b/webclient/login/login-controller.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index d70e83c3bd..b7584a7d33 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js index d7d3bf4053..3209f2cbdf 100644 --- a/webclient/recents/recents-controller.js +++ b/webclient/recents/recents-controller.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js index 45653fca96..d80de6fbeb 100644 --- a/webclient/recents/recents-filter.js +++ b/webclient/recents/recents-filter.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html index db3b0fb32f..9978e08b13 100644 --- a/webclient/recents/recents.html +++ b/webclient/recents/recents.html @@ -23,8 +23,8 @@ <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" > <div ng-switch-when="m.room.member"> {{ room.lastMsg.user_id }} - {{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }} - {{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }} + {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }} + {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }} </div> <div ng-switch-when="m.room.message"> diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 72c290ad73..52c57856ee 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -85,6 +85,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) updatePresence(event); } }); + + $scope.$on(eventHandlerService.POWERLEVEL_EVENT, function(ngEvent, event, isLive) { + if (isLive && event.room_id === $scope.room_id) { + for (var user_id in event.content) { + updateUserPowerLevel(user_id); + } + } + }); $scope.memberCount = function() { return Object.keys($scope.members).length; @@ -161,6 +169,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) var updateMemberList = function(chunk) { if (chunk.room_id != $scope.room_id) return; + // Ignore banned people + if ("ban" === chunk.membership) { + return; + } + // set target_user_id to keep things clear var target_user_id = chunk.state_key; @@ -240,6 +253,29 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) var member = $scope.members[user_id]; if (member) { member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id); + + normaliseMembersPowerLevels(); + } + } + + // Normalise users power levels so that the user with the higher power level + // will have a bar covering 100% of the width of his avatar + var normaliseMembersPowerLevels = function() { + // Find the max power level + var maxPowerLevel = 0; + for (var i in $scope.members) { + var member = $scope.members[i]; + if (member.powerLevel) { + maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel); + } + } + + // Normalized them on a 0..100% scale to be use in css width + if (maxPowerLevel) { + for (var i in $scope.members) { + var member = $scope.members[i]; + member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel; + } } } @@ -250,28 +286,93 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) $scope.state.sending = true; - // Send the text message var promise; - // FIXME: handle other commands too - if ($scope.textInput.indexOf("/me") === 0) { - promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4)); - } - else if ($scope.textInput.indexOf("/nick ") === 0) { - // Change user display name - promise = matrixService.setDisplayName($scope.textInput.substr(6)); + + // Check for IRC style commands first + if ($scope.textInput.indexOf("/") === 0) { + var args = $scope.textInput.split(' '); + var cmd = args[0]; + + switch (cmd) { + case "/me": + var emoteMsg = args.slice(1).join(' '); + promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg); + break; + + case "/nick": + // Change user display name + if (2 === args.length) { + promise = matrixService.setDisplayName(args[1]); + } + break; + + case "/kick": + // Kick a user from the room + if (2 === args.length) { + var user_id = args[1]; + + // Set his state in the room as leave + promise = matrixService.setMembership($scope.room_id, user_id, "leave"); + } + break; + + case "/ban": + // Ban a user from the room + if (2 <= args.length) { + // TODO: The user may have entered the display name + // Need display name -> user_id resolution. Pb: how to manage user with same display names? + var user_id = args[1]; + + // Does the user provide a reason? + if (3 <= args.length) { + var reason = args.slice(2).join(' '); + } + promise = matrixService.ban($scope.room_id, user_id, reason); + } + break; + + case "/unban": + // Unban a user from the room + if (2 === args.length) { + var user_id = args[1]; + + // Reset the user membership to leave to unban him + promise = matrixService.setMembership($scope.room_id, user_id, "leave"); + } + break; + + case "/op": + // Define the power level of a user + if (3 === args.length) { + var user_id = args[1]; + var powerLevel = parseInt(args[2]); + promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel); + } + break; + + case "/deop": + // Reset the power level of a user + if (2 === args.length) { + var user_id = args[1]; + promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined); + } + break; + } } - else { + + if (!promise) { + // Send the text message promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); } promise.then( function() { - console.log("Sent message"); + console.log("Request successfully sent"); $scope.textInput = ""; $scope.state.sending = false; }, function(error) { - $scope.feedback = "Failed to send: " + error.data.error; + $scope.feedback = "Request failed: " + error.data.error; $scope.state.sending = false; }); }; @@ -363,7 +464,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) onInit3(); }, function(reason) { - $scope.feedback = "Can't join room: " + reason; + console.log("Can't join room: " + JSON.stringify(reason)); + $scope.feedback = "You do not have permission to join this room"; }); } else { diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js index 1a99a37abb..659bcbc60f 100644 --- a/webclient/room/room-directive.js +++ b/webclient/room/room-directive.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/room/room.html b/webclient/room/room.html index e672b1d7e2..e29f511ecf 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -24,7 +24,7 @@ title="{{ member.id }}" width="80" height="80"/> <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/> - <div class="userPowerLevel" ng-style="{'width': (10 * member.powerLevel) +'%'}"></div> + <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div> </td> <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')"> @@ -50,8 +50,9 @@ <div class="bubble"> <span ng-show='msg.type === "m.room.member"'> {{ members[msg.user_id].displayname || msg.user_id }} - {{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }} - {{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }} + {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }} + {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }} + </span> <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/> <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/> diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js index dc680ef075..7a26367a1b 100644 --- a/webclient/settings/settings-controller.js +++ b/webclient/settings/settings-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index 03927838d2..49dc603540 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -74,6 +74,21 @@ <div>Access token: {{ config.access_token }} </div> </div> <br/> + + <h3>Commands</h3> + <div class="section"> + The following commands are available in the room chat: + <ul> + <li>/nick <display_name>: change your display name</li> + <li>/me <action>: send the action you are doing. /me will be replaced by your display name</li> + <li>/kick <user_id>: kick the user</li> + <li>/ban <user_id> [<reason>]: ban the user</li> + <li>/unban <user_id>: unban the user</li> + <li>/op <user_id> <power_level>: set user power level</li> + <li>/deop <user_id>: reset user power level to the room default value</li> + </ul> + </div> + <br/> {{ feedback }} diff --git a/webclient/user/user-controller.js b/webclient/user/user-controller.js index b5b2d439a2..3940db6683 100644 --- a/webclient/user/user-controller.js +++ b/webclient/user/user-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. |