presentable_names.py 7.5 KB

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