diff --git a/changelog.d/12619.feature b/changelog.d/12619.feature
new file mode 100644
index 0000000000..b0fc0f5fed
--- /dev/null
+++ b/changelog.d/12619.feature
@@ -0,0 +1 @@
+Add new `mau_appservice_trial_days` configuration option to specify a different trial period for users registered via an appservice.
diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml
index 5eba0fcf3d..a803b8261d 100644
--- a/docs/sample_config.yaml
+++ b/docs/sample_config.yaml
@@ -407,6 +407,11 @@ manhole_settings:
# sign up in a short space of time never to return after their initial
# session.
#
+# The option `mau_appservice_trial_days` is similar to `mau_trial_days`, but
+# applies a different trial number if the user was registered by an appservice.
+# A value of 0 means no trial days are applied. Appservices not listed in this
+# dictionary use the value of `mau_trial_days` instead.
+#
# 'mau_limit_alerting' is a means of limiting client side alerting
# should the mau limit be reached. This is useful for small instances
# where the admin has 5 mau seats (say) for 5 specific people and no
@@ -417,6 +422,8 @@ manhole_settings:
#max_mau_value: 50
#mau_trial_days: 2
#mau_limit_alerting: false
+#mau_appservice_trial_days:
+# "appservice-id": 1
# If enabled, the metrics for the number of monthly active users will
# be populated, however no one will be limited. If limit_usage_by_mau
diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md
index 36db649467..21dad0ac41 100644
--- a/docs/usage/configuration/config_documentation.md
+++ b/docs/usage/configuration/config_documentation.md
@@ -627,6 +627,20 @@ Example configuration:
mau_trial_days: 5
```
---
+Config option: `mau_appservice_trial_days`
+
+The option `mau_appservice_trial_days` is similar to `mau_trial_days`, but applies a different
+trial number if the user was registered by an appservice. A value
+of 0 means no trial days are applied. Appservices not listed in this dictionary
+use the value of `mau_trial_days` instead.
+
+Example configuration:
+```yaml
+mau_appservice_trial_days:
+ my_appservice_id: 3
+ another_appservice_id: 6
+```
+---
Config option: `mau_limit_alerting`
The option `mau_limit_alerting` is a means of limiting client-side alerting
diff --git a/synapse/config/server.py b/synapse/config/server.py
index b6cd326416..1e709c7cf5 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -413,6 +413,7 @@ class ServerConfig(Config):
)
self.mau_trial_days = config.get("mau_trial_days", 0)
+ self.mau_appservice_trial_days = config.get("mau_appservice_trial_days", {})
self.mau_limit_alerting = config.get("mau_limit_alerting", True)
# How long to keep redacted events in the database in unredacted form
@@ -1105,6 +1106,11 @@ class ServerConfig(Config):
# sign up in a short space of time never to return after their initial
# session.
#
+ # The option `mau_appservice_trial_days` is similar to `mau_trial_days`, but
+ # applies a different trial number if the user was registered by an appservice.
+ # A value of 0 means no trial days are applied. Appservices not listed in this
+ # dictionary use the value of `mau_trial_days` instead.
+ #
# 'mau_limit_alerting' is a means of limiting client side alerting
# should the mau limit be reached. This is useful for small instances
# where the admin has 5 mau seats (say) for 5 specific people and no
@@ -1115,6 +1121,8 @@ class ServerConfig(Config):
#max_mau_value: 50
#mau_trial_days: 2
#mau_limit_alerting: false
+ #mau_appservice_trial_days:
+ # "appservice-id": 1
# If enabled, the metrics for the number of monthly active users will
# be populated, however no one will be limited. If limit_usage_by_mau
diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py
index d43163c27c..4991360b70 100644
--- a/synapse/storage/databases/main/registration.py
+++ b/synapse/storage/databases/main/registration.py
@@ -215,7 +215,8 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
async def is_trial_user(self, user_id: str) -> bool:
"""Checks if user is in the "trial" period, i.e. within the first
- N days of registration defined by `mau_trial_days` config
+ N days of registration defined by `mau_trial_days` config or the
+ `mau_appservice_trial_days` config.
Args:
user_id: The user to check for trial status.
@@ -226,7 +227,10 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
return False
now = self._clock.time_msec()
- trial_duration_ms = self.config.server.mau_trial_days * 24 * 60 * 60 * 1000
+ days = self.config.server.mau_appservice_trial_days.get(
+ info["appservice_id"], self.config.server.mau_trial_days
+ )
+ trial_duration_ms = days * 24 * 60 * 60 * 1000
is_trial = (now - info["creation_ts"] * 1000) < trial_duration_ms
return is_trial
diff --git a/tests/test_mau.py b/tests/test_mau.py
index 46bd3075de..5bbc361aa2 100644
--- a/tests/test_mau.py
+++ b/tests/test_mau.py
@@ -14,6 +14,8 @@
"""Tests REST events for /rooms paths."""
+from typing import List
+
from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType
from synapse.api.errors import Codes, HttpResponseException, SynapseError
from synapse.appservice import ApplicationService
@@ -229,6 +231,78 @@ class TestMauLimit(unittest.HomeserverTestCase):
self.reactor.advance(100)
self.assertEqual(2, self.successResultOf(count))
+ @override_config(
+ {
+ "mau_trial_days": 3,
+ "mau_appservice_trial_days": {"SomeASID": 1, "AnotherASID": 2},
+ }
+ )
+ def test_as_trial_days(self):
+ user_tokens: List[str] = []
+
+ def advance_time_and_sync():
+ self.reactor.advance(24 * 60 * 61)
+ for token in user_tokens:
+ self.do_sync_for_user(token)
+
+ # Cheekily add an application service that we use to register a new user
+ # with.
+ as_token_1 = "foobartoken1"
+ self.store.services_cache.append(
+ ApplicationService(
+ token=as_token_1,
+ hostname=self.hs.hostname,
+ id="SomeASID",
+ sender="@as_sender_1:test",
+ namespaces={"users": [{"regex": "@as_1.*", "exclusive": True}]},
+ )
+ )
+
+ as_token_2 = "foobartoken2"
+ self.store.services_cache.append(
+ ApplicationService(
+ token=as_token_2,
+ hostname=self.hs.hostname,
+ id="AnotherASID",
+ sender="@as_sender_2:test",
+ namespaces={"users": [{"regex": "@as_2.*", "exclusive": True}]},
+ )
+ )
+
+ user_tokens.append(self.create_user("kermit1"))
+ user_tokens.append(self.create_user("kermit2"))
+ user_tokens.append(
+ self.create_user("as_1kermit3", token=as_token_1, appservice=True)
+ )
+ user_tokens.append(
+ self.create_user("as_2kermit4", token=as_token_2, appservice=True)
+ )
+
+ # Advance time by 1 day to include the first appservice
+ advance_time_and_sync()
+ self.assertEqual(
+ self.get_success(self.store.get_monthly_active_count_by_service()),
+ {"SomeASID": 1},
+ )
+
+ # Advance time by 1 day to include the next appservice
+ advance_time_and_sync()
+ self.assertEqual(
+ self.get_success(self.store.get_monthly_active_count_by_service()),
+ {"SomeASID": 1, "AnotherASID": 1},
+ )
+
+ # Advance time by 1 day to include the native users
+ advance_time_and_sync()
+ self.assertEqual(
+ self.get_success(self.store.get_monthly_active_count_by_service()),
+ {
+ "SomeASID": 1,
+ "AnotherASID": 1,
+ "native": 2,
+ },
+ )
+
def create_user(self, localpart, token=None, appservice=False):
request_data = {
"username": localpart,
|