mirror of
https://github.com/element-hq/synapse.git
synced 2024-12-19 01:21:09 +03:00
Merge branch 'develop' of github.com:matrix-org/synapse into matrix-org-hotfixes
This commit is contained in:
commit
bbc0dbeec0
11 changed files with 98 additions and 54 deletions
|
@ -399,8 +399,7 @@ class SynchrotronServer(HomeServer):
|
||||||
position = row[position_index]
|
position = row[position_index]
|
||||||
user_id = row[user_index]
|
user_id = row[user_index]
|
||||||
|
|
||||||
rooms = yield store.get_rooms_for_user(user_id)
|
room_ids = yield store.get_rooms_for_user(user_id)
|
||||||
room_ids = [r.room_id for r in rooms]
|
|
||||||
|
|
||||||
notifier.on_new_event(
|
notifier.on_new_event(
|
||||||
"device_list_key", position, rooms=room_ids,
|
"device_list_key", position, rooms=room_ids,
|
||||||
|
|
|
@ -248,8 +248,7 @@ class DeviceHandler(BaseHandler):
|
||||||
user_id, device_ids, list(hosts)
|
user_id, device_ids, list(hosts)
|
||||||
)
|
)
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
room_ids = [r.room_id for r in rooms]
|
|
||||||
|
|
||||||
yield self.notifier.on_new_event(
|
yield self.notifier.on_new_event(
|
||||||
"device_list_key", position, rooms=room_ids,
|
"device_list_key", position, rooms=room_ids,
|
||||||
|
@ -270,8 +269,7 @@ class DeviceHandler(BaseHandler):
|
||||||
user_id (str)
|
user_id (str)
|
||||||
from_token (StreamToken)
|
from_token (StreamToken)
|
||||||
"""
|
"""
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
room_ids = set(r.room_id for r in rooms)
|
|
||||||
|
|
||||||
# First we check if any devices have changed
|
# First we check if any devices have changed
|
||||||
changed = yield self.store.get_user_whose_devices_changed(
|
changed = yield self.store.get_user_whose_devices_changed(
|
||||||
|
@ -347,8 +345,8 @@ class DeviceHandler(BaseHandler):
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def user_left_room(self, user, room_id):
|
def user_left_room(self, user, room_id):
|
||||||
user_id = user.to_string()
|
user_id = user.to_string()
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
if not rooms:
|
if not room_ids:
|
||||||
# We no longer share rooms with this user, so we'll no longer
|
# We no longer share rooms with this user, so we'll no longer
|
||||||
# receive device updates. Mark this in DB.
|
# receive device updates. Mark this in DB.
|
||||||
yield self.store.mark_remote_user_device_list_as_unsubscribed(user_id)
|
yield self.store.mark_remote_user_device_list_as_unsubscribed(user_id)
|
||||||
|
@ -404,8 +402,8 @@ class DeviceListEduUpdater(object):
|
||||||
logger.warning("Got device list update edu for %r from %r", user_id, origin)
|
logger.warning("Got device list update edu for %r from %r", user_id, origin)
|
||||||
return
|
return
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
if not rooms:
|
if not room_ids:
|
||||||
# We don't share any rooms with this user. Ignore update, as we
|
# We don't share any rooms with this user. Ignore update, as we
|
||||||
# probably won't get any further updates.
|
# probably won't get any further updates.
|
||||||
return
|
return
|
||||||
|
|
|
@ -560,9 +560,9 @@ class PresenceHandler(object):
|
||||||
room_ids_to_states = {}
|
room_ids_to_states = {}
|
||||||
users_to_states = {}
|
users_to_states = {}
|
||||||
for state in states:
|
for state in states:
|
||||||
events = yield self.store.get_rooms_for_user(state.user_id)
|
room_ids = yield self.store.get_rooms_for_user(state.user_id)
|
||||||
for e in events:
|
for room_id in room_ids:
|
||||||
room_ids_to_states.setdefault(e.room_id, []).append(state)
|
room_ids_to_states.setdefault(room_id, []).append(state)
|
||||||
|
|
||||||
plist = yield self.store.get_presence_list_observers_accepted(state.user_id)
|
plist = yield self.store.get_presence_list_observers_accepted(state.user_id)
|
||||||
for u in plist:
|
for u in plist:
|
||||||
|
@ -916,11 +916,12 @@ class PresenceHandler(object):
|
||||||
def is_visible(self, observed_user, observer_user):
|
def is_visible(self, observed_user, observer_user):
|
||||||
"""Returns whether a user can see another user's presence.
|
"""Returns whether a user can see another user's presence.
|
||||||
"""
|
"""
|
||||||
observer_rooms = yield self.store.get_rooms_for_user(observer_user.to_string())
|
observer_room_ids = yield self.store.get_rooms_for_user(
|
||||||
observed_rooms = yield self.store.get_rooms_for_user(observed_user.to_string())
|
observer_user.to_string()
|
||||||
|
)
|
||||||
observer_room_ids = set(r.room_id for r in observer_rooms)
|
observed_room_ids = yield self.store.get_rooms_for_user(
|
||||||
observed_room_ids = set(r.room_id for r in observed_rooms)
|
observed_user.to_string()
|
||||||
|
)
|
||||||
|
|
||||||
if observer_room_ids & observed_room_ids:
|
if observer_room_ids & observed_room_ids:
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
|
@ -1177,7 +1178,10 @@ def handle_timeout(state, is_mine, syncing_user_ids, now):
|
||||||
# If there are have been no sync for a while (and none ongoing),
|
# If there are have been no sync for a while (and none ongoing),
|
||||||
# set presence to offline
|
# set presence to offline
|
||||||
if user_id not in syncing_user_ids:
|
if user_id not in syncing_user_ids:
|
||||||
if now - state.last_user_sync_ts > SYNC_ONLINE_TIMEOUT:
|
# If the user has done something recently but hasn't synced,
|
||||||
|
# don't set them as offline.
|
||||||
|
sync_or_active = max(state.last_user_sync_ts, state.last_active_ts)
|
||||||
|
if now - sync_or_active > SYNC_ONLINE_TIMEOUT:
|
||||||
state = state.copy_and_replace(
|
state = state.copy_and_replace(
|
||||||
state=PresenceState.OFFLINE,
|
state=PresenceState.OFFLINE,
|
||||||
status_msg=None,
|
status_msg=None,
|
||||||
|
|
|
@ -156,11 +156,11 @@ class ProfileHandler(BaseHandler):
|
||||||
|
|
||||||
self.ratelimit(requester)
|
self.ratelimit(requester)
|
||||||
|
|
||||||
joins = yield self.store.get_rooms_for_user(
|
room_ids = yield self.store.get_rooms_for_user(
|
||||||
user.to_string(),
|
user.to_string(),
|
||||||
)
|
)
|
||||||
|
|
||||||
for j in joins:
|
for room_id in room_ids:
|
||||||
handler = self.hs.get_handlers().room_member_handler
|
handler = self.hs.get_handlers().room_member_handler
|
||||||
try:
|
try:
|
||||||
# Assume the user isn't a guest because we don't let guests set
|
# Assume the user isn't a guest because we don't let guests set
|
||||||
|
@ -171,12 +171,12 @@ class ProfileHandler(BaseHandler):
|
||||||
yield handler.update_membership(
|
yield handler.update_membership(
|
||||||
requester,
|
requester,
|
||||||
user,
|
user,
|
||||||
j.room_id,
|
room_id,
|
||||||
"join", # We treat a profile update like a join.
|
"join", # We treat a profile update like a join.
|
||||||
ratelimit=False, # Try to hide that these events aren't atomic.
|
ratelimit=False, # Try to hide that these events aren't atomic.
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Failed to update join event for room %s - %s",
|
"Failed to update join event for room %s - %s",
|
||||||
j.room_id, str(e.message)
|
room_id, str(e.message)
|
||||||
)
|
)
|
||||||
|
|
|
@ -210,10 +210,9 @@ class ReceiptEventSource(object):
|
||||||
else:
|
else:
|
||||||
from_key = None
|
from_key = None
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user.to_string())
|
room_ids = yield self.store.get_rooms_for_user(user.to_string())
|
||||||
rooms = [room.room_id for room in rooms]
|
|
||||||
events = yield self.store.get_linearized_receipts_for_rooms(
|
events = yield self.store.get_linearized_receipts_for_rooms(
|
||||||
rooms,
|
room_ids,
|
||||||
from_key=from_key,
|
from_key=from_key,
|
||||||
to_key=to_key,
|
to_key=to_key,
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ from synapse.util.metrics import Measure, measure_func
|
||||||
from synapse.util.caches.response_cache import ResponseCache
|
from synapse.util.caches.response_cache import ResponseCache
|
||||||
from synapse.push.clientformat import format_push_rules_for_user
|
from synapse.push.clientformat import format_push_rules_for_user
|
||||||
from synapse.visibility import filter_events_for_client
|
from synapse.visibility import filter_events_for_client
|
||||||
|
from synapse.types import RoomStreamToken
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
@ -225,8 +226,7 @@ class SyncHandler(object):
|
||||||
with Measure(self.clock, "ephemeral_by_room"):
|
with Measure(self.clock, "ephemeral_by_room"):
|
||||||
typing_key = since_token.typing_key if since_token else "0"
|
typing_key = since_token.typing_key if since_token else "0"
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(sync_config.user.to_string())
|
room_ids = yield self.store.get_rooms_for_user(sync_config.user.to_string())
|
||||||
room_ids = [room.room_id for room in rooms]
|
|
||||||
|
|
||||||
typing_source = self.event_sources.sources["typing"]
|
typing_source = self.event_sources.sources["typing"]
|
||||||
typing, typing_key = yield typing_source.get_new_events(
|
typing, typing_key = yield typing_source.get_new_events(
|
||||||
|
@ -568,16 +568,15 @@ class SyncHandler(object):
|
||||||
since_token = sync_result_builder.since_token
|
since_token = sync_result_builder.since_token
|
||||||
|
|
||||||
if since_token and since_token.device_list_key:
|
if since_token and since_token.device_list_key:
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
room_ids = set(r.room_id for r in rooms)
|
|
||||||
|
|
||||||
user_ids_changed = set()
|
user_ids_changed = set()
|
||||||
changed = yield self.store.get_user_whose_devices_changed(
|
changed = yield self.store.get_user_whose_devices_changed(
|
||||||
since_token.device_list_key
|
since_token.device_list_key
|
||||||
)
|
)
|
||||||
for other_user_id in changed:
|
for other_user_id in changed:
|
||||||
other_rooms = yield self.store.get_rooms_for_user(other_user_id)
|
other_room_ids = yield self.store.get_rooms_for_user(other_user_id)
|
||||||
if room_ids.intersection(e.room_id for e in other_rooms):
|
if room_ids.intersection(other_room_ids):
|
||||||
user_ids_changed.add(other_user_id)
|
user_ids_changed.add(other_user_id)
|
||||||
|
|
||||||
defer.returnValue(user_ids_changed)
|
defer.returnValue(user_ids_changed)
|
||||||
|
@ -765,6 +764,21 @@ class SyncHandler(object):
|
||||||
)
|
)
|
||||||
sync_result_builder.now_token = now_token
|
sync_result_builder.now_token = now_token
|
||||||
|
|
||||||
|
# We check up front if anything has changed, if it hasn't then there is
|
||||||
|
# no point in going futher.
|
||||||
|
since_token = sync_result_builder.since_token
|
||||||
|
if not sync_result_builder.full_state:
|
||||||
|
if since_token and not ephemeral_by_room and not account_data_by_room:
|
||||||
|
have_changed = yield self._have_rooms_changed(sync_result_builder)
|
||||||
|
if not have_changed:
|
||||||
|
tags_by_room = yield self.store.get_updated_tags(
|
||||||
|
user_id,
|
||||||
|
since_token.account_data_key,
|
||||||
|
)
|
||||||
|
if not tags_by_room:
|
||||||
|
logger.debug("no-oping sync")
|
||||||
|
defer.returnValue(([], []))
|
||||||
|
|
||||||
ignored_account_data = yield self.store.get_global_account_data_by_type_for_user(
|
ignored_account_data = yield self.store.get_global_account_data_by_type_for_user(
|
||||||
"m.ignored_user_list", user_id=user_id,
|
"m.ignored_user_list", user_id=user_id,
|
||||||
)
|
)
|
||||||
|
@ -774,13 +788,12 @@ class SyncHandler(object):
|
||||||
else:
|
else:
|
||||||
ignored_users = frozenset()
|
ignored_users = frozenset()
|
||||||
|
|
||||||
if sync_result_builder.since_token:
|
if since_token:
|
||||||
res = yield self._get_rooms_changed(sync_result_builder, ignored_users)
|
res = yield self._get_rooms_changed(sync_result_builder, ignored_users)
|
||||||
room_entries, invited, newly_joined_rooms = res
|
room_entries, invited, newly_joined_rooms = res
|
||||||
|
|
||||||
tags_by_room = yield self.store.get_updated_tags(
|
tags_by_room = yield self.store.get_updated_tags(
|
||||||
user_id,
|
user_id, since_token.account_data_key,
|
||||||
sync_result_builder.since_token.account_data_key,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
res = yield self._get_all_rooms(sync_result_builder, ignored_users)
|
res = yield self._get_all_rooms(sync_result_builder, ignored_users)
|
||||||
|
@ -805,7 +818,7 @@ class SyncHandler(object):
|
||||||
|
|
||||||
# Now we want to get any newly joined users
|
# Now we want to get any newly joined users
|
||||||
newly_joined_users = set()
|
newly_joined_users = set()
|
||||||
if sync_result_builder.since_token:
|
if since_token:
|
||||||
for joined_sync in sync_result_builder.joined:
|
for joined_sync in sync_result_builder.joined:
|
||||||
it = itertools.chain(
|
it = itertools.chain(
|
||||||
joined_sync.timeline.events, joined_sync.state.values()
|
joined_sync.timeline.events, joined_sync.state.values()
|
||||||
|
@ -817,6 +830,38 @@ class SyncHandler(object):
|
||||||
|
|
||||||
defer.returnValue((newly_joined_rooms, newly_joined_users))
|
defer.returnValue((newly_joined_rooms, newly_joined_users))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _have_rooms_changed(self, sync_result_builder):
|
||||||
|
"""Returns whether there may be any new events that should be sent down
|
||||||
|
the sync. Returns True if there are.
|
||||||
|
"""
|
||||||
|
user_id = sync_result_builder.sync_config.user.to_string()
|
||||||
|
since_token = sync_result_builder.since_token
|
||||||
|
now_token = sync_result_builder.now_token
|
||||||
|
|
||||||
|
assert since_token
|
||||||
|
|
||||||
|
# Get a list of membership change events that have happened.
|
||||||
|
rooms_changed = yield self.store.get_membership_changes_for_user(
|
||||||
|
user_id, since_token.room_key, now_token.room_key
|
||||||
|
)
|
||||||
|
|
||||||
|
if rooms_changed:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
app_service = self.store.get_app_service_by_user_id(user_id)
|
||||||
|
if app_service:
|
||||||
|
rooms = yield self.store.get_app_service_rooms(app_service)
|
||||||
|
joined_room_ids = set(r.room_id for r in rooms)
|
||||||
|
else:
|
||||||
|
joined_room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
|
|
||||||
|
stream_id = RoomStreamToken.parse_stream_token(since_token.room_key).stream
|
||||||
|
for room_id in joined_room_ids:
|
||||||
|
if self.store.has_room_changed_since(room_id, stream_id):
|
||||||
|
defer.returnValue(True)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_rooms_changed(self, sync_result_builder, ignored_users):
|
def _get_rooms_changed(self, sync_result_builder, ignored_users):
|
||||||
"""Gets the the changes that have happened since the last sync.
|
"""Gets the the changes that have happened since the last sync.
|
||||||
|
@ -841,8 +886,7 @@ class SyncHandler(object):
|
||||||
rooms = yield self.store.get_app_service_rooms(app_service)
|
rooms = yield self.store.get_app_service_rooms(app_service)
|
||||||
joined_room_ids = set(r.room_id for r in rooms)
|
joined_room_ids = set(r.room_id for r in rooms)
|
||||||
else:
|
else:
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
joined_room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
joined_room_ids = set(r.room_id for r in rooms)
|
|
||||||
|
|
||||||
# Get a list of membership change events that have happened.
|
# Get a list of membership change events that have happened.
|
||||||
rooms_changed = yield self.store.get_membership_changes_for_user(
|
rooms_changed = yield self.store.get_membership_changes_for_user(
|
||||||
|
|
|
@ -304,8 +304,7 @@ class Notifier(object):
|
||||||
if user_stream is None:
|
if user_stream is None:
|
||||||
current_token = yield self.event_sources.get_current_token()
|
current_token = yield self.event_sources.get_current_token()
|
||||||
if room_ids is None:
|
if room_ids is None:
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
room_ids = yield self.store.get_rooms_for_user(user_id)
|
||||||
room_ids = [room.room_id for room in rooms]
|
|
||||||
user_stream = _NotifierUserStream(
|
user_stream = _NotifierUserStream(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
rooms=room_ids,
|
rooms=room_ids,
|
||||||
|
@ -454,8 +453,7 @@ class Notifier(object):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_room_ids(self, user, explicit_room_id):
|
def _get_room_ids(self, user, explicit_room_id):
|
||||||
joined_rooms = yield self.store.get_rooms_for_user(user.to_string())
|
joined_room_ids = yield self.store.get_rooms_for_user(user.to_string())
|
||||||
joined_room_ids = map(lambda r: r.room_id, joined_rooms)
|
|
||||||
if explicit_room_id:
|
if explicit_room_id:
|
||||||
if explicit_room_id in joined_room_ids:
|
if explicit_room_id in joined_room_ids:
|
||||||
defer.returnValue(([explicit_room_id], True))
|
defer.returnValue(([explicit_room_id], True))
|
||||||
|
|
|
@ -33,13 +33,13 @@ def get_badge_count(store, user_id):
|
||||||
|
|
||||||
badge = len(invites)
|
badge = len(invites)
|
||||||
|
|
||||||
for r in joins:
|
for room_id in joins:
|
||||||
if r.room_id in my_receipts_by_room:
|
if room_id in my_receipts_by_room:
|
||||||
last_unread_event_id = my_receipts_by_room[r.room_id]
|
last_unread_event_id = my_receipts_by_room[room_id]
|
||||||
|
|
||||||
notifs = yield (
|
notifs = yield (
|
||||||
store.get_unread_event_push_actions_by_room_for_user(
|
store.get_unread_event_push_actions_by_room_for_user(
|
||||||
r.room_id, user_id, last_unread_event_id
|
room_id, user_id, last_unread_event_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# return one badge count per conversation, as count per
|
# return one badge count per conversation, as count per
|
||||||
|
|
|
@ -749,8 +749,7 @@ class JoinedRoomsRestServlet(ClientV1RestServlet):
|
||||||
def on_GET(self, request):
|
def on_GET(self, request):
|
||||||
requester = yield self.auth.get_user_by_req(request, allow_guest=True)
|
requester = yield self.auth.get_user_by_req(request, allow_guest=True)
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(requester.user.to_string())
|
room_ids = yield self.store.get_rooms_for_user(requester.user.to_string())
|
||||||
room_ids = set(r.room_id for r in rooms) # Ensure they're unique.
|
|
||||||
defer.returnValue((200, {"joined_rooms": list(room_ids)}))
|
defer.returnValue((200, {"joined_rooms": list(room_ids)}))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -274,24 +274,27 @@ class RoomMemberStore(SQLBaseStore):
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@cached(max_entries=500000, iterable=True)
|
@cachedInlineCallbacks(max_entries=500000, iterable=True)
|
||||||
def get_rooms_for_user(self, user_id):
|
def get_rooms_for_user(self, user_id):
|
||||||
return self.get_rooms_for_user_where_membership_is(
|
"""Returns a set of room_ids the user is currently joined to
|
||||||
|
"""
|
||||||
|
rooms = yield self.get_rooms_for_user_where_membership_is(
|
||||||
user_id, membership_list=[Membership.JOIN],
|
user_id, membership_list=[Membership.JOIN],
|
||||||
)
|
)
|
||||||
|
defer.returnValue(frozenset(r.room_id for r in rooms))
|
||||||
|
|
||||||
@cachedInlineCallbacks(max_entries=500000, cache_context=True, iterable=True)
|
@cachedInlineCallbacks(max_entries=500000, cache_context=True, iterable=True)
|
||||||
def get_users_who_share_room_with_user(self, user_id, cache_context):
|
def get_users_who_share_room_with_user(self, user_id, cache_context):
|
||||||
"""Returns the set of users who share a room with `user_id`
|
"""Returns the set of users who share a room with `user_id`
|
||||||
"""
|
"""
|
||||||
rooms = yield self.get_rooms_for_user(
|
room_ids = yield self.get_rooms_for_user(
|
||||||
user_id, on_invalidate=cache_context.invalidate,
|
user_id, on_invalidate=cache_context.invalidate,
|
||||||
)
|
)
|
||||||
|
|
||||||
user_who_share_room = set()
|
user_who_share_room = set()
|
||||||
for room in rooms:
|
for room_id in room_ids:
|
||||||
user_ids = yield self.get_users_in_room(
|
user_ids = yield self.get_users_in_room(
|
||||||
room.room_id, on_invalidate=cache_context.invalidate,
|
room_id, on_invalidate=cache_context.invalidate,
|
||||||
)
|
)
|
||||||
user_who_share_room.update(user_ids)
|
user_who_share_room.update(user_ids)
|
||||||
|
|
||||||
|
|
|
@ -324,7 +324,7 @@ class PresenceTimeoutTestCase(unittest.TestCase):
|
||||||
state = UserPresenceState.default(user_id)
|
state = UserPresenceState.default(user_id)
|
||||||
state = state.copy_and_replace(
|
state = state.copy_and_replace(
|
||||||
state=PresenceState.ONLINE,
|
state=PresenceState.ONLINE,
|
||||||
last_active_ts=now,
|
last_active_ts=0,
|
||||||
last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1,
|
last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue