presentable_names.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 OpenMarket 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. from twisted.internet import defer
  16. import re
  17. import logging
  18. logger = logging.getLogger(__name__)
  19. # intentionally looser than what aliases we allow to be registered since
  20. # other HSes may allow aliases that we would not
  21. ALIAS_RE = re.compile(r"^#.*:.+$")
  22. ALL_ALONE = "Empty Room"
  23. @defer.inlineCallbacks
  24. def calculate_room_name(store, room_state_ids, user_id, fallback_to_members=True,
  25. fallback_to_single_member=True):
  26. """
  27. Works out a user-facing name for the given room as per Matrix
  28. spec recommendations.
  29. Does not yet support internationalisation.
  30. Args:
  31. room_state: Dictionary of the room's state
  32. user_id: The ID of the user to whom the room name is being presented
  33. fallback_to_members: If False, return None instead of generating a name
  34. based on the room's members if the room has no
  35. title or aliases.
  36. Returns:
  37. (string or None) A human readable name for the room.
  38. """
  39. # does it have a name?
  40. if ("m.room.name", "") in room_state_ids:
  41. m_room_name = yield store.get_event(
  42. room_state_ids[("m.room.name", "")], allow_none=True
  43. )
  44. if m_room_name and m_room_name.content and m_room_name.content["name"]:
  45. defer.returnValue(m_room_name.content["name"])
  46. # does it have a canonical alias?
  47. if ("m.room.canonical_alias", "") in room_state_ids:
  48. canon_alias = yield store.get_event(
  49. room_state_ids[("m.room.canonical_alias", "")], allow_none=True
  50. )
  51. if (
  52. canon_alias and canon_alias.content and canon_alias.content["alias"] and
  53. _looks_like_an_alias(canon_alias.content["alias"])
  54. ):
  55. defer.returnValue(canon_alias.content["alias"])
  56. # at this point we're going to need to search the state by all state keys
  57. # for an event type, so rearrange the data structure
  58. room_state_bytype_ids = _state_as_two_level_dict(room_state_ids)
  59. # right then, any aliases at all?
  60. if "m.room.aliases" in room_state_bytype_ids:
  61. m_room_aliases = room_state_bytype_ids["m.room.aliases"]
  62. for alias_id in m_room_aliases.values():
  63. alias_event = yield store.get_event(
  64. alias_id, allow_none=True
  65. )
  66. if alias_event and alias_event.content.get("aliases"):
  67. the_aliases = alias_event.content["aliases"]
  68. if len(the_aliases) > 0 and _looks_like_an_alias(the_aliases[0]):
  69. defer.returnValue(the_aliases[0])
  70. if not fallback_to_members:
  71. defer.returnValue(None)
  72. my_member_event = None
  73. if ("m.room.member", user_id) in room_state_ids:
  74. my_member_event = yield store.get_event(
  75. room_state_ids[("m.room.member", user_id)], allow_none=True
  76. )
  77. if (
  78. my_member_event is not None and
  79. my_member_event.content['membership'] == "invite"
  80. ):
  81. if ("m.room.member", my_member_event.sender) in room_state_ids:
  82. inviter_member_event = yield store.get_event(
  83. room_state_ids[("m.room.member", my_member_event.sender)],
  84. allow_none=True,
  85. )
  86. if inviter_member_event:
  87. if fallback_to_single_member:
  88. defer.returnValue(
  89. "Invite from %s" % (
  90. name_from_member_event(inviter_member_event),
  91. )
  92. )
  93. else:
  94. return
  95. else:
  96. defer.returnValue("Room Invite")
  97. # we're going to have to generate a name based on who's in the room,
  98. # so find out who is in the room that isn't the user.
  99. if "m.room.member" in room_state_bytype_ids:
  100. member_events = yield store.get_events(
  101. room_state_bytype_ids["m.room.member"].values()
  102. )
  103. all_members = [
  104. ev for ev in member_events.values()
  105. if ev.content['membership'] == "join" or ev.content['membership'] == "invite"
  106. ]
  107. # Sort the member events oldest-first so the we name people in the
  108. # order the joined (it should at least be deterministic rather than
  109. # dictionary iteration order)
  110. all_members.sort(key=lambda e: e.origin_server_ts)
  111. other_members = [m for m in all_members if m.state_key != user_id]
  112. else:
  113. other_members = []
  114. all_members = []
  115. if len(other_members) == 0:
  116. if len(all_members) == 1:
  117. # self-chat, peeked room with 1 participant,
  118. # or inbound invite, or outbound 3PID invite.
  119. if all_members[0].sender == user_id:
  120. if "m.room.third_party_invite" in room_state_bytype_ids:
  121. third_party_invites = (
  122. room_state_bytype_ids["m.room.third_party_invite"].values()
  123. )
  124. if len(third_party_invites) > 0:
  125. # technically third party invite events are not member
  126. # events, but they are close enough
  127. # FIXME: no they're not - they look nothing like a member;
  128. # they have a great big encrypted thing as their name to
  129. # prevent leaking the 3PID name...
  130. # return "Inviting %s" % (
  131. # descriptor_from_member_events(third_party_invites)
  132. # )
  133. defer.returnValue("Inviting email address")
  134. else:
  135. defer.returnValue(ALL_ALONE)
  136. else:
  137. defer.returnValue(name_from_member_event(all_members[0]))
  138. else:
  139. defer.returnValue(ALL_ALONE)
  140. elif len(other_members) == 1 and not fallback_to_single_member:
  141. return
  142. else:
  143. defer.returnValue(descriptor_from_member_events(other_members))
  144. def descriptor_from_member_events(member_events):
  145. if len(member_events) == 0:
  146. return "nobody"
  147. elif len(member_events) == 1:
  148. return name_from_member_event(member_events[0])
  149. elif len(member_events) == 2:
  150. return "%s and %s" % (
  151. name_from_member_event(member_events[0]),
  152. name_from_member_event(member_events[1]),
  153. )
  154. else:
  155. return "%s and %d others" % (
  156. name_from_member_event(member_events[0]),
  157. len(member_events) - 1,
  158. )
  159. def name_from_member_event(member_event):
  160. if (
  161. member_event.content and "displayname" in member_event.content and
  162. member_event.content["displayname"]
  163. ):
  164. return member_event.content["displayname"]
  165. return member_event.state_key
  166. def _state_as_two_level_dict(state):
  167. ret = {}
  168. for k, v in state.items():
  169. ret.setdefault(k[0], {})[k[1]] = v
  170. return ret
  171. def _looks_like_an_alias(string):
  172. return ALIAS_RE.match(string) is not None