presentable_names.py 6.4 KB

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