diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000000..1a57677a0e
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# One username per supported platform and one custom link
+patreon: matrixdotorg
+liberapay: matrixdotorg
+custom: https://paypal.me/matrixdotorg
diff --git a/changelog.d/5252.feature b/changelog.d/5252.feature
new file mode 100644
index 0000000000..44115b0382
--- /dev/null
+++ b/changelog.d/5252.feature
@@ -0,0 +1 @@
+Add monthly active users to phonehome stats.
diff --git a/changelog.d/5325.bugfix b/changelog.d/5325.bugfix
new file mode 100644
index 0000000000..b9413388f5
--- /dev/null
+++ b/changelog.d/5325.bugfix
@@ -0,0 +1 @@
+Fix a bug where running synapse_port_db would cause the account validity feature to fail because it didn't set the type of the email_sent column to boolean.
diff --git a/changelog.d/5363.feature b/changelog.d/5363.feature
new file mode 100644
index 0000000000..803fe3fc37
--- /dev/null
+++ b/changelog.d/5363.feature
@@ -0,0 +1 @@
+Allow expired user to trigger renewal email sending manually.
diff --git a/changelog.d/5382.misc b/changelog.d/5382.misc
new file mode 100644
index 0000000000..060cbba2a9
--- /dev/null
+++ b/changelog.d/5382.misc
@@ -0,0 +1 @@
+Add a sponsor button to the repo.
diff --git a/changelog.d/5386.misc b/changelog.d/5386.misc
new file mode 100644
index 0000000000..060cbba2a9
--- /dev/null
+++ b/changelog.d/5386.misc
@@ -0,0 +1 @@
+Add a sponsor button to the repo.
diff --git a/changelog.d/5387.bugfix b/changelog.d/5387.bugfix
new file mode 100644
index 0000000000..2c6c94efc4
--- /dev/null
+++ b/changelog.d/5387.bugfix
@@ -0,0 +1 @@
+Warn about disabling email-based password resets when a reset occurs, and remove warning when someone attempts a phone-based reset.
diff --git a/changelog.d/5412.feature b/changelog.d/5412.feature
new file mode 100644
index 0000000000..ec1503860a
--- /dev/null
+++ b/changelog.d/5412.feature
@@ -0,0 +1 @@
+Add --no-daemonize option to run synapse in the foreground, per issue #4130. Contributed by Soham Gumaste.
\ No newline at end of file
diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db
index 41be9c9220..b6ba19c776 100755
--- a/scripts/synapse_port_db
+++ b/scripts/synapse_port_db
@@ -54,6 +54,7 @@ BOOLEAN_COLUMNS = {
"group_roles": ["is_public"],
"local_group_membership": ["is_publicised", "is_admin"],
"e2e_room_keys": ["is_verified"],
+ "account_validity": ["email_sent"],
}
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 0c6c93a87b..79e2808dc5 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -184,11 +184,22 @@ class Auth(object):
return event_auth.get_public_keys(invite_event)
@defer.inlineCallbacks
- def get_user_by_req(self, request, allow_guest=False, rights="access"):
+ def get_user_by_req(
+ self,
+ request,
+ allow_guest=False,
+ rights="access",
+ allow_expired=False,
+ ):
""" Get a registered user's ID.
Args:
request - An HTTP request with an access_token query parameter.
+ allow_expired - Whether to allow the request through even if the account is
+ expired. If true, Synapse will still require an access token to be
+ provided but won't check if the account it belongs to has expired. This
+ works thanks to /login delivering access tokens regardless of accounts'
+ expiration.
Returns:
defer.Deferred: resolves to a ``synapse.types.Requester`` object
Raises:
@@ -229,7 +240,7 @@ class Auth(object):
is_guest = user_info["is_guest"]
# Deny the request if the user account has expired.
- if self._account_validity.enabled:
+ if self._account_validity.enabled and not allow_expired:
user_id = user.to_string()
expiration_ts = yield self.store.get_expiration_ts_for_user(user_id)
if expiration_ts is not None and self.clock.time_msec() >= expiration_ts:
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index df524a23dd..811b547dd3 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -541,6 +541,7 @@ def run(hs):
stats["total_room_count"] = room_count
stats["daily_active_users"] = yield hs.get_datastore().count_daily_users()
+ stats["monthly_active_users"] = yield hs.get_datastore().count_monthly_users()
stats["daily_active_rooms"] = yield hs.get_datastore().count_daily_active_rooms()
stats["daily_messages"] = yield hs.get_datastore().count_daily_messages()
diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py
index ae04252906..86018dfcce 100644
--- a/synapse/config/emailconfig.py
+++ b/synapse/config/emailconfig.py
@@ -19,15 +19,12 @@ from __future__ import print_function
# This file can't be called email.py because if it is, we cannot:
import email.utils
-import logging
import os
import pkg_resources
from ._base import Config, ConfigError
-logger = logging.getLogger(__name__)
-
class EmailConfig(Config):
def read_config(self, config):
@@ -85,10 +82,12 @@ class EmailConfig(Config):
self.email_password_reset_behaviour = (
"remote" if email_trust_identity_server_for_password_resets else "local"
)
+ self.password_resets_were_disabled_due_to_email_config = False
if self.email_password_reset_behaviour == "local" and email_config == {}:
- logger.warn(
- "User password resets have been disabled due to lack of email config"
- )
+ # We cannot warn the user this has happened here
+ # Instead do so when a user attempts to reset their password
+ self.password_resets_were_disabled_due_to_email_config = True
+
self.email_password_reset_behaviour = "off"
# Get lifetime of a validation token in milliseconds
diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py
index e4c63b69b9..7cfd7ae7dc 100644
--- a/synapse/rest/client/v2_alpha/account.py
+++ b/synapse/rest/client/v2_alpha/account.py
@@ -68,7 +68,13 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
@defer.inlineCallbacks
def on_POST(self, request):
if self.config.email_password_reset_behaviour == "off":
- raise SynapseError(400, "Password resets have been disabled on this server")
+ if self.config.password_resets_were_disabled_due_to_email_config:
+ logger.warn(
+ "User password resets have been disabled due to lack of email config"
+ )
+ raise SynapseError(
+ 400, "Email-based password resets have been disabled on this server",
+ )
body = parse_json_object_from_request(request)
@@ -196,9 +202,6 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
@defer.inlineCallbacks
def on_POST(self, request):
- if not self.config.email_password_reset_behaviour == "off":
- raise SynapseError(400, "Password resets have been disabled on this server")
-
body = parse_json_object_from_request(request)
assert_params_in_dict(body, [
@@ -251,6 +254,14 @@ class PasswordResetSubmitTokenServlet(RestServlet):
400,
"This medium is currently not supported for password resets",
)
+ if self.config.email_password_reset_behaviour == "off":
+ if self.config.password_resets_were_disabled_due_to_email_config:
+ logger.warn(
+ "User password resets have been disabled due to lack of email config"
+ )
+ raise SynapseError(
+ 400, "Email-based password resets have been disabled on this server",
+ )
sid = parse_string(request, "sid")
client_secret = parse_string(request, "client_secret")
diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/v2_alpha/account_validity.py
index 55c4ed5660..63bdc33564 100644
--- a/synapse/rest/client/v2_alpha/account_validity.py
+++ b/synapse/rest/client/v2_alpha/account_validity.py
@@ -79,7 +79,7 @@ class AccountValiditySendMailServlet(RestServlet):
if not self.account_validity.renew_by_email_enabled:
raise AuthError(403, "Account renewal via email is disabled on this server.")
- requester = yield self.auth.get_user_by_req(request)
+ requester = yield self.auth.get_user_by_req(request, allow_expired=True)
user_id = requester.user.to_string()
yield self.account_activity_handler.send_renewal_email_to_user(user_id)
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 71316f7d09..0ca6f6121f 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -279,23 +279,37 @@ class DataStore(
"""
Counts the number of users who used this homeserver in the last 24 hours.
"""
+ yesterday = int(self._clock.time_msec()) - (1000 * 60 * 60 * 24)
+ return self.runInteraction("count_daily_users", self._count_users, yesterday,)
- def _count_users(txn):
- yesterday = int(self._clock.time_msec()) - (1000 * 60 * 60 * 24)
-
- sql = """
- SELECT COALESCE(count(*), 0) FROM (
- SELECT user_id FROM user_ips
- WHERE last_seen > ?
- GROUP BY user_id
- ) u
- """
-
- txn.execute(sql, (yesterday,))
- count, = txn.fetchone()
- return count
+ def count_monthly_users(self):
+ """
+ Counts the number of users who used this homeserver in the last 30 days.
+ Note this method is intended for phonehome metrics only and is different
+ from the mau figure in synapse.storage.monthly_active_users which,
+ amongst other things, includes a 3 day grace period before a user counts.
+ """
+ thirty_days_ago = int(self._clock.time_msec()) - (1000 * 60 * 60 * 24 * 30)
+ return self.runInteraction(
+ "count_monthly_users",
+ self._count_users,
+ thirty_days_ago,
+ )
- return self.runInteraction("count_users", _count_users)
+ def _count_users(self, txn, time_from):
+ """
+ Returns number of users seen in the past time_from period
+ """
+ sql = """
+ SELECT COALESCE(count(*), 0) FROM (
+ SELECT user_id FROM user_ips
+ WHERE last_seen > ?
+ GROUP BY user_id
+ ) u
+ """
+ txn.execute(sql, (time_from,))
+ count, = txn.fetchone()
+ return count
def count_r30_users(self):
"""
diff --git a/synctl b/synctl
index 07a68e6d85..30d751236f 100755
--- a/synctl
+++ b/synctl
@@ -69,10 +69,14 @@ def abort(message, colour=RED, stream=sys.stderr):
sys.exit(1)
-def start(configfile):
+def start(configfile, daemonize=True):
write("Starting ...")
args = SYNAPSE
- args.extend(["--daemonize", "-c", configfile])
+
+ if daemonize:
+ args.extend(["--daemonize", "-c", configfile])
+ else:
+ args.extend(["-c", configfile])
try:
subprocess.check_call(args)
@@ -143,12 +147,21 @@ def main():
help="start or stop all the workers in the given directory"
" and the main synapse process",
)
+ parser.add_argument(
+ "--no-daemonize",
+ action="store_false",
+ help="Run synapse in the foreground for debugging. "
+ "Will work only if the daemonize option is not set in the config."
+ )
options = parser.parse_args()
if options.worker and options.all_processes:
write('Cannot use "--worker" with "--all-processes"', stream=sys.stderr)
sys.exit(1)
+ if options.no_daemonize and options.all_processes:
+ write('Cannot use "--no-daemonize" with "--all-processes"', stream=sys.stderr)
+ sys.exit(1)
configfile = options.configfile
@@ -276,7 +289,7 @@ def main():
# Check if synapse is already running
if os.path.exists(pidfile) and pid_running(int(open(pidfile).read())):
abort("synapse.app.homeserver already running")
- start(configfile)
+ start(configfile, bool(options.no_daemonize))
for worker in workers:
env = os.environ.copy()
diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py
index 0cb6a363d6..1628db501c 100644
--- a/tests/rest/client/v2_alpha/test_register.py
+++ b/tests/rest/client/v2_alpha/test_register.py
@@ -427,6 +427,41 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
self.assertEqual(len(self.email_attempts), 1)
+ def test_manual_email_send_expired_account(self):
+ user_id = self.register_user("kermit", "monkey")
+ tok = self.login("kermit", "monkey")
+
+ # We need to manually add an email address otherwise the handler will do
+ # nothing.
+ now = self.hs.clock.time_msec()
+ self.get_success(
+ self.store.user_add_threepid(
+ user_id=user_id,
+ medium="email",
+ address="kermit@example.com",
+ validated_at=now,
+ added_at=now,
+ )
+ )
+
+ # Make the account expire.
+ self.reactor.advance(datetime.timedelta(days=8).total_seconds())
+
+ # Ignore all emails sent by the automatic background task and only focus on the
+ # ones sent manually.
+ self.email_attempts = []
+
+ # Test that we're still able to manually trigger a mail to be sent.
+ request, channel = self.make_request(
+ b"POST",
+ "/_matrix/client/unstable/account_validity/send_mail",
+ access_token=tok,
+ )
+ self.render(request)
+ self.assertEquals(channel.result["code"], b"200", channel.result)
+
+ self.assertEqual(len(self.email_attempts), 1)
+
class AccountValidityBackgroundJobTestCase(unittest.HomeserverTestCase):
|