mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-10-01 11:49:51 -04:00
Merge branch 'release-v0.4.2' of github.com:matrix-org/synapse
This commit is contained in:
commit
b63691f6e2
16
CHANGES.rst
16
CHANGES.rst
@ -1,3 +1,19 @@
|
||||
Changes in synapse 0.4.2 (2014-10-31)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* Fix bugs where we did not notify users of correct presence updates.
|
||||
* Fix bug where we did not handle sub second event stream timeouts.
|
||||
|
||||
Webclient:
|
||||
* Add ability to click on messages to see JSON.
|
||||
* Add ability to redact messages.
|
||||
* Add ability to view and edit all room state JSON.
|
||||
* Handle incoming redactions.
|
||||
* Improve feedback on errors.
|
||||
* Fix bugs in mobile CSS.
|
||||
* Fix bugs with desktop notifications.
|
||||
|
||||
Changes in synapse 0.4.1 (2014-10-17)
|
||||
=====================================
|
||||
Webclient:
|
||||
|
@ -14,3 +14,4 @@ fi
|
||||
find "$DIR" -name "*.log" -delete
|
||||
find "$DIR" -name "*.db" -delete
|
||||
|
||||
rm -rf $DIR/etc
|
||||
|
@ -8,6 +8,14 @@ cd "$DIR/.."
|
||||
|
||||
mkdir -p demo/etc
|
||||
|
||||
# Check the --no-rate-limit param
|
||||
PARAMS=""
|
||||
if [ $# -eq 1 ]; then
|
||||
if [ $1 = "--no-rate-limit" ]; then
|
||||
PARAMS="--rc-messages-per-second 1000 --rc-message-burst-count 1000"
|
||||
fi
|
||||
fi
|
||||
|
||||
for port in 8080 8081 8082; do
|
||||
echo "Starting server on port $port... "
|
||||
|
||||
@ -23,7 +31,8 @@ for port in 8080 8081 8082; do
|
||||
-d "$DIR/$port.db" \
|
||||
-D --pid-file "$DIR/$port.pid" \
|
||||
--manhole $((port + 1000)) \
|
||||
--tls-dh-params-path "demo/demo.tls.dh"
|
||||
--tls-dh-params-path "demo/demo.tls.dh" \
|
||||
$PARAMS
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
--config-path "demo/etc/$port.config" \
|
||||
|
280
pylint.cfg
Normal file
280
pylint.cfg
Normal file
@ -0,0 +1,280 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Profiled execution.
|
||||
profile=no
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time. See also the "--disable" option for examples.
|
||||
#enable=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=missing-docstring
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]".
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Add a comment according to your evaluation note. This is used by the global
|
||||
# evaluation report (RP0004).
|
||||
comment=no
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set).
|
||||
ignored-classes=SQLObject
|
||||
|
||||
# When zope mode is activated, add a predefined set of Zope acquired attributes
|
||||
# to generated-members.
|
||||
zope=no
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=REQUEST,acl_users,aq_parent
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the beginning of the name of dummy variables
|
||||
# (i.e. not used).
|
||||
dummy-variables-rgx=_$|dummy
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Required attributes for module, separated by a comma
|
||||
required-attributes=
|
||||
|
||||
# List of builtins function names that should not be used, separated by a comma
|
||||
bad-functions=map,filter,apply,input
|
||||
|
||||
# Regular expression which should only match correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression which should only match correct module level names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression which should only match correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression which should only match correct function names
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct method names
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct instance attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct argument names
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct variable names
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct attribute names in class
|
||||
# bodies
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression which should only match correct list comprehension /
|
||||
# generator expression variable names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,Run,_
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=__.*__
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=80
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=regsub,TERMIOS,Bastion,rexec
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of interface methods to ignore, separated by a comma. This is used for
|
||||
# instance to not check methods defines in Zope's Interface base class.
|
||||
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception
|
1
setup.py
1
setup.py
@ -34,6 +34,7 @@ setup(
|
||||
"syutil==0.0.2",
|
||||
"Twisted>=14.0.0",
|
||||
"service_identity>=1.0.0",
|
||||
"pyopenssl>=0.14",
|
||||
"pyyaml",
|
||||
"pyasn1",
|
||||
"pynacl",
|
||||
|
@ -16,4 +16,4 @@
|
||||
""" This is a reference implementation of a synapse home server.
|
||||
"""
|
||||
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.4.2"
|
||||
|
@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
@ -58,4 +58,4 @@ class LoginType(object):
|
||||
EMAIL_CODE = u"m.login.email.code"
|
||||
EMAIL_URL = u"m.login.email.url"
|
||||
EMAIL_IDENTITY = u"m.login.email.identity"
|
||||
RECAPTCHA = u"m.login.recaptcha"
|
||||
RECAPTCHA = u"m.login.recaptcha"
|
||||
|
@ -54,7 +54,7 @@ class SynapseError(CodeMessageException):
|
||||
"""Constructs a synapse error.
|
||||
|
||||
Args:
|
||||
code (int): The integer error code (typically an HTTP response code)
|
||||
code (int): The integer error code (an HTTP response code)
|
||||
msg (str): The human-readable error message.
|
||||
err (str): The error code e.g 'M_FORBIDDEN'
|
||||
"""
|
||||
@ -67,6 +67,7 @@ class SynapseError(CodeMessageException):
|
||||
self.errcode,
|
||||
)
|
||||
|
||||
|
||||
class RoomError(SynapseError):
|
||||
"""An error raised when a room event fails."""
|
||||
pass
|
||||
@ -117,6 +118,7 @@ class InvalidCaptchaError(SynapseError):
|
||||
error_url=self.error_url,
|
||||
)
|
||||
|
||||
|
||||
class LimitExceededError(SynapseError):
|
||||
"""A client has sent too many requests and is being throttled.
|
||||
"""
|
||||
|
@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
@ -116,18 +116,25 @@ class Config(object):
|
||||
config = {}
|
||||
for key, value in vars(args).items():
|
||||
if (key not in set(["config_path", "generate_config"])
|
||||
and value is not None):
|
||||
and value is not None):
|
||||
config[key] = value
|
||||
with open(config_args.config_path, "w") as config_file:
|
||||
# TODO(paul) it would be lovely if we wrote out vim- and emacs-
|
||||
# style mode markers into the file, to hint to people that
|
||||
# this is a YAML file.
|
||||
yaml.dump(config, config_file, default_flow_style=False)
|
||||
print "A config file has been generated in %s for server name '%s') with corresponding SSL keys and self-signed certificates. Please review this file and customise it to your needs." % (config_args.config_path, config['server_name'])
|
||||
print "If this server name is incorrect, you will need to regenerate the SSL certificates"
|
||||
print (
|
||||
"A config file has been generated in %s for server name"
|
||||
" '%s' with corresponding SSL keys and self-signed"
|
||||
" certificates. Please review this file and customise it to"
|
||||
" your needs."
|
||||
) % (
|
||||
config_args.config_path, config['server_name']
|
||||
)
|
||||
print (
|
||||
"If this server name is incorrect, you will need to regenerate"
|
||||
" the SSL certificates"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
return cls(args)
|
||||
|
||||
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
from ._base import Config
|
||||
import os
|
||||
|
||||
|
||||
class DatabaseConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(DatabaseConfig, self).__init__(args)
|
||||
@ -34,4 +35,3 @@ class DatabaseConfig(Config):
|
||||
def generate_config(cls, args, config_dir_path):
|
||||
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
|
||||
args.database_path = os.path.abspath(args.database_path)
|
||||
|
||||
|
@ -35,5 +35,8 @@ class EmailConfig(Config):
|
||||
email_group.add_argument(
|
||||
"--email-smtp-server",
|
||||
default="",
|
||||
help="The SMTP server to send emails from (e.g. for password resets)."
|
||||
)
|
||||
help=(
|
||||
"The SMTP server to send emails from (e.g. for password"
|
||||
" resets)."
|
||||
)
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ from twisted.python.log import PythonLoggingObserver
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
|
||||
class LoggingConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(LoggingConfig, self).__init__(args)
|
||||
@ -51,7 +52,7 @@ class LoggingConfig(Config):
|
||||
|
||||
level = logging.INFO
|
||||
if self.verbosity:
|
||||
level = logging.DEBUG
|
||||
level = logging.DEBUG
|
||||
|
||||
# FIXME: we need a logging.WARN for a -q quiet option
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
from ._base import Config
|
||||
|
||||
|
||||
class RatelimitConfig(Config):
|
||||
|
||||
def __init__(self, args):
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
from ._base import Config
|
||||
|
||||
|
||||
class ContentRepositoryConfig(Config):
|
||||
def __init__(self, args):
|
||||
super(ContentRepositoryConfig, self).__init__(args)
|
||||
|
@ -34,7 +34,7 @@ class ServerConfig(Config):
|
||||
if not args.content_addr:
|
||||
host = args.server_name
|
||||
if ':' not in host:
|
||||
host = "%s:%d" % (host, args.bind_port)
|
||||
host = "%s:%d" % (host, args.bind_port)
|
||||
args.content_addr = "https://%s" % (host,)
|
||||
|
||||
self.content_addr = args.content_addr
|
||||
@ -74,7 +74,7 @@ class ServerConfig(Config):
|
||||
return syutil.crypto.signing_key.read_signing_keys(
|
||||
signing_keys.splitlines(True)
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
raise ConfigError(
|
||||
"Error reading signing_key."
|
||||
" Try running again with --generate-config"
|
||||
|
@ -19,7 +19,7 @@ from OpenSSL import crypto
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
GENERATE_DH_PARAMS=False
|
||||
GENERATE_DH_PARAMS = False
|
||||
|
||||
|
||||
class TlsConfig(Config):
|
||||
|
@ -33,7 +33,10 @@ class VoipConfig(Config):
|
||||
)
|
||||
group.add_argument(
|
||||
"--turn-shared-secret", type=str, default=None,
|
||||
help="The shared secret used to compute passwords for the TURN server"
|
||||
help=(
|
||||
"The shared secret used to compute passwords for the TURN"
|
||||
" server"
|
||||
)
|
||||
)
|
||||
group.add_argument(
|
||||
"--turn-user-lifetime", type=int, default=(1000 * 60 * 60),
|
||||
|
@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
@ -16,6 +16,10 @@ from twisted.internet import ssl
|
||||
from OpenSSL import SSL
|
||||
from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerContextFactory(ssl.ContextFactory):
|
||||
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
||||
@ -31,7 +35,7 @@ class ServerContextFactory(ssl.ContextFactory):
|
||||
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
|
||||
_ecCurve.addECKeyToContext(context)
|
||||
except:
|
||||
pass
|
||||
logger.exception("Failed to enable eliptic curve for TLS")
|
||||
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||
context.use_certificate(config.tls_certificate)
|
||||
context.use_privatekey(config.tls_private_key)
|
||||
@ -40,4 +44,3 @@ class ServerContextFactory(ssl.ContextFactory):
|
||||
|
||||
def getContext(self):
|
||||
return self._context
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
from twisted.web.http import HTTPClient
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.endpoints import connectProtocol
|
||||
from synapse.http.endpoint import matrix_endpoint
|
||||
import json
|
||||
import logging
|
||||
@ -99,4 +98,3 @@ class SynapseKeyClientProtocol(HTTPClient):
|
||||
|
||||
class SynapseKeyClientFactory(Factory):
|
||||
protocol = SynapseKeyClientProtocol
|
||||
|
||||
|
@ -44,7 +44,7 @@ class Keyring(object):
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Not signed with a supported algorithm",
|
||||
Codes.UNAUTHORIZED,
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
try:
|
||||
verify_key = yield self.get_server_verify_key(server_name, key_ids)
|
||||
@ -100,7 +100,7 @@ class Keyring(object):
|
||||
)
|
||||
|
||||
if ("signatures" not in response
|
||||
or server_name not in response["signatures"]):
|
||||
or server_name not in response["signatures"]):
|
||||
raise ValueError("Key response not signed by remote server")
|
||||
|
||||
if "tls_certificate" not in response:
|
||||
|
@ -54,7 +54,7 @@ class LoginHandler(BaseHandler):
|
||||
# pull out the hash for this user if they exist
|
||||
user_info = yield self.store.get_user_by_id(user_id=user)
|
||||
if not user_info:
|
||||
logger.warn("Attempted to login as %s but they do not exist.", user)
|
||||
logger.warn("Attempted to login as %s but they do not exist", user)
|
||||
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||
|
||||
stored_hash = user_info[0]["password_hash"]
|
||||
|
@ -115,8 +115,12 @@ class MessageHandler(BaseHandler):
|
||||
|
||||
user = self.hs.parse_userid(user_id)
|
||||
|
||||
events, next_token = yield data_source.get_pagination_rows(
|
||||
user, pagin_config, room_id
|
||||
events, next_key = yield data_source.get_pagination_rows(
|
||||
user, pagin_config.get_source_config("room"), room_id
|
||||
)
|
||||
|
||||
next_token = pagin_config.from_token.copy_and_replace(
|
||||
"room_key", next_key
|
||||
)
|
||||
|
||||
chunk = {
|
||||
@ -271,7 +275,7 @@ class MessageHandler(BaseHandler):
|
||||
presence_stream = self.hs.get_event_sources().sources["presence"]
|
||||
pagination_config = PaginationConfig(from_token=now_token)
|
||||
presence, _ = yield presence_stream.get_pagination_rows(
|
||||
user, pagination_config, None
|
||||
user, pagination_config.get_source_config("presence"), None
|
||||
)
|
||||
|
||||
public_rooms = yield self.store.get_rooms(is_public=True)
|
||||
|
@ -76,9 +76,7 @@ class PresenceHandler(BaseHandler):
|
||||
"stopped_user_eventstream", self.stopped_user_eventstream
|
||||
)
|
||||
|
||||
distributor.observe("user_joined_room",
|
||||
self.user_joined_room
|
||||
)
|
||||
distributor.observe("user_joined_room", self.user_joined_room)
|
||||
|
||||
distributor.declare("collect_presencelike_data")
|
||||
|
||||
@ -156,14 +154,12 @@ class PresenceHandler(BaseHandler):
|
||||
defer.returnValue(True)
|
||||
|
||||
if (yield self.store.user_rooms_intersect(
|
||||
[u.to_string() for u in observer_user, observed_user]
|
||||
)):
|
||||
[u.to_string() for u in observer_user, observed_user])):
|
||||
defer.returnValue(True)
|
||||
|
||||
if (yield self.store.is_presence_visible(
|
||||
observed_localpart=observed_user.localpart,
|
||||
observer_userid=observer_user.to_string(),
|
||||
)):
|
||||
observed_localpart=observed_user.localpart,
|
||||
observer_userid=observer_user.to_string())):
|
||||
defer.returnValue(True)
|
||||
|
||||
defer.returnValue(False)
|
||||
@ -171,7 +167,8 @@ class PresenceHandler(BaseHandler):
|
||||
@defer.inlineCallbacks
|
||||
def get_state(self, target_user, auth_user):
|
||||
if target_user.is_mine:
|
||||
visible = yield self.is_presence_visible(observer_user=auth_user,
|
||||
visible = yield self.is_presence_visible(
|
||||
observer_user=auth_user,
|
||||
observed_user=target_user
|
||||
)
|
||||
|
||||
@ -219,9 +216,9 @@ class PresenceHandler(BaseHandler):
|
||||
)
|
||||
|
||||
if state["presence"] not in self.STATE_LEVELS:
|
||||
raise SynapseError(400, "'%s' is not a valid presence state" %
|
||||
state["presence"]
|
||||
)
|
||||
raise SynapseError(400, "'%s' is not a valid presence state" % (
|
||||
state["presence"],
|
||||
))
|
||||
|
||||
logger.debug("Updating presence state of %s to %s",
|
||||
target_user.localpart, state["presence"])
|
||||
@ -229,7 +226,7 @@ class PresenceHandler(BaseHandler):
|
||||
state_to_store = dict(state)
|
||||
state_to_store["state"] = state_to_store.pop("presence")
|
||||
|
||||
statuscache=self._get_or_offline_usercache(target_user)
|
||||
statuscache = self._get_or_offline_usercache(target_user)
|
||||
was_level = self.STATE_LEVELS[statuscache.get_state()["presence"]]
|
||||
now_level = self.STATE_LEVELS[state["presence"]]
|
||||
|
||||
@ -649,8 +646,9 @@ class PresenceHandler(BaseHandler):
|
||||
del state["user_id"]
|
||||
|
||||
if "presence" not in state:
|
||||
logger.warning("Received a presence 'push' EDU from %s without"
|
||||
+ " a 'presence' key", origin
|
||||
logger.warning(
|
||||
"Received a presence 'push' EDU from %s without a"
|
||||
" 'presence' key", origin
|
||||
)
|
||||
continue
|
||||
|
||||
@ -745,7 +743,7 @@ class PresenceHandler(BaseHandler):
|
||||
defer.returnValue((localusers, remote_domains))
|
||||
|
||||
def push_update_to_clients(self, observed_user, users_to_push=[],
|
||||
room_ids=[], statuscache=None):
|
||||
room_ids=[], statuscache=None):
|
||||
self.notifier.on_new_user_event(
|
||||
users_to_push,
|
||||
room_ids,
|
||||
@ -765,8 +763,7 @@ class PresenceEventSource(object):
|
||||
presence = self.hs.get_handlers().presence_handler
|
||||
|
||||
if (yield presence.store.user_rooms_intersect(
|
||||
[u.to_string() for u in observer_user, observed_user]
|
||||
)):
|
||||
[u.to_string() for u in observer_user, observed_user])):
|
||||
defer.returnValue(True)
|
||||
|
||||
if observed_user.is_mine:
|
||||
@ -823,15 +820,12 @@ class PresenceEventSource(object):
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
# TODO (erikj): Does this make sense? Ordering?
|
||||
|
||||
from_token = pagination_config.from_token
|
||||
to_token = pagination_config.to_token
|
||||
|
||||
observer_user = user
|
||||
|
||||
from_key = int(from_token.presence_key)
|
||||
from_key = int(pagination_config.from_key)
|
||||
|
||||
if to_token:
|
||||
to_key = int(to_token.presence_key)
|
||||
if pagination_config.to_key:
|
||||
to_key = int(pagination_config.to_key)
|
||||
else:
|
||||
to_key = -1
|
||||
|
||||
@ -841,7 +835,7 @@ class PresenceEventSource(object):
|
||||
updates = []
|
||||
# TODO(paul): use a DeferredList ? How to limit concurrency.
|
||||
for observed_user in cachemap.keys():
|
||||
if not (to_key < cachemap[observed_user].serial < from_key):
|
||||
if not (to_key < cachemap[observed_user].serial <= from_key):
|
||||
continue
|
||||
|
||||
if (yield self.is_visible(observer_user, observed_user)):
|
||||
@ -849,30 +843,15 @@ class PresenceEventSource(object):
|
||||
|
||||
# TODO(paul): limit
|
||||
|
||||
updates = [(k, cachemap[k]) for k in cachemap
|
||||
if to_key < cachemap[k].serial < from_key]
|
||||
|
||||
if updates:
|
||||
clock = self.clock
|
||||
|
||||
earliest_serial = max([x[1].serial for x in updates])
|
||||
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
|
||||
|
||||
if to_token:
|
||||
next_token = to_token
|
||||
else:
|
||||
next_token = from_token
|
||||
|
||||
next_token = next_token.copy_and_replace(
|
||||
"presence_key", earliest_serial
|
||||
)
|
||||
defer.returnValue((data, next_token))
|
||||
defer.returnValue((data, earliest_serial))
|
||||
else:
|
||||
if not to_token:
|
||||
to_token = from_token.copy_and_replace(
|
||||
"presence_key", 0
|
||||
)
|
||||
defer.returnValue(([], to_token))
|
||||
defer.returnValue(([], 0))
|
||||
|
||||
|
||||
class UserPresenceCache(object):
|
||||
|
@ -64,9 +64,11 @@ class RegistrationHandler(BaseHandler):
|
||||
user_id = user.to_string()
|
||||
|
||||
token = self._generate_token(user_id)
|
||||
yield self.store.register(user_id=user_id,
|
||||
yield self.store.register(
|
||||
user_id=user_id,
|
||||
token=token,
|
||||
password_hash=password_hash)
|
||||
password_hash=password_hash
|
||||
)
|
||||
|
||||
self.distributor.fire("registered_user", user)
|
||||
else:
|
||||
@ -181,8 +183,11 @@ class RegistrationHandler(BaseHandler):
|
||||
data = yield httpCli.post_urlencoded_get_json(
|
||||
creds['idServer'],
|
||||
"/_matrix/identity/api/v1/3pid/bind",
|
||||
{'sid': creds['sid'], 'clientSecret': creds['clientSecret'],
|
||||
'mxid': mxid}
|
||||
{
|
||||
'sid': creds['sid'],
|
||||
'clientSecret': creds['clientSecret'],
|
||||
'mxid': mxid,
|
||||
}
|
||||
)
|
||||
defer.returnValue(data)
|
||||
|
||||
@ -223,5 +228,3 @@ class RegistrationHandler(BaseHandler):
|
||||
}
|
||||
)
|
||||
defer.returnValue(data)
|
||||
|
||||
|
||||
|
@ -169,11 +169,6 @@ class RoomCreationHandler(BaseHandler):
|
||||
content=content
|
||||
)
|
||||
|
||||
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||
join_event,
|
||||
do_auth=False
|
||||
)
|
||||
|
||||
content = {"membership": Membership.INVITE}
|
||||
for invitee in invite_list:
|
||||
invite_event = self.event_factory.create_event(
|
||||
@ -617,23 +612,14 @@ class RoomEventSource(object):
|
||||
return self.store.get_room_events_max_id()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
from_token = pagination_config.from_token
|
||||
to_token = pagination_config.to_token
|
||||
limit = pagination_config.limit
|
||||
direction = pagination_config.direction
|
||||
|
||||
to_key = to_token.room_key if to_token else None
|
||||
|
||||
def get_pagination_rows(self, user, config, key):
|
||||
events, next_key = yield self.store.paginate_room_events(
|
||||
room_id=key,
|
||||
from_key=from_token.room_key,
|
||||
to_key=to_key,
|
||||
direction=direction,
|
||||
limit=limit,
|
||||
from_key=config.from_key,
|
||||
to_key=config.to_key,
|
||||
direction=config.direction,
|
||||
limit=config.limit,
|
||||
with_feedback=True
|
||||
)
|
||||
|
||||
next_token = from_token.copy_and_replace("room_key", next_key)
|
||||
|
||||
defer.returnValue((events, next_token))
|
||||
defer.returnValue((events, next_key))
|
||||
|
@ -96,9 +96,10 @@ class TypingNotificationHandler(BaseHandler):
|
||||
remotedomains = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers, remotedomains=remotedomains,
|
||||
ignore_user=user)
|
||||
yield rm_handler.fetch_room_distributions_into(
|
||||
room_id, localusers=localusers, remotedomains=remotedomains,
|
||||
ignore_user=user
|
||||
)
|
||||
|
||||
for u in localusers:
|
||||
self.push_update_to_clients(
|
||||
@ -130,8 +131,9 @@ class TypingNotificationHandler(BaseHandler):
|
||||
localusers = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers)
|
||||
yield rm_handler.fetch_room_distributions_into(
|
||||
room_id, localusers=localusers
|
||||
)
|
||||
|
||||
for u in localusers:
|
||||
self.push_update_to_clients(
|
||||
@ -142,7 +144,7 @@ class TypingNotificationHandler(BaseHandler):
|
||||
)
|
||||
|
||||
def push_update_to_clients(self, room_id, observer_user, observed_user,
|
||||
typing):
|
||||
typing):
|
||||
# TODO(paul) steal this from presence.py
|
||||
pass
|
||||
|
||||
@ -158,4 +160,4 @@ class TypingNotificationEventSource(object):
|
||||
return 0
|
||||
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
return ([], pagination_config.from_token)
|
||||
return ([], pagination_config.from_key)
|
||||
|
@ -12,4 +12,3 @@
|
||||
# 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.
|
||||
|
||||
|
@ -16,7 +16,9 @@
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer, PartialDownloadError
|
||||
from twisted.web.client import (
|
||||
_AgentBase, _URI, readBody, FileBodyProducer, PartialDownloadError
|
||||
)
|
||||
from twisted.web.http_headers import Headers
|
||||
|
||||
from synapse.http.endpoint import matrix_endpoint
|
||||
@ -97,7 +99,7 @@ class BaseHttpClient(object):
|
||||
|
||||
retries_left = 5
|
||||
|
||||
endpoint = self._getEndpoint(reactor, destination);
|
||||
endpoint = self._getEndpoint(reactor, destination)
|
||||
|
||||
while True:
|
||||
|
||||
@ -181,7 +183,7 @@ class MatrixHttpClient(BaseHttpClient):
|
||||
|
||||
auth_headers = []
|
||||
|
||||
for key,sig in request["signatures"][self.server_name].items():
|
||||
for key, sig in request["signatures"][self.server_name].items():
|
||||
auth_headers.append(bytes(
|
||||
"X-Matrix origin=%s,key=\"%s\",sig=\"%s\"" % (
|
||||
self.server_name, key, sig,
|
||||
@ -276,7 +278,6 @@ class MatrixHttpClient(BaseHttpClient):
|
||||
|
||||
defer.returnValue(json.loads(body))
|
||||
|
||||
|
||||
def _getEndpoint(self, reactor, destination):
|
||||
return matrix_endpoint(
|
||||
reactor, destination, timeout=10,
|
||||
@ -351,6 +352,7 @@ class IdentityServerHttpClient(BaseHttpClient):
|
||||
|
||||
defer.returnValue(json.loads(body))
|
||||
|
||||
|
||||
class CaptchaServerHttpClient(MatrixHttpClient):
|
||||
"""Separate HTTP client for talking to google's captcha servers"""
|
||||
|
||||
@ -384,6 +386,7 @@ class CaptchaServerHttpClient(MatrixHttpClient):
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
def _print_ex(e):
|
||||
if hasattr(e, "reasons") and e.reasons:
|
||||
for ex in e.reasons:
|
||||
|
@ -38,8 +38,8 @@ class ContentRepoResource(resource.Resource):
|
||||
|
||||
Uploads are POSTed to wherever this Resource is linked to. This resource
|
||||
returns a "content token" which can be used to GET this content again. The
|
||||
token is typically a path, but it may not be. Tokens can expire, be one-time
|
||||
uses, etc.
|
||||
token is typically a path, but it may not be. Tokens can expire, be
|
||||
one-time uses, etc.
|
||||
|
||||
In this case, the token is a path to the file and contains 3 interesting
|
||||
sections:
|
||||
@ -175,10 +175,9 @@ class ContentRepoResource(resource.Resource):
|
||||
with open(fname, "wb") as f:
|
||||
f.write(request.content.read())
|
||||
|
||||
|
||||
# FIXME (erikj): These should use constants.
|
||||
file_name = os.path.basename(fname)
|
||||
# FIXME: we can't assume what the public mounted path of the repo is
|
||||
# FIXME: we can't assume what the repo's public mounted path is
|
||||
# ...plus self-signed SSL won't work to remote clients anyway
|
||||
# ...and we can't assume that it's SSL anyway, as we might want to
|
||||
# server it via the non-SSL listener...
|
||||
@ -201,6 +200,3 @@ class ContentRepoResource(resource.Resource):
|
||||
500,
|
||||
json.dumps({"error": "Internal server error"}),
|
||||
send_cors=True)
|
||||
|
||||
|
||||
|
||||
|
@ -167,7 +167,8 @@ class Notifier(object):
|
||||
)
|
||||
|
||||
def eb(failure):
|
||||
logger.error("Failed to notify listener",
|
||||
logger.error(
|
||||
"Failed to notify listener",
|
||||
exc_info=(
|
||||
failure.type,
|
||||
failure.value,
|
||||
@ -207,7 +208,7 @@ class Notifier(object):
|
||||
)
|
||||
|
||||
if timeout:
|
||||
reactor.callLater(timeout/1000, self._timeout_listener, listener)
|
||||
reactor.callLater(timeout/1000.0, self._timeout_listener, listener)
|
||||
|
||||
self._register_with_keys(listener)
|
||||
|
||||
|
@ -108,9 +108,9 @@ class ProfileRestServlet(RestServlet):
|
||||
)
|
||||
|
||||
defer.returnValue((200, {
|
||||
"displayname": displayname,
|
||||
"avatar_url": avatar_url
|
||||
}))
|
||||
"displayname": displayname,
|
||||
"avatar_url": avatar_url
|
||||
}))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
|
@ -60,40 +60,45 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
def on_GET(self, request):
|
||||
if self.hs.config.enable_registration_captcha:
|
||||
return (200, {
|
||||
"flows": [
|
||||
return (
|
||||
200,
|
||||
{"flows": [
|
||||
{
|
||||
"type": LoginType.RECAPTCHA,
|
||||
"stages": ([LoginType.RECAPTCHA,
|
||||
LoginType.EMAIL_IDENTITY,
|
||||
LoginType.PASSWORD])
|
||||
"stages": [
|
||||
LoginType.RECAPTCHA,
|
||||
LoginType.EMAIL_IDENTITY,
|
||||
LoginType.PASSWORD
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": LoginType.RECAPTCHA,
|
||||
"stages": [LoginType.RECAPTCHA, LoginType.PASSWORD]
|
||||
}
|
||||
]
|
||||
})
|
||||
]}
|
||||
)
|
||||
else:
|
||||
return (200, {
|
||||
"flows": [
|
||||
return (
|
||||
200,
|
||||
{"flows": [
|
||||
{
|
||||
"type": LoginType.EMAIL_IDENTITY,
|
||||
"stages": ([LoginType.EMAIL_IDENTITY,
|
||||
LoginType.PASSWORD])
|
||||
"stages": [
|
||||
LoginType.EMAIL_IDENTITY, LoginType.PASSWORD
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": LoginType.PASSWORD
|
||||
}
|
||||
]
|
||||
})
|
||||
]}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
register_json = _parse_json(request)
|
||||
|
||||
session = (register_json["session"] if "session" in register_json
|
||||
else None)
|
||||
session = (register_json["session"]
|
||||
if "session" in register_json else None)
|
||||
login_type = None
|
||||
if "type" not in register_json:
|
||||
raise SynapseError(400, "Missing 'type' key.")
|
||||
@ -122,7 +127,9 @@ class RegisterRestServlet(RestServlet):
|
||||
defer.returnValue((200, response))
|
||||
except KeyError as e:
|
||||
logger.exception(e)
|
||||
raise SynapseError(400, "Missing JSON keys for login type %s." % login_type)
|
||||
raise SynapseError(400, "Missing JSON keys for login type %s." % (
|
||||
login_type,
|
||||
))
|
||||
|
||||
def on_OPTIONS(self, request):
|
||||
return (200, {})
|
||||
@ -183,8 +190,10 @@ class RegisterRestServlet(RestServlet):
|
||||
session["user"] = register_json["user"]
|
||||
defer.returnValue(None)
|
||||
else:
|
||||
raise SynapseError(400, "Captcha bypass HMAC incorrect",
|
||||
errcode=Codes.CAPTCHA_NEEDED)
|
||||
raise SynapseError(
|
||||
400, "Captcha bypass HMAC incorrect",
|
||||
errcode=Codes.CAPTCHA_NEEDED
|
||||
)
|
||||
|
||||
challenge = None
|
||||
user_response = None
|
||||
@ -230,12 +239,15 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
if ("user" in session and "user" in register_json and
|
||||
session["user"] != register_json["user"]):
|
||||
raise SynapseError(400, "Cannot change user ID during registration")
|
||||
raise SynapseError(
|
||||
400, "Cannot change user ID during registration"
|
||||
)
|
||||
|
||||
password = register_json["password"].encode("utf-8")
|
||||
desired_user_id = (register_json["user"].encode("utf-8") if "user"
|
||||
in register_json else None)
|
||||
if desired_user_id and urllib.quote(desired_user_id) != desired_user_id:
|
||||
desired_user_id = (register_json["user"].encode("utf-8")
|
||||
if "user" in register_json else None)
|
||||
if (desired_user_id
|
||||
and urllib.quote(desired_user_id) != desired_user_id):
|
||||
raise SynapseError(
|
||||
400,
|
||||
"User ID must only contain characters which do not " +
|
||||
|
@ -48,7 +48,9 @@ class RoomCreateRestServlet(RestServlet):
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
defer.returnValue(
|
||||
self.txns.get_client_transaction(request, txn_id)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@ -98,8 +100,8 @@ class RoomStateEventRestServlet(RestServlet):
|
||||
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
|
||||
|
||||
# /room/$roomid/state/$eventtype/$statekey
|
||||
state_key = ("/rooms/(?P<room_id>[^/]*)/state/" +
|
||||
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$")
|
||||
state_key = ("/rooms/(?P<room_id>[^/]*)/state/"
|
||||
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$")
|
||||
|
||||
http_server.register_path("GET",
|
||||
client_path_pattern(state_key),
|
||||
@ -133,7 +135,9 @@ class RoomStateEventRestServlet(RestServlet):
|
||||
)
|
||||
|
||||
if not data:
|
||||
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
|
||||
raise SynapseError(
|
||||
404, "Event not found.", errcode=Codes.NOT_FOUND
|
||||
)
|
||||
defer.returnValue((200, data[0].get_dict()["content"]))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@ -195,7 +199,9 @@ class RoomSendEventRestServlet(RestServlet):
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, event_type, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
defer.returnValue(
|
||||
self.txns.get_client_transaction(request, txn_id)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@ -254,7 +260,9 @@ class JoinRoomAliasServlet(RestServlet):
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_identifier, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
defer.returnValue(
|
||||
self.txns.get_client_transaction(request, txn_id)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@ -293,7 +301,8 @@ class RoomMemberListRestServlet(RestServlet):
|
||||
target_user = self.hs.parse_userid(event["user_id"])
|
||||
# Presence is an optional cache; don't fail if we can't fetch it
|
||||
try:
|
||||
presence_state = yield self.handlers.presence_handler.get_state(
|
||||
presence_handler = self.handlers.presence_handler
|
||||
presence_state = yield presence_handler.get_state(
|
||||
target_user=target_user, auth_user=user
|
||||
)
|
||||
event["content"].update(presence_state)
|
||||
@ -344,7 +353,7 @@ class RoomInitialSyncRestServlet(RestServlet):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
yield self.auth.get_user_by_req(request)
|
||||
# TODO: Get all the initial sync data for this room and return in the
|
||||
# same format as initial sync, that is:
|
||||
# {
|
||||
@ -359,11 +368,11 @@ class RoomInitialSyncRestServlet(RestServlet):
|
||||
# { state event } , { state event }
|
||||
# ]
|
||||
# }
|
||||
# Probably worth keeping the keys room_id and membership for parity with
|
||||
# /initialSync even though they must be joined to sync this and know the
|
||||
# room ID, so clients can reuse the same code (room_id and membership
|
||||
# are MANDATORY for /initialSync, so the code will expect it to be
|
||||
# there)
|
||||
# Probably worth keeping the keys room_id and membership for parity
|
||||
# with /initialSync even though they must be joined to sync this and
|
||||
# know the room ID, so clients can reuse the same code (room_id and
|
||||
# membership are MANDATORY for /initialSync, so the code will expect
|
||||
# it to be there)
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
|
||||
@ -388,8 +397,8 @@ class RoomMembershipRestServlet(RestServlet):
|
||||
|
||||
def register(self, http_server):
|
||||
# /rooms/$roomid/[invite|join|leave]
|
||||
PATTERN = ("/rooms/(?P<room_id>[^/]*)/" +
|
||||
"(?P<membership_action>join|invite|leave|ban|kick)")
|
||||
PATTERN = ("/rooms/(?P<room_id>[^/]*)/"
|
||||
"(?P<membership_action>join|invite|leave|ban|kick)")
|
||||
register_txn_path(self, PATTERN, http_server)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@ -422,7 +431,9 @@ class RoomMembershipRestServlet(RestServlet):
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, membership_action, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
defer.returnValue(
|
||||
self.txns.get_client_transaction(request, txn_id)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@ -431,6 +442,7 @@ class RoomMembershipRestServlet(RestServlet):
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
|
||||
class RoomRedactEventRestServlet(RestServlet):
|
||||
def register(self, http_server):
|
||||
PATTERN = ("/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)")
|
||||
@ -457,7 +469,9 @@ class RoomRedactEventRestServlet(RestServlet):
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, event_id, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
defer.returnValue(
|
||||
self.txns.get_client_transaction(request, txn_id)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@ -503,10 +517,10 @@ def register_txn_path(servlet, regex_string, http_server, with_get=False):
|
||||
)
|
||||
if with_get:
|
||||
http_server.register_path(
|
||||
"GET",
|
||||
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
|
||||
servlet.on_GET
|
||||
)
|
||||
"GET",
|
||||
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
|
||||
servlet.on_GET
|
||||
)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
|
@ -30,9 +30,9 @@ class HttpTransactionStore(object):
|
||||
"""Retrieve a response for this request.
|
||||
|
||||
Args:
|
||||
key (str): A transaction-independent key for this request. Typically
|
||||
this is a combination of the path (without the transaction id) and
|
||||
the user's access token.
|
||||
key (str): A transaction-independent key for this request. Usually
|
||||
this is a combination of the path (without the transaction id)
|
||||
and the user's access token.
|
||||
txn_id (str): The transaction ID for this request
|
||||
Returns:
|
||||
A tuple of (HTTP response code, response content) or None.
|
||||
@ -51,9 +51,9 @@ class HttpTransactionStore(object):
|
||||
"""Stores an HTTP response tuple.
|
||||
|
||||
Args:
|
||||
key (str): A transaction-independent key for this request. Typically
|
||||
this is a combination of the path (without the transaction id) and
|
||||
the user's access token.
|
||||
key (str): A transaction-independent key for this request. Usually
|
||||
this is a combination of the path (without the transaction id)
|
||||
and the user's access token.
|
||||
txn_id (str): The transaction ID for this request.
|
||||
response (tuple): A tuple of (HTTP response code, response content)
|
||||
"""
|
||||
@ -92,5 +92,3 @@ class HttpTransactionStore(object):
|
||||
token = request.args["access_token"][0]
|
||||
path_without_txn_id = request.path.rsplit("/", 1)[0]
|
||||
return path_without_txn_id + "/" + token
|
||||
|
||||
|
||||
|
@ -34,23 +34,23 @@ class VoipRestServlet(RestServlet):
|
||||
turnSecret = self.hs.config.turn_shared_secret
|
||||
userLifetime = self.hs.config.turn_user_lifetime
|
||||
if not turnUris or not turnSecret or not userLifetime:
|
||||
defer.returnValue( (200, {}) )
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000
|
||||
username = "%d:%s" % (expiry, auth_user.to_string())
|
||||
|
||||
|
||||
mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1)
|
||||
# We need to use standard base64 encoding here, *not* syutil's encode_base64
|
||||
# because we need to add the standard padding to get the same result as the
|
||||
# TURN server.
|
||||
# We need to use standard base64 encoding here, *not* syutil's
|
||||
# encode_base64 because we need to add the standard padding to get the
|
||||
# same result as the TURN server.
|
||||
password = base64.b64encode(mac.digest())
|
||||
|
||||
defer.returnValue( (200, {
|
||||
defer.returnValue((200, {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'ttl': userLifetime / 1000,
|
||||
'uris': turnUris,
|
||||
}) )
|
||||
}))
|
||||
|
||||
def on_OPTIONS(self, request):
|
||||
return (200, {})
|
||||
|
@ -452,10 +452,11 @@ def prepare_database(db_conn):
|
||||
db_conn.commit()
|
||||
|
||||
else:
|
||||
sql_script = "BEGIN TRANSACTION;"
|
||||
for sql_loc in SCHEMAS:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
c.executescript(sql_script)
|
||||
sql_script += read_schema(sql_loc)
|
||||
sql_script += "COMMIT TRANSACTION;"
|
||||
c.executescript(sql_script)
|
||||
db_conn.commit()
|
||||
c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
|
||||
|
||||
|
@ -18,9 +18,10 @@ from _base import SQLBaseStore
|
||||
from twisted.internet import defer
|
||||
|
||||
import OpenSSL
|
||||
from syutil.crypto.signing_key import decode_verify_key_bytes
|
||||
from syutil.crypto.signing_key import decode_verify_key_bytes
|
||||
import hashlib
|
||||
|
||||
|
||||
class KeyStore(SQLBaseStore):
|
||||
"""Persistence for signature verification keys and tls X.509 certificates
|
||||
"""
|
||||
@ -104,7 +105,6 @@ class KeyStore(SQLBaseStore):
|
||||
ts_now_ms (int): The time now in milliseconds
|
||||
verification_key (VerifyKey): The NACL verify key.
|
||||
"""
|
||||
verify_key_bytes = verify_key.encode()
|
||||
return self._simple_insert(
|
||||
table="server_signature_keys",
|
||||
values={
|
||||
|
@ -33,7 +33,9 @@ class RoomMemberStore(SQLBaseStore):
|
||||
target_user_id = event.state_key
|
||||
domain = self.hs.parse_userid(target_user_id).domain
|
||||
except:
|
||||
logger.exception("Failed to parse target_user_id=%s", target_user_id)
|
||||
logger.exception(
|
||||
"Failed to parse target_user_id=%s", target_user_id
|
||||
)
|
||||
raise
|
||||
|
||||
logger.debug(
|
||||
@ -65,7 +67,8 @@ class RoomMemberStore(SQLBaseStore):
|
||||
# Check if this was the last person to have left.
|
||||
member_events = self._get_members_query_txn(
|
||||
txn,
|
||||
where_clause="c.room_id = ? AND m.membership = ? AND m.user_id != ?",
|
||||
where_clause=("c.room_id = ? AND m.membership = ?"
|
||||
" AND m.user_id != ?"),
|
||||
where_values=(event.room_id, Membership.JOIN, target_user_id,)
|
||||
)
|
||||
|
||||
@ -120,7 +123,6 @@ class RoomMemberStore(SQLBaseStore):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_room_members(self, room_id, membership=None):
|
||||
"""Retrieve the current room member list for a room.
|
||||
|
||||
|
@ -22,6 +22,19 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SourcePaginationConfig(object):
|
||||
|
||||
"""A configuration object which stores pagination parameters for a
|
||||
specific event source."""
|
||||
|
||||
def __init__(self, from_key=None, to_key=None, direction='f',
|
||||
limit=0):
|
||||
self.from_key = from_key
|
||||
self.to_key = to_key
|
||||
self.direction = 'f' if direction == 'f' else 'b'
|
||||
self.limit = int(limit)
|
||||
|
||||
|
||||
class PaginationConfig(object):
|
||||
|
||||
"""A configuration object which stores pagination parameters."""
|
||||
@ -82,3 +95,13 @@ class PaginationConfig(object):
|
||||
"<PaginationConfig from_tok=%s, to_tok=%s, "
|
||||
"direction=%s, limit=%s>"
|
||||
) % (self.from_token, self.to_token, self.direction, self.limit)
|
||||
|
||||
def get_source_config(self, source_name):
|
||||
keyname = "%s_key" % source_name
|
||||
|
||||
return SourcePaginationConfig(
|
||||
from_key=getattr(self.from_token, keyname),
|
||||
to_key=getattr(self.to_token, keyname) if self.to_token else None,
|
||||
direction=self.direction,
|
||||
limit=self.limit,
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ class NullSource(object):
|
||||
return defer.succeed(0)
|
||||
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
return defer.succeed(([], pagination_config.from_token))
|
||||
return defer.succeed(([], pagination_config.from_key))
|
||||
|
||||
|
||||
class EventSources(object):
|
||||
|
@ -42,7 +42,8 @@ class Distributor(object):
|
||||
if name in self.signals:
|
||||
raise KeyError("%r already has a signal named %s" % (self, name))
|
||||
|
||||
self.signals[name] = Signal(name,
|
||||
self.signals[name] = Signal(
|
||||
name,
|
||||
suppress_failures=self.suppress_failures,
|
||||
)
|
||||
|
||||
|
@ -42,8 +42,8 @@ def send_email(smtp_server, from_addr, to_addr, subject, body):
|
||||
EmailException if there was a problem sending the mail.
|
||||
"""
|
||||
if not smtp_server or not from_addr or not to_addr:
|
||||
raise EmailException("Need SMTP server, from and to addresses. Check " +
|
||||
"the config to set these.")
|
||||
raise EmailException("Need SMTP server, from and to addresses. Check"
|
||||
" the config to set these.")
|
||||
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
@ -68,4 +68,4 @@ def send_email(smtp_server, from_addr, to_addr, subject, body):
|
||||
twisted.python.log.err()
|
||||
ese = EmailException()
|
||||
ese.cause = origException
|
||||
raise ese
|
||||
raise ese
|
||||
|
@ -13,9 +13,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
class JsonEncodedObject(object):
|
||||
""" A common base class for defining protocol units that are represented
|
||||
as JSON.
|
||||
@ -89,6 +89,7 @@ class JsonEncodedObject(object):
|
||||
def __str__(self):
|
||||
return "(%s, %s)" % (self.__class__.__name__, repr(self.__dict__))
|
||||
|
||||
|
||||
def _encode(obj):
|
||||
if type(obj) is list:
|
||||
return [_encode(o) for o in obj]
|
||||
|
@ -29,6 +29,7 @@ from synapse.server import HomeServer
|
||||
from synapse.api.constants import PresenceState
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.handlers.presence import PresenceHandler, UserPresenceCache
|
||||
from synapse.streams.config import SourcePaginationConfig
|
||||
|
||||
|
||||
OFFLINE = PresenceState.OFFLINE
|
||||
@ -676,6 +677,21 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
msg="Presence event should be visible to self-reflection"
|
||||
)
|
||||
|
||||
config = SourcePaginationConfig(from_key=1, to_key=0)
|
||||
(chunk, _) = yield self.event_source.get_pagination_rows(
|
||||
self.u_apple, config, None
|
||||
)
|
||||
self.assertEquals(chunk,
|
||||
[
|
||||
{"type": "m.presence",
|
||||
"content": {
|
||||
"user_id": "@apple:test",
|
||||
"presence": ONLINE,
|
||||
"last_active_ago": 0,
|
||||
}},
|
||||
]
|
||||
)
|
||||
|
||||
# Banana sees it because of presence subscription
|
||||
(events, _) = yield self.event_source.get_new_events_for_user(
|
||||
self.u_banana, 0, None
|
||||
|
@ -53,7 +53,7 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
||||
* Open a given page.
|
||||
* @param {String} url url of the page
|
||||
*/
|
||||
$scope.goToPage = function(url) {
|
||||
$rootScope.goToPage = function(url) {
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
|
@ -40,4 +40,45 @@ angular.module('matrixWebClient')
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
}])
|
||||
.directive('asjson', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function (scope, element, attrs, ngModelCtrl) {
|
||||
function isValidJson(model) {
|
||||
var flag = true;
|
||||
try {
|
||||
angular.fromJson(model);
|
||||
} catch (err) {
|
||||
flag = false;
|
||||
}
|
||||
return flag;
|
||||
};
|
||||
|
||||
function string2JSON(text) {
|
||||
try {
|
||||
var j = angular.fromJson(text);
|
||||
ngModelCtrl.$setValidity('json', true);
|
||||
return j;
|
||||
} catch (err) {
|
||||
//returning undefined results in a parser error as of angular-1.3-rc.0, and will not go through $validators
|
||||
//return undefined
|
||||
ngModelCtrl.$setValidity('json', false);
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
function JSON2String(object) {
|
||||
return angular.toJson(object, true);
|
||||
};
|
||||
|
||||
//$validators is an object, where key is the error
|
||||
//ngModelCtrl.$validators.json = isValidJson;
|
||||
|
||||
//array pipelines
|
||||
ngModelCtrl.$parsers.push(string2JSON);
|
||||
ngModelCtrl.$formatters.push(JSON2String);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -76,6 +76,17 @@ angular.module('matrixWebClient')
|
||||
return filtered;
|
||||
};
|
||||
})
|
||||
.filter('stateEventsFilter', function($sce) {
|
||||
return function(events) {
|
||||
var filtered = {};
|
||||
angular.forEach(events, function(value, key) {
|
||||
if (value && typeof(value.state_key) === "string") {
|
||||
filtered[key] = value;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
})
|
||||
.filter('unsafe', ['$sce', function($sce) {
|
||||
return function(text) {
|
||||
return $sce.trustAsHtml(text);
|
||||
|
@ -403,6 +403,7 @@ textarea, input {
|
||||
}
|
||||
|
||||
.roomNameSection, .roomTopicSection {
|
||||
text-align: right;
|
||||
float: right;
|
||||
width: 100%;
|
||||
}
|
||||
@ -412,9 +413,40 @@ textarea, input {
|
||||
}
|
||||
|
||||
.roomHeaderInfo {
|
||||
text-align: right;
|
||||
float: right;
|
||||
margin-top: 15px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/*** Room Info Dialog ***/
|
||||
|
||||
.room-info {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.room-info-event {
|
||||
border-bottom: 1pt solid black;
|
||||
}
|
||||
|
||||
.room-info-event-meta {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.room-info-event-content {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.room-info-textarea-content {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*** Participant list ***/
|
||||
|
@ -30,7 +30,10 @@ var matrixWebClient = angular.module('matrixWebClient', [
|
||||
'MatrixCall',
|
||||
'eventStreamService',
|
||||
'eventHandlerService',
|
||||
'infinite-scroll'
|
||||
'notificationService',
|
||||
'infinite-scroll',
|
||||
'ui.bootstrap',
|
||||
'monospaced.elastic'
|
||||
]);
|
||||
|
||||
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
||||
|
5081
webclient/bootstrap.css
vendored
Normal file
5081
webclient/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -27,8 +27,8 @@ Typically, this service will store events or broadcast them to any listeners
|
||||
if typically all the $on method would do is update its own $scope.
|
||||
*/
|
||||
angular.module('eventHandlerService', [])
|
||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence',
|
||||
function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 'notificationService',
|
||||
function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService) {
|
||||
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
|
||||
var MSG_EVENT = "MSG_EVENT";
|
||||
var MEMBER_EVENT = "MEMBER_EVENT";
|
||||
@ -45,44 +45,6 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
var eventMap = {};
|
||||
|
||||
$rootScope.presence = {};
|
||||
|
||||
// TODO: This is attached to the rootScope so .html can just go containsBingWord
|
||||
// for determining classes so it is easy to highlight bing messages. It seems a
|
||||
// bit strange to put the impl in this service though, but I can't think of a better
|
||||
// file to put it in.
|
||||
$rootScope.containsBingWord = function(content) {
|
||||
if (!content || $.type(content) != "string") {
|
||||
return false;
|
||||
}
|
||||
var bingWords = matrixService.config().bingWords;
|
||||
var shouldBing = false;
|
||||
|
||||
// case-insensitive name check for user_id OR display_name if they exist
|
||||
var myUserId = matrixService.config().user_id;
|
||||
if (myUserId) {
|
||||
myUserId = myUserId.toLocaleLowerCase();
|
||||
}
|
||||
var myDisplayName = matrixService.config().display_name;
|
||||
if (myDisplayName) {
|
||||
myDisplayName = myDisplayName.toLocaleLowerCase();
|
||||
}
|
||||
if ( (myDisplayName && content.toLocaleLowerCase().indexOf(myDisplayName) != -1) ||
|
||||
(myUserId && content.toLocaleLowerCase().indexOf(myUserId) != -1) ) {
|
||||
shouldBing = true;
|
||||
}
|
||||
|
||||
// bing word list check
|
||||
if (bingWords && !shouldBing) {
|
||||
for (var i=0; i<bingWords.length; i++) {
|
||||
var re = RegExp(bingWords[i]);
|
||||
if (content.search(re) != -1) {
|
||||
shouldBing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shouldBing;
|
||||
};
|
||||
|
||||
var initialSyncDeferred;
|
||||
|
||||
@ -172,6 +134,17 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
};
|
||||
|
||||
var handleMessage = function(event, isLiveEvent) {
|
||||
// Check for empty event content
|
||||
var hasContent = false;
|
||||
for (var prop in event.content) {
|
||||
hasContent = true;
|
||||
break;
|
||||
}
|
||||
if (!hasContent) {
|
||||
// empty json object is a redacted event, so ignore.
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLiveEvent) {
|
||||
if (event.user_id === matrixService.config().user_id &&
|
||||
(event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
|
||||
@ -190,7 +163,12 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
}
|
||||
|
||||
if (window.Notification && event.user_id != matrixService.config().user_id) {
|
||||
var shouldBing = $rootScope.containsBingWord(event.content.body);
|
||||
var shouldBing = notificationService.containsBingWord(
|
||||
matrixService.config().user_id,
|
||||
matrixService.config().display_name,
|
||||
matrixService.config().bingWords,
|
||||
event.content.body
|
||||
);
|
||||
|
||||
// Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
|
||||
//
|
||||
@ -220,17 +198,29 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
if (event.content.msgtype === "m.emote") {
|
||||
message = "* " + displayname + " " + message;
|
||||
}
|
||||
else if (event.content.msgtype === "m.image") {
|
||||
message = displayname + " sent an image.";
|
||||
}
|
||||
|
||||
var notification = new window.Notification(
|
||||
displayname +
|
||||
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
|
||||
{
|
||||
"body": message,
|
||||
"icon": member ? member.avatar_url : undefined
|
||||
});
|
||||
$timeout(function() {
|
||||
notification.close();
|
||||
}, 5 * 1000);
|
||||
var roomTitle = matrixService.getRoomIdToAliasMapping(event.room_id);
|
||||
var theRoom = $rootScope.events.rooms[event.room_id];
|
||||
if (!roomTitle && theRoom && theRoom["m.room.name"] && theRoom["m.room.name"].content) {
|
||||
roomTitle = theRoom["m.room.name"].content.name;
|
||||
}
|
||||
|
||||
if (!roomTitle) {
|
||||
roomTitle = event.room_id;
|
||||
}
|
||||
|
||||
notificationService.showNotification(
|
||||
displayname + " (" + roomTitle + ")",
|
||||
message,
|
||||
member ? member.avatar_url : undefined,
|
||||
function() {
|
||||
console.log("notification.onclick() room=" + event.room_id);
|
||||
$rootScope.goToPage('room/' + event.room_id);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,7 +246,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
// could be a membership change, display name change, etc.
|
||||
// Find out which one.
|
||||
var memberChanges = undefined;
|
||||
if (event.content.prev !== event.content.membership) {
|
||||
if ((event.prev_content === undefined && event.content.membership) || (event.prev_content && (event.prev_content.membership !== event.content.membership))) {
|
||||
memberChanges = "membership";
|
||||
}
|
||||
else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) {
|
||||
@ -319,6 +309,31 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||
}
|
||||
};
|
||||
|
||||
var handleRedaction = function(event, isLiveEvent) {
|
||||
if (!isLiveEvent) {
|
||||
// we have nothing to remove, so just ignore it.
|
||||
console.log("Received redacted event: "+JSON.stringify(event));
|
||||
return;
|
||||
}
|
||||
|
||||
// we need to remove something possibly: do we know the redacted
|
||||
// event ID?
|
||||
if (eventMap[event.redacts]) {
|
||||
// remove event from list of messages in this room.
|
||||
var eventList = $rootScope.events.rooms[event.room_id].messages;
|
||||
for (var i=0; i<eventList.length; i++) {
|
||||
if (eventList[i].event_id === event.redacts) {
|
||||
console.log("Removing event " + event.redacts);
|
||||
eventList.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast the redaction so controllers can nuke this
|
||||
console.log("Redacted an event.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the event in $rootScope.events.rooms[room_id].messages
|
||||
@ -481,7 +496,17 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||
case 'm.room.topic':
|
||||
handleRoomTopic(event, isLiveEvent, isStateEvent);
|
||||
break;
|
||||
case 'm.room.redaction':
|
||||
handleRedaction(event, isLiveEvent);
|
||||
break;
|
||||
default:
|
||||
// if it is a state event, then just add it in so it
|
||||
// displays on the Room Info screen.
|
||||
if (typeof(event.state_key) === "string") { // incls. 0-len strings
|
||||
if (event.room_id) {
|
||||
handleRoomDateEvent(event, isLiveEvent, false);
|
||||
}
|
||||
}
|
||||
console.log("Unable to handle event type " + event.type);
|
||||
console.log(JSON.stringify(event, undefined, 4));
|
||||
break;
|
||||
|
@ -47,7 +47,6 @@ angular.module('matrixFilter', [])
|
||||
else if (room.members && !isPublicRoom) { // Do not rename public room
|
||||
|
||||
var user_id = matrixService.config().user_id;
|
||||
|
||||
// Else, build the name from its users
|
||||
// Limit the room renaming to 1:1 room
|
||||
if (2 === Object.keys(room.members).length) {
|
||||
@ -65,8 +64,16 @@ angular.module('matrixFilter', [])
|
||||
|
||||
var otherUserId;
|
||||
|
||||
if (Object.keys(room.members)[0] && Object.keys(room.members)[0] !== user_id) {
|
||||
if (Object.keys(room.members)[0]) {
|
||||
otherUserId = Object.keys(room.members)[0];
|
||||
// this could be an invite event (from event stream)
|
||||
if (otherUserId === user_id &&
|
||||
room.members[user_id].content.membership === "invite") {
|
||||
// this is us being invited to this room, so the
|
||||
// *user_id* is the other user ID and not the state
|
||||
// key.
|
||||
otherUserId = room.members[user_id].user_id;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// it's got to be an invite, or failing that a self-chat;
|
||||
|
@ -438,6 +438,14 @@ angular.module('matrixService', [])
|
||||
return this.sendMessage(room_id, msg_id, content);
|
||||
},
|
||||
|
||||
redactEvent: function(room_id, event_id) {
|
||||
var path = "/rooms/$room_id/redact/$event_id";
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$event_id", event_id);
|
||||
var content = {};
|
||||
return doRequest("POST", path, undefined, content);
|
||||
},
|
||||
|
||||
// get a snapshot of the members in a room.
|
||||
getMemberList: function(room_id) {
|
||||
// Like the cmd client, escape room ids
|
||||
|
104
webclient/components/matrix/notification-service.js
Normal file
104
webclient/components/matrix/notification-service.js
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
This service manages notifications: enabling, creating and showing them. This
|
||||
also contains 'bing word' logic.
|
||||
*/
|
||||
angular.module('notificationService', [])
|
||||
.factory('notificationService', ['$timeout', function($timeout) {
|
||||
|
||||
var getLocalPartFromUserId = function(user_id) {
|
||||
if (!user_id) {
|
||||
return null;
|
||||
}
|
||||
var localpartRegex = /@(.*):\w+/i
|
||||
var results = localpartRegex.exec(user_id);
|
||||
if (results && results.length == 2) {
|
||||
return results[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
containsBingWord: function(userId, displayName, bingWords, content) {
|
||||
// case-insensitive name check for user_id OR display_name if they exist
|
||||
var userRegex = "";
|
||||
if (userId) {
|
||||
var localpart = getLocalPartFromUserId(userId);
|
||||
if (localpart) {
|
||||
localpart = localpart.toLocaleLowerCase();
|
||||
userRegex += "\\b" + localpart + "\\b";
|
||||
}
|
||||
}
|
||||
if (displayName) {
|
||||
displayName = displayName.toLocaleLowerCase();
|
||||
if (userRegex.length > 0) {
|
||||
userRegex += "|";
|
||||
}
|
||||
userRegex += "\\b" + displayName + "\\b";
|
||||
}
|
||||
|
||||
var regexList = [new RegExp(userRegex, 'i')];
|
||||
|
||||
// bing word list check
|
||||
if (bingWords && bingWords.length > 0) {
|
||||
for (var i=0; i<bingWords.length; i++) {
|
||||
var re = RegExp(bingWords[i], 'i');
|
||||
regexList.push(re);
|
||||
}
|
||||
}
|
||||
return this.hasMatch(regexList, content);
|
||||
},
|
||||
|
||||
hasMatch: function(regExps, content) {
|
||||
if (!content || $.type(content) != "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (regExps && regExps.length > 0) {
|
||||
for (var i=0; i<regExps.length; i++) {
|
||||
if (content.search(regExps[i]) != -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
showNotification: function(title, body, icon, onclick) {
|
||||
var notification = new window.Notification(
|
||||
title,
|
||||
{
|
||||
"body": body,
|
||||
"icon": icon
|
||||
}
|
||||
);
|
||||
|
||||
if (onclick) {
|
||||
notification.onclick = onclick;
|
||||
}
|
||||
|
||||
$timeout(function() {
|
||||
notification.close();
|
||||
}, 5 * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
@ -5,6 +5,7 @@
|
||||
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<link rel="stylesheet" href="mobile.css">
|
||||
<link rel="stylesheet" href="bootstrap.css">
|
||||
|
||||
<link rel="icon" href="favicon.ico">
|
||||
|
||||
@ -16,8 +17,10 @@
|
||||
<script src="js/angular-route.min.js"></script>
|
||||
<script src="js/angular-sanitize.min.js"></script>
|
||||
<script src="js/angular-animate.min.js"></script>
|
||||
<script type='text/javascript' src="js/ui-bootstrap-tpls-0.11.2.js"></script>
|
||||
<script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script>
|
||||
<script type='text/javascript' src='js/autofill-event.js'></script>
|
||||
<script type='text/javascript' src='js/elastic.js'></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="app-controller.js"></script>
|
||||
@ -38,6 +41,7 @@
|
||||
<script src="components/matrix/matrix-phone-service.js"></script>
|
||||
<script src="components/matrix/event-stream-service.js"></script>
|
||||
<script src="components/matrix/event-handler-service.js"></script>
|
||||
<script src="components/matrix/notification-service.js"></script>
|
||||
<script src="components/matrix/presence-service.js"></script>
|
||||
<script src="components/fileInput/file-input-directive.js"></script>
|
||||
<script src="components/fileUpload/file-upload-service.js"></script>
|
||||
|
216
webclient/js/elastic.js
Normal file
216
webclient/js/elastic.js
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* angular-elastic v2.4.0
|
||||
* (c) 2014 Monospaced http://monospaced.com
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
angular.module('monospaced.elastic', [])
|
||||
|
||||
.constant('msdElasticConfig', {
|
||||
append: ''
|
||||
})
|
||||
|
||||
.directive('msdElastic', [
|
||||
'$timeout', '$window', 'msdElasticConfig',
|
||||
function($timeout, $window, config) {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
require: 'ngModel',
|
||||
restrict: 'A, C',
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
|
||||
// cache a reference to the DOM element
|
||||
var ta = element[0],
|
||||
$ta = element;
|
||||
|
||||
// ensure the element is a textarea, and browser is capable
|
||||
if (ta.nodeName !== 'TEXTAREA' || !$window.getComputedStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set these properties before measuring dimensions
|
||||
$ta.css({
|
||||
'overflow': 'hidden',
|
||||
'overflow-y': 'hidden',
|
||||
'word-wrap': 'break-word'
|
||||
});
|
||||
|
||||
// force text reflow
|
||||
var text = ta.value;
|
||||
ta.value = '';
|
||||
ta.value = text;
|
||||
|
||||
var append = attrs.msdElastic ? attrs.msdElastic.replace(/\\n/g, '\n') : config.append,
|
||||
$win = angular.element($window),
|
||||
mirrorInitStyle = 'position: absolute; top: -999px; right: auto; bottom: auto;' +
|
||||
'left: 0; overflow: hidden; -webkit-box-sizing: content-box;' +
|
||||
'-moz-box-sizing: content-box; box-sizing: content-box;' +
|
||||
'min-height: 0 !important; height: 0 !important; padding: 0;' +
|
||||
'word-wrap: break-word; border: 0;',
|
||||
$mirror = angular.element('<textarea tabindex="-1" ' +
|
||||
'style="' + mirrorInitStyle + '"/>').data('elastic', true),
|
||||
mirror = $mirror[0],
|
||||
taStyle = getComputedStyle(ta),
|
||||
resize = taStyle.getPropertyValue('resize'),
|
||||
borderBox = taStyle.getPropertyValue('box-sizing') === 'border-box' ||
|
||||
taStyle.getPropertyValue('-moz-box-sizing') === 'border-box' ||
|
||||
taStyle.getPropertyValue('-webkit-box-sizing') === 'border-box',
|
||||
boxOuter = !borderBox ? {width: 0, height: 0} : {
|
||||
width: parseInt(taStyle.getPropertyValue('border-right-width'), 10) +
|
||||
parseInt(taStyle.getPropertyValue('padding-right'), 10) +
|
||||
parseInt(taStyle.getPropertyValue('padding-left'), 10) +
|
||||
parseInt(taStyle.getPropertyValue('border-left-width'), 10),
|
||||
height: parseInt(taStyle.getPropertyValue('border-top-width'), 10) +
|
||||
parseInt(taStyle.getPropertyValue('padding-top'), 10) +
|
||||
parseInt(taStyle.getPropertyValue('padding-bottom'), 10) +
|
||||
parseInt(taStyle.getPropertyValue('border-bottom-width'), 10)
|
||||
},
|
||||
minHeightValue = parseInt(taStyle.getPropertyValue('min-height'), 10),
|
||||
heightValue = parseInt(taStyle.getPropertyValue('height'), 10),
|
||||
minHeight = Math.max(minHeightValue, heightValue) - boxOuter.height,
|
||||
maxHeight = parseInt(taStyle.getPropertyValue('max-height'), 10),
|
||||
mirrored,
|
||||
active,
|
||||
copyStyle = ['font-family',
|
||||
'font-size',
|
||||
'font-weight',
|
||||
'font-style',
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'text-transform',
|
||||
'word-spacing',
|
||||
'text-indent'];
|
||||
|
||||
// exit if elastic already applied (or is the mirror element)
|
||||
if ($ta.data('elastic')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Opera returns max-height of -1 if not set
|
||||
maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
|
||||
|
||||
// append mirror to the DOM
|
||||
if (mirror.parentNode !== document.body) {
|
||||
angular.element(document.body).append(mirror);
|
||||
}
|
||||
|
||||
// set resize and apply elastic
|
||||
$ta.css({
|
||||
'resize': (resize === 'none' || resize === 'vertical') ? 'none' : 'horizontal'
|
||||
}).data('elastic', true);
|
||||
|
||||
/*
|
||||
* methods
|
||||
*/
|
||||
|
||||
function initMirror() {
|
||||
var mirrorStyle = mirrorInitStyle;
|
||||
|
||||
mirrored = ta;
|
||||
// copy the essential styles from the textarea to the mirror
|
||||
taStyle = getComputedStyle(ta);
|
||||
angular.forEach(copyStyle, function(val) {
|
||||
mirrorStyle += val + ':' + taStyle.getPropertyValue(val) + ';';
|
||||
});
|
||||
mirror.setAttribute('style', mirrorStyle);
|
||||
}
|
||||
|
||||
function adjust() {
|
||||
var taHeight,
|
||||
taComputedStyleWidth,
|
||||
mirrorHeight,
|
||||
width,
|
||||
overflow;
|
||||
|
||||
if (mirrored !== ta) {
|
||||
initMirror();
|
||||
}
|
||||
|
||||
// active flag prevents actions in function from calling adjust again
|
||||
if (!active) {
|
||||
active = true;
|
||||
|
||||
mirror.value = ta.value + append; // optional whitespace to improve animation
|
||||
mirror.style.overflowY = ta.style.overflowY;
|
||||
|
||||
taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10);
|
||||
|
||||
taComputedStyleWidth = getComputedStyle(ta).getPropertyValue('width');
|
||||
|
||||
// ensure getComputedStyle has returned a readable 'used value' pixel width
|
||||
if (taComputedStyleWidth.substr(taComputedStyleWidth.length - 2, 2) === 'px') {
|
||||
// update mirror width in case the textarea width has changed
|
||||
width = parseInt(taComputedStyleWidth, 10) - boxOuter.width;
|
||||
mirror.style.width = width + 'px';
|
||||
}
|
||||
|
||||
mirrorHeight = mirror.scrollHeight;
|
||||
|
||||
if (mirrorHeight > maxHeight) {
|
||||
mirrorHeight = maxHeight;
|
||||
overflow = 'scroll';
|
||||
} else if (mirrorHeight < minHeight) {
|
||||
mirrorHeight = minHeight;
|
||||
}
|
||||
mirrorHeight += boxOuter.height;
|
||||
|
||||
ta.style.overflowY = overflow || 'hidden';
|
||||
|
||||
if (taHeight !== mirrorHeight) {
|
||||
ta.style.height = mirrorHeight + 'px';
|
||||
scope.$emit('elastic:resize', $ta);
|
||||
}
|
||||
|
||||
// small delay to prevent an infinite loop
|
||||
$timeout(function() {
|
||||
active = false;
|
||||
}, 1);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function forceAdjust() {
|
||||
active = false;
|
||||
adjust();
|
||||
}
|
||||
|
||||
/*
|
||||
* initialise
|
||||
*/
|
||||
|
||||
// listen
|
||||
if ('onpropertychange' in ta && 'oninput' in ta) {
|
||||
// IE9
|
||||
ta['oninput'] = ta.onkeyup = adjust;
|
||||
} else {
|
||||
ta['oninput'] = adjust;
|
||||
}
|
||||
|
||||
$win.bind('resize', forceAdjust);
|
||||
|
||||
scope.$watch(function() {
|
||||
return ngModel.$modelValue;
|
||||
}, function(newValue) {
|
||||
forceAdjust();
|
||||
});
|
||||
|
||||
scope.$on('elastic:adjust', function() {
|
||||
initMirror();
|
||||
forceAdjust();
|
||||
});
|
||||
|
||||
$timeout(adjust);
|
||||
|
||||
/*
|
||||
* destroy
|
||||
*/
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$mirror.remove();
|
||||
$win.unbind('resize', forceAdjust);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
4167
webclient/js/ui-bootstrap-tpls-0.11.2.js
Normal file
4167
webclient/js/ui-bootstrap-tpls-0.11.2.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -140,6 +140,9 @@ angular.module('RegisterController', ['matrixService'])
|
||||
$scope.feedback = "Captcha is required on this home " +
|
||||
"server.";
|
||||
}
|
||||
else if (error.data.error) {
|
||||
$scope.feedback = error.data.error;
|
||||
}
|
||||
}
|
||||
else if (error.status === 0) {
|
||||
$scope.feedback = "Unable to talk to the server.";
|
||||
|
@ -65,13 +65,16 @@
|
||||
}
|
||||
|
||||
#roomName {
|
||||
float: left;
|
||||
font-size: 14px ! important;
|
||||
font-size: 12px ! important;
|
||||
margin-top: 0px ! important;
|
||||
}
|
||||
|
||||
.roomTopicSection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#roomPage {
|
||||
top: 35px ! important;
|
||||
top: 40px ! important;
|
||||
left: 5px ! important;
|
||||
right: 5px ! important;
|
||||
bottom: 70px ! important;
|
||||
|
@ -42,12 +42,12 @@
|
||||
<span ng-if="lastMsg.user_id === lastMsg.state_key">
|
||||
{{lastMsg.state_key | mUserDisplayName: room.room_id }} left
|
||||
</span>
|
||||
<span ng-if="lastMsg.user_id !== lastMsg.state_key">
|
||||
<span ng-if="lastMsg.user_id !== lastMsg.state_key && lastMsg.prev_content">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||
{{ {"join": "kicked", "ban": "unbanned"}[lastMsg.content.prev] }}
|
||||
{{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[lastMsg.prev_content.membership] }}
|
||||
{{ lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||
</span>
|
||||
<span ng-if="'join' === lastMsg.content.prev && lastMsg.content.reason">
|
||||
<span ng-if="lastMsg.prev_content && 'join' === lastMsg.prev_content.membership && lastMsg.content.reason">
|
||||
: {{ lastMsg.content.reason }}
|
||||
</span>
|
||||
</span>
|
||||
@ -55,7 +55,7 @@
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||
{{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
|
||||
{{ lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||
<span ng-if="'ban' === lastMsg.content.prev && lastMsg.content.reason">
|
||||
<span ng-if="lastMsg.prev_content && 'ban' === lastMsg.prev_content.membership && lastMsg.content.reason">
|
||||
: {{ lastMsg.content.reason }}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -15,11 +15,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||
.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
|
||||
function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
|
||||
.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService',
|
||||
function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService) {
|
||||
'use strict';
|
||||
var MESSAGES_PER_PAGINATION = 30;
|
||||
var THUMBNAIL_SIZE = 320;
|
||||
|
||||
// .html needs this
|
||||
$scope.containsBingWord = function(content) {
|
||||
return notificationService.containsBingWord(
|
||||
matrixService.config().user_id,
|
||||
matrixService.config().display_name,
|
||||
matrixService.config().bingWords,
|
||||
content
|
||||
);
|
||||
};
|
||||
|
||||
// Room ids. Computed and resolved in onInit
|
||||
$scope.room_id = undefined;
|
||||
@ -133,7 +143,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||
// Do not autoscroll to the bottom to display the new event if the user is not at the bottom.
|
||||
// Exception: in case where the event is from the user, we want to force scroll to the bottom
|
||||
var objDiv = document.getElementById("messageTableWrapper");
|
||||
if ((objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
|
||||
// add a 10px buffer to this check so if the message list is not *quite*
|
||||
// at the bottom it still scrolls since it basically is at the bottom.
|
||||
if ((10 + objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
|
||||
|
||||
$timeout(function() {
|
||||
objDiv.scrollTop = objDiv.scrollHeight;
|
||||
@ -189,16 +201,20 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||
// Notify when a user joins
|
||||
if ((document.hidden || matrixService.presence.unavailable === mPresence.getState())
|
||||
&& event.state_key !== $scope.state.user_id && "join" === event.membership) {
|
||||
var notification = new window.Notification(
|
||||
event.content.displayname +
|
||||
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
|
||||
{
|
||||
"body": event.content.displayname + " joined",
|
||||
"icon": event.content.avatar_url ? event.content.avatar_url : undefined
|
||||
});
|
||||
$timeout(function() {
|
||||
notification.close();
|
||||
}, 5 * 1000);
|
||||
var userName = event.content.displayname;
|
||||
if (!userName) {
|
||||
userName = event.state_key;
|
||||
}
|
||||
notificationService.showNotification(
|
||||
userName +
|
||||
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")",
|
||||
userName + " joined",
|
||||
event.content.avatar_url ? event.content.avatar_url : undefined,
|
||||
function() {
|
||||
console.log("notification.onclick() room=" + event.room_id);
|
||||
$rootScope.goToPage('room/' + event.room_id);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -830,7 +846,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||
$scope.userIDToInvite = "";
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + reason;
|
||||
$scope.feedback = "Failure: " + reason.data.error;
|
||||
});
|
||||
};
|
||||
|
||||
@ -982,4 +998,88 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
$scope.openJson = function(content) {
|
||||
$scope.event_selected = content;
|
||||
// scope this so the template can check power levels and enable/disable
|
||||
// buttons
|
||||
$scope.pow = matrixService.getUserPowerLevel;
|
||||
|
||||
var modalInstance = $modal.open({
|
||||
templateUrl: 'eventInfoTemplate.html',
|
||||
controller: 'EventInfoController',
|
||||
scope: $scope
|
||||
});
|
||||
|
||||
modalInstance.result.then(function(action) {
|
||||
if (action === "redact") {
|
||||
var eventId = $scope.event_selected.event_id;
|
||||
console.log("Redacting event ID " + eventId);
|
||||
matrixService.redactEvent(
|
||||
$scope.event_selected.room_id,
|
||||
eventId
|
||||
).then(function(response) {
|
||||
console.log("Redaction = " + JSON.stringify(response));
|
||||
}, function(error) {
|
||||
console.error("Failed to redact event: "+JSON.stringify(error));
|
||||
if (error.data.error) {
|
||||
$scope.feedback = error.data.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
// any dismiss code
|
||||
});
|
||||
};
|
||||
|
||||
$scope.openRoomInfo = function() {
|
||||
$scope.roomInfo = {};
|
||||
$scope.roomInfo.newEvent = {
|
||||
content: {},
|
||||
type: "",
|
||||
state_key: ""
|
||||
};
|
||||
|
||||
var stateFilter = $filter("stateEventsFilter");
|
||||
var stateEvents = stateFilter($scope.events.rooms[$scope.room_id]);
|
||||
// The modal dialog will 2-way bind this field, so we MUST make a deep
|
||||
// copy of the state events else we will be *actually adjusing our view
|
||||
// of the world* when fiddling with the JSON!! Apparently parse/stringify
|
||||
// is faster than jQuery's extend when doing deep copies.
|
||||
$scope.roomInfo.stateEvents = JSON.parse(JSON.stringify(stateEvents));
|
||||
var modalInstance = $modal.open({
|
||||
templateUrl: 'roomInfoTemplate.html',
|
||||
controller: 'RoomInfoController',
|
||||
size: 'lg',
|
||||
scope: $scope
|
||||
});
|
||||
};
|
||||
|
||||
}])
|
||||
.controller('EventInfoController', function($scope, $modalInstance) {
|
||||
console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected));
|
||||
$scope.redact = function() {
|
||||
console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+
|
||||
" Redact level = "+$scope.events.rooms[$scope.room_id]["m.room.ops_levels"].content.redact_level);
|
||||
console.log("Redact event >> " + JSON.stringify($scope.event_selected));
|
||||
$modalInstance.close("redact");
|
||||
};
|
||||
})
|
||||
.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) {
|
||||
console.log("Displaying room info.");
|
||||
|
||||
$scope.submit = function(event) {
|
||||
if (event.content) {
|
||||
console.log("submit >>> " + JSON.stringify(event.content));
|
||||
matrixService.sendStateEvent($scope.room_id, event.type,
|
||||
event.content, event.state_key).then(function(response) {
|
||||
$modalInstance.dismiss();
|
||||
}, function(err) {
|
||||
$scope.feedback = err.data.error;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.dismiss = $modalInstance.dismiss;
|
||||
|
||||
});
|
||||
|
@ -1,5 +1,59 @@
|
||||
<div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;">
|
||||
|
||||
<script type="text/ng-template" id="eventInfoTemplate.html">
|
||||
<div class="modal-body">
|
||||
<pre> {{event_selected | json}} </pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-click="redact()" type="button" class="btn btn-danger"
|
||||
ng-disabled="!events.rooms[room_id]['m.room.ops_levels'].content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < events.rooms[room_id]['m.room.ops_levels'].content.redact_level"
|
||||
title="Delete this event on all home servers. This cannot be undone.">
|
||||
Redact
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="roomInfoTemplate.html">
|
||||
<div class="modal-body">
|
||||
<table class="room-info">
|
||||
<tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event">
|
||||
<td class="room-info-event-meta" width="30%">
|
||||
<span class="monospace">{{ key }}</span>
|
||||
<br/>
|
||||
{{ (event.origin_server_ts) | date:'MMM d HH:mm' }}
|
||||
<br/>
|
||||
Set by: <span class="monospace">{{ event.user_id }}</span>
|
||||
<br/>
|
||||
<span ng-show="event.required_power_level >= 0">Required power level: {{event.required_power_level}}<br/></span>
|
||||
<button ng-click="submit(event)" type="button" class="btn btn-success" ng-disabled="!event.content">
|
||||
Submit
|
||||
</button>
|
||||
</td>
|
||||
<td class="room-info-event-content" width="70%">
|
||||
<textarea class="room-info-textarea-content" msd-elastic ng-model="event.content" asjson></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="room-info-event-meta" width="30%">
|
||||
<input ng-model="roomInfo.newEvent.type" placeholder="your.event.type" />
|
||||
<br/>
|
||||
<button ng-click="submit(roomInfo.newEvent)" type="button" class="btn btn-success" ng-disabled="!roomInfo.newEvent.content || !roomInfo.newEvent.type">
|
||||
Submit
|
||||
</button>
|
||||
</td>
|
||||
<td class="room-info-event-content" width="70%">
|
||||
<textarea class="room-info-textarea-content" msd-elastic ng-model="roomInfo.newEvent.content" asjson></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-click="dismiss()" type="button" class="btn">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<div id="roomHeader">
|
||||
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
|
||||
<div class="roomHeaderInfo">
|
||||
@ -79,11 +133,11 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="avatar">
|
||||
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
|
||||
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
|
||||
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
|
||||
</td>
|
||||
<td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
||||
<div class="bubble">
|
||||
<div class="bubble" ng-click="openJson(msg)">
|
||||
<span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
|
||||
{{ members[msg.state_key].displayname || msg.state_key }} joined
|
||||
</span>
|
||||
@ -91,11 +145,11 @@
|
||||
<span ng-if="msg.user_id === msg.state_key">
|
||||
{{ members[msg.state_key].displayname || msg.state_key }} left
|
||||
</span>
|
||||
<span ng-if="msg.user_id !== msg.state_key">
|
||||
<span ng-if="msg.user_id !== msg.state_key && msg.prev_content">
|
||||
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||
{{ {"join": "kicked", "ban": "unbanned"}[msg.content.prev] }}
|
||||
{{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[msg.prev_content.membership] }}
|
||||
{{ members[msg.state_key].displayname || msg.state_key }}
|
||||
<span ng-if="'join' === msg.content.prev && msg.content.reason">
|
||||
<span ng-if="'join' === msg.prev_content.membership && msg.content.reason">
|
||||
: {{ msg.content.reason }}
|
||||
</span>
|
||||
</span>
|
||||
@ -105,7 +159,7 @@
|
||||
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||
{{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
|
||||
{{ members[msg.state_key].displayname || msg.state_key }}
|
||||
<span ng-if="'ban' === msg.content.prev && msg.content.reason">
|
||||
<span ng-if="msg.prev_content && 'ban' === msg.prev_content.membership && msg.content.reason">
|
||||
: {{ msg.content.reason }}
|
||||
</span>
|
||||
</span>
|
||||
@ -115,7 +169,8 @@
|
||||
|
||||
<span ng-show='msg.content.msgtype === "m.emote"'
|
||||
ng-class="msg.echo_msg_state"
|
||||
ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
||||
ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"
|
||||
/>
|
||||
|
||||
<span ng-show='msg.content.msgtype === "m.text"'
|
||||
class="message"
|
||||
@ -133,7 +188,7 @@
|
||||
</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"/>
|
||||
ng-click="$parent.fullScreenImageURL = msg.content.url; $event.stopPropagation();"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -202,6 +257,9 @@
|
||||
>
|
||||
Video Call
|
||||
</button>
|
||||
<button ng-click="openRoomInfo()">
|
||||
Room Info
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ feedback }}
|
||||
|
Loading…
Reference in New Issue
Block a user