Browse Source

Add `mau_appservice_trial_days` config (#12619)

* Add mau_appservice_trial_days

* Add a test

* Tweaks

* changelog

* Ensure we sync after the delay

* Fix types

* Add config statement

* Fix test

* Reinstate logging that got removed

* Fix feature name
Will Hunt 2 years ago
parent
commit
2d74a8c178

+ 1 - 0
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.

+ 7 - 0
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

+ 14 - 0
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

+ 8 - 0
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

+ 6 - 2
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
 

+ 74 - 0
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,