mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-12-27 03:29:25 -05:00
Merge branch 'release-v0.0.1' of github.com:matrix-org/synapse
This commit is contained in:
commit
e1297c922d
20
CHANGES.rst
Normal file
20
CHANGES.rst
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
Changes in synapse 0.0.1
|
||||||
|
=======================
|
||||||
|
Homeserver:
|
||||||
|
* Completely change the database schema to support generic event types.
|
||||||
|
* Improve presence reliability.
|
||||||
|
* Improve reliability of joining remote rooms.
|
||||||
|
* Fix bug where room join events were duplicated.
|
||||||
|
* Improve initial sync API to return more information to the client.
|
||||||
|
* Stop generating fake messages for room membership events.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add tab completion of names.
|
||||||
|
* Add ability to upload and send images.
|
||||||
|
* Add profile pages.
|
||||||
|
* Improve CSS layout of room.
|
||||||
|
* Disambiguate identical display names.
|
||||||
|
* Don't get remote users display names and avatars individually.
|
||||||
|
* Use the new initial sync API to reduce number of round trips to the homeserver.
|
||||||
|
* Change url scheme to use room aliases instead of room ids where known.
|
||||||
|
* Increase longpoll timeout.
|
19
README.rst
19
README.rst
@ -24,11 +24,8 @@ To get up and running:
|
|||||||
|
|
||||||
- To run your own **private** homeserver on localhost:8080, install synapse
|
- To run your own **private** homeserver on localhost:8080, install synapse
|
||||||
with ``python setup.py develop --user`` and then run one with
|
with ``python setup.py develop --user`` and then run one with
|
||||||
``python synapse/app/homeserver.py``
|
``python synapse/app/homeserver.py`` - you will find a webclient running
|
||||||
|
at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
|
||||||
- To run your own webclient, add ``-w``:
|
|
||||||
``python synapse/app/homeserver.py -w`` and hit http://localhost:8080/matrix/client
|
|
||||||
in your web browser (a recent Chrome, Safari or Firefox for now,
|
|
||||||
please...)
|
please...)
|
||||||
|
|
||||||
- To make the homeserver **public** and let it exchange messages with
|
- To make the homeserver **public** and let it exchange messages with
|
||||||
@ -37,6 +34,7 @@ To get up and running:
|
|||||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
||||||
say hi! :)
|
say hi! :)
|
||||||
|
|
||||||
|
|
||||||
About Matrix
|
About Matrix
|
||||||
============
|
============
|
||||||
|
|
||||||
@ -146,6 +144,13 @@ This should end with a 'PASSED' result::
|
|||||||
PASSED (successes=143)
|
PASSED (successes=143)
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading an existing homeserver
|
||||||
|
================================
|
||||||
|
|
||||||
|
Before upgrading an existing homeserver to a new version, please refer to
|
||||||
|
UPGRADE.rst for any additional instructions.
|
||||||
|
|
||||||
|
|
||||||
Setting up Federation
|
Setting up Federation
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@ -201,9 +206,7 @@ http://localhost:8080. Simply run::
|
|||||||
Running The Demo Web Client
|
Running The Demo Web Client
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
You can run the web client when you run the homeserver by adding ``-w`` to the
|
The homeserver runs a web client by default at http://localhost:8080.
|
||||||
command to run ``homeserver.py``. The web client can be accessed via
|
|
||||||
http://localhost:8080/matrix/client
|
|
||||||
|
|
||||||
If this is the first time you have used the client from that browser (it uses
|
If this is the first time you have used the client from that browser (it uses
|
||||||
HTML5 local storage to remember its config), you will need to log in to your
|
HTML5 local storage to remember its config), you will need to log in to your
|
||||||
|
24
UPGRADE.rst
Normal file
24
UPGRADE.rst
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
Upgrading to v0.0.1
|
||||||
|
===================
|
||||||
|
|
||||||
|
This release completely changes the database schema and so requires upgrading
|
||||||
|
it before starting the new version of the homeserver.
|
||||||
|
|
||||||
|
The script "database-prepare-for-0.0.1.sh" should be used to upgrade the
|
||||||
|
database. This will save all user information, such as logins and profiles,
|
||||||
|
but will otherwise purge the database. This includes messages, which
|
||||||
|
rooms the home server was a member of and room alias mappings.
|
||||||
|
|
||||||
|
Before running the command the homeserver should be first completely
|
||||||
|
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||||
|
|
||||||
|
./database-prepare-for-0.0.1.sh "homeserver.db"
|
||||||
|
|
||||||
|
Once this has successfully completed it will be safe to restart the
|
||||||
|
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||||
|
restart than usual as it reinitializes the database.
|
||||||
|
|
||||||
|
On startup of the new version, users can either rejoin remote rooms using room
|
||||||
|
aliases or by being reinvited. Alternatively, if any other homeserver sends a
|
||||||
|
message to a room that the homeserver was previously in the local HS will
|
||||||
|
automatically rejoin the room.
|
@ -233,56 +233,68 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
|
|
||||||
def do_3pidrequest(self, line):
|
def do_emailrequest(self, line):
|
||||||
"""Requests the association of a third party identifier
|
"""Requests the association of a third party identifier
|
||||||
<medium> The medium of the identifer (currently only 'email')
|
<address> The email address)
|
||||||
<address> The address of the identifer (ie. the email address)
|
<clientSecret> A string of characters generated when requesting an email that you'll supply in subsequent calls to identify yourself
|
||||||
|
<sendAttempt> The number of times the user has requested an email. Leave this the same between requests to retry the request at the transport level. Increment it to request that the email be sent again.
|
||||||
"""
|
"""
|
||||||
args = self._parse(line, ['medium', 'address'])
|
args = self._parse(line, ['address', 'clientSecret', 'sendAttempt'])
|
||||||
|
|
||||||
if not args['medium'] == 'email':
|
postArgs = {'email': args['address'], 'clientSecret': args['clientSecret'], 'sendAttempt': args['sendAttempt']}
|
||||||
print "Only email is supported currently"
|
|
||||||
return
|
|
||||||
|
|
||||||
postArgs = {'email': args['address'], 'clientSecret': '____'}
|
reactor.callFromThread(self._do_emailrequest, postArgs)
|
||||||
|
|
||||||
reactor.callFromThread(self._do_3pidrequest, postArgs)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_3pidrequest(self, args):
|
def _do_emailrequest(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
|
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
print json_res
|
print json_res
|
||||||
if 'tokenId' in json_res:
|
if 'sid' in json_res:
|
||||||
print "Token ID %s sent" % (json_res['tokenId'])
|
print "Token sent. Your session ID is %s" % (json_res['sid'])
|
||||||
|
|
||||||
def do_3pidvalidate(self, line):
|
def do_emailvalidate(self, line):
|
||||||
"""Validate and associate a third party ID
|
"""Validate and associate a third party ID
|
||||||
<medium> The medium of the identifer (currently only 'email')
|
<sid> The session ID (sid) given to you in the response to requestToken
|
||||||
<tokenId> The identifier iof the token given in 3pidrequest
|
|
||||||
<token> The token sent to your third party identifier address
|
<token> The token sent to your third party identifier address
|
||||||
|
<clientSecret> The same clientSecret you supplied in requestToken
|
||||||
"""
|
"""
|
||||||
args = self._parse(line, ['medium', 'tokenId', 'token'])
|
args = self._parse(line, ['sid', 'token', 'clientSecret'])
|
||||||
|
|
||||||
if not args['medium'] == 'email':
|
postArgs = { 'sid' : args['sid'], 'token' : args['token'], 'clientSecret': args['clientSecret'] }
|
||||||
print "Only email is supported currently"
|
|
||||||
return
|
|
||||||
|
|
||||||
postArgs = { 'tokenId' : args['tokenId'], 'token' : args['token'] }
|
reactor.callFromThread(self._do_emailvalidate, postArgs)
|
||||||
postArgs['mxId'] = self.config["user"]
|
|
||||||
|
|
||||||
reactor.callFromThread(self._do_3pidvalidate, postArgs)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_3pidvalidate(self, args):
|
def _do_emailvalidate(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
|
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
print json_res
|
print json_res
|
||||||
|
|
||||||
|
def do_3pidbind(self, line):
|
||||||
|
"""Validate and associate a third party ID
|
||||||
|
<sid> The session ID (sid) given to you in the response to requestToken
|
||||||
|
<clientSecret> The same clientSecret you supplied in requestToken
|
||||||
|
"""
|
||||||
|
args = self._parse(line, ['sid', 'clientSecret'])
|
||||||
|
|
||||||
|
postArgs = { 'sid' : args['sid'], 'clientSecret': args['clientSecret'] }
|
||||||
|
postArgs['mxid'] = self.config["user"]
|
||||||
|
|
||||||
|
reactor.callFromThread(self._do_3pidbind, postArgs)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _do_3pidbind(self, args):
|
||||||
|
url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind"
|
||||||
|
|
||||||
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
|
print json_res
|
||||||
|
|
||||||
def do_join(self, line):
|
def do_join(self, line):
|
||||||
"""Joins a room: "join <roomid>" """
|
"""Joins a room: "join <roomid>" """
|
||||||
try:
|
try:
|
||||||
|
21
database-prepare-for-0.0.1.sh
Executable file
21
database-prepare-for-0.0.1.sh
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This is will prepare a synapse database for running with v0.0.1 of synapse.
|
||||||
|
# It will store all the user information, but will *delete* all messages and
|
||||||
|
# room data.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cp "$1" "$1.bak"
|
||||||
|
|
||||||
|
DUMP=$(sqlite3 "$1" << 'EOF'
|
||||||
|
.dump users
|
||||||
|
.dump access_tokens
|
||||||
|
.dump presence
|
||||||
|
.dump profiles
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
rm "$1"
|
||||||
|
|
||||||
|
sqlite3 "$1" <<< "$DUMP"
|
@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
# $ sqlite3 homeserver.db < table-save.sql
|
# $ sqlite3 homeserver.db < table-save.sql
|
||||||
|
|
||||||
sqlite3 homeserver.db <<'EOF' >table-save.sql
|
sqlite3 "$1" <<'EOF' >table-save.sql
|
||||||
.dump users
|
.dump users
|
||||||
.dump access_tokens
|
.dump access_tokens
|
||||||
.dump presence
|
.dump presence
|
||||||
|
@ -113,7 +113,7 @@ def make_graph(pdus, room, filename_prefix):
|
|||||||
graph.add_edge(state_edge)
|
graph.add_edge(state_edge)
|
||||||
|
|
||||||
graph.write('%s.dot' % filename_prefix, format='raw', prog='dot')
|
graph.write('%s.dot' % filename_prefix, format='raw', prog='dot')
|
||||||
graph.write_png("%s.png" % filename_prefix, prog='dot')
|
# graph.write_png("%s.png" % filename_prefix, prog='dot')
|
||||||
graph.write_svg("%s.svg" % filename_prefix, prog='dot')
|
graph.write_svg("%s.svg" % filename_prefix, prog='dot')
|
||||||
|
|
||||||
|
|
||||||
|
2
setup.py
2
setup.py
@ -25,7 +25,7 @@ def read(fname):
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="SynapseHomeServer",
|
name="SynapseHomeServer",
|
||||||
version="0.1",
|
version="0.0.1",
|
||||||
packages=find_packages(exclude=["tests"]),
|
packages=find_packages(exclude=["tests"]),
|
||||||
description="Reference Synapse Home Server",
|
description="Reference Synapse Home Server",
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
@ -15,3 +15,5 @@
|
|||||||
|
|
||||||
""" This is a reference implementation of a synapse home server.
|
""" This is a reference implementation of a synapse home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.0.1"
|
||||||
|
@ -51,6 +51,7 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"depth",
|
"depth",
|
||||||
"destinations",
|
"destinations",
|
||||||
"origin",
|
"origin",
|
||||||
|
"outlier",
|
||||||
]
|
]
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||||
InviteJoinEvent, RoomConfigEvent
|
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
@ -25,6 +25,7 @@ class EventFactory(object):
|
|||||||
|
|
||||||
_event_classes = [
|
_event_classes = [
|
||||||
RoomTopicEvent,
|
RoomTopicEvent,
|
||||||
|
RoomNameEvent,
|
||||||
MessageEvent,
|
MessageEvent,
|
||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
FeedbackEvent,
|
FeedbackEvent,
|
||||||
@ -32,20 +33,24 @@ class EventFactory(object):
|
|||||||
RoomConfigEvent
|
RoomConfigEvent
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, hs):
|
||||||
self._event_list = {} # dict of TYPE to event class
|
self._event_list = {} # dict of TYPE to event class
|
||||||
for event_class in EventFactory._event_classes:
|
for event_class in EventFactory._event_classes:
|
||||||
self._event_list[event_class.TYPE] = event_class
|
self._event_list[event_class.TYPE] = event_class
|
||||||
|
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
def create_event(self, etype=None, **kwargs):
|
def create_event(self, etype=None, **kwargs):
|
||||||
kwargs["type"] = etype
|
kwargs["type"] = etype
|
||||||
if "event_id" not in kwargs:
|
if "event_id" not in kwargs:
|
||||||
kwargs["event_id"] = random_string(10)
|
kwargs["event_id"] = random_string(10)
|
||||||
|
|
||||||
try:
|
if "ts" not in kwargs:
|
||||||
|
kwargs["ts"] = int(self.clock.time_msec())
|
||||||
|
|
||||||
|
if etype in self._event_list:
|
||||||
handler = self._event_list[etype]
|
handler = self._event_list[etype]
|
||||||
except KeyError: # unknown event type
|
else:
|
||||||
# TODO allow custom event types.
|
handler = GenericEvent
|
||||||
raise NotImplementedError("Unknown etype=%s" % etype)
|
|
||||||
|
|
||||||
return handler(**kwargs)
|
return handler(**kwargs)
|
||||||
|
@ -16,17 +16,45 @@
|
|||||||
from . import SynapseEvent
|
from . import SynapseEvent
|
||||||
|
|
||||||
|
|
||||||
|
class GenericEvent(SynapseEvent):
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class RoomTopicEvent(SynapseEvent):
|
class RoomTopicEvent(SynapseEvent):
|
||||||
TYPE = "m.room.topic"
|
TYPE = "m.room.topic"
|
||||||
|
|
||||||
|
internal_keys = SynapseEvent.internal_keys + [
|
||||||
|
"topic",
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs["state_key"] = ""
|
kwargs["state_key"] = ""
|
||||||
|
if "topic" in kwargs["content"]:
|
||||||
|
kwargs["topic"] = kwargs["content"]["topic"]
|
||||||
super(RoomTopicEvent, self).__init__(**kwargs)
|
super(RoomTopicEvent, self).__init__(**kwargs)
|
||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {"topic": u"string"}
|
return {"topic": u"string"}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomNameEvent(SynapseEvent):
|
||||||
|
TYPE = "m.room.name"
|
||||||
|
|
||||||
|
internal_keys = SynapseEvent.internal_keys + [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs["state_key"] = ""
|
||||||
|
if "name" in kwargs["content"]:
|
||||||
|
kwargs["name"] = kwargs["content"]["name"]
|
||||||
|
super(RoomNameEvent, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {"name": u"string"}
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberEvent(SynapseEvent):
|
class RoomMemberEvent(SynapseEvent):
|
||||||
TYPE = "m.room.member"
|
TYPE = "m.room.member"
|
||||||
|
|
||||||
@ -38,6 +66,8 @@ class RoomMemberEvent(SynapseEvent):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
if "target_user_id" in kwargs:
|
if "target_user_id" in kwargs:
|
||||||
kwargs["state_key"] = kwargs["target_user_id"]
|
kwargs["state_key"] = kwargs["target_user_id"]
|
||||||
|
if "membership" not in kwargs:
|
||||||
|
kwargs["membership"] = kwargs.get("content", {}).get("membership")
|
||||||
super(RoomMemberEvent, self).__init__(**kwargs)
|
super(RoomMemberEvent, self).__init__(**kwargs)
|
||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership
|
||||||
from synapse.api.events.room import RoomMemberEvent
|
from synapse.api.events.room import RoomMemberEvent
|
||||||
|
from synapse.api.streams.event import EventsStreamData
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
@ -66,7 +67,7 @@ class Notifier(object):
|
|||||||
self._notify_and_callback(
|
self._notify_and_callback(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
event_data=event.get_dict(),
|
event_data=event.get_dict(),
|
||||||
stream_type=event.type,
|
stream_type=EventsStreamData.EVENT_TYPE,
|
||||||
store_id=store_id)
|
store_id=store_id)
|
||||||
|
|
||||||
def on_new_user_event(self, user_id, event_data, stream_type, store_id):
|
def on_new_user_event(self, user_id, event_data, stream_type, store_id):
|
||||||
|
@ -20,23 +20,24 @@ class PaginationConfig(object):
|
|||||||
|
|
||||||
"""A configuration object which stores pagination parameters."""
|
"""A configuration object which stores pagination parameters."""
|
||||||
|
|
||||||
def __init__(self, from_tok=None, to_tok=None, limit=0):
|
def __init__(self, from_tok=None, to_tok=None, direction='f', limit=0):
|
||||||
self.from_tok = from_tok
|
self.from_tok = from_tok
|
||||||
self.to_tok = to_tok
|
self.to_tok = to_tok
|
||||||
|
self.direction = direction
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, request, raise_invalid_params=True):
|
def from_request(cls, request, raise_invalid_params=True):
|
||||||
params = {
|
params = {
|
||||||
"from_tok": PaginationStream.TOK_START,
|
"from_tok": "END",
|
||||||
"to_tok": PaginationStream.TOK_END,
|
"direction": 'f',
|
||||||
"limit": 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query_param_mappings = [ # 3-tuple of qp_key, attribute, rules
|
query_param_mappings = [ # 3-tuple of qp_key, attribute, rules
|
||||||
("from", "from_tok", lambda x: type(x) == str),
|
("from", "from_tok", lambda x: type(x) == str),
|
||||||
("to", "to_tok", lambda x: type(x) == str),
|
("to", "to_tok", lambda x: type(x) == str),
|
||||||
("limit", "limit", lambda x: x.isdigit())
|
("limit", "limit", lambda x: x.isdigit()),
|
||||||
|
("dir", "direction", lambda x: x == 'f' or x == 'b'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for qp, attr, is_valid in query_param_mappings:
|
for qp, attr, is_valid in query_param_mappings:
|
||||||
@ -48,12 +49,17 @@ class PaginationConfig(object):
|
|||||||
|
|
||||||
return PaginationConfig(**params)
|
return PaginationConfig(**params)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
"<PaginationConfig from_tok=%s, to_tok=%s, "
|
||||||
|
"direction=%s, limit=%s>"
|
||||||
|
) % (self.from_tok, self.to_tok, self.direction, self.limit)
|
||||||
|
|
||||||
|
|
||||||
class PaginationStream(object):
|
class PaginationStream(object):
|
||||||
|
|
||||||
""" An interface for streaming data as chunks. """
|
""" An interface for streaming data as chunks. """
|
||||||
|
|
||||||
TOK_START = "START"
|
|
||||||
TOK_END = "END"
|
TOK_END = "END"
|
||||||
|
|
||||||
def get_chunk(self, config=None):
|
def get_chunk(self, config=None):
|
||||||
@ -76,7 +82,7 @@ class StreamData(object):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
def get_rows(self, user_id, from_pkey, to_pkey, limit):
|
def get_rows(self, user_id, from_pkey, to_pkey, limit, direction):
|
||||||
""" Get event stream data between the specified pkeys.
|
""" Get event stream data between the specified pkeys.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import EventStreamError
|
from synapse.api.errors import EventStreamError
|
||||||
|
from synapse.api.events import SynapseEvent
|
||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
RoomMemberEvent, MessageEvent, FeedbackEvent, RoomTopicEvent
|
RoomMemberEvent, MessageEvent, FeedbackEvent, RoomTopicEvent
|
||||||
)
|
)
|
||||||
@ -28,17 +29,17 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MessagesStreamData(StreamData):
|
class EventsStreamData(StreamData):
|
||||||
EVENT_TYPE = MessageEvent.TYPE
|
EVENT_TYPE = "EventsStream"
|
||||||
|
|
||||||
def __init__(self, hs, room_id=None, feedback=False):
|
def __init__(self, hs, room_id=None, feedback=False):
|
||||||
super(MessagesStreamData, self).__init__(hs)
|
super(EventsStreamData, self).__init__(hs)
|
||||||
self.room_id = room_id
|
self.room_id = room_id
|
||||||
self.with_feedback = feedback
|
self.with_feedback = feedback
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_rows(self, user_id, from_key, to_key, limit):
|
def get_rows(self, user_id, from_key, to_key, limit, direction):
|
||||||
(data, latest_ver) = yield self.store.get_message_stream(
|
data, latest_ver = yield self.store.get_room_events(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
from_key=from_key,
|
from_key=from_key,
|
||||||
to_key=to_key,
|
to_key=to_key,
|
||||||
@ -50,74 +51,7 @@ class MessagesStreamData(StreamData):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def max_token(self):
|
def max_token(self):
|
||||||
val = yield self.store.get_max_message_id()
|
val = yield self.store.get_room_events_max_id()
|
||||||
defer.returnValue(val)
|
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberStreamData(StreamData):
|
|
||||||
EVENT_TYPE = RoomMemberEvent.TYPE
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_rows(self, user_id, from_key, to_key, limit):
|
|
||||||
(data, latest_ver) = yield self.store.get_room_member_stream(
|
|
||||||
user_id=user_id,
|
|
||||||
from_key=from_key,
|
|
||||||
to_key=to_key
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue((data, latest_ver))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def max_token(self):
|
|
||||||
val = yield self.store.get_max_room_member_id()
|
|
||||||
defer.returnValue(val)
|
|
||||||
|
|
||||||
|
|
||||||
class FeedbackStreamData(StreamData):
|
|
||||||
EVENT_TYPE = FeedbackEvent.TYPE
|
|
||||||
|
|
||||||
def __init__(self, hs, room_id=None):
|
|
||||||
super(FeedbackStreamData, self).__init__(hs)
|
|
||||||
self.room_id = room_id
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_rows(self, user_id, from_key, to_key, limit):
|
|
||||||
(data, latest_ver) = yield self.store.get_feedback_stream(
|
|
||||||
user_id=user_id,
|
|
||||||
from_key=from_key,
|
|
||||||
to_key=to_key,
|
|
||||||
limit=limit,
|
|
||||||
room_id=self.room_id
|
|
||||||
)
|
|
||||||
defer.returnValue((data, latest_ver))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def max_token(self):
|
|
||||||
val = yield self.store.get_max_feedback_id()
|
|
||||||
defer.returnValue(val)
|
|
||||||
|
|
||||||
|
|
||||||
class RoomDataStreamData(StreamData):
|
|
||||||
EVENT_TYPE = RoomTopicEvent.TYPE # TODO need multiple event types
|
|
||||||
|
|
||||||
def __init__(self, hs, room_id=None):
|
|
||||||
super(RoomDataStreamData, self).__init__(hs)
|
|
||||||
self.room_id = room_id
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_rows(self, user_id, from_key, to_key, limit):
|
|
||||||
(data, latest_ver) = yield self.store.get_room_data_stream(
|
|
||||||
user_id=user_id,
|
|
||||||
from_key=from_key,
|
|
||||||
to_key=to_key,
|
|
||||||
limit=limit,
|
|
||||||
room_id=self.room_id
|
|
||||||
)
|
|
||||||
defer.returnValue((data, latest_ver))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def max_token(self):
|
|
||||||
val = yield self.store.get_max_room_data_id()
|
|
||||||
defer.returnValue(val)
|
defer.returnValue(val)
|
||||||
|
|
||||||
|
|
||||||
@ -136,6 +70,15 @@ class EventStream(PaginationStream):
|
|||||||
pagination_config.from_tok)
|
pagination_config.from_tok)
|
||||||
pagination_config.to_tok = yield self.fix_token(
|
pagination_config.to_tok = yield self.fix_token(
|
||||||
pagination_config.to_tok)
|
pagination_config.to_tok)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not pagination_config.to_tok
|
||||||
|
and pagination_config.direction == 'f'
|
||||||
|
):
|
||||||
|
pagination_config.to_tok = yield self.get_current_max_token()
|
||||||
|
|
||||||
|
logger.debug("pagination_config: %s", pagination_config)
|
||||||
|
|
||||||
defer.returnValue(pagination_config)
|
defer.returnValue(pagination_config)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@ -147,39 +90,42 @@ class EventStream(PaginationStream):
|
|||||||
Returns:
|
Returns:
|
||||||
The fixed-up token, which may == token.
|
The fixed-up token, which may == token.
|
||||||
"""
|
"""
|
||||||
# replace TOK_START and TOK_END with 0_0_0 or -1_-1_-1 depending.
|
if token == PaginationStream.TOK_END:
|
||||||
replacements = [
|
new_token = yield self.get_current_max_token()
|
||||||
(PaginationStream.TOK_START, "0"),
|
|
||||||
(PaginationStream.TOK_END, "-1")
|
|
||||||
]
|
|
||||||
for magic_token, key in replacements:
|
|
||||||
if magic_token == token:
|
|
||||||
token = EventStream.SEPARATOR.join(
|
|
||||||
[key] * len(self.stream_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
# replace -1 values with an actual pkey
|
logger.debug("fix_token: From %s to %s", token, new_token)
|
||||||
token_segments = self._split_token(token)
|
|
||||||
for i, tok in enumerate(token_segments):
|
token = new_token
|
||||||
if tok == -1:
|
|
||||||
# add 1 to the max token because results are EXCLUSIVE from the
|
defer.returnValue(token)
|
||||||
# latest version.
|
|
||||||
token_segments[i] = 1 + (yield self.stream_data[i].max_token())
|
|
||||||
defer.returnValue(EventStream.SEPARATOR.join(
|
|
||||||
str(x) for x in token_segments
|
|
||||||
))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_chunk(self, config=None):
|
def get_current_max_token(self):
|
||||||
|
new_token_parts = []
|
||||||
|
for s in self.stream_data:
|
||||||
|
mx = yield s.max_token()
|
||||||
|
new_token_parts.append(str(mx))
|
||||||
|
|
||||||
|
new_token = EventStream.SEPARATOR.join(new_token_parts)
|
||||||
|
|
||||||
|
logger.debug("get_current_max_token: %s", new_token)
|
||||||
|
|
||||||
|
defer.returnValue(new_token)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_chunk(self, config):
|
||||||
# no support for limit on >1 streams, makes no sense.
|
# no support for limit on >1 streams, makes no sense.
|
||||||
if config.limit and len(self.stream_data) > 1:
|
if config.limit and len(self.stream_data) > 1:
|
||||||
raise EventStreamError(
|
raise EventStreamError(
|
||||||
400, "Limit not supported on multiplexed streams."
|
400, "Limit not supported on multiplexed streams."
|
||||||
)
|
)
|
||||||
|
|
||||||
(chunk_data, next_tok) = yield self._get_chunk_data(config.from_tok,
|
chunk_data, next_tok = yield self._get_chunk_data(
|
||||||
|
config.from_tok,
|
||||||
config.to_tok,
|
config.to_tok,
|
||||||
config.limit)
|
config.limit,
|
||||||
|
config.direction,
|
||||||
|
)
|
||||||
|
|
||||||
defer.returnValue({
|
defer.returnValue({
|
||||||
"chunk": chunk_data,
|
"chunk": chunk_data,
|
||||||
@ -188,7 +134,7 @@ class EventStream(PaginationStream):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_chunk_data(self, from_tok, to_tok, limit):
|
def _get_chunk_data(self, from_tok, to_tok, limit, direction):
|
||||||
""" Get event data between the two tokens.
|
""" Get event data between the two tokens.
|
||||||
|
|
||||||
Tokens are SEPARATOR separated values representing pkey values of
|
Tokens are SEPARATOR separated values representing pkey values of
|
||||||
@ -206,6 +152,7 @@ class EventStream(PaginationStream):
|
|||||||
EventStreamError if something went wrong.
|
EventStreamError if something went wrong.
|
||||||
"""
|
"""
|
||||||
# sanity check
|
# sanity check
|
||||||
|
if to_tok is not None:
|
||||||
if (from_tok.count(EventStream.SEPARATOR) !=
|
if (from_tok.count(EventStream.SEPARATOR) !=
|
||||||
to_tok.count(EventStream.SEPARATOR) or
|
to_tok.count(EventStream.SEPARATOR) or
|
||||||
(from_tok.count(EventStream.SEPARATOR) + 1) !=
|
(from_tok.count(EventStream.SEPARATOR) + 1) !=
|
||||||
@ -224,10 +171,13 @@ class EventStream(PaginationStream):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
(event_chunk, max_pkey) = yield self.stream_data[i].get_rows(
|
(event_chunk, max_pkey) = yield self.stream_data[i].get_rows(
|
||||||
self.user_id, from_pkey, to_pkey, limit
|
self.user_id, from_pkey, to_pkey, limit, direction,
|
||||||
)
|
)
|
||||||
|
|
||||||
chunk += event_chunk
|
chunk.extend([
|
||||||
|
e.get_dict() if isinstance(e, SynapseEvent) else e
|
||||||
|
for e in event_chunk
|
||||||
|
])
|
||||||
next_ver.append(str(max_pkey))
|
next_ver.append(str(max_pkey))
|
||||||
|
|
||||||
defer.returnValue((chunk, EventStream.SEPARATOR.join(next_ver)))
|
defer.returnValue((chunk, EventStream.SEPARATOR.join(next_ver)))
|
||||||
@ -240,9 +190,8 @@ class EventStream(PaginationStream):
|
|||||||
Returns:
|
Returns:
|
||||||
A list of ints.
|
A list of ints.
|
||||||
"""
|
"""
|
||||||
|
if token:
|
||||||
segments = token.split(EventStream.SEPARATOR)
|
segments = token.split(EventStream.SEPARATOR)
|
||||||
try:
|
else:
|
||||||
int_segments = [int(x) for x in segments]
|
segments = [None] * len(self.stream_data)
|
||||||
except ValueError:
|
return segments
|
||||||
raise EventStreamError(400, "Bad token: %s" % token)
|
|
||||||
return int_segments
|
|
||||||
|
@ -56,7 +56,7 @@ class SynapseHomeServer(HomeServer):
|
|||||||
return File("webclient") # TODO configurable?
|
return File("webclient") # TODO configurable?
|
||||||
|
|
||||||
def build_resource_for_content_repo(self):
|
def build_resource_for_content_repo(self):
|
||||||
return ContentRepoResource("uploads", self.auth)
|
return ContentRepoResource(self, self.upload_dir, self.auth)
|
||||||
|
|
||||||
def build_db_pool(self):
|
def build_db_pool(self):
|
||||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
||||||
@ -235,8 +235,8 @@ def setup():
|
|||||||
parser.add_argument('--pid-file', dest="pid", help="When running as a "
|
parser.add_argument('--pid-file', dest="pid", help="When running as a "
|
||||||
"daemon, the file to store the pid in",
|
"daemon, the file to store the pid in",
|
||||||
default="hs.pid")
|
default="hs.pid")
|
||||||
parser.add_argument("-w", "--webclient", dest="webclient",
|
parser.add_argument("-W", "--webclient", dest="webclient", default=True,
|
||||||
action="store_true", help="Host the web client.")
|
action="store_false", help="Don't host a web client.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
verbosity = int(args.verbose) if args.verbose else None
|
verbosity = int(args.verbose) if args.verbose else None
|
||||||
@ -257,7 +257,8 @@ def setup():
|
|||||||
|
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
args.host,
|
args.host,
|
||||||
db_name=db_name
|
upload_dir=os.path.abspath("uploads"),
|
||||||
|
db_name=db_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# This object doesn't need to be saved because it's set as the handler for
|
# This object doesn't need to be saved because it's set as the handler for
|
||||||
|
@ -63,7 +63,7 @@ class FederationEventHandler(object):
|
|||||||
Deferred: Resolved when it has successfully been queued for
|
Deferred: Resolved when it has successfully been queued for
|
||||||
processing.
|
processing.
|
||||||
"""
|
"""
|
||||||
yield self._fill_out_prev_events(event)
|
yield self.fill_out_prev_events(event)
|
||||||
|
|
||||||
pdu = self.pdu_codec.pdu_from_event(event)
|
pdu = self.pdu_codec.pdu_from_event(event)
|
||||||
|
|
||||||
@ -74,10 +74,18 @@ class FederationEventHandler(object):
|
|||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def backfill(self, room_id, limit):
|
def backfill(self, dest, room_id, limit):
|
||||||
# TODO: Work out which destinations to ask for backfill
|
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
|
||||||
# self.replication_layer.backfill(dest, room_id, limit)
|
|
||||||
pass
|
if not pdus:
|
||||||
|
defer.returnValue([])
|
||||||
|
|
||||||
|
events = [
|
||||||
|
self.pdu_codec.event_from_pdu(pdu)
|
||||||
|
for pdu in pdus
|
||||||
|
]
|
||||||
|
|
||||||
|
defer.returnValue(events)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def get_state_for_room(self, destination, room_id):
|
def get_state_for_room(self, destination, room_id):
|
||||||
@ -87,7 +95,7 @@ class FederationEventHandler(object):
|
|||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_receive_pdu(self, pdu):
|
def on_receive_pdu(self, pdu, backfilled):
|
||||||
""" Called by the ReplicationLayer when we have a new pdu. We need to
|
""" Called by the ReplicationLayer when we have a new pdu. We need to
|
||||||
do auth checks and put it throught the StateHandler.
|
do auth checks and put it throught the StateHandler.
|
||||||
"""
|
"""
|
||||||
@ -95,7 +103,7 @@ class FederationEventHandler(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with (yield self.lock_manager.lock(pdu.context)):
|
with (yield self.lock_manager.lock(pdu.context)):
|
||||||
if event.is_state:
|
if event.is_state and not backfilled:
|
||||||
is_new_state = yield self.state_handler.handle_new_state(
|
is_new_state = yield self.state_handler.handle_new_state(
|
||||||
pdu
|
pdu
|
||||||
)
|
)
|
||||||
@ -104,7 +112,7 @@ class FederationEventHandler(object):
|
|||||||
else:
|
else:
|
||||||
is_new_state = False
|
is_new_state = False
|
||||||
|
|
||||||
yield self.event_handler.on_receive(event, is_new_state)
|
yield self.event_handler.on_receive(event, is_new_state, backfilled)
|
||||||
|
|
||||||
except AuthError:
|
except AuthError:
|
||||||
# TODO: Implement something in federation that allows us to
|
# TODO: Implement something in federation that allows us to
|
||||||
@ -129,7 +137,7 @@ class FederationEventHandler(object):
|
|||||||
yield self.event_handler.on_receive(new_state_event)
|
yield self.event_handler.on_receive(new_state_event)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _fill_out_prev_events(self, event):
|
def fill_out_prev_events(self, event):
|
||||||
if hasattr(event, "prev_events"):
|
if hasattr(event, "prev_events"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -209,7 +209,7 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
pdus = [Pdu(outlier=False, **p) for p in transaction.pdus]
|
pdus = [Pdu(outlier=False, **p) for p in transaction.pdus]
|
||||||
for pdu in pdus:
|
for pdu in pdus:
|
||||||
yield self._handle_new_pdu(pdu)
|
yield self._handle_new_pdu(pdu, backfilled=True)
|
||||||
|
|
||||||
defer.returnValue(pdus)
|
defer.returnValue(pdus)
|
||||||
|
|
||||||
@ -416,7 +416,7 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def _handle_new_pdu(self, pdu):
|
def _handle_new_pdu(self, pdu, backfilled=False):
|
||||||
# We reprocess pdus when we have seen them only as outliers
|
# We reprocess pdus when we have seen them only as outliers
|
||||||
existing = yield self._get_persisted_pdu(pdu.pdu_id, pdu.origin)
|
existing = yield self._get_persisted_pdu(pdu.pdu_id, pdu.origin)
|
||||||
|
|
||||||
@ -452,7 +452,10 @@ class ReplicationLayer(object):
|
|||||||
# Persist the Pdu, but don't mark it as processed yet.
|
# Persist the Pdu, but don't mark it as processed yet.
|
||||||
yield self.pdu_actions.persist_received(pdu)
|
yield self.pdu_actions.persist_received(pdu)
|
||||||
|
|
||||||
ret = yield self.handler.on_receive_pdu(pdu)
|
if not backfilled:
|
||||||
|
ret = yield self.handler.on_receive_pdu(pdu, backfilled=backfilled)
|
||||||
|
else:
|
||||||
|
ret = None
|
||||||
|
|
||||||
yield self.pdu_actions.mark_as_processed(pdu)
|
yield self.pdu_actions.mark_as_processed(pdu)
|
||||||
|
|
||||||
|
@ -24,4 +24,5 @@ class BaseHandler(object):
|
|||||||
self.notifier = hs.get_notifier()
|
self.notifier = hs.get_notifier()
|
||||||
self.room_lock = hs.get_room_lock_manager()
|
self.room_lock = hs.get_room_lock_manager()
|
||||||
self.state_handler = hs.get_state_handler()
|
self.state_handler = hs.get_state_handler()
|
||||||
|
self.distributor = hs.get_distributor()
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
@ -17,8 +17,7 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
from synapse.api.streams.event import (
|
from synapse.api.streams.event import (
|
||||||
EventStream, MessagesStreamData, RoomMemberStreamData, FeedbackStreamData,
|
EventStream, EventsStreamData
|
||||||
RoomDataStreamData
|
|
||||||
)
|
)
|
||||||
from synapse.handlers.presence import PresenceStreamData
|
from synapse.handlers.presence import PresenceStreamData
|
||||||
|
|
||||||
@ -26,10 +25,7 @@ from synapse.handlers.presence import PresenceStreamData
|
|||||||
class EventStreamHandler(BaseHandler):
|
class EventStreamHandler(BaseHandler):
|
||||||
|
|
||||||
stream_data_classes = [
|
stream_data_classes = [
|
||||||
MessagesStreamData,
|
EventsStreamData,
|
||||||
RoomMemberStreamData,
|
|
||||||
FeedbackStreamData,
|
|
||||||
RoomDataStreamData,
|
|
||||||
PresenceStreamData,
|
PresenceStreamData,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -32,10 +32,19 @@ logger = logging.getLogger(__name__)
|
|||||||
class FederationHandler(BaseHandler):
|
class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
"""Handles events that originated from federation."""
|
"""Handles events that originated from federation."""
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(FederationHandler, self).__init__(hs)
|
||||||
|
|
||||||
|
self.distributor.observe(
|
||||||
|
"user_joined_room",
|
||||||
|
self._on_user_joined
|
||||||
|
)
|
||||||
|
|
||||||
|
self.waiting_for_join_list = {}
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_receive(self, event, is_new_state):
|
def on_receive(self, event, is_new_state, backfilled):
|
||||||
if hasattr(event, "state_key") and not is_new_state:
|
if hasattr(event, "state_key") and not is_new_state:
|
||||||
logger.debug("Ignoring old state.")
|
logger.debug("Ignoring old state.")
|
||||||
return
|
return
|
||||||
@ -70,6 +79,115 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
with (yield self.room_lock.lock(event.room_id)):
|
with (yield self.room_lock.lock(event.room_id)):
|
||||||
store_id = yield self.store.persist_event(event)
|
store_id = yield self.store.persist_event(event, backfilled)
|
||||||
|
|
||||||
|
room = yield self.store.get_room(event.room_id)
|
||||||
|
|
||||||
|
if not room:
|
||||||
|
# Huh, let's try and get the current state
|
||||||
|
try:
|
||||||
|
federation = self.hs.get_federation()
|
||||||
|
yield federation.get_state_for_room(
|
||||||
|
event.origin, event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
hosts = yield self.store.get_joined_hosts_for_room(
|
||||||
|
event.room_id
|
||||||
|
)
|
||||||
|
if self.hs.hostname in hosts:
|
||||||
|
try:
|
||||||
|
yield self.store.store_room(
|
||||||
|
event.room_id,
|
||||||
|
"",
|
||||||
|
is_public=False
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to get current state for room %s",
|
||||||
|
event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not backfilled:
|
||||||
yield self.notifier.on_new_room_event(event, store_id)
|
yield self.notifier.on_new_room_event(event, store_id)
|
||||||
|
|
||||||
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
if event.membership == Membership.JOIN:
|
||||||
|
user = self.hs.parse_userid(event.target_user_id)
|
||||||
|
self.distributor.fire(
|
||||||
|
"user_joined_room", user=user, room_id=event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def backfill(self, dest, room_id, limit):
|
||||||
|
events = yield self.hs.get_federation().backfill(dest, room_id, limit)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
try:
|
||||||
|
yield self.store.persist_event(event, backfilled=True)
|
||||||
|
except:
|
||||||
|
logger.exception("Failed to persist event: %s", event)
|
||||||
|
|
||||||
|
defer.returnValue(events)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def do_invite_join(self, target_host, room_id, joinee, content):
|
||||||
|
federation = self.hs.get_federation()
|
||||||
|
|
||||||
|
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
|
if self.hs.hostname in hosts:
|
||||||
|
# We are already in the room.
|
||||||
|
logger.debug("We're already in the room apparently")
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
# First get current state to see if we are already joined.
|
||||||
|
try:
|
||||||
|
yield federation.get_state_for_room(target_host, room_id)
|
||||||
|
|
||||||
|
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
|
if self.hs.hostname in hosts:
|
||||||
|
# Oh, we were actually in the room already.
|
||||||
|
logger.debug("We're already in the room apparently")
|
||||||
|
defer.returnValue(False)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to get current state")
|
||||||
|
|
||||||
|
new_event = self.event_factory.create_event(
|
||||||
|
etype=InviteJoinEvent.TYPE,
|
||||||
|
target_host=target_host,
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=joinee,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
|
||||||
|
new_event.destinations = [target_host]
|
||||||
|
|
||||||
|
yield federation.handle_new_event(new_event)
|
||||||
|
|
||||||
|
# TODO (erikj): Time out here.
|
||||||
|
d = defer.Deferred()
|
||||||
|
self.waiting_for_join_list.setdefault((joinee, room_id), []).append(d)
|
||||||
|
yield d
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield self.store.store_room(
|
||||||
|
event.room_id,
|
||||||
|
"",
|
||||||
|
is_public=False
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def _on_user_joined(self, user, room_id):
|
||||||
|
waiters = self.waiting_for_join_list.get((user.to_string(), room_id), [])
|
||||||
|
while waiters:
|
||||||
|
waiters.pop().callback(None)
|
||||||
|
@ -685,7 +685,10 @@ class PresenceStreamData(StreamData):
|
|||||||
super(PresenceStreamData, self).__init__(hs)
|
super(PresenceStreamData, self).__init__(hs)
|
||||||
self.presence = hs.get_handlers().presence_handler
|
self.presence = hs.get_handlers().presence_handler
|
||||||
|
|
||||||
def get_rows(self, user_id, from_key, to_key, limit):
|
def get_rows(self, user_id, from_key, to_key, limit, direction):
|
||||||
|
from_key = int(from_key)
|
||||||
|
to_key = int(to_key)
|
||||||
|
|
||||||
cachemap = self.presence._user_cachemap
|
cachemap = self.presence._user_cachemap
|
||||||
|
|
||||||
# TODO(paul): limit, and filter by visibility
|
# TODO(paul): limit, and filter by visibility
|
||||||
|
@ -23,7 +23,8 @@ from synapse.api.events.room import (
|
|||||||
RoomTopicEvent, MessageEvent, InviteJoinEvent, RoomMemberEvent,
|
RoomTopicEvent, MessageEvent, InviteJoinEvent, RoomMemberEvent,
|
||||||
RoomConfigEvent
|
RoomConfigEvent
|
||||||
)
|
)
|
||||||
from synapse.api.streams.event import EventStream, MessagesStreamData
|
from synapse.api.streams.event import EventStream, EventsStreamData
|
||||||
|
from synapse.handlers.presence import PresenceStreamData
|
||||||
from synapse.util import stringutils
|
from synapse.util import stringutils
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@ -59,12 +60,14 @@ class MessageHandler(BaseHandler):
|
|||||||
yield self.auth.check_joined_room(room_id, user_id)
|
yield self.auth.check_joined_room(room_id, user_id)
|
||||||
|
|
||||||
# Pull out the message from the db
|
# Pull out the message from the db
|
||||||
msg = yield self.store.get_message(room_id=room_id,
|
# msg = yield self.store.get_message(
|
||||||
msg_id=msg_id,
|
# room_id=room_id,
|
||||||
user_id=sender_id)
|
# msg_id=msg_id,
|
||||||
|
# user_id=sender_id
|
||||||
|
# )
|
||||||
|
|
||||||
|
# TODO (erikj): Once we work out the correct c-s api we need to think on how to do this.
|
||||||
|
|
||||||
if msg:
|
|
||||||
defer.returnValue(msg)
|
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@ -114,8 +117,9 @@ class MessageHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
yield self.auth.check_joined_room(room_id, user_id)
|
yield self.auth.check_joined_room(room_id, user_id)
|
||||||
|
|
||||||
data_source = [MessagesStreamData(self.hs, room_id=room_id,
|
data_source = [
|
||||||
feedback=feedback)]
|
EventsStreamData(self.hs, room_id=room_id, feedback=feedback)
|
||||||
|
]
|
||||||
event_stream = EventStream(user_id, data_source)
|
event_stream = EventStream(user_id, data_source)
|
||||||
pagin_config = yield event_stream.fix_tokens(pagin_config)
|
pagin_config = yield event_stream.fix_tokens(pagin_config)
|
||||||
data_chunk = yield event_stream.get_chunk(config=pagin_config)
|
data_chunk = yield event_stream.get_chunk(config=pagin_config)
|
||||||
@ -141,12 +145,7 @@ class MessageHandler(BaseHandler):
|
|||||||
yield self.state_handler.handle_new_event(event)
|
yield self.state_handler.handle_new_event(event)
|
||||||
|
|
||||||
# store in db
|
# store in db
|
||||||
store_id = yield self.store.store_room_data(
|
store_id = yield self.store.persist_event(event)
|
||||||
room_id=event.room_id,
|
|
||||||
etype=event.type,
|
|
||||||
state_key=event.state_key,
|
|
||||||
content=json.dumps(event.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
event.destinations = yield self.store.get_joined_hosts_for_room(
|
event.destinations = yield self.store.get_joined_hosts_for_room(
|
||||||
event.room_id
|
event.room_id
|
||||||
@ -201,19 +200,17 @@ class MessageHandler(BaseHandler):
|
|||||||
raise RoomError(
|
raise RoomError(
|
||||||
403, "Member does not meet private room rules.")
|
403, "Member does not meet private room rules.")
|
||||||
|
|
||||||
data = yield self.store.get_room_data(room_id, event_type, state_key)
|
data = yield self.store.get_current_state(
|
||||||
|
room_id, event_type, state_key
|
||||||
|
)
|
||||||
defer.returnValue(data)
|
defer.returnValue(data)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_feedback(self, room_id=None, msg_sender_id=None, msg_id=None,
|
def get_feedback(self, event_id):
|
||||||
user_id=None, fb_sender_id=None, fb_type=None):
|
# yield self.auth.check_joined_room(room_id, user_id)
|
||||||
yield self.auth.check_joined_room(room_id, user_id)
|
|
||||||
|
|
||||||
# Pull out the feedback from the db
|
# Pull out the feedback from the db
|
||||||
fb = yield self.store.get_feedback(
|
fb = yield self.store.get_feedback(event_id)
|
||||||
room_id=room_id, msg_id=msg_id, msg_sender_id=msg_sender_id,
|
|
||||||
fb_sender_id=fb_sender_id, fb_type=fb_type
|
|
||||||
)
|
|
||||||
|
|
||||||
if fb:
|
if fb:
|
||||||
defer.returnValue(fb)
|
defer.returnValue(fb)
|
||||||
@ -260,20 +257,59 @@ class MessageHandler(BaseHandler):
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
membership_list=[Membership.INVITE, Membership.JOIN]
|
membership_list=[Membership.INVITE, Membership.JOIN]
|
||||||
)
|
)
|
||||||
for room_info in room_list:
|
|
||||||
if room_info["membership"] != Membership.JOIN:
|
rooms_ret = []
|
||||||
|
|
||||||
|
now_rooms_token = yield self.store.get_room_events_max_id()
|
||||||
|
|
||||||
|
# FIXME (erikj): Fix this.
|
||||||
|
presence_stream = PresenceStreamData(self.hs)
|
||||||
|
now_presence_token = yield presence_stream.max_token()
|
||||||
|
presence = yield presence_stream.get_rows(
|
||||||
|
user_id, 0, now_presence_token, None, None
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME (erikj): We need to not generate this token,
|
||||||
|
now_token = "%s_%s" % (now_rooms_token, now_presence_token)
|
||||||
|
|
||||||
|
for event in room_list:
|
||||||
|
d = {
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"membership": event.membership,
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.membership == Membership.INVITE:
|
||||||
|
d["inviter"] = event.user_id
|
||||||
|
|
||||||
|
rooms_ret.append(d)
|
||||||
|
|
||||||
|
if event.membership != Membership.JOIN:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
event_chunk = yield self.get_messages(
|
messages, token = yield self.store.get_recent_events_for_room(
|
||||||
user_id=user_id,
|
event.room_id,
|
||||||
pagin_config=pagin_config,
|
limit=10,
|
||||||
feedback=feedback,
|
end_token=now_rooms_token,
|
||||||
room_id=room_info["room_id"]
|
|
||||||
)
|
)
|
||||||
room_info["messages"] = event_chunk
|
|
||||||
|
d["messages"] = {
|
||||||
|
"chunk": [m.get_dict() for m in messages],
|
||||||
|
"start": token[0],
|
||||||
|
"end": token[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
current_state = yield self.store.get_current_state(event.room_id)
|
||||||
|
d["state"] = [c.get_dict() for c in current_state]
|
||||||
except:
|
except:
|
||||||
pass
|
logger.exception("Failed to get snapshot")
|
||||||
defer.returnValue(room_list)
|
|
||||||
|
user = self.hs.parse_userid(user_id)
|
||||||
|
|
||||||
|
ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token}
|
||||||
|
|
||||||
|
logger.debug("snapshot_all_rooms returning: %s", ret)
|
||||||
|
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
|
||||||
class RoomCreationHandler(BaseHandler):
|
class RoomCreationHandler(BaseHandler):
|
||||||
@ -372,7 +408,6 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
|
|
||||||
yield self.hs.get_handlers().room_member_handler.change_membership(
|
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||||
join_event,
|
join_event,
|
||||||
broadcast_msg=True,
|
|
||||||
do_auth=False
|
do_auth=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -451,11 +486,11 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
|
|
||||||
member_list = yield self.store.get_room_members(room_id=room_id)
|
member_list = yield self.store.get_room_members(room_id=room_id)
|
||||||
event_list = [
|
event_list = [
|
||||||
entry.as_event(self.event_factory).get_dict()
|
entry.get_dict()
|
||||||
for entry in member_list
|
for entry in member_list
|
||||||
]
|
]
|
||||||
chunk_data = {
|
chunk_data = {
|
||||||
"start": "START",
|
"start": "START", # FIXME (erikj): START is no longer a valid value
|
||||||
"end": "END",
|
"end": "END",
|
||||||
"chunk": event_list
|
"chunk": event_list
|
||||||
}
|
}
|
||||||
@ -484,29 +519,28 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
defer.returnValue(member)
|
defer.returnValue(member)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def change_membership(self, event=None, broadcast_msg=False, do_auth=True):
|
def change_membership(self, event=None, do_auth=True):
|
||||||
""" Change the membership status of a user in a room.
|
""" Change the membership status of a user in a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event (SynapseEvent): The membership event
|
event (SynapseEvent): The membership event
|
||||||
broadcast_msg (bool): True to inject a membership message into this
|
|
||||||
room on success.
|
|
||||||
Raises:
|
Raises:
|
||||||
SynapseError if there was a problem changing the membership.
|
SynapseError if there was a problem changing the membership.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#broadcast_msg = False
|
|
||||||
|
|
||||||
prev_state = yield self.store.get_room_member(
|
prev_state = yield self.store.get_room_member(
|
||||||
event.target_user_id, event.room_id
|
event.target_user_id, event.room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if prev_state and prev_state.membership == event.membership:
|
if prev_state:
|
||||||
# treat this event as a NOOP.
|
event.content["prev"] = prev_state.membership
|
||||||
if do_auth: # This is mainly to fix a unit test.
|
|
||||||
yield self.auth.check(event, raises=True)
|
# if prev_state and prev_state.membership == event.membership:
|
||||||
defer.returnValue({})
|
# # treat this event as a NOOP.
|
||||||
return
|
# if do_auth: # This is mainly to fix a unit test.
|
||||||
|
# yield self.auth.check(event, raises=True)
|
||||||
|
# defer.returnValue({})
|
||||||
|
# return
|
||||||
|
|
||||||
room_id = event.room_id
|
room_id = event.room_id
|
||||||
|
|
||||||
@ -514,9 +548,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
# if this HS is not currently in the room, i.e. we have to do the
|
# if this HS is not currently in the room, i.e. we have to do the
|
||||||
# invite/join dance.
|
# invite/join dance.
|
||||||
if event.membership == Membership.JOIN:
|
if event.membership == Membership.JOIN:
|
||||||
yield self._do_join(
|
yield self._do_join(event, do_auth=do_auth)
|
||||||
event, do_auth=do_auth, broadcast_msg=broadcast_msg
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# This is not a JOIN, so we can handle it normally.
|
# This is not a JOIN, so we can handle it normally.
|
||||||
if do_auth:
|
if do_auth:
|
||||||
@ -534,7 +566,6 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
yield self._do_local_membership_update(
|
yield self._do_local_membership_update(
|
||||||
event,
|
event,
|
||||||
membership=event.content["membership"],
|
membership=event.content["membership"],
|
||||||
broadcast_msg=broadcast_msg,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue({"room_id": room_id})
|
defer.returnValue({"room_id": room_id})
|
||||||
@ -569,14 +600,14 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
defer.returnValue({"room_id": room_id})
|
defer.returnValue({"room_id": room_id})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_join(self, event, room_host=None, do_auth=True, broadcast_msg=True):
|
def _do_join(self, event, room_host=None, do_auth=True):
|
||||||
joinee = self.hs.parse_userid(event.target_user_id)
|
joinee = self.hs.parse_userid(event.target_user_id)
|
||||||
# room_id = RoomID.from_string(event.room_id, self.hs)
|
# room_id = RoomID.from_string(event.room_id, self.hs)
|
||||||
room_id = event.room_id
|
room_id = event.room_id
|
||||||
|
|
||||||
# If event doesn't include a display name, add one.
|
# If event doesn't include a display name, add one.
|
||||||
yield self._fill_out_join_content(
|
yield self.distributor.fire(
|
||||||
joinee, event.content
|
"collect_presencelike_data", joinee, event.content
|
||||||
)
|
)
|
||||||
|
|
||||||
# XXX: We don't do an auth check if we are doing an invite
|
# XXX: We don't do an auth check if we are doing an invite
|
||||||
@ -584,9 +615,9 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
# that we are allowed to join when we decide whether or not we
|
# that we are allowed to join when we decide whether or not we
|
||||||
# need to do the invite/join dance.
|
# need to do the invite/join dance.
|
||||||
|
|
||||||
room = yield self.store.get_room(room_id)
|
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
|
|
||||||
if room:
|
if self.hs.hostname in hosts:
|
||||||
should_do_dance = False
|
should_do_dance = False
|
||||||
elif room_host:
|
elif room_host:
|
||||||
should_do_dance = True
|
should_do_dance = True
|
||||||
@ -598,7 +629,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
if prev_state and prev_state.membership == Membership.INVITE:
|
if prev_state and prev_state.membership == Membership.INVITE:
|
||||||
room = yield self.store.get_room(room_id)
|
room = yield self.store.get_room(room_id)
|
||||||
inviter = UserID.from_string(
|
inviter = UserID.from_string(
|
||||||
prev_state.sender, self.hs
|
prev_state.user_id, self.hs
|
||||||
)
|
)
|
||||||
|
|
||||||
should_do_dance = not inviter.is_mine and not room
|
should_do_dance = not inviter.is_mine and not room
|
||||||
@ -606,8 +637,15 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
should_do_dance = False
|
should_do_dance = False
|
||||||
|
|
||||||
|
have_joined = False
|
||||||
|
if should_do_dance:
|
||||||
|
handler = self.hs.get_handlers().federation_handler
|
||||||
|
have_joined = yield handler.do_invite_join(
|
||||||
|
room_host, room_id, event.user_id, event.content
|
||||||
|
)
|
||||||
|
|
||||||
# We want to do the _do_update inside the room lock.
|
# We want to do the _do_update inside the room lock.
|
||||||
if not should_do_dance:
|
if not have_joined:
|
||||||
logger.debug("Doing normal join")
|
logger.debug("Doing normal join")
|
||||||
|
|
||||||
if do_auth:
|
if do_auth:
|
||||||
@ -617,16 +655,6 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
yield self._do_local_membership_update(
|
yield self._do_local_membership_update(
|
||||||
event,
|
event,
|
||||||
membership=event.content["membership"],
|
membership=event.content["membership"],
|
||||||
broadcast_msg=broadcast_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if should_do_dance:
|
|
||||||
yield self._do_invite_join_dance(
|
|
||||||
room_id=room_id,
|
|
||||||
joinee=event.user_id,
|
|
||||||
target_host=room_host,
|
|
||||||
content=event.content,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user = self.hs.parse_userid(event.user_id)
|
user = self.hs.parse_userid(event.user_id)
|
||||||
@ -634,32 +662,6 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
"user_joined_room", user=user, room_id=room_id
|
"user_joined_room", user=user, room_id=room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _fill_out_join_content(self, user_id, content):
|
|
||||||
# If event doesn't include a display name, add one.
|
|
||||||
profile_handler = self.hs.get_handlers().profile_handler
|
|
||||||
if "displayname" not in content:
|
|
||||||
try:
|
|
||||||
display_name = yield profile_handler.get_displayname(
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if display_name:
|
|
||||||
content["displayname"] = display_name
|
|
||||||
except:
|
|
||||||
logger.exception("Failed to set display_name")
|
|
||||||
|
|
||||||
if "avatar_url" not in content:
|
|
||||||
try:
|
|
||||||
avatar_url = yield profile_handler.get_avatar_url(
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if avatar_url:
|
|
||||||
content["avatar_url"] = avatar_url
|
|
||||||
except:
|
|
||||||
logger.exception("Failed to set display_name")
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _should_invite_join(self, room_id, prev_state, do_auth):
|
def _should_invite_join(self, room_id, prev_state, do_auth):
|
||||||
logger.debug("_should_invite_join: room_id: %s", room_id)
|
logger.debug("_should_invite_join: room_id: %s", room_id)
|
||||||
@ -694,18 +696,12 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
user_id=user.to_string(), membership_list=membership_list
|
user_id=user.to_string(), membership_list=membership_list
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue([r["room_id"] for r in rooms])
|
defer.returnValue([r.room_id for r in rooms])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_local_membership_update(self, event, membership, broadcast_msg):
|
def _do_local_membership_update(self, event, membership):
|
||||||
# store membership
|
# store membership
|
||||||
store_id = yield self.store.store_room_member(
|
store_id = yield self.store.persist_event(event)
|
||||||
user_id=event.target_user_id,
|
|
||||||
sender=event.user_id,
|
|
||||||
room_id=event.room_id,
|
|
||||||
content=event.content,
|
|
||||||
membership=membership
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send a PDU to all hosts who have joined the room.
|
# Send a PDU to all hosts who have joined the room.
|
||||||
destinations = yield self.store.get_joined_hosts_for_room(
|
destinations = yield self.store.get_joined_hosts_for_room(
|
||||||
@ -732,78 +728,11 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
yield self.hs.get_federation().handle_new_event(event)
|
yield self.hs.get_federation().handle_new_event(event)
|
||||||
self.notifier.on_new_room_event(event, store_id)
|
self.notifier.on_new_room_event(event, store_id)
|
||||||
|
|
||||||
if broadcast_msg:
|
|
||||||
yield self._inject_membership_msg(
|
|
||||||
source=event.user_id,
|
|
||||||
target=event.target_user_id,
|
|
||||||
room_id=event.room_id,
|
|
||||||
membership=event.content["membership"]
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _do_invite_join_dance(self, room_id, joinee, target_host, content):
|
|
||||||
logger.debug("Doing remote join dance")
|
|
||||||
|
|
||||||
# do invite join dance
|
|
||||||
federation = self.hs.get_federation()
|
|
||||||
new_event = self.event_factory.create_event(
|
|
||||||
etype=InviteJoinEvent.TYPE,
|
|
||||||
target_host=target_host,
|
|
||||||
room_id=room_id,
|
|
||||||
user_id=joinee,
|
|
||||||
content=content
|
|
||||||
)
|
|
||||||
|
|
||||||
new_event.destinations = [target_host]
|
|
||||||
|
|
||||||
yield self.store.store_room(
|
|
||||||
room_id, "", is_public=False
|
|
||||||
)
|
|
||||||
|
|
||||||
#yield self.state_handler.handle_new_event(event)
|
|
||||||
yield federation.handle_new_event(new_event)
|
|
||||||
yield federation.get_state_for_room(
|
|
||||||
target_host, room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _inject_membership_msg(self, room_id=None, source=None, target=None,
|
|
||||||
membership=None):
|
|
||||||
# TODO this should be a different type of message, not m.text
|
|
||||||
if membership == Membership.INVITE:
|
|
||||||
body = "%s invited %s to the room." % (source, target)
|
|
||||||
elif membership == Membership.JOIN:
|
|
||||||
body = "%s joined the room." % (target)
|
|
||||||
elif membership == Membership.LEAVE:
|
|
||||||
body = "%s left the room." % (target)
|
|
||||||
else:
|
|
||||||
raise RoomError(500, "Unknown membership value %s" % membership)
|
|
||||||
|
|
||||||
membership_json = {
|
|
||||||
"msgtype": u"m.text",
|
|
||||||
"body": body,
|
|
||||||
"membership_source": source,
|
|
||||||
"membership_target": target,
|
|
||||||
"membership": membership,
|
|
||||||
}
|
|
||||||
|
|
||||||
msg_id = "m%s" % int(self.clock.time_msec())
|
|
||||||
|
|
||||||
event = self.event_factory.create_event(
|
|
||||||
etype=MessageEvent.TYPE,
|
|
||||||
room_id=room_id,
|
|
||||||
user_id="_homeserver_",
|
|
||||||
msg_id=msg_id,
|
|
||||||
content=membership_json
|
|
||||||
)
|
|
||||||
|
|
||||||
handler = self.hs.get_handlers().message_handler
|
|
||||||
yield handler.send_message(event, suppress_auth=True)
|
|
||||||
|
|
||||||
|
|
||||||
class RoomListHandler(BaseHandler):
|
class RoomListHandler(BaseHandler):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_public_room_list(self):
|
def get_public_room_list(self):
|
||||||
chunk = yield self.store.get_rooms(is_public=True, with_topics=True)
|
chunk = yield self.store.get_rooms(is_public=True)
|
||||||
|
# FIXME (erikj): START is no longer a valid value
|
||||||
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
||||||
|
@ -212,8 +212,9 @@ class ContentRepoResource(resource.Resource):
|
|||||||
"""
|
"""
|
||||||
isLeaf = True
|
isLeaf = True
|
||||||
|
|
||||||
def __init__(self, directory, auth):
|
def __init__(self, hs, directory, auth):
|
||||||
resource.Resource.__init__(self)
|
resource.Resource.__init__(self)
|
||||||
|
self.hs = hs
|
||||||
self.directory = directory
|
self.directory = directory
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
|
|
||||||
@ -250,7 +251,8 @@ class ContentRepoResource(resource.Resource):
|
|||||||
file_ext = re.sub("[^a-z]", "", file_ext)
|
file_ext = re.sub("[^a-z]", "", file_ext)
|
||||||
suffix += "." + file_ext
|
suffix += "." + file_ext
|
||||||
|
|
||||||
file_path = os.path.join(self.directory, prefix + main_part + suffix)
|
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",
|
logger.info("User %s is uploading a file to path %s",
|
||||||
auth_user.to_string(),
|
auth_user.to_string(),
|
||||||
file_path)
|
file_path)
|
||||||
@ -259,8 +261,8 @@ class ContentRepoResource(resource.Resource):
|
|||||||
attempts = 0
|
attempts = 0
|
||||||
while os.path.exists(file_path):
|
while os.path.exists(file_path):
|
||||||
main_part = random_string(24)
|
main_part = random_string(24)
|
||||||
file_path = os.path.join(self.directory,
|
file_name = prefix + main_part + suffix
|
||||||
prefix + main_part + suffix)
|
file_path = os.path.join(self.directory, file_name)
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if attempts > 25: # really? Really?
|
if attempts > 25: # really? Really?
|
||||||
raise SynapseError(500, "Unable to create file.")
|
raise SynapseError(500, "Unable to create file.")
|
||||||
@ -272,11 +274,14 @@ class ContentRepoResource(resource.Resource):
|
|||||||
# servers.
|
# servers.
|
||||||
|
|
||||||
# TODO: A little crude here, we could do this better.
|
# TODO: A little crude here, we could do this better.
|
||||||
filename = request.path.split(self.directory + "/")[1]
|
filename = request.path.split('/')[-1]
|
||||||
# be paranoid
|
# be paranoid
|
||||||
filename = re.sub("[^0-9A-z.-_]", "", filename)
|
filename = re.sub("[^0-9A-z.-_]", "", filename)
|
||||||
|
|
||||||
file_path = self.directory + "/" + filename
|
file_path = self.directory + "/" + filename
|
||||||
|
|
||||||
|
logger.debug("Searching for %s", file_path)
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
# filename has the content type
|
# filename has the content type
|
||||||
base64_contentype = filename.split(".")[1]
|
base64_contentype = filename.split(".")[1]
|
||||||
@ -304,6 +309,10 @@ class ContentRepoResource(resource.Resource):
|
|||||||
self._async_render(request)
|
self._async_render(request)
|
||||||
return server.NOT_DONE_YET
|
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
|
@defer.inlineCallbacks
|
||||||
def _async_render(self, request):
|
def _async_render(self, request):
|
||||||
try:
|
try:
|
||||||
@ -313,8 +322,13 @@ class ContentRepoResource(resource.Resource):
|
|||||||
with open(fname, "wb") as f:
|
with open(fname, "wb") as f:
|
||||||
f.write(request.content.read())
|
f.write(request.content.read())
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME (erikj): These should use constants.
|
||||||
|
file_name = os.path.basename(fname)
|
||||||
|
url = "http://%s/matrix/content/%s" % (self.hs.hostname, file_name)
|
||||||
|
|
||||||
respond_with_json_bytes(request, 200,
|
respond_with_json_bytes(request, 200,
|
||||||
json.dumps({"content_token": fname}),
|
json.dumps({"content_token": url}),
|
||||||
send_cors=True)
|
send_cors=True)
|
||||||
|
|
||||||
except CodeMessageException as e:
|
except CodeMessageException as e:
|
||||||
|
@ -115,7 +115,7 @@ class RoomTopicRestServlet(RestServlet):
|
|||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
raise SynapseError(404, "Topic not found.", errcode=Codes.NOT_FOUND)
|
raise SynapseError(404, "Topic not found.", errcode=Codes.NOT_FOUND)
|
||||||
defer.returnValue((200, json.loads(data.content)))
|
defer.returnValue((200, data.content))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_PUT(self, request, room_id):
|
def on_PUT(self, request, room_id):
|
||||||
@ -177,7 +177,7 @@ class RoomMemberRestServlet(RestServlet):
|
|||||||
if not member:
|
if not member:
|
||||||
raise SynapseError(404, "Member not found.",
|
raise SynapseError(404, "Member not found.",
|
||||||
errcode=Codes.NOT_FOUND)
|
errcode=Codes.NOT_FOUND)
|
||||||
defer.returnValue((200, json.loads(member.content)))
|
defer.returnValue((200, member.content))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_DELETE(self, request, roomid, target_user_id):
|
def on_DELETE(self, request, roomid, target_user_id):
|
||||||
@ -193,7 +193,7 @@ class RoomMemberRestServlet(RestServlet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
handler = self.handlers.room_member_handler
|
handler = self.handlers.room_member_handler
|
||||||
yield handler.change_membership(event, broadcast_msg=True)
|
yield handler.change_membership(event)
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, ""))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@ -220,7 +220,7 @@ class RoomMemberRestServlet(RestServlet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
handler = self.handlers.room_member_handler
|
handler = self.handlers.room_member_handler
|
||||||
yield handler.change_membership(event, broadcast_msg=True)
|
yield handler.change_membership(event)
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, ""))
|
||||||
|
|
||||||
|
|
||||||
@ -287,25 +287,28 @@ class FeedbackRestServlet(RestServlet):
|
|||||||
feedback_type):
|
feedback_type):
|
||||||
user = yield (self.auth.get_user_by_req(request))
|
user = yield (self.auth.get_user_by_req(request))
|
||||||
|
|
||||||
if feedback_type not in Feedback.LIST:
|
# TODO (erikj): Implement this?
|
||||||
raise SynapseError(400, "Bad feedback type.",
|
raise NotImplementedError("Getting feedback is not supported")
|
||||||
errcode=Codes.BAD_JSON)
|
|
||||||
|
|
||||||
msg_handler = self.handlers.message_handler
|
# if feedback_type not in Feedback.LIST:
|
||||||
feedback = yield msg_handler.get_feedback(
|
# raise SynapseError(400, "Bad feedback type.",
|
||||||
room_id=urllib.unquote(room_id),
|
# errcode=Codes.BAD_JSON)
|
||||||
msg_sender_id=msg_sender_id,
|
#
|
||||||
msg_id=msg_id,
|
# msg_handler = self.handlers.message_handler
|
||||||
user_id=user.to_string(),
|
# feedback = yield msg_handler.get_feedback(
|
||||||
fb_sender_id=fb_sender_id,
|
# room_id=urllib.unquote(room_id),
|
||||||
fb_type=feedback_type
|
# msg_sender_id=msg_sender_id,
|
||||||
)
|
# msg_id=msg_id,
|
||||||
|
# user_id=user.to_string(),
|
||||||
if not feedback:
|
# fb_sender_id=fb_sender_id,
|
||||||
raise SynapseError(404, "Feedback not found.",
|
# fb_type=feedback_type
|
||||||
errcode=Codes.NOT_FOUND)
|
# )
|
||||||
|
#
|
||||||
defer.returnValue((200, json.loads(feedback.content)))
|
# if not feedback:
|
||||||
|
# raise SynapseError(404, "Feedback not found.",
|
||||||
|
# errcode=Codes.NOT_FOUND)
|
||||||
|
#
|
||||||
|
# defer.returnValue((200, json.loads(feedback.content)))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_PUT(self, request, room_id, sender_id, msg_id, fb_sender_id,
|
def on_PUT(self, request, room_id, sender_id, msg_id, fb_sender_id,
|
||||||
@ -382,6 +385,21 @@ class RoomMessageListRestServlet(RestServlet):
|
|||||||
defer.returnValue((200, msgs))
|
defer.returnValue((200, msgs))
|
||||||
|
|
||||||
|
|
||||||
|
class RoomTriggerBackfill(RestServlet):
|
||||||
|
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$")
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request, room_id):
|
||||||
|
remote_server = urllib.unquote(request.args["remote"][0])
|
||||||
|
room_id = urllib.unquote(room_id)
|
||||||
|
limit = int(request.args["limit"][0])
|
||||||
|
|
||||||
|
handler = self.handlers.federation_handler
|
||||||
|
events = yield handler.backfill(remote_server, room_id, limit)
|
||||||
|
|
||||||
|
res = [event.get_dict() for event in events]
|
||||||
|
defer.returnValue((200, res))
|
||||||
|
|
||||||
def _parse_json(request):
|
def _parse_json(request):
|
||||||
try:
|
try:
|
||||||
content = json.loads(request.content.read())
|
content = json.loads(request.content.read())
|
||||||
@ -402,3 +420,4 @@ def register_servlets(hs, http_server):
|
|||||||
RoomMemberListRestServlet(hs).register(http_server)
|
RoomMemberListRestServlet(hs).register(http_server)
|
||||||
RoomMessageListRestServlet(hs).register(http_server)
|
RoomMessageListRestServlet(hs).register(http_server)
|
||||||
JoinRoomAliasServlet(hs).register(http_server)
|
JoinRoomAliasServlet(hs).register(http_server)
|
||||||
|
RoomTriggerBackfill(hs).register(http_server)
|
||||||
|
@ -159,7 +159,7 @@ class HomeServer(BaseHomeServer):
|
|||||||
return DataStore(self)
|
return DataStore(self)
|
||||||
|
|
||||||
def build_event_factory(self):
|
def build_event_factory(self):
|
||||||
return EventFactory()
|
return EventFactory(self)
|
||||||
|
|
||||||
def build_handlers(self):
|
def build_handlers(self):
|
||||||
return Handlers(self)
|
return Handlers(self)
|
||||||
|
@ -86,7 +86,7 @@ class StateHandler(object):
|
|||||||
else:
|
else:
|
||||||
event.depth = 0
|
event.depth = 0
|
||||||
|
|
||||||
current_state = yield self.store.get_current_state(
|
current_state = yield self.store.get_current_state_pdu(
|
||||||
key.context, key.type, key.state_key
|
key.context, key.type, key.state_key
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,30 +13,35 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
RoomMemberEvent, MessageEvent, RoomTopicEvent, FeedbackEvent,
|
RoomMemberEvent, MessageEvent, RoomTopicEvent, FeedbackEvent,
|
||||||
RoomConfigEvent
|
RoomConfigEvent, RoomNameEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
from .directory import DirectoryStore
|
from .directory import DirectoryStore
|
||||||
from .feedback import FeedbackStore
|
from .feedback import FeedbackStore
|
||||||
from .message import MessageStore
|
|
||||||
from .presence import PresenceStore
|
from .presence import PresenceStore
|
||||||
from .profile import ProfileStore
|
from .profile import ProfileStore
|
||||||
from .registration import RegistrationStore
|
from .registration import RegistrationStore
|
||||||
from .room import RoomStore
|
from .room import RoomStore
|
||||||
from .roommember import RoomMemberStore
|
from .roommember import RoomMemberStore
|
||||||
from .roomdata import RoomDataStore
|
|
||||||
from .stream import StreamStore
|
from .stream import StreamStore
|
||||||
from .pdu import StatePduStore, PduStore
|
from .pdu import StatePduStore, PduStore
|
||||||
from .transactions import TransactionStore
|
from .transactions import TransactionStore
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class DataStore(RoomDataStore, RoomMemberStore, MessageStore, RoomStore,
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DataStore(RoomMemberStore, RoomStore,
|
||||||
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
|
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
|
||||||
PresenceStore, PduStore, StatePduStore, TransactionStore,
|
PresenceStore, PduStore, StatePduStore, TransactionStore,
|
||||||
DirectoryStore):
|
DirectoryStore):
|
||||||
@ -44,51 +49,147 @@ class DataStore(RoomDataStore, RoomMemberStore, MessageStore, RoomStore,
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(DataStore, self).__init__(hs)
|
super(DataStore, self).__init__(hs)
|
||||||
self.event_factory = hs.get_event_factory()
|
self.event_factory = hs.get_event_factory()
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
def persist_event(self, event):
|
self.min_token_deferred = self._get_min_token()
|
||||||
if event.type == MessageEvent.TYPE:
|
self.min_token = None
|
||||||
return self.store_message(
|
|
||||||
user_id=event.user_id,
|
@defer.inlineCallbacks
|
||||||
room_id=event.room_id,
|
@log_function
|
||||||
msg_id=event.msg_id,
|
def persist_event(self, event, backfilled=False):
|
||||||
content=json.dumps(event.content)
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
)
|
yield self._store_room_member(event)
|
||||||
elif event.type == RoomMemberEvent.TYPE:
|
|
||||||
return self.store_room_member(
|
|
||||||
user_id=event.target_user_id,
|
|
||||||
sender=event.user_id,
|
|
||||||
room_id=event.room_id,
|
|
||||||
content=event.content,
|
|
||||||
membership=event.content["membership"]
|
|
||||||
)
|
|
||||||
elif event.type == FeedbackEvent.TYPE:
|
elif event.type == FeedbackEvent.TYPE:
|
||||||
return self.store_feedback(
|
yield self._store_feedback(event)
|
||||||
room_id=event.room_id,
|
# elif event.type == RoomConfigEvent.TYPE:
|
||||||
msg_id=event.msg_id,
|
# yield self._store_room_config(event)
|
||||||
msg_sender_id=event.msg_sender_id,
|
elif event.type == RoomNameEvent.TYPE:
|
||||||
fb_sender_id=event.user_id,
|
yield self._store_room_name(event)
|
||||||
fb_type=event.feedback_type,
|
|
||||||
content=json.dumps(event.content)
|
|
||||||
)
|
|
||||||
elif event.type == RoomTopicEvent.TYPE:
|
elif event.type == RoomTopicEvent.TYPE:
|
||||||
return self.store_room_data(
|
yield self._store_room_topic(event)
|
||||||
room_id=event.room_id,
|
|
||||||
etype=event.type,
|
ret = yield self._store_event(event, backfilled)
|
||||||
state_key=event.state_key,
|
defer.returnValue(ret)
|
||||||
content=json.dumps(event.content)
|
|
||||||
)
|
@defer.inlineCallbacks
|
||||||
elif event.type == RoomConfigEvent.TYPE:
|
def get_event(self, event_id):
|
||||||
if "visibility" in event.content:
|
events_dict = yield self._simple_select_one(
|
||||||
visibility = event.content["visibility"]
|
"events",
|
||||||
return self.store_room_config(
|
{"event_id": event_id},
|
||||||
room_id=event.room_id,
|
[
|
||||||
visibility=visibility
|
"event_id",
|
||||||
|
"type",
|
||||||
|
"sender",
|
||||||
|
"room_id",
|
||||||
|
"content",
|
||||||
|
"unrecognized_keys"
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event = self._parse_event_from_row(events_dict)
|
||||||
|
defer.returnValue(event)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def _store_event(self, event, backfilled):
|
||||||
|
# FIXME (erikj): This should be removed when we start amalgamating
|
||||||
|
# event and pdu storage
|
||||||
|
yield self.hs.get_federation().fill_out_prev_events(event)
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
"topological_ordering": event.depth,
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"type": event.type,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"content": json.dumps(event.content),
|
||||||
|
"processed": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasattr(event, "outlier"):
|
||||||
|
vals["outlier"] = event.outlier
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
vals["outlier"] = False
|
||||||
"Don't know how to persist type=%s" % event.type
|
|
||||||
|
if backfilled:
|
||||||
|
if not self.min_token_deferred.called:
|
||||||
|
yield self.min_token_deferred
|
||||||
|
self.min_token -= 1
|
||||||
|
vals["stream_ordering"] = self.min_token
|
||||||
|
|
||||||
|
unrec = {
|
||||||
|
k: v
|
||||||
|
for k, v in event.get_full_dict().items()
|
||||||
|
if k not in vals.keys()
|
||||||
|
}
|
||||||
|
vals["unrecognized_keys"] = json.dumps(unrec)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield self._simple_insert("events", vals)
|
||||||
|
except:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to persist, probably duplicate: %s",
|
||||||
|
event.event_id
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not backfilled and hasattr(event, "state_key"):
|
||||||
|
vals = {
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"type": event.type,
|
||||||
|
"state_key": event.state_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasattr(event, "prev_state"):
|
||||||
|
vals["prev_state"] = event.prev_state
|
||||||
|
|
||||||
|
yield self._simple_insert("state_events", vals)
|
||||||
|
|
||||||
|
yield self._simple_insert(
|
||||||
|
"current_state_events",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"type": event.type,
|
||||||
|
"state_key": event.state_key,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
latest = yield self.get_room_events_max_id()
|
||||||
|
defer.returnValue(latest)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_current_state(self, room_id, event_type=None, state_key=""):
|
||||||
|
sql = (
|
||||||
|
"SELECT e.* FROM events as e "
|
||||||
|
"INNER JOIN current_state_events as c ON e.event_id = c.event_id "
|
||||||
|
"INNER JOIN state_events as s ON e.event_id = s.event_id "
|
||||||
|
"WHERE c.room_id = ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
if event_type:
|
||||||
|
sql += " AND s.type = ? AND s.state_key = ? "
|
||||||
|
args = (room_id, event_type, state_key)
|
||||||
|
else:
|
||||||
|
args = (room_id, )
|
||||||
|
|
||||||
|
results = yield self._execute_and_decode(sql, *args)
|
||||||
|
|
||||||
|
defer.returnValue([self._parse_event_from_row(r) for r in results])
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _get_min_token(self):
|
||||||
|
row = yield self._execute(
|
||||||
|
None,
|
||||||
|
"SELECT MIN(stream_ordering) FROM events"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.min_token = row[0][0] if row and row[0] and row[0][0] else -1
|
||||||
|
self.min_token = min(self.min_token, -1)
|
||||||
|
|
||||||
|
logger.debug("min_token is: %s", self.min_token)
|
||||||
|
|
||||||
|
defer.returnValue(self.min_token)
|
||||||
|
|
||||||
|
|
||||||
def schema_path(schema):
|
def schema_path(schema):
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
@ -20,6 +19,9 @@ from twisted.internet import defer
|
|||||||
from synapse.api.errors import StoreError
|
from synapse.api.errors import StoreError
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ class SQLBaseStore(object):
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self._db_pool = hs.get_db_pool()
|
self._db_pool = hs.get_db_pool()
|
||||||
|
self.event_factory = hs.get_event_factory()
|
||||||
self._clock = hs.get_clock()
|
self._clock = hs.get_clock()
|
||||||
|
|
||||||
def cursor_to_dict(self, cursor):
|
def cursor_to_dict(self, cursor):
|
||||||
@ -57,14 +60,22 @@ class SQLBaseStore(object):
|
|||||||
The result of decoder(results)
|
The result of decoder(results)
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[SQL] %s Args=%s Func=%s", query, args, decoder.__name__
|
"[SQL] %s Args=%s Func=%s",
|
||||||
|
query, args, decoder.__name__ if decoder else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def interaction(txn):
|
def interaction(txn):
|
||||||
cursor = txn.execute(query, args)
|
cursor = txn.execute(query, args)
|
||||||
|
if decoder:
|
||||||
return decoder(cursor)
|
return decoder(cursor)
|
||||||
|
else:
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
return self._db_pool.runInteraction(interaction)
|
return self._db_pool.runInteraction(interaction)
|
||||||
|
|
||||||
|
def _execute_and_decode(self, query, *args):
|
||||||
|
return self._execute(self.cursor_to_dict, query, *args)
|
||||||
|
|
||||||
# "Simple" SQL API methods that operate on a single table with no JOINs,
|
# "Simple" SQL API methods that operate on a single table with no JOINs,
|
||||||
# no complex WHERE clauses, just a dict of values for columns.
|
# no complex WHERE clauses, just a dict of values for columns.
|
||||||
|
|
||||||
@ -281,6 +292,22 @@ class SQLBaseStore(object):
|
|||||||
|
|
||||||
return self._db_pool.runInteraction(func)
|
return self._db_pool.runInteraction(func)
|
||||||
|
|
||||||
|
def _parse_event_from_row(self, row_dict):
|
||||||
|
d = copy.deepcopy({k: v for k, v in row_dict.items() if v})
|
||||||
|
|
||||||
|
d.pop("stream_ordering", None)
|
||||||
|
d.pop("topological_ordering", None)
|
||||||
|
d.pop("processed", None)
|
||||||
|
|
||||||
|
d.update(json.loads(row_dict["unrecognized_keys"]))
|
||||||
|
d["content"] = json.loads(d["content"])
|
||||||
|
del d["unrecognized_keys"]
|
||||||
|
|
||||||
|
return self.event_factory.create_event(
|
||||||
|
etype=d["type"],
|
||||||
|
**d
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Table(object):
|
class Table(object):
|
||||||
""" A base class used to store information about a particular table.
|
""" A base class used to store information about a particular table.
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
from ._base import SQLBaseStore, Table
|
from ._base import SQLBaseStore, Table
|
||||||
from synapse.api.events.room import FeedbackEvent
|
from synapse.api.events.room import FeedbackEvent
|
||||||
|
|
||||||
@ -22,54 +24,28 @@ import json
|
|||||||
|
|
||||||
class FeedbackStore(SQLBaseStore):
|
class FeedbackStore(SQLBaseStore):
|
||||||
|
|
||||||
def store_feedback(self, room_id, msg_id, msg_sender_id,
|
def _store_feedback(self, event):
|
||||||
fb_sender_id, fb_type, content):
|
return self._simple_insert("feedback", {
|
||||||
return self._simple_insert(FeedbackTable.table_name, dict(
|
"event_id": event.event_id,
|
||||||
room_id=room_id,
|
"feedback_type": event.feedback_type,
|
||||||
msg_id=msg_id,
|
"room_id": event.room_id,
|
||||||
msg_sender_id=msg_sender_id,
|
"target_event_id": event.target_event,
|
||||||
fb_sender_id=fb_sender_id,
|
"sender": event.user_id,
|
||||||
fb_type=fb_type,
|
})
|
||||||
content=content,
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_feedback(self, room_id=None, msg_id=None, msg_sender_id=None,
|
@defer.inlineCallbacks
|
||||||
fb_sender_id=None, fb_type=None):
|
def get_feedback_for_event(self, event_id):
|
||||||
query = FeedbackTable.select_statement(
|
sql = (
|
||||||
"msg_sender_id = ? AND room_id = ? AND msg_id = ? " +
|
"SELECT events.* FROM events INNER JOIN feedback "
|
||||||
"AND fb_sender_id = ? AND feedback_type = ? " +
|
"ON events.event_id = feedback.event_id "
|
||||||
"ORDER BY id DESC LIMIT 1")
|
"WHERE feedback.target_event_id = ? "
|
||||||
return self._execute(
|
|
||||||
FeedbackTable.decode_single_result,
|
|
||||||
query, msg_sender_id, room_id, msg_id, fb_sender_id, fb_type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_max_feedback_id(self):
|
rows = yield self._execute_and_decode(sql, event_id)
|
||||||
return self._simple_max_id(FeedbackTable.table_name)
|
|
||||||
|
|
||||||
|
defer.returnValue(
|
||||||
class FeedbackTable(Table):
|
[
|
||||||
table_name = "feedback"
|
self._parse_event_from_row(r)
|
||||||
|
for r in rows
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"content",
|
|
||||||
"feedback_type",
|
|
||||||
"fb_sender_id",
|
|
||||||
"msg_id",
|
|
||||||
"room_id",
|
|
||||||
"msg_sender_id"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
class EntryType(collections.namedtuple("FeedbackEntry", fields)):
|
|
||||||
|
|
||||||
def as_event(self, event_factory):
|
|
||||||
return event_factory.create_event(
|
|
||||||
etype=FeedbackEvent.TYPE,
|
|
||||||
room_id=self.room_id,
|
|
||||||
msg_id=self.msg_id,
|
|
||||||
msg_sender_id=self.msg_sender_id,
|
|
||||||
user_id=self.fb_sender_id,
|
|
||||||
feedback_type=self.feedback_type,
|
|
||||||
content=json.loads(self.content),
|
|
||||||
)
|
)
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
# -*- 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 SQLBaseStore, Table
|
|
||||||
from synapse.api.events.room import MessageEvent
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class MessageStore(SQLBaseStore):
|
|
||||||
|
|
||||||
def get_message(self, user_id, room_id, msg_id):
|
|
||||||
"""Get a message from the store.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): The ID of the user who sent the message.
|
|
||||||
room_id (str): The room the message was sent in.
|
|
||||||
msg_id (str): The unique ID for this user/room combo.
|
|
||||||
"""
|
|
||||||
query = MessagesTable.select_statement(
|
|
||||||
"user_id = ? AND room_id = ? AND msg_id = ? " +
|
|
||||||
"ORDER BY id DESC LIMIT 1")
|
|
||||||
return self._execute(
|
|
||||||
MessagesTable.decode_single_result,
|
|
||||||
query, user_id, room_id, msg_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def store_message(self, user_id, room_id, msg_id, content):
|
|
||||||
"""Store a message in the store.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): The ID of the user who sent the message.
|
|
||||||
room_id (str): The room the message was sent in.
|
|
||||||
msg_id (str): The unique ID for this user/room combo.
|
|
||||||
content (str): The content of the message (JSON)
|
|
||||||
"""
|
|
||||||
return self._simple_insert(MessagesTable.table_name, dict(
|
|
||||||
user_id=user_id,
|
|
||||||
room_id=room_id,
|
|
||||||
msg_id=msg_id,
|
|
||||||
content=content,
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_max_message_id(self):
|
|
||||||
return self._simple_max_id(MessagesTable.table_name)
|
|
||||||
|
|
||||||
|
|
||||||
class MessagesTable(Table):
|
|
||||||
table_name = "messages"
|
|
||||||
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"user_id",
|
|
||||||
"room_id",
|
|
||||||
"msg_id",
|
|
||||||
"content"
|
|
||||||
]
|
|
||||||
|
|
||||||
class EntryType(collections.namedtuple("MessageEntry", fields)):
|
|
||||||
|
|
||||||
def as_event(self, event_factory):
|
|
||||||
return event_factory.create_event(
|
|
||||||
etype=MessageEvent.TYPE,
|
|
||||||
room_id=self.room_id,
|
|
||||||
user_id=self.user_id,
|
|
||||||
msg_id=self.msg_id,
|
|
||||||
content=json.loads(self.content),
|
|
||||||
)
|
|
@ -13,6 +13,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
from ._base import SQLBaseStore, Table, JoinHelper
|
from ._base import SQLBaseStore, Table, JoinHelper
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
@ -319,6 +321,7 @@ class PduStore(SQLBaseStore):
|
|||||||
|
|
||||||
return [(row[0], row[1], row[2]) for row in results]
|
return [(row[0], row[1], row[2]) for row in results]
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def get_oldest_pdus_in_context(self, context):
|
def get_oldest_pdus_in_context(self, context):
|
||||||
"""Get a list of Pdus that we haven't backfilled beyond yet (and haven't
|
"""Get a list of Pdus that we haven't backfilled beyond yet (and haven't
|
||||||
seen). This list is used when we want to backfill backwards and is the
|
seen). This list is used when we want to backfill backwards and is the
|
||||||
@ -331,17 +334,14 @@ class PduStore(SQLBaseStore):
|
|||||||
Returns:
|
Returns:
|
||||||
list: A list of PduIdTuple.
|
list: A list of PduIdTuple.
|
||||||
"""
|
"""
|
||||||
return self._db_pool.runInteraction(
|
results = yield self._execute(
|
||||||
self._get_oldest_pdus_in_context, context
|
None,
|
||||||
)
|
|
||||||
|
|
||||||
def _get_oldest_pdus_in_context(self, txn, context):
|
|
||||||
txn.execute(
|
|
||||||
"SELECT pdu_id, origin FROM %(back)s WHERE context = ?"
|
"SELECT pdu_id, origin FROM %(back)s WHERE context = ?"
|
||||||
% {"back": PduBackwardExtremitiesTable.table_name, },
|
% {"back": PduBackwardExtremitiesTable.table_name, },
|
||||||
(context,)
|
context
|
||||||
)
|
)
|
||||||
return [PduIdTuple(i, o) for i, o in txn.fetchall()]
|
|
||||||
|
defer.returnValue([PduIdTuple(i, o) for i, o in results])
|
||||||
|
|
||||||
def is_pdu_new(self, pdu_id, origin, context, depth):
|
def is_pdu_new(self, pdu_id, origin, context, depth):
|
||||||
"""For a given Pdu, try and figure out if it's 'new', i.e., if it's
|
"""For a given Pdu, try and figure out if it's 'new', i.e., if it's
|
||||||
@ -580,7 +580,7 @@ class StatePduStore(SQLBaseStore):
|
|||||||
|
|
||||||
txn.execute(query, query_args)
|
txn.execute(query, query_args)
|
||||||
|
|
||||||
def get_current_state(self, context, pdu_type, state_key):
|
def get_current_state_pdu(self, context, pdu_type, state_key):
|
||||||
"""For a given context, pdu_type, state_key 3-tuple, return what is
|
"""For a given context, pdu_type, state_key 3-tuple, return what is
|
||||||
currently considered the current state.
|
currently considered the current state.
|
||||||
|
|
||||||
@ -595,10 +595,10 @@ class StatePduStore(SQLBaseStore):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return self._db_pool.runInteraction(
|
return self._db_pool.runInteraction(
|
||||||
self._get_current_state, context, pdu_type, state_key
|
self._get_current_state_pdu, context, pdu_type, state_key
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_current_state(self, txn, context, pdu_type, state_key):
|
def _get_current_state_pdu(self, txn, context, pdu_type, state_key):
|
||||||
return self._get_current_interaction(txn, context, pdu_type, state_key)
|
return self._get_current_interaction(txn, context, pdu_type, state_key)
|
||||||
|
|
||||||
def _get_current_interaction(self, txn, context, pdu_type, state_key):
|
def _get_current_interaction(self, txn, context, pdu_type, state_key):
|
||||||
|
@ -76,49 +76,80 @@ class RoomStore(SQLBaseStore):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_rooms(self, is_public, with_topics):
|
def get_rooms(self, is_public):
|
||||||
"""Retrieve a list of all public rooms.
|
"""Retrieve a list of all public rooms.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
is_public (bool): True if the rooms returned should be public.
|
is_public (bool): True if the rooms returned should be public.
|
||||||
with_topics (bool): True to include the current topic for the room
|
|
||||||
in the response.
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of room dicts containing at least a "room_id" key, and a
|
A list of room dicts containing at least a "room_id" key, a
|
||||||
"topic" key if one is set and with_topic=True.
|
"topic" key if one is set, and a "name" key if one is set
|
||||||
"""
|
"""
|
||||||
room_data_type = RoomTopicEvent.TYPE
|
|
||||||
public = 1 if is_public else 0
|
|
||||||
|
|
||||||
latest_topic = ("SELECT max(room_data.id) FROM room_data WHERE "
|
topic_subquery = (
|
||||||
+ "room_data.type = ? GROUP BY room_id")
|
"SELECT topics.event_id as event_id, "
|
||||||
|
"topics.room_id as room_id, topic "
|
||||||
query = ("SELECT rooms.*, room_data.content, room_alias FROM rooms "
|
"FROM topics "
|
||||||
+ "LEFT JOIN "
|
"INNER JOIN current_state_events as c "
|
||||||
+ "room_aliases ON room_aliases.room_id = rooms.room_id "
|
"ON c.event_id = topics.event_id "
|
||||||
+ "LEFT JOIN "
|
|
||||||
+ "room_data ON rooms.room_id = room_data.room_id WHERE "
|
|
||||||
+ "(room_data.id IN (" + latest_topic + ") "
|
|
||||||
+ "OR room_data.id IS NULL) AND rooms.is_public = ?")
|
|
||||||
|
|
||||||
res = yield self._execute(
|
|
||||||
self.cursor_to_dict, query, room_data_type, public
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# return only the keys the specification expects
|
name_subquery = (
|
||||||
ret_keys = ["room_id", "topic", "room_alias"]
|
"SELECT room_names.event_id as event_id, "
|
||||||
|
"room_names.room_id as room_id, name "
|
||||||
|
"FROM room_names "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON c.event_id = room_names.event_id "
|
||||||
|
)
|
||||||
|
|
||||||
# extract topic from the json (icky) FIXME
|
# We use non printing ascii character US () as a seperator
|
||||||
for i, room_row in enumerate(res):
|
sql = (
|
||||||
try:
|
"SELECT r.room_id, n.name, t.topic, "
|
||||||
content_json = json.loads(room_row["content"])
|
"group_concat(a.room_alias, '') "
|
||||||
room_row["topic"] = content_json["topic"]
|
"FROM rooms AS r "
|
||||||
except:
|
"LEFT JOIN (%(topic)s) AS t ON t.room_id = r.room_id "
|
||||||
pass # no topic set
|
"LEFT JOIN (%(name)s) AS n ON n.room_id = r.room_id "
|
||||||
# filter the dict based on ret_keys
|
"INNER JOIN room_aliases AS a ON a.room_id = r.room_id "
|
||||||
res[i] = {k: v for k, v in room_row.iteritems() if k in ret_keys}
|
"WHERE r.is_public = ? "
|
||||||
|
"GROUP BY r.room_id "
|
||||||
|
) % {
|
||||||
|
"topic": topic_subquery,
|
||||||
|
"name": name_subquery,
|
||||||
|
}
|
||||||
|
|
||||||
defer.returnValue(res)
|
rows = yield self._execute(None, sql, is_public)
|
||||||
|
|
||||||
|
ret = [
|
||||||
|
{
|
||||||
|
"room_id": r[0],
|
||||||
|
"name": r[1],
|
||||||
|
"topic": r[2],
|
||||||
|
"aliases": r[3].split(""),
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
def _store_room_topic(self, event):
|
||||||
|
return self._simple_insert(
|
||||||
|
"topics",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"topic": event.topic,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_room_name(self, event):
|
||||||
|
return self._simple_insert(
|
||||||
|
"room_names",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"name": event.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomsTable(Table):
|
class RoomsTable(Table):
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
# -*- 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 SQLBaseStore, Table
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class RoomDataStore(SQLBaseStore):
|
|
||||||
|
|
||||||
"""Provides various CRUD operations for Room Events. """
|
|
||||||
|
|
||||||
def get_room_data(self, room_id, etype, state_key=""):
|
|
||||||
"""Retrieve the data stored under this type and state_key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
room_id (str)
|
|
||||||
etype (str)
|
|
||||||
state_key (str)
|
|
||||||
Returns:
|
|
||||||
namedtuple: Or None if nothing exists at this path.
|
|
||||||
"""
|
|
||||||
query = RoomDataTable.select_statement(
|
|
||||||
"room_id = ? AND type = ? AND state_key = ? "
|
|
||||||
"ORDER BY id DESC LIMIT 1"
|
|
||||||
)
|
|
||||||
return self._execute(
|
|
||||||
RoomDataTable.decode_single_result,
|
|
||||||
query, room_id, etype, state_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
def store_room_data(self, room_id, etype, state_key="", content=None):
|
|
||||||
"""Stores room specific data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
room_id (str)
|
|
||||||
etype (str)
|
|
||||||
state_key (str)
|
|
||||||
data (str)- The data to store for this path in JSON.
|
|
||||||
Returns:
|
|
||||||
The store ID for this data.
|
|
||||||
"""
|
|
||||||
return self._simple_insert(RoomDataTable.table_name, dict(
|
|
||||||
etype=etype,
|
|
||||||
state_key=state_key,
|
|
||||||
room_id=room_id,
|
|
||||||
content=content,
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_max_room_data_id(self):
|
|
||||||
return self._simple_max_id(RoomDataTable.table_name)
|
|
||||||
|
|
||||||
|
|
||||||
class RoomDataTable(Table):
|
|
||||||
table_name = "room_data"
|
|
||||||
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"room_id",
|
|
||||||
"type",
|
|
||||||
"state_key",
|
|
||||||
"content"
|
|
||||||
]
|
|
||||||
|
|
||||||
class EntryType(collections.namedtuple("RoomDataEntry", fields)):
|
|
||||||
|
|
||||||
def as_event(self, event_factory):
|
|
||||||
return event_factory.create_event(
|
|
||||||
etype=self.type,
|
|
||||||
room_id=self.room_id,
|
|
||||||
content=json.loads(self.content),
|
|
||||||
)
|
|
@ -31,6 +31,38 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class RoomMemberStore(SQLBaseStore):
|
class RoomMemberStore(SQLBaseStore):
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _store_room_member(self, event):
|
||||||
|
"""Store a room member in the database.
|
||||||
|
"""
|
||||||
|
domain = self.hs.parse_userid(event.target_user_id).domain
|
||||||
|
|
||||||
|
yield self._simple_insert(
|
||||||
|
"room_memberships",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"user_id": event.target_user_id,
|
||||||
|
"sender": event.user_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"membership": event.membership,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update room hosts table
|
||||||
|
if event.membership == Membership.JOIN:
|
||||||
|
sql = (
|
||||||
|
"INSERT OR IGNORE INTO room_hosts (room_id, host) "
|
||||||
|
"VALUES (?, ?)"
|
||||||
|
)
|
||||||
|
yield self._execute(None, sql, event.room_id, domain)
|
||||||
|
else:
|
||||||
|
sql = (
|
||||||
|
"DELETE FROM room_hosts WHERE room_id = ? AND host = ?"
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self._execute(None, sql, event.room_id, domain)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def get_room_member(self, user_id, room_id):
|
def get_room_member(self, user_id, room_id):
|
||||||
"""Retrieve the current state of a room member.
|
"""Retrieve the current state of a room member.
|
||||||
|
|
||||||
@ -38,36 +70,15 @@ class RoomMemberStore(SQLBaseStore):
|
|||||||
user_id (str): The member's user ID.
|
user_id (str): The member's user ID.
|
||||||
room_id (str): The room the member is in.
|
room_id (str): The room the member is in.
|
||||||
Returns:
|
Returns:
|
||||||
namedtuple: The room member from the database, or None if this
|
Deferred: Results in a MembershipEvent or None.
|
||||||
member does not exist.
|
|
||||||
"""
|
"""
|
||||||
query = RoomMemberTable.select_statement(
|
rows = yield self._get_members_by_dict({
|
||||||
"room_id = ? AND user_id = ? ORDER BY id DESC LIMIT 1")
|
"e.room_id": room_id,
|
||||||
return self._execute(
|
"m.user_id": user_id,
|
||||||
RoomMemberTable.decode_single_result,
|
})
|
||||||
query, room_id, user_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def store_room_member(self, user_id, sender, room_id, membership, content):
|
defer.returnValue(rows[0] if rows else None)
|
||||||
"""Store a room member in the database.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): The member's user ID.
|
|
||||||
room_id (str): The room in relation to the member.
|
|
||||||
membership (synapse.api.constants.Membership): The new membership
|
|
||||||
state.
|
|
||||||
content (dict): The content of the membership (JSON).
|
|
||||||
"""
|
|
||||||
content_json = json.dumps(content)
|
|
||||||
return self._simple_insert(RoomMemberTable.table_name, dict(
|
|
||||||
user_id=user_id,
|
|
||||||
sender=sender,
|
|
||||||
room_id=room_id,
|
|
||||||
membership=membership,
|
|
||||||
content=content_json,
|
|
||||||
))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_room_members(self, room_id, membership=None):
|
def get_room_members(self, room_id, membership=None):
|
||||||
"""Retrieve the current room member list for a room.
|
"""Retrieve the current room member list for a room.
|
||||||
|
|
||||||
@ -79,17 +90,12 @@ class RoomMemberStore(SQLBaseStore):
|
|||||||
Returns:
|
Returns:
|
||||||
list of namedtuples representing the members in this room.
|
list of namedtuples representing the members in this room.
|
||||||
"""
|
"""
|
||||||
query = RoomMemberTable.select_statement(
|
|
||||||
"id IN (SELECT MAX(id) FROM " + RoomMemberTable.table_name
|
where = {"m.room_id": room_id}
|
||||||
+ " WHERE room_id = ? GROUP BY user_id)"
|
|
||||||
)
|
|
||||||
res = yield self._execute(
|
|
||||||
RoomMemberTable.decode_results, query, room_id,
|
|
||||||
)
|
|
||||||
# strip memberships which don't match
|
|
||||||
if membership:
|
if membership:
|
||||||
res = [entry for entry in res if entry.membership == membership]
|
where["m.membership"] = membership
|
||||||
defer.returnValue(res)
|
|
||||||
|
return self._get_members_by_dict(where)
|
||||||
|
|
||||||
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
|
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
|
||||||
""" Get all the rooms for this user where the membership for this user
|
""" Get all the rooms for this user where the membership for this user
|
||||||
@ -106,70 +112,40 @@ class RoomMemberStore(SQLBaseStore):
|
|||||||
return defer.succeed(None)
|
return defer.succeed(None)
|
||||||
|
|
||||||
args = [user_id]
|
args = [user_id]
|
||||||
membership_placeholder = ["membership=?"] * len(membership_list)
|
args.extend(membership_list)
|
||||||
where_membership = "(" + " OR ".join(membership_placeholder) + ")"
|
|
||||||
for membership in membership_list:
|
|
||||||
args.append(membership)
|
|
||||||
|
|
||||||
# sub-select finds the row ID for the most recent (i.e. current)
|
where_clause = "user_id = ? AND (%s)" % (
|
||||||
# state change of this user per room, then the outer select finds those
|
" OR ".join(["membership = ?" for _ in membership_list]),
|
||||||
query = ("SELECT room_id, membership FROM room_memberships"
|
|
||||||
+ " WHERE id IN (SELECT MAX(id) FROM room_memberships"
|
|
||||||
+ " WHERE user_id=? GROUP BY room_id)"
|
|
||||||
+ " AND " + where_membership)
|
|
||||||
return self._execute(
|
|
||||||
self.cursor_to_dict, query, *args
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return self._get_members_query(where_clause, args)
|
||||||
|
|
||||||
|
def get_joined_hosts_for_room(self, room_id):
|
||||||
|
return self._simple_select_onecol(
|
||||||
|
"room_hosts",
|
||||||
|
{"room_id": room_id},
|
||||||
|
"host"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_members_by_dict(self, where_dict):
|
||||||
|
clause = " AND ".join("%s = ?" % k for k in where_dict.keys())
|
||||||
|
vals = where_dict.values()
|
||||||
|
return self._get_members_query(clause, vals)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_joined_hosts_for_room(self, room_id):
|
def _get_members_query(self, where_clause, where_values):
|
||||||
query = RoomMemberTable.select_statement(
|
sql = (
|
||||||
"id IN (SELECT MAX(id) FROM " + RoomMemberTable.table_name
|
"SELECT e.* FROM events as e "
|
||||||
+ " WHERE room_id = ? GROUP BY user_id)"
|
"INNER JOIN room_memberships as m "
|
||||||
)
|
"ON e.event_id = m.event_id "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON m.event_id = c.event_id "
|
||||||
|
"WHERE %s "
|
||||||
|
) % (where_clause,)
|
||||||
|
|
||||||
res = yield self._execute(
|
rows = yield self._execute_and_decode(sql, *where_values)
|
||||||
RoomMemberTable.decode_results, query, room_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def host_from_user_id_string(user_id):
|
logger.debug("_get_members_query Got rows %s", rows)
|
||||||
domain = UserID.from_string(entry.user_id, self.hs).domain
|
|
||||||
return domain
|
|
||||||
|
|
||||||
# strip memberships which don't match
|
results = [self._parse_event_from_row(r) for r in rows]
|
||||||
hosts = [
|
defer.returnValue(results)
|
||||||
host_from_user_id_string(entry.user_id)
|
|
||||||
for entry in res
|
|
||||||
if entry.membership == Membership.JOIN
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.debug("Returning hosts: %s from results: %s", hosts, res)
|
|
||||||
|
|
||||||
defer.returnValue(hosts)
|
|
||||||
|
|
||||||
def get_max_room_member_id(self):
|
|
||||||
return self._simple_max_id(RoomMemberTable.table_name)
|
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberTable(Table):
|
|
||||||
table_name = "room_memberships"
|
|
||||||
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"user_id",
|
|
||||||
"sender",
|
|
||||||
"room_id",
|
|
||||||
"membership",
|
|
||||||
"content"
|
|
||||||
]
|
|
||||||
|
|
||||||
class EntryType(collections.namedtuple("RoomMemberEntry", fields)):
|
|
||||||
|
|
||||||
def as_event(self, event_factory):
|
|
||||||
return event_factory.create_event(
|
|
||||||
etype=RoomMemberEvent.TYPE,
|
|
||||||
room_id=self.room_id,
|
|
||||||
target_user_id=self.user_id,
|
|
||||||
user_id=self.sender,
|
|
||||||
content=json.loads(self.content),
|
|
||||||
)
|
|
||||||
|
@ -12,43 +12,71 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 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 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 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 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(
|
CREATE TABLE IF NOT EXISTS rooms(
|
||||||
room_id TEXT PRIMARY KEY NOT NULL,
|
room_id TEXT PRIMARY KEY NOT NULL,
|
||||||
is_public INTEGER,
|
is_public INTEGER,
|
||||||
creator TEXT
|
creator TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_memberships(
|
CREATE TABLE IF NOT EXISTS room_hosts(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id TEXT NOT NULL, -- no foreign key to users table, it could be an id belonging to another home server
|
|
||||||
sender TEXT NOT NULL,
|
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
membership TEXT NOT NULL,
|
host TEXT NOT NULL
|
||||||
content TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS messages(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id TEXT,
|
|
||||||
room_id TEXT,
|
|
||||||
msg_id TEXT,
|
|
||||||
content TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS feedback(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
content TEXT,
|
|
||||||
feedback_type TEXT,
|
|
||||||
fb_sender_id TEXT,
|
|
||||||
msg_id TEXT,
|
|
||||||
room_id TEXT,
|
|
||||||
msg_sender_id TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_data(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
room_id TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
state_key TEXT NOT NULL,
|
|
||||||
content TEXT
|
|
||||||
);
|
);
|
||||||
|
@ -13,267 +13,287 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
""" This module is responsible for getting events from the DB for pagination
|
||||||
|
and event streaming.
|
||||||
|
|
||||||
|
The order it returns events in depend on whether we are streaming forwards or
|
||||||
|
are paginating backwards. We do this because we want to handle out of order
|
||||||
|
messages nicely, while still returning them in the correct order when we
|
||||||
|
paginate bacwards.
|
||||||
|
|
||||||
|
This is implemented by keeping two ordering columns: stream_ordering and
|
||||||
|
topological_ordering. Stream ordering is basically insertion/received order
|
||||||
|
(except for events from backfill requests). The topolgical_ordering is a
|
||||||
|
weak ordering of events based on the pdu graph.
|
||||||
|
|
||||||
|
This means that we have to have two different types of tokens, depending on
|
||||||
|
what sort order was used:
|
||||||
|
- stream tokens are of the form: "s%d", which maps directly to the column
|
||||||
|
- topological tokems: "t%d-%d", where the integers map to the topological
|
||||||
|
and stream ordering columns respectively.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
from ._base import SQLBaseStore
|
from ._base import SQLBaseStore
|
||||||
from .message import MessagesTable
|
from synapse.api.errors import SynapseError
|
||||||
from .feedback import FeedbackTable
|
from synapse.api.constants import Membership
|
||||||
from .roomdata import RoomDataTable
|
from synapse.util.logutils import log_function
|
||||||
from .roommember import RoomMemberTable
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
MAX_STREAM_SIZE = 1000
|
||||||
|
|
||||||
|
|
||||||
|
_STREAM_TOKEN = "stream"
|
||||||
|
_TOPOLOGICAL_TOKEN = "topological"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_stream_token(string):
|
||||||
|
try:
|
||||||
|
if string[0] != 's':
|
||||||
|
raise
|
||||||
|
return int(string[1:])
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid token")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_topological_token(string):
|
||||||
|
try:
|
||||||
|
if string[0] != 't':
|
||||||
|
raise
|
||||||
|
parts = string[1:].split('-', 1)
|
||||||
|
return (int(parts[0]), int(parts[1]))
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid token")
|
||||||
|
|
||||||
|
|
||||||
|
def is_stream_token(string):
|
||||||
|
try:
|
||||||
|
_parse_stream_token(string)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_topological_token(string):
|
||||||
|
try:
|
||||||
|
_parse_topological_token(string)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_token_bound(token, comparison):
|
||||||
|
try:
|
||||||
|
s = _parse_stream_token(token)
|
||||||
|
return "%s %s %d" % ("stream_ordering", comparison, s)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
top, stream = _parse_topological_token(token)
|
||||||
|
return "%s %s %d AND %s %s %d" % (
|
||||||
|
"topological_ordering", comparison, top,
|
||||||
|
"stream_ordering", comparison, stream,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise SynapseError(400, "Invalid token")
|
||||||
|
|
||||||
|
|
||||||
class StreamStore(SQLBaseStore):
|
class StreamStore(SQLBaseStore):
|
||||||
|
@log_function
|
||||||
|
def get_room_events(self, user_id, from_key, to_key, room_id, limit=0,
|
||||||
|
direction='f', with_feedback=False):
|
||||||
|
# We deal with events request in two different ways depending on if
|
||||||
|
# this looks like an /events request or a pagination request.
|
||||||
|
is_events = (
|
||||||
|
direction == 'f'
|
||||||
|
and user_id
|
||||||
|
and is_stream_token(from_key)
|
||||||
|
and to_key and is_stream_token(to_key)
|
||||||
|
)
|
||||||
|
|
||||||
def get_message_stream(self, user_id, from_key, to_key, room_id, limit=0,
|
if is_events:
|
||||||
|
return self.get_room_events_stream(
|
||||||
|
user_id=user_id,
|
||||||
|
from_key=from_key,
|
||||||
|
to_key=to_key,
|
||||||
|
room_id=room_id,
|
||||||
|
limit=limit,
|
||||||
|
with_feedback=with_feedback,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.paginate_room_events(
|
||||||
|
from_key=from_key,
|
||||||
|
to_key=to_key,
|
||||||
|
room_id=room_id,
|
||||||
|
limit=limit,
|
||||||
|
with_feedback=with_feedback,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def get_room_events_stream(self, user_id, from_key, to_key, room_id,
|
||||||
|
limit=0, with_feedback=False):
|
||||||
|
# TODO (erikj): Handle compressed feedback
|
||||||
|
|
||||||
|
current_room_membership_sql = (
|
||||||
|
"SELECT m.room_id FROM room_memberships as m "
|
||||||
|
"INNER JOIN current_state_events as c ON m.event_id = c.event_id "
|
||||||
|
"WHERE m.user_id = ?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# We also want to get any membership events about that user, e.g.
|
||||||
|
# invites or leave notifications.
|
||||||
|
membership_sql = (
|
||||||
|
"SELECT m.event_id FROM room_memberships as m "
|
||||||
|
"INNER JOIN current_state_events as c ON m.event_id = c.event_id "
|
||||||
|
"WHERE m.user_id = ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
limit = max(limit, MAX_STREAM_SIZE)
|
||||||
|
else:
|
||||||
|
limit = MAX_STREAM_SIZE
|
||||||
|
|
||||||
|
# From and to keys should be integers from ordering.
|
||||||
|
from_id = _parse_stream_token(from_key)
|
||||||
|
to_id = _parse_stream_token(to_key)
|
||||||
|
|
||||||
|
if from_key == to_key:
|
||||||
|
defer.returnValue(([], to_key))
|
||||||
|
return
|
||||||
|
|
||||||
|
sql = (
|
||||||
|
"SELECT * FROM events as e WHERE "
|
||||||
|
"((room_id IN (%(current)s)) OR "
|
||||||
|
"(event_id IN (%(invites)s))) "
|
||||||
|
"AND e.stream_ordering > ? AND e.stream_ordering < ? "
|
||||||
|
"AND e.outlier = 0 "
|
||||||
|
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
||||||
|
) % {
|
||||||
|
"current": current_room_membership_sql,
|
||||||
|
"invites": membership_sql,
|
||||||
|
"limit": limit
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = yield self._execute_and_decode(
|
||||||
|
sql,
|
||||||
|
user_id, user_id, from_id, to_id
|
||||||
|
)
|
||||||
|
|
||||||
|
ret = [self._parse_event_from_row(r) for r in rows]
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
key = "s%d" % max([r["stream_ordering"] for r in rows])
|
||||||
|
else:
|
||||||
|
# Assume we didn't get anything because there was nothing to get.
|
||||||
|
key = to_key
|
||||||
|
|
||||||
|
defer.returnValue((ret, key))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def paginate_room_events(self, room_id, from_key, to_key=None,
|
||||||
|
direction='b', limit=-1,
|
||||||
with_feedback=False):
|
with_feedback=False):
|
||||||
"""Get all messages for this user between the given keys.
|
# TODO (erikj): Handle compressed feedback
|
||||||
|
|
||||||
Args:
|
from_comp = '<' if direction =='b' else '>'
|
||||||
user_id (str): The user who is requesting messages.
|
to_comp = '>' if direction =='b' else '<'
|
||||||
from_key (int): The ID to start returning results from (exclusive).
|
order = "DESC" if direction == 'b' else "ASC"
|
||||||
to_key (int): The ID to stop returning results (exclusive).
|
|
||||||
room_id (str): Gets messages only for this room. Can be None, in
|
args = [room_id]
|
||||||
which case all room messages will be returned.
|
|
||||||
Returns:
|
bounds = _get_token_bound(from_key, from_comp)
|
||||||
A tuple of rows (list of namedtuples), new_id(int)
|
if to_key:
|
||||||
"""
|
bounds = "%s AND %s" % (bounds, _get_token_bound(to_key, to_comp))
|
||||||
if with_feedback and room_id: # with fb MUST specify a room ID
|
|
||||||
return self._db_pool.runInteraction(
|
if int(limit) > 0:
|
||||||
self._get_message_rows_with_feedback,
|
args.append(int(limit))
|
||||||
user_id, from_key, to_key, room_id, limit
|
limit_str = " LIMIT ?"
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return self._db_pool.runInteraction(
|
limit_str = ""
|
||||||
self._get_message_rows,
|
|
||||||
user_id, from_key, to_key, room_id, limit
|
sql = (
|
||||||
|
"SELECT * FROM events "
|
||||||
|
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
|
||||||
|
"ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s "
|
||||||
|
) % {"bounds": bounds, "order": order, "limit": limit_str}
|
||||||
|
|
||||||
|
rows = yield self._execute_and_decode(
|
||||||
|
sql,
|
||||||
|
*args
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_message_rows(self, txn, user_id, from_pkey, to_pkey, room_id,
|
if rows:
|
||||||
limit):
|
topo = rows[-1]["topological_ordering"]
|
||||||
# work out which rooms this user is joined in on and join them with
|
toke = rows[-1]["stream_ordering"]
|
||||||
# the room id on the messages table, bounded by the specified pkeys
|
next_token = "t%s-%s" % (topo, toke)
|
||||||
|
|
||||||
# get all messages where the *current* membership state is 'join' for
|
|
||||||
# this user in that room.
|
|
||||||
query = ("SELECT messages.* FROM messages WHERE ? IN"
|
|
||||||
+ " (SELECT membership from room_memberships WHERE user_id=?"
|
|
||||||
+ " AND room_id = messages.room_id ORDER BY id DESC LIMIT 1)")
|
|
||||||
query_args = ["join", user_id]
|
|
||||||
|
|
||||||
if room_id:
|
|
||||||
query += " AND messages.room_id=?"
|
|
||||||
query_args.append(room_id)
|
|
||||||
|
|
||||||
(query, query_args) = self._append_stream_operations(
|
|
||||||
"messages", query, query_args, from_pkey, to_pkey, limit=limit
|
|
||||||
)
|
|
||||||
|
|
||||||
cursor = txn.execute(query, query_args)
|
|
||||||
return self._as_events(cursor, MessagesTable, from_pkey)
|
|
||||||
|
|
||||||
def _get_message_rows_with_feedback(self, txn, user_id, from_pkey, to_pkey,
|
|
||||||
room_id, limit):
|
|
||||||
# this col represents the compressed feedback JSON as per spec
|
|
||||||
compressed_feedback_col = (
|
|
||||||
"'[' || group_concat('{\"sender_id\":\"' || f.fb_sender_id"
|
|
||||||
+ " || '\",\"feedback_type\":\"' || f.feedback_type"
|
|
||||||
+ " || '\",\"content\":' || f.content || '}') || ']'"
|
|
||||||
)
|
|
||||||
|
|
||||||
global_msg_id_join = ("f.room_id = messages.room_id"
|
|
||||||
+ " and f.msg_id = messages.msg_id"
|
|
||||||
+ " and messages.user_id = f.msg_sender_id")
|
|
||||||
|
|
||||||
select_query = (
|
|
||||||
"SELECT messages.*, f.content AS fb_content, f.fb_sender_id"
|
|
||||||
+ ", " + compressed_feedback_col + " AS compressed_fb"
|
|
||||||
+ " FROM messages LEFT JOIN feedback f ON " + global_msg_id_join)
|
|
||||||
|
|
||||||
current_membership_sub_query = (
|
|
||||||
"(SELECT membership from room_memberships rm"
|
|
||||||
+ " WHERE user_id=? AND room_id = rm.room_id"
|
|
||||||
+ " ORDER BY id DESC LIMIT 1)")
|
|
||||||
|
|
||||||
where = (" WHERE ? IN " + current_membership_sub_query
|
|
||||||
+ " AND messages.room_id=?")
|
|
||||||
|
|
||||||
query = select_query + where
|
|
||||||
query_args = ["join", user_id, room_id]
|
|
||||||
|
|
||||||
(query, query_args) = self._append_stream_operations(
|
|
||||||
"messages", query, query_args, from_pkey, to_pkey,
|
|
||||||
limit=limit, group_by=" GROUP BY messages.id "
|
|
||||||
)
|
|
||||||
|
|
||||||
cursor = txn.execute(query, query_args)
|
|
||||||
|
|
||||||
# convert the result set into events
|
|
||||||
entries = self.cursor_to_dict(cursor)
|
|
||||||
events = []
|
|
||||||
for entry in entries:
|
|
||||||
# TODO we should spec the cursor > event mapping somewhere else.
|
|
||||||
event = {}
|
|
||||||
straight_mappings = ["msg_id", "user_id", "room_id"]
|
|
||||||
for key in straight_mappings:
|
|
||||||
event[key] = entry[key]
|
|
||||||
event["content"] = json.loads(entry["content"])
|
|
||||||
if entry["compressed_fb"]:
|
|
||||||
event["feedback"] = json.loads(entry["compressed_fb"])
|
|
||||||
events.append(event)
|
|
||||||
|
|
||||||
latest_pkey = from_pkey if len(entries) == 0 else entries[-1]["id"]
|
|
||||||
|
|
||||||
return (events, latest_pkey)
|
|
||||||
|
|
||||||
def get_room_member_stream(self, user_id, from_key, to_key):
|
|
||||||
"""Get all room membership events for this user between the given keys.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): The user who is requesting membership events.
|
|
||||||
from_key (int): The ID to start returning results from (exclusive).
|
|
||||||
to_key (int): The ID to stop returning results (exclusive).
|
|
||||||
Returns:
|
|
||||||
A tuple of rows (list of namedtuples), new_id(int)
|
|
||||||
"""
|
|
||||||
return self._db_pool.runInteraction(
|
|
||||||
self._get_room_member_rows, user_id, from_key, to_key
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_room_member_rows(self, txn, user_id, from_pkey, to_pkey):
|
|
||||||
# get all room membership events for rooms which the user is
|
|
||||||
# *currently* joined in on, or all invite events for this user.
|
|
||||||
current_membership_sub_query = (
|
|
||||||
"(SELECT membership FROM room_memberships"
|
|
||||||
+ " WHERE user_id=? AND room_id = rm.room_id"
|
|
||||||
+ " ORDER BY id DESC LIMIT 1)")
|
|
||||||
|
|
||||||
query = ("SELECT rm.* FROM room_memberships rm "
|
|
||||||
# all membership events for rooms you've currently joined.
|
|
||||||
+ " WHERE (? IN " + current_membership_sub_query
|
|
||||||
# all invite membership events for this user
|
|
||||||
+ " OR rm.membership=? AND user_id=?)"
|
|
||||||
+ " AND rm.id > ?")
|
|
||||||
query_args = ["join", user_id, "invite", user_id, from_pkey]
|
|
||||||
|
|
||||||
if to_pkey != -1:
|
|
||||||
query += " AND rm.id < ?"
|
|
||||||
query_args.append(to_pkey)
|
|
||||||
|
|
||||||
cursor = txn.execute(query, query_args)
|
|
||||||
return self._as_events(cursor, RoomMemberTable, from_pkey)
|
|
||||||
|
|
||||||
def get_feedback_stream(self, user_id, from_key, to_key, room_id, limit=0):
|
|
||||||
return self._db_pool.runInteraction(
|
|
||||||
self._get_feedback_rows,
|
|
||||||
user_id, from_key, to_key, room_id, limit
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_feedback_rows(self, txn, user_id, from_pkey, to_pkey, room_id,
|
|
||||||
limit):
|
|
||||||
# work out which rooms this user is joined in on and join them with
|
|
||||||
# the room id on the feedback table, bounded by the specified pkeys
|
|
||||||
|
|
||||||
# get all messages where the *current* membership state is 'join' for
|
|
||||||
# this user in that room.
|
|
||||||
query = (
|
|
||||||
"SELECT feedback.* FROM feedback WHERE ? IN "
|
|
||||||
+ "(SELECT membership from room_memberships WHERE user_id=?"
|
|
||||||
+ " AND room_id = feedback.room_id ORDER BY id DESC LIMIT 1)")
|
|
||||||
query_args = ["join", user_id]
|
|
||||||
|
|
||||||
if room_id:
|
|
||||||
query += " AND feedback.room_id=?"
|
|
||||||
query_args.append(room_id)
|
|
||||||
|
|
||||||
(query, query_args) = self._append_stream_operations(
|
|
||||||
"feedback", query, query_args, from_pkey, to_pkey, limit=limit
|
|
||||||
)
|
|
||||||
|
|
||||||
cursor = txn.execute(query, query_args)
|
|
||||||
return self._as_events(cursor, FeedbackTable, from_pkey)
|
|
||||||
|
|
||||||
def get_room_data_stream(self, user_id, from_key, to_key, room_id,
|
|
||||||
limit=0):
|
|
||||||
return self._db_pool.runInteraction(
|
|
||||||
self._get_room_data_rows,
|
|
||||||
user_id, from_key, to_key, room_id, limit
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_room_data_rows(self, txn, user_id, from_pkey, to_pkey, room_id,
|
|
||||||
limit):
|
|
||||||
# work out which rooms this user is joined in on and join them with
|
|
||||||
# the room id on the feedback table, bounded by the specified pkeys
|
|
||||||
|
|
||||||
# get all messages where the *current* membership state is 'join' for
|
|
||||||
# this user in that room.
|
|
||||||
query = (
|
|
||||||
"SELECT room_data.* FROM room_data WHERE ? IN "
|
|
||||||
+ "(SELECT membership from room_memberships WHERE user_id=?"
|
|
||||||
+ " AND room_id = room_data.room_id ORDER BY id DESC LIMIT 1)")
|
|
||||||
query_args = ["join", user_id]
|
|
||||||
|
|
||||||
if room_id:
|
|
||||||
query += " AND room_data.room_id=?"
|
|
||||||
query_args.append(room_id)
|
|
||||||
|
|
||||||
(query, query_args) = self._append_stream_operations(
|
|
||||||
"room_data", query, query_args, from_pkey, to_pkey, limit=limit
|
|
||||||
)
|
|
||||||
|
|
||||||
cursor = txn.execute(query, query_args)
|
|
||||||
return self._as_events(cursor, RoomDataTable, from_pkey)
|
|
||||||
|
|
||||||
def _append_stream_operations(self, table_name, query, query_args,
|
|
||||||
from_pkey, to_pkey, limit=None,
|
|
||||||
group_by=""):
|
|
||||||
LATEST_ROW = -1
|
|
||||||
order_by = ""
|
|
||||||
if to_pkey > from_pkey:
|
|
||||||
if from_pkey != LATEST_ROW:
|
|
||||||
# e.g. from=5 to=9 >> from 5 to 9 >> id>5 AND id<9
|
|
||||||
query += (" AND %s.id > ? AND %s.id < ?" %
|
|
||||||
(table_name, table_name))
|
|
||||||
query_args.append(from_pkey)
|
|
||||||
query_args.append(to_pkey)
|
|
||||||
else:
|
else:
|
||||||
# e.g. from=-1 to=5 >> from now to 5 >> id>5 ORDER BY id DESC
|
# TODO (erikj): We should work out what to do here instead.
|
||||||
query += " AND %s.id > ? " % table_name
|
next_token = to_key if to_key else from_key
|
||||||
order_by = "ORDER BY id DESC"
|
|
||||||
query_args.append(to_pkey)
|
defer.returnValue(
|
||||||
elif from_pkey > to_pkey:
|
(
|
||||||
if to_pkey != LATEST_ROW:
|
[self._parse_event_from_row(r) for r in rows],
|
||||||
# from=9 to=5 >> from 9 to 5 >> id>5 AND id<9 ORDER BY id DESC
|
next_token
|
||||||
query += (" AND %s.id > ? AND %s.id < ? " %
|
)
|
||||||
(table_name, table_name))
|
)
|
||||||
order_by = "ORDER BY id DESC"
|
|
||||||
query_args.append(to_pkey)
|
@defer.inlineCallbacks
|
||||||
query_args.append(from_pkey)
|
def get_recent_events_for_room(self, room_id, limit, end_token,
|
||||||
|
with_feedback=False):
|
||||||
|
# TODO (erikj): Handle compressed feedback
|
||||||
|
|
||||||
|
sql = (
|
||||||
|
"SELECT * FROM events "
|
||||||
|
"WHERE room_id = ? AND stream_ordering <= ? "
|
||||||
|
"ORDER BY topological_ordering, stream_ordering DESC LIMIT ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = yield self._execute_and_decode(
|
||||||
|
sql,
|
||||||
|
room_id, end_token, limit
|
||||||
|
)
|
||||||
|
|
||||||
|
rows.reverse() # As we selected with reverse ordering
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
topo = rows[0]["topological_ordering"]
|
||||||
|
toke = rows[0]["stream_ordering"]
|
||||||
|
start_token = "t%s-%s" % (topo, toke)
|
||||||
|
|
||||||
|
token = (start_token, end_token)
|
||||||
else:
|
else:
|
||||||
# from=5 to=-1 >> from 5 to now >> id>5
|
token = (end_token, end_token)
|
||||||
query += " AND %s.id > ?" % table_name
|
|
||||||
query_args.append(from_pkey)
|
|
||||||
|
|
||||||
query += group_by + order_by
|
defer.returnValue(
|
||||||
|
(
|
||||||
|
[self._parse_event_from_row(r) for r in rows],
|
||||||
|
token
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if limit and limit > 0:
|
@defer.inlineCallbacks
|
||||||
query += " LIMIT ?"
|
def get_room_events_max_id(self):
|
||||||
query_args.append(str(limit))
|
res = yield self._execute_and_decode(
|
||||||
|
"SELECT MAX(stream_ordering) as m FROM events"
|
||||||
|
)
|
||||||
|
|
||||||
return (query, query_args)
|
logger.debug("get_room_events_max_id: %s", res)
|
||||||
|
|
||||||
def _as_events(self, cursor, table, from_pkey):
|
if not res or not res[0] or not res[0]["m"]:
|
||||||
data_entries = table.decode_results(cursor)
|
defer.returnValue("s1")
|
||||||
last_pkey = from_pkey
|
return
|
||||||
if data_entries:
|
|
||||||
last_pkey = data_entries[-1].id
|
|
||||||
|
|
||||||
events = [
|
key = res[0]["m"] + 1
|
||||||
entry.as_event(self.event_factory).get_dict()
|
defer.returnValue("s%d" % (key,))
|
||||||
for entry in data_entries
|
|
||||||
]
|
|
||||||
|
|
||||||
return (events, last_pkey)
|
|
||||||
|
@ -38,6 +38,14 @@ class DomainSpecificString(
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
raise ValueError("Attempted to iterate a %s" % (type(self).__name__))
|
raise ValueError("Attempted to iterate a %s" % (type(self).__name__))
|
||||||
|
|
||||||
|
# Because this class is a namedtuple of strings and booleans, it is deeply
|
||||||
|
# immutable.
|
||||||
|
def __copy__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, s, hs):
|
def from_string(cls, s, hs):
|
||||||
"""Parse the string given by 's' into a structure object."""
|
"""Parse the string given by 's' into a structure object."""
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 matrix.org
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -41,6 +40,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
datastore=NonCallableMock(spec_set=[
|
datastore=NonCallableMock(spec_set=[
|
||||||
"persist_event",
|
"persist_event",
|
||||||
"store_room",
|
"store_room",
|
||||||
|
"get_room",
|
||||||
]),
|
]),
|
||||||
resource_for_federation=NonCallableMock(),
|
resource_for_federation=NonCallableMock(),
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
@ -69,10 +69,11 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
store_id = "ASD"
|
store_id = "ASD"
|
||||||
self.datastore.persist_event.return_value = defer.succeed(store_id)
|
self.datastore.persist_event.return_value = defer.succeed(store_id)
|
||||||
|
self.datastore.get_room.return_value = defer.succeed(True)
|
||||||
|
|
||||||
yield self.handlers.federation_handler.on_receive(event, False)
|
yield self.handlers.federation_handler.on_receive(event, False, False)
|
||||||
|
|
||||||
self.datastore.persist_event.assert_called_once_with(event)
|
self.datastore.persist_event.assert_called_once_with(event, False)
|
||||||
self.notifier.on_new_room_event.assert_called_once_with(
|
self.notifier.on_new_room_event.assert_called_once_with(
|
||||||
event, store_id)
|
event, store_id)
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
content={},
|
content={},
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.handlers.federation_handler.on_receive(event, False)
|
yield self.handlers.federation_handler.on_receive(event, False, False)
|
||||||
|
|
||||||
mem_handler = self.handlers.room_member_handler
|
mem_handler = self.handlers.room_member_handler
|
||||||
self.assertEquals(1, mem_handler.change_membership.call_count)
|
self.assertEquals(1, mem_handler.change_membership.call_count)
|
||||||
@ -115,7 +116,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
content={},
|
content={},
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.handlers.federation_handler.on_receive(event, False)
|
yield self.handlers.federation_handler.on_receive(event, False, False)
|
||||||
|
|
||||||
mem_handler = self.handlers.room_member_handler
|
mem_handler = self.handlers.room_member_handler
|
||||||
self.assertEquals(0, mem_handler.change_membership.call_count)
|
self.assertEquals(0, mem_handler.change_membership.call_count)
|
||||||
|
@ -40,7 +40,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
self.hostname,
|
self.hostname,
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
datastore=NonCallableMock(spec_set=[
|
datastore=NonCallableMock(spec_set=[
|
||||||
"store_room_member",
|
"persist_event",
|
||||||
"get_joined_hosts_for_room",
|
"get_joined_hosts_for_room",
|
||||||
"get_room_member",
|
"get_room_member",
|
||||||
"get_room",
|
"get_room",
|
||||||
@ -69,6 +69,8 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
self.distributor = hs.get_distributor()
|
self.distributor = hs.get_distributor()
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
|
self.distributor.declare("collect_presencelike_data")
|
||||||
|
|
||||||
self.handlers.room_member_handler = RoomMemberHandler(self.hs)
|
self.handlers.room_member_handler = RoomMemberHandler(self.hs)
|
||||||
self.handlers.profile_handler = ProfileHandler(self.hs)
|
self.handlers.profile_handler = ProfileHandler(self.hs)
|
||||||
self.room_member_handler = self.handlers.room_member_handler
|
self.room_member_handler = self.handlers.room_member_handler
|
||||||
@ -97,7 +99,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
store_id = "store_id_fooo"
|
store_id = "store_id_fooo"
|
||||||
self.datastore.store_room_member.return_value = defer.succeed(store_id)
|
self.datastore.persist_event.return_value = defer.succeed(store_id)
|
||||||
|
|
||||||
# Actual invocation
|
# Actual invocation
|
||||||
yield self.room_member_handler.change_membership(event)
|
yield self.room_member_handler.change_membership(event)
|
||||||
@ -110,12 +112,8 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
set(event.destinations)
|
set(event.destinations)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.datastore.store_room_member.assert_called_once_with(
|
self.datastore.persist_event.assert_called_once_with(
|
||||||
user_id=target_user_id,
|
event
|
||||||
sender=user_id,
|
|
||||||
room_id=room_id,
|
|
||||||
content=content,
|
|
||||||
membership=Membership.INVITE,
|
|
||||||
)
|
)
|
||||||
self.notifier.on_new_room_event.assert_called_once_with(
|
self.notifier.on_new_room_event.assert_called_once_with(
|
||||||
event, store_id)
|
event, store_id)
|
||||||
@ -144,12 +142,14 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
joined = ["red", "green"]
|
joined = ["red", "green"]
|
||||||
|
|
||||||
self.state_handler.handle_new_event.return_value = defer.succeed(True)
|
self.state_handler.handle_new_event.return_value = defer.succeed(True)
|
||||||
self.datastore.get_joined_hosts_for_room.return_value = (
|
|
||||||
defer.succeed(joined)
|
def get_joined(*args):
|
||||||
)
|
return defer.succeed(joined)
|
||||||
|
|
||||||
|
self.datastore.get_joined_hosts_for_room.side_effect = get_joined
|
||||||
|
|
||||||
store_id = "store_id_fooo"
|
store_id = "store_id_fooo"
|
||||||
self.datastore.store_room_member.return_value = defer.succeed(store_id)
|
self.datastore.persist_event.return_value = defer.succeed(store_id)
|
||||||
self.datastore.get_room.return_value = defer.succeed(1) # Not None.
|
self.datastore.get_room.return_value = defer.succeed(1) # Not None.
|
||||||
|
|
||||||
prev_state = NonCallableMock()
|
prev_state = NonCallableMock()
|
||||||
@ -171,12 +171,8 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
set(event.destinations)
|
set(event.destinations)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.datastore.store_room_member.assert_called_once_with(
|
self.datastore.persist_event.assert_called_once_with(
|
||||||
user_id=target_user_id,
|
event
|
||||||
sender=user_id,
|
|
||||||
room_id=room_id,
|
|
||||||
content=content,
|
|
||||||
membership=Membership.JOIN,
|
|
||||||
)
|
)
|
||||||
self.notifier.on_new_room_event.assert_called_once_with(
|
self.notifier.on_new_room_event.assert_called_once_with(
|
||||||
event, store_id)
|
event, store_id)
|
||||||
|
@ -190,9 +190,7 @@ class EventStreamPermissionsTestCase(RestTestCase):
|
|||||||
"/events?access_token=%s&timeout=0" % (self.token))
|
"/events?access_token=%s&timeout=0" % (self.token))
|
||||||
self.assertEquals(200, code, msg=str(response))
|
self.assertEquals(200, code, msg=str(response))
|
||||||
|
|
||||||
# First message is a reflection of my own presence status change
|
self.assertEquals(0, len(response["chunk"]))
|
||||||
self.assertEquals(1, len(response["chunk"]))
|
|
||||||
self.assertEquals("m.presence", response["chunk"][0]["type"])
|
|
||||||
|
|
||||||
# joined room (expect all content for room)
|
# joined room (expect all content for room)
|
||||||
yield self.join(room=room_id, user=self.user_id, tok=self.token)
|
yield self.join(room=room_id, user=self.user_id, tok=self.token)
|
||||||
|
@ -287,14 +287,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
# all be ours
|
# all be ours
|
||||||
|
|
||||||
# I'll already get my own presence state change
|
# I'll already get my own presence state change
|
||||||
self.assertEquals({"start": "0", "end": "1", "chunk": [
|
self.assertEquals({"start": "1", "end": "1", "chunk": []}, response)
|
||||||
{"type": "m.presence",
|
|
||||||
"content": {
|
|
||||||
"user_id": "@apple:test",
|
|
||||||
"state": ONLINE,
|
|
||||||
"mtime_age": 0,
|
|
||||||
}},
|
|
||||||
]}, response)
|
|
||||||
|
|
||||||
self.mock_datastore.set_presence_state.return_value = defer.succeed(
|
self.mock_datastore.set_presence_state.return_value = defer.succeed(
|
||||||
{"state": ONLINE})
|
{"state": ONLINE})
|
||||||
|
@ -46,6 +46,7 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
resource_for_client=self.mock_resource,
|
resource_for_client=self.mock_resource,
|
||||||
federation=Mock(),
|
federation=Mock(),
|
||||||
replication_layer=Mock(),
|
replication_layer=Mock(),
|
||||||
|
datastore=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_user_by_token(token=None):
|
def _get_user_by_token(token=None):
|
||||||
|
@ -104,36 +104,36 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
# @defer.inlineCallbacks
|
||||||
def test_get_message(self):
|
# def test_get_message(self):
|
||||||
# get message in uncreated room, expect 403
|
# # get message in uncreated room, expect 403
|
||||||
(code, response) = yield self.mock_resource.trigger_get(
|
# (code, response) = yield self.mock_resource.trigger_get(
|
||||||
"/rooms/noroom/messages/someid/m1")
|
# "/rooms/noroom/messages/someid/m1")
|
||||||
self.assertEquals(403, code, msg=str(response))
|
# self.assertEquals(403, code, msg=str(response))
|
||||||
|
#
|
||||||
# get message in created room not joined (no state), expect 403
|
# # get message in created room not joined (no state), expect 403
|
||||||
(code, response) = yield self.mock_resource.trigger_get(
|
# (code, response) = yield self.mock_resource.trigger_get(
|
||||||
self.created_rmid_msg_path)
|
# self.created_rmid_msg_path)
|
||||||
self.assertEquals(403, code, msg=str(response))
|
# self.assertEquals(403, code, msg=str(response))
|
||||||
|
#
|
||||||
# get message in created room and invited, expect 403
|
# # get message in created room and invited, expect 403
|
||||||
yield self.invite(room=self.created_rmid, src=self.rmcreator_id,
|
# yield self.invite(room=self.created_rmid, src=self.rmcreator_id,
|
||||||
targ=self.user_id)
|
# targ=self.user_id)
|
||||||
(code, response) = yield self.mock_resource.trigger_get(
|
# (code, response) = yield self.mock_resource.trigger_get(
|
||||||
self.created_rmid_msg_path)
|
# self.created_rmid_msg_path)
|
||||||
self.assertEquals(403, code, msg=str(response))
|
# self.assertEquals(403, code, msg=str(response))
|
||||||
|
#
|
||||||
# get message in created room and joined, expect 200
|
# # get message in created room and joined, expect 200
|
||||||
yield self.join(room=self.created_rmid, user=self.user_id)
|
# yield self.join(room=self.created_rmid, user=self.user_id)
|
||||||
(code, response) = yield self.mock_resource.trigger_get(
|
# (code, response) = yield self.mock_resource.trigger_get(
|
||||||
self.created_rmid_msg_path)
|
# self.created_rmid_msg_path)
|
||||||
self.assertEquals(200, code, msg=str(response))
|
# self.assertEquals(200, code, msg=str(response))
|
||||||
|
#
|
||||||
# get message in created room and left, expect 403
|
# # get message in created room and left, expect 403
|
||||||
yield self.leave(room=self.created_rmid, user=self.user_id)
|
# yield self.leave(room=self.created_rmid, user=self.user_id)
|
||||||
(code, response) = yield self.mock_resource.trigger_get(
|
# (code, response) = yield self.mock_resource.trigger_get(
|
||||||
self.created_rmid_msg_path)
|
# self.created_rmid_msg_path)
|
||||||
self.assertEquals(403, code, msg=str(response))
|
# self.assertEquals(403, code, msg=str(response))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_send_message(self):
|
def test_send_message(self):
|
||||||
@ -794,7 +794,12 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET", path, None)
|
(code, response) = yield self.mock_resource.trigger("GET", path, None)
|
||||||
self.assertEquals(200, code, msg=str(response))
|
self.assertEquals(200, code, msg=str(response))
|
||||||
self.assertEquals(json.loads(content), response)
|
|
||||||
|
expected_response = {
|
||||||
|
"membership": Membership.JOIN,
|
||||||
|
"prev": Membership.JOIN,
|
||||||
|
}
|
||||||
|
self.assertEquals(expected_response, response)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_rooms_members_other(self):
|
def test_rooms_members_other(self):
|
||||||
@ -913,9 +918,9 @@ class RoomMessagesTestCase(RestTestCase):
|
|||||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||||
self.assertEquals(200, code, msg=str(response))
|
self.assertEquals(200, code, msg=str(response))
|
||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET", path, None)
|
# (code, response) = yield self.mock_resource.trigger("GET", path, None)
|
||||||
self.assertEquals(200, code, msg=str(response))
|
# self.assertEquals(200, code, msg=str(response))
|
||||||
self.assert_dict(json.loads(content), response)
|
# self.assert_dict(json.loads(content), response)
|
||||||
|
|
||||||
# m.text message type
|
# m.text message type
|
||||||
path = "/rooms/%s/messages/%s/mid2" % (
|
path = "/rooms/%s/messages/%s/mid2" % (
|
||||||
@ -925,9 +930,9 @@ class RoomMessagesTestCase(RestTestCase):
|
|||||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||||
self.assertEquals(200, code, msg=str(response))
|
self.assertEquals(200, code, msg=str(response))
|
||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET", path, None)
|
# (code, response) = yield self.mock_resource.trigger("GET", path, None)
|
||||||
self.assertEquals(200, code, msg=str(response))
|
# self.assertEquals(200, code, msg=str(response))
|
||||||
self.assert_dict(json.loads(content), response)
|
# self.assert_dict(json.loads(content), response)
|
||||||
|
|
||||||
# trying to send message in different user path
|
# trying to send message in different user path
|
||||||
path = "/rooms/%s/messages/%s/mid2" % (
|
path = "/rooms/%s/messages/%s/mid2" % (
|
||||||
|
@ -36,7 +36,7 @@ class StateTestCase(unittest.TestCase):
|
|||||||
"get_unresolved_state_tree",
|
"get_unresolved_state_tree",
|
||||||
"update_current_state",
|
"update_current_state",
|
||||||
"get_latest_pdus_in_context",
|
"get_latest_pdus_in_context",
|
||||||
"get_current_state",
|
"get_current_state_pdu",
|
||||||
"get_pdu",
|
"get_pdu",
|
||||||
])
|
])
|
||||||
self.replication = Mock(spec=["get_pdu"])
|
self.replication = Mock(spec=["get_pdu"])
|
||||||
@ -247,7 +247,7 @@ class StateTestCase(unittest.TestCase):
|
|||||||
pdus = [tup]
|
pdus = [tup]
|
||||||
|
|
||||||
self.persistence.get_latest_pdus_in_context.return_value = pdus
|
self.persistence.get_latest_pdus_in_context.return_value = pdus
|
||||||
self.persistence.get_current_state.return_value = state_pdu
|
self.persistence.get_current_state_pdu.return_value = state_pdu
|
||||||
|
|
||||||
yield self.state.handle_new_event(event)
|
yield self.state.handle_new_event(event)
|
||||||
|
|
||||||
|
168
tests/utils.py
168
tests/utils.py
@ -112,35 +112,20 @@ class MockClock(object):
|
|||||||
|
|
||||||
class MemoryDataStore(object):
|
class MemoryDataStore(object):
|
||||||
|
|
||||||
class RoomMember(namedtuple(
|
Room = namedtuple(
|
||||||
"RoomMember",
|
"Room",
|
||||||
["room_id", "user_id", "sender", "membership", "content"]
|
["room_id", "is_public", "creator"]
|
||||||
)):
|
|
||||||
def as_event(self, event_factory):
|
|
||||||
return event_factory.create_event(
|
|
||||||
etype=RoomMemberEvent.TYPE,
|
|
||||||
room_id=self.room_id,
|
|
||||||
target_user_id=self.user_id,
|
|
||||||
user_id=self.sender,
|
|
||||||
content=json.loads(self.content),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
PathData = namedtuple("PathData",
|
|
||||||
["room_id", "path", "content"])
|
|
||||||
|
|
||||||
Message = namedtuple("Message",
|
|
||||||
["room_id", "msg_id", "user_id", "content"])
|
|
||||||
|
|
||||||
Room = namedtuple("Room",
|
|
||||||
["room_id", "is_public", "creator"])
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.tokens_to_users = {}
|
self.tokens_to_users = {}
|
||||||
self.paths_to_content = {}
|
self.paths_to_content = {}
|
||||||
|
|
||||||
self.members = {}
|
self.members = {}
|
||||||
self.messages = {}
|
|
||||||
self.rooms = {}
|
self.rooms = {}
|
||||||
self.room_members = {}
|
|
||||||
|
self.current_state = {}
|
||||||
|
self.events = []
|
||||||
|
|
||||||
def register(self, user_id, token, password_hash):
|
def register(self, user_id, token, password_hash):
|
||||||
if user_id in self.tokens_to_users.values():
|
if user_id in self.tokens_to_users.values():
|
||||||
@ -163,117 +148,60 @@ class MemoryDataStore(object):
|
|||||||
if room_id in self.rooms:
|
if room_id in self.rooms:
|
||||||
raise StoreError(409, "Conflicting room!")
|
raise StoreError(409, "Conflicting room!")
|
||||||
|
|
||||||
room = MemoryDataStore.Room(room_id=room_id, is_public=is_public,
|
room = MemoryDataStore.Room(
|
||||||
creator=room_creator_user_id)
|
room_id=room_id,
|
||||||
|
is_public=is_public,
|
||||||
|
creator=room_creator_user_id
|
||||||
|
)
|
||||||
self.rooms[room_id] = room
|
self.rooms[room_id] = room
|
||||||
#self.store_room_member(user_id=room_creator_user_id, room_id=room_id,
|
|
||||||
#membership=Membership.JOIN,
|
|
||||||
#content={"membership": Membership.JOIN})
|
|
||||||
|
|
||||||
def get_message(self, user_id=None, room_id=None, msg_id=None):
|
def get_room_member(self, user_id, room_id):
|
||||||
try:
|
return self.members.get(room_id, {}).get(user_id)
|
||||||
return self.messages[user_id + room_id + msg_id]
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def store_message(self, user_id=None, room_id=None, msg_id=None,
|
def get_room_members(self, room_id, membership=None):
|
||||||
content=None):
|
if membership:
|
||||||
msg = MemoryDataStore.Message(room_id=room_id, msg_id=msg_id,
|
return [
|
||||||
user_id=user_id, content=content)
|
v for k, v in self.members.get(room_id, {}).items()
|
||||||
self.messages[user_id + room_id + msg_id] = msg
|
if v.membership == membership
|
||||||
|
]
|
||||||
def get_room_member(self, user_id=None, room_id=None):
|
else:
|
||||||
try:
|
return self.members.get(room_id, {}).values()
|
||||||
return self.members[user_id + room_id]
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_room_members(self, room_id=None, membership=None):
|
|
||||||
try:
|
|
||||||
return self.room_members[room_id]
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
|
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
|
||||||
return [r for r in self.room_members
|
return [
|
||||||
if user_id in self.room_members[r]]
|
r for r in self.members
|
||||||
|
if self.members[r].get(user_id).membership in membership_list
|
||||||
|
]
|
||||||
|
|
||||||
def store_room_member(self, user_id=None, sender=None, room_id=None,
|
def get_room_events_stream(self, user_id=None, from_key=None, to_key=None,
|
||||||
membership=None, content=None):
|
|
||||||
member = MemoryDataStore.RoomMember(room_id=room_id, user_id=user_id,
|
|
||||||
sender=sender, membership=membership, content=json.dumps(content))
|
|
||||||
self.members[user_id + room_id] = member
|
|
||||||
|
|
||||||
# TODO should be latest state
|
|
||||||
if room_id not in self.room_members:
|
|
||||||
self.room_members[room_id] = []
|
|
||||||
self.room_members[room_id].append(member)
|
|
||||||
|
|
||||||
def get_room_data(self, room_id, etype, state_key=""):
|
|
||||||
path = "%s-%s-%s" % (room_id, etype, state_key)
|
|
||||||
try:
|
|
||||||
return self.paths_to_content[path]
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def store_room_data(self, room_id, etype, state_key="", content=None):
|
|
||||||
path = "%s-%s-%s" % (room_id, etype, state_key)
|
|
||||||
data = MemoryDataStore.PathData(path=path, room_id=room_id,
|
|
||||||
content=content)
|
|
||||||
self.paths_to_content[path] = data
|
|
||||||
|
|
||||||
def get_message_stream(self, user_id=None, from_key=None, to_key=None,
|
|
||||||
room_id=None, limit=0, with_feedback=False):
|
room_id=None, limit=0, with_feedback=False):
|
||||||
return ([], from_key) # TODO
|
return ([], from_key) # TODO
|
||||||
|
|
||||||
def get_room_member_stream(self, user_id=None, from_key=None, to_key=None):
|
|
||||||
return ([], from_key) # TODO
|
|
||||||
|
|
||||||
def get_feedback_stream(self, user_id=None, from_key=None, to_key=None,
|
|
||||||
room_id=None, limit=0):
|
|
||||||
return ([], from_key) # TODO
|
|
||||||
|
|
||||||
def get_room_data_stream(self, user_id=None, from_key=None, to_key=None,
|
|
||||||
room_id=None, limit=0):
|
|
||||||
return ([], from_key) # TODO
|
|
||||||
|
|
||||||
def to_events(self, data_store_list):
|
|
||||||
return data_store_list # TODO
|
|
||||||
|
|
||||||
def get_max_message_id(self):
|
|
||||||
return 0 # TODO
|
|
||||||
|
|
||||||
def get_max_feedback_id(self):
|
|
||||||
return 0 # TODO
|
|
||||||
|
|
||||||
def get_max_room_member_id(self):
|
|
||||||
return 0 # TODO
|
|
||||||
|
|
||||||
def get_max_room_data_id(self):
|
|
||||||
return 0 # TODO
|
|
||||||
|
|
||||||
def get_joined_hosts_for_room(self, room_id):
|
def get_joined_hosts_for_room(self, room_id):
|
||||||
return defer.succeed([])
|
return defer.succeed([])
|
||||||
|
|
||||||
def persist_event(self, event):
|
def persist_event(self, event):
|
||||||
if event.type == MessageEvent.TYPE:
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
return self.store_message(
|
room_id = event.room_id
|
||||||
user_id=event.user_id,
|
user = event.target_user_id
|
||||||
room_id=event.room_id,
|
membership = event.membership
|
||||||
msg_id=event.msg_id,
|
self.members.setdefault(room_id, {})[user] = event
|
||||||
content=json.dumps(event.content)
|
|
||||||
)
|
if hasattr(event, "state_key"):
|
||||||
elif event.type == RoomMemberEvent.TYPE:
|
key = (event.room_id, event.type, event.state_key)
|
||||||
return self.store_room_member(
|
self.current_state[key] = event
|
||||||
user_id=event.target_user_id,
|
|
||||||
room_id=event.room_id,
|
self.events.append(event)
|
||||||
content=event.content,
|
|
||||||
membership=event.content["membership"]
|
def get_current_state(self, room_id, event_type=None, state_key=""):
|
||||||
)
|
if event_type:
|
||||||
|
key = (room_id, event_type, state_key)
|
||||||
|
return self.current_state.get(key)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
return [
|
||||||
"Don't know how to persist type=%s" % event.type
|
e for e in self.current_state
|
||||||
)
|
if e[0] == room_id
|
||||||
|
]
|
||||||
|
|
||||||
def set_presence_state(self, user_localpart, state):
|
def set_presence_state(self, user_localpart, state):
|
||||||
return defer.succeed({"state": 0})
|
return defer.succeed({"state": 0})
|
||||||
@ -281,6 +209,8 @@ class MemoryDataStore(object):
|
|||||||
def get_presence_list(self, user_localpart, accepted):
|
def get_presence_list(self, user_localpart, accepted):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_room_events_max_id(self):
|
||||||
|
return 0 # TODO (erikj)
|
||||||
|
|
||||||
def _format_call(args, kwargs):
|
def _format_call(args, kwargs):
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
|
@ -53,7 +53,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (matrixService.isUserLoggedIn()) {
|
if (matrixService.isUserLoggedIn()) {
|
||||||
eventStreamService.resume();
|
// eventStreamService.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logs the user out
|
// Logs the user out
|
||||||
@ -66,7 +66,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
|||||||
matrixService.saveConfig();
|
matrixService.saveConfig();
|
||||||
|
|
||||||
// And go to the login page
|
// And go to the login page
|
||||||
$location.path("login");
|
$location.url("login");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to the event indicating that the access token is no longer valid.
|
// Listen to the event indicating that the access token is no longer valid.
|
||||||
|
@ -58,8 +58,11 @@ angular.module('matrixWebClient')
|
|||||||
angular.forEach(displayNames, function(value, key) {
|
angular.forEach(displayNames, function(value, key) {
|
||||||
if (value.length > 1) {
|
if (value.length > 1) {
|
||||||
// console.log(key + ": " + value);
|
// console.log(key + ": " + value);
|
||||||
for (i=0; i < value.length; i++) {
|
for (var i=0; i < value.length; i++) {
|
||||||
var v = value[i];
|
var v = value[i];
|
||||||
|
// FIXME: this permenantly rewrites the displayname for a given
|
||||||
|
// room member. which means we can't reset their name if it is
|
||||||
|
// no longer ambiguous!
|
||||||
members[v].displayname += " (" + v + ")";
|
members[v].displayname += " (" + v + ")";
|
||||||
// console.log(v + " " + members[v]);
|
// console.log(v + " " + members[v]);
|
||||||
};
|
};
|
||||||
|
@ -66,6 +66,10 @@ h1 {
|
|||||||
background-color: #faa;
|
background-color: #faa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mouse-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/*** Participant list ***/
|
/*** Participant list ***/
|
||||||
|
|
||||||
#usersTableWrapper {
|
#usersTableWrapper {
|
||||||
@ -89,7 +93,6 @@ h1 {
|
|||||||
height: 100px;
|
height: 100px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.userAvatar .userAvatarImage {
|
.userAvatar .userAvatarImage {
|
||||||
@ -142,6 +145,7 @@ h1 {
|
|||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messageTable td {
|
#messageTable td {
|
||||||
@ -149,7 +153,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.leftBlock {
|
.leftBlock {
|
||||||
width: 10em;
|
width: 14em;
|
||||||
|
word-wrap: break-word;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #888;
|
color: #888;
|
||||||
@ -187,24 +192,13 @@ h1 {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
|
||||||
background-color: #eee;
|
|
||||||
border: 1px solid #d8d8d8;
|
|
||||||
height: 31px;
|
|
||||||
display: inline-table;
|
|
||||||
max-width: 90%;
|
|
||||||
font-size: 16px;
|
|
||||||
/* word-wrap: break-word; */
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emote {
|
.emote {
|
||||||
background-color: #fff ! important;
|
background-color: transparent ! important;
|
||||||
border: 0px ! important;
|
border: 0px ! important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership {
|
.membership {
|
||||||
background-color: #fff ! important;
|
background-color: transparent ! important;
|
||||||
border: 0px ! important;
|
border: 0px ! important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,35 +210,68 @@ h1 {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #d8d8d8;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
max-width: 90%;
|
||||||
|
font-size: 16px;
|
||||||
|
word-wrap: break-word;
|
||||||
padding-top: 7px;
|
padding-top: 7px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
-webkit-text-size-adjust:100%
|
||||||
}
|
}
|
||||||
|
|
||||||
.differentUser td {
|
.differentUser td {
|
||||||
padding-top: 5px ! important;
|
padding-bottom: 5px ! important;
|
||||||
margin-top: 5px ! important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine {
|
.mine {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine .text {
|
.text.emote .bubble,
|
||||||
background-color: #f8f8ff ! important;
|
.text.membership .bubble,
|
||||||
}
|
.mine .text.emote .bubble,
|
||||||
|
.mine .text.membership .bubble
|
||||||
.mine .emote {
|
{
|
||||||
background-color: #fff ! important;
|
background-color: transparent ! important;
|
||||||
|
border: 0px ! important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine .text .bubble {
|
.mine .text .bubble {
|
||||||
|
background-color: #f8f8ff ! important;
|
||||||
text-align: left ! important;
|
text-align: left ! important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#room-fullscreen-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
height: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#room-fullscreen-image img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
overflow: auto;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/*** Profile ***/
|
/*** Profile ***/
|
||||||
|
|
||||||
.profile-avatar {
|
.profile-avatar {
|
||||||
|
@ -80,6 +80,6 @@ matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', functio
|
|||||||
$location.path("login");
|
$location.path("login");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
eventStreamService.resume();
|
// eventStreamService.resume();
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
@ -20,19 +20,20 @@
|
|||||||
/*
|
/*
|
||||||
* Upload an HTML5 file to a server
|
* Upload an HTML5 file to a server
|
||||||
*/
|
*/
|
||||||
angular.module('mFileUpload', [])
|
angular.module('mFileUpload', ['matrixService', 'mUtilities'])
|
||||||
.service('mFileUpload', ['matrixService', '$q', function (matrixService, $q) {
|
.service('mFileUpload', ['$q', 'matrixService', 'mUtilities', function ($q, matrixService, mUtilities) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Upload an HTML5 file to a server and returned a promise
|
* Upload an HTML5 file or blob to a server and returned a promise
|
||||||
* that will provide the URL of the uploaded file.
|
* that will provide the URL of the uploaded file.
|
||||||
|
* @param {File|Blob} file the file data to send
|
||||||
*/
|
*/
|
||||||
this.uploadFile = function(file) {
|
this.uploadFile = function(file) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
console.log("Uploading " + file.name + "... to /matrix/content");
|
console.log("Uploading " + file.name + "... to /matrix/content");
|
||||||
matrixService.uploadContent(file).then(
|
matrixService.uploadContent(file).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
var content_url = location.origin + "/matrix/content/" + response.data.content_token;
|
var content_url = response.data.content_token;
|
||||||
console.log(" -> Successfully uploaded! Available at " + content_url);
|
console.log(" -> Successfully uploaded! Available at " + content_url);
|
||||||
deferred.resolve(content_url);
|
deferred.resolve(content_url);
|
||||||
},
|
},
|
||||||
@ -44,4 +45,135 @@ angular.module('mFileUpload', [])
|
|||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Upload an image file plus generate a thumbnail of it and upload it so that
|
||||||
|
* we will have all information to fulfill an image message request data.
|
||||||
|
* @param {File} imageFile the imageFile to send
|
||||||
|
* @param {Integer} thumbnailSize the max side size of the thumbnail to create
|
||||||
|
* @returns {promise} A promise that will be resolved by a image message object
|
||||||
|
* ready to be send with the Matrix API
|
||||||
|
*/
|
||||||
|
this.uploadImageAndThumbnail = function(imageFile, thumbnailSize) {
|
||||||
|
var self = this;
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
console.log("uploadImageAndThumbnail " + imageFile.name + " - thumbnailSize: " + thumbnailSize);
|
||||||
|
|
||||||
|
// The message structure that will be returned in the promise
|
||||||
|
var imageMessage = {
|
||||||
|
msgtype: "m.image",
|
||||||
|
url: undefined,
|
||||||
|
body: {
|
||||||
|
size: undefined,
|
||||||
|
w: undefined,
|
||||||
|
h: undefined,
|
||||||
|
mimetype: undefined
|
||||||
|
},
|
||||||
|
thumbnail_url: undefined,
|
||||||
|
thumbnail_info: {
|
||||||
|
size: undefined,
|
||||||
|
w: undefined,
|
||||||
|
h: undefined,
|
||||||
|
mimetype: undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, get the image size
|
||||||
|
mUtilities.getImageSize(imageFile).then(
|
||||||
|
function(size) {
|
||||||
|
console.log("image size: " + JSON.stringify(size));
|
||||||
|
|
||||||
|
// The final operation: send imageFile
|
||||||
|
var uploadImage = function() {
|
||||||
|
self.uploadFile(imageFile).then(
|
||||||
|
function(url) {
|
||||||
|
// Update message metadata
|
||||||
|
imageMessage.url = url;
|
||||||
|
imageMessage.body = {
|
||||||
|
size: imageFile.size,
|
||||||
|
w: size.width,
|
||||||
|
h: size.height,
|
||||||
|
mimetype: imageFile.type
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there is no thumbnail (because the original image is smaller than thumbnailSize),
|
||||||
|
// reuse the original image info for thumbnail data
|
||||||
|
if (!imageMessage.thumbnail_url) {
|
||||||
|
imageMessage.thumbnail_url = imageMessage.url;
|
||||||
|
imageMessage.thumbnail_info = imageMessage.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are done
|
||||||
|
deferred.resolve(imageMessage);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log(" -> Can't upload image");
|
||||||
|
deferred.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a thumbnail if the image size exceeds thumbnailSize
|
||||||
|
if (Math.max(size.width, size.height) > thumbnailSize) {
|
||||||
|
console.log(" Creating thumbnail...");
|
||||||
|
mUtilities.resizeImage(imageFile, thumbnailSize).then(
|
||||||
|
function(thumbnailBlob) {
|
||||||
|
|
||||||
|
// Get its size
|
||||||
|
mUtilities.getImageSize(thumbnailBlob).then(
|
||||||
|
function(thumbnailSize) {
|
||||||
|
console.log(" -> Thumbnail size: " + JSON.stringify(thumbnailSize));
|
||||||
|
|
||||||
|
// Upload it to the server
|
||||||
|
self.uploadFile(thumbnailBlob).then(
|
||||||
|
function(thumbnailUrl) {
|
||||||
|
|
||||||
|
// Update image message data
|
||||||
|
imageMessage.thumbnail_url = thumbnailUrl;
|
||||||
|
imageMessage.thumbnail_info = {
|
||||||
|
size: thumbnailBlob.size,
|
||||||
|
w: thumbnailSize.width,
|
||||||
|
h: thumbnailSize.height,
|
||||||
|
mimetype: thumbnailBlob.type
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then, upload the original image
|
||||||
|
uploadImage();
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log(" -> Can't upload thumbnail");
|
||||||
|
deferred.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log(" -> Failed to get thumbnail size");
|
||||||
|
deferred.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log(" -> Failed to create thumbnail: " + error);
|
||||||
|
deferred.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No need of thumbnail
|
||||||
|
console.log(" Thumbnail is not required");
|
||||||
|
uploadImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log(" -> Failed to get image size");
|
||||||
|
deferred.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
@ -36,6 +36,8 @@ angular.module('eventHandlerService', [])
|
|||||||
rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
|
rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$rootScope.presence = {};
|
||||||
|
|
||||||
var initRoom = function(room_id) {
|
var initRoom = function(room_id) {
|
||||||
if (!(room_id in $rootScope.events.rooms)) {
|
if (!(room_id in $rootScope.events.rooms)) {
|
||||||
console.log("Creating new handler entry for " + room_id);
|
console.log("Creating new handler entry for " + room_id);
|
||||||
@ -45,6 +47,12 @@ angular.module('eventHandlerService', [])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reInitRoom = function(room_id) {
|
||||||
|
$rootScope.events.rooms[room_id] = {};
|
||||||
|
$rootScope.events.rooms[room_id].messages = [];
|
||||||
|
$rootScope.events.rooms[room_id].members = {};
|
||||||
|
}
|
||||||
|
|
||||||
var handleMessage = function(event, isLiveEvent) {
|
var handleMessage = function(event, isLiveEvent) {
|
||||||
if ("membership_target" in event.content) {
|
if ("membership_target" in event.content) {
|
||||||
event.user_id = event.content.membership_target;
|
event.user_id = event.content.membership_target;
|
||||||
@ -69,11 +77,23 @@ angular.module('eventHandlerService', [])
|
|||||||
|
|
||||||
var handleRoomMember = function(event, isLiveEvent) {
|
var handleRoomMember = function(event, isLiveEvent) {
|
||||||
initRoom(event.room_id);
|
initRoom(event.room_id);
|
||||||
|
|
||||||
|
// add membership changes as if they were a room message if something interesting changed
|
||||||
|
if (event.content.prev !== event.content.membership) {
|
||||||
|
if (isLiveEvent) {
|
||||||
|
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rootScope.events.rooms[event.room_id].members[event.user_id] = event;
|
$rootScope.events.rooms[event.room_id].members[event.user_id] = event;
|
||||||
$rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
|
$rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
var handlePresence = function(event, isLiveEvent) {
|
var handlePresence = function(event, isLiveEvent) {
|
||||||
|
$rootScope.presence[event.content.user_id] = event;
|
||||||
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,6 +127,10 @@ angular.module('eventHandlerService', [])
|
|||||||
for (var i=0; i<events.length; i++) {
|
for (var i=0; i<events.length; i++) {
|
||||||
this.handleEvent(events[i], isLiveEvents);
|
this.handleEvent(events[i], isLiveEvents);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
reInitRoom: function(room_id) {
|
||||||
|
reInitRoom(room_id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
@ -25,7 +25,6 @@ the eventHandlerService.
|
|||||||
angular.module('eventStreamService', [])
|
angular.module('eventStreamService', [])
|
||||||
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
|
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
|
||||||
var END = "END";
|
var END = "END";
|
||||||
var START = "START";
|
|
||||||
var TIMEOUT_MS = 30000;
|
var TIMEOUT_MS = 30000;
|
||||||
var ERR_TIMEOUT_MS = 5000;
|
var ERR_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
@ -50,10 +49,11 @@ angular.module('eventStreamService', [])
|
|||||||
localStorage.setItem("streamSettings", JSON.stringify(settings));
|
localStorage.setItem("streamSettings", JSON.stringify(settings));
|
||||||
};
|
};
|
||||||
|
|
||||||
var startEventStream = function() {
|
var doEventStream = function(deferred) {
|
||||||
settings.shouldPoll = true;
|
settings.shouldPoll = true;
|
||||||
settings.isActive = true;
|
settings.isActive = true;
|
||||||
var deferred = $q.defer();
|
deferred = deferred || $q.defer();
|
||||||
|
|
||||||
// run the stream from the latest token
|
// run the stream from the latest token
|
||||||
matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
|
matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
@ -64,13 +64,16 @@ angular.module('eventStreamService', [])
|
|||||||
|
|
||||||
settings.from = response.data.end;
|
settings.from = response.data.end;
|
||||||
|
|
||||||
console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end);
|
console.log(
|
||||||
|
"[EventStream] Got response from "+settings.from+
|
||||||
|
" to "+response.data.end
|
||||||
|
);
|
||||||
eventHandlerService.handleEvents(response.data.chunk, true);
|
eventHandlerService.handleEvents(response.data.chunk, true);
|
||||||
|
|
||||||
deferred.resolve(response);
|
deferred.resolve(response);
|
||||||
|
|
||||||
if (settings.shouldPoll) {
|
if (settings.shouldPoll) {
|
||||||
$timeout(startEventStream, 0);
|
$timeout(doEventStream, 0);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("[EventStream] Stopping poll.");
|
console.log("[EventStream] Stopping poll.");
|
||||||
@ -84,13 +87,48 @@ angular.module('eventStreamService', [])
|
|||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
|
|
||||||
if (settings.shouldPoll) {
|
if (settings.shouldPoll) {
|
||||||
$timeout(startEventStream, ERR_TIMEOUT_MS);
|
$timeout(doEventStream, ERR_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("[EventStream] Stopping polling.");
|
console.log("[EventStream] Stopping polling.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startEventStream = function() {
|
||||||
|
settings.shouldPoll = true;
|
||||||
|
settings.isActive = true;
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
// FIXME: We are discarding all the messages.
|
||||||
|
matrixService.rooms().then(
|
||||||
|
function(response) {
|
||||||
|
var rooms = response.data.rooms;
|
||||||
|
for (var i = 0; i < rooms.length; ++i) {
|
||||||
|
var room = rooms[i];
|
||||||
|
if ("state" in room) {
|
||||||
|
for (var j = 0; j < room.state.length; ++j) {
|
||||||
|
eventHandlerService.handleEvents(room.state[j], false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var presence = response.data.presence;
|
||||||
|
for (var i = 0; i < presence.length; ++i) {
|
||||||
|
eventHandlerService.handleEvent(presence[i], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.from = response.data.end
|
||||||
|
doEventStream(deferred);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Failure: " + error.data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,16 +61,23 @@ angular.module('matrixService', [])
|
|||||||
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
|
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
var doBaseRequest = function(baseUrl, method, path, params, data, headers) {
|
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
|
||||||
return $http({
|
|
||||||
|
var request = {
|
||||||
method: method,
|
method: method,
|
||||||
url: baseUrl + path,
|
url: baseUrl + path,
|
||||||
params: params,
|
params: params,
|
||||||
data: data,
|
data: data,
|
||||||
headers: headers
|
headers: headers
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add additional $http parameters
|
||||||
|
if ($httpParams) {
|
||||||
|
angular.extend(request, $httpParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $http(request);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/****** Home server API ******/
|
/****** Home server API ******/
|
||||||
@ -204,11 +211,11 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Send an image message
|
// Send an image message
|
||||||
sendImageMessage: function(room_id, image_url, image_alt, msg_id) {
|
sendImageMessage: function(room_id, image_url, image_body, msg_id) {
|
||||||
var content = {
|
var content = {
|
||||||
msgtype: "m.image",
|
msgtype: "m.image",
|
||||||
url: image_url,
|
url: image_url,
|
||||||
body: image_alt
|
body: image_body
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sendMessage(room_id, msg_id, content);
|
return this.sendMessage(room_id, msg_id, content);
|
||||||
@ -239,8 +246,8 @@ angular.module('matrixService', [])
|
|||||||
path = path.replace("$room_id", room_id);
|
path = path.replace("$room_id", room_id);
|
||||||
var params = {
|
var params = {
|
||||||
from: from_token,
|
from: from_token,
|
||||||
to: "START",
|
limit: limit,
|
||||||
limit: limit
|
dir: 'b'
|
||||||
};
|
};
|
||||||
return doRequest("GET", path, params);
|
return doRequest("GET", path, params);
|
||||||
},
|
},
|
||||||
@ -302,17 +309,25 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
// hit the Identity Server for a 3PID request.
|
// hit the Identity Server for a 3PID request.
|
||||||
linkEmail: function(email) {
|
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=abc123&email=" + encodeURIComponent(email);
|
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
|
||||||
var headers = {};
|
var headers = {};
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||||
},
|
},
|
||||||
|
|
||||||
authEmail: function(userId, tokenId, code) {
|
authEmail: function(clientSecret, tokenId, code) {
|
||||||
var path = "/matrix/identity/api/v1/validate/email/submitToken";
|
var path = "/matrix/identity/api/v1/validate/email/submitToken";
|
||||||
var data = "token="+code+"&mxId="+encodeURIComponent(userId)+"&tokenId="+tokenId;
|
var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||||
|
var headers = {};
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
|
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||||
|
},
|
||||||
|
|
||||||
|
bindEmail: function(userId, tokenId, clientSecret) {
|
||||||
|
var path = "/matrix/identity/api/v1/3pid/bind";
|
||||||
|
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||||
var headers = {};
|
var headers = {};
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||||
@ -326,7 +341,17 @@ angular.module('matrixService', [])
|
|||||||
var params = {
|
var params = {
|
||||||
access_token: config.access_token
|
access_token: config.access_token
|
||||||
};
|
};
|
||||||
return doBaseRequest(config.homeserver, "POST", path, params, file, headers);
|
|
||||||
|
// If the file is actually a Blob object, prevent $http from JSON-stringified it before sending
|
||||||
|
// (Equivalent to jQuery ajax processData = false)
|
||||||
|
var $httpParams;
|
||||||
|
if (file instanceof Blob) {
|
||||||
|
$httpParams = {
|
||||||
|
transformRequest: angular.identity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
|
||||||
},
|
},
|
||||||
|
|
||||||
// start listening on /events
|
// start listening on /events
|
||||||
@ -375,6 +400,7 @@ angular.module('matrixService', [])
|
|||||||
// Set a new config (Use saveConfig to actually store it permanently)
|
// Set a new config (Use saveConfig to actually store it permanently)
|
||||||
setConfig: function(newConfig) {
|
setConfig: function(newConfig) {
|
||||||
config = newConfig;
|
config = newConfig;
|
||||||
|
console.log("new IS: "+config.identityServer);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Commits config into permanent storage
|
// Commits config into permanent storage
|
||||||
|
151
webclient/components/utilities/utilities-service.js
Normal file
151
webclient/components/utilities/utilities-service.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This service contains multipurpose helper functions.
|
||||||
|
*/
|
||||||
|
angular.module('mUtilities', [])
|
||||||
|
.service('mUtilities', ['$q', function ($q) {
|
||||||
|
/*
|
||||||
|
* Get the size of an image
|
||||||
|
* @param {File|Blob} imageFile the file containing the image
|
||||||
|
* @returns {promise} A promise that will be resolved by an object with 2 members:
|
||||||
|
* width & height
|
||||||
|
*/
|
||||||
|
this.getImageSize = function(imageFile) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
// Load the file into an html element
|
||||||
|
var img = document.createElement("img");
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
img.src = e.target.result;
|
||||||
|
|
||||||
|
// Once ready, returns its size
|
||||||
|
img.onload = function() {
|
||||||
|
deferred.resolve({
|
||||||
|
width: img.width,
|
||||||
|
height: img.height
|
||||||
|
});
|
||||||
|
};
|
||||||
|
img.onerror = function(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reader.onerror = function(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(imageFile);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resize the image to fit in a square of the side maxSize.
|
||||||
|
* The aspect ratio is kept. The returned image data uses JPEG compression.
|
||||||
|
* Source: http://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/
|
||||||
|
* @param {File} imageFile the file containing the image
|
||||||
|
* @param {Integer} maxSize the max side size
|
||||||
|
* @returns {promise} A promise that will be resolved by a Blob object containing
|
||||||
|
* the resized image data
|
||||||
|
*/
|
||||||
|
this.resizeImage = function(imageFile, maxSize) {
|
||||||
|
var self = this;
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
var canvas = document.createElement("canvas");
|
||||||
|
|
||||||
|
var img = document.createElement("img");
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
|
||||||
|
img.src = e.target.result;
|
||||||
|
|
||||||
|
// Once ready, returns its size
|
||||||
|
img.onload = function() {
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
var MAX_WIDTH = maxSize;
|
||||||
|
var MAX_HEIGHT = maxSize;
|
||||||
|
var width = img.width;
|
||||||
|
var height = img.height;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
if (width > MAX_WIDTH) {
|
||||||
|
height *= MAX_WIDTH / width;
|
||||||
|
width = MAX_WIDTH;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > MAX_HEIGHT) {
|
||||||
|
width *= MAX_HEIGHT / height;
|
||||||
|
height = MAX_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Extract image data in the same format as the original one.
|
||||||
|
// The 0.7 compression value will work with formats that supports it like JPEG.
|
||||||
|
var dataUrl = canvas.toDataURL(imageFile.type, 0.7);
|
||||||
|
deferred.resolve(self.dataURItoBlob(dataUrl));
|
||||||
|
};
|
||||||
|
img.onerror = function(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reader.onerror = function(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(imageFile);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert a dataURI string to a blob
|
||||||
|
* Source: http://stackoverflow.com/a/17682951
|
||||||
|
* @param {String} dataURI the dataURI can be a base64 encoded string or an URL encoded string.
|
||||||
|
* @returns {Blob} the blob
|
||||||
|
*/
|
||||||
|
this.dataURItoBlob = function(dataURI) {
|
||||||
|
// convert base64 to raw binary data held in a string
|
||||||
|
// doesn't handle URLEncoded DataURIs
|
||||||
|
var byteString;
|
||||||
|
if (dataURI.split(',')[0].indexOf('base64') >= 0)
|
||||||
|
byteString = atob(dataURI.split(',')[1]);
|
||||||
|
else
|
||||||
|
byteString = unescape(dataURI.split(',')[1]);
|
||||||
|
// separate out the mime component
|
||||||
|
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||||
|
|
||||||
|
// write the bytes of the string to an ArrayBuffer
|
||||||
|
var ab = new ArrayBuffer(byteString.length);
|
||||||
|
var ia = new Uint8Array(ab);
|
||||||
|
for (var i = 0; i < byteString.length; i++) {
|
||||||
|
ia[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the ArrayBuffer to a blob, and you're done
|
||||||
|
return new Blob([ab],{type: mimeString});
|
||||||
|
};
|
||||||
|
|
||||||
|
}]);
|
@ -25,6 +25,7 @@
|
|||||||
<script src="components/matrix/event-handler-service.js"></script>
|
<script src="components/matrix/event-handler-service.js"></script>
|
||||||
<script src="components/fileInput/file-input-directive.js"></script>
|
<script src="components/fileInput/file-input-directive.js"></script>
|
||||||
<script src="components/fileUpload/file-upload-service.js"></script>
|
<script src="components/fileUpload/file-upload-service.js"></script>
|
||||||
|
<script src="components/utilities/utilities-service.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -53,7 +53,7 @@ angular.module('LoginController', ['matrixService'])
|
|||||||
matrixService.saveConfig();
|
matrixService.saveConfig();
|
||||||
eventStreamService.resume();
|
eventStreamService.resume();
|
||||||
// Go to the user's rooms list page
|
// Go to the user's rooms list page
|
||||||
$location.path("rooms");
|
$location.url("rooms");
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
if (error.data) {
|
if (error.data) {
|
||||||
@ -70,6 +70,7 @@ angular.module('LoginController', ['matrixService'])
|
|||||||
$scope.login = function() {
|
$scope.login = function() {
|
||||||
matrixService.setConfig({
|
matrixService.setConfig({
|
||||||
homeserver: $scope.account.homeserver,
|
homeserver: $scope.account.homeserver,
|
||||||
|
identityServer: $scope.account.identityServer,
|
||||||
user_id: $scope.account.user_id
|
user_id: $scope.account.user_id
|
||||||
});
|
});
|
||||||
// try to login
|
// try to login
|
||||||
@ -79,12 +80,13 @@ angular.module('LoginController', ['matrixService'])
|
|||||||
$scope.feedback = "Login successful.";
|
$scope.feedback = "Login successful.";
|
||||||
matrixService.setConfig({
|
matrixService.setConfig({
|
||||||
homeserver: $scope.account.homeserver,
|
homeserver: $scope.account.homeserver,
|
||||||
|
identityServer: $scope.account.identityServer,
|
||||||
user_id: response.data.user_id,
|
user_id: response.data.user_id,
|
||||||
access_token: response.data.access_token
|
access_token: response.data.access_token
|
||||||
});
|
});
|
||||||
matrixService.saveConfig();
|
matrixService.saveConfig();
|
||||||
eventStreamService.resume();
|
eventStreamService.resume();
|
||||||
$location.path("rooms");
|
$location.url("rooms");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);
|
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);
|
||||||
|
@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('RoomController', ['ngSanitize'])
|
angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService',
|
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
|
||||||
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService) {
|
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var MESSAGES_PER_PAGINATION = 30;
|
var MESSAGES_PER_PAGINATION = 30;
|
||||||
|
var THUMBNAIL_SIZE = 320;
|
||||||
|
|
||||||
// Room ids. Computed and resolved in onInit
|
// Room ids. Computed and resolved in onInit
|
||||||
$scope.room_id = undefined;
|
$scope.room_id = undefined;
|
||||||
@ -28,9 +29,12 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
user_id: matrixService.config().user_id,
|
user_id: matrixService.config().user_id,
|
||||||
events_from: "END", // when to start the event stream from.
|
events_from: "END", // when to start the event stream from.
|
||||||
earliest_token: "END", // stores how far back we've paginated.
|
earliest_token: "END", // stores how far back we've paginated.
|
||||||
|
first_pagination: true, // this is toggled off when the first pagination is done
|
||||||
can_paginate: true, // this is toggled off when we run out of items
|
can_paginate: true, // this is toggled off when we run out of items
|
||||||
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
||||||
stream_failure: undefined // the response when the stream fails
|
stream_failure: undefined, // the response when the stream fails
|
||||||
|
// FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
|
||||||
|
sending: false // true when a message is being sent. It helps to disable the UI when a process is running
|
||||||
};
|
};
|
||||||
$scope.members = {};
|
$scope.members = {};
|
||||||
$scope.autoCompleting = false;
|
$scope.autoCompleting = false;
|
||||||
@ -88,7 +92,7 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
|
|
||||||
var paginate = function(numItems) {
|
var paginate = function(numItems) {
|
||||||
// console.log("paginate " + numItems);
|
// console.log("paginate " + numItems);
|
||||||
if ($scope.state.paginating) {
|
if ($scope.state.paginating || !$scope.room_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -98,7 +102,6 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
var originalTopRow = $("#messageTable>tbody>tr:first")[0];
|
var originalTopRow = $("#messageTable>tbody>tr:first")[0];
|
||||||
matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
|
matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
var firstPagination = !$scope.events.rooms[$scope.room_id];
|
|
||||||
eventHandlerService.handleEvents(response.data.chunk, false);
|
eventHandlerService.handleEvents(response.data.chunk, false);
|
||||||
$scope.state.earliest_token = response.data.end;
|
$scope.state.earliest_token = response.data.end;
|
||||||
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
|
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
|
||||||
@ -124,8 +127,9 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstPagination) {
|
if ($scope.state.first_pagination) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
$scope.state.first_pagination = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// lock the scroll position
|
// lock the scroll position
|
||||||
@ -144,10 +148,12 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
console.log("Failed to paginateBackMessages: " + JSON.stringify(error));
|
console.log("Failed to paginateBackMessages: " + JSON.stringify(error));
|
||||||
$scope.state.paginating = false;
|
$scope.state.paginating = false;
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
var updateMemberList = function(chunk) {
|
var updateMemberList = function(chunk) {
|
||||||
|
if (chunk.room_id != $scope.room_id) return;
|
||||||
|
|
||||||
var isNewMember = !(chunk.target_user_id in $scope.members);
|
var isNewMember = !(chunk.target_user_id in $scope.members);
|
||||||
if (isNewMember) {
|
if (isNewMember) {
|
||||||
// FIXME: why are we copying these fields around inside chunk?
|
// FIXME: why are we copying these fields around inside chunk?
|
||||||
@ -157,8 +163,7 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
if ("mtime_age" in chunk.content) {
|
if ("mtime_age" in chunk.content) {
|
||||||
chunk.mtime_age = chunk.content.mtime_age;
|
chunk.mtime_age = chunk.content.mtime_age;
|
||||||
}
|
}
|
||||||
/*
|
// Once the HS reliably returns the displaynames & avatar_urls for both
|
||||||
// FIXME: once the HS reliably returns the displaynames & avatar_urls for both
|
|
||||||
// local and remote users, we should use this rather than the evalAsync block
|
// local and remote users, we should use this rather than the evalAsync block
|
||||||
// below
|
// below
|
||||||
if ("displayname" in chunk.content) {
|
if ("displayname" in chunk.content) {
|
||||||
@ -167,9 +172,11 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
if ("avatar_url" in chunk.content) {
|
if ("avatar_url" in chunk.content) {
|
||||||
chunk.avatar_url = chunk.content.avatar_url;
|
chunk.avatar_url = chunk.content.avatar_url;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
$scope.members[chunk.target_user_id] = chunk;
|
$scope.members[chunk.target_user_id] = chunk;
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Stale code for explicitly hammering the homeserver for every displayname & avatar_url
|
||||||
|
|
||||||
// get their display name and profile picture and set it to their
|
// get their display name and profile picture and set it to their
|
||||||
// member entry in $scope.members. We HAVE to use $timeout with 0 delay
|
// member entry in $scope.members. We HAVE to use $timeout with 0 delay
|
||||||
// to make this function run AFTER the current digest cycle, else the
|
// to make this function run AFTER the current digest cycle, else the
|
||||||
@ -193,6 +200,11 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (chunk.target_user_id in $rootScope.presence) {
|
||||||
|
updatePresence($rootScope.presence[chunk.target_user_id]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// selectively update membership else it will nuke the picture and displayname too :/
|
// selectively update membership else it will nuke the picture and displayname too :/
|
||||||
@ -233,6 +245,8 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.state.sending = true;
|
||||||
|
|
||||||
// Send the text message
|
// Send the text message
|
||||||
var promise;
|
var promise;
|
||||||
// FIXME: handle other commands too
|
// FIXME: handle other commands too
|
||||||
@ -247,14 +261,15 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
function() {
|
function() {
|
||||||
console.log("Sent message");
|
console.log("Sent message");
|
||||||
$scope.textInput = "";
|
$scope.textInput = "";
|
||||||
|
$scope.state.sending = false;
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to send: " + error.data.error;
|
$scope.feedback = "Failed to send: " + error.data.error;
|
||||||
|
$scope.state.sending = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.onInit = function() {
|
$scope.onInit = function() {
|
||||||
// $timeout(function() { document.getElementById('textInput').focus() }, 0);
|
|
||||||
console.log("onInit");
|
console.log("onInit");
|
||||||
|
|
||||||
// Does the room ID provided in the URL?
|
// Does the room ID provided in the URL?
|
||||||
@ -284,7 +299,7 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
else {
|
else {
|
||||||
// In case of issue, go to the default page
|
// In case of issue, go to the default page
|
||||||
console.log("Error: cannot extract room alias");
|
console.log("Error: cannot extract room alias");
|
||||||
$location.path("/");
|
$location.url("/");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,12 +316,14 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
function () {
|
function () {
|
||||||
// In case of issue, go to the default page
|
// In case of issue, go to the default page
|
||||||
console.log("Error: cannot resolve room alias");
|
console.log("Error: cannot resolve room alias");
|
||||||
$location.path("/");
|
$location.url("/");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var onInit2 = function() {
|
var onInit2 = function() {
|
||||||
|
eventHandlerService.reInitRoom($scope.room_id);
|
||||||
|
|
||||||
// Join the room
|
// Join the room
|
||||||
matrixService.join($scope.room_id).then(
|
matrixService.join($scope.room_id).then(
|
||||||
function() {
|
function() {
|
||||||
@ -319,6 +336,7 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
var chunk = response.data.chunk[i];
|
var chunk = response.data.chunk[i];
|
||||||
updateMemberList(chunk);
|
updateMemberList(chunk);
|
||||||
}
|
}
|
||||||
|
eventStreamService.resume();
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed get member list: " + error.data.error;
|
$scope.feedback = "Failed get member list: " + error.data.error;
|
||||||
@ -354,35 +372,50 @@ angular.module('RoomController', ['ngSanitize'])
|
|||||||
matrixService.leave($scope.room_id).then(
|
matrixService.leave($scope.room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
console.log("Left room ");
|
console.log("Left room ");
|
||||||
$location.path("rooms");
|
$location.url("rooms");
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to leave room: " + error.data.error;
|
$scope.feedback = "Failed to leave room: " + error.data.error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.sendImage = function(url) {
|
$scope.sendImage = function(url, body) {
|
||||||
matrixService.sendImageMessage($scope.room_id, url).then(
|
$scope.state.sending = true;
|
||||||
|
|
||||||
|
matrixService.sendImageMessage($scope.room_id, url, body).then(
|
||||||
function() {
|
function() {
|
||||||
console.log("Image sent");
|
console.log("Image sent");
|
||||||
|
$scope.state.sending = false;
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to send image: " + error.data.error;
|
$scope.feedback = "Failed to send image: " + error.data.error;
|
||||||
|
$scope.state.sending = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.imageFileToSend;
|
$scope.imageFileToSend;
|
||||||
$scope.$watch("imageFileToSend", function(newValue, oldValue) {
|
$scope.$watch("imageFileToSend", function(newValue, oldValue) {
|
||||||
if ($scope.imageFileToSend) {
|
if ($scope.imageFileToSend) {
|
||||||
// First download the image to the Internet
|
|
||||||
console.log("Uploading image...");
|
$scope.state.sending = true;
|
||||||
mFileUpload.uploadFile($scope.imageFileToSend).then(
|
|
||||||
function(url) {
|
// Upload this image with its thumbnail to Internet
|
||||||
// Then share the URL
|
mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then(
|
||||||
$scope.sendImage(url);
|
function(imageMessage) {
|
||||||
|
// imageMessage is complete message structure, send it as is
|
||||||
|
matrixService.sendMessage($scope.room_id, undefined, imageMessage).then(
|
||||||
|
function() {
|
||||||
|
console.log("Image message sent");
|
||||||
|
$scope.state.sending = false;
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Failed to send image message: " + error.data.error;
|
||||||
|
$scope.state.sending = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Can't upload image";
|
$scope.feedback = "Can't upload image";
|
||||||
|
$scope.state.sending = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -17,30 +17,30 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RoomController')
|
angular.module('RoomController')
|
||||||
.directive('autoComplete', ['$timeout', function ($timeout) {
|
.directive('tabComplete', ['$timeout', function ($timeout) {
|
||||||
return function (scope, element, attrs) {
|
return function (scope, element, attrs) {
|
||||||
element.bind("keydown keypress", function (event) {
|
element.bind("keydown keypress", function (event) {
|
||||||
// console.log("event: " + event.which);
|
// console.log("event: " + event.which);
|
||||||
if (event.which === 9) {
|
if (event.which === 9) {
|
||||||
if (!scope.autoCompleting) { // cache our starting text
|
if (!scope.tabCompleting) { // cache our starting text
|
||||||
// console.log("caching " + element[0].value);
|
// console.log("caching " + element[0].value);
|
||||||
scope.autoCompleteOriginal = element[0].value;
|
scope.tabCompleteOriginal = element[0].value;
|
||||||
scope.autoCompleting = true;
|
scope.tabCompleting = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
scope.autoCompleteIndex--;
|
scope.tabCompleteIndex--;
|
||||||
if (scope.autoCompleteIndex < 0) {
|
if (scope.tabCompleteIndex < 0) {
|
||||||
scope.autoCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.autoCompleteIndex++;
|
scope.tabCompleteIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchIndex = 0;
|
var searchIndex = 0;
|
||||||
var targetIndex = scope.autoCompleteIndex;
|
var targetIndex = scope.tabCompleteIndex;
|
||||||
var text = scope.autoCompleteOriginal;
|
var text = scope.tabCompleteOriginal;
|
||||||
|
|
||||||
// console.log("targetIndex: " + targetIndex + ", text=" + text);
|
// console.log("targetIndex: " + targetIndex + ", text=" + text);
|
||||||
|
|
||||||
@ -90,17 +90,17 @@ angular.module('RoomController')
|
|||||||
element[0].className = "";
|
element[0].className = "";
|
||||||
}, 150);
|
}, 150);
|
||||||
element[0].value = text;
|
element[0].value = text;
|
||||||
scope.autoCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.autoCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
else if (event.which !== 16 && scope.autoCompleting) {
|
else if (event.which !== 16 && scope.tabCompleting) {
|
||||||
scope.autoCompleting = false;
|
scope.tabCompleting = false;
|
||||||
scope.autoCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div id="usersTableWrapper">
|
<div id="usersTableWrapper">
|
||||||
<table id="usersTable">
|
<table id="usersTable">
|
||||||
<tr ng-repeat="member in members | orderMembersList">
|
<tr ng-repeat="member in members | orderMembersList">
|
||||||
<td class="userAvatar" ng-click="goToUserPage(member.id)">
|
<td class="userAvatar mouse-pointer" ng-click="goToUserPage(member.id)">
|
||||||
<img class="userAvatarImage"
|
<img class="userAvatarImage"
|
||||||
ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}"
|
ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}"
|
||||||
alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
|
alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
|
||||||
@ -26,22 +26,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messageTableWrapper" keep-scroll>
|
<div id="messageTableWrapper" keep-scroll>
|
||||||
|
<!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
|
||||||
<table id="messageTable" infinite-scroll="paginateMore()">
|
<table id="messageTable" infinite-scroll="paginateMore()">
|
||||||
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
||||||
ng-class="(events.rooms[room_id].messages[$index - 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
||||||
<td class="leftBlock">
|
<td class="leftBlock">
|
||||||
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
||||||
<div class="timestamp">{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}</div>
|
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm:ss' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="avatar">
|
<td class="avatar">
|
||||||
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
|
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
|
||||||
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
|
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
|
||||||
</td>
|
</td>
|
||||||
<td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
|
<span ng-hide='msg.type !== "m.room.member"'>
|
||||||
|
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||||
|
{{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
|
||||||
|
{{ msg.content.target_id || '' }}
|
||||||
|
</span>
|
||||||
<span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
<span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
||||||
<span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="msg.content.body | linky:'_blank'"/>
|
<span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
|
||||||
<img class="image" ng-hide='msg.content.msgtype !== "m.image"' ng-src="{{ msg.content.url }}" alt="{{ msg.content.body }}"/>
|
<div ng-show='msg.content.msgtype === "m.image"'>
|
||||||
|
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
|
||||||
|
<img class="image" ng-src="{{ msg.content.url }}"/>
|
||||||
|
</div>
|
||||||
|
<div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
|
||||||
|
<img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"
|
||||||
|
ng-click="$parent.fullScreenImageURL = msg.content.url"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="rightBlock">
|
<td class="rightBlock">
|
||||||
@ -63,30 +77,16 @@
|
|||||||
{{ state.user_id }}
|
{{ state.user_id }}
|
||||||
</td>
|
</td>
|
||||||
<td width="*" style="min-width: 100px">
|
<td width="*" style="min-width: 100px">
|
||||||
<input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" auto-complete/>
|
<input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
|
||||||
</td>
|
</td>
|
||||||
<td width="1">
|
<td width="150px">
|
||||||
<button ng-click="send()">Send</button>
|
<button ng-click="send()">Send</button>
|
||||||
|
<button m-file-input="imageFileToSend">Send Image</button>
|
||||||
</td>
|
</td>
|
||||||
<td width="1">
|
<td width="1">
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input id="mainInput" ng-model="imageURLToSend" ng-enter="sendImage(imageURLToSend)" placeholder="Image URL"/>
|
|
||||||
</td>
|
|
||||||
<td width="100px">
|
|
||||||
<button ng-click="sendImage(imageURLToSend)">Send URL</button>
|
|
||||||
</td>
|
|
||||||
<!-- TODO: To enable once we have an upload server
|
|
||||||
<td width="100px">
|
|
||||||
<button m-file-input="imageFileToSend">Send Image</button>
|
|
||||||
</td>
|
|
||||||
-->
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
@ -103,4 +103,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="room-fullscreen-image" ng-show="fullScreenImageURL" ng-click="fullScreenImageURL = undefined;">
|
||||||
|
<img ng-src="{{ fullScreenImageURL }}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,8 +17,8 @@ limitations under the License.
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
|
angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
|
||||||
.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService',
|
.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService',
|
||||||
function($scope, $location, matrixService, mFileUpload, eventHandlerService) {
|
function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
|
||||||
|
|
||||||
$scope.rooms = {};
|
$scope.rooms = {};
|
||||||
$scope.public_rooms = [];
|
$scope.public_rooms = [];
|
||||||
@ -48,6 +48,8 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
linkNewEmail: "", // the email entry box
|
linkNewEmail: "", // the email entry box
|
||||||
emailBeingAuthed: undefined, // to populate verification text
|
emailBeingAuthed: undefined, // to populate verification text
|
||||||
authTokenId: undefined, // the token id from the IS
|
authTokenId: undefined, // the token id from the IS
|
||||||
|
clientSecret: undefined, // our client secret
|
||||||
|
sendAttempt: 1,
|
||||||
emailCode: "", // the code entry box
|
emailCode: "", // the code entry box
|
||||||
linkedEmailList: matrixService.config().emailList // linked email list
|
linkedEmailList: matrixService.config().emailList // linked email list
|
||||||
};
|
};
|
||||||
@ -59,7 +61,7 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
// FIXME push membership to top level key to match /im/sync
|
// FIXME push membership to top level key to match /im/sync
|
||||||
event.membership = event.content.membership;
|
event.membership = event.content.membership;
|
||||||
// FIXME bodge a nicer name than the room ID for this invite.
|
// FIXME bodge a nicer name than the room ID for this invite.
|
||||||
event.room_alias = event.user_id + "'s room";
|
event.room_display_name = event.user_id + "'s room";
|
||||||
$scope.rooms[event.room_id] = event;
|
$scope.rooms[event.room_id] = event;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -70,14 +72,20 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
if (alias) {
|
if (alias) {
|
||||||
// use the existing alias from storage
|
// use the existing alias from storage
|
||||||
data[i].room_alias = alias;
|
data[i].room_alias = alias;
|
||||||
|
data[i].room_display_name = alias;
|
||||||
}
|
}
|
||||||
else if (data[i].room_alias) {
|
else if (data[i].aliases && data[i].aliases[0]) {
|
||||||
// save the mapping
|
// save the mapping
|
||||||
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].room_alias);
|
// TODO: select the smarter alias from the array
|
||||||
|
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
|
||||||
|
data[i].room_display_name = data[i].aliases[0];
|
||||||
|
}
|
||||||
|
else if (data[i].membership == "invite" && "inviter" in data[i]) {
|
||||||
|
data[i].room_display_name = data[i].inviter + "'s room"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// last resort use the room id
|
// last resort use the room id
|
||||||
data[i].room_alias = data[i].room_id;
|
data[i].room_display_name = data[i].room_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
@ -87,11 +95,16 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
// List all rooms joined or been invited to
|
// List all rooms joined or been invited to
|
||||||
matrixService.rooms().then(
|
matrixService.rooms().then(
|
||||||
function(response) {
|
function(response) {
|
||||||
var data = assignRoomAliases(response.data);
|
var data = assignRoomAliases(response.data.rooms);
|
||||||
$scope.feedback = "Success";
|
$scope.feedback = "Success";
|
||||||
for (var i=0; i<data.length; i++) {
|
for (var i=0; i<data.length; i++) {
|
||||||
$scope.rooms[data[i].room_id] = data[i];
|
$scope.rooms[data[i].room_id] = data[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var presence = response.data.presence;
|
||||||
|
for (var i = 0; i < presence.length; ++i) {
|
||||||
|
eventHandlerService.handleEvent(presence[i], false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failure: " + error.data;
|
$scope.feedback = "Failure: " + error.data;
|
||||||
@ -102,6 +115,8 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
eventStreamService.resume();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createNewRoom = function(room_id, isPrivate) {
|
$scope.createNewRoom = function(room_id, isPrivate) {
|
||||||
@ -128,17 +143,17 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
// Go to a room
|
// Go to a room
|
||||||
$scope.goToRoom = function(room_id) {
|
$scope.goToRoom = function(room_id) {
|
||||||
// Simply open the room page on this room id
|
// Simply open the room page on this room id
|
||||||
//$location.path("room/" + room_id);
|
//$location.url("room/" + room_id);
|
||||||
matrixService.join(room_id).then(
|
matrixService.join(room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.data.hasOwnProperty("room_id")) {
|
if (response.data.hasOwnProperty("room_id")) {
|
||||||
if (response.data.room_id != room_id) {
|
if (response.data.room_id != room_id) {
|
||||||
$location.path("room/" + response.data.room_id);
|
$location.url("room/" + response.data.room_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$location.path("room/" + room_id);
|
$location.url("room/" + room_id);
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Can't join room: " + error.data;
|
$scope.feedback = "Can't join room: " + error.data;
|
||||||
@ -150,7 +165,7 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
matrixService.joinAlias(room_alias).then(
|
matrixService.joinAlias(room_alias).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
// Go to this room
|
// Go to this room
|
||||||
$location.path("room/" + room_alias);
|
$location.url("room/" + room_alias);
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Can't join room: " + error.data;
|
$scope.feedback = "Can't join room: " + error.data;
|
||||||
@ -206,11 +221,27 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var generateClientSecret = function() {
|
||||||
|
var ret = "";
|
||||||
|
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
|
for (var i = 0; i < 32; i++) {
|
||||||
|
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
$scope.linkEmail = function(email) {
|
$scope.linkEmail = function(email) {
|
||||||
matrixService.linkEmail(email).then(
|
if (email != $scope.linkedEmails.emailBeingAuthed) {
|
||||||
|
$scope.linkedEmails.clientSecret = generateClientSecret();
|
||||||
|
$scope.linkedEmails.sendAttempt = 1;
|
||||||
|
}
|
||||||
|
matrixService.linkEmail(email, $scope.linkedEmails.clientSecret, $scope.linkedEmails.sendAttempt).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.data.success === true) {
|
if (response.data.success === true) {
|
||||||
$scope.linkedEmails.authTokenId = response.data.tokenId;
|
$scope.linkedEmails.authTokenId = response.data.sid;
|
||||||
$scope.emailFeedback = "You have been sent an email.";
|
$scope.emailFeedback = "You have been sent an email.";
|
||||||
$scope.linkedEmails.emailBeingAuthed = email;
|
$scope.linkedEmails.emailBeingAuthed = email;
|
||||||
}
|
}
|
||||||
@ -230,18 +261,20 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
matrixService.authEmail(matrixService.config().user_id, tokenId, code, $scope.linkedEmails.clientSecret).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if ("success" in response.data && response.data.success === false) {
|
if ("success" in response.data && response.data.success === false) {
|
||||||
$scope.emailFeedback = "Failed to authenticate email.";
|
$scope.emailFeedback = "Failed to authenticate email.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.linkedEmails.clientSecret).then(
|
||||||
|
function(response) {
|
||||||
var config = matrixService.config();
|
var config = matrixService.config();
|
||||||
var emailList = {};
|
var emailList = {};
|
||||||
if ("emailList" in config) {
|
if ("emailList" in config) {
|
||||||
emailList = config.emailList;
|
emailList = config.emailList;
|
||||||
}
|
}
|
||||||
emailList[response.address] = response;
|
emailList[$scope.linkedEmails.emailBeingAuthed] = response;
|
||||||
// save the new email list
|
// save the new email list
|
||||||
config.emailList = emailList;
|
config.emailList = emailList;
|
||||||
matrixService.setConfig(config);
|
matrixService.setConfig(config);
|
||||||
@ -252,6 +285,10 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
|
|||||||
$scope.linkedEmails.linkedEmailList = emailList;
|
$scope.linkedEmails.linkedEmailList = emailList;
|
||||||
$scope.linkedEmails.linkNewEmail = "";
|
$scope.linkedEmails.linkNewEmail = "";
|
||||||
$scope.linkedEmails.emailCode = "";
|
$scope.linkedEmails.emailCode = "";
|
||||||
|
}, function(reason) {
|
||||||
|
$scope.emailFeedback = "Failed to link email: " + reason;
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(reason) {
|
||||||
$scope.emailFeedback = "Failed to auth email: " + reason;
|
$scope.emailFeedback = "Failed to auth email: " + reason;
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
|
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
|
||||||
<div>
|
<div>
|
||||||
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_alias }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
|
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
|
Loading…
Reference in New Issue
Block a user