consent_server_notices.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from six import iteritems, string_types
  17. from twisted.internet import defer
  18. from synapse.api.errors import SynapseError
  19. from synapse.api.urls import ConsentURIBuilder
  20. from synapse.config import ConfigError
  21. from synapse.types import get_localpart_from_id
  22. logger = logging.getLogger(__name__)
  23. class ConsentServerNotices(object):
  24. """Keeps track of whether we need to send users server_notices about
  25. privacy policy consent, and sends one if we do.
  26. """
  27. def __init__(self, hs):
  28. """
  29. Args:
  30. hs (synapse.server.HomeServer):
  31. """
  32. self._server_notices_manager = hs.get_server_notices_manager()
  33. self._store = hs.get_datastore()
  34. self._users_in_progress = set()
  35. self._current_consent_version = hs.config.user_consent_version
  36. self._server_notice_content = hs.config.user_consent_server_notice_content
  37. self._send_to_guests = hs.config.user_consent_server_notice_to_guests
  38. if self._server_notice_content is not None:
  39. if not self._server_notices_manager.is_enabled():
  40. raise ConfigError(
  41. "user_consent configuration requires server notices, but "
  42. "server notices are not enabled.",
  43. )
  44. if 'body' not in self._server_notice_content:
  45. raise ConfigError(
  46. "user_consent server_notice_consent must contain a 'body' "
  47. "key.",
  48. )
  49. self._consent_uri_builder = ConsentURIBuilder(hs.config)
  50. @defer.inlineCallbacks
  51. def maybe_send_server_notice_to_user(self, user_id):
  52. """Check if we need to send a notice to this user, and does so if so
  53. Args:
  54. user_id (str): user to check
  55. Returns:
  56. Deferred
  57. """
  58. if self._server_notice_content is None:
  59. # not enabled
  60. return
  61. # make sure we don't send two messages to the same user at once
  62. if user_id in self._users_in_progress:
  63. return
  64. self._users_in_progress.add(user_id)
  65. try:
  66. u = yield self._store.get_user_by_id(user_id)
  67. if u["is_guest"] and not self._send_to_guests:
  68. # don't send to guests
  69. return
  70. if u["consent_version"] == self._current_consent_version:
  71. # user has already consented
  72. return
  73. if u["consent_server_notice_sent"] == self._current_consent_version:
  74. # we've already sent a notice to the user
  75. return
  76. # need to send a message.
  77. try:
  78. consent_uri = self._consent_uri_builder.build_user_consent_uri(
  79. get_localpart_from_id(user_id),
  80. )
  81. content = copy_with_str_subst(
  82. self._server_notice_content, {
  83. 'consent_uri': consent_uri,
  84. },
  85. )
  86. yield self._server_notices_manager.send_notice(
  87. user_id, content,
  88. )
  89. yield self._store.user_set_consent_server_notice_sent(
  90. user_id, self._current_consent_version,
  91. )
  92. except SynapseError as e:
  93. logger.error("Error sending server notice about user consent: %s", e)
  94. finally:
  95. self._users_in_progress.remove(user_id)
  96. def copy_with_str_subst(x, substitutions):
  97. """Deep-copy a structure, carrying out string substitions on any strings
  98. Args:
  99. x (object): structure to be copied
  100. substitutions (object): substitutions to be made - passed into the
  101. string '%' operator
  102. Returns:
  103. copy of x
  104. """
  105. if isinstance(x, string_types):
  106. return x % substitutions
  107. if isinstance(x, dict):
  108. return {
  109. k: copy_with_str_subst(v, substitutions) for (k, v) in iteritems(x)
  110. }
  111. if isinstance(x, (list, tuple)):
  112. return [copy_with_str_subst(y) for y in x]
  113. # assume it's uninterested and can be shallow-copied.
  114. return x